mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-08-22 15:16:07 +00:00
fix(kap-server): route prompt submits by agent_id
`resolveLegacy` always resolved the main agent, ignoring `agent_id` in the prompt submission body. A `/btw <question>` submit carries the forked side-channel child's `agent_id`, so it was answered in the main view instead of the right-side BTW panel. Resolve the agent named by `agent_id` (falling back to `main`), and return 40401 for an unknown agent id. Add regression tests for side-channel routing and the unknown-agent case.
This commit is contained in:
parent
7b041a8e03
commit
e6b90d95f5
2 changed files with 73 additions and 4 deletions
|
|
@ -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<IAgentPromptLegacyService> {
|
||||
async function resolveLegacy(
|
||||
core: Scope,
|
||||
sessionId: string,
|
||||
agentId?: string,
|
||||
): Promise<IAgentPromptLegacyService> {
|
||||
// `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<IAgentProm
|
|||
if (session === undefined) {
|
||||
throw new KimiError('session.not_found', `session ${sessionId} does not exist`);
|
||||
}
|
||||
const agent = await ensureMainAgent(session);
|
||||
// A prompt may target a forked side-channel agent (e.g. `/btw`) via
|
||||
// `body.agent_id`. Default to `main` when absent; only `main` is
|
||||
// auto-created — any other id must already exist (forked beforehand), or it
|
||||
// is reported as `agent.not_found`.
|
||||
const agent =
|
||||
agentId === undefined || agentId === MAIN_AGENT_ID
|
||||
? await ensureMainAgent(session)
|
||||
: session.accessor.get(IAgentLifecycleService).getHandle(agentId);
|
||||
if (agent === undefined) {
|
||||
throw new KimiError('agent.not_found', `agent ${agentId} does not exist`);
|
||||
}
|
||||
return agent.accessor.get(IAgentPromptLegacyService);
|
||||
}
|
||||
|
||||
|
|
@ -116,7 +131,7 @@ export function registerPromptsRoutes(app: PromptRouteHost, core: Scope): void {
|
|||
async (req, reply) => {
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -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<PromptItemWire>('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<null>('POST', `/api/v1/sessions/${id}/prompts`, {
|
||||
content: [{ type: 'text', text: 'hello' }],
|
||||
agent_id: 'agent_does_not_exist',
|
||||
});
|
||||
expect(body.code).toBe(40401);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue