Skip to content

Chapter 6 · Tool Definition and Execution ​

Chapter goals: Define tools with AgentTool and 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-layer Tool: pure declaration (name/description/parameters), telling the model what tools exist;
  • pi-agent-core-layer AgentTool: 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:

typescript
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 ​

typescript
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 ​

text
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 answer

You 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 ​

typescript
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 beforeToolCall hook 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 ​

  1. Implement a list_dir tool (list directory contents); in the description, clearly state "accepts only relative paths" and observe whether the model complies.
  2. Intentionally make the tool throw for non-existent paths and observe the model's retry behavior after receiving the error in the terminal.
  3. 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.