Import des bestehenden Projektstands in Git. - .NET 10 WinForms Anwendung (Multi-Agent / Tool-System) - .gitignore fuer Build-Artefakte, Secrets und Runtime-Daten ergaenzt Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
701 lines
26 KiB
C#
701 lines
26 KiB
C#
using System.Diagnostics;
|
||
using System.Text.Json;
|
||
using ClawdDotNet.Core.Api;
|
||
using ClawdDotNet.Core.Api.Models;
|
||
using ClawdDotNet.Core.Config;
|
||
using ClawdDotNet.Core.Security;
|
||
using ClawdDotNet.Core.Tools;
|
||
using ClawdDotNet.Core.State;
|
||
using Microsoft.Extensions.Logging;
|
||
|
||
namespace ClawdDotNet.Core.Engine;
|
||
|
||
public sealed class AgentEngine : IAgentMessageRouter
|
||
{
|
||
private readonly OpenRouterClient _client;
|
||
private readonly ToolRegistry _toolRegistry;
|
||
private readonly PermissionGate _permissionGate;
|
||
private readonly IStateStore _stateStore;
|
||
private readonly ILoggerFactory _loggerFactory;
|
||
private readonly ContextCompactor _compactor;
|
||
|
||
private readonly Dictionary<string, List<ChatEntry>> _chatHistories = new();
|
||
private readonly Dictionary<string, List<ChatMessage>> _chatContexts = new();
|
||
private readonly Dictionary<string, CancellationTokenSource> _runningChats = new();
|
||
private readonly Lock _lock = new();
|
||
|
||
private Func<IReadOnlyList<AgentConfig>>? _agentConfigProvider;
|
||
private Func<string, string?>? _agentDirResolver;
|
||
private string _instanceId = "";
|
||
|
||
private static readonly JsonSerializerOptions _jsonOpts = new()
|
||
{
|
||
WriteIndented = true,
|
||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||
};
|
||
|
||
public AgentEngine(
|
||
OpenRouterClient client,
|
||
ToolRegistry toolRegistry,
|
||
PermissionGate permissionGate,
|
||
IStateStore stateStore,
|
||
ILoggerFactory loggerFactory)
|
||
{
|
||
_client = client;
|
||
_toolRegistry = toolRegistry;
|
||
_permissionGate = permissionGate;
|
||
_stateStore = stateStore;
|
||
_loggerFactory = loggerFactory;
|
||
_compactor = new ContextCompactor(client, loggerFactory);
|
||
}
|
||
|
||
public event Action<string, string>? OnStepCompleted;
|
||
|
||
/// <summary>
|
||
/// Wird ausgelöst wenn ein neuer ChatEntry hinzugefügt wird (agentId, role, content, source).
|
||
/// Erlaubt der UI, Nachrichten aus Hintergrund-Runs (ToolJobs, AgentComm) live anzuzeigen.
|
||
/// Source gibt an, woher die Nachricht kam (webview, telegram, agentcomm, job, null).
|
||
/// </summary>
|
||
public event Action<string, string, string, string?>? OnChatEntryAdded;
|
||
|
||
/// <summary>
|
||
/// Wird nach jedem abgeschlossenen Run/Chat ausgelöst (model, result).
|
||
/// Erlaubt der UI, Token-Verbrauch und Kosten für ALLE Runs zu tracken.
|
||
/// </summary>
|
||
public event Action<string, AgentRunResult>? OnRunCompleted;
|
||
|
||
public async Task<AgentRunResult> RunAsync(
|
||
AgentConfig agentConfig,
|
||
string userMessage,
|
||
string instanceId,
|
||
CancellationToken externalCt)
|
||
{
|
||
var logger = _loggerFactory.CreateLogger($"ClawdDotNet.Core.Engine.{agentConfig.AgentId}");
|
||
var loopGuard = new LoopGuard(agentConfig.LoopGuard);
|
||
var sw = Stopwatch.StartNew();
|
||
|
||
using var timeoutCts = new CancellationTokenSource(agentConfig.LoopGuard.Timeout);
|
||
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(externalCt, timeoutCts.Token);
|
||
var ct = linkedCts.Token;
|
||
|
||
logger.LogInformation("Agent run started: {AgentId}, model={Model}",
|
||
agentConfig.AgentId, agentConfig.Model);
|
||
|
||
try
|
||
{
|
||
var tools = _toolRegistry.GetForAgent(agentConfig);
|
||
var toolDefinitions = BuildToolDefinitions(tools);
|
||
|
||
var messages = new List<ChatMessage>();
|
||
|
||
var systemPrompt = agentConfig.FullSystemPrompt;
|
||
if (!string.IsNullOrWhiteSpace(systemPrompt))
|
||
messages.Add(ChatMessage.System(systemPrompt));
|
||
|
||
messages.Add(ChatMessage.User(userMessage));
|
||
|
||
string? finalMessage = null;
|
||
var totalTokens = 0;
|
||
|
||
while (true)
|
||
{
|
||
ct.ThrowIfCancellationRequested();
|
||
loopGuard.RecordStep();
|
||
|
||
var request = new ChatRequest
|
||
{
|
||
Model = agentConfig.Model,
|
||
Messages = messages,
|
||
Tools = toolDefinitions.Count > 0 ? toolDefinitions : null
|
||
};
|
||
|
||
var response = await _client.CompleteAsync(request, ct);
|
||
|
||
var promptTokens = 0;
|
||
if (response.Usage is not null)
|
||
{
|
||
totalTokens += response.Usage.TotalTokens;
|
||
promptTokens = response.Usage.PromptTokens;
|
||
loopGuard.RecordTokens(response.Usage.TotalTokens);
|
||
}
|
||
|
||
// Context-Compaction auch in RunAsync – verhindert Token-Explosion bei komplexen Analysen
|
||
var compacted = await _compactor.CompactIfNeededAsync(
|
||
messages, promptTokens, agentConfig.LoopGuard, agentConfig.Model, ct);
|
||
if (compacted)
|
||
{
|
||
OnStepCompleted?.Invoke(agentConfig.AgentId, "Context kompaktiert (RunAsync)");
|
||
}
|
||
|
||
var choice = response.Choices.FirstOrDefault();
|
||
if (choice is null)
|
||
{
|
||
finalMessage = "[No response from model]";
|
||
break;
|
||
}
|
||
|
||
var assistantMessage = choice.Message;
|
||
|
||
if (assistantMessage.ToolCalls is { Count: > 0 })
|
||
{
|
||
messages.Add(ChatMessage.AssistantWithToolCalls(assistantMessage.ToolCalls));
|
||
|
||
foreach (var toolCall in assistantMessage.ToolCalls)
|
||
{
|
||
var toolResult = await ExecuteToolCallAsync(
|
||
toolCall, agentConfig, instanceId, tools, logger, ct);
|
||
|
||
messages.Add(ChatMessage.ToolResponse(toolCall.Id, toolResult));
|
||
}
|
||
|
||
OnStepCompleted?.Invoke(agentConfig.AgentId,
|
||
$"Step {loopGuard.Steps}: {assistantMessage.ToolCalls.Count} tool call(s) executed");
|
||
}
|
||
else
|
||
{
|
||
finalMessage = assistantMessage.Content ?? "";
|
||
break;
|
||
}
|
||
}
|
||
|
||
sw.Stop();
|
||
logger.LogInformation(
|
||
"Agent run completed: {AgentId}, steps={Steps}, tokens={Tokens}, duration={Duration}ms",
|
||
agentConfig.AgentId, loopGuard.Steps, totalTokens, sw.ElapsedMilliseconds);
|
||
|
||
var result = new AgentRunResult(
|
||
agentConfig.AgentId,
|
||
AgentRunStatus.Completed,
|
||
finalMessage,
|
||
loopGuard.Steps,
|
||
totalTokens,
|
||
sw.Elapsed);
|
||
OnRunCompleted?.Invoke(agentConfig.Model, result);
|
||
return result;
|
||
}
|
||
catch (OperationCanceledException) when (timeoutCts.IsCancellationRequested)
|
||
{
|
||
sw.Stop();
|
||
logger.LogWarning("Agent run timed out: {AgentId} after {Duration}ms",
|
||
agentConfig.AgentId, sw.ElapsedMilliseconds);
|
||
|
||
var result = new AgentRunResult(
|
||
agentConfig.AgentId, AgentRunStatus.Cancelled, null,
|
||
loopGuard.Steps, loopGuard.Tokens, sw.Elapsed);
|
||
OnRunCompleted?.Invoke(agentConfig.Model, result);
|
||
return result;
|
||
}
|
||
catch (OperationCanceledException)
|
||
{
|
||
sw.Stop();
|
||
logger.LogInformation("Agent run cancelled: {AgentId}", agentConfig.AgentId);
|
||
|
||
var result = new AgentRunResult(
|
||
agentConfig.AgentId, AgentRunStatus.Cancelled, null,
|
||
loopGuard.Steps, loopGuard.Tokens, sw.Elapsed);
|
||
OnRunCompleted?.Invoke(agentConfig.Model, result);
|
||
return result;
|
||
}
|
||
catch (LoopLimitExceededException ex)
|
||
{
|
||
sw.Stop();
|
||
logger.LogWarning("Agent run loop limit exceeded: {AgentId}: {Message}",
|
||
agentConfig.AgentId, ex.Message);
|
||
|
||
var result = new AgentRunResult(
|
||
agentConfig.AgentId, AgentRunStatus.LoopLimitExceeded, null,
|
||
loopGuard.Steps, loopGuard.Tokens, sw.Elapsed, ex);
|
||
OnRunCompleted?.Invoke(agentConfig.Model, result);
|
||
return result;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
sw.Stop();
|
||
logger.LogError(ex, "Agent run failed: {AgentId}", agentConfig.AgentId);
|
||
|
||
var result = new AgentRunResult(
|
||
agentConfig.AgentId, AgentRunStatus.Failed, null,
|
||
loopGuard.Steps, loopGuard.Tokens, sw.Elapsed, ex);
|
||
OnRunCompleted?.Invoke(agentConfig.Model, result);
|
||
return result;
|
||
}
|
||
}
|
||
|
||
public async Task<AgentRunResult> ChatAsync(
|
||
AgentConfig agentConfig,
|
||
string userMessage,
|
||
string instanceId,
|
||
CancellationToken externalCt,
|
||
string? source = null)
|
||
{
|
||
var logger = _loggerFactory.CreateLogger($"ClawdDotNet.Core.Engine.Chat.{agentConfig.AgentId}");
|
||
var loopGuard = new LoopGuard(agentConfig.LoopGuard);
|
||
var sw = Stopwatch.StartNew();
|
||
|
||
using var timeoutCts = new CancellationTokenSource(agentConfig.LoopGuard.Timeout);
|
||
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(externalCt, timeoutCts.Token);
|
||
var ct = linkedCts.Token;
|
||
|
||
lock (_lock)
|
||
_runningChats[agentConfig.AgentId] = linkedCts;
|
||
|
||
try
|
||
{
|
||
var tools = _toolRegistry.GetForAgent(agentConfig);
|
||
var toolDefinitions = BuildToolDefinitions(tools);
|
||
|
||
List<ChatMessage> messages;
|
||
lock (_lock)
|
||
{
|
||
if (!_chatContexts.TryGetValue(agentConfig.AgentId, out messages!))
|
||
{
|
||
messages = new List<ChatMessage>();
|
||
var systemPrompt = agentConfig.FullSystemPrompt;
|
||
if (!string.IsNullOrWhiteSpace(systemPrompt))
|
||
messages.Add(ChatMessage.System(systemPrompt));
|
||
_chatContexts[agentConfig.AgentId] = messages;
|
||
}
|
||
}
|
||
|
||
// Routing-Hinweis: Dem Agenten mitteilen, woher die Nachricht kommt
|
||
var routedMessage = source switch
|
||
{
|
||
ChatSource.WebView => $"[WebView Chat – antworte als normaler Text, NICHT über Telegram senden]\n{userMessage}",
|
||
ChatSource.Telegram => userMessage, // Telegram-Nachrichten kommen bereits mit [Telegram] Prefix vom Job
|
||
_ => userMessage
|
||
};
|
||
|
||
messages.Add(ChatMessage.User(routedMessage));
|
||
AddChatEntry(agentConfig.AgentId, "user", userMessage, source);
|
||
|
||
string? finalMessage = null;
|
||
var totalTokens = 0;
|
||
|
||
while (true)
|
||
{
|
||
ct.ThrowIfCancellationRequested();
|
||
loopGuard.RecordStep();
|
||
|
||
var request = new ChatRequest
|
||
{
|
||
Model = agentConfig.Model,
|
||
Messages = messages,
|
||
Tools = toolDefinitions.Count > 0 ? toolDefinitions : null
|
||
};
|
||
|
||
var response = await _client.CompleteAsync(request, ct);
|
||
|
||
var promptTokens = 0;
|
||
if (response.Usage is not null)
|
||
{
|
||
totalTokens += response.Usage.TotalTokens;
|
||
promptTokens = response.Usage.PromptTokens;
|
||
loopGuard.RecordTokens(response.Usage.TotalTokens);
|
||
}
|
||
|
||
// Context-Compaction nach API-Response prüfen
|
||
var compacted = await _compactor.CompactIfNeededAsync(
|
||
messages, promptTokens, agentConfig.LoopGuard, agentConfig.Model, ct);
|
||
if (compacted)
|
||
{
|
||
PersistChatState(agentConfig.AgentId);
|
||
OnStepCompleted?.Invoke(agentConfig.AgentId, "Context kompaktiert");
|
||
}
|
||
|
||
var choice = response.Choices.FirstOrDefault();
|
||
if (choice is null)
|
||
{
|
||
finalMessage = "[No response from model]";
|
||
break;
|
||
}
|
||
|
||
var assistantMessage = choice.Message;
|
||
|
||
if (assistantMessage.ToolCalls is { Count: > 0 })
|
||
{
|
||
messages.Add(ChatMessage.AssistantWithToolCalls(assistantMessage.ToolCalls));
|
||
|
||
foreach (var toolCall in assistantMessage.ToolCalls)
|
||
{
|
||
var toolResult = await ExecuteToolCallAsync(
|
||
toolCall, agentConfig, instanceId, tools, logger, ct);
|
||
messages.Add(ChatMessage.ToolResponse(toolCall.Id, toolResult));
|
||
}
|
||
|
||
OnStepCompleted?.Invoke(agentConfig.AgentId,
|
||
$"Chat step {loopGuard.Steps}: {assistantMessage.ToolCalls.Count} tool call(s)");
|
||
}
|
||
else
|
||
{
|
||
finalMessage = assistantMessage.Content ?? "";
|
||
messages.Add(ChatMessage.Assistant(finalMessage));
|
||
AddChatEntry(agentConfig.AgentId, "assistant", finalMessage, source);
|
||
break;
|
||
}
|
||
}
|
||
|
||
sw.Stop();
|
||
var result = new AgentRunResult(
|
||
agentConfig.AgentId, AgentRunStatus.Completed, finalMessage,
|
||
loopGuard.Steps, totalTokens, sw.Elapsed);
|
||
OnRunCompleted?.Invoke(agentConfig.Model, result);
|
||
return result;
|
||
}
|
||
catch (OperationCanceledException)
|
||
{
|
||
sw.Stop();
|
||
var cancelMsg = "[Chat abgebrochen]";
|
||
AddChatEntry(agentConfig.AgentId, "assistant", cancelMsg, source);
|
||
var result = new AgentRunResult(
|
||
agentConfig.AgentId, AgentRunStatus.Cancelled, cancelMsg,
|
||
loopGuard.Steps, loopGuard.Tokens, sw.Elapsed);
|
||
OnRunCompleted?.Invoke(agentConfig.Model, result);
|
||
return result;
|
||
}
|
||
catch (LoopLimitExceededException ex)
|
||
{
|
||
sw.Stop();
|
||
var limitMsg = $"[Loop-Limit erreicht: {ex.Message}]";
|
||
logger.LogWarning("Chat loop limit: {AgentId}: {Message}", agentConfig.AgentId, ex.Message);
|
||
AddChatEntry(agentConfig.AgentId, "assistant", limitMsg, source);
|
||
var result = new AgentRunResult(
|
||
agentConfig.AgentId, AgentRunStatus.LoopLimitExceeded, limitMsg,
|
||
loopGuard.Steps, loopGuard.Tokens, sw.Elapsed, ex);
|
||
OnRunCompleted?.Invoke(agentConfig.Model, result);
|
||
return result;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
sw.Stop();
|
||
var errorMsg = $"[Fehler: {ex.Message}]";
|
||
logger.LogError(ex, "Chat failed: {AgentId}", agentConfig.AgentId);
|
||
AddChatEntry(agentConfig.AgentId, "assistant", errorMsg, source);
|
||
var result = new AgentRunResult(
|
||
agentConfig.AgentId, AgentRunStatus.Failed, errorMsg,
|
||
loopGuard.Steps, loopGuard.Tokens, sw.Elapsed, ex);
|
||
OnRunCompleted?.Invoke(agentConfig.Model, result);
|
||
return result;
|
||
}
|
||
finally
|
||
{
|
||
lock (_lock)
|
||
_runningChats.Remove(agentConfig.AgentId);
|
||
}
|
||
}
|
||
|
||
public IReadOnlyList<ChatEntry> GetChatHistory(string agentId)
|
||
{
|
||
lock (_lock)
|
||
return _chatHistories.TryGetValue(agentId, out var history)
|
||
? history.ToList().AsReadOnly()
|
||
: [];
|
||
}
|
||
|
||
public bool IsRunning(string agentId)
|
||
{
|
||
lock (_lock)
|
||
return _runningChats.ContainsKey(agentId);
|
||
}
|
||
|
||
public void AbortChat(string agentId)
|
||
{
|
||
lock (_lock)
|
||
{
|
||
if (_runningChats.TryGetValue(agentId, out var cts))
|
||
cts.Cancel();
|
||
}
|
||
}
|
||
|
||
public void ClearChatHistory(string agentId)
|
||
{
|
||
lock (_lock)
|
||
{
|
||
_chatHistories.Remove(agentId);
|
||
_chatContexts.Remove(agentId);
|
||
}
|
||
|
||
var dir = _agentDirResolver?.Invoke(agentId);
|
||
if (dir is null) return;
|
||
|
||
var historyPath = Path.Combine(dir, "ChatHistory.json");
|
||
var contextPath = Path.Combine(dir, "ChatContext.json");
|
||
if (File.Exists(historyPath)) File.Delete(historyPath);
|
||
if (File.Exists(contextPath)) File.Delete(contextPath);
|
||
}
|
||
|
||
public void SetAgentConfigProvider(
|
||
Func<IReadOnlyList<AgentConfig>> provider,
|
||
string instanceId,
|
||
Func<string, string?>? agentDirResolver = null)
|
||
{
|
||
_agentConfigProvider = provider;
|
||
_instanceId = instanceId;
|
||
_agentDirResolver = agentDirResolver;
|
||
}
|
||
|
||
public void LoadPersistedChats()
|
||
{
|
||
var configs = _agentConfigProvider?.Invoke() ?? [];
|
||
foreach (var agent in configs)
|
||
{
|
||
var dir = _agentDirResolver?.Invoke(agent.AgentId);
|
||
if (dir is null || !Directory.Exists(dir)) continue;
|
||
|
||
try
|
||
{
|
||
var historyPath = Path.Combine(dir, "ChatHistory.json");
|
||
if (File.Exists(historyPath))
|
||
{
|
||
var history = JsonSerializer.Deserialize<List<ChatEntry>>(
|
||
File.ReadAllText(historyPath), _jsonOpts);
|
||
if (history is { Count: > 0 })
|
||
{
|
||
lock (_lock)
|
||
_chatHistories[agent.AgentId] = history;
|
||
}
|
||
}
|
||
|
||
var contextPath = Path.Combine(dir, "ChatContext.json");
|
||
if (File.Exists(contextPath))
|
||
{
|
||
var raw = File.ReadAllText(contextPath);
|
||
List<ChatMessage>? context = null;
|
||
|
||
// Versuche zuerst als Array (direktes List<ChatMessage>)
|
||
// Dann als Wrapper-Objekt {"messages":[...]}
|
||
try
|
||
{
|
||
context = JsonSerializer.Deserialize<List<ChatMessage>>(raw, _jsonOpts);
|
||
}
|
||
catch (JsonException)
|
||
{
|
||
try
|
||
{
|
||
using var doc = JsonDocument.Parse(raw);
|
||
if (doc.RootElement.TryGetProperty("messages", out var msgs))
|
||
{
|
||
context = JsonSerializer.Deserialize<List<ChatMessage>>(
|
||
msgs.GetRawText(), _jsonOpts);
|
||
}
|
||
}
|
||
catch { /* Beide Formate fehlgeschlagen — ignorieren */ }
|
||
}
|
||
|
||
if (context is { Count: > 0 })
|
||
{
|
||
lock (_lock)
|
||
_chatContexts[agent.AgentId] = context;
|
||
}
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_loggerFactory.CreateLogger("ClawdDotNet.Core.Engine.Persistence")
|
||
.LogWarning(ex, "Failed to load chat state for agent {AgentId}", agent.AgentId);
|
||
}
|
||
}
|
||
}
|
||
|
||
// ─── IAgentMessageRouter ───
|
||
|
||
public IReadOnlyList<AgentInfo> ListAgents(string callerAgentId)
|
||
{
|
||
var configs = _agentConfigProvider?.Invoke() ?? [];
|
||
return configs
|
||
.Where(a => a.AgentId != callerAgentId)
|
||
.Select(a => new AgentInfo(a.AgentId, a.DisplayName, a.Description, a.Model, IsRunning(a.AgentId)))
|
||
.ToList();
|
||
}
|
||
|
||
public async Task<AgentMessageResult> SendMessageAsync(
|
||
string fromAgentId, string toAgentId, string message, CancellationToken ct)
|
||
{
|
||
var configs = _agentConfigProvider?.Invoke();
|
||
if (configs is null)
|
||
return new AgentMessageResult(false, null, "Agent config provider not set.");
|
||
|
||
var targetConfig = configs.FirstOrDefault(a => a.AgentId == toAgentId);
|
||
if (targetConfig is null)
|
||
return new AgentMessageResult(false, null, $"Agent '{toAgentId}' not found.");
|
||
|
||
var fromConfig = configs.FirstOrDefault(a => a.AgentId == fromAgentId);
|
||
var fromName = fromConfig?.DisplayName ?? fromAgentId;
|
||
|
||
var wrappedMessage = $"[Nachricht von Agent \"{fromName}\" ({fromAgentId})]\n\n{message}";
|
||
|
||
try
|
||
{
|
||
var result = await ChatAsync(targetConfig, wrappedMessage, _instanceId, ct, source: ChatSource.AgentComm);
|
||
|
||
return result.Status switch
|
||
{
|
||
AgentRunStatus.Completed => new AgentMessageResult(true, result.FinalMessage),
|
||
AgentRunStatus.LoopLimitExceeded => new AgentMessageResult(true, result.FinalMessage,
|
||
"Agent hat das Step-Limit erreicht, die Nachricht wurde aber zugestellt."),
|
||
_ => new AgentMessageResult(false, null, $"Agent run status: {result.Status}")
|
||
};
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
return new AgentMessageResult(false, null, $"Failed to reach agent: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
public async Task<AgentSpawnResult> SpawnAgentAsync(
|
||
string fromAgentId, string targetAgentId, string taskMessage, CancellationToken ct)
|
||
{
|
||
var configs = _agentConfigProvider?.Invoke();
|
||
if (configs is null)
|
||
return new AgentSpawnResult(false, targetAgentId, null, "Agent config provider not set.");
|
||
|
||
var targetConfig = configs.FirstOrDefault(a => a.AgentId == targetAgentId);
|
||
if (targetConfig is null)
|
||
return new AgentSpawnResult(false, targetAgentId, null, $"Agent '{targetAgentId}' not found.");
|
||
|
||
if (IsRunning(targetAgentId))
|
||
return new AgentSpawnResult(false, targetAgentId, null,
|
||
$"Agent '{targetConfig.DisplayName}' läuft bereits. Verwende send_message statt spawn.");
|
||
|
||
var fromConfig = configs.FirstOrDefault(a => a.AgentId == fromAgentId);
|
||
var fromName = fromConfig?.DisplayName ?? fromAgentId;
|
||
|
||
var wrappedMessage = $"[Spawn-Auftrag von Agent \"{fromName}\" ({fromAgentId})]\n\n{taskMessage}";
|
||
|
||
try
|
||
{
|
||
var result = await RunAsync(targetConfig, wrappedMessage, _instanceId, ct);
|
||
|
||
return result.Status == AgentRunStatus.Completed
|
||
? new AgentSpawnResult(true, targetAgentId, result.FinalMessage)
|
||
: new AgentSpawnResult(false, targetAgentId, null, $"Agent run status: {result.Status}");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
return new AgentSpawnResult(false, targetAgentId, null, $"Spawn failed: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
private void AddChatEntry(string agentId, string role, string content, string? source = null)
|
||
{
|
||
lock (_lock)
|
||
{
|
||
if (!_chatHistories.TryGetValue(agentId, out var history))
|
||
{
|
||
history = new List<ChatEntry>();
|
||
_chatHistories[agentId] = history;
|
||
}
|
||
history.Add(new ChatEntry(role, content, DateTime.Now, source));
|
||
}
|
||
PersistChatState(agentId);
|
||
OnChatEntryAdded?.Invoke(agentId, role, content, source);
|
||
}
|
||
|
||
private void PersistChatState(string agentId)
|
||
{
|
||
var dir = _agentDirResolver?.Invoke(agentId);
|
||
if (dir is null) return;
|
||
|
||
try
|
||
{
|
||
Directory.CreateDirectory(dir);
|
||
|
||
List<ChatEntry>? history;
|
||
List<ChatMessage>? context;
|
||
lock (_lock)
|
||
{
|
||
_chatHistories.TryGetValue(agentId, out history);
|
||
_chatContexts.TryGetValue(agentId, out context);
|
||
}
|
||
|
||
if (history is not null)
|
||
File.WriteAllText(
|
||
Path.Combine(dir, "ChatHistory.json"),
|
||
JsonSerializer.Serialize(history, _jsonOpts));
|
||
|
||
if (context is not null)
|
||
File.WriteAllText(
|
||
Path.Combine(dir, "ChatContext.json"),
|
||
JsonSerializer.Serialize(context, _jsonOpts));
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_loggerFactory.CreateLogger("ClawdDotNet.Core.Engine.Persistence")
|
||
.LogWarning(ex, "Failed to persist chat state for agent {AgentId}", agentId);
|
||
}
|
||
}
|
||
|
||
private async Task<string> ExecuteToolCallAsync(
|
||
ToolCall toolCall,
|
||
AgentConfig agentConfig,
|
||
string instanceId,
|
||
IReadOnlyList<IAgentTool> availableTools,
|
||
ILogger logger,
|
||
CancellationToken ct)
|
||
{
|
||
var toolName = toolCall.Function.Name;
|
||
|
||
try
|
||
{
|
||
_permissionGate.Enforce(agentConfig.AgentId, toolName, agentConfig);
|
||
|
||
var tool = availableTools.FirstOrDefault(t => t.Name == toolName);
|
||
if (tool is null)
|
||
return JsonSerializer.Serialize(ToolResult.Fail($"Tool '{toolName}' not found."));
|
||
|
||
var input = string.IsNullOrWhiteSpace(toolCall.Function.Arguments)
|
||
? default
|
||
: JsonDocument.Parse(toolCall.Function.Arguments).RootElement;
|
||
|
||
var toolConfig = agentConfig.Tools.TryGetValue(toolName, out var cfg)
|
||
? cfg.AsReadOnly()
|
||
: new Dictionary<string, object?>().AsReadOnly();
|
||
|
||
var toolLogger = _loggerFactory.CreateLogger($"ClawdDotNet.Tools.{toolName}.Execution");
|
||
|
||
var context = new AgentToolContext(
|
||
agentConfig.AgentId,
|
||
instanceId,
|
||
toolConfig,
|
||
_stateStore,
|
||
toolLogger,
|
||
ct,
|
||
agentConfig.WorkspacePath,
|
||
agentConfig.SharedWorkspacePath,
|
||
this);
|
||
|
||
logger.LogDebug("Executing tool {Tool} for agent {AgentId}", toolName, agentConfig.AgentId);
|
||
|
||
var result = await tool.ExecuteAsync(input, context, ct);
|
||
|
||
logger.LogDebug("Tool {Tool} completed: success={Success}", toolName, result.Success);
|
||
|
||
return result.Success
|
||
? result.Content
|
||
: JsonSerializer.Serialize(new { error = result.ErrorMessage });
|
||
}
|
||
catch (ToolAccessDeniedException ex)
|
||
{
|
||
logger.LogWarning("Tool access denied: {Message}", ex.Message);
|
||
return JsonSerializer.Serialize(new { error = ex.Message });
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
logger.LogError(ex, "Tool {Tool} threw an exception", toolName);
|
||
return JsonSerializer.Serialize(new { error = $"Tool execution failed: {ex.Message}" });
|
||
}
|
||
}
|
||
|
||
private static List<ToolDefinition> BuildToolDefinitions(IReadOnlyList<IAgentTool> tools)
|
||
{
|
||
return tools.Select(t => new ToolDefinition
|
||
{
|
||
Function = new FunctionDefinition
|
||
{
|
||
Name = t.Name,
|
||
Description = t.Description,
|
||
Parameters = t.InputSchema
|
||
}
|
||
}).ToList();
|
||
}
|
||
}
|