using System.Collections.Concurrent; using System.Text.Json; using ClawdDotNet.Core.Tools; namespace ClawdDotNet.Core.Tests.Infrastructure; /// /// A tool that commissions another agent — the replica of what /// AgentComm.send_message and AgentSpawn.spawn_agent do. /// /// The replica is necessary because both tools lie in projects of their own that /// ClawdDotNet.Core.Tests does not reference. All that matters anyway is the /// one line in which they are the same here: the delegation chain of the own run /// is passed on. A tool that suppresses it turns a cycle into a /// new commission — exactly the case B8 describes. /// /// Whom an agent commissions is in : agentId → target. /// Whoever does not appear there does not delegate and simply answers. /// internal sealed class DelegatingTool : IAgentTool { private readonly IReadOnlyDictionary _routes; private readonly bool _spawn; private readonly TimeSpan _delayBeforeHandoff; public string Name => "TestTool"; public string Description => "Beauftragt einen anderen Agenten."; public JsonElement InputSchema { get; } = JsonDocument.Parse(""" { "type": "object", "properties": { "message": { "type": "string" } } } """).RootElement.Clone(); /// What came back — caller, target and the result of the hand-off. public ConcurrentBag<(string From, string To, bool Delivered, string? Error)> Handoffs { get; } = new(); /// /// Waiting time before handing off. The fake client answers synchronously; without /// this pause a chat runs through completely before the next one even /// begins, and two chains cannot overlap. /// public DelegatingTool( IReadOnlyDictionary routes, bool spawn = false, TimeSpan delayBeforeHandoff = default) { _routes = routes; _spawn = spawn; _delayBeforeHandoff = delayBeforeHandoff; } public async Task ExecuteAsync( JsonElement input, AgentToolContext context, CancellationToken ct) { if (!_routes.TryGetValue(context.AgentId, out var target)) return ToolResult.Ok("nichts zu delegieren"); if (_delayBeforeHandoff > TimeSpan.Zero) await Task.Delay(_delayBeforeHandoff, ct); if (_spawn) { var spawned = await context.MessageRouter!.SpawnAgentAsync( context.AgentId, target, "Auftrag", context.InstanceId, context.Delegation, ct); Handoffs.Add((context.AgentId, target, spawned.Started, spawned.Error)); return spawned.Started ? ToolResult.Ok(spawned.FinalMessage ?? "") : ToolResult.Ok(JsonSerializer.Serialize(new { spawned = false, error = spawned.Error })); } var result = await context.MessageRouter!.SendMessageAsync( context.AgentId, target, "Frage", context.InstanceId, context.Delegation, ct); Handoffs.Add((context.AgentId, target, result.Delivered, result.Error)); // Like the real tool: a rejection is not a throw, but a result that // the agent can read and react to differently. return result.Delivered ? ToolResult.Ok(result.Response ?? "") : ToolResult.Ok(JsonSerializer.Serialize(new { delivered = false, error = result.Error })); } }