Der Konversationskontext _chatContexts[agentId] ist eine geteilte List<ChatMessage>. Nur der Lookup lief unter Lock, alle Add-Aufrufe im Schleifenkoerper waren ungeschuetzt. Da WebView, ToolJob-Wakeups und AgentComm denselben Agenten gleichzeitig ansprechen koennen, verschraenkten sich ihre Nachrichten zu einer ungueltigen Tool-Sequenz, die die API mit HTTP 400 ablehnt. ChatAsync laeuft jetzt hinter einem SemaphoreSlim(1,1) pro Agent; verschiedene Agenten bleiben unabhaengig. Die Timeout-Uhr startet erst nach dem Eintritt, damit Wartezeit in der Warteschlange den Lauf nicht aufzehrt. Zwei Folgeprobleme mit demselben Ursprung: - _runningChats hielt nur EINE CancellationTokenSource je Agent; der zweite Lauf ueberschrieb den ersten. AbortChat brach dadurch nur einen ab, der andere lief bis ins Run-Timeout. Jetzt eine Liste, die auch wartende Laeufe erfasst. - ExecuteToolCallAsync fing OperationCanceledException mit ab und gab sie als Tool-Ergebnis zurueck, wodurch der Abbruch erst einen Schritt spaeter griff. Cancellation wird nun durchgereicht. Ausserdem: send_message an den eigenen Agenten wird abgelehnt — es waere mit dem neuen Gate in einen Deadlock gelaufen. Neu: GetChatContext(agentId) als Momentaufnahme des Kontexts, fuer Diagnose und Kontextgroessen-Anzeige. Build-Fix: Das WinForms-Projekt globbt **/*.cs und kompilierte dadurch die Test-Quellen mit. tests\** wird jetzt wie src\** ausgeschlossen. Alle 49 Tests gruen. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
71 lines
2.4 KiB
C#
71 lines
2.4 KiB
C#
using System.Collections.Concurrent;
|
|
using System.Text.Json;
|
|
using ClawdDotNet.Core.Tools;
|
|
|
|
namespace ClawdDotNet.Core.Tests.Infrastructure;
|
|
|
|
/// <summary>
|
|
/// Konfigurierbares Tool für Engine-Tests: liefert feste Ergebnisse, kann verzögern
|
|
/// (um Nebenläufigkeit zu provozieren) oder werfen.
|
|
/// </summary>
|
|
internal sealed class FakeTool : IAgentTool
|
|
{
|
|
private readonly Func<JsonElement, AgentToolContext, CancellationToken, Task<ToolResult>> _handler;
|
|
|
|
public string Name { get; }
|
|
public string Description => $"Test-Tool {Name}";
|
|
|
|
public JsonElement InputSchema { get; } = JsonDocument.Parse("""
|
|
{ "type": "object", "properties": { "action": { "type": "string" } } }
|
|
""").RootElement.Clone();
|
|
|
|
/// <summary>Jeder Aufruf wird mitgeschrieben — inklusive der aufrufenden Agent-Id.</summary>
|
|
public ConcurrentBag<string> Invocations { get; } = new();
|
|
|
|
private FakeTool(string name, Func<JsonElement, AgentToolContext, CancellationToken, Task<ToolResult>> handler)
|
|
{
|
|
Name = name;
|
|
_handler = handler;
|
|
}
|
|
|
|
public static FakeTool Returning(string result, string name = "TestTool")
|
|
=> new(name, (_, _, _) => Task.FromResult(ToolResult.Ok(result)));
|
|
|
|
/// <summary>Verzögert die Ausführung — vergrößert das Zeitfenster für Races.</summary>
|
|
public static FakeTool Slow(TimeSpan delay, string result = "ok", string name = "TestTool")
|
|
=> new(name, async (_, _, ct) =>
|
|
{
|
|
await Task.Delay(delay, ct);
|
|
return ToolResult.Ok(result);
|
|
});
|
|
|
|
public static FakeTool Throwing(Exception ex, string name = "TestTool")
|
|
=> new(name, (_, _, _) => throw ex);
|
|
|
|
public Task<ToolResult> ExecuteAsync(JsonElement input, AgentToolContext context, CancellationToken ct)
|
|
{
|
|
Invocations.Add(context.AgentId);
|
|
return _handler(input, context, ct);
|
|
}
|
|
}
|
|
|
|
internal sealed class InMemoryStateStore : ClawdDotNet.Core.State.IStateStore
|
|
{
|
|
private readonly ConcurrentDictionary<string, string> _values = new();
|
|
|
|
public Task<string?> GetAsync(string key, CancellationToken ct)
|
|
=> Task.FromResult(_values.GetValueOrDefault(key));
|
|
|
|
public Task SetAsync(string key, string value, CancellationToken ct)
|
|
{
|
|
_values[key] = value;
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public Task DeleteAsync(string key, CancellationToken ct)
|
|
{
|
|
_values.TryRemove(key, out _);
|
|
return Task.CompletedTask;
|
|
}
|
|
}
|