Skip to content

Commit b7466f5

Browse files
Expose Codex subagent activity over ACP (#304)
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.
1 parent 52d637a commit b7466f5

9 files changed

Lines changed: 198 additions & 2 deletions

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Use [OpenAI Codex](https://github.com/openai/codex) from [Agent Client Protocol]
1212
- Model, reasoning effort, fast mode, approval, and sandbox mode configuration.
1313
- Text prompts, embedded context, images, resource links, and additional workspace directories.
1414
- Shell command, file change, permission request, MCP tool call, terminal output, reasoning, plan, web search, image generation, image view, token usage, and review events.
15+
- Subagent launches as standard ACP tool calls, with Codex thread identity and activity details in namespaced `_meta.codex.subagent` metadata.
1516
- Client-provided MCP servers over command-based stdio config and HTTP transport.
1617
- Slash commands: `/status`, `/mcp`, `/skills`, `/review`, `/review-branch`, `/review-commit`, `/compact`, and `/logout`, as well as configured skills.
1718

src/CodexAcpServer.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ import {
6060
createImageGenerationUpdate,
6161
createImageViewUpdate,
6262
createMcpToolCallUpdate,
63+
createSubAgentActivityUpdate,
6364
formatWebSearchTitle,
6465
} from "./CodexToolCallMapper";
6566
import {
@@ -1179,9 +1180,10 @@ export class CodexAcpServer {
11791180
case "userMessage":
11801181
return this.createUserMessageUpdates(item);
11811182
case "hookPrompt":
1182-
case "subAgentActivity":
11831183
case "sleep":
11841184
return [];
1185+
case "subAgentActivity":
1186+
return [createSubAgentActivityUpdate(item, "completed", "tool_call")];
11851187
case "agentMessage": {
11861188
const meta = createCodexMessagePhaseMeta(item.phase);
11871189
return [{

src/CodexEventHandler.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ import {
5151
createFuzzyFileSearchComplete,
5252
createFuzzyFileSearchStartOrUpdate,
5353
createMcpToolCallUpdate,
54+
createSubAgentActivityUpdate,
5455
createWebSearchCompleteUpdate,
5556
createWebSearchStartUpdate,
5657
fuzzyFileSearchToolCallId,
@@ -79,6 +80,7 @@ export class CodexEventHandler {
7980
private readonly terminalCommandIds = new Set<string>();
8081
private readonly terminalCommandOutputIds = new Set<string>();
8182
private readonly agentMessagePhases = new Map<string, string | null>();
83+
private readonly activeSubAgentActivities = new Set<string>();
8284

8385
constructor(connection: AcpClientConnection, sessionState: SessionState) {
8486
this.connection = connection;
@@ -330,6 +332,8 @@ export class CodexEventHandler {
330332
case "contextCompaction":
331333
return createContextCompactionStartUpdate(event.item);
332334
case "subAgentActivity":
335+
this.activeSubAgentActivities.add(event.item.id);
336+
return createSubAgentActivityUpdate(event.item, "in_progress", "tool_call");
333337
case "sleep":
334338
case "userMessage":
335339
case "hookPrompt":
@@ -387,7 +391,12 @@ export class CodexEventHandler {
387391
case "contextCompaction":
388392
return createContextCompactionCompleteUpdate(event.item);
389393
//ignored types
390-
case "subAgentActivity":
394+
case "subAgentActivity": {
395+
const sessionUpdate = this.activeSubAgentActivities.delete(event.item.id)
396+
? "tool_call_update"
397+
: "tool_call";
398+
return createSubAgentActivityUpdate(event.item, "completed", sessionUpdate);
399+
}
391400
case "sleep":
392401
case "userMessage":
393402
case "hookPrompt":

src/CodexToolCallMapper.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ type GuardianApprovalReviewNotification =
4040
| ItemGuardianApprovalReviewCompletedNotification;
4141
type WebSearchItem = ThreadItem & { type: "webSearch" };
4242
type CollabAgentToolCallItem = ThreadItem & { type: "collabAgentToolCall" };
43+
type SubAgentActivityItem = ThreadItem & { type: "subAgentActivity" };
4344
type CommandExecutionItem = ThreadItem & { type: "commandExecution" };
4445
type ContextCompactionItem = ThreadItem & { type: "contextCompaction" };
4546
type AcpToolCallEvent = Extract<UpdateSessionEvent, { sessionUpdate: "tool_call" }>;
@@ -412,6 +413,7 @@ export function createCollabAgentToolCallUpdate(
412413
title: item.tool,
413414
status: toAcpStatus(item.status),
414415
rawInput: createCollabAgentToolCallRawInput(item),
416+
_meta: createCollabAgentToolCallMeta(item),
415417
};
416418
}
417419

@@ -424,6 +426,7 @@ export function createCollabAgentToolCallCompleteUpdate(
424426
title: item.tool,
425427
status: toAcpStatus(item.status),
426428
rawInput: createCollabAgentToolCallRawInput(item),
429+
_meta: createCollabAgentToolCallMeta(item),
427430
};
428431
}
429432

@@ -433,10 +436,74 @@ function createCollabAgentToolCallRawInput(item: CollabAgentToolCallItem) {
433436
senderThreadId: item.senderThreadId,
434437
receiverThreadIds: item.receiverThreadIds,
435438
agentsStates: item.agentsStates,
439+
model: item.model,
440+
reasoningEffort: item.reasoningEffort,
436441
status: item.status,
437442
};
438443
}
439444

445+
function createCollabAgentToolCallMeta(item: CollabAgentToolCallItem) {
446+
return {
447+
codex: {
448+
collaboration: {
449+
tool: item.tool,
450+
senderThreadId: item.senderThreadId,
451+
receiverThreadIds: item.receiverThreadIds,
452+
},
453+
},
454+
};
455+
}
456+
457+
export function createSubAgentActivityUpdate(
458+
item: SubAgentActivityItem,
459+
status: "in_progress" | "completed",
460+
sessionUpdate: "tool_call" | "tool_call_update",
461+
): UpdateSessionEvent {
462+
const name = item.agentPath.split("/").filter(Boolean).at(-1) ?? "subagent";
463+
const title = formatSubAgentActivityTitle(item.kind, name);
464+
const common = {
465+
toolCallId: item.id,
466+
status,
467+
rawInput: {
468+
agentThreadId: item.agentThreadId,
469+
agentPath: item.agentPath,
470+
activityKind: item.kind,
471+
},
472+
_meta: {
473+
codex: {
474+
subagent: {
475+
threadId: item.agentThreadId,
476+
path: item.agentPath,
477+
activity: item.kind,
478+
},
479+
},
480+
},
481+
};
482+
if (sessionUpdate === "tool_call") {
483+
return {
484+
sessionUpdate,
485+
title,
486+
kind: "other",
487+
...common,
488+
};
489+
}
490+
return {
491+
sessionUpdate,
492+
...common,
493+
};
494+
}
495+
496+
function formatSubAgentActivityTitle(kind: SubAgentActivityItem["kind"], name: string): string {
497+
switch (kind) {
498+
case "started":
499+
return `Start subagent ${name}`;
500+
case "interacted":
501+
return `Interact with subagent ${name}`;
502+
case "interrupted":
503+
return `Interrupt subagent ${name}`;
504+
}
505+
}
506+
440507
export function formatWebSearchTitle(item: WebSearchItem): string {
441508
const action = item.action;
442509
if (!action) {

src/__tests__/CodexACPAgent/collab-agent-events.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,4 +84,30 @@ describe("CodexEventHandler - collab agent tool call events", () => {
8484
"data/collab-agent-tool-call-flow.json"
8585
);
8686
});
87+
88+
it("maps live subagent activity to an ACP tool call", async () => {
89+
const notifications: ServerNotification[] = [
90+
{
91+
method: "item/completed",
92+
params: {
93+
threadId: sessionId,
94+
turnId: "turn-1",
95+
completedAtMs: 0,
96+
item: {
97+
type: "subAgentActivity",
98+
id: "call-spawn-weather",
99+
kind: "started",
100+
agentThreadId: "thread-paris",
101+
agentPath: "/root/weather_research",
102+
},
103+
},
104+
},
105+
];
106+
107+
await setupPromptAndSendNotifications(mockFixture, sessionId, sessionState, notifications);
108+
109+
await expect(`${mockFixture.getAcpConnectionDump([])}\n`).toMatchFileSnapshot(
110+
"data/subagent-activity-flow.json"
111+
);
112+
});
87113
});

src/__tests__/CodexACPAgent/data/collab-agent-tool-call-flow.json

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,20 @@
2121
"message": "Checking weather"
2222
}
2323
},
24+
"model": null,
25+
"reasoningEffort": null,
2426
"status": "inProgress"
27+
},
28+
"_meta": {
29+
"codex": {
30+
"collaboration": {
31+
"tool": "spawnAgent",
32+
"senderThreadId": "thread-main",
33+
"receiverThreadIds": [
34+
"thread-paris"
35+
]
36+
}
37+
}
2538
}
2639
}
2740
}
@@ -49,7 +62,20 @@
4962
"message": null
5063
}
5164
},
65+
"model": null,
66+
"reasoningEffort": null,
5267
"status": "completed"
68+
},
69+
"_meta": {
70+
"codex": {
71+
"collaboration": {
72+
"tool": "spawnAgent",
73+
"senderThreadId": "thread-main",
74+
"receiverThreadIds": [
75+
"thread-paris"
76+
]
77+
}
78+
}
5379
}
5480
}
5581
}

src/__tests__/CodexACPAgent/data/load-session-history.json

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,4 +400,33 @@
400400
}
401401
}
402402
]
403+
}
404+
{
405+
"method": "sessionUpdate",
406+
"args": [
407+
{
408+
"sessionId": "session-1",
409+
"update": {
410+
"sessionUpdate": "tool_call",
411+
"title": "Start subagent test_audit",
412+
"kind": "other",
413+
"toolCallId": "item-subagent-1",
414+
"status": "completed",
415+
"rawInput": {
416+
"agentThreadId": "thread-child-1",
417+
"agentPath": "/root/test_audit",
418+
"activityKind": "started"
419+
},
420+
"_meta": {
421+
"codex": {
422+
"subagent": {
423+
"threadId": "thread-child-1",
424+
"path": "/root/test_audit",
425+
"activity": "started"
426+
}
427+
}
428+
}
429+
}
430+
}
431+
]
403432
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"method": "sessionUpdate",
3+
"args": [
4+
{
5+
"sessionId": "test-session-id",
6+
"update": {
7+
"sessionUpdate": "tool_call",
8+
"title": "Start subagent weather_research",
9+
"kind": "other",
10+
"toolCallId": "call-spawn-weather",
11+
"status": "completed",
12+
"rawInput": {
13+
"agentThreadId": "thread-paris",
14+
"agentPath": "/root/weather_research",
15+
"activityKind": "started"
16+
},
17+
"_meta": {
18+
"codex": {
19+
"subagent": {
20+
"threadId": "thread-paris",
21+
"path": "/root/weather_research",
22+
"activity": "started"
23+
}
24+
}
25+
}
26+
}
27+
}
28+
]
29+
}

src/__tests__/CodexACPAgent/load-session.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,13 @@ describe("CodexACPAgent - loadSession", () => {
165165
type: "contextCompaction",
166166
id: "item-context-compaction-1",
167167
},
168+
{
169+
type: "subAgentActivity",
170+
id: "item-subagent-1",
171+
kind: "started",
172+
agentThreadId: "thread-child-1",
173+
agentPath: "/root/test_audit",
174+
},
168175
],
169176
},
170177
],

0 commit comments

Comments
 (0)