using System.Text; using System.Text.Json; using AxCopilot.Models; namespace AxCopilot.Services.Agent; /// /// P5: 배치 서브에이전트 생성 도구. /// 여러 서브에이전트를 한 번에 생성하고 통합 결과를 반환합니다. /// public class SpawnAgentsTool : IAgentTool { public string Name => "spawn_agents"; public string Description => "Create multiple sub-agents in batch for parallel research or task execution.\n" + "Each agent runs independently with its own task and optional profile.\n" + "Collect results later with wait_agents."; public ToolParameterSchema Parameters => new() { Properties = new() { ["agents"] = new ToolProperty { Type = "array", Description = "List of sub-agent definitions. Each has: id (unique identifier), task (work description), profile (optional: researcher/coder/writer/reviewer/planner).", Items = new ToolProperty { Type = "object", Description = "Sub-agent definition with id, task, and optional profile." } }, }, Required = new() { "agents" } }; public async Task ExecuteAsync(JsonElement args, AgentContext context, CancellationToken ct = default) { if (!args.SafeTryGetProperty("agents", out var agentsEl) || agentsEl.ValueKind != JsonValueKind.Array) return ToolResult.Fail("agents array is required."); var agentDefs = new List<(string Id, string Task, string? Profile)>(); foreach (var item in agentsEl.EnumerateArray()) { var id = item.SafeTryGetProperty("id", out var idEl) ? idEl.SafeGetString() ?? "" : ""; var task = item.SafeTryGetProperty("task", out var taskEl) ? taskEl.SafeGetString() ?? "" : ""; var profile = item.SafeTryGetProperty("profile", out var profEl) ? profEl.SafeGetString() : null; if (string.IsNullOrWhiteSpace(id) || string.IsNullOrWhiteSpace(task)) return ToolResult.Fail($"Each agent must have non-empty 'id' and 'task'. Found invalid entry."); agentDefs.Add((id, task, profile)); } if (agentDefs.Count == 0) return ToolResult.Fail("agents array is empty."); // 용량 사전 검증 (빠른 실패용 — 실제 원자성은 SubAgentTool.ExecuteAsync 내부 lock에서 보장) var app = System.Windows.Application.Current as App; var maxAgents = app?.SettingsService?.Settings.Llm.MaxSubAgents ?? 5; var activeTasks = SubAgentTool.ActiveTasks; var running = activeTasks.Values.Count(x => x.CompletedAt == null); if (running + agentDefs.Count > maxAgents) return ToolResult.Fail( $"Cannot spawn {agentDefs.Count} agents: {running} already running, max is {maxAgents}."); // 중복 ID 검사 var duplicateIds = agentDefs.GroupBy(a => a.Id, StringComparer.OrdinalIgnoreCase) .Where(g => g.Count() > 1) .Select(g => g.Key) .ToList(); if (duplicateIds.Count > 0) return ToolResult.Fail($"Duplicate agent ids: {string.Join(", ", duplicateIds)}"); // 기존 활성 태스크와 ID 충돌 검사 var conflictIds = agentDefs.Where(a => activeTasks.ContainsKey(a.Id)).Select(a => a.Id).ToList(); if (conflictIds.Count > 0) return ToolResult.Fail($"Agent ids already exist: {string.Join(", ", conflictIds)}"); // 각 에이전트를 SubAgentTool을 통해 생성 var spawnTool = new SubAgentTool(); var results = new List<(string Id, bool Success, string Message)>(); var cancelled = false; foreach (var (id, task, profile) in agentDefs) { // 루프 중 취소 감지 — 이미 생성된 에이전트는 유지하고 나머지만 건너뜀 if (ct.IsCancellationRequested) { cancelled = true; results.Add((id, false, "Cancelled: batch spawn interrupted.")); continue; } // SubAgentTool.ExecuteAsync용 JsonElement 구성 var jsonObj = new Dictionary { ["id"] = id, ["task"] = task, }; if (!string.IsNullOrWhiteSpace(profile)) jsonObj["profile"] = profile; var jsonStr = JsonSerializer.Serialize(jsonObj); using var doc = JsonDocument.Parse(jsonStr); var result = await spawnTool.ExecuteAsync(doc.RootElement, context, ct).ConfigureAwait(false); results.Add((id, result.Success, result.Output ?? "")); } // 통합 결과 메시지 var sb = new StringBuilder(); var successCount = results.Count(r => r.Success); var failCount = results.Count(r => !r.Success); sb.AppendLine($"Batch spawn: {successCount} started, {failCount} failed (total: {agentDefs.Count}){(cancelled ? " [partially cancelled]" : "")}"); sb.AppendLine(); foreach (var (id, success, message) in results) { var status = success ? "✓" : "✗"; var profileName = agentDefs.First(a => a.Id == id).Profile ?? "researcher"; sb.AppendLine($"{status} [{id}] profile={profileName}"); if (!success) sb.AppendLine($" Error: {message}"); } sb.AppendLine(); sb.AppendLine("Use wait_agents to collect results when ready."); return failCount == agentDefs.Count ? ToolResult.Fail(sb.ToString().TrimEnd()) : ToolResult.Ok(sb.ToString().TrimEnd()); } }