변경 목적: - AX Agent의 도구 이름, 내부 설정, 스킬 정책, 실행 루프 사이의 불일치를 줄이고 전체 동작 품질을 높인다. - claw-code 수준의 일관된 동작 품질을 참고하되 AX 구조에 맞는 고유한 카탈로그·정규화 레이어로 재구성한다. 핵심 수정사항: - 도구 canonical id, legacy alias, 탭 노출, 설정 카테고리, read-only 분류를 중앙 카탈로그로 통합했다. - ToolRegistry, AgentLoopService, 병렬 실행 분류, 권한 처리, 훅 처리, 스킬 allowed-tools 해석이 같은 이름 체계를 사용하도록 정리했다. - Agent 설정/일반 설정/도움말의 도구 카드와 훅 편집기, 스킬 설명을 현재 런타임 구조에 맞게 갱신했다. - 컨텍스트 압축, intent gate, spawn agents, session learning, model prompt adapter, workspace context 관련 변경과 테스트 추가를 함께 반영했다. - 문서 이력과 비교/로드맵 문서를 최신 상태로 갱신했다. 검증 결과: - dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify_toolcat\ -p:IntermediateOutputPath=obj\verify_toolcat\ : 경고 0 / 오류 0 - dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter AgentToolCatalogTests -p:OutputPath=bin\verify_toolcat_tests\ -p:IntermediateOutputPath=obj\verify_toolcat_tests\ : 통과 8
138 lines
5.7 KiB
C#
138 lines
5.7 KiB
C#
using System.Text;
|
|
using System.Text.Json;
|
|
using AxCopilot.Models;
|
|
|
|
namespace AxCopilot.Services.Agent;
|
|
|
|
/// <summary>
|
|
/// P5: 배치 서브에이전트 생성 도구.
|
|
/// 여러 서브에이전트를 한 번에 생성하고 통합 결과를 반환합니다.
|
|
/// </summary>
|
|
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<ToolResult> 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<string, object?>
|
|
{
|
|
["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());
|
|
}
|
|
}
|