Preserve Programmatic Tool Calling caller linkage in App Server and rollouts

Open 💬 2 comments Opened Aug 26, 2026 by joshmouch

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 --experimental exposes call_id on raw function_call, function_call_output, custom_tool_call, and custom_tool_call_output response items.
  • The generated ResponseItem union has no program or program_output variant and those call/result variants have no caller field.
  • v2 ThreadItem call variants such as commandExecution, fileChange, and dynamicToolCall have a presentation id, status, and some duration fields, but no stable nested callId or parent caller.
  • item/started and item/completed already carry startedAtMs and completedAtMs respectively, so lifecycle timing exists but cannot be joined into the programmatic caller graph.
  • A real Codex rollout containing functions.exec programmatic calls persisted the outer custom_tool_call / custom_tool_call_output and independently observed command/edit items, but no raw program, 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 program item with its own call_id;
  • program-issued calls with their own call_id and caller: { type: "program", caller_id: <program call id> };
  • caller preservation when returning the matching call output; and
  • program_output.call_id matching 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

  1. Extend the core raw response model so program and program_output round-trip without loss, and program-issued function/custom calls and outputs retain their optional caller { type, caller_id }.
  2. Preserve those raw items unchanged in rollout JSONL and rawResponseItem/completed notifications.
  3. Add stable callId plus optional caller { type, callerId } to App Server ThreadItem call variants. For programmatic calls, emit the same identifiers on both item/started and item/completed.
  4. Keep presentation id, lifecycle join callId, and parent join callerId distinct.
  5. 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.
  6. 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_output variants and caller fields, plus caller linkage on relevant ThreadItem variants.
  • 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 callId and callerId on 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.

View original on GitHub ↗

2 Comments

joshmouch · 2 days ago

Current source touchpoints, rechecked on public main at 00b7152a601218aeb8424d9cefef621a127242a8:

  • codex-rs/protocol/src/models.rs owns the raw ResponseItem function/custom call and output variants. They retain call_id but currently have no caller, and the enum has no program / program_output variants.
  • codex-rs/app-server-protocol/src/protocol/v2/item.rs owns the v2 ThreadItem variants. CommandExecution, FileChange, McpToolCall, and DynamicToolCall currently expose presentation id without the nested callId / caller relationship.
  • The CoreTurnItem conversion 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.rs forwards the core ResponseItem unchanged in rawResponseItem/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 dropped caller_id, particularly for parallel siblings or arbitrary nesting.

joshmouch · 2 days ago

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_ROOT set, the source-owned rollout trace emitted:

  • code_cell_started with runtime_cell_id and outer model_visible_call_id;
  • tool_call_started with stable tool_call_id, code_mode_runtime_tool_id, and requester: { type: "code_cell", runtime_cell_id };
  • tool_call_runtime_started / tool_call_runtime_ended;
  • tool_call_ended with the same tool_call_id and terminal status; and
  • code_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_output plus the independently observed CommandExecution; 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 outer exec envelope 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 caller contract.