Files
AX-Copilot-Codex/src/AxCopilot/Services/Agent/SuggestActionsTool.cs
lacvet 33c1db4dae
Some checks failed
Release Gate / gate (push) Has been cancelled
에이전트 선택적 탐색 구조 개선과 경고 정리 반영
- claude-code 선택적 탐색 흐름을 참고해 Cowork/Code 시스템 프롬프트에서 folder_map 상시 선행 지시를 완화하고 glob/grep 기반 좁은 탐색을 우선하도록 조정함

- FolderMapTool 기본 depth를 2로, include_files 기본값을 false로 낮추고 MultiReadTool 최대 파일 수를 8개로 줄여 초기 과탐색 폭을 보수적으로 조정함

- AgentLoopExplorationPolicy partial을 추가해 탐색 범위 분류, broad-scan corrective hint, exploration_breadth 성능 로그를 연결함

- AgentLoopService에 탐색 범위 가이드 주입과 실행 중 탐색 폭 추적을 추가하고, 좁은 질문에서 반복적인 folder_map/대량 multi_read를 교정하도록 정리함

- DocxToHtmlConverter nullable 경고를 수정해 Release 빌드 경고 0 / 오류 0 기준을 다시 충족함

- README와 docs/DEVELOPMENT.md에 2026-04-09 10:36 (KST) 기준 개발 이력을 반영함
2026-04-09 14:27:59 +09:00

104 lines
4.3 KiB
C#

using System.Text;
using System.Text.Json;
namespace AxCopilot.Services.Agent;
/// <summary>
/// 작업 완료 후 후속 액션을 구조화하여 제안하는 도구.
/// UI가 클릭 가능한 칩으로 렌더링할 수 있도록 JSON 형태로 반환합니다.
/// </summary>
public class SuggestActionsTool : IAgentTool
{
public string Name => "suggest_actions";
public string Description =>
"Suggest 2-5 follow-up actions after completing a task. " +
"Returns structured JSON that the UI renders as clickable action chips. " +
"Each action has a label (display text), command (slash command or natural language prompt), " +
"optional icon (Segoe MDL2 Assets code), and priority (high/medium/low).";
public ToolParameterSchema Parameters => new()
{
Properties = new()
{
["actions"] = new()
{
Type = "array",
Description = "List of action objects. Each object: {\"label\": \"표시 텍스트\", \"command\": \"/slash 또는 자연어\", \"icon\": \"\\uE8A5\" (optional), \"priority\": \"high|medium|low\"}",
Items = new() { Type = "object", Description = "Action object with label, command, icon, priority" },
},
["context"] = new()
{
Type = "string",
Description = "Current task context summary (optional)",
},
},
Required = ["actions"],
};
public Task<ToolResult> ExecuteAsync(JsonElement args, AgentContext context, CancellationToken ct = default)
{
try
{
if (!args.SafeTryGetProperty("actions", out var actionsEl) || actionsEl.ValueKind != JsonValueKind.Array)
return Task.FromResult(ToolResult.Fail("actions 배열이 필요합니다."));
var actions = new List<Dictionary<string, string>>();
foreach (var item in actionsEl.EnumerateArray())
{
var label = item.SafeTryGetProperty("label", out var l) ? l.SafeGetString() ?? "" : "";
var command = item.SafeTryGetProperty("command", out var c) ? c.SafeGetString() ?? "" : "";
var icon = item.SafeTryGetProperty("icon", out var i) ? i.SafeGetString() ?? "" : "";
var priority = item.SafeTryGetProperty("priority", out var p) ? p.SafeGetString() ?? "medium" : "medium";
if (string.IsNullOrWhiteSpace(label))
return Task.FromResult(ToolResult.Fail("각 action에는 label이 필요합니다."));
if (string.IsNullOrWhiteSpace(command))
return Task.FromResult(ToolResult.Fail("각 action에는 command가 필요합니다."));
// priority 유효성 검사
var validPriorities = new[] { "high", "medium", "low" };
if (!validPriorities.Contains(priority))
priority = "medium";
var action = new Dictionary<string, string>
{
["label"] = label,
["command"] = command,
["priority"] = priority,
};
if (!string.IsNullOrEmpty(icon))
action["icon"] = icon;
actions.Add(action);
}
if (actions.Count < 1 || actions.Count > 5)
return Task.FromResult(ToolResult.Fail("actions는 1~5개 사이여야 합니다."));
var contextSummary = args.SafeTryGetProperty("context", out var ctx) ? ctx.SafeGetString() ?? "" : "";
// 구조화된 JSON 응답 생성
var result = new Dictionary<string, object>
{
["type"] = "suggest_actions",
["actions"] = actions,
};
if (!string.IsNullOrEmpty(contextSummary))
result["context"] = contextSummary;
var json = JsonSerializer.Serialize(result, new JsonSerializerOptions
{
WriteIndented = true,
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
});
return Task.FromResult(ToolResult.Ok(json));
}
catch (Exception ex)
{
return Task.FromResult(ToolResult.Fail($"액션 제안 오류: {ex.Message}"));
}
}
}