Chapter 7 · Tool Call Event Streams
Chapter goals: Understand the streaming process of the model generating tool calls, handle
toolcall_deltapartial parameters, and validate withvalidateToolCall.
7.1 Two Layers of "Tool Calls"
First clarify the naming to avoid confusion:
| Layer | Event | Meaning |
|---|---|---|
| pi-ai stream layer | toolcall_start/delta/end | The model is generating a tool call (JSON params arrive incrementally) |
| pi-agent-core execution layer | tool_execution_start/update/end | The runtime is executing a completed tool call |
This chapter focuses on the first layer: how the model "writes out" a tool call while "thinking."
7.2 Partial JSON in Streaming
Tool parameters stream out character by character. pi-ai does best-effort parsing — every delta received produces the current partial parse result:
// Observe the raw stream directly at the pi-ai layer
const s = models.stream(model, context);
for await (const event of s) {
if (event.type === "toolcall_delta") {
// partial.content[contentIndex] is the block currently being generated
const toolCall = event.partial.content[event.contentIndex];
if (toolCall.type === "toolCall" && toolCall.arguments) {
// arguments is a partially parsed result: fields may be missing, strings may be truncated
if (toolCall.name === "write_file" && toolCall.arguments.path) {
console.log(`About to write: ${toolCall.arguments.path}`);
// content may not be fully transmitted yet — defensive checks are mandatory!
if (toolCall.arguments.content) {
console.log(`Content preview: ${toolCall.arguments.content.slice(0, 100)}...`);
}
}
}
}
if (event.type === "toolcall_end") {
// At this point arguments is complete (but not yet validated)
console.log("Tool call complete:", event.toolCall.name, event.toolCall.arguments);
}
}The iron law of partial parameters
- Fields may be missing or incomplete — existence checks are mandatory before use;
- Strings may be truncated mid-word; arrays may be incomplete;
- The fallback is always an empty object
{}, neverundefined; - The Google provider does not support function call streaming: you will receive a single complete
toolcall_delta.
7.3 validateToolCall: Mandatory Validation Before Execution
When writing your own execution loop, use validateToolCall to validate parameters against the schema:
import { validateToolCall, type Tool } from "@earendil-works/pi-ai";
const tools: Tool[] = [weatherTool, calculatorTool];
const s = models.stream(model, { messages, tools });
for await (const event of s) {
if (event.type === "toolcall_end") {
const toolCall = event.toolCall;
try {
// Validate and coerce values against the tool's TypeBox schema (throws on failure)
const validatedArgs = validateToolCall(tools, toolCall);
const result = await executeMyTool(toolCall.name, validatedArgs);
// Normal toolResult feedback…
} catch (error) {
// On validation failure, also feed back as toolResult (isError:true) → model can self-correct
context.messages.push({
role: "toolResult",
toolCallId: toolCall.id,
toolName: toolCall.name,
content: [{ type: "text", text: error.message }],
isError: true,
timestamp: Date.now(),
});
}
}
}Feeding validation errors back to the model as a toolResult rather than throwing to the top level lets the model self-repair parameter issues and retry — this is a key design for Agent robustness.
7.4 Observing the Full Flow at the Agent Layer
// Events seen by the Agent class user
agent.subscribe((e) => {
switch (e.type) {
case "message_update": {
const inner = e.assistantMessageEvent;
// Stream-layer events are relayed through message_update
if (inner.type === "toolcall_delta") {
process.stdout.write("▍"); // Parameters still being generated
}
break;
}
case "tool_execution_start":
console.log(`\n▶ Executing ${e.toolName}(${JSON.stringify(e.args)})`);
break;
case "tool_execution_end":
console.log(`◀ Done`);
break;
}
});7.5 Chapter Summary
toolcall_*are events from the model generation phase;tool_execution_*are from the runtime execution phase;- During
toolcall_delta, arguments are best-effort partial JSON: read defensively, fallback is{}; validateToolCall(tools, call)validates against schema; on failure, feed back as an isError toolResult to let the model self-heal;- The Google provider does not support tool call streaming; full parameters arrive in a single event.
🧪 随堂测验
点击你认为正确的选项。答错时会展示正确答案与原因解析。
1. In a toolcall_delta event, what is the worst-case state of the partially parsed arguments?
2. After validateToolCall throws, what is the best practice?
3. Regarding the Google provider's tool call streaming support, which statement is correct?
4. At toolcall_end, what state is event.toolCall.arguments in?
🛠️ Hands-on Practice
- Write a listener that renders a three-stage progress bar for the write_file tool: "path → content preview → done".
- Construct a scenario where wrong parameter types are passed (e.g. intentionally vague description) and verify that validateToolCall catches it and lets the model self-heal.
- Compare the toolcall_delta arrival pattern differences between Anthropic and (if available) Google models and record your findings.
Next Chapter 8: AgentMessage type system and convertToLlm bridging.