Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
4 changes: 3 additions & 1 deletion src/CodexAcpServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ import {
createImageGenerationUpdate,
createImageViewUpdate,
createMcpToolCallUpdate,
createSubAgentActivityUpdate,
formatWebSearchTitle,
} from "./CodexToolCallMapper";
import {
Expand Down Expand Up @@ -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 [{
Expand Down
11 changes: 10 additions & 1 deletion src/CodexEventHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import {
createFuzzyFileSearchComplete,
createFuzzyFileSearchStartOrUpdate,
createMcpToolCallUpdate,
createSubAgentActivityUpdate,
createWebSearchCompleteUpdate,
createWebSearchStartUpdate,
fuzzyFileSearchToolCallId,
Expand Down Expand Up @@ -79,6 +80,7 @@ export class CodexEventHandler {
private readonly terminalCommandIds = new Set<string>();
private readonly terminalCommandOutputIds = new Set<string>();
private readonly agentMessagePhases = new Map<string, string | null>();
private readonly activeSubAgentActivities = new Set<string>();

constructor(connection: AcpClientConnection, sessionState: SessionState) {
this.connection = connection;
Expand Down Expand Up @@ -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":
Expand Down Expand Up @@ -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":
Expand Down
67 changes: 67 additions & 0 deletions src/CodexToolCallMapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<UpdateSessionEvent, { sessionUpdate: "tool_call" }>;
Expand Down Expand Up @@ -412,6 +413,7 @@ export function createCollabAgentToolCallUpdate(
title: item.tool,
status: toAcpStatus(item.status),
rawInput: createCollabAgentToolCallRawInput(item),
_meta: createCollabAgentToolCallMeta(item),
};
}

Expand All @@ -424,6 +426,7 @@ export function createCollabAgentToolCallCompleteUpdate(
title: item.tool,
status: toAcpStatus(item.status),
rawInput: createCollabAgentToolCallRawInput(item),
_meta: createCollabAgentToolCallMeta(item),
};
}

Expand All @@ -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) {
Expand Down
26 changes: 26 additions & 0 deletions src/__tests__/CodexACPAgent/collab-agent-events.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
);
});
});
26 changes: 26 additions & 0 deletions src/__tests__/CodexACPAgent/data/collab-agent-tool-call-flow.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,20 @@
"message": "Checking weather"
}
},
"model": null,
"reasoningEffort": null,
"status": "inProgress"
},
"_meta": {
"codex": {
"collaboration": {
"tool": "spawnAgent",
"senderThreadId": "thread-main",
"receiverThreadIds": [
"thread-paris"
]
}
}
}
}
}
Expand Down Expand Up @@ -49,7 +62,20 @@
"message": null
}
},
"model": null,
"reasoningEffort": null,
"status": "completed"
},
"_meta": {
"codex": {
"collaboration": {
"tool": "spawnAgent",
"senderThreadId": "thread-main",
"receiverThreadIds": [
"thread-paris"
]
}
}
}
}
}
Expand Down
29 changes: 29 additions & 0 deletions src/__tests__/CodexACPAgent/data/load-session-history.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
}
}
}
]
}
29 changes: 29 additions & 0 deletions src/__tests__/CodexACPAgent/data/subagent-activity-flow.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
}
}
]
}
7 changes: 7 additions & 0 deletions src/__tests__/CodexACPAgent/load-session.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
],
},
],
Expand Down