Unhandled Tool Calls

Handle tool calls that don't have registered executors.

Overview

When an agent calls a tool that:

  • Doesn't have a registered executor

  • Requires manual approval

  • Needs user intervention

You can handle these "unhandled" tool calls.

Basic Handling

Event Listener

void Start()
{
    agent.onUnhandledToolCall.AddListener(OnUnhandledToolCall);
}

void OnUnhandledToolCall(ToolCall toolCall)
{
    Debug.LogWarning($"Unhandled tool: {toolCall.Function.Name}");
    Debug.Log($"Arguments: {toolCall.Function.Arguments}");
    
    // Show UI to user
    ShowToolApprovalDialog(toolCall);
}

Manual Tool Execution

Execute and Submit

async void OnUnhandledToolCall(ToolCall toolCall)
{
    // Execute manually
    string output = await ExecuteManually(toolCall);
    
    // Submit result
    await agent.SubmitToolOutputAsync(toolCall.Id, output);
}

async UniTask<string> ExecuteManually(ToolCall toolCall)
{
    switch (toolCall.Function.Name)
    {
        case "purchase_item":
            return await HandlePurchase(toolCall);
            
        case "send_message":
            return await HandleMessage(toolCall);
            
        default:
            return "Tool not supported";
    }
}

User Approval Flow

Approval Dialog

public class ToolApprovalDialog : MonoBehaviour
{
    [SerializeField] private AgentBehaviour agent;
    [SerializeField] private GameObject dialogPanel;
    [SerializeField] private TMP_Text toolNameText;
    [SerializeField] private TMP_Text toolDescriptionText;
    [SerializeField] private Button approveButton;
    [SerializeField] private Button denyButton;
    
    private ToolCall currentToolCall;
    
    void Start()
    {
        agent.onUnhandledToolCall.AddListener(OnUnhandledToolCall);
        
        approveButton.onClick.AddListener(OnApprove);
        denyButton.onClick.AddListener(OnDeny);
    }
    
    void OnUnhandledToolCall(ToolCall toolCall)
    {
        currentToolCall = toolCall;
        
        // Parse arguments
        var args = ParseArguments(toolCall);
        
        // Show dialog
        toolNameText.text = GetFriendlyName(toolCall.Function.Name);
        toolDescriptionText.text = GetDescription(toolCall.Function.Name, args);
        dialogPanel.SetActive(true);
    }
    
    async void OnApprove()
    {
        dialogPanel.SetActive(false);
        
        try
        {
            // Execute tool
            string output = await ExecuteTool(currentToolCall);
            
            // Submit output
            await agent.SubmitToolOutputAsync(currentToolCall.Id, output);
            
            ShowMessage("Action completed");
        }
        catch (Exception ex)
        {
            Debug.LogException(ex);
            await agent.SubmitToolOutputAsync(currentToolCall.Id, $"Error: {ex.Message}");
        }
    }
    
    void OnDeny()
    {
        dialogPanel.SetActive(false);
        
        // Submit rejection
        agent.SubmitToolOutputAsync(currentToolCall.Id, "User denied the action");
    }
    
    Dictionary<string, object> ParseArguments(ToolCall toolCall)
    {
        return JsonUtility.FromJson<Dictionary<string, object>>(
            toolCall.Function.Arguments
        );
    }
    
    string GetFriendlyName(string toolName)
    {
        return toolName switch
        {
            "purchase_item" => "Purchase Item",
            "send_message" => "Send Message",
            "delete_data" => "Delete Data",
            _ => toolName
        };
    }
    
    string GetDescription(string toolName, Dictionary<string, object> args)
    {
        return toolName switch
        {
            "purchase_item" => $"Purchase {args["item"]} for ${args["price"]}?",
            "send_message" => $"Send message to {args["recipient"]}?",
            "delete_data" => $"Delete {args["count"]} items?",
            _ => "Approve this action?"
        };
    }
    
    async UniTask<string> ExecuteTool(ToolCall toolCall)
    {
        return toolCall.Function.Name switch
        {
            "purchase_item" => await PurchaseItem(toolCall),
            "send_message" => await SendMessage(toolCall),
            "delete_data" => await DeleteData(toolCall),
            _ => "Unknown tool"
        };
    }
    
    async UniTask<string> PurchaseItem(ToolCall toolCall)
    {
        var args = JsonUtility.FromJson<PurchaseArgs>(toolCall.Function.Arguments);
        
        // Purchase logic
        await UniTask.Delay(500);
        
        return $"Purchased {args.item} for ${args.price}";
    }
    
    async UniTask<string> SendMessage(ToolCall toolCall)
    {
        var args = JsonUtility.FromJson<MessageArgs>(toolCall.Function.Arguments);
        
        // Send logic
        await UniTask.Delay(300);
        
        return $"Message sent to {args.recipient}";
    }
    
    async UniTask<string> DeleteData(ToolCall toolCall)
    {
        var args = JsonUtility.FromJson<DeleteArgs>(toolCall.Function.Arguments);
        
        // Delete logic
        await UniTask.Delay(200);
        
        return $"Deleted {args.count} items";
    }
    
    [System.Serializable]
    class PurchaseArgs
    {
        public string item;
        public float price;
    }
    
    [System.Serializable]
    class MessageArgs
    {
        public string recipient;
        public string message;
    }
    
    [System.Serializable]
    class DeleteArgs
    {
        public int count;
    }
}

Dangerous Operations

Require Confirmation

public class DangerousToolHandler : MonoBehaviour
{
    [SerializeField] private AgentBehaviour agent;
    
    private readonly HashSet<string> dangerousTools = new()
    {
        "delete_account",
        "reset_data",
        "format_drive",
        "send_email_all"
    };
    
    void Start()
    {
        agent.onUnhandledToolCall.AddListener(OnUnhandledToolCall);
    }
    
    async void OnUnhandledToolCall(ToolCall toolCall)
    {
        if (dangerousTools.Contains(toolCall.Function.Name))
        {
            bool confirmed = await ShowDangerConfirmation(toolCall);
            
            if (confirmed)
            {
                string output = await ExecuteDangerousTool(toolCall);
                await agent.SubmitToolOutputAsync(toolCall.Id, output);
            }
            else
            {
                await agent.SubmitToolOutputAsync(
                    toolCall.Id,
                    "Operation canceled by user"
                );
            }
        }
        else
        {
            // Handle normally
            string output = await ExecuteTool(toolCall);
            await agent.SubmitToolOutputAsync(toolCall.Id, output);
        }
    }
    
    async UniTask<bool> ShowDangerConfirmation(ToolCall toolCall)
    {
        // Show warning dialog
        Debug.LogWarning($"⚠️ Dangerous operation: {toolCall.Function.Name}");
        
        // For example purposes, always return false
        // In real app, show UI and wait for user input
        await UniTask.Delay(100);
        return false;
    }
    
    async UniTask<string> ExecuteDangerousTool(ToolCall toolCall)
    {
        Debug.Log($"🔥 Executing dangerous tool: {toolCall.Function.Name}");
        await UniTask.Delay(1000);
        return "Operation completed (with caution)";
    }
    
    async UniTask<string> ExecuteTool(ToolCall toolCall)
    {
        await UniTask.Delay(100);
        return "Tool executed";
    }
}

Rate Limiting

Limit Tool Usage

public class RateLimitedHandler : MonoBehaviour
{
    [SerializeField] private AgentBehaviour agent;
    [SerializeField] private int maxCallsPerMinute = 10;
    
    private Dictionary<string, Queue<DateTime>> callHistory = new();
    
    void Start()
    {
        agent.onUnhandledToolCall.AddListener(OnUnhandledToolCall);
    }
    
    async void OnUnhandledToolCall(ToolCall toolCall)
    {
        if (IsRateLimited(toolCall.Function.Name))
        {
            await agent.SubmitToolOutputAsync(
                toolCall.Id,
                "Error: Rate limit exceeded. Please try again later."
            );
            return;
        }
        
        RecordCall(toolCall.Function.Name);
        
        string output = await ExecuteTool(toolCall);
        await agent.SubmitToolOutputAsync(toolCall.Id, output);
    }
    
    bool IsRateLimited(string toolName)
    {
        if (!callHistory.ContainsKey(toolName))
        {
            callHistory[toolName] = new Queue<DateTime>();
        }
        
        var history = callHistory[toolName];
        var cutoff = DateTime.Now.AddMinutes(-1);
        
        // Remove old calls
        while (history.Count > 0 && history.Peek() < cutoff)
        {
            history.Dequeue();
        }
        
        return history.Count >= maxCallsPerMinute;
    }
    
    void RecordCall(string toolName)
    {
        if (!callHistory.ContainsKey(toolName))
        {
            callHistory[toolName] = new Queue<DateTime>();
        }
        
        callHistory[toolName].Enqueue(DateTime.Now);
    }
    
    async UniTask<string> ExecuteTool(ToolCall toolCall)
    {
        await UniTask.Delay(100);
        return "Executed";
    }
}

Logging & Monitoring

Log All Unhandled Calls

public class ToolCallLogger : MonoBehaviour
{
    [SerializeField] private AgentBehaviour agent;
    [SerializeField] private string logFilePath = "tool_calls.log";
    
    void Start()
    {
        agent.onUnhandledToolCall.AddListener(OnUnhandledToolCall);
    }
    
    async void OnUnhandledToolCall(ToolCall toolCall)
    {
        // Log to file
        LogToFile(toolCall);
        
        // Log to console
        Debug.Log($"📝 Unhandled: {toolCall.Function.Name}");
        Debug.Log($"   Arguments: {toolCall.Function.Arguments}");
        
        // Execute
        string output = await ExecuteTool(toolCall);
        
        // Log result
        LogResult(toolCall, output);
        
        // Submit
        await agent.SubmitToolOutputAsync(toolCall.Id, output);
    }
    
    void LogToFile(ToolCall toolCall)
    {
        string log = $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] " +
                    $"Tool: {toolCall.Function.Name}, " +
                    $"Args: {toolCall.Function.Arguments}\n";
        
        File.AppendAllText(logFilePath, log);
    }
    
    void LogResult(ToolCall toolCall, string output)
    {
        string log = $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] " +
                    $"Result for {toolCall.Function.Name}: {output}\n";
        
        File.AppendAllText(logFilePath, log);
    }
    
    async UniTask<string> ExecuteTool(ToolCall toolCall)
    {
        await UniTask.Delay(100);
        return "Success";
    }
}

Fallback Handling

Default Behavior

public class FallbackHandler : MonoBehaviour
{
    [SerializeField] private AgentBehaviour agent;
    
    void Start()
    {
        agent.onUnhandledToolCall.AddListener(OnUnhandledToolCall);
    }
    
    async void OnUnhandledToolCall(ToolCall toolCall)
    {
        Debug.LogWarning($"Unhandled tool: {toolCall.Function.Name}");
        
        // Try common patterns
        string output = toolCall.Function.Name switch
        {
            var name when name.StartsWith("get_") => await HandleGet(toolCall),
            var name when name.StartsWith("set_") => await HandleSet(toolCall),
            var name when name.StartsWith("create_") => await HandleCreate(toolCall),
            var name when name.StartsWith("delete_") => await HandleDelete(toolCall),
            _ => await HandleUnknown(toolCall)
        };
        
        await agent.SubmitToolOutputAsync(toolCall.Id, output);
    }
    
    async UniTask<string> HandleGet(ToolCall toolCall)
    {
        Debug.Log($"GET operation: {toolCall.Function.Name}");
        await UniTask.Delay(100);
        return "Data retrieved";
    }
    
    async UniTask<string> HandleSet(ToolCall toolCall)
    {
        Debug.Log($"SET operation: {toolCall.Function.Name}");
        await UniTask.Delay(100);
        return "Data updated";
    }
    
    async UniTask<string> HandleCreate(ToolCall toolCall)
    {
        Debug.Log($"CREATE operation: {toolCall.Function.Name}");
        await UniTask.Delay(100);
        return "Item created";
    }
    
    async UniTask<string> HandleDelete(ToolCall toolCall)
    {
        Debug.Log($"DELETE operation: {toolCall.Function.Name}");
        await UniTask.Delay(100);
        return "Item deleted";
    }
    
    async UniTask<string> HandleUnknown(ToolCall toolCall)
    {
        Debug.LogError($"Unknown tool pattern: {toolCall.Function.Name}");
        await UniTask.Delay(10);
        return $"Error: Tool '{toolCall.Function.Name}' not supported";
    }
}

Best Practices

1. Always Submit Output

async void OnUnhandledToolCall(ToolCall toolCall)
{
    try
    {
        string output = await ExecuteTool(toolCall);
        await agent.SubmitToolOutputAsync(toolCall.Id, output);
    }
    catch (Exception ex)
    {
        // Still submit even on error
        await agent.SubmitToolOutputAsync(
            toolCall.Id,
            $"Error: {ex.Message}"
        );
    }
}

2. Provide Clear Feedback

string output = toolCall.Function.Name switch
{
    "unknown_tool" => "Error: This tool is not available",
    "unauthorized" => "Error: You don't have permission",
    "rate_limited" => "Error: Too many requests, please wait",
    _ => "Error: Unknown error"
};

3. Handle Timeouts

async void OnUnhandledToolCall(ToolCall toolCall)
{
    var cts = new CancellationTokenSource();
    cts.CancelAfterSlim(TimeSpan.FromSeconds(30));
    
    try
    {
        string output = await ExecuteTool(toolCall, cts.Token);
        await agent.SubmitToolOutputAsync(toolCall.Id, output);
    }
    catch (OperationCanceledException)
    {
        await agent.SubmitToolOutputAsync(
            toolCall.Id,
            "Error: Operation timed out"
        );
    }
}

Complete Example

using UnityEngine;
using Glitch9.AIDevKit.Agents;
using Cysharp.Threading.Tasks;

public class UnhandledToolManager : MonoBehaviour
{
    [SerializeField] private AgentBehaviour agent;
    [SerializeField] private GameObject approvalDialogPrefab;
    
    private Queue<ToolCall> pendingTools = new();
    private bool processingTool = false;
    
    void Start()
    {
        agent.onUnhandledToolCall.AddListener(OnUnhandledToolCall);
    }
    
    void OnUnhandledToolCall(ToolCall toolCall)
    {
        Debug.Log($"📋 Unhandled tool queued: {toolCall.Function.Name}");
        pendingTools.Enqueue(toolCall);
        
        if (!processingTool)
        {
            ProcessNextTool();
        }
    }
    
    async void ProcessNextTool()
    {
        if (pendingTools.Count == 0)
        {
            processingTool = false;
            return;
        }
        
        processingTool = true;
        var toolCall = pendingTools.Dequeue();
        
        Debug.Log($"🔧 Processing: {toolCall.Function.Name}");
        
        try
        {
            string output;
            
            if (RequiresApproval(toolCall))
            {
                output = await RequestApproval(toolCall);
            }
            else
            {
                output = await ExecuteAutomatically(toolCall);
            }
            
            await agent.SubmitToolOutputAsync(toolCall.Id, output);
            Debug.Log($"✓ Completed: {toolCall.Function.Name}");
        }
        catch (Exception ex)
        {
            Debug.LogException(ex);
            await agent.SubmitToolOutputAsync(
                toolCall.Id,
                $"Error: {ex.Message}"
            );
        }
        
        // Process next
        ProcessNextTool();
    }
    
    bool RequiresApproval(ToolCall toolCall)
    {
        string[] approvalRequired = {
            "purchase_item",
            "send_message",
            "delete_data",
            "share_location"
        };
        
        return System.Array.Exists(approvalRequired, 
            name => name == toolCall.Function.Name);
    }
    
    async UniTask<string> RequestApproval(ToolCall toolCall)
    {
        Debug.Log($"🔔 Requesting approval for: {toolCall.Function.Name}");
        
        // Show approval dialog
        var dialog = Instantiate(approvalDialogPrefab);
        var handler = dialog.GetComponent<ToolApprovalDialog>();
        
        bool approved = await handler.ShowAndWaitAsync(toolCall);
        
        if (approved)
        {
            return await ExecuteTool(toolCall);
        }
        else
        {
            return "User denied the operation";
        }
    }
    
    async UniTask<string> ExecuteAutomatically(ToolCall toolCall)
    {
        Debug.Log($"⚡ Auto-executing: {toolCall.Function.Name}");
        return await ExecuteTool(toolCall);
    }
    
    async UniTask<string> ExecuteTool(ToolCall toolCall)
    {
        return toolCall.Function.Name switch
        {
            "get_data" => GetData(toolCall),
            "set_data" => SetData(toolCall),
            "purchase_item" => await PurchaseItem(toolCall),
            _ => "Tool not implemented"
        };
    }
    
    string GetData(ToolCall toolCall)
    {
        return "Data: { ... }";
    }
    
    string SetData(ToolCall toolCall)
    {
        return "Data updated";
    }
    
    async UniTask<string> PurchaseItem(ToolCall toolCall)
    {
        await UniTask.Delay(500);
        return "Purchase completed";
    }
}

Next Steps

Last updated