diff --git a/packages/kap-server/src/routes/prompts.ts b/packages/kap-server/src/routes/prompts.ts index 111b5bf59..8bd2a435d 100644 --- a/packages/kap-server/src/routes/prompts.ts +++ b/packages/kap-server/src/routes/prompts.ts @@ -6,6 +6,7 @@ */ import { + IAgentLifecycleService, IAgentPromptLegacyService, ISessionLifecycleService, isKimiError, @@ -25,7 +26,7 @@ import { z } from 'zod'; import { errEnvelope, okEnvelope } from '../envelope'; import { defineRoute } from '../middleware/defineRoute'; -import { ensureMainAgent } from '../transport/mainAgent'; +import { ensureMainAgent, MAIN_AGENT_ID } from '../transport/mainAgent'; import { parseActionSuffix } from './action-suffix'; interface PromptRouteHost { @@ -55,7 +56,11 @@ const validationDetailsSchema = z.array(z.object({ path: z.string(), message: z. const authProviderDetailsSchema = z.object({ provider_id: z.string() }); const authModelDetailsSchema = z.object({ model_id: z.string(), provider_id: z.string() }).partial(); -async function resolveLegacy(core: Scope, sessionId: string): Promise { +async function resolveLegacy( + core: Scope, + sessionId: string, + agentId?: string, +): Promise { // `resume` (not `get`) so a persisted-but-cold session — created by a previous // process, by v1, or closed in this one — is loaded from disk instead of // being reported as `session.not_found`. Mirrors the snapshot route. Returns @@ -64,7 +69,17 @@ async function resolveLegacy(core: Scope, sessionId: string): Promise { try { const { session_id } = req.params; - const legacy = await resolveLegacy(core, session_id); + const legacy = await resolveLegacy(core, session_id, req.body.agent_id); const result = await legacy.submit(req.body); reply.send(okEnvelope(result, req.id)); } catch (error) { diff --git a/packages/kap-server/test/prompts.test.ts b/packages/kap-server/test/prompts.test.ts index 0a6eb1af4..2a53f5830 100644 --- a/packages/kap-server/test/prompts.test.ts +++ b/packages/kap-server/test/prompts.test.ts @@ -3,6 +3,7 @@ import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { + IAgentContextMemoryService, IAgentLifecycleService, ISessionLifecycleService, } from '@moonshot-ai/agent-core-v2'; @@ -179,4 +180,57 @@ describe('server-v2 /api/v1 prompts', () => { expect(list.body.data.active).toBeNull(); expect(list.body.data.queued).toEqual([]); }); + + it('routes a submitted prompt to the agent named by agent_id (BTW side channel)', async () => { + const id = await createSession(home as string); + await createMainAgent(id); + + // Fork the main agent into a side-channel child the way `/btw` does. + const session = server!.core.accessor.get(ISessionLifecycleService).get(id); + if (session === undefined) throw new Error(`session ${id} not found`); + const lifecycle = session.accessor.get(IAgentLifecycleService); + const child = await lifecycle.fork('main'); + + const submitted = await call('POST', `/api/v1/sessions/${id}/prompts`, { + content: [{ type: 'text', text: 'side question' }], + agent_id: child.id, + }); + expect(submitted.body.code).toBe(0); + + // The user message is appended to the target agent's context before the turn + // runs, so it persists even after the (model-less) turn settles — a durable + // signal of which agent actually received the prompt. + const contextHasUserText = ( + handle: { accessor: { get: typeof child.accessor.get } }, + text: string, + ): boolean => + handle.accessor + .get(IAgentContextMemoryService) + .get() + .some( + (m) => + m.role === 'user' && + m.content.some((p) => p.type === 'text' && p.text === text), + ); + + // The side-channel child received the prompt. + expect(contextHasUserText(child, 'side question')).toBe(true); + + // The main agent must NOT have received it — previously the route ignored + // agent_id and always targeted main, so the reply landed in the main view. + const main = lifecycle.getHandle('main'); + expect(main).toBeDefined(); + expect(contextHasUserText(main!, 'side question')).toBe(false); + }); + + it('returns 40401 when agent_id names an unknown agent', async () => { + const id = await createSession(home as string); + await createMainAgent(id); + + const { body } = await call('POST', `/api/v1/sessions/${id}/prompts`, { + content: [{ type: 'text', text: 'hello' }], + agent_id: 'agent_does_not_exist', + }); + expect(body.code).toBe(40401); + }); });