Skip to main content

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.

ToolRuntime Class

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

registry
ToolRegistry
required
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>
call
ToolCall
required
The tool call to execute
call.id
string
Optional call identifier for tracking
call.name
string
required
Name of the tool to execute
call.input
ToolInput
required
Input parameters for the tool
context
Partial<ToolContext>
Optional context to pass to the tool. Missing fields are filled with defaults.
result
ToolResult
The execution result
ok
boolean
true if execution succeeded, false otherwise
output
JsonValue
The tool’s output or error information
error
string
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;
};
now
Date
required
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.
signal
AbortSignal
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:
  1. Tool Lookup: Searches the registry for the tool by name
  2. Context Resolution: Merges provided context with defaults
  3. Tool Execution: Calls the tool’s run function
  4. Error Handling: Catches exceptions and converts to error results
  5. 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 */ }
// }

Tool Not Found

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"
// }

Tool Execution Error

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:

Tool-Level Errors

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:
ok
boolean
Always false for errors
output
object
Error metadata
output.callId
string | null
The call ID if provided
output.tool
string
The tool name that was called
output.status
string
Error status: "not_found" or "error"
error
string
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
    };
  }
}

Validate Input Early

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);
}

Build docs developers (and LLMs) love