Preserve Programmatic Tool Calling caller linkage in App Server and rollouts
Summary
Codex currently drops the Responses API Programmatic Tool Calling parent relationship before App Server and rollout consumers can observe it. Preserve the upstream program / nested call / result / program_output relationship so clients can measure real nested tool execution without treating source text or temporal containment as runtime call identity.
Current observed boundary
I checked Codex CLI 0.149.1 and public main at dc08ace7821614a702b1214c9d08ae0db2634d82.
codex app-server generate-json-schema --experimentalexposescall_idon rawfunction_call,function_call_output,custom_tool_call, andcustom_tool_call_outputresponse items.- The generated
ResponseItemunion has noprogramorprogram_outputvariant and those call/result variants have nocallerfield. - v2
ThreadItemcall variants such ascommandExecution,fileChange, anddynamicToolCallhave a presentationid, status, and some duration fields, but no stable nestedcallIdor parentcaller. item/startedanditem/completedalready carrystartedAtMsandcompletedAtMsrespectively, so lifecycle timing exists but cannot be joined into the programmatic caller graph.- A real Codex rollout containing
functions.execprogrammatic calls persisted the outercustom_tool_call/custom_tool_call_outputand independently observed command/edit items, but no rawprogram,program_output, or nested caller-linked lifecycle rows.
This is not a claim that the current App Server documentation promises those fields. It is a request to preserve the separate, documented Responses API Programmatic Tool Calling relationship through Codex's protocol and persistence surfaces.
Upstream contract
The official Programmatic Tool Calling guide defines:
- a
programitem with its owncall_id; - program-issued calls with their own
call_idandcaller: { type: "program", caller_id: <program call id> }; - caller preservation when returning the matching call output; and
program_output.call_idmatching the program, with terminal status.
Guide: https://developers.openai.com/api/docs/guides/tools-programmatic-tool-calling
The App Server guide defines item/started and item/completed, with the completed item authoritative:
https://developers.openai.com/codex/app-server
Requested Codex behavior
- Extend the core raw response model so
programandprogram_outputround-trip without loss, and program-issued function/custom calls and outputs retain their optionalcaller { type, caller_id }. - Preserve those raw items unchanged in rollout JSONL and
rawResponseItem/completednotifications. - Add stable
callIdplus optionalcaller { type, callerId }to App ServerThreadItemcall variants. For programmatic calls, emit the same identifiers on bothitem/startedanditem/completed. - Keep presentation
id, lifecycle joincallId, and parent joincallerIddistinct. - Preserve started nested calls across failure, interruption, cancellation, and missing-result cases. Allow a nested call to be the caller of another call so the graph has arbitrary depth.
- Represent terminal state explicitly (
completed,failed,cancelled/interrupted, or the existing equivalent) and keep the existing lifecycle timestamps.
Equivalent names are fine if the protocol owns a different canonical vocabulary; the required information and stable joins are the contract.
Acceptance evidence
- Generated JSON Schema and TypeScript include the raw
program/program_outputvariants and caller fields, plus caller linkage on relevantThreadItemvariants. - Serialization round-trip tests prove caller preservation on both calls and outputs.
- App Server integration tests cover sequential siblings, parallel siblings, depth greater than one, failure, cancellation/interruption, and a started call with no result.
- A real rollout proves the same stable nested
callIdandcallerIdon lifecycle start/completion, while the outer orchestration envelope remains distinguishable from its nested operations.
Without those fields, consumers can still report independently observed commands and edits as unlinked lower-bound evidence, but cannot honestly claim complete nested-call attribution, depth, concurrency, or inclusive nested operational duration.
2 Comments
Current source touchpoints, rechecked on public
mainat00b7152a601218aeb8424d9cefef621a127242a8:codex-rs/protocol/src/models.rsowns the rawResponseItemfunction/custom call and output variants. They retaincall_idbut currently have nocaller, and the enum has noprogram/program_outputvariants.codex-rs/app-server-protocol/src/protocol/v2/item.rsowns the v2ThreadItemvariants.CommandExecution,FileChange,McpToolCall, andDynamicToolCallcurrently expose presentationidwithout the nestedcallId/callerrelationship.CoreTurnItemconversion in that same file is where core command/dynamic items become App Server thread items, so the join identifiers must survive into this conversion rather than being reconstructed by a client.codex-rs/app-server/src/bespoke_event_handling.rsforwards the coreResponseItemunchanged inrawResponseItem/completed; extending the owning core enum should therefore make this raw notification preservation testable at one boundary.The repository CODEOWNERS file assigns
codex-rs/core/to@openai/codex-core-agent-team; App Server/protocol ownership still needs maintainer triage. No consumer-side inference can recreate a droppedcaller_id, particularly for parallel siblings or arbitrary nesting.Root-cause correction from a real Codex 0.149.1 run on 2026-08-25:
Codex already has an opt-in local producer for this relationship. With
CODEX_ROLLOUT_TRACE_ROOTset, the source-owned rollout trace emitted:code_cell_startedwithruntime_cell_idand outermodel_visible_call_id;tool_call_startedwith stabletool_call_id,code_mode_runtime_tool_id, andrequester: { type: "code_cell", runtime_cell_id };tool_call_runtime_started/tool_call_runtime_ended;tool_call_endedwith the sametool_call_idand terminal status; andcode_cell_ended.The successful nested command itself retained the required ten-second process deadline, so command timeout wrapping is not the cause. The ordinary persisted rollout for the same run still contained only the outer
custom_tool_call/custom_tool_call_outputplus the independently observedCommandExecution; the caller graph lived only in the local diagnostic trace.A consumer joined the real transcript and trace without inference and reported one caller-linked nested
exec_command, zero unlinked runtime items, depth 1, complete terminal/timestamp coverage, and no wrapper double count. The nested trace interval was 106 ms; the separately observed command duration was 4 ms; the 157 ms outerexecenvelope remained visible but excluded from nested totals.Current source owners:
So this issue should not be implemented as a new telemetry producer, a timeout exemption, or client-side temporal reconstruction. The remaining request is to project the existing source-owned identities and requester relation into the documented/public persisted rollout, raw Responses, and App Server lifecycle surfaces (or make the trace an explicit supported App Server artifact), while preserving the official Programmatic Tool Calling
callercontract.