> ## Documentation Index
> Fetch the complete documentation index at: https://docs.heylua.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# LuaTool

> Interface for implementing individual AI tool functions

## Overview

`LuaTool` is the interface that all tools must implement. A tool is a single function that the AI can call to accomplish a specific task.

```typescript theme={null}
import { LuaTool } from 'lua-cli';
import { z } from 'zod';

export default class MyTool implements LuaTool {
  name = "my_tool";
  description = "What the tool does";
  inputSchema = z.object({ param: z.string() });
  
  async execute(input: any) {
    return { result: "success" };
  }
}
```

## Interface Definition

```typescript theme={null}
interface LuaTool<TInput extends ZodType = ZodType> {
  name: string;
  description: string;
  inputSchema: TInput;
  execute: (input: z.infer<TInput>) => Promise<any>;
  condition?: () => Promise<boolean>;
}
```

## Required Properties

### name

Unique identifier for the tool.

<ParamField path="name" type="string" required>
  Tool name using only: `a-z`, `A-Z`, `0-9`, `-`, `_`

  **Examples**: `"get_weather"`, `"create-order"`, `"sendEmail123"`

  **Invalid**: `"get weather"`, `"tool.name"`, `"send@email"`
</ParamField>

```typescript theme={null}
// ✅ Good
name = "get_weather";
name = "create-product";
name = "search_items";

// ❌ Bad
name = "get weather";  // No spaces
name = "tool.name";    // No dots
name = "send@email";   // No special chars
```

### description

Clear, concise description of what the tool does.

<ParamField path="description" type="string" required>
  One sentence describing the tool's purpose

  Helps the AI understand when to use this tool
</ParamField>

```typescript theme={null}
// ✅ Good
description = "Get current weather conditions for any city worldwide";
description = "Create a new product in the catalog with price and details";
description = "Search for products by name, category, or description";

// ❌ Bad
description = "Gets data";           // Too vague
description = "Does weather stuff";  // Unclear
```

### inputSchema

Zod schema that validates and types the input.

<ParamField path="inputSchema" type="ZodType" required>
  Zod schema defining valid inputs

  Provides runtime validation and TypeScript types
</ParamField>

```typescript theme={null}
import { z } from 'zod';

// Simple
inputSchema = z.object({
  city: z.string()
});

// With validation
inputSchema = z.object({
  email: z.string().email(),
  age: z.number().min(0).max(120)
});

// With descriptions
inputSchema = z.object({
  city: z.string().describe("City name (e.g., 'London', 'Tokyo')"),
  units: z.enum(['metric', 'imperial']).describe("Temperature units")
});

// With optional and default values
inputSchema = z.object({
  query: z.string(),
  limit: z.number().default(10),
  offset: z.number().optional()
});
```

### execute

Async function that implements the tool's logic.

<ParamField path="execute" type="function" required>
  ```typescript theme={null}
  async execute(input: z.infer<typeof this.inputSchema>): Promise<any>
  ```

  * Input is automatically validated
  * Must return a JSON-serializable value
  * Can throw errors for failures
</ParamField>

## Optional Properties

### condition

Async function that determines if the tool should be available to the AI.

<ParamField path="condition" type="function">
  ```typescript theme={null}
  async condition(): Promise<boolean>
  ```

  * Runs **before** the tool is offered to the AI
  * Return `true` to enable the tool, `false` to hide it
  * If the function throws an error, the tool is disabled (fail-closed)
  * Has access to all Platform APIs (User, Data, Products, etc.)
</ParamField>

Use conditions to dynamically enable/disable tools based on:

* User subscription status (premium features)
* User verification or account status
* Feature flags or A/B testing
* Region-specific functionality
* Time-based access

```typescript theme={null}
import { LuaTool, User } from 'lua-cli';
import { z } from 'zod';

export default class PremiumSearchTool implements LuaTool {
  name = "premium_search";
  description = "Advanced search with filters - premium users only";
  
  inputSchema = z.object({
    query: z.string(),
    filters: z.object({
      minPrice: z.number().optional(),
      maxPrice: z.number().optional()
    }).optional()
  });

  // Only show this tool to premium users
  condition = async () => {
    const user = await User.get();
    return user.data?.isPremium === true;
  };

  async execute(input: z.infer<typeof this.inputSchema>) {
    // This only runs if condition returned true
    return { results: [] };
  }
}
```

<Note>
  **Fail-closed behavior:** If your condition function throws an error or times out (30s), the tool is automatically disabled. This ensures tools aren't accidentally exposed when conditions can't be evaluated.
</Note>

## Implementation Examples

### Simple Tool

```typescript theme={null}
import { LuaTool } from 'lua-cli';
import { z } from 'zod';

export default class GreetTool implements LuaTool {
  name = "greet_user";
  description = "Greet a user by name";
  
  inputSchema = z.object({
    name: z.string()
  });

  async execute(input: z.infer<typeof this.inputSchema>) {
    return {
      message: `Hello, ${input.name}!`,
      timestamp: new Date().toISOString()
    };
  }
}
```

### External API Tool

```typescript theme={null}
import { LuaTool } from 'lua-cli';
import { z } from 'zod';

export default class GetWeatherTool implements LuaTool {
  name = "get_weather";
  description = "Get current weather for a city";
  
  inputSchema = z.object({
    city: z.string().describe("City name"),
    units: z.enum(['metric', 'imperial']).optional().default('metric')
  });

  async execute(input: z.infer<typeof this.inputSchema>) {
    const { city, units } = input;
    
    // Call external API
    const response = await fetch(
      `https://api.weather.com/v1/weather?city=${city}&units=${units}`
    );
    
    if (!response.ok) {
      throw new Error(`Weather API error: ${response.statusText}`);
    }
    
    const data = await response.json();
    
    return {
      city: data.location,
      temperature: data.temp,
      condition: data.condition,
      humidity: data.humidity
    };
  }
}
```

### Platform API Tool

```typescript theme={null}
import { LuaTool, Products } from 'lua-cli';
import { z } from 'zod';

export default class SearchProductsTool implements LuaTool {
  name = "search_products";
  description = "Search for products by name or description";
  
  inputSchema = z.object({
    query: z.string().describe("Search query"),
    limit: z.number().min(1).max(100).default(10)
  });

  async execute(input: z.infer<typeof this.inputSchema>) {
    const results = await Products.search(input.query);
    
    // Take only requested number of results
    const products = results.products.slice(0, input.limit);
    
    return {
      products: products.map(p => ({
        id: p.id,
        name: p.name,
        price: `$${p.price.toFixed(2)}`,
        inStock: p.inStock
      })),
      total: results.length,
      showing: products.length
    };
  }
}
```

### Environment Variables Tool

```typescript theme={null}
import { LuaTool, env } from 'lua-cli';
import { z } from 'zod';

export default class SendEmailTool implements LuaTool {
  name = "send_email";
  description = "Send an email via SendGrid";
  
  inputSchema = z.object({
    to: z.string().email(),
    subject: z.string(),
    body: z.string()
  });

  async execute(input: z.infer<typeof this.inputSchema>) {
    // Get API key from environment
    const apiKey = env('SENDGRID_API_KEY');
    
    if (!apiKey) {
      throw new Error('SENDGRID_API_KEY not configured');
    }
    
    // Send email
    const response = await fetch('https://api.sendgrid.com/v3/mail/send', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${apiKey}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        personalizations: [{ to: [{ email: input.to }] }],
        from: { email: 'noreply@example.com' },
        subject: input.subject,
        content: [{ type: 'text/plain', value: input.body }]
      })
    });
    
    if (!response.ok) {
      throw new Error(`Email failed: ${response.statusText}`);
    }
    
    return {
      success: true,
      message: `Email sent to ${input.to}`
    };
  }
}
```

### Multi-Step Tool

```typescript theme={null}
import { LuaTool, Products, Baskets } from 'lua-cli';
import { z } from 'zod';

export default class QuickCheckoutTool implements LuaTool {
  name = "quick_checkout";
  description = "Search, add to cart, and checkout in one step";
  
  inputSchema = z.object({
    productName: z.string(),
    quantity: z.number().min(1).default(1),
    shippingAddress: z.object({
      street: z.string(),
      city: z.string(),
      zip: z.string()
    })
  });

  async execute(input: z.infer<typeof this.inputSchema>) {
    // Step 1: Search for product
    const products = await Products.search(input.productName);
    if (products.length === 0) {
      throw new Error(`Product not found: ${input.productName}`);
    }
    const product = products.products[0];
    
    // Step 2: Create basket
    const basket = await Baskets.create({ currency: 'USD' });
    
    // Step 3: Add product
    await Baskets.addItem(basket.id, {
      id: product.id,
      price: product.price,
      quantity: input.quantity
    });
    
    // Step 4: Checkout
    const order = await Baskets.placeOrder({
      shippingAddress: input.shippingAddress,
      paymentMethod: 'stripe'
    }, basket.id);
    
    return {
      orderId: order.id,
      product: product.name,
      quantity: input.quantity,
      total: `$${(product.price * input.quantity).toFixed(2)}`,
      message: 'Order created successfully'
    };
  }
}
```

### Conditional Tool (Premium Feature)

```typescript theme={null}
import { LuaTool, User, Products } from 'lua-cli';
import { z } from 'zod';

export default class PremiumAdvancedSearchTool implements LuaTool {
  name = "premium_advanced_search";
  description = "Advanced search with filters and sorting - premium users only";
  
  inputSchema = z.object({
    query: z.string().describe("Search query"),
    filters: z.object({
      category: z.string().optional(),
      minPrice: z.number().optional(),
      maxPrice: z.number().optional()
    }).optional(),
    sortBy: z.enum(["relevance", "price_asc", "price_desc", "newest"]).optional()
  });

  // Condition: Only show to premium users
  condition = async () => {
    const user = await User.get();
    
    // Check subscription status
    const isPremium = user.data?.subscription === "premium" 
                   || user.data?.isPremium === true;
    
    return isPremium;
  };

  async execute(input: z.infer<typeof this.inputSchema>) {
    const { query, filters, sortBy } = input;
    
    const searchResult = await Products.search({ query, limit: 20 });
    let products = searchResult.products;
    
    // Apply price filters
    if (filters?.minPrice !== undefined || filters?.maxPrice !== undefined) {
      products = products.filter(p => {
        if (filters.minPrice && p.price < filters.minPrice) return false;
        if (filters.maxPrice && p.price > filters.maxPrice) return false;
        return true;
      });
    }
    
    return {
      query,
      totalResults: products.length,
      results: products.slice(0, 10),
      sortedBy: sortBy || "relevance"
    };
  }
}
```

## Reusing Tools Across Agents

When multiple agents share the same tool logic — same API, same auth, same shape — but differ on a couple of config values, declare the shared parts on an abstract base and have each agent's tool extend it. The compiler walks the full `extends` chain, so a leaf class is detected as a tool whether it extends `LuaTool` directly or through any number of intermediate bases.

```typescript theme={null}
// Shared base — published once, imported by every agent
import { LuaTool } from 'lua-cli';
import { z } from 'zod';

export abstract class SearchTool implements LuaTool {
  name = 'search';
  description = 'Search the knowledge base.';
  inputSchema = z.object({ query: z.string() });

  // Subclasses override this. The default lets the type-check pass
  // for the abstract base; concrete subclasses must set a real value.
  searchPath: string = '/default/search';

  async execute(input: { query: string }) {
    const r = await fetch(`https://api.example.com${this.searchPath}`, {
      method: 'POST',
      body: JSON.stringify({ q: input.query }),
    });
    return r.json();
  }
}
```

```typescript theme={null}
// Per-agent leaf — only what differs
import { SearchTool } from '@my-org/shared';

export class BlackshipSearchTool extends SearchTool {
  name = 'search_blackship';
  searchPath = '/blk/search';
}
```

Register the leaf class on a skill — pass the class itself, not an instance:

```typescript theme={null}
import { LuaSkill } from 'lua-cli';
import { BlackshipSearchTool } from './tools/BlackshipSearch';

export default new LuaSkill({
  name: 'support',
  description: 'Customer support tools',
  context: 'Use search_blackship to look things up.',
  tools: [BlackshipSearchTool],
});
```

Field initializers on the leaf run during construction, so `this.searchPath` inside the parent's `execute` resolves to `'/blk/search'`. The leaf inherits `inputSchema`, `description`, and `execute` from the parent unless it overrides them.

<Warning>
  Don't pass constructor arguments at the reference site:

  ```typescript theme={null}
  // ❌ The string is silently dropped — one tool artifact is shared across
  //    every reference and there's no carrier for per-reference args.
  tools: [new BlackshipSearchTool('/blk/search')]
  ```

  The compiler reports `lua/constructor-args-dropped` if it sees this. Use a subclass with a field override instead.
</Warning>

## Input Schema Patterns

### Optional Fields

```typescript theme={null}
inputSchema = z.object({
  required: z.string(),
  optional: z.string().optional(),
  withDefault: z.string().default('default value')
});
```

### Validation

```typescript theme={null}
inputSchema = z.object({
  email: z.string().email(),
  age: z.number().min(18).max(120),
  phone: z.string().regex(/^\+?[1-9]\d{1,14}$/),
  url: z.string().url(),
  password: z.string().min(8)
});
```

### Nested Objects

```typescript theme={null}
inputSchema = z.object({
  user: z.object({
    name: z.string(),
    email: z.string().email()
  }),
  preferences: z.object({
    notifications: z.boolean(),
    language: z.string()
  }).optional()
});
```

### Arrays

```typescript theme={null}
inputSchema = z.object({
  items: z.array(z.object({
    id: z.string(),
    quantity: z.number()
  })),
  tags: z.array(z.string()).optional()
});
```

### Enums

```typescript theme={null}
inputSchema = z.object({
  status: z.enum(['pending', 'active', 'completed']),
  priority: z.enum(['low', 'medium', 'high']).default('medium')
});
```

## Error Handling

### Throwing Errors

```typescript theme={null}
async execute(input: any) {
  // Validate business logic
  if (input.amount <= 0) {
    throw new Error("Amount must be positive");
  }
  
  // API errors
  const response = await fetch(url);
  if (!response.ok) {
    throw new Error(`API error: ${response.statusText}`);
  }
  
  // Not found errors
  const item = await findItem(input.id);
  if (!item) {
    throw new Error(`Item not found: ${input.id}`);
  }
  
  return result;
}
```

### Try-Catch Pattern

```typescript theme={null}
async execute(input: any) {
  try {
    const result = await externalService.call(input);
    
    if (!result.success) {
      throw new Error(result.error || 'Operation failed');
    }
    
    return result.data;
  } catch (error) {
    // Add context to errors
    throw new Error(`Failed to process request: ${error.message}`);
  }
}
```

## Return Value Patterns

### Structured Data

```typescript theme={null}
// ✅ Good - Structured
return {
  success: true,
  data: { id, name, price },
  metadata: { timestamp, version }
};

// ❌ Bad - Unstructured string
return "Product created with ID 123";
```

### Lists

```typescript theme={null}
return {
  items: [...],
  total: 100,
  page: 1,
  hasMore: true
};
```

### Status Updates

```typescript theme={null}
return {
  status: 'completed',
  message: 'Order shipped successfully',
  trackingNumber: 'ABC123',
  estimatedDelivery: '2025-10-10'
};
```

## Best Practices

<AccordionGroup>
  <Accordion title="Use Type-Safe Inputs">
    ```typescript theme={null}
    async execute(input: z.infer<typeof this.inputSchema>) {
      // input is fully typed!
      const { city, units } = input;
    }
    ```
  </Accordion>

  <Accordion title="Add Input Descriptions">
    ```typescript theme={null}
    inputSchema = z.object({
      city: z.string().describe("City name (e.g., 'London', 'Tokyo')"),
      units: z.enum(['metric', 'imperial']).describe("Temperature units")
    });
    ```
  </Accordion>

  <Accordion title="Return Structured Data">
    Always return objects, not strings:

    ```typescript theme={null}
    // ✅ Good
    return { temperature: 72, condition: "sunny" };

    // ❌ Bad
    return "The temperature is 72 and sunny";
    ```
  </Accordion>

  <Accordion title="Handle Errors Gracefully">
    Provide helpful error messages:

    ```typescript theme={null}
    if (!apiKey) {
      throw new Error('API_KEY environment variable is required. Set it in .env file.');
    }
    ```
  </Accordion>

  <Accordion title="Keep Tools Focused">
    One tool = one responsibility:

    ```typescript theme={null}
    // ✅ Good - Single purpose
    class CreateProductTool { ... }
    class UpdateProductTool { ... }

    // ❌ Bad - Multiple purposes
    class ProductTool {
      // Does create, update, delete, search...
    }
    ```
  </Accordion>
</AccordionGroup>

## Next Steps

<CardGroup cols={2}>
  <Card title="LuaSkill Class" icon="puzzle-piece" href="/api/luaskill">
    Learn about skills
  </Card>

  <Card title="Platform APIs" icon="server" href="/concepts/platform-apis">
    Use built-in APIs
  </Card>

  <Card title="Tool Examples" icon="layer-group" href="/examples/overview">
    See working examples
  </Card>

  <Card title="Build Your First Skill" icon="hammer" href="/getting-started/first-skill">
    Complete tutorial
  </Card>
</CardGroup>
