Overview
The ToolRuntime class manages tool execution, providing context injection, error handling, and result normalization. It acts as the execution layer between tool calls and tool implementations.
The runtime orchestrates tool execution through a registry.
import { ToolRuntime } from "loaf/tools";
import { createToolRegistry } from "loaf/tools";
const registry = createToolRegistry();
const runtime = new ToolRuntime(registry);
Constructor
The tool registry to use for looking up tools
constructor(registry: ToolRegistry)
execute Method
Executes a tool call with optional context.
async execute(call: ToolCall, context?: Partial<ToolContext>): Promise<ToolResult>
The tool call to executeOptional call identifier for tracking
Name of the tool to execute
Input parameters for the tool
Optional context to pass to the tool. Missing fields are filled with defaults.
The execution resulttrue if execution succeeded, false otherwise
The tool’s output or error information
Error message if execution failed
ToolContext
The context object passed to every tool execution provides runtime information and utilities.
type ToolContext = {
now: Date;
log?: (message: string) => void;
signal?: AbortSignal;
};
Current timestamp when the tool started executing. Useful for time-sensitive operations.
log
(message: string) => void
Optional logging function. Tools can use this to output progress or debug information without affecting the result.
Optional abort signal for canceling long-running operations. Tools should check this periodically and stop execution if aborted.
Using Context in Tools
export async function run(input, context) {
context.log?.("Starting operation...");
// Check current time
const startTime = context.now.getTime();
// Respect abort signals
if (context.signal?.aborted) {
return {
ok: false,
output: null,
error: "Operation aborted"
};
}
// Long-running operation
for (let i = 0; i < 1000; i++) {
if (context.signal?.aborted) {
return { ok: false, output: null, error: "Aborted" };
}
// Do work...
}
context.log?.("Operation complete");
return {
ok: true,
output: { duration: Date.now() - startTime }
};
}
Execution Flow
The runtime follows this execution flow:
- Tool Lookup: Searches the registry for the tool by name
- Context Resolution: Merges provided context with defaults
- Tool Execution: Calls the tool’s
run function
- Error Handling: Catches exceptions and converts to error results
- Result Normalization: Ensures result conforms to
ToolResult type
Success Path
const result = await runtime.execute({
name: "my-tool",
input: { query: "test" }
});
// result = {
// ok: true,
// output: { /* tool output */ }
// }
const result = await runtime.execute({
id: "call_123",
name: "nonexistent-tool",
input: {}
});
// result = {
// ok: false,
// output: {
// callId: "call_123",
// tool: "nonexistent-tool",
// status: "not_found"
// },
// error: "unknown tool: nonexistent-tool"
// }
const result = await runtime.execute({
id: "call_456",
name: "failing-tool",
input: {}
});
// result = {
// ok: false,
// output: {
// callId: "call_456",
// tool: "failing-tool",
// status: "error"
// },
// error: "Error: Something went wrong"
// }
Error Handling
The runtime provides multiple layers of error handling:
Tools should return error results for expected failures:
export async function run(input, context) {
if (!input.required_field) {
return {
ok: false,
output: { message: "Missing required field" },
error: "Validation failed: required_field is missing"
};
}
// Continue with valid input...
}
Runtime Exceptions
Unhandled exceptions are caught by the runtime:
export async function run(input, context) {
// This exception will be caught by the runtime
throw new Error("Unexpected condition");
}
// Becomes:
// {
// ok: false,
// output: { callId, tool, status: "error" },
// error: "Unexpected condition"
// }
Error Result Structure
All errors follow a consistent structure:
Error metadataThe tool name that was called
Error status: "not_found" or "error"
Human-readable error message
Context Resolution
The runtime automatically fills in missing context fields:
// Minimal context
await runtime.execute(call, {});
// Resolved to: { now: new Date() }
// Partial context
await runtime.execute(call, {
log: console.log
});
// Resolved to: { now: new Date(), log: console.log }
// Full context
await runtime.execute(call, {
now: new Date('2024-01-01'),
log: myLogger,
signal: abortController.signal
});
// Used as provided
The now field is always set to the current time if not provided, ensuring tools have a consistent timestamp for the execution.
Best Practices
Handle Abort Signals
For long-running operations, check the abort signal periodically:
export async function run(input, context) {
const items = await fetchManyItems(input.count);
for (const item of items) {
if (context.signal?.aborted) {
return {
ok: false,
output: { processed: results.length },
error: "Operation cancelled"
};
}
await processItem(item);
}
return { ok: true, output: { processed: items.length } };
}
Use Logging for Observability
Log important milestones without cluttering the output:
export async function run(input, context) {
context.log?.("Validating input...");
if (!isValid(input)) {
return { ok: false, output: null, error: "Invalid input" };
}
context.log?.("Fetching data...");
const data = await fetchData(input);
context.log?.("Processing results...");
const results = processData(data);
context.log?.("Done");
return { ok: true, output: results };
}
Return Structured Errors
Provide detailed error information in the output:
export async function run(input, context) {
try {
const result = await riskyOperation(input);
return { ok: true, output: result };
} catch (err) {
return {
ok: false,
output: {
operation: "riskyOperation",
input: input,
timestamp: context.now.toISOString()
},
error: err.message
};
}
}
Check input requirements before doing expensive work:
export const inputSchema = {
type: "object",
properties: {
url: { type: "string", format: "uri" },
timeout: { type: "number", minimum: 0 }
},
required: ["url"]
};
export async function run(input, context) {
// Additional validation beyond schema
if (input.timeout && input.timeout > 60000) {
return {
ok: false,
output: null,
error: "Timeout cannot exceed 60 seconds"
};
}
// Proceed with valid input
const result = await fetch(input.url, {
signal: context.signal,
timeout: input.timeout || 5000
});
return { ok: true, output: await result.json() };
}
Complete Example
Here’s a complete example showing all runtime features:
// weather-tool.js
export const name = "weather";
export const description = "Fetches weather data for a location";
export const inputSchema = {
type: "object",
properties: {
location: {
type: "string",
description: "City name or zip code"
},
units: {
type: "string",
enum: ["metric", "imperial"],
default: "metric"
}
},
required: ["location"]
};
export async function run(input, context) {
context.log?.(`Fetching weather for ${input.location}...`);
// Check abort signal before expensive operation
if (context.signal?.aborted) {
return {
ok: false,
output: null,
error: "Request was cancelled"
};
}
try {
const response = await fetch(
`https://api.weather.example.com/current?location=${encodeURIComponent(input.location)}&units=${input.units || 'metric'}`,
{ signal: context.signal }
);
if (!response.ok) {
return {
ok: false,
output: {
status: response.status,
location: input.location
},
error: `Weather API returned ${response.status}`
};
}
const data = await response.json();
context.log?.("Weather data retrieved successfully");
return {
ok: true,
output: {
location: input.location,
temperature: data.temp,
conditions: data.conditions,
units: input.units || 'metric',
timestamp: context.now.toISOString()
}
};
} catch (err) {
context.log?.(`Error: ${err.message}`);
return {
ok: false,
output: {
location: input.location,
error_type: err.name
},
error: `Failed to fetch weather: ${err.message}`
};
}
}
// Using the tool
import { ToolRuntime } from "loaf/tools";
import { createToolRegistry } from "loaf/tools";
const registry = createToolRegistry();
const runtime = new ToolRuntime(registry);
// Load and register the weather tool
const weatherTool = await loadCustomToolFile("./weather-tool.js");
if (weatherTool) {
registry.register(weatherTool.tool);
}
// Execute with context
const abortController = new AbortController();
const result = await runtime.execute(
{
id: "weather_001",
name: "weather",
input: {
location: "San Francisco",
units: "imperial"
}
},
{
log: (msg) => console.log(`[weather] ${msg}`),
signal: abortController.signal
}
);
if (result.ok) {
console.log("Temperature:", result.output.temperature);
console.log("Conditions:", result.output.conditions);
} else {
console.error("Error:", result.error);
}