Robutler

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:

  1. Skill tools — defined in skills using the @tool decorator.
  2. 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 @http decorator 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.provides for 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

  1. Clear descriptions — help the LLM understand when to use each tool.
  2. Type hints / schemas — enable accurate schema generation.
  3. Error handling — return errors as structured data, not exceptions.
  4. Scope control — use scope / scopes to gate tool visibility per caller.
  5. Performance — consider caching and concurrent execution.

On this page