Chapter 6 · Tool Definition and Execution
Chapter goals: Define tools with
AgentTooland TypeBox, understand the Agent's tool execution loop, and the parallel vs. sequential execution modes.
6.1 Tools Are the Agent's Hands and Feet
LLMs can only generate text by themselves; tools let them read files, query databases, and call APIs. pi's tool protocol has two layers:
pi-ai-layerTool: pure declaration (name/description/parameters), telling the model what tools exist;pi-agent-core-layerAgentTool: declaration +execute()implementation; the Agent loop calls it automatically.
6.2 Defining Parameter Schemas with TypeBox
TypeBox provides type-safe JSON Schema that can also be serialized to plain JSON:
import { Type } from "typebox";
import type { AgentTool } from "@earendil-works/pi-agent-core";
import fs from "node:fs/promises";
// A complete AgentTool example
const readFileTool: AgentTool = {
name: "read_file",
label: "Read File", // UI display name (optional)
description: "Read the contents of a file at the specified path", // Explanation for the model — be clear
parameters: Type.Object({
// The description of each field becomes the model's basis for understanding
path: Type.String({ description: "Absolute or relative path to the file" }),
}),
execute: async (toolCallId, params, signal, onUpdate) => {
const content = await fs.readFile(params.path, "utf-8");
// Optional: stream progress updates to the UI
onUpdate?.({
content: [{ type: "text", text: "Read complete" }],
details: {},
});
// Return an array of content blocks + any extra details
return {
content: [{ type: "text", text: content }],
details: { path: params.path, size: content.length },
};
},
};
// Attach to the Agent
agent.state.tools = [readFileTool];description is prompt engineering
The model's tool selection depends almost entirely on description and parameter descriptions. The more specific you write (when to use it, when not to, units and format conventions), the more accurate the invocation will be.
6.3 Tool Failures: Throw, Don't Return Error Text
execute: async (toolCallId, params) => {
if (!fs.existsSync(params.path)) {
// ✅ Correct: throw an exception — the Agent catches it and reports it as isError:true to the model
throw new Error(`File not found: ${params.path}`);
}
// ❌ Wrong: return { content: [{type:'text',text:'Error: file not found'}] }
// The model cannot distinguish success from failure and may continue reasoning based on erroneous content
}Thrown errors are uniformly converted by the runtime into toolResult messages with isError: true — the model can see the error reason and retry or change course on its own.
6.4 Execution Loop: How the Agent Decides to Call
User message → LLM generates assistant message (with toolCall requests)
→ Runtime validates parameters → executes tools → generates toolResult messages
→ Feeds back into history → calls LLM again
→ If more toolCalls remain, continue looping, until pure text answerYou don't need to write any loop code — Agent.prompt() completes this automatically internally. To manually control each step, use the lower-level agentLoop() (demonstrated in Chapter 20).
6.5 Parallel vs. Sequential Execution Modes
const agent = new Agent({
// Global default: parallel (all tools in a batch run concurrently)
toolExecution: "parallel",
initialState: { systemPrompt: "...", model },
streamFn: models.streamSimple.bind(models),
});
// You can also override on a per-tool basis
const exclusiveTool: AgentTool = {
name: "deploy",
executionMode: "sequential", // Force the entire batch to execute sequentially
// ...
};Key rules:
- parallel (default): pre-checks run sequentially; allowed tools run concurrently; completion events are emitted as they finish, but persisted toolResults still follow the original order given by the model;
- If any tool in a batch is marked
sequential, the entire batch degrades to sequential; - The
beforeToolCallhook can intercept dangerous calls after parameter validation (combined with permissions in Chapter 13).
6.6 Chapter Summary
AgentTool = Tool declaration + execute(); parameters described with TypeBox schema;- Writing a clear description is the first lever for improving invocation accuracy;
- Tool failures should throw; the runtime converts them into isError results to feed back to the model;
- Default is parallel concurrent execution; per-tool
executionMode:"sequential"can force the entire batch to run serially.
🧪 随堂测验
点击你认为正确的选项。答错时会展示正确答案与原因解析。
1. When a tool execution fails, what is the recommended error handling approach?
2. When the global toolExecution is parallel and one tool in the batch has executionMode:"sequential", what is the result?
3. What library is used for the tool's parameters field?
4. What is the purpose of the onUpdate parameter in execute?
🛠️ Hands-on Practice
- Implement a
list_dirtool (list directory contents); in the description, clearly state "accepts only relative paths" and observe whether the model complies. - Intentionally make the tool throw for non-existent paths and observe the model's retry behavior after receiving the error in the terminal.
- Define two slow tools (each sleeping 2 seconds) and measure the total execution time difference under global parallel vs. when both are marked sequential.
Now that you know how to define tools, Chapter 7 shows how tool calls are generated step by step at the streaming layer.