Skip to content

Tool Creation Guide

This guide explains the two approaches for creating tools in the Azad system: using the builder pattern or directly defining a ToolSchema object.

Introduction to Tools

Tools in the Azad system are defined with a consistent structure that includes:

  • Name: A unique identifier for the tool
  • Schema: A Zod schema that defines the input parameters
  • Description: A concise explanation of what the tool does
  • Examples: Sample usages of the tool for reference
  • Context: Tool-specific state that persists across invocations
  • Lifecycle methods: Functions that handle initialization, disposal, etc.
  • Execution logic: The core function that implements the tool's behavior

Approach 1: Builder Pattern

The builder pattern provides a fluent API for creating tools. This approach uses method chaining to build up a tool definition step by step.

Example: Creating a Tool with the Builder Pattern

import { tool } from '../utils/tool-builder';
import { z } from 'zod';

// Define the schema
const greetingSchema = z
  .object({
    name: z
      .string()
      .min(1)
      .describe('The name to greet')
      .isStreamable(true),
    language: z
      .enum(['en', 'es', 'fr'])
      .optional()
      .describe('Language for the greeting (default: en)')
      .isStreamable(false),
  })
  .strict();

// Define result interface
interface GreetingResult {
  message: string;
  timestamp: string;
}

// Define context interface
interface GreetingContext {
  count: number;
}

// Create the tool using the builder pattern
const greetingTool = tool
  .name('greeting_tool')
  .input(greetingSchema)
  .description('Generate a personalized greeting message')
  .examples([
    {
      explanation: 'Simple English greeting',
      parameters: {
        name: 'John',
      },
    },
    {
      explanation: 'Spanish greeting',
      parameters: {
        name: 'Maria',
        language: 'es',
      },
    },
  ])
  .output<GreetingResult>()
  .context<GreetingContext>({
    count: 0,
  })
  .onInit((ctx) => {
    console.log('Initializing greeting tool');
  })
  .onDispose((ctx) => {
    console.log(`Tool was used ${ctx.count} times before disposal`);
  })
  .executeTool(async (params, { executionContext, toolContext }) => {
    // Increment usage count
    toolContext.count++;

    const { name, language = 'en' } = params;

    // Generate greeting based on language
    let greeting;
    switch (language) {
      case 'es':
        greeting = `¡Hola, ${name}!`;
        break;
      case 'fr':
        greeting = `Bonjour, ${name}!`;
        break;
      default:
        greeting = `Hello, ${name}!`;
    }

    // Return success response
    return executionContext.createSuccessResponse({
      message: greeting,
      timestamp: new Date().toISOString(),
    });
  })
  .build();

export { greetingTool, greetingSchema };

Builder Pattern Flow

The builder pattern enforces a specific sequence of method calls:

  1. .name() - Set the tool name
  2. .input() - Define the input schema
  3. .description() - Provide a description
  4. .examples() - Add usage examples
  5. .output<T>() - Specify the result type
  6. .context<C>() - Define context (optional)
  7. .onInit(), .onDispose() - Add lifecycle methods (optional)
  8. .executeTool() - Implement the core functionality
  9. .build() - Finalize and create the tool instance

Approach 2: Direct ToolSchema Object

Alternatively, tools can be created by directly defining a ToolSchema object using the createToolSchema function. This approach is more concise and allows for defining the entire tool in a single object.

Example: Creating a Tool with ToolSchema Object

import { z } from 'zod';
import { createToolSchema } from '../utils/tool-schema';

// Define the schema
const greetingSchema = z
  .object({
    name: z
      .string()
      .min(1)
      .describe('The name to greet')
      .isStreamable(true),
    language: z
      .enum(['en', 'es', 'fr'])
      .optional()
      .describe('Language for the greeting (default: en)')
      .isStreamable(false),
  })
  .strict();

// Define result interface
interface GreetingResult {
  message: string;
  timestamp: string;
}

// Define context interface
interface GreetingContext {
  count: number;
}

// Create the tool using the direct object approach
const greetingTool = createToolSchema<
  'greeting_tool',
  typeof greetingSchema,
  GreetingResult,
  GreetingContext
>('greeting_tool', {
  schema: greetingSchema,
  description: 'Generate a personalized greeting message',
  examples: [
    {
      explanation: 'Simple English greeting',
      parameters: {
        name: 'John',
      },
    },
    {
      explanation: 'Spanish greeting',
      parameters: {
        name: 'Maria',
        language: 'es',
      },
    },
  ],
  initialContext: {
    count: 0,
  },
  onInit: (ctx) => {
    console.log('Initializing greeting tool');
  },
  onDispose: (ctx) => {
    console.log(`Tool was used ${ctx.count} times before disposal`);
  },
  execute: async (params, { executionContext, toolContext }) => {
    // Increment usage count
    toolContext.count++;

    const { name, language = 'en' } = params;

    // Generate greeting based on language
    let greeting;
    switch (language) {
      case 'es':
        greeting = `¡Hola, ${name}!`;
        break;
      case 'fr':
        greeting = `Bonjour, ${name}!`;
        break;
      default:
        greeting = `Hello, ${name}!`;
    }

    // Return success response
    return executionContext.createSuccessResponse({
      message: greeting,
      timestamp: new Date().toISOString(),
    });
  },
  executeDirect: async (params) => {
    const { name, language = 'en' } = params;

    // Generate greeting based on language
    let greeting;
    switch (language) {
      case 'es':
        greeting = `¡Hola, ${name}!`;
        break;
      case 'fr':
        greeting = `Bonjour, ${name}!`;
        break;
      default:
        greeting = `Hello, ${name}!`;
    }

    return {
      message: greeting,
      timestamp: new Date().toISOString(),
    };
  }
});

export { greetingTool, greetingSchema };

Comparing the Approaches

Builder Pattern Advantages

  • Guided Creation: The builder enforces a specific sequence of steps
  • Progressive Disclosure: Each method provides a clear focus on one aspect
  • Type Safety: Type inference flows naturally through the chain
  • Readability: The chain of method calls clearly shows the tool's structure

ToolSchema Object Advantages

  • Conciseness: Defines the entire tool in a single object
  • At-a-glance Overview: All tool properties are visible in one place
  • Less Boilerplate: Avoids repeated method chaining
  • Direct Structure: Represents the final structure more closely

When to Use Each Approach

Use the Builder Pattern when:

  • You're new to creating tools and want guidance on the required parts
  • You prefer a step-by-step construction process
  • You're creating a complex tool with many options
  • You value explicit type inference at each step

Use the ToolSchema Object when:

  • You're experienced with creating tools
  • You prefer a more concise definition
  • You want to see the entire tool structure at once
  • You're creating a simple tool with standard behavior

Best Practices

Regardless of the approach you choose, follow these best practices:

  1. Clear Descriptions: Provide clear, concise descriptions for the tool and all parameters
  2. Thorough Examples: Include examples that cover common use cases
  3. Proper Error Handling: Handle errors gracefully in the execute function
  4. Contextual State: Use the context object for persistent state
  5. Cleanup: Implement onDispose to clean up resources
  6. Type Safety: Always define proper types for parameters, results, and context
  7. Parameter Validation: Use Zod to enforce parameter validation

Tool Registration

After creating your tool, register it in the appropriate index file:

// In src/environment/tools/definitions/index.ts
import { greetingTool } from './greeting-tool';

export const agentTools = [
  // ... other tools
  greetingTool,
];

Conclusion

Both approaches to creating tools have their merits. The builder pattern provides a guided, step-by-step process, while the direct ToolSchema object approach offers a more concise definition. Choose the approach that best fits your preferences and the complexity of the tool you're creating.