From f3a31e97b153963ad0e25de2360b2604d3975257 Mon Sep 17 00:00:00 2001 From: lacvet Date: Wed, 15 Apr 2026 12:51:53 +0900 Subject: [PATCH] =?UTF-8?q?AX=20Agent=20=EC=A7=84=ED=96=89=20=EC=9D=B4?= =?UTF-8?q?=EB=A0=A5=20=ED=8C=8C=ED=8E=B8=20=EB=A9=94=EC=8B=9C=EC=A7=80=20?= =?UTF-8?q?=EC=A0=95=EC=A0=9C=20=EB=B0=8F=20=EB=A0=8C=EB=8D=94=EB=A7=81=20?= =?UTF-8?q?=EC=95=88=EC=A0=95=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 스트리밍 TextDelta와 Thinking summary에 공통 정제기를 적용해 1, [, file_read] 같은 저품질 파편 문구가 이벤트와 카드에 쌓이지 않도록 개선 - V2 라이브 진행 카드와 이력 렌더링에서 정제된 thinking summary만 표시하고 low-signal 조각은 숨기며 process feed는 안전한 기본 문구로 폴백 - AgentProgressSummarySanitizerTests와 AgentLoopResponseClassificationServiceTests를 추가/확장하고 dotnet build 경고 0 오류 0, 지정 테스트 22건 통과를 확인 --- README.md | 8 ++ docs/DEVELOPMENT.md | 8 ++ ...tLoopResponseClassificationServiceTests.cs | 23 +++++ .../AgentProgressSummarySanitizerTests.cs | 47 ++++++++++ .../AgentLoopResponseClassificationService.cs | 9 +- .../Services/Agent/AgentLoopService.cs | 19 +++- .../Agent/AgentProgressSummarySanitizer.cs | 90 +++++++++++++++++++ .../Views/ChatWindow.AgentEventRendering.cs | 6 +- .../ChatWindow.V2AgentEventPresentation.cs | 7 +- .../ChatWindow.V2LiveProgressPresentation.cs | 8 +- 10 files changed, 208 insertions(+), 17 deletions(-) create mode 100644 src/AxCopilot.Tests/Services/AgentProgressSummarySanitizerTests.cs create mode 100644 src/AxCopilot/Services/Agent/AgentProgressSummarySanitizer.cs diff --git a/README.md b/README.md index 1f286c1..62687d7 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,14 @@ Windows 전용 시맨틱 런처 & 워크스페이스 매니저 `docs/claw-code-parity-plan.md` - 업데이트: 2026-04-15 10:57 (KST) +- 업데이트: 2026-04-15 12:51 (KST) +- AX Agent 진행 이력에 `1`, `[`, `file_read]` 같은 깨진 조각이 보이던 문제를 정리했습니다. 새 [AgentProgressSummarySanitizer.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentProgressSummarySanitizer.cs)가 스트리밍 미리보기, `Thinking` 요약, `[이전 도구 호출: ...]` transcript 꼬리 문자열을 공통 규칙으로 정제해 저품질 파편을 제거합니다. +- [AgentLoopService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentLoopService.cs)는 스트리밍 `TextDelta` preview와 일반 `Thinking` emit 전에 정제기를 적용해, 의미 없는 단문은 이벤트 자체를 만들지 않도록 했습니다. +- [ChatWindow.V2LiveProgressPresentation.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.V2LiveProgressPresentation.cs), [ChatWindow.V2AgentEventPresentation.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.V2AgentEventPresentation.cs), [ChatWindow.AgentEventRendering.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.AgentEventRendering.cs)는 렌더링 직전에도 같은 정제 로직을 사용해, 빈약한 조각 문자열은 숨기고 process feed는 `진행 내용 정리`로 안전하게 폴백합니다. +- 테스트: [AgentProgressSummarySanitizerTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/AgentProgressSummarySanitizerTests.cs) 추가, [AgentLoopResponseClassificationServiceTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/AgentLoopResponseClassificationServiceTests.cs) 확장 +- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_agent_progress_cleanup\\ -p:IntermediateOutputPath=obj\\verify_agent_progress_cleanup\\` 경고 0 / 오류 0 +- 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "AgentProgressSummarySanitizerTests|AgentLoopResponseClassificationServiceTests|AgentStatusNarrativeCatalogTests|AgentLoopIterationPreparationServiceTests|AgentToolResultBudgetTests|ChatStorageServiceTests" -p:OutputPath=bin\\verify_agent_progress_cleanup_tests\\ -p:IntermediateOutputPath=obj\\verify_agent_progress_cleanup_tests\\` 통과 22 + - 업데이트: 2026-04-15 12:14 (KST) - 코워크/코드에서 보이는 AX Agent 현재 상태 문구를 더 풍부한 narrative 기준으로 정리했습니다. 새 [AgentStatusNarrativeCatalog.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentStatusNarrativeCatalog.cs)가 이벤트 타입, 도구 성격, 탭(Cowork/Code), 대상 힌트를 함께 해석해 `작업 분석 중 → 관련 파일 확인 중 → 변경 적용 중 → 결과 검증 중`처럼 더 이해하기 쉬운 현재 상태 문구와 상세 설명을 만듭니다. - [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs), [ChatWindow.AgentStatusPresentation.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.AgentStatusPresentation.cs), [ChatWindow.AgentEventRendering.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.AgentEventRendering.cs)는 이제 같은 narrative 카탈로그를 사용해 초기 상태, live pulse 상태, idle 진행 힌트, readable process feed 요약을 일관된 문장으로 보여줍니다. 도구명 한 줄 표시보다 `왜 기다리는지`, `무엇을 정리 중인지`, `다음 단계가 무엇인지`가 더 잘 드러납니다. diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index cd069b2..9d54db4 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -1,4 +1,12 @@ 업데이트: 2026-04-14 19:50 (KST) +업데이트: 2026-04-15 12:51 (KST) +- AX Agent 진행 이력 정제를 위해 `src/AxCopilot/Services/Agent/AgentProgressSummarySanitizer.cs`를 추가했습니다. 스트리밍 중간 preview, `Thinking` 요약, `[이전 도구 호출: ...]` transcript 꼬리, 숫자/대괄호 같은 저품질 조각 문자열을 공통 규칙으로 정리합니다. +- `src/AxCopilot/Services/Agent/AgentLoopService.cs`는 스트리밍 `TextDelta` preview emit과 일반 `Thinking` emit 전에 정제기를 적용합니다. 정제 후 비어버린 summary는 이벤트 자체를 만들지 않아, 중간 응답의 `1`, `[`, `file_read]` 같은 파편이 timeline/history에 쌓이지 않습니다. +- `src/AxCopilot/Views/ChatWindow.V2LiveProgressPresentation.cs`, `src/AxCopilot/Views/ChatWindow.V2AgentEventPresentation.cs`, `src/AxCopilot/Views/ChatWindow.AgentEventRendering.cs`는 렌더링 직전에도 같은 정제 로직을 사용합니다. 기존 세션의 오래된 low-signal thinking event가 다시 그려질 때도 빈약한 단문을 숨기고 process feed에는 `진행 내용 정리`로 폴백합니다. +- 테스트: `src/AxCopilot.Tests/Services/AgentProgressSummarySanitizerTests.cs` 추가, `src/AxCopilot.Tests/Services/AgentLoopResponseClassificationServiceTests.cs` 확장 +- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_agent_progress_cleanup\\ -p:IntermediateOutputPath=obj\\verify_agent_progress_cleanup\\` 경고 0 / 오류 0 +- 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "AgentProgressSummarySanitizerTests|AgentLoopResponseClassificationServiceTests|AgentStatusNarrativeCatalogTests|AgentLoopIterationPreparationServiceTests|AgentToolResultBudgetTests|ChatStorageServiceTests" -p:OutputPath=bin\\verify_agent_progress_cleanup_tests\\ -p:IntermediateOutputPath=obj\\verify_agent_progress_cleanup_tests\\` 통과 22 + 업데이트: 2026-04-14 21:25 (KST) - 문서 생성 고도화 2차를 반영했습니다. `src/AxCopilot/Services/Agent/ArtifactQualityReviewService.cs`를 추가해 HTML/DOCX/XLSX 산출물에 대해 로컬 품질 점수와 보완 포인트를 계산하고, `HtmlSkill`, `DocxSkill`, `ExcelSkill`이 같은 리뷰 모델을 공유하도록 맞췄습니다. - `src/AxCopilot/Services/Agent/DocxSkill.cs`는 `template_path`, `cover_subtitle`, `cover_meta`, `toc`를 지원하도록 확장했습니다. DOCX 템플릿 복제 후 본문을 재구성하고, 커버 페이지와 TOC 필드를 삽입한 뒤 structured review 결과를 함께 반환합니다. diff --git a/src/AxCopilot.Tests/Services/AgentLoopResponseClassificationServiceTests.cs b/src/AxCopilot.Tests/Services/AgentLoopResponseClassificationServiceTests.cs index 51f7647..d4dac4a 100644 --- a/src/AxCopilot.Tests/Services/AgentLoopResponseClassificationServiceTests.cs +++ b/src/AxCopilot.Tests/Services/AgentLoopResponseClassificationServiceTests.cs @@ -39,4 +39,27 @@ public class AgentLoopResponseClassificationServiceTests result.TextParts.Should().HaveCount(2); result.NextConsecutiveNoToolResponses.Should().Be(2); } + + [Fact] + public void BuildThinkingSummary_ShouldDropTranscriptArtifacts() + { + var blocks = new List + { + new() + { + Type = "text", + Text = """ + [이전 도구 호출: file_read] + 1. + file_read] + 실제 수정 범위를 다시 확인합니다. + """ + }, + new() { Type = "tool_use", ToolName = "file_read", ToolId = "tool-1" } + }; + + var result = AgentLoopResponseClassificationService.Classify(blocks, consecutiveNoToolResponses: 0); + + result.BuildThinkingSummary().Should().Be("실제 수정 범위를 다시 확인합니다."); + } } diff --git a/src/AxCopilot.Tests/Services/AgentProgressSummarySanitizerTests.cs b/src/AxCopilot.Tests/Services/AgentProgressSummarySanitizerTests.cs new file mode 100644 index 0000000..76f6fc9 --- /dev/null +++ b/src/AxCopilot.Tests/Services/AgentProgressSummarySanitizerTests.cs @@ -0,0 +1,47 @@ +using AxCopilot.Services.Agent; +using FluentAssertions; +using Xunit; + +namespace AxCopilot.Tests.Services; + +public class AgentProgressSummarySanitizerTests +{ + [Theory] + [InlineData("1")] + [InlineData("1.")] + [InlineData("[")] + [InlineData("[O/")] + [InlineData("file_read]")] + public void NormalizeThinkingSummary_ShouldDropLowSignalFragments(string fragment) + { + AgentProgressSummarySanitizer.NormalizeThinkingSummary(fragment).Should().BeEmpty(); + } + + [Fact] + public void NormalizeThinkingSummary_ShouldStripPreviousToolCallTranscriptHints() + { + var summary = """ + [이전 도구 호출: file_read] + + 1. time-clock.html 파일을 새로 생성했습니다. + """; + + var normalized = AgentProgressSummarySanitizer.NormalizeThinkingSummary(summary); + + normalized.Should().Be("time-clock.html 파일을 새로 생성했습니다."); + } + + [Fact] + public void NormalizeThinkingSummary_ShouldTrimAndClampMeaningfulText() + { + var summary = """ + 1. 현재 디렉터리 확인 중 + 2. 필요한 HTML 구조를 정리하고 있습니다. + """; + + var normalized = AgentProgressSummarySanitizer.NormalizeThinkingSummary(summary, maxLength: 20); + + normalized.Should().StartWith("현재 디렉터리 확인 중 필요한"); + normalized.Should().EndWith("…"); + } +} diff --git a/src/AxCopilot/Services/Agent/AgentLoopResponseClassificationService.cs b/src/AxCopilot/Services/Agent/AgentLoopResponseClassificationService.cs index ba3b871..64d9697 100644 --- a/src/AxCopilot/Services/Agent/AgentLoopResponseClassificationService.cs +++ b/src/AxCopilot/Services/Agent/AgentLoopResponseClassificationService.cs @@ -10,17 +10,12 @@ internal sealed record AgentLoopResponseClassificationResult( { public string BuildThinkingSummary(int maxLength = 150) { - if (string.IsNullOrEmpty(TextResponse)) - return string.Empty; - - return TextResponse.Length > maxLength - ? TextResponse[..maxLength] + "…" - : TextResponse; + return AgentProgressSummarySanitizer.NormalizeThinkingSummary(TextResponse, maxLength: maxLength); } } /// -/// LLM 응답 블록을 텍스트와 tool_use로 분리하고, 무도구 응답 연속 횟수를 계산한다. +/// Splits LLM response blocks into text and tool_use items and tracks consecutive no-tool replies. /// internal static class AgentLoopResponseClassificationService { diff --git a/src/AxCopilot/Services/Agent/AgentLoopService.cs b/src/AxCopilot/Services/Agent/AgentLoopService.cs index 3eabef5..d1c1df1 100644 --- a/src/AxCopilot/Services/Agent/AgentLoopService.cs +++ b/src/AxCopilot/Services/Agent/AgentLoopService.cs @@ -582,10 +582,14 @@ public partial class AgentLoopService var now = DateTime.UtcNow; if ((now - lastStreamUiUpdateAt).TotalMilliseconds >= 450 && streamedTextPreview.Length > 0) { - var preview = streamedTextPreview.ToString(); - preview = preview.Length > 140 ? preview[..140] + "…" : preview; - EmitEvent(AgentEventType.Thinking, "", preview); - lastStreamUiUpdateAt = now; + var preview = AgentProgressSummarySanitizer.NormalizeThinkingSummary( + streamedTextPreview.ToString(), + maxLength: 140); + if (!string.IsNullOrWhiteSpace(preview)) + { + EmitEvent(AgentEventType.Thinking, "", preview); + lastStreamUiUpdateAt = now; + } } } break; @@ -4952,6 +4956,13 @@ public partial class AgentLoopService long elapsedMs = 0, int inputTokens = 0, int outputTokens = 0, string? toolInput = null, int iteration = 0, bool? successOverride = null) { + if (type == AgentEventType.Thinking) + { + summary = AgentProgressSummarySanitizer.NormalizeThinkingSummary(summary, toolName); + if (string.IsNullOrWhiteSpace(summary)) + return; + } + if (type == AgentEventType.Thinking && ShouldSuppressPostCompactionThinking(summary)) { _runPostCompactionSuppressedThinkingCount++; diff --git a/src/AxCopilot/Services/Agent/AgentProgressSummarySanitizer.cs b/src/AxCopilot/Services/Agent/AgentProgressSummarySanitizer.cs new file mode 100644 index 0000000..840abc4 --- /dev/null +++ b/src/AxCopilot/Services/Agent/AgentProgressSummarySanitizer.cs @@ -0,0 +1,90 @@ +using System.Text.RegularExpressions; + +namespace AxCopilot.Services.Agent; + +internal static class AgentProgressSummarySanitizer +{ + private static readonly Regex s_previousToolCallRegex = + new(@"\[이전 도구 호출[^\]\n]*(?:\]|$)", RegexOptions.Compiled); + + private static readonly Regex s_previousToolBundleRegex = + new(@"\[이전 도구 호출 묶음 축약[^\]\n]*(?:\]|$)", RegexOptions.Compiled); + + private static readonly Regex s_listPrefixRegex = + new(@"^\s*(?:\d+\s*[\.\)]|[-*•])\s*", RegexOptions.Compiled); + + private static readonly Regex s_punctuationOnlyRegex = + new(@"^[\[\]\(\)\{\}/\\<>\.\-_:;,`'""*]+$", RegexOptions.Compiled); + + private static readonly Regex s_numericFragmentRegex = + new(@"^\d+\s*[\.\)]?$", RegexOptions.Compiled); + + private static readonly Regex s_toolTokenRegex = + new(@"^[A-Za-z][A-Za-z0-9_\-]{1,40}\]?$", RegexOptions.Compiled); + + public static string NormalizeThinkingSummary(string? summary, string? toolName = null, int maxLength = 0) + { + if (string.IsNullOrWhiteSpace(summary)) + return string.Empty; + + var text = summary + .Replace("\r\n", "\n") + .Replace('\r', '\n') + .Replace("```", string.Empty) + .Replace("**", string.Empty) + .Replace('`', ' '); + + text = s_previousToolCallRegex.Replace(text, string.Empty); + text = s_previousToolBundleRegex.Replace(text, string.Empty); + + var cleanedLines = text + .Split('\n') + .Select(CleanThinkingLine) + .Where(line => !string.IsNullOrWhiteSpace(line)) + .Where(line => !IsLowSignalFragment(line)) + .Distinct(StringComparer.Ordinal) + .ToList(); + + if (cleanedLines.Count == 0) + return string.Empty; + + var normalized = string.Join(" ", cleanedLines); + normalized = Regex.Replace(normalized, @"\s{2,}", " ").Trim(); + + if (maxLength > 0 && normalized.Length > maxLength) + normalized = normalized[..maxLength].TrimEnd() + "…"; + + return normalized; + } + + private static string CleanThinkingLine(string line) + { + var cleaned = s_listPrefixRegex.Replace(line.Trim(), string.Empty).Trim(); + cleaned = cleaned.Trim('*', '-', '•', '[', ']', '(', ')', '{', '}', ':', ';'); + cleaned = Regex.Replace(cleaned, @"\s{2,}", " ").Trim(); + return cleaned; + } + + private static bool IsLowSignalFragment(string line) + { + if (string.IsNullOrWhiteSpace(line)) + return true; + + if (s_numericFragmentRegex.IsMatch(line)) + return true; + + if (s_punctuationOnlyRegex.IsMatch(line)) + return true; + + if (s_toolTokenRegex.IsMatch(line)) + return true; + + if (line.Length <= 3 && line.Any(ch => char.IsPunctuation(ch))) + return true; + + if (line.Length <= 4 && line.All(ch => char.IsPunctuation(ch) || char.IsWhiteSpace(ch))) + return true; + + return false; + } +} diff --git a/src/AxCopilot/Views/ChatWindow.AgentEventRendering.cs b/src/AxCopilot/Views/ChatWindow.AgentEventRendering.cs index 74b1158..74d3ebe 100644 --- a/src/AxCopilot/Views/ChatWindow.AgentEventRendering.cs +++ b/src/AxCopilot/Views/ChatWindow.AgentEventRendering.cs @@ -187,7 +187,11 @@ public partial class ChatWindow AgentEventType.StepDone when evt.StepTotal > 0 => $"{evt.StepCurrent}/{evt.StepTotal} 단계 완료", AgentEventType.Thinking when !string.IsNullOrWhiteSpace(evt.Summary) - => evt.Summary, + => AgentProgressSummarySanitizer.NormalizeThinkingSummary(evt.Summary, evt.ToolName, maxLength: 120) switch + { + { Length: > 0 } cleaned => cleaned, + _ => "진행 내용 정리", + }, AgentEventType.ToolCall => string.IsNullOrWhiteSpace(itemDisplayName) ? $"{transcriptBadgeLabel} 실행" diff --git a/src/AxCopilot/Views/ChatWindow.V2AgentEventPresentation.cs b/src/AxCopilot/Views/ChatWindow.V2AgentEventPresentation.cs index 63c9a2e..9c78549 100644 --- a/src/AxCopilot/Views/ChatWindow.V2AgentEventPresentation.cs +++ b/src/AxCopilot/Views/ChatWindow.V2AgentEventPresentation.cs @@ -302,7 +302,10 @@ public partial class ChatWindow var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; var msgMaxWidth = GetMessageMaxWidth(); - var summary = agentEvent.Summary; + var summary = AgentProgressSummarySanitizer.NormalizeThinkingSummary( + agentEvent.Summary, + agentEvent.ToolName, + maxLength: 200); if (string.IsNullOrWhiteSpace(summary)) return new Border { Width = 0, Height = 0 }; // 빈 thinking은 숨김 @@ -356,7 +359,7 @@ public partial class ChatWindow }); thinkStack.Children.Add(new TextBlock { - Text = summary.Length > 200 ? summary[..200] + "..." : summary, + Text = summary, FontSize = 11, FontStyle = FontStyles.Italic, Foreground = secondaryText, diff --git a/src/AxCopilot/Views/ChatWindow.V2LiveProgressPresentation.cs b/src/AxCopilot/Views/ChatWindow.V2LiveProgressPresentation.cs index 6b8df0e..9492a00 100644 --- a/src/AxCopilot/Views/ChatWindow.V2LiveProgressPresentation.cs +++ b/src/AxCopilot/Views/ChatWindow.V2LiveProgressPresentation.cs @@ -301,7 +301,11 @@ public partial class ChatWindow case AgentEventType.Thinking: { - if (string.IsNullOrWhiteSpace(agentEvent.Summary)) break; + var thinkText = AgentProgressSummarySanitizer.NormalizeThinkingSummary( + agentEvent.Summary, + agentEvent.ToolName, + maxLength: 100); + if (string.IsNullOrWhiteSpace(thinkText)) break; // 사고 과정을 간략히 표시 var thinkRow = new StackPanel @@ -318,8 +322,6 @@ public partial class ChatWindow VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 4, 0), }); - var thinkText = agentEvent.Summary; - if (thinkText.Length > 100) thinkText = thinkText[..100] + "..."; thinkRow.Children.Add(new TextBlock { Text = thinkText,