Skip to content

Tool Usage ​

Tools allow LLMs to interact with your code, APIs, and external systems. Mojentic makes tool integration seamless with automatic execution and result handling.

What are Tools? ​

Tools are functions that LLMs can call to perform actions or retrieve information they can't handle directly:

  • Date/Time Operations: "What's the date next Friday?"
  • API Calls: "What's the weather in Tokyo?"
  • File Operations: "Read the contents of config.json"
  • Calculations: "What's 15% of 2,847?"
  • Database Queries: "How many users registered today?"

Creating a Tool ​

Implement the LlmTool interface or extend BaseTool:

typescript
import { BaseTool, ToolArgs, ToolDescriptor, ToolResult, Ok, Err } from 'mojentic';

export class WeatherTool extends BaseTool {
  async run(args: ToolArgs): Promise<Result<ToolResult, Error>> {
    try {
      const location = args.location as string;

      // Your implementation
      const weather = await fetchWeather(location);

      return Ok({
        location,
        temperature: weather.temp,
        condition: weather.condition
      });
    } catch (error) {
      return Err(new Error(`Weather fetch failed: ${error.message}`));
    }
  }

  descriptor(): ToolDescriptor {
    return {
      type: 'function',
      function: {
        name: 'get_weather',
        description: 'Get current weather for a location',
        parameters: {
          type: 'object',
          properties: {
            location: {
              type: 'string',
              description: 'City name or location'
            }
          },
          required: ['location']
        }
      }
    };
  }
}

Using Tools ​

Basic Usage ​

typescript
import { LlmBroker, OllamaGateway, Message, DateResolverTool, isOk } from 'mojentic';

const gateway = new OllamaGateway();
const broker = new LlmBroker('qwen3:32b', gateway);

// Create tool instances
const tools = [new DateResolverTool()];

// Ask a question that requires the tool
const messages = [
  Message.system('You are a helpful assistant with access to tools.'),
  Message.user('What day of the week is next Friday?')
];

const result = await broker.generate(messages, tools);

if (isOk(result)) {
  console.log(result.value);
  // "Next Friday is December 20, 2025, which is a Saturday."
}

Multiple Tools ​

typescript
const tools = [
  new DateResolverTool(),
  new WeatherTool(),
  new CalculatorTool()
];

const messages = [
  Message.user('What will the weather be like next Monday in Tokyo?')
];

// The LLM will:
// 1. Call DateResolverTool to get next Monday's date
// 2. Call WeatherTool with that date and "Tokyo"
// 3. Respond with the weather information
const result = await broker.generate(messages, tools);

Tool Execution Flow ​

When you provide tools to the broker:

  1. LLM Decides: The LLM determines if it needs to call tools
  2. Tool Call: LLM returns tool call requests with arguments
  3. Execution: Broker executes the requested tools
  4. Result Injection: Results are added to the conversation
  5. Continuation: LLM is called again with tool results
  6. Final Response: LLM provides the final answer
typescript
// User: "What's the date 2 weeks from now?"
//
// Step 1: LLM receives question
// Step 2: LLM requests: resolve_date("in 2 weeks")
// Step 3: Tool returns: {"resolved": "2025-12-25", "day_of_week": "Thursday"}
// Step 4: Result added to conversation
// Step 5: LLM called again with tool result
// Step 6: LLM responds: "Two weeks from now is December 25, 2025, a Thursday."

Built-in Tools ​

DateResolverTool ​

Resolves relative date references:

typescript
import { DateResolverTool } from 'mojentic';

const tool = new DateResolverTool();

// Handles:
// - "next Friday"
// - "tomorrow"
// - "in 2 weeks"
// - "last Monday"

Tool Descriptor ​

The descriptor tells the LLM about your tool:

typescript
{
  type: 'function',
  function: {
    name: 'tool_name',              // Unique identifier
    description: 'What it does',    // Clear description
    parameters: {                   // JSON Schema
      type: 'object',
      properties: {
        arg1: {
          type: 'string',
          description: 'What this arg is for'
        }
      },
      required: ['arg1']
    }
  }
}

Writing Good Descriptions ​

Be specific and clear:

❌ Bad:

typescript
description: 'Gets weather'

✅ Good:

typescript
description: 'Get current weather conditions and temperature for a specific city or location'

Error Handling ​

Returning Errors ​

typescript
async run(args: ToolArgs): Promise<Result<ToolResult, Error>> {
  const location = args.location as string;

  if (!location) {
    return Ok({ error: 'Location is required' });
  }

  try {
    const data = await fetchData(location);
    return Ok(data);
  } catch (error) {
    return Err(new ToolError(
      `Failed to fetch data: ${error.message}`,
      'MyTool'
    ));
  }
}

Handling Tool Failures ​

The broker handles tool failures gracefully:

typescript
// If a tool fails, the broker:
// 1. Logs the error
// 2. Sends error message back to LLM
// 3. Lets LLM try again or respond differently

const result = await broker.generate(messages, tools);
// Even if a tool fails, you get a response

Advanced Patterns ​

Stateful Tools ​

typescript
class DatabaseTool extends BaseTool {
  constructor(private db: Database) {
    super();
  }

  async run(args: ToolArgs): Promise<Result<ToolResult, Error>> {
    const query = args.query as string;
    const results = await this.db.query(query);
    return Ok({ results });
  }

  descriptor(): ToolDescriptor {
    return {
      type: 'function',
      function: {
        name: 'query_database',
        description: 'Execute a database query',
        parameters: {
          type: 'object',
          properties: {
            query: { type: 'string', description: 'SQL query to execute' }
          },
          required: ['query']
        }
      }
    };
  }
}

// Usage with state
const db = await connectDatabase();
const tools = [new DatabaseTool(db)];

Async Operations ​

Tools support async operations naturally:

typescript
class ApiTool extends BaseTool {
  async run(args: ToolArgs): Promise<Result<ToolResult, Error>> {
    // Make API calls
    const data = await fetch(url);
    const json = await data.json();

    // Process results
    const processed = await processData(json);

    return Ok(processed);
  }
}

Tool Chains ​

Tools can call other tools indirectly through the LLM:

typescript
// The LLM can chain tools:
// User: "What's the weather next Friday in Paris?"
//
// 1. LLM calls: resolve_date("next Friday")
// 2. LLM calls: get_weather("Paris", "2025-12-19")
// 3. LLM responds with combined information

Best Practices ​

1. Keep Tools Focused ​

Each tool should do one thing well:

❌ Bad:

typescript
class DataTool {
  // Does too much
  async run(args) {
    if (args.action === 'fetch') { /* ... */ }
    else if (args.action === 'save') { /* ... */ }
    else if (args.action === 'delete') { /* ... */ }
  }
}

✅ Good:

typescript
class FetchDataTool { /* ... */ }
class SaveDataTool { /* ... */ }
class DeleteDataTool { /* ... */ }

2. Validate Input ​

typescript
async run(args: ToolArgs): Promise<Result<ToolResult, Error>> {
  const email = args.email as string;

  if (!email || !email.includes('@')) {
    return Ok({ error: 'Invalid email address' });
  }

  // Proceed with valid input
}

3. Provide Clear Results ​

typescript
// Return structured, useful data
return Ok({
  success: true,
  data: {
    temperature: 22,
    condition: 'sunny',
    humidity: 45
  },
  timestamp: new Date().toISOString()
});

4. Handle Rate Limits ​

typescript
class RateLimitedTool extends BaseTool {
  private lastCall = 0;
  private minInterval = 1000; // 1 second

  async run(args: ToolArgs): Promise<Result<ToolResult, Error>> {
    const now = Date.now();
    if (now - this.lastCall < this.minInterval) {
      return Ok({ error: 'Rate limit exceeded, try again later' });
    }

    this.lastCall = now;
    // Proceed with operation
  }
}

Example: Complete Tool ​

typescript
import { BaseTool, ToolArgs, ToolDescriptor, ToolResult, Ok, Err, ToolError } from 'mojentic';

export class GithubTool extends BaseTool {
  constructor(private apiKey: string) {
    super();
  }

  async run(args: ToolArgs): Promise<Result<ToolResult, Error>> {
    try {
      const owner = args.owner as string;
      const repo = args.repo as string;

      if (!owner || !repo) {
        return Ok({ error: 'Both owner and repo are required' });
      }

      const response = await fetch(
        `https://api.github.com/repos/${owner}/${repo}`,
        {
          headers: {
            'Authorization': `Bearer ${this.apiKey}`,
            'Accept': 'application/vnd.github.v3+json'
          }
        }
      );

      if (!response.ok) {
        return Ok({ error: `GitHub API error: ${response.status}` });
      }

      const data = await response.json();

      return Ok({
        name: data.name,
        description: data.description,
        stars: data.stargazers_count,
        forks: data.forks_count,
        language: data.language,
        url: data.html_url
      });
    } catch (error) {
      return Err(new ToolError(
        `GitHub API call failed: ${error.message}`,
        'GithubTool'
      ));
    }
  }

  descriptor(): ToolDescriptor {
    return {
      type: 'function',
      function: {
        name: 'get_github_repo',
        description: 'Get information about a GitHub repository including stars, forks, and description',
        parameters: {
          type: 'object',
          properties: {
            owner: {
              type: 'string',
              description: 'GitHub username or organization name'
            },
            repo: {
              type: 'string',
              description: 'Repository name'
            }
          },
          required: ['owner', 'repo']
        }
      }
    };
  }
}

// Usage
const tool = new GithubTool(process.env.GITHUB_TOKEN);
const tools = [tool];

const messages = [
  Message.user('Tell me about the microsoft/TypeScript repository')
];

const result = await broker.generate(messages, tools);

See Also ​

Legacy OpenAI-compatible streams preserve provider tool call ids. When an id is missing, the parser assigns a unique id so each call keeps its own result.

Pending streamed tool calls complete when the provider sends finish_reason: "tool_calls" or [DONE]. Each batch is emitted once. A disconnected stream without either marker does not complete its pending calls.

Breaking out of a legacy OpenAI-compatible stream cancels the response body.

Released under the MIT License.