Tools are functions that agents can invoke during reasoning. They extend agent capabilities beyond text generation, enabling actions like:
Searching the web
Querying databases
Calling external APIs
Reading/writing files
Performing calculations
Interacting with user systems
Tools are the bridge between LLM reasoning and real-world actions.
Every tool consists of two parts:
Schema - JSON Schema describing the tool’s name, description, and parameters
Execute - Async function that implements the tool’s logic
// Source: packages/core/src/types/index.ts:49-64
export interface ToolSchema {
name : string
description : string
parameters : Record < string , unknown > // JSON Schema
}
export interface ToolDefinition < TData = unknown > {
schema : ToolSchema
execute ( args : Record < string , unknown >, ctx : ExecutionContext < TData >) : Promise < unknown >
}
The recommended way to create tools is with the defineTool helper:
import { defineTool } from '@agentlib/core'
const weatherTool = defineTool ({
schema: {
name: 'get_weather' ,
description: 'Get current weather for a city' ,
parameters: {
type: 'object' ,
properties: {
city: {
type: 'string' ,
description: 'City name (e.g., "San Francisco")'
},
units: {
type: 'string' ,
enum: [ 'celsius' , 'fahrenheit' ],
description: 'Temperature units'
}
},
required: [ 'city' ]
}
},
execute : async ( args ) => {
const city = args . city as string
const units = args . units as string || 'celsius'
// Call weather API
const response = await fetch (
`https://api.weather.com/v1/current?city= ${ city } &units= ${ units } `
)
return response . json ()
}
})
defineTool is a type-safe identity function that helps TypeScript infer the correct types. Source: packages/core/src/tool/registry.ts:36-43
Using Decorators
For class-based agents, use the @Tool and @Arg decorators:
import { Agent , Tool , Arg } from '@agentlib/core'
@ Agent ( 'weather-assistant' )
class WeatherAgent {
@ Tool ( 'get_weather' , 'Get current weather for a city' )
async getWeather (
@ Arg ({ name: 'city' , description: 'City name' }) city : string ,
@ Arg ({ name: 'units' , description: 'Temperature units' , required: false }) units ?: string
) : Promise < any > {
const response = await fetch (
`https://api.weather.com/v1/current?city= ${ city } &units= ${ units || 'celsius' } `
)
return response . json ()
}
}
Source: packages/core/src/factory/decorators.ts:24-29
Accessing ExecutionContext
Tools receive an ExecutionContext as the second parameter, giving access to:
// Source: packages/core/src/types/index.ts:250-265
export interface ExecutionContext < TData = unknown > {
/** User input for this run */
input : string
/** User-defined typed state */
data : TData
/** Internal runtime state — do not mutate */
state : ExecutionState
/** Active session ID for this run */
sessionId : string
/** Memory provider scoped to this run */
memory : MemoryProvider | null
/** Cancel the current execution */
cancel () : void
/** Emit a custom event */
emit ( event : string , payload ?: unknown ) : void
}
Example: Using Custom Data
interface MyAppData {
apiKey : string
userId : string
}
const searchTool = defineTool < MyAppData >({
schema: {
name: 'search' ,
description: 'Search the database' ,
parameters: {
type: 'object' ,
properties: {
query: { type: 'string' }
},
required: [ 'query' ]
}
},
execute : async ( args , ctx ) => {
// Access custom data
const { apiKey , userId } = ctx . data
// Use session ID for logging
console . log ( `Search by user ${ userId } in session ${ ctx . sessionId } ` )
// Perform search with API key
const results = await fetch ( 'https://api.example.com/search' , {
headers: { 'Authorization' : `Bearer ${ apiKey } ` },
body: JSON . stringify ({ query: args . query , userId })
})
return results . json ()
}
})
Agents maintain an internal ToolRegistry that manages registered tools:
// Source: packages/core/src/tool/registry.ts:6-34
export class ToolRegistry < TData = unknown > {
private readonly tools = new Map < string , ToolDefinition < TData >>()
register ( tool : ToolDefinition < TData >) : this {
this . tools . set ( tool . schema . name , tool )
return this
}
get ( name : string ) : ToolDefinitionBase | undefined {
return this . tools . get ( name )
}
getSchemas () : ToolSchema [] {
return this . getAll (). map (( t ) => t . schema )
}
isAllowed ( name : string , allowedTools ?: string []) : boolean {
if ( ! allowedTools ) return true
return allowedTools . includes ( name )
}
}
Tools can be registered in three ways:
import { createAgent } from '@agentlib/core'
// 1. During agent creation
const agent = createAgent ({
name: 'assistant' ,
tools: [ weatherTool , searchTool ]
})
// 2. Using the fluent API
agent . tool ( calculatorTool )
// 3. Via decorators (automatic)
@ Agent ( 'my-agent' )
class MyAgent {
@ Tool ( 'my_tool' , 'Does something' )
async myTool () { /* ... */ }
}
When a reasoning engine calls a tool, the following happens:
1. Tool Call Step Recorded
The tool:before event is emitted with the tool name and arguments.
3. Middleware: tool:before
All middleware registered for tool:before scope execute. Source: packages/core/src/reasoning/context.ts:68
The tool’s execute function is called with the arguments and execution context. Source: packages/core/src/reasoning/context.ts:74
A ToolResultStep is pushed to the execution state: // Source: packages/core/src/types/index.ts:175-182
export interface ToolResultStep {
type : 'tool_result'
toolName : string
callId : string
result : unknown
error ?: string
engine : string
}
The result is also appended to ctx.state.messages as a tool role message. Source: packages/core/src/reasoning/context.ts:99-113
The tool:after event is emitted with the tool name, arguments, and result.
7. Middleware: tool:after
All middleware registered for tool:after scope execute. Source: packages/core/src/reasoning/context.ts:116
Error Handling
If a tool throws an error, it’s caught and recorded in the ToolResultStep:
// Source: packages/core/src/reasoning/context.ts:73-96
try {
result = await tool . execute ( args , ctx )
} catch ( err ) {
error = err instanceof Error ? err . message : String ( err )
const resultStep : ToolResultStep = {
type: 'tool_result' ,
toolName: name ,
callId ,
result: null ,
error ,
engine: 'runtime' ,
}
this . pushStep ( resultStep )
ctx . state . toolCalls . push ({ call: { id: callId , name , arguments: args }, result: null })
ctx . state . messages . push ({
role: 'tool' ,
content: JSON . stringify ({ error }),
toolCallId: callId ,
})
void emitter . emit ( 'tool:after' , { name , args , error })
await middleware . run ({ scope: 'tool:after' , ctx , tool: { name , args , result: null } })
throw err
}
The error is re-thrown after being logged, allowing the reasoning engine to handle it.
Graceful Error Handling
For production tools, handle errors gracefully:
const apiTool = defineTool ({
schema: {
name: 'call_api' ,
description: 'Call external API' ,
parameters: { /* ... */ }
},
execute : async ( args ) => {
try {
const response = await fetch ( 'https://api.example.com/data' )
if ( ! response . ok ) {
return {
success: false ,
error: `API returned ${ response . status } : ${ response . statusText } `
}
}
return {
success: true ,
data: await response . json ()
}
} catch ( err ) {
return {
success: false ,
error: err instanceof Error ? err . message : 'Unknown error'
}
}
}
})
Returning error information in the result allows the LLM to reason about failures and potentially retry or take alternative actions.
Policy Constraints
You can restrict which tools an agent can use via the allowedTools policy:
const agent = createAgent ({
name: 'restricted-agent' ,
tools: [ searchTool , deleteTool , sendEmailTool ],
policy: {
allowedTools: [ 'search' , 'send_email' ] // deleteTool is registered but not allowed
}
})
Attempting to call a disallowed tool throws an error:
// Source: packages/core/src/reasoning/context.ts:54-56
if ( ! tools . isAllowed ( name , policy . allowedTools )) {
throw new Error ( `[Reasoning] Tool not allowed by policy: " ${ name } "` )
}
Some reasoning engines support calling multiple tools in parallel. The engine receives all tool calls from the model response and can execute them concurrently:
// Example from ReAct engine
// Source: packages/reasoning/src/engines/react.ts:68-70
if ( ! response . toolCalls ?. length ) {
// No tool calls → done
return response . message . content
}
// Execute all tool calls
await executeToolCalls ( rCtx , response )
The executeToolCalls utility handles parallel execution:
import { executeToolCalls } from '@agentlib/reasoning/utils'
// Executes all tool calls in parallel and appends results to messages
await executeToolCalls ( rCtx , modelResponse )
Write clear descriptions. The LLM relies entirely on your description and parameter descriptions to understand when and how to use the tool.
Be specific in descriptions
// Bad
description : 'Gets data'
// Good
description : 'Retrieves user profile data from the database by user ID'
Use JSON Schema validation
parameters : {
type : 'object' ,
properties : {
age : {
type : 'number' ,
minimum : 0 ,
maximum : 120 ,
description : 'User age in years'
}
}
}
Return structured data
// Return objects, not plain strings when possible
return {
status: 'success' ,
data: results ,
timestamp: new Date (). toISOString ()
}
Keep tools focused
One tool = one responsibility
Compose complex workflows from simple tools
Handle errors gracefully
Don’t let tools crash the agent
Return error information the LLM can reason about
Use enums for constrained values
properties : {
format : {
type : 'string' ,
enum : [ 'json' , 'csv' , 'xml' ],
description : 'Output format'
}
}
Next Steps
Agents - Learn about agent configuration
Reasoning - Understand how engines decide when to call tools
Middleware - Intercept tool calls with middleware
Events - Monitor tool execution