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 _activeTasks = new Dictionary(); 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 { ["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 { "task", "id" } }; public static IReadOnlyDictionary ActiveTasks { get { lock (_lock) { return new Dictionary(_activeTasks); } } } public static event Action? StatusChanged; public Task 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 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 messages = new List { 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(); llm.ToolPermissions = new Dictionary(StringComparer.OrdinalIgnoreCase); llm.DisabledTools = new List { "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 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 events) { List 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 WaitAsync(IEnumerable? ids = null, bool completedOnly = false, CancellationToken ct = default(CancellationToken)) { List tasks; lock (_lock) { tasks = _activeTasks.Values.ToList(); } List requestedIds = (from x in ids?.Where((string x) => !string.IsNullOrWhiteSpace(x)) select x.Trim()).Distinct(StringComparer.OrdinalIgnoreCase).ToList(); if (requestedIds != null && requestedIds.Count > 0) { tasks = tasks.Where((SubAgentTask t) => requestedIds.Contains(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 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 { } } }