Skip to content
Merged
8 changes: 3 additions & 5 deletions apps/kimi-code/src/cli/v2/run-v2-print.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,7 @@ import {
IOAuthToolkit,
ISessionCronService,
ISessionIndex,
ISessionLifecycleService,
IWorkspaceLifecycleService,
ISessionManager,
ITelemetryService,
PRINT_MAX_TURNS_DEFAULT,
PRINT_WAIT_CEILING_S_DEFAULT,
Expand Down Expand Up @@ -262,7 +261,7 @@ async function resolveNativeSession(
defaultModel: string | undefined,
stderr: PromptOutput,
): Promise<ResolvedNativeSession> {
const workspaceLifecycle = app.accessor.get(IWorkspaceLifecycleService);
const sessions = app.accessor.get(ISessionManager);
const index = app.accessor.get(ISessionIndex);

// `--agent` selects a catalog profile by name; otherwise `--agent-file`
Expand Down Expand Up @@ -379,8 +378,7 @@ async function resolveNativeSession(
}

const model = requireConfiguredModel(opts.model, defaultModel);
const handler = await workspaceLifecycle.handlerFor({ root: workDir });
const session = await handler.accessor.get(ISessionLifecycleService).create({
const session = await sessions.create({
workDir,
additionalDirs: opts.addDirs?.length ? opts.addDirs : undefined,
mainAgentBinding: {
Expand Down
49 changes: 17 additions & 32 deletions apps/kimi-code/test/cli/v2-run-print.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@ import {
IOAuthToolkit,
ISessionCronService,
ISessionIndex,
ISessionLifecycleService,
IWorkspaceLifecycleService,
ISessionManager,
ITelemetryService,
type BootstrapInput,
type DomainEvent,
Expand Down Expand Up @@ -178,17 +177,6 @@ function makeFakeHarness() {
]);
const session = fakeScope('ses_v2', sessionServices);

const handlerServices = new Map<unknown, unknown>([
[
ISessionLifecycleService,
{
create: vi.fn(async () => session),
resume: vi.fn(async () => session),
},
],
]);
const workspace = fakeScope('wd_v2', handlerServices);

const appServices = new Map<unknown, unknown>([
[
IConfigService,
Expand All @@ -203,10 +191,13 @@ function makeFakeHarness() {
},
],
[
IWorkspaceLifecycleService,
ISessionManager,
{
handlerFor: vi.fn(async () => workspace),
},
create: vi.fn(async () => session),
resume: vi.fn(async () => session),
get: vi.fn(() => session),
list: vi.fn(() => [session]),
} as unknown as ISessionManager,
],
[
ISessionIndex,
Expand Down Expand Up @@ -255,7 +246,7 @@ function makeFakeHarness() {
],
]);
const app = fakeScope('app', appServices);
return { app, agent, session, agentServices, appServices, handlerServices, profileState };
return { app, agent, session, agentServices, appServices, profileState };
}

describe('runV2Print', () => {
Expand Down Expand Up @@ -328,7 +319,7 @@ describe('runV2Print', () => {
it('seeds explicit agent files from --agentFile and binds the --agent profile', async () => {
const stdout = writer();
const stderr = writer();
const { app, agent, appServices, agentServices, handlerServices } = makeFakeHarness();
const { app, agent, appServices, agentServices } = makeFakeHarness();

mocks.bootstrap.mockReturnValue({ app });
mocks.ensureMainAgent.mockResolvedValue(agent);
Expand All @@ -342,10 +333,8 @@ describe('runV2Print', () => {
const input = mocks.bootstrap.mock.calls[0]?.[0] as BootstrapInput;
expect(input.args?.agentFiles).toEqual(['/agents/reviewer.md']);

const lifecycle = handlerServices.get(ISessionLifecycleService) as {
create: ReturnType<typeof vi.fn>;
};
expect(lifecycle.create).toHaveBeenCalledWith({
const sessions = appServices.get(ISessionManager) as { create: ReturnType<typeof vi.fn> };
expect(sessions.create).toHaveBeenCalledWith({
workDir: process.cwd(),
additionalDirs: undefined,
mainAgentBinding: { profile: 'reviewer', model: 'k2' },
Expand All @@ -363,7 +352,7 @@ describe('runV2Print', () => {
);
const stdout = writer();
const stderr = writer();
const { app, agent, appServices, agentServices, handlerServices } = makeFakeHarness();
const { app, agent, appServices, agentServices } = makeFakeHarness();

mocks.bootstrap.mockReturnValue({ app });
mocks.ensureMainAgent.mockResolvedValue(agent);
Expand All @@ -376,10 +365,8 @@ describe('runV2Print', () => {
const input = mocks.bootstrap.mock.calls[0]?.[0] as BootstrapInput;
expect(input.args?.agentFiles).toEqual([agentFile]);

const lifecycle = handlerServices.get(ISessionLifecycleService) as {
create: ReturnType<typeof vi.fn>;
};
expect(lifecycle.create).toHaveBeenCalledWith({
const sessions = appServices.get(ISessionManager) as { create: ReturnType<typeof vi.fn> };
expect(sessions.create).toHaveBeenCalledWith({
workDir: process.cwd(),
additionalDirs: undefined,
mainAgentBinding: { profile: 'file-reviewer', model: 'k2' },
Expand All @@ -391,11 +378,9 @@ describe('runV2Print', () => {
it('does not materialize a main agent after fresh profile binding fails', async () => {
const stdout = writer();
const stderr = writer();
const { app, handlerServices } = makeFakeHarness();
const lifecycle = handlerServices.get(ISessionLifecycleService) as {
create: ReturnType<typeof vi.fn>;
};
lifecycle.create.mockRejectedValueOnce(new Error('Unknown agent profile'));
const { app, appServices } = makeFakeHarness();
const sessions = appServices.get(ISessionManager) as { create: ReturnType<typeof vi.fn> };
sessions.create.mockRejectedValueOnce(new Error('Unknown agent profile'));
mocks.bootstrap.mockReturnValue({ app });

await expect(
Expand Down
15 changes: 5 additions & 10 deletions apps/kimi-inspect/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@
* chat timeline.
*/

import { ISessionIndex } from '@moonshot-ai/agent-core-v2/app/sessionIndex/sessionIndex';
import { ISessionLifecycleService } from '@moonshot-ai/agent-core-v2/workspace/sessionLifecycle/sessionLifecycle';
import { ISessionManager } from '@moonshot-ai/agent-core-v2/app/sessionManager/sessionManager';
import { useEffect, useState } from 'react';

import type { AuditTrail } from './audit/trail';
Expand Down Expand Up @@ -61,14 +60,10 @@ export function App() {
setReady(false);
setResumeError(null);
klient
.core(ISessionIndex)
.get(sessionId)
.then((summary) => {
if (summary === undefined) throw new Error(`session ${sessionId} does not exist`);
return klient
.workspace(summary.workspaceId)
.service(ISessionLifecycleService)
.resume(sessionId);
.core(ISessionManager)
.resume(sessionId)
.then((session) => {
if (session === undefined) throw new Error(`session ${sessionId} does not exist`);
})
.then(() => {
if (!cancelled) setReady(true);
Expand Down
42 changes: 42 additions & 0 deletions apps/kimi-inspect/src/channel/channel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,13 @@ import { afterEach, describe, expect, it, vi } from 'vitest';

import type { Event, IChannel } from './channel';
import { probeDebugSurface } from './channels';
import { createInspectClient } from './client';
import { RPCError } from './errors';
import {
fetchAgentRuntimeBinding,
fetchSessionWorkspaceAssociation,
fetchWorkspaceSnapshot,
} from '../snapshots/api';
import { makeProxy } from './proxy';
import { ProxyChannel } from './proxyChannel';

Expand Down Expand Up @@ -116,6 +122,42 @@ describe('ProxyChannel.listen', () => {
});
});

describe('business snapshots', () => {
it('uses explicit workspace, session association, and agent binding routes', async () => {
const calls: string[] = [];
vi.stubGlobal('fetch', async (url: string | URL) => {
const value = String(url);
calls.push(value);
if (value.endsWith('/workspace/w%201/snapshot')) {
return { json: async () => ok({ metadata: { id: 'w 1' } }) };
}
if (value.endsWith('/session/s%201/association')) {
return { json: async () => ok({ sessionId: 's 1', workspaceId: 'w 1', cwd: '/work' }) };
}
return {
json: async () => ok({
binding: { workspaceId: 'w 1', runtimeId: 'remote' },
available: true,
runtime: { runtimeId: 'remote', generation: 'g2', status: 'ready', capabilities: ['process'] },
}),
};
});
const client = createInspectClient({ url: 'http://h:9', token: 'tok' });

await expect(fetchWorkspaceSnapshot(client, 'w 1')).resolves.toMatchObject({ metadata: { id: 'w 1' } });
await expect(fetchSessionWorkspaceAssociation(client, 's 1')).resolves.toMatchObject({ workspaceId: 'w 1' });
await expect(fetchAgentRuntimeBinding(client, 's 1', 'main')).resolves.toMatchObject({
binding: { runtimeId: 'remote' },
runtime: { generation: 'g2' },
});
expect(calls).toEqual([
'http://h:9/api/v1/debug/workspace/w%201/snapshot',
'http://h:9/api/v1/debug/session/s%201/association',
'http://h:9/api/v1/debug/session/s%201/agent/main/runtime-binding',
]);
});
});

describe('probeDebugSurface', () => {
function stubProbeFetch(impl: (url: string, init?: RequestInit) => unknown) {
const calls: { url: string; init?: RequestInit }[] = [];
Expand Down
7 changes: 1 addition & 6 deletions apps/kimi-inspect/src/channel/channels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { DEBUG_RPC_BASE, type InspectClient } from './client';
import { RPCError } from './errors';

/** Wire scope kinds reported by the channels endpoint (`app` ≡ the core route). */
export type ChannelScope = 'app' | 'workspace' | 'session' | 'agent';
export type ChannelScope = 'app' | 'session' | 'agent';

/** Mirror of `ChannelDescriptor` in kap-server (`GET /api/v1/debug/channels`). */
export interface ChannelDescriptor {
Expand Down Expand Up @@ -94,7 +94,6 @@ export async function probeDebugSurface(options: {

export interface ServiceTarget {
readonly scope: ChannelScope;
readonly workspaceId?: string;
readonly sessionId?: string;
readonly agentId?: string;
}
Expand All @@ -113,10 +112,6 @@ export function serviceByName<T extends object>(
): ServiceProxy<T> | undefined {
const id = createDecorator<T>(name);
if (target.scope === 'app') return client.core(id);
if (target.scope === 'workspace') {
if (target.workspaceId === undefined) return undefined;
return client.workspace(target.workspaceId).service(id);
}
if (target.sessionId === undefined) return undefined;
const base = client.session(target.sessionId);
if (target.scope === 'session') return base.service(id);
Expand Down
4 changes: 0 additions & 4 deletions apps/kimi-inspect/src/channel/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ export interface InspectClient {
/** Bearer token in use, when any. */
readonly token?: string;
core<T extends object>(id: ServiceRef<T>): ServiceProxy<T>;
workspace(workspaceId: string): InspectAgentHandle;
session(sessionId: string): InspectSessionHandle;
}

Expand Down Expand Up @@ -69,9 +68,6 @@ export function createInspectClient(options: InspectClientOptions): InspectClien
baseUrl: url,
token: options.token,
core: (id) => proxy('', id),
workspace: (workspaceId) => ({
service: (id) => proxy(`/workspace/${encodeURIComponent(workspaceId)}`, id),
}),
session: (sessionId) => {
const scopePath = `/session/${encodeURIComponent(sessionId)}`;
return {
Expand Down
30 changes: 30 additions & 0 deletions apps/kimi-inspect/src/components/Inspector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { useEffect, useMemo, useState } from 'react';
import { serviceByName } from '../channel';
import { useConnection } from '../connection';
import { type AnyService } from '../panels';
import { fetchAgentRuntimeBinding } from '../snapshots/api';
import { fetchTranscriptPlan, type TranscriptPlanInfo } from '../transcript/api';
import { ActionButton, Badge, ErrorLine } from '../ui';
import { ScopePanels } from './ServicePanels';
Expand Down Expand Up @@ -57,6 +58,12 @@ export function Inspector({

// Keep the selected agent valid as the registry changes.
const effectiveAgent = agentIds.includes(agentId) ? agentId : agentIds[0]!;
const runtimeBinding = useQuery({
queryKey: ['agent-runtime-binding', klient.baseUrl, sessionId, effectiveAgent],
queryFn: () => fetchAgentRuntimeBinding(klient, sessionId as string, effectiveAgent),
enabled: sessionId !== null && ready,
refetchInterval: 1_000,
});
useEffect(() => {
if (effectiveAgent !== agentId) onAgentChange(effectiveAgent);
}, [effectiveAgent, agentId, onAgentChange]);
Expand Down Expand Up @@ -131,6 +138,29 @@ export function Inspector({
</div>
) : (
<>
<div className="mb-3 rounded border border-neutral-800 bg-neutral-950/40 p-2 text-[11px]">
<div className="mb-1 flex items-center gap-2 font-semibold uppercase tracking-wider text-neutral-500">
Runtime binding
{runtimeBinding.data !== undefined ? (
<Badge tone={runtimeBinding.data.available ? 'green' : 'red'}>
{runtimeBinding.data.available ? 'available' : 'unavailable'}
</Badge>
) : null}
</div>
<div className="grid grid-cols-[80px_minmax(0,1fr)] gap-1 font-mono">
<span className="text-neutral-600">workspace</span>
<span className="break-all text-neutral-300">{runtimeBinding.data?.binding.workspaceId ?? 'loading…'}</span>
<span className="text-neutral-600">runtime</span>
<span className="break-all text-neutral-300">{runtimeBinding.data?.binding.runtimeId ?? 'loading…'}</span>
<span className="text-neutral-600">generation</span>
<span className="break-all text-neutral-300">{runtimeBinding.data?.runtime?.generation ?? 'unavailable'}</span>
<span className="text-neutral-600">status</span>
<span className="text-neutral-300">{runtimeBinding.data?.runtime?.status ?? 'unavailable'}</span>
<span className="text-neutral-600">capabilities</span>
<span className="text-neutral-300">{runtimeBinding.data?.runtime?.capabilities.join(', ') ?? 'none'}</span>
</div>
{runtimeBinding.isError ? <ErrorLine error={runtimeBinding.error} /> : null}
</div>
<PlanCard sessionId={sessionId} agentId={effectiveAgent} />
<ScopePanels
scope="agent"
Expand Down
11 changes: 2 additions & 9 deletions apps/kimi-inspect/src/components/ModelCatalogView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@
*/

import { IAgentProfileService } from '@moonshot-ai/agent-core-v2/agent/profile/profile';
import { ISessionIndex } from '@moonshot-ai/agent-core-v2/app/sessionIndex/sessionIndex';
import { ISessionLifecycleService } from '@moonshot-ai/agent-core-v2/workspace/sessionLifecycle/sessionLifecycle';
import { ISessionManager } from '@moonshot-ai/agent-core-v2/app/sessionManager/sessionManager';
import type { InspectionSource } from '@moonshot-ai/agent-core-v2/kosong/contract/inspection';
import type { TokenUsage } from '@moonshot-ai/agent-core-v2/kosong/contract/usage';
import {
Expand Down Expand Up @@ -406,13 +405,7 @@ function ModelSection({
const envelope = (await res.json()) as { code: number; msg: string; data: { id: string } };
if (envelope.code !== 0) throw new Error(envelope.msg);
const sessionId = envelope.data.id;
const summary = await klient.core(ISessionIndex).get(sessionId);
if (summary !== undefined) {
await klient
.workspace(summary.workspaceId)
.service(ISessionLifecycleService)
.resume(sessionId);
}
await klient.core(ISessionManager).resume(sessionId);
await klient
.session(sessionId)
.agent('main')
Expand Down
2 changes: 1 addition & 1 deletion apps/kimi-inspect/src/components/NavRail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ const VIEWS: readonly ViewDef[] = [
},
{
id: 'workspace',
title: 'Workspace Services',
title: 'Workspace Runtime',
icon: (
<svg {...iconProps}>
<path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z" />
Expand Down
Loading
Loading