diff --git a/packages/server/src/server/agent/agent-prompt.test.ts b/packages/server/src/server/agent/agent-prompt.test.ts index 84632dda1..a88a8b9a3 100644 --- a/packages/server/src/server/agent/agent-prompt.test.ts +++ b/packages/server/src/server/agent/agent-prompt.test.ts @@ -11,6 +11,97 @@ import { } from "./agent-prompt.js"; import type { AgentManagerEvent, ManagedAgent } from "./agent-manager.js"; +interface FinishNotificationScenarioOptions { + childLastAssistantMessage?: string | null; +} + +interface FinishNotificationScenario { + startWatchingChild(): void; + finishChildAndReadParentPrompt(): Promise; +} + +function createFinishNotificationScenario( + options?: FinishNotificationScenarioOptions, +): FinishNotificationScenario { + let subscriber: ((event: AgentManagerEvent) => void) | null = null; + let resolveParentPrompt: ((prompt: string) => void) | null = null; + + const childAgent: ManagedAgent = Object.create(null); + Reflect.set(childAgent, "id", "child-agent"); + Reflect.set(childAgent, "lifecycle", "idle"); + Reflect.set(childAgent, "config", { title: "Child Agent" }); + + const callerAgent: ManagedAgent = Object.create(null); + Reflect.set(callerAgent, "id", "caller-agent"); + Reflect.set(callerAgent, "lifecycle", "idle"); + Reflect.set(callerAgent, "config", { title: "Caller Agent" }); + + const agentManager: AgentManager = Object.create(AgentManager.prototype); + Reflect.set(agentManager, "getAgent", (agentId: string) => { + if (agentId === "child-agent") { + return childAgent; + } + if (agentId === "caller-agent") { + return callerAgent; + } + return null; + }); + Reflect.set(agentManager, "subscribe", (callback: (event: AgentManagerEvent) => void) => { + subscriber = callback; + return () => { + subscriber = null; + }; + }); + Reflect.set(agentManager, "getLastAssistantMessage", async () => { + return options?.childLastAssistantMessage ?? null; + }); + Reflect.set(agentManager, "tryRunOutOfBand", () => false); + Reflect.set(agentManager, "hasInFlightRun", () => false); + Reflect.set(agentManager, "streamAgent", (_agentId: string, prompt: string) => { + resolveParentPrompt?.(prompt); + return (async function* noop() {})(); + }); + + const agentStorage: AgentStorage = Object.create(AgentStorage.prototype); + Reflect.set(agentStorage, "get", async (agentId: string) => { + if (agentId === "child-agent") { + return { title: "Child Agent" }; + } + return null; + }); + + return { + startWatchingChild() { + setupFinishNotification({ + agentManager, + agentStorage, + childAgentId: "child-agent", + callerAgentId: "caller-agent", + logger: createTestLogger(), + }); + }, + async finishChildAndReadParentPrompt() { + const parentPrompt = new Promise((resolve) => { + resolveParentPrompt = resolve; + }); + + childAgent.lifecycle = "running"; + subscriber?.({ + type: "agent_state", + agent: childAgent, + }); + + childAgent.lifecycle = "idle"; + subscriber?.({ + type: "agent_state", + agent: childAgent, + }); + + return parentPrompt; + }, + }; +} + test("isSystemInjectedEnvelope matches the envelope formatSystemNotificationPrompt produces", () => { expect(isSystemInjectedEnvelope(formatSystemNotificationPrompt("child finished"))).toBe(true); expect(isSystemInjectedEnvelope("hello world")).toBe(false); @@ -55,6 +146,21 @@ test("sendPromptToAgent forwards the client message id as run options", async () }); }); +test("finish notifications tell the parent the child's last assistant message", async () => { + const scenario = createFinishNotificationScenario({ + childLastAssistantMessage: "Implemented the cleanup and all checks pass.", + }); + + scenario.startWatchingChild(); + const parentPrompt = await scenario.finishChildAndReadParentPrompt(); + + expect(parentPrompt).toEqual( + formatSystemNotificationPrompt( + "Agent child-agent (Child Agent) finished.\n\n\nImplemented the cleanup and all checks pass.\n", + ), + ); +}); + it("does not notify archived callers", async () => { let subscriber: ((event: AgentManagerEvent) => void) | null = null; diff --git a/packages/server/src/server/agent/agent-prompt.ts b/packages/server/src/server/agent/agent-prompt.ts index 6331a9d7c..e7c7d9973 100644 --- a/packages/server/src/server/agent/agent-prompt.ts +++ b/packages/server/src/server/agent/agent-prompt.ts @@ -252,6 +252,22 @@ export interface SetupFinishNotificationParams { logger: Logger; } +interface FinishNotificationBodyInput { + childAgentId: string; + title: string; + reason: "finished" | "errored" | "needs permission"; + lastAssistantMessage: string | null; +} + +function formatFinishNotificationBody(params: FinishNotificationBodyInput): string { + const statusLine = `Agent ${params.childAgentId} (${params.title}) ${params.reason}.`; + const lastAssistantMessage = params.lastAssistantMessage?.trim(); + if (!lastAssistantMessage) { + return statusLine; + } + return `${statusLine}\n\n\n${lastAssistantMessage}\n`; +} + export function setupFinishNotification(params: SetupFinishNotificationParams): void { const { agentManager, agentStorage, childAgentId, callerAgentId, logger } = params; let hasSeenRunning = false; @@ -265,9 +281,20 @@ export function setupFinishNotification(params: SetupFinishNotificationParams): fired = true; unsubscribe?.(); + const callerRecord = await agentStorage.get(callerAgentId); + if (callerRecord?.archivedAt) { + return; + } + const record = await agentStorage.get(childAgentId); const title = record?.title ?? childAgentId; - const body = `Agent ${childAgentId} (${title}) ${reason}.`; + const lastAssistantMessage = await agentManager.getLastAssistantMessage(childAgentId); + const body = formatFinishNotificationBody({ + childAgentId, + title, + reason, + lastAssistantMessage, + }); await sendPromptToAgent({ agentManager,