feat(ui): complete Avalonia UI port with 7 main pages, tool settings & top MenuBar
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
using System.Text;
|
||||
using ClawdDotNet.Core.Config;
|
||||
using ClawdDotNet.Core.Engine;
|
||||
using ClawdDotNet.Core.State;
|
||||
using ClawdDotNet.Core.Tools;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace ClawdDotNet.Core.Tasks;
|
||||
|
||||
/// <summary>
|
||||
/// Verbindet den Scanner mit der Engine: übersetzt den Assignee einer fälligen Aufgabe in
|
||||
/// einen konkreten Lauf.
|
||||
///
|
||||
/// - <c>@new</c> / <c>@new:<agent></c> → <see cref="AgentEngine.RunAsync"/>: frischer
|
||||
/// Lauf ohne Historie.
|
||||
/// - <c>@<agent></c> → <see cref="AgentEngine.ChatAsync"/>: der Agent mit seinem
|
||||
/// bestehenden Kontext. Das Agent-Gate der Engine serialisiert solche Läufe (B2).
|
||||
///
|
||||
/// Ist der Ziel-Agent nicht auflösbar (unbekannt, oder bloßes <c>@new</c> bei mehreren
|
||||
/// Agenten), scheitert der Dispatch mit einer klaren Meldung, statt einen falschen Agenten
|
||||
/// zu raten.
|
||||
/// </summary>
|
||||
public sealed class EngineTaskDispatcher : ITaskDispatcher
|
||||
{
|
||||
private readonly AgentEngine _engine;
|
||||
private readonly Func<IReadOnlyList<AgentConfig>> _agents;
|
||||
private readonly string _instanceId;
|
||||
private readonly ToolRegistry _toolRegistry;
|
||||
private readonly IStateStore _stateStore;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public EngineTaskDispatcher(
|
||||
AgentEngine engine,
|
||||
Func<IReadOnlyList<AgentConfig>> agents,
|
||||
string instanceId,
|
||||
ToolRegistry toolRegistry,
|
||||
IStateStore stateStore,
|
||||
ILoggerFactory loggerFactory)
|
||||
{
|
||||
_engine = engine;
|
||||
_agents = agents;
|
||||
_instanceId = instanceId;
|
||||
_toolRegistry = toolRegistry;
|
||||
_stateStore = stateStore;
|
||||
_logger = loggerFactory.CreateLogger("ClawdDotNet.Core.Tasks.Dispatcher");
|
||||
}
|
||||
|
||||
public async Task<bool> DispatchAsync(TaskItem task, CancellationToken ct)
|
||||
{
|
||||
// Poll-Tasks (tool_job) ticken ein Tool statt einen Agenten direkt zu starten.
|
||||
if (task.Type == TaskItemType.ToolJob)
|
||||
return await DispatchToolJobAsync(task, ct);
|
||||
|
||||
var agents = _agents();
|
||||
var message = BuildMessage(task);
|
||||
var kind = TaskAssignee.KindOf(task.Assignee);
|
||||
|
||||
AgentRunResult result;
|
||||
switch (kind)
|
||||
{
|
||||
case TaskAssigneeKind.New:
|
||||
{
|
||||
var config = ResolveFreshAgent(agents, TaskAssignee.AgentId(task.Assignee));
|
||||
if (config is null)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Aufgabe {TaskId}: Assignee '{Assignee}' nicht auflösbar (frischer Lauf braucht einen eindeutigen Agenten)",
|
||||
task.Id, task.Assignee);
|
||||
return false;
|
||||
}
|
||||
result = await _engine.RunAsync(
|
||||
config, message, _instanceId, ct, source: ChatSource.Task, taskId: task.Id);
|
||||
break;
|
||||
}
|
||||
case TaskAssigneeKind.Agent:
|
||||
{
|
||||
var agentId = TaskAssignee.AgentId(task.Assignee);
|
||||
var config = agents.FirstOrDefault(a => a.AgentId == agentId);
|
||||
if (config is null)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Aufgabe {TaskId}: Agent '{AgentId}' nicht gefunden", task.Id, agentId);
|
||||
return false;
|
||||
}
|
||||
result = await _engine.ChatAsync(
|
||||
config, message, _instanceId, ct, source: ChatSource.Task, taskId: task.Id);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
// @human wird vom Scanner gar nicht erst angestoßen.
|
||||
return false;
|
||||
}
|
||||
|
||||
return result.Status == AgentRunStatus.Completed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tickt einen <see cref="IToolJobProvider"/> (Poll) und weckt den Ziel-Agenten nur,
|
||||
/// wenn der Tick etwas meldet. Der Tick selbst gilt als erfolgreich, auch wenn nichts
|
||||
/// zu tun war — ein Poll ohne Fund ist kein Fehler.
|
||||
/// </summary>
|
||||
private async Task<bool> DispatchToolJobAsync(TaskItem task, CancellationToken ct)
|
||||
{
|
||||
if (_toolRegistry.Get(task.ToolName) is not IToolJobProvider provider)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Tool-Job {TaskId}: Tool '{Tool}' ist kein IToolJobProvider oder nicht registriert",
|
||||
task.Id, task.ToolName);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Zielagent (zum Wecken) und dessen Tool-Konfiguration/Workspace.
|
||||
var agentId = TaskAssignee.AgentId(task.Assignee);
|
||||
var config = _agents().FirstOrDefault(a => a.AgentId == agentId);
|
||||
var toolConfig = config is not null && config.Tools.TryGetValue(task.ToolName, out var cfg)
|
||||
? (IReadOnlyDictionary<string, object?>)cfg.AsReadOnly()
|
||||
: new Dictionary<string, object?>().AsReadOnly();
|
||||
|
||||
var result = await provider.ExecuteJobAsync(
|
||||
task.JobTypeId, toolConfig, _stateStore, _logger, ct, agentId, config?.WorkspacePath);
|
||||
|
||||
if (!result.ShouldWakeAgent || string.IsNullOrWhiteSpace(result.WakeMessage))
|
||||
return true; // Poll lief, nichts zu wecken
|
||||
|
||||
if (config is null)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Tool-Job {TaskId} wollte Agent '{AgentId}' wecken, der aber nicht gefunden wurde",
|
||||
task.Id, agentId);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Der Provider entscheidet je Tick, ob mit Kontext (ChatAsync) oder zustandslos.
|
||||
if (result.UseChatContext)
|
||||
await _engine.ChatAsync(config, result.WakeMessage, _instanceId, ct, source: ChatSource.Job, taskId: task.Id);
|
||||
else
|
||||
await _engine.RunAsync(config, result.WakeMessage, _instanceId, ct, source: ChatSource.Job, taskId: task.Id);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static AgentConfig? ResolveFreshAgent(IReadOnlyList<AgentConfig> agents, string agentId)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(agentId))
|
||||
return agents.FirstOrDefault(a => a.AgentId == agentId);
|
||||
|
||||
// Bloßes @new ohne Agent: nur eindeutig, wenn die Instanz genau einen Agenten hat.
|
||||
return agents.Count == 1 ? agents[0] : null;
|
||||
}
|
||||
|
||||
private static string BuildMessage(TaskItem task)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine($"[Aufgabe {task.Id}] {task.Title}");
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(task.Body))
|
||||
{
|
||||
sb.AppendLine();
|
||||
sb.AppendLine(task.Body.Trim());
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(task.Acceptance))
|
||||
{
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("Abnahmekriterien (das Ergebnis wird daran gemessen):");
|
||||
sb.AppendLine(task.Acceptance.Trim());
|
||||
}
|
||||
|
||||
return sb.ToString().TrimEnd();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user