Submit Tool Output

Submit results from tool execution back to the agent.

Basic Usage

Submit Single Output

// After executing a tool
string output = ExecuteTool(toolCall);

// Submit result
await agent.SubmitToolOutputAsync(toolCall.Id, output);

Complete Flow

void Start()
{
    agent.onToolCallStarted.AddListener(OnToolCallStarted);
}

async void OnToolCallStarted(ToolCall toolCall)
{
    // Execute tool
    string output = await ExecuteMyTool(toolCall);
    
    // Submit output
    await agent.SubmitToolOutputAsync(toolCall.Id, output);
}

async UniTask<string> ExecuteMyTool(ToolCall toolCall)
{
    // Your tool logic
    await UniTask.Delay(100);
    return "Tool result";
}

Multiple Tool Outputs

Batch Submission

var outputs = new List<ToolOutput>();

foreach (var toolCall in toolCalls)
{
    string result = await ExecuteTool(toolCall);
    outputs.Add(new ToolOutput
    {
        ToolCallId = toolCall.Id,
        Output = result
    });
}

// Submit all at once
await agent.SubmitToolOutputsAsync(outputs);

Parallel Execution

async UniTask SubmitParallelOutputs(List<ToolCall> toolCalls)
{
    var tasks = toolCalls.Select(async toolCall =>
    {
        string output = await ExecuteTool(toolCall);
        return new ToolOutput
        {
            ToolCallId = toolCall.Id,
            Output = output
        };
    });
    
    var outputs = await UniTask.WhenAll(tasks);
    await agent.SubmitToolOutputsAsync(outputs.ToList());
}

Structured Output

JSON Output

async UniTask<string> GetPlayerStats(ToolCall toolCall)
{
    var stats = new
    {
        health = player.Health,
        level = player.Level,
        position = new
        {
            x = player.transform.position.x,
            y = player.transform.position.y,
            z = player.transform.position.z
        },
        inventory = player.Inventory.GetItems()
    };
    
    return JsonUtility.ToJson(stats);
}

Formatted Text

string FormatWeatherOutput(WeatherData data)
{
    return $@"Weather in {data.Location}:
Temperature: {data.Temperature}°C
Conditions: {data.Conditions}
Humidity: {data.Humidity}%
Wind: {data.WindSpeed} km/h";
}

Error Handling

Submit Errors

async void OnToolCall(ToolCall toolCall)
{
    try
    {
        string output = await ExecuteTool(toolCall);
        await agent.SubmitToolOutputAsync(toolCall.Id, output);
    }
    catch (Exception ex)
    {
        Debug.LogException(ex);
        
        // Submit error as output
        await agent.SubmitToolOutputAsync(
            toolCall.Id,
            $"Error: {ex.Message}"
        );
    }
}

Structured Errors

string CreateErrorOutput(Exception ex)
{
    var error = new
    {
        success = false,
        error = new
        {
            message = ex.Message,
            type = ex.GetType().Name
        }
    };
    
    return JsonUtility.ToJson(error);
}

// Usage
catch (Exception ex)
{
    string errorOutput = CreateErrorOutput(ex);
    await agent.SubmitToolOutputAsync(toolCall.Id, errorOutput);
}

Timeouts

Handle Execution Timeout

async UniTask SubmitWithTimeout(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: Tool execution timed out"
        );
    }
}

Streaming Tool Output

Progressive Updates

async UniTask StreamToolOutput(ToolCall toolCall)
{
    var outputBuilder = new StringBuilder();
    
    await foreach (var chunk in ExecuteStreamingTool(toolCall))
    {
        outputBuilder.Append(chunk);
        
        // Optional: Send intermediate updates
        Debug.Log($"Progress: {chunk}");
    }
    
    // Submit final output
    await agent.SubmitToolOutputAsync(
        toolCall.Id,
        outputBuilder.ToString()
    );
}

async IAsyncEnumerable<string> ExecuteStreamingTool(ToolCall toolCall)
{
    for (int i = 0; i < 10; i++)
    {
        await UniTask.Delay(100);
        yield return $"Chunk {i}\n";
    }
}

Large Outputs

Handle Large Data

async UniTask<string> HandleLargeOutput(ToolCall toolCall)
{
    string fullOutput = await GetLargeData(toolCall);
    
    // Truncate if too large
    const int maxLength = 10000;
    if (fullOutput.Length > maxLength)
    {
        fullOutput = fullOutput.Substring(0, maxLength) + 
                    "\n... (output truncated)";
    }
    
    return fullOutput;
}

File-Based Output

async UniTask<string> SaveToFile(ToolCall toolCall)
{
    string data = await GetLargeData(toolCall);
    
    // Save to file
    string filePath = Path.Combine(
        Application.persistentDataPath,
        $"tool_output_{DateTime.Now:yyyyMMddHHmmss}.txt"
    );
    
    await File.WriteAllTextAsync(filePath, data);
    
    // Return file reference
    return $"Output saved to: {filePath}\nSize: {data.Length} bytes";
}

Validation

Validate Before Submit

async UniTask SubmitValidated(ToolCall toolCall)
{
    string output = await ExecuteTool(toolCall);
    
    if (ValidateOutput(output))
    {
        await agent.SubmitToolOutputAsync(toolCall.Id, output);
    }
    else
    {
        await agent.SubmitToolOutputAsync(
            toolCall.Id,
            "Error: Invalid tool output"
        );
    }
}

bool ValidateOutput(string output)
{
    // Check output is valid
    if (string.IsNullOrEmpty(output))
        return false;
    
    // Check JSON if expected
    try
    {
        JsonUtility.FromJson<object>(output);
        return true;
    }
    catch
    {
        return false;
    }
}

Retry Logic

Retry on Failure

async UniTask SubmitWithRetry(ToolCall toolCall, int maxRetries = 3)
{
    for (int attempt = 0; attempt < maxRetries; attempt++)
    {
        try
        {
            string output = await ExecuteTool(toolCall);
            await agent.SubmitToolOutputAsync(toolCall.Id, output);
            return; // Success
        }
        catch (Exception ex) when (attempt < maxRetries - 1)
        {
            Debug.LogWarning($"Attempt {attempt + 1} failed: {ex.Message}");
            await UniTask.Delay(1000 * (attempt + 1)); // Exponential backoff
        }
    }
    
    // All retries failed
    await agent.SubmitToolOutputAsync(
        toolCall.Id,
        "Error: Tool execution failed after retries"
    );
}

Caching

Cache Tool Results

public class CachedToolExecutor : MonoBehaviour
{
    private Dictionary<string, string> cache = new();
    
    async UniTask<string> ExecuteWithCache(ToolCall toolCall)
    {
        string cacheKey = GetCacheKey(toolCall);
        
        // Check cache
        if (cache.TryGetValue(cacheKey, out string cachedOutput))
        {
            Debug.Log("📦 Using cached result");
            return cachedOutput;
        }
        
        // Execute
        string output = await ExecuteTool(toolCall);
        
        // Cache result
        cache[cacheKey] = output;
        
        return output;
    }
    
    string GetCacheKey(ToolCall toolCall)
    {
        return $"{toolCall.Function.Name}:{toolCall.Function.Arguments}";
    }
}

Logging

Log All Submissions

public class ToolOutputLogger : MonoBehaviour
{
    [SerializeField] private AgentBehaviour agent;
    
    async UniTask SubmitWithLogging(ToolCall toolCall, string output)
    {
        // Log before submit
        Debug.Log($"📤 Submitting output for: {toolCall.Function.Name}");
        Debug.Log($"   Output length: {output.Length} chars");
        Debug.Log($"   Output preview: {GetPreview(output, 100)}");
        
        var stopwatch = System.Diagnostics.Stopwatch.StartNew();
        
        await agent.SubmitToolOutputAsync(toolCall.Id, output);
        
        stopwatch.Stop();
        Debug.Log($"✓ Submitted in {stopwatch.ElapsedMilliseconds}ms");
    }
    
    string GetPreview(string text, int maxLength)
    {
        if (text.Length <= maxLength)
            return text;
        
        return text.Substring(0, maxLength) + "...";
    }
}

Complete Example

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

public class ToolOutputManager : MonoBehaviour
{
    [SerializeField] private AgentBehaviour agent;
    [SerializeField] private int timeoutSeconds = 30;
    [SerializeField] private int maxRetries = 3;
    
    private Dictionary<string, string> cache = new();
    
    void Start()
    {
        agent.onToolCallStarted.AddListener(OnToolCallStarted);
    }
    
    async void OnToolCallStarted(ToolCall toolCall)
    {
        Debug.Log($"🔧 Tool called: {toolCall.Function.Name}");
        
        // Check cache
        string cacheKey = GetCacheKey(toolCall);
        if (cache.TryGetValue(cacheKey, out string cachedOutput))
        {
            Debug.Log("📦 Using cached result");
            await SubmitOutput(toolCall.Id, cachedOutput);
            return;
        }
        
        // Execute with timeout and retry
        await ExecuteAndSubmit(toolCall);
    }
    
    async UniTask ExecuteAndSubmit(ToolCall toolCall)
    {
        for (int attempt = 0; attempt < maxRetries; attempt++)
        {
            var cts = new CancellationTokenSource();
            cts.CancelAfterSlim(TimeSpan.FromSeconds(timeoutSeconds));
            
            try
            {
                // Execute with cancellation
                string output = await ExecuteTool(toolCall, cts.Token);
                
                // Validate
                if (!ValidateOutput(output))
                {
                    throw new Exception("Invalid output format");
                }
                
                // Cache
                string cacheKey = GetCacheKey(toolCall);
                cache[cacheKey] = output;
                
                // Submit
                await SubmitOutput(toolCall.Id, output);
                return; // Success
            }
            catch (OperationCanceledException)
            {
                Debug.LogWarning($"Attempt {attempt + 1}: Timeout");
                if (attempt == maxRetries - 1)
                {
                    await SubmitError(toolCall.Id, "Tool execution timed out");
                }
            }
            catch (Exception ex)
            {
                Debug.LogWarning($"Attempt {attempt + 1}: {ex.Message}");
                if (attempt == maxRetries - 1)
                {
                    await SubmitError(toolCall.Id, ex.Message);
                }
                else
                {
                    await UniTask.Delay(1000 * (attempt + 1)); // Backoff
                }
            }
        }
    }
    
    async UniTask<string> ExecuteTool(ToolCall toolCall, CancellationToken ct)
    {
        return toolCall.Function.Name switch
        {
            "get_weather" => await GetWeather(toolCall, ct),
            "search_files" => await SearchFiles(toolCall, ct),
            "analyze_data" => await AnalyzeData(toolCall, ct),
            _ => "Tool not implemented"
        };
    }
    
    async UniTask<string> GetWeather(ToolCall toolCall, CancellationToken ct)
    {
        await UniTask.Delay(500, cancellationToken: ct);
        
        return JsonUtility.ToJson(new
        {
            location = "Tokyo",
            temperature = 22,
            conditions = "Sunny"
        });
    }
    
    async UniTask<string> SearchFiles(ToolCall toolCall, CancellationToken ct)
    {
        await UniTask.Delay(1000, cancellationToken: ct);
        return "Found 5 files matching query";
    }
    
    async UniTask<string> AnalyzeData(ToolCall toolCall, CancellationToken ct)
    {
        var output = new StringBuilder();
        
        for (int i = 0; i < 10; i++)
        {
            await UniTask.Delay(200, cancellationToken: ct);
            output.AppendLine($"Analyzing chunk {i + 1}/10...");
        }
        
        output.AppendLine("Analysis complete");
        return output.ToString();
    }
    
    async UniTask SubmitOutput(string toolCallId, string output)
    {
        Debug.Log($"📤 Submitting output ({output.Length} chars)");
        
        var stopwatch = System.Diagnostics.Stopwatch.StartNew();
        await agent.SubmitToolOutputAsync(toolCallId, output);
        stopwatch.Stop();
        
        Debug.Log($"✓ Submitted in {stopwatch.ElapsedMilliseconds}ms");
    }
    
    async UniTask SubmitError(string toolCallId, string errorMessage)
    {
        Debug.LogError($"❌ Submitting error: {errorMessage}");
        
        var error = new
        {
            success = false,
            error = errorMessage
        };
        
        await agent.SubmitToolOutputAsync(
            toolCallId,
            JsonUtility.ToJson(error)
        );
    }
    
    bool ValidateOutput(string output)
    {
        if (string.IsNullOrEmpty(output))
            return false;
        
        // Add more validation as needed
        return true;
    }
    
    string GetCacheKey(ToolCall toolCall)
    {
        return $"{toolCall.Function.Name}:{toolCall.Function.Arguments}";
    }
}

Best Practices

1. Always Submit

// ❌ Bad - no output submitted
async void OnToolCall(ToolCall toolCall)
{
    ExecuteTool(toolCall); // Fire and forget
}

// ✅ Good - always submit
async void OnToolCall(ToolCall toolCall)
{
    string output = await ExecuteTool(toolCall);
    await agent.SubmitToolOutputAsync(toolCall.Id, output);
}

2. Handle Errors

// Always submit even on error
try
{
    output = await ExecuteTool(toolCall);
}
catch (Exception ex)
{
    output = $"Error: {ex.Message}";
}
finally
{
    await agent.SubmitToolOutputAsync(toolCall.Id, output);
}

3. Provide Context

// ❌ Bad
return "Done";

// ✅ Good
return "Successfully processed 42 items in 1.5 seconds";

Next Steps

Last updated