P220 min

Tools & Actions

Tools are how agents interact with the outside world. Learn how to design, implement, and use agent tools.

SDK Focustools configcanUseToolAskUserQuestiontool_use_summaryresult

What are Tools?

Tools are interfaces that let agents take real actions. Without tools, an agent can only generate text; with tools, an agent can:

  • Read and write files
  • Execute code
  • Call APIs
  • Search information
  • Interact with databases
  • Send messages

Function Calling

Modern LLMs support tool use, which is the foundation of agent actions. In Anthropic's raw Messages API, the flow looks like this:

text
1. Define the tool schema
{
  "name": "read_file",
  "description": "Read the contents of a file",
  "input_schema": {
    "type": "object",
    "properties": {
      "path": {
        "type": "string",
        "description": "Path to the file"
      }
    },
    "required": ["path"]
  }
}

2. The model emits a tool request
{
  "type": "tool_use",
  "id": "toolu_123",
  "name": "read_file",
  "input": {"path": "config.json"}
}

3. Execute the tool and return the result
{
  "type": "tool_result",
  "tool_use_id": "toolu_123",
  "content": "{\"name\": \"my-app\", ...}"
}

Boundary: Raw Tool Blocks vs SDK Messages

The raw API uses tool_use and tool_result content blocks. The Claude Agent SDK then exposes higher-level stream messages such as tool_use_summary and result. If you need exact tool inputs or outputs, inspect stream_event or the underlying message content blocks. tool_use_summary is prose plus tool-use IDs; it is not a complete audit record.

tool-calling.ts
import { query } from "@anthropic-ai/claude-agent-sdk";

const response = query({
prompt: "Read package.json and tell me the project name",
options: {
  // Control which tools the agent can see
  tools: ["Read", "Glob"],

  // Auto-approve these visible tools without a permission prompt
  allowedTools: ["Read", "Glob"],

  // Or use preset + overrides
  // tools: { type: 'preset', preset: 'claude_code' },
  // disallowedTools: ["Write", "Bash"]

}
});

for await (const message of response) {
if (message.type === "tool_use_summary") {
  console.log(`Tool run: ${message.summary}`);
}
if (message.type === "result" && message.subtype === "success") {
  console.log(`Result: ${message.result.slice(0, 100)}...`);
}
}
piPi equivalentLowercase allowlist, push events through subscribe
typescript
// tool-calling.ts
import { createAgentSession } from "@earendil-works/pi-coding-agent";

// Tool exposure uses lowercase names; built-ins ship with the session.
const { session } = await createAgentSession({ tools: ["read", "glob"] });

let lastText = "";
const unsubscribe = session.subscribe((event) => {
  if (event.type === "tool_execution_start") {
    console.log(`Tool run: ${event.toolName}`);
  } else if (event.type === "agent_end") {
    for (const m of event.messages) {
      if (m.role !== "assistant" || !Array.isArray(m.content)) continue;
      for (const b of m.content) if (b.type === "text") lastText = b.text;
    }
  }
});

try {
  await session.prompt("Read package.json and tell me the project name");
  console.log(`Result: ${lastText.slice(0, 100)}...`);
} finally {
  unsubscribe();
}

Key mappings

Tool exposure casing:

Pi tools use lowercase (["read", "glob"]); Claude Agent SDK tools use PascalCase (["Read", "Glob"]), and allowedTools only controls auto-approval.

Tool execution events:

tool_execution_start / tool_execution_end (per-call) vs tool_use_summary (rolled-up summary).

Final result:

agent_end event carries the final assistant messages — no dedicated result event.

Tool Categories

The lowercase calls below are conceptual, not SDK functions. SDK built-ins use Read, Write, Bash, Grep, and Glob; custom and MCP tools require their implementations or servers.

File Tools

ToolFunction
read_fileRead file contents
write_fileCreate or overwrite a file
edit_fileEdit part of a file
list_directoryList directory contents

Shell Tools

typescript
bash({
  command: "npm install",
  working_dir: "/project",
  timeout: 60,
});

Search Tools

  • grep - Text search
  • glob - Filename matching
  • semantic_search - Semantic search
  • web_search - Web search

API Tools

  • HTTP requests (GET/POST/PUT/DELETE)
  • Database queries
  • Third-party service integrations (GitHub, Slack, etc.)

Tool Design Principles

Atomicity

Each tool should do only one thing:

typescript
// BAD: too many responsibilities
create_and_run_test(file, test_content, run_args);

// GOOD: split into atomic operations
write_file(path, content);
run_command("pytest " + path);

Clear Descriptions

Descriptions should be detailed enough for the LLM to know when to use them:

typescript
// BAD
const badDescription = "Search files";

// GOOD
const goodDescription = `Search for files by name pattern using glob syntax.
Use this when you need to find files matching a pattern like '*.py'
or 'src/**/*.ts'. Returns a list of matching file paths.`;

Reasonable Parameters

json
{
  "name": "edit_file",
  "parameters": {
    "path": "string (required) - File to edit",
    "old_string": "string (required) - Exact text to replace",
    "new_string": "string (required) - Replacement text",
    "expected_count": "number (optional) - Expected match count"
  }
}

Helpful Error Messages

jsonc
// BAD
{"error": "Failed"}

// GOOD
{
  "error": "FileNotFoundError",
  "message": "File 'config.json' not found. Available files in current directory: package.json, tsconfig.json, src/"
}
can-use-tool.ts
declare function isSafeRead(tool: string, input: unknown): Promise<boolean>;
const response = query({
prompt: "Clean up the temp directory",
options: {
  tools: ["Read", "Bash", "Glob"],
  allowedTools: ["Read", "Glob"],

  // Custom permission gate
  canUseTool: async (toolName, input) => {
    // Read-only still needs workspace and sensitive-path validation
    if (["Read", "Glob", "Grep"].includes(toolName) && await isSafeRead(toolName, input)) {
      return { behavior: "allow" };
    }

    // This read-only workflow exposes no general shell escape hatch.
    if (toolName === "Bash") {
      return { behavior: "deny", message: "Shell access is not permitted" };
    }

    // Deny anything else with a clear reason
    return {
      behavior: "deny",
      message: `${toolName} is not permitted in this workflow`
    };
  }

}
});
piPi equivalentPolicy lives at the tool boundary, not a global hook
typescript
// can-use-tool.ts
import { createAgentSession, defineTool } from "@earendil-works/pi-coding-agent";
import { Type } from "@earendil-works/pi-ai";
import { execFile } from "node:child_process";

// Prefer structured intent to filtering shell strings. This tool can remove
// plain filenames only, from one application-owned temp directory.
const cleanTemp = defineTool({
  name: "clean_temp",
  label: "Clean temp files",
  description: "Delete named files from the application temp directory.",
  parameters: Type.Object({ files: Type.Array(Type.String()) }),
  async execute(_id, { files }) {
    if (files.some((file) => !/^[A-Za-z0-9._-]+$/.test(file))) {
      return {
        content: [{ type: "text", text: "DENIED: filenames only" }],
        details: null,
        isError: true,
      };
    }
    const result = await new Promise<{ text: string; isError: boolean }>((res) =>
      execFile("rm", ["-f", "--", ...files], { cwd: "./tmp" }, (error, stdout, stderr) =>
        res({ text: error?.message || stderr || stdout || "Done", isError: !!error })
      )
    );
    return {
      content: [{ type: "text", text: result.text }],
      details: null,
      isError: result.isError,
    };
  },
});

const { session } = await createAgentSession({
  customTools: [cleanTemp],
  tools: ["clean_temp"], // add reads only behind workspace + secret-path policy
});

await session.prompt("Clean up the temp directory");

Key mappings

Where policy lives:

tool boundary (execute() of a custom tool) vs callback hook (canUseTool).

Tool exposure:

omit the built-in (e.g. bash) and expose only the gated wrapper, vs allowlist + decision callback per call.

Schema:

TypeBox Type.Object(…) vs JSON Schema in input_schema.

SDK Insight: Two Permission Behaviors

canUseTool returns one of two decisions: allow (auto-execute, optionally with updatedInput) or deny (block with a message). It fires precisely when a prompt would otherwise be shown, so "ask" is configured via permission rules or a PreToolUse hook — expanded in P11 (HITL).

Model Context Protocol (MCP)

MCP is a tool standardization protocol proposed by Anthropic that lets different agents share tools:

jsonc
// MCP tools/list result item (the server identity lives in client configuration)
{
  "name": "github_create_issue",
  "description": "Create a GitHub issue",
  "inputSchema": {
    "type": "object",
    "properties": {
      "repo": { "type": "string" },
      "title": { "type": "string" },
      "body": { "type": "string" },
    },
  },
}

MCP advantages:

  • Reusable tools
  • Cross-agent compatibility
  • Standardized interface
  • Rich ecosystem

Safety Considerations

  • Sandboxed execution - Limit the tool runtime environment
  • Permission control - Distinguish read-only vs writable tools
  • Confirmation mechanism - Dangerous actions require user confirmation
  • Audit logs - Record actor, tool, decision, and redacted evidence with retention limits
  • Rate limits - Prevent runaway loops

Production Gap

Tool definitions here use simple schemas. Production tools need input sanitization, timeout handling, retry logic, and rate limiting — especially for tools that call external APIs.

At work

Rolling it out at your company. A scoped tool catalog is the safest first agent surface:

  • Pick one read-only pilot before any writes. A "Slack-archive search + JIRA cross-link" agent with tools: ["search_slack", "search_jira"] plus allowedTools for those read-only tools ships in a day, has zero blast radius, and proves the action-space model to security review.
  • Treat tools as the service contract and allowedTools as pre-approval. Document each tool's scope, rate limit, and side effects in the same place you document REST endpoints — security reviewers will ask, and the spec doubles as the LLM's API doc.
  • Keep writes out of allowedTools for the first month. Let permission rules, host approval, or a canUseTool deny path force review. Every approval prompt is a free labeled training example for tightening the policy later.
  • Use tool_use_summary for readable telemetry, not audit. Build protected, redacted audit records from tool blocks or hooks, including actor, decision, and timestamp.

Transitioning into an agent engineering role. Action-space design is the central skill:

  • Senior agent engineers spend the majority of their design time on which tools exist, what their inputs look like, and what scopes they hold — not on prompts. A clean 6-tool catalog beats a sloppy 30-tool one every time.
  • Interviewers probe: "Why is this one tool instead of three? How did you pick the schema? What happens when the tool times out? How does the LLM recover from a 4xx-shaped error?" Have answers.
  • A portfolio-worthy tool design shows a typed schema, a canUseTool policy, an error contract the LLM can recover from, and a logged tool-call trace for one real workflow. That's a tighter story than five MCP servers nobody uses.

Try ItTool Permission Gate

Build a permission-controlled agent for a support team investigating customer incidents.

  1. Create an agent with tools: ["Read", "Bash", "Glob"] and allowedTools: ["Read", "Glob"]
  2. Classify actions as read-only, reversible write, or irreversible; define who approves each tier
  3. Implement canUseTool to log every request and block rm or sudo as irreversible examples
  4. Ask it to inspect .ts files for a reported issue, then test one safe and one blocked request
  5. Review the stream: count false blocks/approvals and decide whether the workflow is safe to pilot

Gate: P2 Complete — Tool calls are logged, canUseTool blocks dangerous ops, multi-tool sequences observed in stream.

Sign in to save lesson progress, unlock flashcards, and continue where you left off.