diff --git a/packages/server/src/server/daemon.e2e.test.ts b/packages/server/src/server/daemon.e2e.test.ts index adaf94bb5..c19419df4 100644 --- a/packages/server/src/server/daemon.e2e.test.ts +++ b/packages/server/src/server/daemon.e2e.test.ts @@ -366,6 +366,119 @@ describe("daemon E2E", () => { ); }); + describe("multi-agent orchestration", () => { + test( + "parent agent creates child agent via agent-control MCP", + async () => { + const cwd = tmpCwd(); + const childCwd = tmpCwd(); + + // Create parent Codex agent + const parent = await ctx.client.createAgent({ + provider: "codex", + cwd, + title: "Parent Agent", + }); + + expect(parent.id).toBeTruthy(); + expect(parent.status).toBe("idle"); + + // Clear message queue before sending prompt + ctx.client.clearMessageQueue(); + + // Prompt the parent to create a child agent using agent-control MCP + const prompt = [ + `Use the create_agent tool from the agent-control MCP server to create a new codex agent.`, + `Set the cwd to: ${childCwd}`, + `Set the title to: Child Agent`, + `Set agentType to: codex`, + `Do NOT set an initialPrompt - just create the agent.`, + `After creating the agent, reply with "CREATED" followed by the child's agentId.`, + ].join(" "); + + await ctx.client.sendMessage(parent.id, prompt); + + // Wait for parent to complete + const afterCreate = await ctx.client.waitForAgentIdle( + parent.id, + 120000 + ); + expect(afterCreate.status).toBe("idle"); + + // Verify timeline contains a tool call to create_agent + const queue = ctx.client.getMessageQueue(); + const timelineItems: AgentTimelineItem[] = []; + for (const m of queue) { + if ( + m.type === "agent_stream" && + m.payload.agentId === parent.id && + m.payload.event.type === "timeline" + ) { + timelineItems.push(m.payload.event.item); + } + } + + // Should have a tool call to create_agent from agent-control + const hasCreateAgentCall = timelineItems.some( + (item) => + item.type === "tool_call" && + item.tool === "create_agent" && + item.server === "agent-control" + ); + expect(hasCreateAgentCall).toBe(true); + + // Now verify we can see both agents via session_state + // Send a list_persisted_agents_request to trigger session_state refresh + // Or we can check the queue for agent_state messages + const agentStateMessages = queue.filter( + (m) => m.type === "agent_state" + ); + + // Extract unique agent IDs from state messages + const agentIds = new Set(); + for (const m of agentStateMessages) { + if (m.type === "agent_state") { + agentIds.add(m.payload.id); + } + } + + // Should have at least 2 agents (parent + child) + expect(agentIds.size).toBeGreaterThanOrEqual(2); + expect(agentIds.has(parent.id)).toBe(true); + + // Get the child agent ID from the tool call output + const createAgentCall = timelineItems.find( + (item) => + item.type === "tool_call" && + item.tool === "create_agent" && + item.server === "agent-control" + ); + + let childAgentId: string | null = null; + if ( + createAgentCall && + createAgentCall.type === "tool_call" && + createAgentCall.output + ) { + // The output contains the agentId + const output = createAgentCall.output as { agentId?: string }; + if (output.agentId) { + childAgentId = output.agentId; + } + } + + // Verify we found the child agent ID + expect(childAgentId).toBeTruthy(); + expect(agentIds.has(childAgentId!)).toBe(true); + + // Cleanup + rmSync(cwd, { recursive: true, force: true }); + rmSync(childCwd, { recursive: true, force: true }); + }, + 300000 // 5 minute timeout for multi-agent E2E + ); + }); + // Claude permission tests are skipped due to SDK behavior: // - The sandbox config IS passed correctly to Claude SDK // - Claude executes tool calls without requesting permission diff --git a/plan.md b/plan.md index 2053eef06..b4bfa7d0a 100644 --- a/plan.md +++ b/plan.md @@ -1032,7 +1032,7 @@ Build a new Codex MCP provider side‑by‑side with the existing Codex SDK prov - Persistence round-trip works via DaemonClient - **Done (2025-12-25 17:23)**: WHAT: Modified `daemon-client.ts:217-265` to fix `resumeAgent()` to properly wait for the new agent's idle state using `skipQueueBefore` option (not cached old agent messages). Added E2E test `daemon.e2e.test.ts:255-366` "persists and resumes Codex agent with conversation history" that creates agent, sends message, deletes, and resumes from persistence handle. RESULT: Persistence round-trip works - agent is deleted, resumed via persistence handle with conversationId, and responds to follow-up messages. EVIDENCE: `npm run test -- daemon.e2e.test.ts -t "persists and resumes"` passed (9.2s). Note: `listPersistedAgents()` and `resumeAgent()` methods already existed; the fix was to make `resumeAgent()` properly skip stale queue messages when waiting for the new agent. -- [ ] **Implement**: Multi-agent E2E test (Phase 4). +- [x] **Implement**: Multi-agent E2E test (Phase 4). **Add E2E test**: - `multi-agent: agent A launches agent B` - parent agent uses agent-control MCP to create child @@ -1040,6 +1040,7 @@ Build a new Codex MCP provider side‑by‑side with the existing Codex SDK prov **Acceptance criteria**: - Multi-agent orchestration works via DaemonClient - Both parent and child agents visible in listAgents() + - **Done (2025-12-25 17:32)**: WHAT: Added `daemon.e2e.test.ts:369-483` describe block "multi-agent orchestration" with test "parent agent creates child agent via agent-control MCP". Test creates a parent Codex agent, prompts it to call `create_agent` tool via agent-control MCP, then verifies: (1) tool call to `create_agent` with server `agent-control` exists in timeline, (2) both parent and child agent IDs are visible via `agent_state` messages, (3) child agent ID from tool output matches tracked agents. RESULT: Multi-agent orchestration verified - parent agent successfully creates child agent using agent-control MCP, both agents tracked by daemon. EVIDENCE: `npm run test --workspace=@paseo/server -- daemon.e2e.test.ts` (5 passed, 2 skipped in 40.8s), `npm run typecheck --workspace=@paseo/server` (exit 0). - [ ] **Review**: Audit daemon E2E test coverage.