using System.Collections.Concurrent; using System.Text.Json; using ClawdDotNet.Core.Tools; namespace ClawdDotNet.Core.Tests.Infrastructure; /// /// Ein Tool, das einen anderen Agenten beauftragt — der Nachbau dessen, was /// AgentComm.send_message und AgentSpawn.spawn_agent tun. /// /// Der Nachbau ist nötig, weil beide Werkzeuge in eigenen Projekten liegen, die /// ClawdDotNet.Core.Tests nicht referenziert. Entscheidend ist ohnehin nur die /// eine Zeile, in der sie sich hier gleichen: Die Delegationskette des eigenen Laufs /// wird weitergereicht. Ein Tool, das sie unterschlägt, macht aus einem Zyklus eine /// neue Beauftragung — genau der Fall, den B8 beschreibt. /// /// Wen ein Agent beauftragt, steht in : agentId → Ziel. /// Wer dort nicht vorkommt, delegiert nicht und antwortet schlicht. /// 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(); /// Was zurückkam — Aufrufer, Ziel und das Ergebnis der Weitergabe. public ConcurrentBag<(string From, string To, bool Delivered, string? Error)> Handoffs { get; } = new(); /// /// Wartezeit, bevor weitergegeben wird. Der Fake-Client antwortet synchron; ohne /// diese Pause läuft ein Chat vollständig durch, bevor der nächste überhaupt /// beginnt, und zwei Ketten können sich nicht überschneiden. /// 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)); // Wie das echte Tool: Eine Ablehnung ist kein Wurf, sondern ein Ergebnis, das // der Agent lesen und auf das er anders reagieren kann. return result.Delivered ? ToolResult.Ok(result.Response ?? "") : ToolResult.Ok(JsonSerializer.Serialize(new { delivered = false, error = result.Error })); } }