Files

404 lines
14 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using AxCopilot.Models;
namespace AxCopilot.Services.Agent;
public class SubAgentTool : IAgentTool
{
private static readonly Dictionary<string, SubAgentTask> _activeTasks = new Dictionary<string, SubAgentTask>();
private static readonly object _lock = new object();
public string Name => "spawn_agent";
public string Description => "Create a read-only sub-agent for bounded parallel research or codebase analysis.\nUse this when a side task can run independently while the main agent continues.\nThe sub-agent can inspect files, search code, review diffs, and summarize findings.\nCollect results later with wait_agents.";
public ToolParameterSchema Parameters => new ToolParameterSchema
{
Properties = new Dictionary<string, ToolProperty>
{
["task"] = new ToolProperty
{
Type = "string",
Description = "The self-contained task for the sub-agent."
},
["id"] = new ToolProperty
{
Type = "string",
Description = "A unique sub-agent identifier used by wait_agents."
}
},
Required = new List<string> { "task", "id" }
};
public static IReadOnlyDictionary<string, SubAgentTask> ActiveTasks
{
get
{
lock (_lock)
{
return new Dictionary<string, SubAgentTask>(_activeTasks);
}
}
}
public static event Action<SubAgentStatusEvent>? StatusChanged;
public Task<ToolResult> ExecuteAsync(JsonElement args, AgentContext context, CancellationToken ct = default(CancellationToken))
{
JsonElement value;
string task = (args.TryGetProperty("task", out value) ? (value.GetString() ?? "") : "");
JsonElement value2;
string id = (args.TryGetProperty("id", out value2) ? (value2.GetString() ?? "") : "");
if (string.IsNullOrWhiteSpace(task) || string.IsNullOrWhiteSpace(id))
{
return Task.FromResult(ToolResult.Fail("task and id are required."));
}
CleanupStale();
int num = ((!(Application.Current is App app)) ? ((int?)null) : app.SettingsService?.Settings.Llm.MaxSubAgents) ?? 3;
lock (_lock)
{
if (_activeTasks.ContainsKey(id))
{
return Task.FromResult(ToolResult.Fail("Sub-agent id already exists: " + id));
}
int num2 = _activeTasks.Values.Count((SubAgentTask x) => !x.CompletedAt.HasValue);
if (num2 >= num)
{
return Task.FromResult(ToolResult.Fail($"Maximum concurrent sub-agents reached ({num})."));
}
}
SubAgentTask subTask = new SubAgentTask
{
Id = id,
Task = task,
StartedAt = DateTime.Now
};
subTask.RunTask = Task.Run(async delegate
{
try
{
string result = await RunSubAgentAsync(id, task, context).ConfigureAwait(continueOnCapturedContext: false);
subTask.Result = result;
subTask.Success = true;
NotifyStatus(new SubAgentStatusEvent
{
Id = id,
Task = task,
Status = SubAgentRunStatus.Completed,
Summary = "Sub-agent '" + id + "' completed.",
Result = result,
Timestamp = DateTime.Now
});
}
catch (Exception ex)
{
Exception ex2 = ex;
subTask.Result = "Error: " + ex2.Message;
subTask.Success = false;
NotifyStatus(new SubAgentStatusEvent
{
Id = id,
Task = task,
Status = SubAgentRunStatus.Failed,
Summary = "Sub-agent '" + id + "' failed: " + ex2.Message,
Result = subTask.Result,
Timestamp = DateTime.Now
});
}
finally
{
subTask.CompletedAt = DateTime.Now;
}
}, CancellationToken.None);
lock (_lock)
{
_activeTasks[id] = subTask;
}
NotifyStatus(new SubAgentStatusEvent
{
Id = id,
Task = task,
Status = SubAgentRunStatus.Started,
Summary = "Sub-agent '" + id + "' started.",
Timestamp = DateTime.Now
});
return Task.FromResult(ToolResult.Ok($"Sub-agent '{id}' started.\nTask: {task}\nUse wait_agents later to collect the result."));
}
private static async Task<string> RunSubAgentAsync(string id, string task, AgentContext parentContext)
{
SettingsService settings = CreateSubAgentSettings(parentContext);
using LlmService llm = new LlmService(settings);
using ToolRegistry tools = await CreateSubAgentRegistryAsync(settings).ConfigureAwait(continueOnCapturedContext: false);
AgentLoopService loop = new AgentLoopService(llm, tools, settings)
{
ActiveTab = parentContext.ActiveTab
};
List<ChatMessage> messages = new List<ChatMessage>
{
new ChatMessage
{
Role = "system",
Content = BuildSubAgentSystemPrompt(parentContext)
},
new ChatMessage
{
Role = "user",
Content = task
}
};
string finalText = await loop.RunAsync(messages, CancellationToken.None).ConfigureAwait(continueOnCapturedContext: false);
string eventSummary = SummarizeEvents(loop.Events);
StringBuilder sb = new StringBuilder();
StringBuilder stringBuilder = sb;
StringBuilder stringBuilder2 = stringBuilder;
StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(12, 1, stringBuilder);
handler.AppendLiteral("[Sub-agent ");
handler.AppendFormatted(id);
handler.AppendLiteral("]");
stringBuilder2.AppendLine(ref handler);
stringBuilder = sb;
StringBuilder stringBuilder3 = stringBuilder;
handler = new StringBuilder.AppendInterpolatedStringHandler(6, 1, stringBuilder);
handler.AppendLiteral("Task: ");
handler.AppendFormatted(task);
stringBuilder3.AppendLine(ref handler);
if (!string.IsNullOrWhiteSpace(parentContext.WorkFolder))
{
stringBuilder = sb;
StringBuilder stringBuilder4 = stringBuilder;
handler = new StringBuilder.AppendInterpolatedStringHandler(13, 1, stringBuilder);
handler.AppendLiteral("Work folder: ");
handler.AppendFormatted(parentContext.WorkFolder);
stringBuilder4.AppendLine(ref handler);
}
if (!string.IsNullOrWhiteSpace(eventSummary))
{
sb.AppendLine();
sb.AppendLine("Observed work:");
sb.AppendLine(eventSummary);
}
sb.AppendLine();
sb.AppendLine("Result:");
sb.AppendLine(string.IsNullOrWhiteSpace(finalText) ? "(empty)" : finalText.Trim());
return sb.ToString().TrimEnd();
}
private static SettingsService CreateSubAgentSettings(AgentContext parentContext)
{
SettingsService settingsService = new SettingsService();
settingsService.Load();
LlmSettings llm = settingsService.Settings.Llm;
llm.WorkFolder = parentContext.WorkFolder;
llm.FilePermission = "Deny";
llm.PlanMode = "off";
llm.AgentHooks = new List<AgentHookEntry>();
llm.ToolPermissions = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
llm.DisabledTools = new List<string>
{
"spawn_agent", "wait_agents", "file_write", "file_edit", "process", "build_run", "snippet_runner", "memory", "notify", "open_external",
"user_ask", "checkpoint", "diff_preview", "playbook", "http_tool", "clipboard", "sql_tool"
};
return settingsService;
}
private static async Task<ToolRegistry> CreateSubAgentRegistryAsync(SettingsService settings)
{
ToolRegistry registry = new ToolRegistry();
registry.Register(new FileReadTool());
registry.Register(new GlobTool());
registry.Register(new GrepTool());
registry.Register(new FolderMapTool());
registry.Register(new DocumentReaderTool());
registry.Register(new DevEnvDetectTool());
registry.Register(new GitTool());
registry.Register(new LspTool());
registry.Register(new CodeSearchTool());
registry.Register(new CodeReviewTool());
registry.Register(new ProjectRuleTool());
registry.Register(new SkillManagerTool());
registry.Register(new JsonTool());
registry.Register(new RegexTool());
registry.Register(new DiffTool());
registry.Register(new Base64Tool());
registry.Register(new HashTool());
registry.Register(new DateTimeTool());
registry.Register(new MathTool());
registry.Register(new XmlTool());
registry.Register(new MultiReadTool());
registry.Register(new FileInfoTool());
registry.Register(new DocumentReviewTool());
await registry.RegisterMcpToolsAsync(settings.Settings.Llm.McpServers).ConfigureAwait(continueOnCapturedContext: false);
return registry;
}
private static string BuildSubAgentSystemPrompt(AgentContext parentContext)
{
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.AppendLine("You are a focused sub-agent for AX Copilot.");
stringBuilder.AppendLine("You are running a bounded, read-only investigation.");
stringBuilder.AppendLine("Use tools to inspect the project and gather evidence, then return a concise result.");
stringBuilder.AppendLine("Do not ask the user questions.");
stringBuilder.AppendLine("Do not attempt file edits, command execution, notifications, or external side effects.");
stringBuilder.AppendLine("Prefer direct evidence from files and tool results over speculation.");
stringBuilder.AppendLine("If something is uncertain, say so briefly.");
if (!string.IsNullOrWhiteSpace(parentContext.WorkFolder))
{
StringBuilder stringBuilder2 = stringBuilder;
StringBuilder stringBuilder3 = stringBuilder2;
StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(21, 1, stringBuilder2);
handler.AppendLiteral("Current work folder: ");
handler.AppendFormatted(parentContext.WorkFolder);
stringBuilder3.AppendLine(ref handler);
}
if (!string.IsNullOrWhiteSpace(parentContext.ActiveTab))
{
StringBuilder stringBuilder2 = stringBuilder;
StringBuilder stringBuilder4 = stringBuilder2;
StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(13, 1, stringBuilder2);
handler.AppendLiteral("Current tab: ");
handler.AppendFormatted(parentContext.ActiveTab);
stringBuilder4.AppendLine(ref handler);
}
stringBuilder.AppendLine("Final answer format:");
stringBuilder.AppendLine("1. Short conclusion");
stringBuilder.AppendLine("2. Key evidence");
stringBuilder.AppendLine("3. Risks or unknowns");
return stringBuilder.ToString().TrimEnd();
}
private static string SummarizeEvents(IEnumerable<AgentEvent> events)
{
List<string> list = events.Where(delegate(AgentEvent e)
{
AgentEventType type = e.Type;
return (type == AgentEventType.StepStart || (uint)(type - 4) <= 1u) ? true : false;
}).TakeLast(12).Select(delegate(AgentEvent e)
{
AgentEventType type = e.Type;
if (1 == 0)
{
}
string text = type switch
{
AgentEventType.ToolCall => "tool:" + e.ToolName,
AgentEventType.ToolResult => "result:" + e.ToolName,
AgentEventType.StepStart => "step",
_ => e.Type.ToString().ToLowerInvariant(),
};
if (1 == 0)
{
}
string text2 = text;
string text3 = (string.IsNullOrWhiteSpace(e.Summary) ? "" : (" - " + e.Summary.Trim()));
return "- " + text2 + text3;
})
.ToList();
return (list.Count == 0) ? "" : string.Join(Environment.NewLine, list);
}
public static async Task<string> WaitAsync(IEnumerable<string>? ids = null, bool completedOnly = false, CancellationToken ct = default(CancellationToken))
{
List<SubAgentTask> tasks;
lock (_lock)
{
tasks = _activeTasks.Values.ToList();
}
List<string> requestedIds = (from x in ids?.Where((string x) => !string.IsNullOrWhiteSpace(x))
select x.Trim()).Distinct<string>(StringComparer.OrdinalIgnoreCase).ToList();
if (requestedIds != null && requestedIds.Count > 0)
{
tasks = tasks.Where((SubAgentTask t) => requestedIds.Contains<string>(t.Id, StringComparer.OrdinalIgnoreCase)).ToList();
}
if (tasks.Count == 0)
{
if (requestedIds != null && requestedIds.Count > 0)
{
return "No matching sub-agents found for: " + string.Join(", ", requestedIds);
}
return "No active sub-agents.";
}
if (!completedOnly)
{
await Task.WhenAll(from t in tasks
where t.RunTask != null
select t.RunTask).WaitAsync(ct);
}
else
{
tasks = tasks.Where((SubAgentTask t) => t.CompletedAt.HasValue).ToList();
}
if (tasks.Count == 0)
{
return "No requested sub-agents have completed yet.";
}
StringBuilder sb = new StringBuilder();
StringBuilder stringBuilder = sb;
StringBuilder stringBuilder2 = stringBuilder;
StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(31, 1, stringBuilder);
handler.AppendLiteral("Collected ");
handler.AppendFormatted(tasks.Count);
handler.AppendLiteral(" sub-agent result(s):");
stringBuilder2.AppendLine(ref handler);
foreach (SubAgentTask task in tasks.OrderBy((SubAgentTask t) => t.StartedAt))
{
string status = (task.Success ? "OK" : "FAIL");
string duration = (task.CompletedAt.HasValue ? $"{(task.CompletedAt.Value - task.StartedAt).TotalSeconds:F1}s" : "running");
sb.AppendLine();
stringBuilder = sb;
StringBuilder stringBuilder3 = stringBuilder;
handler = new StringBuilder.AppendInterpolatedStringHandler(14, 3, stringBuilder);
handler.AppendLiteral("--- [");
handler.AppendFormatted(status);
handler.AppendLiteral("] ");
handler.AppendFormatted(task.Id);
handler.AppendLiteral(" (");
handler.AppendFormatted(duration);
handler.AppendLiteral(") ---");
stringBuilder3.AppendLine(ref handler);
sb.AppendLine(task.Result ?? "(no result)");
}
lock (_lock)
{
foreach (SubAgentTask task2 in tasks)
{
_activeTasks.Remove(task2.Id);
}
}
return sb.ToString().TrimEnd();
}
public static void CleanupStale()
{
lock (_lock)
{
List<string> list = (from kv in _activeTasks
where kv.Value.CompletedAt.HasValue && (DateTime.Now - kv.Value.CompletedAt.Value).TotalMinutes > 10.0
select kv.Key).ToList();
foreach (string item in list)
{
_activeTasks.Remove(item);
}
}
}
private static void NotifyStatus(SubAgentStatusEvent evt)
{
try
{
SubAgentTool.StatusChanged?.Invoke(evt);
}
catch
{
}
}
}