This document summarizes the tool-call pipeline used by the Pi agent runtime. It is based on the implementation and documentation in the local Pi project, with details flattened into a public-facing technical note. The key invariant is simple: the model never executes tools directly. It emits structured intent; the runtime validates, executes, records, and feeds results back into the next model turn.
Scope and invariants
- The model receives tool declarations and emits structured tool-call blocks.
- The local runtime owns lookup, argument preparation, schema validation, policy hooks, execution, and result emission.
- The assistant message is finalized before tool preflight begins.
- Execution is driven by assistant message content, not only by a provider stop reason.
- Tool results are persisted as transcript messages and become input to the next model request.
- Live execution order and persisted transcript order can differ in parallel mode.
Runtime data model
Pi separates the provider-facing tool shape from the local runtime tool shape. The provider-facing layer describes what the model is allowed to request. The runtime layer adds local execution behavior, progress reporting, execution-mode hints, and hooks.
| Concept | Role in the pipeline |
|---|---|
| Tool declaration | Name, description, and schema exposed to the model. |
| Assistant tool call | Structured assistant content containing call id, tool name, and parsed arguments. |
| Runtime tool | Local executable definition with label, schema, optional argument preparation, execution mode, and execute handler. |
| Tool result | Model-visible content plus structured runtime details and an optional termination hint. |
| Tool-result message | Persisted transcript message paired back to the assistant tool-call id. |
Provider normalization
Provider APIs serialize tool calls differently. Pi normalizes those formats into one internal stream shape so the agent loop does not need provider-specific execution logic. Streaming adapters can accumulate partial JSON arguments while the model is still producing them, expose partial updates to the UI, and keep only parsed arguments in the finalized assistant message.
| Provider family | Typical wire shape | Internal representation |
|---|---|---|
| Chat-completion style APIs | Function-call deltas | Streaming tool-call start, delta, and end events. |
| Response-item APIs | Function-call and function-output items | Assistant tool-call blocks and tool-result messages. |
| Block/part-based APIs | Tool-use and tool-result content blocks or parts | The same assistant/tool-result transcript pair used by the runtime. |
Request construction
Before a model can request tools, the active tool set is serialized into the provider request. The schema remains the source of truth for arguments. Provider-level options can influence whether the model may emit tool calls, but they do not decide how local execution is scheduled. Local scheduling is controlled by the agent configuration and per-tool execution mode.
Execution stages
- Scan the finalized assistant message for tool-call blocks.
- Choose batch mode: sequential if globally configured, or if any requested tool requires sequential execution; otherwise parallel.
- Emit a tool-execution start event for each requested call as it enters preflight.
- Find the runtime tool by name. Unknown names become immediate error results.
- Optionally prepare raw arguments for compatibility before validation.
- Validate arguments against the tool schema.
- Run the pre-execution hook. A blocking result becomes an immediate error result.
- Execute the tool with the validated arguments, abort signal, and optional progress callback.
- Convert thrown errors into error tool results.
- Run the post-execution hook, which may replace content, details, error state, or termination hint.
- Emit tool-execution end, create the persisted tool-result message, and emit message lifecycle events.
Sequential execution
Sequential mode processes the assistant's tool calls in source order. Each call is prepared, executed, finalized, emitted, and persisted before the next call begins. This mode is selected when the agent is configured for sequential execution, or when any tool in the batch declares that it must run sequentially. The latter rule is batch-wide: one sequential-only tool makes the whole batch sequential.
tool_execution_start A
tool_execution_update A, optional
tool_execution_end A
message_start toolResult A
message_end toolResult A
tool_execution_start B
tool_execution_update B, optional
tool_execution_end B
message_start toolResult B
message_end toolResult B
turn_end toolResults [A, B]
Parallel execution
Parallel mode is the default. It still performs preflight sequentially in assistant source order: lookup, argument preparation, schema validation, and the pre-execution hook run deterministically. Calls that fail preflight are finalized immediately as error results. Calls that pass preflight are started concurrently.
Completion events follow actual completion order. Persisted tool-result messages follow assistant source order. That split lets UI update quickly while keeping transcript replay and the next model request deterministic.
assistant source order: A, B
completion order: B, A
tool_execution_start A
tool_execution_start B
tool_execution_end B
tool_execution_end A
message_start toolResult A
message_end toolResult A
message_start toolResult B
message_end toolResult B
turn_end toolResults [A, B]
Preflight failures in parallel mode
A failed preflight does not necessarily cancel the entire batch. An unknown tool, invalid arguments, blocked action, or observed abort can become an immediate error result. Other prepared calls can still execute if the batch has not been aborted. The final transcript remains ordered by the assistant's original call order.
assistant source order: A invalid, B valid
tool_execution_start A
tool_execution_end A error
tool_execution_start B
tool_execution_end B success
message_start toolResult A
message_end toolResult A
message_start toolResult B
message_end toolResult B
Progress updates
Tools can emit partial results while running. These partial results produce tool-execution update events for the interface, but they are not persisted as transcript messages. The runtime stops accepting late updates after the tool promise settles, then waits for already queued update emissions before finalizing the call.
Hooks and mutation boundaries
The pre-execution hook receives validated arguments and may block execution. In the coding-agent integration, extension-level tool-call handlers can mutate the input object before execution, and later handlers observe earlier mutations. Those mutations are not revalidated automatically, so hook authors must preserve the schema contract themselves.
The post-execution hook runs after the tool finishes but before final execution-end and tool-result message events. It can replace the visible content, structured details, error flag, or termination hint. Missing fields keep the original values; no deep merge is performed.
Termination semantics
A tool result may include a termination hint. The agent stops after the current tool batch only when every finalized tool result in that batch sets the hint. If any finalized result does not terminate, the agent sends all tool results to the next model request and continues normally.
terminate = finalizedCalls.length > 0
and every finalized result has terminate === true
Steering, follow-up, and next-turn refresh
Steering messages are not injected in the middle of a tool batch. The loop waits for the assistant response and its tool executions to complete, emits the turn-end event, refreshes next-turn state, checks stop conditions, and then polls steering messages. Follow-up messages are polled only after the agent would otherwise stop because there are no more tool calls or steering messages.
In the coding-agent integration, active tools, system prompt, model, and thinking level are refreshed at the next safe turn boundary. Changing the active tool set during a run affects the next provider request, not the in-flight request or already executing tools.
Persistence and replay
Tool calls and tool results are stored as transcript messages. The assistant message contains the tool-call blocks; later tool-result messages point back to the original call id. Replay and export use that id pairing. Provider adapters may normalize ids when replaying history across providers and must keep matching tool-result ids in sync. If a provider requires every historical tool call to have a matching result, missing results can be synthesized as explicit error results before serialization.
UI event contract
UI layers should treat assistant streaming events and local tool execution events separately. Assistant message updates show the model constructing text and tool-call arguments. Tool execution events show local execution lifecycle.
| Event family | Use it for |
|---|---|
| message_start / message_update / message_end | Rendering user, assistant, and final tool-result transcript messages. |
| tool_execution_start | Creating a live tool card and showing that preflight has begun. |
| tool_execution_update | Streaming progress output from a running tool. |
| tool_execution_end | Marking local execution complete. In parallel mode this follows completion order. |
Failure modes
Failure is represented as data whenever possible. This keeps the model, transcript, and UI in sync.
| Failure | Runtime behavior |
|---|---|
| Tool name is unknown | Immediate error tool result. |
| Arguments fail validation | Immediate error tool result. |
| Pre-execution hook blocks | Immediate error tool result with the block reason when provided. |
| Tool throws during execution | Thrown error is converted to an error tool result. |
| Post-execution hook throws | Final result becomes an error tool result. |
| Abort signal is observed | Preparation or execution returns an aborted/error result depending on where it is observed. |