mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-08-19 05:35:34 +00:00
refactor(agent-core-v2): extract SessionBtwService for btw
- add Session-scoped ISessionBtwService/SessionBtwService that forks the
main agent into a side-question child agent (tools disabled,
side-channel reminder) via IAgentLifecycleService.fork('main')
- remove startBtw from AgentAPI (core-api) and ISessionLegacyService;
the server-v2 POST /sessions/{id}/btw route now resolves
ISessionBtwService directly
- add a unit test for SessionBtwService
This commit is contained in:
parent
d4a6a20db8
commit
4571edf90a
7 changed files with 176 additions and 5 deletions
|
|
@ -402,7 +402,6 @@ export interface AgentAPI {
|
|||
clearContext: (payload: EmptyPayload) => void;
|
||||
activateSkill: (payload: ActivateSkillPayload) => void;
|
||||
activatePluginCommand: (payload: ActivatePluginCommandPayload) => void;
|
||||
startBtw: (payload: EmptyPayload) => string;
|
||||
createGoal: (payload: CreateGoalPayload) => GoalSnapshot;
|
||||
getGoal: (payload: EmptyPayload) => GoalToolResult;
|
||||
pauseGoal: (payload: EmptyPayload) => GoalSnapshot;
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ import type {
|
|||
ForkSessionRequest,
|
||||
SessionAbortResponse,
|
||||
SessionStatus,
|
||||
StartBtwSessionResponse,
|
||||
UndoSessionRequest,
|
||||
UndoSessionResponse,
|
||||
} from '@moonshot-ai/protocol';
|
||||
|
|
@ -65,7 +64,6 @@ export interface ISessionLegacyService {
|
|||
compact(sessionId: string, body: CompactSessionRequest): Promise<CompactSessionResponse>;
|
||||
undo(sessionId: string, body: UndoSessionRequest): Promise<UndoSessionResponse>;
|
||||
abort(sessionId: string): Promise<SessionAbortResponse>;
|
||||
startBtw(sessionId: string): Promise<StartBtwSessionResponse>;
|
||||
archive(sessionId: string): Promise<ArchiveSessionResponse>;
|
||||
}
|
||||
|
||||
|
|
|
|||
45
packages/agent-core-v2/src/session/btw/btw.ts
Normal file
45
packages/agent-core-v2/src/session/btw/btw.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
/**
|
||||
* `btw` domain — side-question ("by the way") child agent contract.
|
||||
*
|
||||
* A `btw` agent is a lightweight fork of the main agent used for a side-channel
|
||||
* conversation: it inherits the parent's profile and context, but all tool calls
|
||||
* are disabled and a side-channel system reminder is appended so it answers with
|
||||
* text only. Follow-up turns reuse the same child agent.
|
||||
*/
|
||||
|
||||
import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiation';
|
||||
|
||||
/** Rejection message returned by the deny-all permission policy for tool calls. */
|
||||
export const TOOL_CALL_DISABLED_MESSAGE =
|
||||
'Tool calls are disabled for side questions. Answer with text only.';
|
||||
|
||||
/**
|
||||
* System reminder appended to a `btw` child agent. Tool definitions remain
|
||||
* visible only for prompt-cache reasons; the model must not call them.
|
||||
*/
|
||||
export const SIDE_QUESTION_SYSTEM_REMINDER = `
|
||||
This is a side-channel conversation with the user. You should answer user questions directly based on what you already know.
|
||||
|
||||
IMPORTANT:
|
||||
- You are a separate, lightweight instance.
|
||||
- The main agent continues independently; do not reference being interrupted.
|
||||
- Do not call any tools. All tool calls are disabled and will be rejected.
|
||||
Even though tool definitions are visible in this request, they exist only
|
||||
for technical reasons (prompt cache). You must not use them.
|
||||
- Respond only with text based on what you already know from the conversation
|
||||
and this side-channel conversation.
|
||||
- Follow-up turns may happen in this side-channel conversation.
|
||||
- If you do not know the answer, say so directly.
|
||||
`.trim();
|
||||
|
||||
export interface ISessionBtwService {
|
||||
readonly _serviceBrand: undefined;
|
||||
/**
|
||||
* Fork the main agent into a side-question child agent (tools disabled,
|
||||
* side-channel reminder appended) and return the child's id.
|
||||
*/
|
||||
start(): Promise<string>;
|
||||
}
|
||||
|
||||
export const ISessionBtwService: ServiceIdentifier<ISessionBtwService> =
|
||||
createDecorator<ISessionBtwService>('sessionBtwService');
|
||||
51
packages/agent-core-v2/src/session/btw/btwService.ts
Normal file
51
packages/agent-core-v2/src/session/btw/btwService.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
/**
|
||||
* `btw` domain — `ISessionBtwService` implementation.
|
||||
*
|
||||
* Forks the main agent into a side-question child: inherits profile/context via
|
||||
* `IAgentLifecycleService.fork`, then disables tool calls (deny-all permission
|
||||
* policy) and appends the side-channel system reminder. Bound at Session scope —
|
||||
* `fork('main')` is a session-level operation, so the service injects the
|
||||
* session's `IAgentLifecycleService` directly rather than resolving it through
|
||||
* the main agent's accessor.
|
||||
*/
|
||||
|
||||
import { InstantiationType } from '#/_base/di/extensions';
|
||||
import { LifecycleScope, registerScopedService } from '#/_base/di/scope';
|
||||
import {
|
||||
DenyAllPermissionPolicyService,
|
||||
IAgentPermissionPolicyService,
|
||||
} from '#/agent/permissionPolicy';
|
||||
import { IAgentSystemReminderService } from '#/agent/systemReminder';
|
||||
import { IAgentLifecycleService } from '#/session/agent-lifecycle';
|
||||
|
||||
import { ISessionBtwService, SIDE_QUESTION_SYSTEM_REMINDER, TOOL_CALL_DISABLED_MESSAGE } from './btw';
|
||||
|
||||
export class SessionBtwService implements ISessionBtwService {
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
constructor(
|
||||
@IAgentLifecycleService private readonly lifecycle: IAgentLifecycleService,
|
||||
) {}
|
||||
|
||||
async start(): Promise<string> {
|
||||
const child = await this.lifecycle.fork('main');
|
||||
child.accessor
|
||||
.get(IAgentSystemReminderService)
|
||||
?.appendSystemReminder(SIDE_QUESTION_SYSTEM_REMINDER, {
|
||||
kind: 'system_trigger',
|
||||
name: 'btw',
|
||||
});
|
||||
child.accessor
|
||||
.get(IAgentPermissionPolicyService)
|
||||
?.registerPolicy(new DenyAllPermissionPolicyService(TOOL_CALL_DISABLED_MESSAGE));
|
||||
return child.id;
|
||||
}
|
||||
}
|
||||
|
||||
registerScopedService(
|
||||
LifecycleScope.Session,
|
||||
ISessionBtwService,
|
||||
SessionBtwService,
|
||||
InstantiationType.Delayed,
|
||||
'session-btw',
|
||||
);
|
||||
7
packages/agent-core-v2/src/session/btw/index.ts
Normal file
7
packages/agent-core-v2/src/session/btw/index.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
/**
|
||||
* `btw` domain barrel — re-exports the side-question child-agent contract and
|
||||
* implementation.
|
||||
*/
|
||||
|
||||
export * from './btw';
|
||||
export * from './btwService';
|
||||
60
packages/agent-core-v2/test/btw/btw.test.ts
Normal file
60
packages/agent-core-v2/test/btw/btw.test.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { SyncDescriptor } from '#/_base/di/descriptors';
|
||||
import { DisposableStore } from '#/_base/di/lifecycle';
|
||||
import { TestInstantiationService } from '#/_base/di/test';
|
||||
import {
|
||||
DenyAllPermissionPolicyService,
|
||||
IAgentPermissionPolicyService,
|
||||
} from '#/agent/permissionPolicy';
|
||||
import { IAgentSystemReminderService } from '#/agent/systemReminder';
|
||||
import { IAgentLifecycleService } from '#/session/agent-lifecycle';
|
||||
import { ISessionBtwService, SIDE_QUESTION_SYSTEM_REMINDER } from '#/session/btw/btw';
|
||||
import { SessionBtwService } from '#/session/btw/btwService';
|
||||
|
||||
describe('SessionBtwService', () => {
|
||||
let disposables: DisposableStore;
|
||||
let ix: TestInstantiationService;
|
||||
let fork: ReturnType<typeof vi.fn>;
|
||||
let appendSystemReminder: ReturnType<typeof vi.fn>;
|
||||
let registerPolicy: ReturnType<typeof vi.fn>;
|
||||
|
||||
beforeEach(() => {
|
||||
disposables = new DisposableStore();
|
||||
ix = disposables.add(new TestInstantiationService());
|
||||
appendSystemReminder = vi.fn();
|
||||
registerPolicy = vi.fn();
|
||||
|
||||
const child = {
|
||||
id: 'agent-btw-1',
|
||||
accessor: {
|
||||
get: (id: unknown) => {
|
||||
if (id === IAgentSystemReminderService) return { appendSystemReminder };
|
||||
if (id === IAgentPermissionPolicyService) return { registerPolicy };
|
||||
return undefined;
|
||||
},
|
||||
},
|
||||
};
|
||||
fork = vi.fn(async () => child);
|
||||
ix.stub(IAgentLifecycleService, {
|
||||
_serviceBrand: undefined,
|
||||
fork,
|
||||
} as unknown as IAgentLifecycleService);
|
||||
ix.set(ISessionBtwService, new SyncDescriptor(SessionBtwService));
|
||||
});
|
||||
afterEach(() => disposables.dispose());
|
||||
|
||||
it('forks main and configures a side-question child agent', async () => {
|
||||
const svc = ix.get(ISessionBtwService);
|
||||
const id = await svc.start();
|
||||
|
||||
expect(id).toBe('agent-btw-1');
|
||||
expect(fork).toHaveBeenCalledWith('main');
|
||||
expect(appendSystemReminder).toHaveBeenCalledWith(SIDE_QUESTION_SYSTEM_REMINDER, {
|
||||
kind: 'system_trigger',
|
||||
name: 'btw',
|
||||
});
|
||||
expect(registerPolicy).toHaveBeenCalledTimes(1);
|
||||
expect(registerPolicy.mock.calls[0]![0]).toBeInstanceOf(DenyAllPermissionPolicyService);
|
||||
});
|
||||
});
|
||||
|
|
@ -42,6 +42,9 @@
|
|||
*/
|
||||
|
||||
import {
|
||||
ErrorCodes,
|
||||
IAuthSummaryService,
|
||||
ISessionBtwService,
|
||||
ISessionActivity,
|
||||
ISessionContext,
|
||||
ISessionIndex,
|
||||
|
|
@ -526,8 +529,16 @@ export function registerSessionsRoutes(app: SessionRouteHost, core: Scope): void
|
|||
}
|
||||
|
||||
if (parsed.action === 'btw') {
|
||||
const result = await legacy.startBtw(parsed.id);
|
||||
reply.send(okEnvelope(result, req.id));
|
||||
const session = core.accessor.get(ISessionLifecycleService).get(parsed.id);
|
||||
if (session === undefined) {
|
||||
throw new KimiError(
|
||||
ErrorCodes.SESSION_NOT_FOUND,
|
||||
`session ${parsed.id} does not exist`,
|
||||
);
|
||||
}
|
||||
await core.accessor.get(IAuthSummaryService).ensureReady();
|
||||
const agentId = await session.accessor.get(ISessionBtwService).start();
|
||||
reply.send(okEnvelope({ agent_id: agentId }, req.id));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue