Initial commit: ClawdDotNet
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>
This commit is contained in:
@@ -0,0 +1,700 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user