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 }));
}
}