From 22c6a095c83e2634551f1eb1291045bc2ef9d5e9 Mon Sep 17 00:00:00 2001 From: Nikita Ashikhmin Date: Wed, 15 Jul 2026 23:01:35 +0400 Subject: [PATCH] Expose Codex subagent activity over ACP Map subAgentActivity notifications to standard ACP tool calls with namespaced Codex metadata. Preserve collaboration model metadata and replay the same events when loading session history. --- README.md | 1 + src/CodexAcpServer.ts | 4 +- src/CodexEventHandler.ts | 11 ++- src/CodexToolCallMapper.ts | 67 +++++++++++++++++++ .../CodexACPAgent/collab-agent-events.test.ts | 26 +++++++ .../data/collab-agent-tool-call-flow.json | 26 +++++++ .../data/load-session-history.json | 29 ++++++++ .../data/subagent-activity-flow.json | 29 ++++++++ .../CodexACPAgent/load-session.test.ts | 7 ++ 9 files changed, 198 insertions(+), 2 deletions(-) create mode 100644 src/__tests__/CodexACPAgent/data/subagent-activity-flow.json 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", + }, ], }, ],