Skip to content

Commit 22c6a09

Browse files
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.
1 parent 52d637a commit 22c6a09

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)