Agent Tools
Tools extend agent capabilities with executable functions. There are two types: internal tools and external tools. Internal tools live inside the agent process; external tools follow OpenAI's tool-calling protocol and are executed by the client.
Tool Types
Internal Tools
Internal tools are executed within the agent's process. They can be:
- Skill tools — defined in skills using the
@tooldecorator. - Standalone tools — decorated functions passed directly to the agent.
External Tools
External tools are defined in the request and executed on the client side. The agent emits OpenAI tool calls; your client is responsible for executing them and returning results in a follow-up message. This keeps server responsibilities minimal while remaining compatible with OpenAI tooling.
For creating custom HTTP API endpoints, see Agent Endpoints, which covers the
@httpdecorator and REST API creation.
Internal Tools
Standalone Tools
import { BaseAgent, tool, Skill } from 'webagents';
class CalculatorTools extends Skill {
readonly name = 'calculator-tools';
@tool({ description: 'Calculate mathematical expressions' })
async calculate(params: { expression: string }): Promise<string> {
try {
const result = Function(`"use strict"; return (${params.expression})`)();
return String(result);
} catch {
return 'Invalid expression';
}
}
@tool({ description: 'Owner-only administrative function', scopes: ['owner'] })
async adminFunction(params: { action: string }): Promise<string> {
return `Admin action: ${params.action}`;
}
}
const agent = new BaseAgent({
name: 'my-agent',
model: 'openai/gpt-4o',
skills: [new CalculatorTools()],
});Skill Tools
import { Skill, tool } from 'webagents';
class CalculatorSkill extends Skill {
readonly name = 'calculator';
@tool({ description: 'Add two numbers' })
async add(params: { a: number; b: number }): Promise<number> {
return params.a + params.b;
}
@tool({ description: 'Multiply two numbers (owner only)', scopes: ['owner'] })
async multiply(params: { x: number; y: number }): Promise<number> {
return params.x * params.y;
}
}Tool Parameters
@tool({
name: 'custom_name', // Override method name
description: 'Custom', // Description for the LLM
scopes: ['all'], // Access control: 'all' | 'owner' | 'admin' | …
provides: 'chart', // Capability this tool provides (for discovery)
parameters: { /* JSON Schema */ },
})
async myTool(params: { value: string }): Promise<string> {
return `Result: ${params.value}`;
}The provides field
The provides field declares what capability a tool provides. This is used for:
- Agent capability discovery — Clients can query what an agent can do.
- UAMP capabilities — Exposed in
Capabilities.providesfor agent-to-agent communication.
@tool({ provides: 'web_search', description: 'Search the web for information' })
async searchWeb(params: { query: string }): Promise<string> { /* ... */ return ''; }
@tool({ provides: 'chart', description: 'Render data as a chart widget' })
async renderChart(params: { data: string }): Promise<string> { /* ... */ return ''; }
@tool({ provides: 'tts', description: 'Convert text to speech audio' })
async textToSpeech(params: { text: string }): Promise<Uint8Array> { /* ... */ return new Uint8Array(); }The agent aggregates all provides values from tools, handoffs, and endpoints into its capabilities.
OpenAI Schema Generation
Tools generate OpenAI-compatible schemas automatically. In Python the schema is derived from type hints and the docstring; in TypeScript pass an explicit parameters object (JSON Schema) when richer descriptions are needed.
@tool({
description: 'Search the web for information',
parameters: {
type: 'object',
properties: {
query: { type: 'string', description: 'Search query string' },
max_results: { type: 'integer', description: 'Maximum results', default: 10 },
},
required: ['query'],
},
})
async searchWeb(params: { query: string; max_results?: number }): Promise<string[]> {
return ['result1', 'result2'];
}Both produce the same schema:
{
"type": "function",
"function": {
"name": "search_web",
"description": "Search the web for information",
"parameters": {
"type": "object",
"properties": {
"query": { "type": "string", "description": "Search query string" },
"max_results": { "type": "integer", "description": "Maximum results to return", "default": 10 }
},
"required": ["query"]
}
}
}External Tools
External tools are defined in the request's tools parameter and executed on the requester's side. They follow the standard OpenAI tool definition format.
{
"tools": [
{
"type": "function",
"function": {
"name": "function_name",
"description": "Function description",
"parameters": {
"type": "object",
"properties": {
"param_name": { "type": "string", "description": "Parameter description" }
},
"required": ["param_name"]
}
}
}
]
}Using External Tools
const externalTools = [
{
type: 'function',
function: {
name: 'get_weather',
description: 'Get current weather for a location',
parameters: {
type: 'object',
properties: {
location: { type: 'string', description: 'The city and state, e.g. San Francisco, CA' },
unit: { type: 'string', description: 'Temperature unit', enum: ['celsius', 'fahrenheit'] },
},
required: ['location'],
},
},
},
] as const;
const messages = [{ role: 'user' as const, content: "What's the weather in Paris?" }];
const response = await agent.run(messages, { tools: externalTools as any });Handling Tool Calls
When the agent emits tool calls, you execute them client-side and feed the results back:
const response = await agent.run(messages, { tools: externalTools as any });
const message = response;
if (message.tool_calls?.length) {
for (const call of message.tool_calls) {
const args = JSON.parse(call.function.arguments);
let result = '';
if (call.function.name === 'get_weather') {
result = await getWeatherExternal(args.location);
}
messages.push({ role: 'assistant', content: message.content, tool_calls: [call] } as any);
messages.push({ role: 'tool', tool_call_id: call.id, content: result } as any);
}
const final = await agent.run(messages, { tools: externalTools as any });
console.log(final.content);
}
async function getWeatherExternal(location: string): Promise<string> {
return `Sunny in ${location}, 22°C`;
}Tool Execution
Automatic Tool Calling
const response = await agent.run([
{ role: 'user', content: "What's the weather in Paris?" },
]);Manual Tool Results
const messages = [
{ role: 'user' as const, content: 'Calculate 42 * 17' },
{
role: 'assistant' as const,
content: "I'll calculate that for you.",
tool_calls: [
{
id: 'call_123',
type: 'function',
function: { name: 'multiply', arguments: '{"x": 42, "y": 17}' },
},
],
},
{ role: 'tool' as const, tool_call_id: 'call_123', content: '714' },
];
const response = await agent.run(messages as any);Advanced Tool Features
Dynamic Tool Registration
import { Skill, hook } from 'webagents';
class AdaptiveSkill extends Skill {
readonly name = 'adaptive';
@hook({ lifecycle: 'on_connection' })
async registerDynamicTools(data, ctx) {
if (ctx.auth?.userId === 'admin') {
// TS does not yet support runtime self-registration of decorated tools;
// expose the tool unconditionally and gate it via @tool({ scopes: ['admin'] }).
}
return data;
}
}Tool Middleware
import { Skill, hook } from 'webagents';
class ToolMonitor extends Skill {
readonly name = 'tool-monitor';
@hook({ lifecycle: 'before_toolcall', priority: 1 })
async validateTool(data, ctx) {
const toolName = data.tool_call?.function?.name;
if (this.isRateLimited(toolName)) {
throw new Error(`Tool ${toolName} rate limited`);
}
const args = JSON.parse(data.tool_call?.function?.arguments ?? '{}');
this.validateArgs(toolName, args);
return data;
}
@hook({ lifecycle: 'after_toolcall', priority: 90 })
async logResult(data, ctx) {
await this.logToolUsage({
tool: data.tool_call?.function?.name,
result: data.tool_result,
duration: data.tool_duration,
});
return data;
}
private isRateLimited(_: string) { return false; }
private validateArgs(_: string, __: unknown) {}
private async logToolUsage(_: object) {}
}Tool Pricing
import { Skill, tool, pricing } from 'webagents';
class PaidToolsSkill extends Skill {
readonly name = 'paid-tools';
@pricing({ creditsPerCall: 0.10 })
@tool({ description: 'Call expensive external API' })
async expensiveApiCall(params: { query: string }): Promise<string> {
return await this.callPaidApi(params.query);
}
@pricing({ creditsPerCall: 0.01 })
@tool({ description: 'Execute database query' })
async databaseQuery(params: { sql: string }): Promise<unknown[]> {
return await this.executeSql(params.sql);
}
private async callPaidApi(_: string) { return ''; }
private async executeSql(_: string): Promise<unknown[]> { return []; }
}Tool Patterns
Validation Pattern
@tool({ description: 'Update record with validation' })
async updateRecord(params: { recordId: string; data: Record<string, unknown> }) {
if (!this.validateRecordId(params.recordId)) {
return { error: 'Invalid record ID' };
}
if (!this.validateData(params.data)) {
return { error: 'Invalid data format' };
}
try {
const result = await this.db.update(params.recordId, params.data);
return { success: true, record: result };
} catch (e) {
return { error: (e as Error).message };
}
}Async Pattern
@tool({ description: 'Fetch data from multiple URLs concurrently' })
async fetchData(params: { urls: string[] }): Promise<unknown[]> {
return await Promise.all(params.urls.map((u) => this.fetchUrl(u)));
}Caching Pattern
class CachedToolsSkill extends Skill {
readonly name = 'cached-tools';
private cache = new Map<string, string>();
@tool({ description: 'Cached expensive calculation' })
async expensiveCalculation(params: { input: string }): Promise<string> {
const cached = this.cache.get(params.input);
if (cached) return cached;
const result = await this.performCalculation(params.input);
this.cache.set(params.input, result);
return result;
}
private async performCalculation(_: string) { return ''; }
}Best Practices
- Clear descriptions — help the LLM understand when to use each tool.
- Type hints / schemas — enable accurate schema generation.
- Error handling — return errors as structured data, not exceptions.
- Scope control — use
scope/scopesto gate tool visibility per caller. - Performance — consider caching and concurrent execution.