404 lines
14 KiB
C#
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
|
|
{
|
|
}
|
|
}
|
|
}
|