Files
2026-07-29 10:59:18 +07:00

710 lines
16 KiB
Markdown

# Tool Execution Guide
## Overview
Tools are how the agent **interacts with the external world**. They can read files, execute commands, make API calls, or perform any action.
---
## Tool Definition
### Basic Structure
```typescript
interface AgentTool<TParameters extends TSchema, TDetails> extends Tool<TParameters> {
label: string; // Human-readable name for UI
prepareArguments?: (args: unknown) => Static<TParameters>; // Optional arg transformation
execute(
toolCallId: string,
params: Static<TParameters>,
signal?: AbortSignal,
onUpdate?: AgentToolUpdateCallback<TDetails>
): Promise<AgentToolResult<TDetails>>;
}
```
### Tool Result
```typescript
interface AgentToolResult<T> {
content: (TextContent | ImageContent)[]; // Returned to model
details: T; // Arbitrary data for logs/UI
usage?: Usage; // Tool-specific usage (not for LLM context)
addedToolNames?: string[]; // New tools introduced
terminate?: boolean; // Early termination hint
}
```
---
## Tool Execution Flow
```
1. LLM sends tool call
└─► AssistantMessage with toolCall content block
2. prepareToolCall()
├─► Find tool by name
├─► prepareArguments() [optional]
├─► validateToolArguments()
└─► beforeToolCall() hook
├─► Return {block: true} → Error tool result
└─► Continue
3. executePreparedToolCall()
├─► tool.execute() with onUpdate callback
└─► onUpdate(partialResult) → Emit tool_execution_update
4. finalizeExecutedToolCall()
└─► afterToolCall() hook
└─► Override result fields
5. Emit events
├─► tool_execution_end
├─► message_start (toolResult)
└─► message_end (toolResult)
```
---
## Built-in Tools
### 1. Bash Tool
**Purpose**: Execute shell commands.
**Parameters**:
```typescript
interface BashToolInput {
command: string;
}
```
**Returns**: Command output as text.
**Options**:
- `cwd`: Working directory
- `timeout`: Command timeout in seconds
- `maxStdoutLines`: Truncate stdout after N lines
- `maxStderrLines`: Truncate stderr after N lines
**Example**:
```typescript
const bashTool = createBashTool({
cwd: "/home/user/project",
timeout: 30,
maxStdoutLines: 1000,
maxStderrLines: 100
});
await bashTool.execute(
"run_123",
{ command: "ls -la" },
undefined,
onUpdate
);
// Result:
// {
// content: [{ type: "text", text: "drwxr-xr-x ... " }],
// details: {
// command: "ls -la",
// cwd: "/home/user/project",
// exitCode: 0,
// stdout: "...",
// stderr: ""
// }
// }
```
### 2. Read Tool
**Purpose**: Read files (text or binary).
**Parameters**:
```typescript
interface ReadToolInput {
path: string;
startLine?: number; // Optional line range
endLine?: number;
}
```
**Returns**: File contents as text or images (for image files).
**Options**:
- `maxSize`: Maximum file size in bytes
- `maxLines`: Maximum lines for text files
- `maxTotalSize`: Maximum total bytes for multiple files
- `imageProcessor`: Custom image handler
**Example**:
```typescript
const readTool = createReadTool({
maxSize: 1024 * 1024, // 1MB
maxLines: 5000,
imageProcessor: async (buffer) => ({
type: "text",
text: `Image of ${buffer.length} bytes`
})
});
await readTool.execute(
"read_456",
{ path: "src/app.ts", startLine: 1, endLine: 50 },
undefined,
onUpdate
);
// Result:
// {
// content: [{ type: "text", text: "import React from 'react';\n..." }],
// details: { path: "src/app.ts", linesRead: 50 }
// }
```
### 3. Write Tool
**Purpose**: Write files (create or overwrite).
**Parameters**:
```typescript
interface WriteToolInput {
path: string;
content: string;
}
```
**Returns**: Success/failure message.
**Example**:
```typescript
const writeTool = createWriteTool();
await writeTool.execute(
"write_789",
{ path: "src/app.ts", content: "console.log('Hello');" },
undefined,
onUpdate
);
// Result:
// {
// content: [{ type: "text", text: "✓ Wrote 25 bytes to src/app.ts" }],
// details: { path: "src/app.ts", bytesWritten: 25 }
// }
```
### 4. Edit Tool
**Purpose**: Make precise edits to files using line numbers or search/replace.
**Parameters**:
```typescript
interface EditToolInput {
path: string;
startLine: number;
endLine: number;
content: string;
}
```
**Returns**: Success/failure message with diff.
**Example**:
```typescript
const editTool = createEditTool();
await editTool.execute(
"edit_101",
{ path: "src/app.ts", startLine: 5, endLine: 10, content: "const x = 42;" },
undefined,
onUpdate
);
// Result:
// {
// content: [{ type: "text", text: "✓ Edited lines 5-10 in src/app.ts" }],
// details: {
// path: "src/app.ts",
// startLine: 5,
// endLine: 10,
// linesChanged: 6,
// diff: "- const x = 1\n+ const x = 42"
// }
// }
```
---
## Creating Custom Tools
### Basic Custom Tool
```typescript
const weatherTool: AgentTool<TSchema, WeatherDetails> = {
name: "get_weather",
label: "Get Weather",
description: "Get current weather for a city",
parameters: Type.Object({
city: Type.String({ description: "City name" })
}),
execute: async (toolCallId, params, signal, onUpdate) => {
try {
const response = await fetch(
`https://api.weather.com/v1/weather?city=${params.city}`,
{ signal }
);
if (!response.ok) {
throw new Error(`Weather API error: ${response.status}`);
}
const data = await response.json();
return {
content: [{ type: "text", text: `Temperature: ${data.temp}°C` }],
details: {
city: params.city,
temp: data.temp,
humidity: data.humidity,
condition: data.condition
},
usage: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 0,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }
}
};
} catch (error) {
if (error instanceof Error && error.name === "AbortError") {
throw error; // Re-throw abort
}
return {
content: [{ type: "text", text: `Error: ${error.message}` }],
details: { error: error.message },
isError: true
};
}
}
};
```
### Tool with Streaming Updates
```typescript
const backupTool: AgentTool<TSchema, BackupDetails> = {
name: "backup_database",
label: "Backup Database",
description: "Create database backup with progress updates",
parameters: Type.Object({
database: Type.String(),
destination: Type.String()
}),
execute: async (toolCallId, params, signal, onUpdate) => {
const totalSize = await getDatabaseSize(params.database);
let uploaded = 0;
const stream = createBackupStream(params.database);
for await (const chunk of stream) {
uploaded += chunk.length;
// Stream progress updates
onUpdate({
content: [{
type: "text",
text: `Backup progress: ${(uploaded / totalSize * 100).toFixed(1)}%`
}],
details: { uploaded, total: totalSize }
});
if (signal?.aborted) {
throw new Error("Backup cancelled");
}
}
await uploadToStorage(stream, params.destination);
return {
content: [{ type: "text", text: "Backup completed successfully" }],
details: {
database: params.database,
destination: params.destination,
size: uploaded,
duration: Date.now() - startTime
}
};
}
};
```
### Tool with Custom Error Handling
```typescript
const apiTool: AgentTool<TSchema, ApiDetails> = {
name: "make_api_call",
label: "Make API Call",
description: "Make HTTP request to external API",
parameters: Type.Object({
url: Type.String({ format: "uri" }),
method: Type.Optional(Type.String({ enum: ["GET", "POST", "PUT", "DELETE"] })),
headers: Type.Optional(Type.Record(Type.String(), Type.String())),
body: Type.Optional(Type.String())
}),
execute: async (toolCallId, params, signal, onUpdate) => {
try {
const response = await fetch(params.url, {
method: params.method || "GET",
headers: params.headers,
body: params.body,
signal
});
// Handle HTTP errors
if (!response.ok) {
const errorBody = await response.text();
return {
content: [{
type: "text",
text: `HTTP ${response.status}: ${response.statusText}\n${errorBody}`
}],
details: {
url: params.url,
method: params.method,
statusCode: response.status,
body: errorBody
},
isError: true
};
}
const contentType = response.headers.get("content-type") || "";
let responseText = await response.text();
// Handle JSON responses
if (contentType.includes("application/json")) {
try {
const jsonData = JSON.parse(responseText);
responseText = JSON.stringify(jsonData, null, 2);
} catch {
// Not valid JSON, use as-is
}
}
return {
content: [{ type: "text", text: responseText }],
details: {
url: params.url,
method: params.method,
statusCode: response.status,
headers: Object.fromEntries(response.headers.entries())
}
};
} catch (error) {
// Handle network errors
return {
content: [{ type: "text", text: `Network error: ${error.message}` }],
details: {
url: params.url,
error: error.message
},
isError: true
};
}
}
};
```
---
## Tool Configuration
### Tool Options
Tools can be configured with options:
```typescript
const bashTool = createBashTool({
cwd: "/home/user/project",
timeout: 30,
maxStdoutLines: 1000,
maxStderrLines: 100
});
const readTool = createReadTool({
maxSize: 1024 * 1024, // 1MB
maxLines: 5000,
maxTotalSize: 10 * 1024 * 1024 // 10MB total
});
```
### Tool Context
Tools can receive application context:
```typescript
interface ToolContext {
userId: string;
environment: "dev" | "staging" | "prod";
permissions: string[];
}
const tool: AgentHarnessTool<ToolContext> = {
name: "deploy_service",
label: "Deploy Service",
description: "Deploy service to environment",
parameters: Type.Object({
service: Type.String(),
environment: Type.String({ enum: ["dev", "staging", "prod"] })
}),
execute: async (toolCallId, params, signal, onUpdate, context) => {
// Access context
if (!context.permissions.includes("deploy")) {
throw new Error("Permission denied");
}
if (context.environment === "prod" && !params.environment) {
throw new Error("Must specify environment for prod deployment");
}
// ...
}
};
const harness = new AgentHarness({
tools: [tool],
toolContext: {
userId: "user123",
environment: "prod",
permissions: ["read", "write", "deploy"]
}
});
```
---
## Tool Execution Modes
### Sequential Mode
Tools marked as sequential execute **one at a time**:
```typescript
const sequentialTool: AgentTool<TSchema> = {
name: "sequential_tool",
label: "Sequential Tool",
description: "Must run one at a time",
parameters: Type.Object({}),
executionMode: "sequential", // Key point
execute: async (toolCallId, params, signal, onUpdate) => {
// This tool won't run concurrently with other sequential tools
// Even if LLM sends multiple tool calls
}
};
```
### Parallel Mode (Default)
Tools execute **concurrently** by default:
```typescript
const parallelTool: AgentTool<TSchema> = {
name: "parallel_tool",
label: "Parallel Tool",
description: "Can run concurrently",
parameters: Type.Object({}),
// executionMode defaults to "parallel"
execute: async (toolCallId, params, signal, onUpdate) => {
// This tool can run alongside other parallel tools
}
};
```
### Agent-Level Execution Mode
```typescript
const agent = new Agent({
initialState: {...},
streamFn: ...
toolExecution: "sequential" // All tools sequential by default
});
```
---
## Error Handling
### Tool Errors
Tools should **throw** on critical errors (abort, timeout) but **return error results** on recoverable errors:
```typescript
execute: async (toolCallId, params, signal, onUpdate) => {
try {
// Check for abort first
if (signal?.aborted) {
throw new Error("Operation aborted");
}
// Do work...
// Return error result for recoverable errors
return {
content: [{ type: "text", text: "Error: Invalid input" }],
details: { error: "Invalid input" },
isError: true
};
} catch (error) {
// Re-throw abort errors
if (error instanceof Error && error.name === "AbortError") {
throw error;
}
// Return error result for other errors
return {
content: [{ type: "text", text: `Error: ${error.message}` }],
details: { error: error.message },
isError: true
};
}
}
```
### Blockable Tools
Use `beforeToolCall` hook to block tool execution:
```typescript
beforeToolCall: async ({ toolCall, args }, signal) => {
if (toolCall.name === "bash") {
// Check for dangerous commands
const dangerousPatterns = ["rm -rf", "sudo", "dd if="];
for (const pattern of dangerousPatterns) {
if (args.command?.includes(pattern)) {
return { block: true, reason: "Dangerous command blocked" };
}
}
}
return undefined; // Allow execution
}
```
---
## Best Practices
### 1. Respect Abort Signals
```typescript
execute: async (toolCallId, params, signal, onUpdate) => {
if (signal?.aborted) {
throw new Error("Operation aborted");
}
// Long-running operation
for await (const item of longProcess()) {
if (signal?.aborted) {
throw new Error("Operation aborted");
}
onUpdate({ content: [{ type: "text", text: "Processing..." }] });
}
}
```
### 2. Return Meaningful Error Messages
```typescript
// Bad
return { content: [{ type: "text", text: "Error" }], isError: true };
// Good
return {
content: [{ type: "text", text: "Failed to read file: permission denied" }],
details: { path: "/etc/passwd", error: "EACCES" },
isError: true
};
```
### 3. Stream Progress for Long Operations
```typescript
execute: async (toolCallId, params, signal, onUpdate) => {
for (let i = 0; i < 100; i++) {
// Do work...
onUpdate({
content: [{ type: "text", text: `Progress: ${i}%` }],
details: { progress: i }
});
}
return {
content: [{ type: "text", text: "Complete" }],
details: { progress: 100 }
};
}
```
### 4. Use Proper Tool Result Types
```typescript
interface BashDetails {
command: string;
cwd: string;
exitCode: number;
stdout: string;
stderr: string;
}
return {
content: [{ type: "text", text: "Command executed" }],
details: { command, cwd, exitCode, stdout, stderr } as BashDetails
};
```
### 5. Handle Large Outputs
```typescript
execute: async (toolCallId, params, signal, onUpdate) => {
const stdoutLines: string[] = [];
const stderrLines: string[] = [];
for await (const chunk of process.stdout) {
stdoutLines.push(chunk);
if (stdoutLines.length > MAX_LINES) {
break; // Truncate
}
}
return {
content: [{ type: "text", text: truncate(stdoutLines.join("\n")) }],
details: { stdout: stdoutLines.join("\n") }
};
}
```
---
## Summary
**Tools are the bridge** between the agent and the external world.
**Key principles**:
- Return `isError: true` for recoverable errors
- Throw on abort/timeout
- Stream progress for long operations
- Respect abort signals throughout
- Use detailed error messages
**Built-in tools**:
- `bash`: Execute shell commands
- `read`: Read files
- `write`: Write files
- `edit`: Make precise edits
**Custom tools** can do anything: API calls, database queries, file operations, etc.