AX Agent transcript 품질 향상과 회귀 기준 정리
Some checks failed
Release Gate / gate (push) Has been cancelled

- 도구/스킬 transcript 표시 카탈로그를 분리해 파일, 빌드, Git, 문서, 질문, 제안, 스킬 분류를 공통 라벨로 통일함
- task summary popup을 active 우선, recent/debug 보조 기준으로 축소하고 transcript policy helper를 partial로 분리함
- AX Agent와 claw-code 비교용 회귀 프롬프트 세트를 별도 문서로 추가하고 관련 개발 문서를 즉시 갱신함
- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ (경고 0 / 오류 0)
This commit is contained in:
2026-04-05 19:20:05 +09:00
parent 8dc2841da6
commit 36828ba199
7 changed files with 262 additions and 68 deletions

View File

@@ -0,0 +1,111 @@
namespace AxCopilot.Services.Agent;
internal static class AgentTranscriptDisplayCatalog
{
public static string GetDisplayName(string? rawName, bool slashPrefix = false)
{
if (string.IsNullOrWhiteSpace(rawName))
return slashPrefix ? "/스킬" : "도구";
var normalized = rawName.Trim();
var mapped = normalized.ToLowerInvariant() switch
{
"file_read" => "파일 읽기",
"file_write" => "파일 쓰기",
"file_edit" => "파일 편집",
"document_reader" => "문서 읽기",
"document_planner" => "문서 계획",
"document_assembler" => "문서 조합",
"document_review" => "문서 검토",
"format_convert" => "형식 변환",
"code_search" => "코드 검색",
"code_review" => "코드 리뷰",
"build_run" => "빌드/실행",
"git_tool" => "Git",
"process" => "프로세스",
"glob" => "파일 찾기",
"grep" => "내용 검색",
"folder_map" => "폴더 맵",
"memory" => "메모리",
"user_ask" => "의견 요청",
"suggest_actions" => "다음 작업 제안",
"task_create" => "작업 생성",
"task_update" => "작업 업데이트",
"task_list" => "작업 목록",
"task_get" => "작업 조회",
"task_stop" => "작업 중지",
"task_output" => "작업 출력",
"spawn_agent" => "서브에이전트",
"wait_agents" => "에이전트 대기",
_ => normalized.Replace('_', ' ').Trim(),
};
if (!slashPrefix)
return mapped;
return normalized.StartsWith('/') ? normalized : "/" + normalized.Replace(' ', '-');
}
public static string GetEventBadgeLabel(AgentEvent evt)
{
if (evt.Type == AgentEventType.SkillCall)
return "스킬";
if (evt.Type is AgentEventType.PermissionRequest or AgentEventType.PermissionGranted or AgentEventType.PermissionDenied)
return "권한";
return GetToolCategoryLabel(evt.ToolName);
}
public static string GetTaskCategoryLabel(string? kind, string? title)
{
if (string.Equals(kind, "permission", System.StringComparison.OrdinalIgnoreCase))
return "권한";
if (string.Equals(kind, "queue", System.StringComparison.OrdinalIgnoreCase))
return "큐";
if (string.Equals(kind, "hook", System.StringComparison.OrdinalIgnoreCase))
return "훅";
if (string.Equals(kind, "subagent", System.StringComparison.OrdinalIgnoreCase))
return "에이전트";
if (string.Equals(kind, "tool", System.StringComparison.OrdinalIgnoreCase))
return GetToolCategoryLabel(title);
return "작업";
}
public static string BuildEventSummary(AgentEvent evt, string displayName)
{
var summary = (evt.Summary ?? "").Trim();
if (!string.IsNullOrWhiteSpace(summary))
return summary;
return evt.Type switch
{
AgentEventType.ToolCall => $"{displayName} 실행 준비",
AgentEventType.ToolResult => evt.Success ? $"{displayName} 실행 완료" : $"{displayName} 실행 실패",
AgentEventType.SkillCall => $"{displayName} 실행",
AgentEventType.PermissionRequest => $"{displayName} 실행 전 사용자 확인 필요",
AgentEventType.PermissionGranted => $"{displayName} 실행이 허용됨",
AgentEventType.PermissionDenied => $"{displayName} 실행이 거부됨",
_ => summary,
};
}
private static string GetToolCategoryLabel(string? rawName)
{
if (string.IsNullOrWhiteSpace(rawName))
return "도구";
return rawName.Trim().ToLowerInvariant() switch
{
"file_read" or "file_write" or "file_edit" or "glob" or "grep" or "folder_map" or "file_watch" or "file_info" or "file_manage" => "파일",
"build_run" or "test_loop" or "dev_env_detect" => "빌드",
"git_tool" or "diff_tool" or "diff_preview" => "Git",
"document_reader" or "document_planner" or "document_assembler" or "document_review" or "format_convert" or "template_render" => "문서",
"user_ask" => "질문",
"suggest_actions" => "제안",
"process" => "실행",
"spawn_agent" or "wait_agents" => "에이전트",
_ => "도구",
};
}
}

View File

@@ -0,0 +1,26 @@
using System.Windows.Media;
using AxCopilot.Services;
using AxCopilot.Services.Agent;
namespace AxCopilot.Views;
public partial class ChatWindow
{
private bool IsDebugTranscriptMode()
=> string.Equals(_settings.Settings.Llm.AgentLogLevel, "debug", StringComparison.OrdinalIgnoreCase);
private string GetTranscriptBadgeLabel(AgentEvent evt)
=> AgentTranscriptDisplayCatalog.GetEventBadgeLabel(evt);
private string GetTranscriptTaskCategory(TaskRunStore.TaskRun task)
=> AgentTranscriptDisplayCatalog.GetTaskCategoryLabel(task.Kind, task.Title);
private string GetTranscriptDisplayName(string? rawName, bool slashPrefix = false)
=> AgentTranscriptDisplayCatalog.GetDisplayName(rawName, slashPrefix);
private string GetTranscriptEventSummary(AgentEvent evt, string displayName)
=> AgentTranscriptDisplayCatalog.BuildEventSummary(evt, displayName);
private bool ShouldIncludeRecentTaskSummary(IReadOnlyCollection<TaskRunStore.TaskRun> activeTasks)
=> IsDebugTranscriptMode() || activeTasks.Count == 0;
}

View File

@@ -9375,73 +9375,16 @@ public partial class ChatWindow : Window
_ => "진행 중",
};
private static string GetAgentItemDisplayName(string? rawName, bool slashPrefix = false)
{
if (string.IsNullOrWhiteSpace(rawName))
return slashPrefix ? "/스킬" : "도구";
var normalized = rawName.Trim();
var mapped = normalized.ToLowerInvariant() switch
{
"file_read" => "파일 읽기",
"file_write" => "파일 쓰기",
"file_edit" => "파일 편집",
"document_reader" => "문서 읽기",
"document_planner" => "문서 계획",
"document_assembler" => "문서 조합",
"code_search" => "코드 검색",
"code_review" => "코드 리뷰",
"build_run" => "빌드/실행",
"git_tool" => "Git",
"process" => "프로세스",
"glob" => "파일 찾기",
"grep" => "내용 검색",
"folder_map" => "폴더 맵",
"memory" => "메모리",
"user_ask" => "의견 요청",
"suggest_actions" => "다음 작업 제안",
"task_create" => "작업 생성",
"task_update" => "작업 업데이트",
"task_list" => "작업 목록",
"task_get" => "작업 조회",
"task_stop" => "작업 중지",
"task_output" => "작업 출력",
"spawn_agent" => "서브에이전트",
"wait_agents" => "에이전트 대기",
_ => normalized.Replace('_', ' ').Trim(),
};
if (!slashPrefix)
return mapped;
var slashName = normalized.StartsWith('/')
? normalized
: "/" + normalized.Replace(' ', '-');
return slashName;
}
private string GetAgentItemDisplayName(string? rawName, bool slashPrefix = false)
=> GetTranscriptDisplayName(rawName, slashPrefix);
private static bool IsTranscriptToolLikeEvent(AgentEvent evt)
=> evt.Type is AgentEventType.ToolCall or AgentEventType.ToolResult or AgentEventType.SkillCall
|| (!string.IsNullOrWhiteSpace(evt.ToolName)
&& evt.Type is AgentEventType.PermissionRequest or AgentEventType.PermissionGranted or AgentEventType.PermissionDenied);
private static string BuildAgentEventSummaryText(AgentEvent evt, string displayName)
{
var summary = (evt.Summary ?? "").Trim();
if (!string.IsNullOrWhiteSpace(summary))
return summary;
return evt.Type switch
{
AgentEventType.ToolCall => $"{displayName} 실행 준비",
AgentEventType.ToolResult => evt.Success ? $"{displayName} 실행 완료" : $"{displayName} 실행 실패",
AgentEventType.SkillCall => $"{displayName} 실행",
AgentEventType.PermissionRequest => $"{displayName} 실행 전 사용자 확인 필요",
AgentEventType.PermissionGranted => $"{displayName} 실행이 허용됨",
AgentEventType.PermissionDenied => $"{displayName} 실행이 거부됨",
_ => summary,
};
}
private string BuildAgentEventSummaryText(AgentEvent evt, string displayName)
=> GetTranscriptEventSummary(evt, displayName);
private IEnumerable<TaskRunStore.TaskRun> FilterTaskSummaryItems(IEnumerable<TaskRunStore.TaskRun> tasks)
=> _taskSummaryTaskFilter switch
@@ -10794,9 +10737,9 @@ public partial class ChatWindow : Window
AgentEventType.PermissionGranted => GetPermissionBadgeMeta(evt.ToolName, pending: false),
AgentEventType.PermissionDenied => ("\uE783", "권한 거부", "#FEF2F2", "#DC2626"),
AgentEventType.Decision => GetDecisionBadgeMeta(evt.Summary),
AgentEventType.ToolCall => ("\uE8A7", "도구", "#EEF6FF", "#3B82F6"),
AgentEventType.ToolResult => ("\uE73E", "도구 결과", "#EEF9EE", "#16A34A"),
AgentEventType.SkillCall => ("\uE8A5", "스킬", "#FFF7ED", "#EA580C"),
AgentEventType.ToolCall => ("\uE8A7", GetTranscriptBadgeLabel(evt), "#EEF6FF", "#3B82F6"),
AgentEventType.ToolResult => ("\uE73E", GetTranscriptBadgeLabel(evt), "#EEF9EE", "#16A34A"),
AgentEventType.SkillCall => ("\uE8A5", GetTranscriptBadgeLabel(evt), "#FFF7ED", "#EA580C"),
AgentEventType.Error => ("\uE783", "오류", "#FEF2F2", "#DC2626"),
AgentEventType.Complete => ("\uE930", "완료", "#F0FFF4", "#15803D"),
AgentEventType.StepDone => ("\uE73E", "단계 완료", "#EEF9EE", "#16A34A"),
@@ -20795,13 +20738,19 @@ private static (string icon, string label, string bgHex, string fgHex) GetDecisi
}
}
foreach (var task in FilterTaskSummaryItems(_appState.ActiveTasks).Take(3))
var activeTasks = FilterTaskSummaryItems(_appState.ActiveTasks).Take(3).ToList();
var recentTasks = FilterTaskSummaryItems(_appState.RecentTasks).Take(2).ToList();
foreach (var task in activeTasks)
panel.Children.Add(BuildTaskSummaryCard(task, active: true));
foreach (var task in FilterTaskSummaryItems(_appState.RecentTasks).Take(2))
panel.Children.Add(BuildTaskSummaryCard(task, active: false));
if (ShouldIncludeRecentTaskSummary(activeTasks))
{
foreach (var task in recentTasks)
panel.Children.Add(BuildTaskSummaryCard(task, active: false));
}
if (!FilterTaskSummaryItems(_appState.ActiveTasks).Any() && !FilterTaskSummaryItems(_appState.RecentTasks).Any())
if (activeTasks.Count == 0 && recentTasks.Count == 0)
{
panel.Children.Add(new TextBlock
{
@@ -21591,6 +21540,7 @@ private static (string icon, string label, string bgHex, string fgHex) GetDecisi
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.Black;
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.DimGray;
var (kindIcon, kindColor) = GetTaskKindVisual(task.Kind);
var categoryLabel = GetTranscriptTaskCategory(task);
var displayTitle = string.Equals(task.Kind, "tool", StringComparison.OrdinalIgnoreCase)
|| string.Equals(task.Kind, "permission", StringComparison.OrdinalIgnoreCase)
|| string.Equals(task.Kind, "hook", StringComparison.OrdinalIgnoreCase)
@@ -21623,6 +21573,24 @@ private static (string icon, string label, string bgHex, string fgHex) GetDecisi
});
taskStack.Children.Add(headerRow);
taskStack.Children.Add(new Border
{
Background = BrushFromHex("#F8FAFC"),
BorderBrush = BrushFromHex("#E5E7EB"),
BorderThickness = new Thickness(1),
CornerRadius = new CornerRadius(999),
Padding = new Thickness(6, 1, 6, 1),
Margin = new Thickness(0, 0, 0, 4),
HorizontalAlignment = HorizontalAlignment.Left,
Child = new TextBlock
{
Text = categoryLabel,
FontSize = 8,
FontWeight = FontWeights.SemiBold,
Foreground = secondaryText,
},
});
if (!string.IsNullOrWhiteSpace(task.Summary))
{
taskStack.Children.Add(new TextBlock