710 lines
16 KiB
Markdown
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.
|