Tools & Tool Choice

Configure tools and tool execution strategies.

Overview

Tool Choice controls how the agent decides to use tools:

  • Auto - Agent decides when to use tools

  • Required - Agent must use at least one tool

  • None - Agent cannot use tools

  • Specific - Force a specific tool

Tool Choice Modes

agent.ToolChoice = ToolChoice.Auto;

// Agent decides based on context
await agent.SendAsync("What's the weather in Tokyo?");
// → Calls get_weather tool

await agent.SendAsync("Hello!");
// → Responds directly without tools

Required

agent.ToolChoice = ToolChoice.Required;

// Agent MUST use a tool
await agent.SendAsync("Help me");
// → Will use one of the available tools, even if not needed

None

agent.ToolChoice = ToolChoice.None;

// Agent cannot use tools
await agent.SendAsync("What's the weather?");
// → Responds with text, no tool calls

Specific Tool

// Force specific tool
agent.ToolChoice = new ToolChoice("get_weather");

await agent.SendAsync("Hello");
// → Will try to call get_weather even though it's not relevant

Registering Tools

Single Tool

// Register tool executor
agent.RegisterToolExecutor(new WeatherToolExecutor());

// Now agent can call get_weather
await agent.SendAsync("What's the weather in Tokyo?");

Multiple Tools

// Register multiple tools
agent.RegisterToolExecutors(new IToolExecutor[]
{
    new WeatherToolExecutor(),
    new CalculatorToolExecutor(),
    new SearchToolExecutor()
});

Tool Definitions in Settings

// In AgentSettings, add tool definitions
settings.ToolDefinitions.Add(new FunctionToolDefinition
{
    Name = "get_weather",
    Description = "Get current weather for a location",
    Parameters = new
    {
        type = "object",
        properties = new
        {
            location = new
            {
                type = "string",
                description = "City name, e.g. Tokyo"
            },
            unit = new
            {
                type = "string",
                @enum = new[] { "celsius", "fahrenheit" }
            }
        },
        required = new[] { "location" }
    }
});

Tool Choice Strategies

Context-Aware Tool Choice

public class SmartToolChoice : MonoBehaviour
{
    [SerializeField] private AgentBehaviour agent;
    
    public async void SendSmart(string message)
    {
        // Analyze message
        if (NeedsTools(message))
        {
            agent.ToolChoice = ToolChoice.Auto;
        }
        else
        {
            agent.ToolChoice = ToolChoice.None;
        }
        
        await agent.SendAsync(message);
    }
    
    bool NeedsTools(string message)
    {
        string lower = message.ToLower();
        
        // Check for tool keywords
        return lower.Contains("weather") ||
               lower.Contains("calculate") ||
               lower.Contains("search") ||
               lower.Contains("find");
    }
}

Dynamic Tool Choice

public ToolChoice GetToolChoice(string intent)
{
    return intent switch
    {
        "weather" => new ToolChoice("get_weather"),
        "calculation" => new ToolChoice("calculator"),
        "search" => new ToolChoice("web_search"),
        "chat" => ToolChoice.None,
        _ => ToolChoice.Auto
    };
}

// Usage
string intent = ClassifyIntent(message);
agent.ToolChoice = GetToolChoice(intent);
await agent.SendAsync(message);

Built-in Tools

// Enable File Search
settings.EnableFileSearch = true;
settings.FileSearchFileIds = new[] { "file-abc123" };

// Agent can search uploaded files
agent.ToolChoice = ToolChoice.Auto;
await agent.SendAsync("What does the documentation say about Unity?");

Code Interpreter

// Enable Code Interpreter
settings.EnableCodeInterpreter = true;

// Agent can execute Python code
await agent.SendAsync("Calculate the first 10 Fibonacci numbers");
// Enable Web Search (Google/Bing)
settings.EnableWebSearch = true;

// Agent can search the web
await agent.SendAsync("What are the latest Unity features?");

Custom Tools

Define Custom Tool

public class CustomToolExecutor : IToolExecutor
{
    public string ToolName => "custom_tool";
    
    public async UniTask<string> ExecuteAsync(
        string arguments,
        CancellationToken ct = default)
    {
        // Parse arguments
        var args = JsonConvert.DeserializeObject<CustomArgs>(arguments);
        
        // Execute tool logic
        string result = await DoCustomWorkAsync(args);
        
        // Return result as JSON
        return JsonConvert.SerializeObject(new { result });
    }
}

// Register
agent.RegisterToolExecutor(new CustomToolExecutor());

Tool with Parameters

public class ParameterizedTool : IToolExecutor
{
    public string ToolName => "complex_tool";
    
    public class Args
    {
        public string Required { get; set; }
        public string Optional { get; set; }
        public int Number { get; set; }
        public bool Flag { get; set; }
    }
    
    public async UniTask<string> ExecuteAsync(
        string arguments,
        CancellationToken ct = default)
    {
        var args = JsonConvert.DeserializeObject<Args>(arguments);
        
        // Validate required parameters
        if (string.IsNullOrEmpty(args.Required))
        {
            return JsonConvert.SerializeObject(new
            {
                error = "Required parameter missing"
            });
        }
        
        // Execute with parameters
        var result = ProcessWithParams(args);
        
        return JsonConvert.SerializeObject(result);
    }
}

Tool Events

Handle Tool Calls

void Start()
{
    agent.onToolCallStarted.AddListener(OnToolCallStarted);
    agent.onToolCallCompleted.AddListener(OnToolCallCompleted);
    agent.onUnhandledToolCall.AddListener(OnUnhandledToolCall);
}

void OnToolCallStarted(ToolCall toolCall)
{
    Debug.Log($"Tool started: {toolCall.Function.Name}");
    ShowToolIndicator(toolCall.Function.Name);
}

void OnToolCallCompleted(ToolCall toolCall, string output)
{
    Debug.Log($"Tool completed: {toolCall.Function.Name}");
    HideToolIndicator();
}

void OnUnhandledToolCall(ToolCall toolCall)
{
    Debug.LogWarning($"Unhandled tool: {toolCall.Function.Name}");
}

Tool Call Progress

public class ToolProgress : MonoBehaviour
{
    [SerializeField] private AgentBehaviour agent;
    [SerializeField] private TMP_Text statusText;
    
    void Start()
    {
        agent.onToolCallStarted.AddListener(OnToolStarted);
        agent.onToolCallCompleted.AddListener(OnToolCompleted);
    }
    
    void OnToolStarted(ToolCall toolCall)
    {
        statusText.text = $"Using {toolCall.Function.Name}...";
    }
    
    void OnToolCompleted(ToolCall toolCall, string output)
    {
        statusText.text = $"✓ {toolCall.Function.Name} completed";
        
        // Clear after delay
        Invoke(nameof(ClearStatus), 2f);
    }
    
    void ClearStatus()
    {
        statusText.text = "";
    }
}

Tool Timeouts

Configure Timeout

// In AgentBehaviour
agent.SubmitToolOutputTimeoutSeconds = 30;

// If tool doesn't complete in 30 seconds, it will timeout

Handle Timeout

agent.onError.AddListener(OnError);

void OnError(string error)
{
    if (error.Contains("timeout"))
    {
        Debug.LogWarning("Tool execution timed out");
        ShowMessage("Tool took too long to respond");
    }
}

Parallel Tool Calls

Multiple Tools at Once

// Some models can call multiple tools in parallel
agent.ToolChoice = ToolChoice.Auto;

await agent.SendAsync("Get weather for Tokyo, London, and New York");

// Agent may call get_weather three times in parallel:
// - get_weather(location="Tokyo")
// - get_weather(location="London")
// - get_weather(location="New York")

Handle Parallel Calls

void Start()
{
    agent.onResponseCompleted.AddListener(OnResponseCompleted);
}

void OnResponseCompleted(Response response)
{
    if (response.ToolCalls != null && response.ToolCalls.Count > 1)
    {
        Debug.Log($"Parallel tool calls: {response.ToolCalls.Count}");
        
        foreach (var toolCall in response.ToolCalls)
        {
            Debug.Log($"  - {toolCall.Function.Name}");
        }
    }
}

Unhandled Tools

Behavior Options

// Skip unhandled tools
agent.UnhandledToolCallBehaviour = UnhandledToolCallBehaviour.Skip;

// Throw error
agent.UnhandledToolCallBehaviour = UnhandledToolCallBehaviour.ThrowError;

// Ask user for approval/input
agent.UnhandledToolCallBehaviour = UnhandledToolCallBehaviour.AskUser;

Handle Unregistered Tools

agent.onUnhandledToolCall += async (toolCall) =>
{
    Debug.LogWarning($"Unhandled tool: {toolCall.Function.Name}");
    
    // Provide default response
    string output = JsonConvert.SerializeObject(new
    {
        error = $"Tool {toolCall.Function.Name} is not available"
    });
    
    await agent.SubmitToolOutputAsync(toolCall.Id, output);
};

Best Practices

1. Start with Auto

// Let agent decide
agent.ToolChoice = ToolChoice.Auto;

// Agent is smart about when to use tools

2. Provide Clear Tool Descriptions

new FunctionToolDefinition
{
    Name = "get_weather",
    Description = "Get current weather and forecast for a specific location. " +
                 "Returns temperature, conditions, humidity, and wind speed.",
    Parameters = // ...
}

3. Validate Tool Results

public class ValidatedToolExecutor : IToolExecutor
{
    public async UniTask<string> ExecuteAsync(string arguments, CancellationToken ct)
    {
        try
        {
            var result = await ExecuteToolLogic(arguments);
            
            // Validate result
            if (IsValidResult(result))
            {
                return JsonConvert.SerializeObject(result);
            }
            else
            {
                return JsonConvert.SerializeObject(new
                {
                    error = "Invalid tool result"
                });
            }
        }
        catch (Exception ex)
        {
            return JsonConvert.SerializeObject(new
            {
                error = ex.Message
            });
        }
    }
}

Complete Example

using UnityEngine;
using Glitch9.AIDevKit.Agents;
using System.Collections.Generic;

public class ToolManager : MonoBehaviour
{
    [SerializeField] private AgentBehaviour agent;
    
    [Header("Tool Settings")]
    [SerializeField] private bool allowTools = true;
    [SerializeField] private ToolChoiceMode defaultMode = ToolChoiceMode.Auto;
    
    void Start()
    {
        // Register tools
        RegisterTools();
        
        // Configure tool choice
        ConfigureToolChoice();
        
        // Setup events
        agent.onToolCallStarted.AddListener(OnToolStarted);
        agent.onToolCallCompleted.AddListener(OnToolCompleted);
        agent.onUnhandledToolCall.AddListener(OnUnhandledTool);
    }
    
    void RegisterTools()
    {
        var tools = new List<IToolExecutor>
        {
            new WeatherToolExecutor(),
            new CalculatorToolExecutor(),
            new SearchToolExecutor()
        };
        
        agent.RegisterToolExecutors(tools);
        
        Debug.Log($"✓ Registered {tools.Count} tools");
    }
    
    void ConfigureToolChoice()
    {
        if (!allowTools)
        {
            agent.ToolChoice = ToolChoice.None;
            return;
        }
        
        agent.ToolChoice = defaultMode switch
        {
            ToolChoiceMode.Auto => ToolChoice.Auto,
            ToolChoiceMode.Required => ToolChoice.Required,
            ToolChoiceMode.None => ToolChoice.None,
            _ => ToolChoice.Auto
        };
    }
    
    void OnToolStarted(ToolCall toolCall)
    {
        Debug.Log($"🔧 Tool: {toolCall.Function.Name}");
        Debug.Log($"   Args: {toolCall.Function.Arguments}");
    }
    
    void OnToolCompleted(ToolCall toolCall, string output)
    {
        Debug.Log($"✓ Tool completed: {toolCall.Function.Name}");
        Debug.Log($"   Output: {output.Substring(0, System.Math.Min(100, output.Length))}...");
    }
    
    void OnUnhandledTool(ToolCall toolCall)
    {
        Debug.LogWarning($"⚠ Unhandled tool: {toolCall.Function.Name}");
    }
    
    enum ToolChoiceMode
    {
        Auto,
        Required,
        None
    }
}

Next Steps

Last updated