Summary
Restructure the codex (codex app-server) provider so command/tool execution is surfaced on the live Event stream as a tool call + a complete tool result, matching the new EventToolResult / Event.ToolCallID contract being added for live tool-result streaming.
Background
Captain is gaining a live tool-result contract on pkg/ai/types.go:
Event.ToolCallID string — correlation key set on tool-call and tool-result events.
EventToolResult EventKind = "tool_result" — the complete result of a prior call (ToolCallID, output in Text, Success, Raw).
claude-agent is being wired up in the same change (forwarding the SDK's tool_result blocks). Codex is deferred here because its mapping needs a real restructure rather than a field addition.
Problem (current behaviour)
In pkg/ai/provider/codex_appserver_protocol.go:
item/commandExecution/outputDelta is mapped to EventToolUse{Tool:"command", Input:{"delta": ...}} — i.e. command output is emitted as if it were a tool call.
item/started / item/completed for command/tool/file items also emit EventToolUse (via mapAppServerItem), with the item id stashed only on Event.Raw (not on the Event itself).
So there is no clean call→result correlation, output masquerades as a call, and there is no ToolCallID on the Event.
Proposed change
Per the uniform contract (one EventToolResult per call = complete output; backends that stream output accumulate internally):
item/started (command/tool/file) → EventToolUse{ToolCallID: it.ID, Tool, Input} (the call).
item/commandExecution/outputDelta → accumulate into per-turn state keyed by ItemID (no longer emit an EventToolUse). Accumulation lives alongside the existing usage fold in the codex turn state (codex_appserver.go).
item/completed for that item → emit one EventToolResult{ToolCallID: it.ID, Text: accumulated, Success} (and stash a claude.ToolUse with Response/IsError on Raw for the shared renderer).
- Set
ToolCallID on every codex tool event.
Acceptance criteria
- A codex turn that runs a command surfaces: one
EventToolUse (the command, with ToolCallID) followed by one EventToolResult (the accumulated output, same ToolCallID).
captain live render and captain history render the codex command output identically.
clicky/aichat correlates the tool-output-available SSE part to the right tool card via ToolCallID (no synthetic placeholder).
- Unit tests in
pkg/ai/provider/codex_appserver_test.go cover the call/result split, ToolCallID, and multi-delta accumulation.
References
- Contract + claude-agent + aichat changes: (the related branch/PR adding
EventToolResult/ToolCallID).
- Files:
pkg/ai/provider/codex_appserver_protocol.go (mapAppServerNotification, mapAppServerItem), pkg/ai/provider/codex_appserver.go (turn state), pkg/ai/provider/codex_events.go (Raw stash helpers).
Summary
Restructure the codex (
codex app-server) provider so command/tool execution is surfaced on the liveEventstream as a tool call + a complete tool result, matching the newEventToolResult/Event.ToolCallIDcontract being added for live tool-result streaming.Background
Captain is gaining a live tool-result contract on
pkg/ai/types.go:Event.ToolCallID string— correlation key set on tool-call and tool-result events.EventToolResult EventKind = "tool_result"— the complete result of a prior call (ToolCallID, output inText,Success,Raw).claude-agent is being wired up in the same change (forwarding the SDK's
tool_resultblocks). Codex is deferred here because its mapping needs a real restructure rather than a field addition.Problem (current behaviour)
In
pkg/ai/provider/codex_appserver_protocol.go:item/commandExecution/outputDeltais mapped toEventToolUse{Tool:"command", Input:{"delta": ...}}— i.e. command output is emitted as if it were a tool call.item/started/item/completedfor command/tool/file items also emitEventToolUse(viamapAppServerItem), with the item id stashed only onEvent.Raw(not on theEventitself).So there is no clean call→result correlation, output masquerades as a call, and there is no
ToolCallIDon theEvent.Proposed change
Per the uniform contract (one
EventToolResultper call = complete output; backends that stream output accumulate internally):item/started(command/tool/file) →EventToolUse{ToolCallID: it.ID, Tool, Input}(the call).item/commandExecution/outputDelta→ accumulate into per-turn state keyed byItemID(no longer emit anEventToolUse). Accumulation lives alongside the existing usage fold in the codex turn state (codex_appserver.go).item/completedfor that item → emit oneEventToolResult{ToolCallID: it.ID, Text: accumulated, Success}(and stash aclaude.ToolUsewithResponse/IsErroronRawfor the shared renderer).ToolCallIDon every codex tool event.Acceptance criteria
EventToolUse(the command, withToolCallID) followed by oneEventToolResult(the accumulated output, sameToolCallID).captainlive render andcaptain historyrender the codex command output identically.clicky/aichatcorrelates thetool-output-availableSSE part to the right tool card viaToolCallID(no synthetic placeholder).pkg/ai/provider/codex_appserver_test.gocover the call/result split,ToolCallID, and multi-delta accumulation.References
EventToolResult/ToolCallID).pkg/ai/provider/codex_appserver_protocol.go(mapAppServerNotification,mapAppServerItem),pkg/ai/provider/codex_appserver.go(turn state),pkg/ai/provider/codex_events.go(Raw stash helpers).