diff --git a/README.md b/README.md index fe401b19..7cf19dae 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ Use [OpenAI Codex](https://github.com/openai/codex) from [Agent Client Protocol] - Model, reasoning effort, fast mode, approval, and sandbox mode configuration. - Text prompts, embedded context, images, resource links, and additional workspace directories. - Shell command, file change, permission request, MCP tool call, terminal output, reasoning, plan, web search, image generation, image view, token usage, and review events. +- Subagent launches as standard ACP tool calls, with Codex thread identity and activity details in namespaced `_meta.codex.subagent` metadata. - Client-provided MCP servers over command-based stdio config and HTTP transport. - Slash commands: `/status`, `/mcp`, `/skills`, `/review`, `/review-branch`, `/review-commit`, `/compact`, and `/logout`, as well as configured skills. diff --git a/src/CodexAcpServer.ts b/src/CodexAcpServer.ts index eedaf6a3..24a883e7 100644 --- a/src/CodexAcpServer.ts +++ b/src/CodexAcpServer.ts @@ -60,6 +60,7 @@ import { createImageGenerationUpdate, createImageViewUpdate, createMcpToolCallUpdate, + createSubAgentActivityUpdate, formatWebSearchTitle, } from "./CodexToolCallMapper"; import { @@ -1179,9 +1180,10 @@ export class CodexAcpServer { case "userMessage": return this.createUserMessageUpdates(item); case "hookPrompt": - case "subAgentActivity": case "sleep": return []; + case "subAgentActivity": + return [createSubAgentActivityUpdate(item, "completed", "tool_call")]; case "agentMessage": { const meta = createCodexMessagePhaseMeta(item.phase); return [{ diff --git a/src/CodexEventHandler.ts b/src/CodexEventHandler.ts index 22e8248b..9b83a1d6 100644 --- a/src/CodexEventHandler.ts +++ b/src/CodexEventHandler.ts @@ -51,6 +51,7 @@ import { createFuzzyFileSearchComplete, createFuzzyFileSearchStartOrUpdate, createMcpToolCallUpdate, + createSubAgentActivityUpdate, createWebSearchCompleteUpdate, createWebSearchStartUpdate, fuzzyFileSearchToolCallId, @@ -79,6 +80,7 @@ export class CodexEventHandler { private readonly terminalCommandIds = new Set(); private readonly terminalCommandOutputIds = new Set(); private readonly agentMessagePhases = new Map(); + private readonly activeSubAgentActivities = new Set(); constructor(connection: AcpClientConnection, sessionState: SessionState) { this.connection = connection; @@ -330,6 +332,8 @@ export class CodexEventHandler { case "contextCompaction": return createContextCompactionStartUpdate(event.item); case "subAgentActivity": + this.activeSubAgentActivities.add(event.item.id); + return createSubAgentActivityUpdate(event.item, "in_progress", "tool_call"); case "sleep": case "userMessage": case "hookPrompt": @@ -387,7 +391,12 @@ export class CodexEventHandler { case "contextCompaction": return createContextCompactionCompleteUpdate(event.item); //ignored types - case "subAgentActivity": + case "subAgentActivity": { + const sessionUpdate = this.activeSubAgentActivities.delete(event.item.id) + ? "tool_call_update" + : "tool_call"; + return createSubAgentActivityUpdate(event.item, "completed", sessionUpdate); + } case "sleep": case "userMessage": case "hookPrompt": diff --git a/src/CodexToolCallMapper.ts b/src/CodexToolCallMapper.ts index 20465fc2..c14612eb 100644 --- a/src/CodexToolCallMapper.ts +++ b/src/CodexToolCallMapper.ts @@ -40,6 +40,7 @@ type GuardianApprovalReviewNotification = | ItemGuardianApprovalReviewCompletedNotification; type WebSearchItem = ThreadItem & { type: "webSearch" }; type CollabAgentToolCallItem = ThreadItem & { type: "collabAgentToolCall" }; +type SubAgentActivityItem = ThreadItem & { type: "subAgentActivity" }; type CommandExecutionItem = ThreadItem & { type: "commandExecution" }; type ContextCompactionItem = ThreadItem & { type: "contextCompaction" }; type AcpToolCallEvent = Extract; @@ -412,6 +413,7 @@ export function createCollabAgentToolCallUpdate( title: item.tool, status: toAcpStatus(item.status), rawInput: createCollabAgentToolCallRawInput(item), + _meta: createCollabAgentToolCallMeta(item), }; } @@ -424,6 +426,7 @@ export function createCollabAgentToolCallCompleteUpdate( title: item.tool, status: toAcpStatus(item.status), rawInput: createCollabAgentToolCallRawInput(item), + _meta: createCollabAgentToolCallMeta(item), }; } @@ -433,10 +436,74 @@ function createCollabAgentToolCallRawInput(item: CollabAgentToolCallItem) { senderThreadId: item.senderThreadId, receiverThreadIds: item.receiverThreadIds, agentsStates: item.agentsStates, + model: item.model, + reasoningEffort: item.reasoningEffort, status: item.status, }; } +function createCollabAgentToolCallMeta(item: CollabAgentToolCallItem) { + return { + codex: { + collaboration: { + tool: item.tool, + senderThreadId: item.senderThreadId, + receiverThreadIds: item.receiverThreadIds, + }, + }, + }; +} + +export function createSubAgentActivityUpdate( + item: SubAgentActivityItem, + status: "in_progress" | "completed", + sessionUpdate: "tool_call" | "tool_call_update", +): UpdateSessionEvent { + const name = item.agentPath.split("/").filter(Boolean).at(-1) ?? "subagent"; + const title = formatSubAgentActivityTitle(item.kind, name); + const common = { + toolCallId: item.id, + status, + rawInput: { + agentThreadId: item.agentThreadId, + agentPath: item.agentPath, + activityKind: item.kind, + }, + _meta: { + codex: { + subagent: { + threadId: item.agentThreadId, + path: item.agentPath, + activity: item.kind, + }, + }, + }, + }; + if (sessionUpdate === "tool_call") { + return { + sessionUpdate, + title, + kind: "other", + ...common, + }; + } + return { + sessionUpdate, + ...common, + }; +} + +function formatSubAgentActivityTitle(kind: SubAgentActivityItem["kind"], name: string): string { + switch (kind) { + case "started": + return `Start subagent ${name}`; + case "interacted": + return `Interact with subagent ${name}`; + case "interrupted": + return `Interrupt subagent ${name}`; + } +} + export function formatWebSearchTitle(item: WebSearchItem): string { const action = item.action; if (!action) { diff --git a/src/__tests__/CodexACPAgent/collab-agent-events.test.ts b/src/__tests__/CodexACPAgent/collab-agent-events.test.ts index 9964d2d1..5ad2c72a 100644 --- a/src/__tests__/CodexACPAgent/collab-agent-events.test.ts +++ b/src/__tests__/CodexACPAgent/collab-agent-events.test.ts @@ -84,4 +84,30 @@ describe("CodexEventHandler - collab agent tool call events", () => { "data/collab-agent-tool-call-flow.json" ); }); + + it("maps live subagent activity to an ACP tool call", async () => { + const notifications: ServerNotification[] = [ + { + method: "item/completed", + params: { + threadId: sessionId, + turnId: "turn-1", + completedAtMs: 0, + item: { + type: "subAgentActivity", + id: "call-spawn-weather", + kind: "started", + agentThreadId: "thread-paris", + agentPath: "/root/weather_research", + }, + }, + }, + ]; + + await setupPromptAndSendNotifications(mockFixture, sessionId, sessionState, notifications); + + await expect(`${mockFixture.getAcpConnectionDump([])}\n`).toMatchFileSnapshot( + "data/subagent-activity-flow.json" + ); + }); }); diff --git a/src/__tests__/CodexACPAgent/data/collab-agent-tool-call-flow.json b/src/__tests__/CodexACPAgent/data/collab-agent-tool-call-flow.json index 8e7ed13c..4391e5a9 100644 --- a/src/__tests__/CodexACPAgent/data/collab-agent-tool-call-flow.json +++ b/src/__tests__/CodexACPAgent/data/collab-agent-tool-call-flow.json @@ -21,7 +21,20 @@ "message": "Checking weather" } }, + "model": null, + "reasoningEffort": null, "status": "inProgress" + }, + "_meta": { + "codex": { + "collaboration": { + "tool": "spawnAgent", + "senderThreadId": "thread-main", + "receiverThreadIds": [ + "thread-paris" + ] + } + } } } } @@ -49,7 +62,20 @@ "message": null } }, + "model": null, + "reasoningEffort": null, "status": "completed" + }, + "_meta": { + "codex": { + "collaboration": { + "tool": "spawnAgent", + "senderThreadId": "thread-main", + "receiverThreadIds": [ + "thread-paris" + ] + } + } } } } diff --git a/src/__tests__/CodexACPAgent/data/load-session-history.json b/src/__tests__/CodexACPAgent/data/load-session-history.json index 106786b4..0018e170 100644 --- a/src/__tests__/CodexACPAgent/data/load-session-history.json +++ b/src/__tests__/CodexACPAgent/data/load-session-history.json @@ -400,4 +400,33 @@ } } ] +} +{ + "method": "sessionUpdate", + "args": [ + { + "sessionId": "session-1", + "update": { + "sessionUpdate": "tool_call", + "title": "Start subagent test_audit", + "kind": "other", + "toolCallId": "item-subagent-1", + "status": "completed", + "rawInput": { + "agentThreadId": "thread-child-1", + "agentPath": "/root/test_audit", + "activityKind": "started" + }, + "_meta": { + "codex": { + "subagent": { + "threadId": "thread-child-1", + "path": "/root/test_audit", + "activity": "started" + } + } + } + } + } + ] } \ No newline at end of file diff --git a/src/__tests__/CodexACPAgent/data/subagent-activity-flow.json b/src/__tests__/CodexACPAgent/data/subagent-activity-flow.json new file mode 100644 index 00000000..e1b6749c --- /dev/null +++ b/src/__tests__/CodexACPAgent/data/subagent-activity-flow.json @@ -0,0 +1,29 @@ +{ + "method": "sessionUpdate", + "args": [ + { + "sessionId": "test-session-id", + "update": { + "sessionUpdate": "tool_call", + "title": "Start subagent weather_research", + "kind": "other", + "toolCallId": "call-spawn-weather", + "status": "completed", + "rawInput": { + "agentThreadId": "thread-paris", + "agentPath": "/root/weather_research", + "activityKind": "started" + }, + "_meta": { + "codex": { + "subagent": { + "threadId": "thread-paris", + "path": "/root/weather_research", + "activity": "started" + } + } + } + } + } + ] +} diff --git a/src/__tests__/CodexACPAgent/load-session.test.ts b/src/__tests__/CodexACPAgent/load-session.test.ts index 1b2b81e0..a64089e1 100644 --- a/src/__tests__/CodexACPAgent/load-session.test.ts +++ b/src/__tests__/CodexACPAgent/load-session.test.ts @@ -165,6 +165,13 @@ describe("CodexACPAgent - loadSession", () => { type: "contextCompaction", id: "item-context-compaction-1", }, + { + type: "subAgentActivity", + id: "item-subagent-1", + kind: "started", + agentThreadId: "thread-child-1", + agentPath: "/root/test_audit", + }, ], }, ],