Start with a normal action: the model wants to run tests. In the UI this looks like a shell tool call. In the source, several questions appear at once. Why was shell visible in this turn? How is the model's call parsed? Who decides whether it can run in parallel? Where do approval and sandbox checks happen? If the tool fails, what does the model see next?
That is the reading route for Codex tools. Do not begin with a tool inventory. Follow the contract: a tool starts as a model-visible spec, returns as a model call, becomes a runtime invocation, and leaves output plus events behind.
Keep four objects in view:
ToolSpec is the model-visible menu;
ToolRouter is the turn-scoped router;
ToolInvocation is the runtime execution request;
ResponseInputItem is the tool result shape returned
to the next model call.
Evidence boundary.
This article describes only the tool construction, routing, execution,
and recording logic visible in the public openai/codex source. Terms
such as tool menu, router, execution request, and evidence are reading
aids for source objects including ToolSpec,
ToolRouter, ToolInvocation, EventMsg,
and ResponseInputItem. It does not infer private model
service behavior or private deployment-specific tools.
This part follows six questions:
- Where does the tool menu for each model request come from?
- Why is the model-visible tool list not the same as the runtime registry?
- How does a model function call become a
ToolInvocation? - Where do parallelism, cancellation, hooks, and lifecycle notifications attach?
- Why do approval and sandboxing belong to the runtime path, not to the model output?
- How does a tool result flow back to the model, clients, and history?
1. Split "Tool" Into Layers
The word tool is overloaded. It can mean a schema in the model request, a shell executor, apply_patch, MCP, plugins, dynamic tools, multi-agent collaboration tools, or tools discoverable later through tool search. The source becomes readable only after separating model visibility from runtime dispatch.
| Layer | Source Object | Responsibility | Common Misread |
|---|---|---|---|
| Model menu | ToolSpec |
Describe how the model may request tools in this turn. | Treating schema visibility as execution authority. |
| Runtime index | ToolRegistry |
Map tool names to handlers and capability metadata. | Assuming hidden-from-model means absent from runtime. |
| Turn router | ToolRouter |
Parse model outputs into tool calls and dispatch them. | Missing direct, deferred, and dispatch-only exposure. |
| Execution request | ToolInvocation |
Carry session, turn, call id, cancellation, diff tracking, and payload. | Thinking handlers receive only model arguments. |
| Returned evidence | ToolOutput / ResponseInputItem |
Normalize results for the next model call and history. | Looking only at terminal output. |
These layers make Codex tools a runtime contract. The model proposes an action. The runtime decides whether the call is known, whether it can run in parallel, which hooks apply, whether approval or sandboxing is required, and what output shape returns to the model.
2. The Tool Menu Is Built Per Turn
The menu is constructed before each sampling request.
run_sampling_request
calls built_tools(sess, turn_context, cancellation_token) to obtain
the turn's ToolRouter, then creates ToolCallRuntime.
build_prompt
sends router.model_visible_specs() as Prompt.tools and
passes the model's parallel-tool-call capability into the prompt.
That menu depends on the current TurnContext. Shell availability,
model tool shape, MCP servers, enabled plugins and apps, discoverable
connectors, extension executors, and dynamic tools can all change the
router for this specific turn.
built_tools
makes those sources visible: it loads MCP tools, plugins, connectors,
discoverable tools, MCP exposure, extension executors, and dynamic tools,
then passes them to ToolRouter::from_turn_context.
The key property is turn-scoped. In the same Codex process, different turns can have different tool surfaces.
3. Model Visibility and Runtime Registration Are Separate
ToolRouter stores two things:
registry and model_visible_specs.
The first is for dispatch. The second is for the model request. They are
related, but not identical.
Both are built in
build_tool_specs_and_registry.
Codex creates PlannedTools, adds shell, MCP resource tools, core
utility tools, collaboration tools, MCP runtime tools, extension tools,
dynamic tools, and hosted model specs, then appends tool search and
prepends code-mode executors.
The split happens in
build_model_visible_specs_and_registry.
Only direct exposure that is not hidden by code-mode-only becomes a
model-visible spec. The registry is built from all runtimes. A tool can
therefore be hidden from the model while still remaining dispatchable.
Shell tools show why this matters.
add_shell_tools
can make unified exec model-visible while keeping the legacy shell
handler registered as dispatch-only. The prompt can move forward without
breaking older runtime calls.
4. A Model Call Becomes a Runtime Invocation
When the model stream contains a tool call, Codex does not execute raw
JSON. ToolRouter::build_tool_call
parses response items into ToolCall: function calls become
named tool payloads, client-side tool search calls become tool-search
payloads, and custom tool calls keep their custom input.
Then
dispatch_tool_call_with_code_mode_result_inner
wraps the call in
ToolInvocation.
The invocation carries session, turn, cancellation token, diff tracker,
call id, tool name, source, and payload. That complete request is passed
to the registry.
Shape sketch:
model function_call
↓
ToolRouter::build_tool_call(...)
↓
ToolCall { tool_name, call_id, payload }
↓
ToolInvocation { session, turn, cancellation_token, tracker, source, payload }
↓
ToolRegistry dispatch
5. Registry Owns Hooks, Lifecycle, and Normalized Output
ToolRegistry is the map from tool name to runtime.
from_tools
rejects duplicate names. During dispatch,
dispatch_any_with_terminal_outcome
increments the active turn's tool count and looks up the handler. Unknown
tools become model-visible failures instead of silent drops.
Registry also wraps the handler with cross-cutting runtime behavior. It notifies lifecycle start, runs pre-tool-use hooks that can block or rewrite input, records telemetry, runs the handler, runs post-tool-use hooks, records additional context, optionally replaces model-visible output, and finally notifies lifecycle finish. That flow is visible in the second half of dispatch.
6. Parallelism and Cancellation Are Runtime Decisions
A prompt can say the model supports parallel tool calls, but Codex still
gates actual concurrency per tool. ToolCallRuntime
holds a parallel_execution read/write lock and asks the router
whether each tool supports parallel execution.
In
handle_tool_call_with_source,
parallel-safe tools take the read lock; non-parallel tools take the
write lock. Cancellation is handled in the same runtime layer: Codex
either waits for runtime teardown or aborts the task, then creates an
aborted response and notifies lifecycle contributors.
Parallel tool calls mean the model may express parallel intent.
Actual concurrency is narrowed by each tool runtime's capability and
ToolCallRuntime's lock policy.
7. Side Effects Enter Approval and Sandbox Gates
Tools that affect the external environment enter a narrower runtime
gate. The header of
orchestrator.rs
describes it as the central place for approvals, sandbox selection, and
retry semantics. ToolOrchestrator::run
checks approval requirements, selects the first sandbox attempt, and
runs the tool.
If the sandboxed attempt is denied, the retry path uses denial details, network policy, approval policy, and tool escalation rules to decide whether to request approval again and which retry sandbox to use. The model's request and the real side effect are therefore separated by runtime authority checks.
8. Results Return to the Model, Events, and History
Tool results first need a model-visible shape.
FunctionToolOutput,
ApplyPatchToolOutput,
and
ExecCommandToolOutput
implement to_response_item, producing ResponseInputItem.
Shell output includes wall time, exit code or session id, token counts,
and truncated output text.
Client progress comes through the event layer. Tool events include
emit_exec_command_begin
and ToolEmitter variants for shell, apply patch, and unified
exec. Extension lifecycle contributors receive start and finish through
notify_tool_start / notify_tool_finish.
History is closed by the turn loop.
drain_in_flight
waits for in-flight tool futures, converts ResponseInputItem
into ResponseItem, and records conversation items. A tool output
is therefore not just user-visible output. It becomes model input and
recoverable history.
9. Rules To Carry Forward
| Observation | Source Question | Handle |
|---|---|---|
| The model can call a tool. | Is it direct, deferred, or dispatch-only? | Separate model_visible_specs from ToolRegistry. |
| The model emits a function call. | Which ToolPayload does it parse into? |
ToolRouter::build_tool_call. |
| A tool starts running. | Which turn-level data does the handler receive? | ToolInvocation. |
| Tools appear to run in parallel. | Does each runtime support parallel calls? | ToolCallRuntime read/write locking. |
| A command is approved, sandboxed, denied, or retried. | Did it enter the orchestrator path? | ToolOrchestrator::run. |
| A tool returns output. | What shape is seen by the user, the model, and history? | EventMsg, ToolOutput, ResponseInputItem. |
The tool path is now closed: turn context builds the tool surface; the prompt exposes model-visible specs; the model returns a call; the router creates an invocation; registry dispatches it with hooks and lifecycle; side-effecting handlers pass through approval and sandbox gates; outputs return to the model, event stream, and history.
The next part can now focus on authority: where approval policy, permission hooks, sandbox policy, and exec policy each intercept a tool that wants to touch processes, files, or the network.
Sources
- Pinned openai/codex source snapshot
- build_prompt puts router.model_visible_specs() into Prompt.tools
- run_sampling_request builds ToolRouter and ToolCallRuntime
- built_tools
- ToolRouter registry and model_visible_specs
- ToolRouter::build_tool_call
- router creates ToolInvocation and dispatches it
- build_tool_specs_and_registry
- add_tool_sources
- add_shell_tools
- MCP runtime tools, dynamic tools, extension tools, and tool search
- ToolInvocation
- ToolOutput to ResponseInputItem conversion
- ToolRegistry
- dispatch_any_with_terminal_outcome
- ToolCallRuntime parallelism and cancellation
- ToolOrchestrator header
- ToolOrchestrator::run approval, sandbox, and retry
- Tool event emission
- Tool lifecycle contributors
- drain_in_flight records tool outputs