diff --git a/packages/server/src/server/agent/mcp-server.test.ts b/packages/server/src/server/agent/mcp-server.test.ts index e695c84048..dbe0885d5c 100644 --- a/packages/server/src/server/agent/mcp-server.test.ts +++ b/packages/server/src/server/agent/mcp-server.test.ts @@ -3704,18 +3704,29 @@ class HeldTurnAgentSession implements AgentSession { readonly prompts: string[] = []; private readonly subscribers = new Set<(event: AgentStreamEvent) => void>(); private activeTurnId: string | null = null; + private turnStartGate: Promise | null = null; constructor( readonly provider: AgentProvider, private readonly holdTurns: boolean, ) {} + /** Hold the next turn's acknowledgment, as a provider awaiting its turn-start request does. */ + holdTurnStart(): () => void { + let release!: () => void; + this.turnStartGate = new Promise((resolve) => { + release = resolve; + }); + return release; + } + async run(): Promise { return { sessionId: this.id, finalText: "", timeline: [] }; } async startTurn(prompt: AgentPromptInput): Promise<{ turnId: string }> { this.prompts.push(typeof prompt === "string" ? prompt : JSON.stringify(prompt)); + await this.turnStartGate; const turnId = randomUUID(); this.activeTurnId = turnId; setTimeout(() => { @@ -4085,6 +4096,127 @@ describe("send_agent_prompt MCP tool", () => { await removeAgentStateDir(agentManager, storage, workdir); } }); + + it("reports a background prompt's accepted turn as running", async () => { + const workdir = await mkdtemp(join(tmpdir(), "mcp-background-send-status-")); + const storage = new AgentStorage(join(workdir, "agents"), logger); + const parentClient = new HeldTurnAgentClient("claude", false); + const childClient = new HeldTurnAgentClient("codex", true); + const agentManager = new AgentManager({ + clients: { claude: parentClient, codex: childClient }, + registry: storage, + logger, + }); + + try { + const parent = await agentManager.createAgent( + { provider: "claude", cwd: existingCwd }, + undefined, + { workspaceId: "wks_parent" }, + ); + const child = await agentManager.createAgent( + { provider: "codex", cwd: existingCwd }, + undefined, + { workspaceId: "wks_parent" }, + ); + const server = await createAgentMcpServer({ + agentManager, + agentStorage: storage, + callerAgentId: parent.id, + providerSnapshotManager: createOpenCodeManager().manager, + logger, + }); + const tool = registeredTool(server, "send_agent_prompt"); + const childSession = childClient.sessions[0]!; + const acknowledgeTurnStart = childSession.holdTurnStart(); + + const pending = invokeToolWithParsedInput(tool, { + agentId: child.id, + prompt: "Follow up", + }); + await vi.waitFor(() => expect(childSession.prompts).toEqual(["Follow up"])); + acknowledgeTurnStart(); + const response = await pending; + + expect(response.structuredContent).toMatchObject({ success: true, status: "running" }); + expect(agentManager.getAgent(child.id)?.lifecycle).toBe("running"); + + childSession.finishTurn(); + await vi.waitFor(() => { + const parentPrompts = parentClient.sessions[0]!.prompts; + expect(parentPrompts).toHaveLength(1); + expect(parentPrompts[0]).toContain(child.id); + expect(parentPrompts[0]).toContain("finished"); + }); + } finally { + await removeAgentStateDir(agentManager, storage, workdir); + } + }); + + it("accepts a background turn completed as the manager publishes its start", async () => { + const workdir = await mkdtemp(join(tmpdir(), "mcp-fast-background-send-")); + const storage = new AgentStorage(join(workdir, "agents"), logger); + const parentClient = new HeldTurnAgentClient("claude", false); + const childClient = new HeldTurnAgentClient("codex", true); + const agentManager = new AgentManager({ + clients: { claude: parentClient, codex: childClient }, + registry: storage, + logger, + }); + + try { + const parent = await agentManager.createAgent( + { provider: "claude", cwd: existingCwd }, + undefined, + { workspaceId: "wks_parent" }, + ); + const child = await agentManager.createAgent( + { provider: "codex", cwd: existingCwd }, + undefined, + { workspaceId: "wks_parent" }, + ); + const server = await createAgentMcpServer({ + agentManager, + agentStorage: storage, + callerAgentId: parent.id, + providerSnapshotManager: createOpenCodeManager().manager, + logger, + }); + let finished = false; + const unsubscribe = agentManager.subscribe( + (event) => { + if ( + !finished && + event.type === "agent_state" && + event.agent.id === child.id && + event.agent.lifecycle === "running" + ) { + finished = true; + childClient.sessions[0]!.finishTurn(); + } + }, + { agentId: child.id, replayState: false }, + ); + + const response = await invokeToolWithParsedInput( + registeredTool(server, "send_agent_prompt"), + { + agentId: child.id, + prompt: "Fast reply", + }, + ); + unsubscribe(); + expect(response.structuredContent).toMatchObject({ success: true, status: "idle" }); + await vi.waitFor(() => { + const parentPrompts = parentClient.sessions[0]!.prompts; + expect(parentPrompts).toHaveLength(1); + expect(parentPrompts[0]).toContain(child.id); + expect(parentPrompts[0]).toContain("finished"); + }); + } finally { + await removeAgentStateDir(agentManager, storage, workdir); + } + }); }); describe("update_agent MCP tool", () => { diff --git a/packages/server/src/server/agent/tools/paseo-tools.ts b/packages/server/src/server/agent/tools/paseo-tools.ts index 6ad1c60574..07f9f21098 100644 --- a/packages/server/src/server/agent/tools/paseo-tools.ts +++ b/packages/server/src/server/agent/tools/paseo-tools.ts @@ -61,7 +61,11 @@ import { toScheduleSummary, waitForAgentWithTimeout, } from "../mcp-shared.js"; -import { sendPromptToAgent, setupFinishNotification } from "../agent-prompt.js"; +import { + sendPromptToAgent, + setupFinishNotification, + waitForAgentRunStartWithTimeout, +} from "../agent-prompt.js"; import { respondToAgentPermission } from "../permission-response.js"; import { archiveAgentCommand, @@ -1924,7 +1928,7 @@ export function createPaseoToolCatalog(options: PaseoToolHostDependencies): Pase return true; } - await sendPromptToAgent({ + const { disposition } = await sendPromptToAgent({ agentManager, agentStorage, agentId, @@ -1963,8 +1967,10 @@ export function createPaseoToolCatalog(options: PaseoToolHostDependencies): Pase const notifying = armFinishNotification(); - // Return immediately if background=true - // Re-fetch snapshot since the state may have changed + // Return once the provider has accepted the turn, so the status reports it running. + if (disposition === "turn_started") { + await waitForAgentRunStartWithTimeout(agentManager, agentId); + } const currentSnapshot = agentManager.getAgent(agentId); const responseData = {