From 67961f280f3de5a10e40bd09169d095bcd7e2483 Mon Sep 17 00:00:00 2001 From: lacvet Date: Sun, 5 Apr 2026 12:23:09 +0900 Subject: [PATCH] =?UTF-8?q?AX=20Agent=20=EC=8B=A4=ED=96=89=20=EC=A4=80?= =?UTF-8?q?=EB=B9=84=20=EB=A1=9C=EC=A7=81=EC=9D=84=20=EC=97=94=EC=A7=84?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=B6=94=EA=B0=80=20=EC=9D=B4=EA=B4=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AxAgentExecutionEngine에 PreparedExecution, PrepareExecution, NormalizeAssistantContent를 추가해 실행 준비와 최종 응답 보정 책임을 더 모음 - ChatWindow의 일반 전송과 재생성이 PrepareExecutionForConversation을 통해 같은 준비 경로를 쓰도록 정리함 - Cowork/Code 시스템 프롬프트 선택, 실행 모드 판정, 프롬프트 스택 조합, outbound message 준비의 중복 분기를 줄임 - 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ 경고 0 / 오류 0 --- README.md | 3 + docs/DEVELOPMENT.md | 4 + .../Services/Agent/AxAgentExecutionEngine.cs | 57 ++++++++++ src/AxCopilot/Views/ChatWindow.xaml.cs | 104 +++++++----------- 4 files changed, 104 insertions(+), 64 deletions(-) diff --git a/README.md b/README.md index c741f77..1ef4a50 100644 --- a/README.md +++ b/README.md @@ -737,8 +737,11 @@ ow + toggle 시각 언어로 통일했습니다. - [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml) 에서는 메시지 컬럼과 빈 상태 폭을 `920px` 축으로 맞추고, 컴포저를 `760px` 기준으로 넓히면서 입력 셸을 하나의 안정적인 하단 컬럼으로 다시 정리했습니다. 같은 수정에서 컴포저 안의 `대화 내보내기` 버튼은 숨겨 `claw-code`처럼 입력과 전송에 더 집중된 구조로 단순화했습니다. - [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 의 입력창 높이 계산은 이제 실제 줄바꿈 수만 기준으로 `Height`를 직접 다시 잡습니다. 전송 후에도 남아 있던 과도한 높이를 줄이고, `Shift+Enter`로 개행이 생길 때만 높이가 커지도록 더 강하게 고정했습니다. - 같은 파일에서 `메시지 편집 후 재생성`, `피드백 후 재생성` 경로도 직접 `AddMessageBubble(...)`를 꽂지 않고 `RenderMessages()` 축으로 다시 돌리게 맞췄습니다. 재생성 경로 자체도 `Cowork/Code`에서는 일반 LLM 호출이 아니라 `ResolveExecutionMode(...)` + `RunAgentLoopAsync(...)`를 타도록 바꿔, 코워크/코드가 채팅 재생성 때 일반 Chat 경로로 잘못 떨어지던 문제를 줄였습니다. +- 이어서 [AxAgentExecutionEngine.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AxAgentExecutionEngine.cs)에 `PrepareExecution(...)`, `NormalizeAssistantContent(...)`를 추가하고, [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)의 일반 전송과 재생성이 모두 같은 준비 함수를 타도록 정리했습니다. 이제 실행 모드 판정, 프롬프트 스택 구성, 전송 메시지 조립, 최종 assistant 내용 보정이 한 엔진 축에서 처리됩니다. +- 이 변경으로 `SendMessageAsync()`와 `SendRegenerateAsync()`가 각자 따로 Cowork/Code 시스템 프롬프트와 실행 모드를 계산하던 중복 분기가 줄었고, 이후 Cowork/Code 엔진을 `claw-code` 기준으로 더 밀 때도 준비 로직은 엔진 한 곳만 고치면 되게 정리했습니다. - 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\` 경고 0 / 오류 0 - 업데이트: 2026-04-05 12:24 (KST) +- 업데이트: 2026-04-05 12:31 (KST) --- diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 3a3ab7f..7e36461 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -4488,4 +4488,8 @@ ow + toggle ?쒓컖 ?몄뼱濡??ㅼ떆 ?뺣젹?덈떎. - [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)의 `UpdateInputBoxHeight()`는 `TextBox.MinLines/MaxLines`만 믿지 않고 실제 줄바꿈 개수 기준으로 `Height`를 직접 다시 계산하도록 바꿨습니다. 이 수정은 Chat/Cowork/Code 공통으로 “전송 후 빈 상태인데 입력창 높이가 남아 있는” 버그를 더 강하게 억제하기 위한 것입니다. - 메시지 편집 후 재생성과 수정 피드백 후 재생성도 직접 `AddMessageBubble(...)`를 삽입하지 않고, conversation 모델 갱신 후 `RenderMessages(preserveViewport: true)`로 다시 그리게 정리했습니다. 그래서 재생성 직전에 UI만 앞서 나가면서 모델과 화면이 어긋나는 경로를 더 줄였습니다. - `SendRegenerateAsync(...)`는 `claw-code` 기준의 단일 실행 축에 더 가깝게 맞췄습니다. 이제 Cowork/Code 재생성도 `ResolveExecutionMode(...)`와 `BuildPromptStack(...)`를 거쳐 필요 시 `RunAgentLoopAsync(...)`를 사용하고, 최종 assistant 텍스트만 커밋합니다. 이전처럼 재생성만 일반 `_llm.SendAsync(...)`로 빠져 코워크/코드가 Chat 경로로 잘못 처리되던 분기를 제거했습니다. +- 업데이트: 2026-04-05 12:31 (KST) +- [AxAgentExecutionEngine.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AxAgentExecutionEngine.cs)에 `PreparedExecution` record와 `PrepareExecution(...)` 메서드를 추가해, 실행 모드 판정(`ResolveExecutionMode`)과 프롬프트 스택 조합(`BuildPromptStack`), 최종 전송 메시지 준비(`PrepareTurn`)를 한 번에 묶도록 정리했습니다. +- 같은 엔진에 `NormalizeAssistantContent(...)`도 옮겨서, 최종 assistant 텍스트가 비었을 때 최근 실행 이벤트 요약을 어떻게 대체할지까지 UI가 아니라 엔진이 책임지게 바꿨습니다. 이건 `claw-code`처럼 UI보다 세션/실행 레이어가 메시지 결과를 더 많이 책임지게 만드는 방향의 일부입니다. +- [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 는 새 `PrepareExecutionForConversation(...)`을 통해 일반 전송과 재생성 모두 같은 엔진 준비 경로를 사용합니다. 그래서 Cowork/Code 시스템 프롬프트 선택, 실행 모드 판정, 프롬프트 스택 구성, outbound message 조립이 각 메서드마다 중복 구현되지 않게 됐고, 다음 단계부터는 AgentLoop 완료 처리와 후속 큐 정리도 같은 방식으로 더 엔진 쪽으로 밀 수 있는 상태가 됐습니다. - 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify\ -p:IntermediateOutputPath=obj\verify\` 경고 0 / 오류 0 diff --git a/src/AxCopilot/Services/Agent/AxAgentExecutionEngine.cs b/src/AxCopilot/Services/Agent/AxAgentExecutionEngine.cs index 0638697..a64e67b 100644 --- a/src/AxCopilot/Services/Agent/AxAgentExecutionEngine.cs +++ b/src/AxCopilot/Services/Agent/AxAgentExecutionEngine.cs @@ -12,6 +12,10 @@ public sealed class AxAgentExecutionEngine { public sealed record PreparedTurn(List Messages); public sealed record ExecutionMode(bool UseAgentLoop, bool UseStreamingTransport, string? TaskSystemPrompt); + public sealed record PreparedExecution( + ExecutionMode Mode, + IReadOnlyList PromptStack, + List Messages); public IReadOnlyList BuildPromptStack( string? conversationSystem, @@ -44,6 +48,32 @@ public sealed class AxAgentExecutionEngine return new ExecutionMode(false, false, null); } + public PreparedExecution PrepareExecution( + ChatConversation conversation, + string runTab, + bool streamingEnabled, + string resolvedService, + string? conversationSystem, + string? slashSystem, + string? coworkSystemPrompt, + string? codeSystemPrompt, + string? fileContext = null, + IReadOnlyList? images = null) + { + var mode = ResolveExecutionMode( + runTab, + streamingEnabled, + resolvedService, + coworkSystemPrompt, + codeSystemPrompt); + var promptStack = BuildPromptStack( + conversationSystem, + slashSystem, + mode.TaskSystemPrompt); + var preparedTurn = PrepareTurn(conversation, promptStack, fileContext, images); + return new PreparedExecution(mode, promptStack, preparedTurn.Messages); + } + public PreparedTurn PrepareTurn( ChatConversation conversation, IEnumerable systemPrompts, @@ -103,6 +133,33 @@ public sealed class AxAgentExecutionEngine return assistant; } + public string NormalizeAssistantContent( + ChatConversation conversation, + string runTab, + string? content) + { + if (!string.IsNullOrWhiteSpace(content)) + return content; + + var latestEventSummary = conversation.ExecutionEvents? + .Where(evt => !string.IsNullOrWhiteSpace(evt.Summary)) + .OrderByDescending(evt => evt.Timestamp) + .Select(evt => evt.Summary.Trim()) + .FirstOrDefault(); + + if (!string.IsNullOrWhiteSpace(latestEventSummary)) + { + return runTab switch + { + "Cowork" => $"코워크 작업이 완료되었습니다.\n\n{latestEventSummary}", + "Code" => $"코드 작업이 완료되었습니다.\n\n{latestEventSummary}", + _ => latestEventSummary, + }; + } + + return "(빈 응답)"; + } + private static ChatMessage CloneMessage(ChatMessage source) { return new ChatMessage diff --git a/src/AxCopilot/Views/ChatWindow.xaml.cs b/src/AxCopilot/Views/ChatWindow.xaml.cs index fad1f1f..a29656a 100644 --- a/src/AxCopilot/Views/ChatWindow.xaml.cs +++ b/src/AxCopilot/Views/ChatWindow.xaml.cs @@ -5728,30 +5728,6 @@ public partial class ChatWindow : Window : ScrollBarVisibility.Disabled; } - private static string BuildAssistantFallbackContent(ChatConversation conv, string runTab, string? content) - { - if (!string.IsNullOrWhiteSpace(content)) - return content; - - var latestEventSummary = conv.ExecutionEvents? - .Where(evt => !string.IsNullOrWhiteSpace(evt.Summary)) - .OrderByDescending(evt => evt.Timestamp) - .Select(evt => evt.Summary.Trim()) - .FirstOrDefault(); - - if (!string.IsNullOrWhiteSpace(latestEventSummary)) - { - return runTab switch - { - "Cowork" => $"코워크 작업이 완료되었습니다.\n\n{latestEventSummary}", - "Code" => $"코드 작업이 완료되었습니다.\n\n{latestEventSummary}", - _ => latestEventSummary, - }; - } - - return "(빈 응답)"; - } - private static void SyncLatestAssistantMessage(ChatConversation conv, string content) { if (conv.Messages.Count == 0) @@ -8393,27 +8369,14 @@ public partial class ChatWindow : Window if (_attachedFiles.Count == 0) AttachedFilesPanel.Visibility = Visibility.Collapsed; } - var coworkSystem = string.Equals(runTab, "Cowork", StringComparison.OrdinalIgnoreCase) - ? BuildCoworkSystemPrompt() - : null; - var codeSystem = string.Equals(runTab, "Code", StringComparison.OrdinalIgnoreCase) - ? BuildCodeSystemPrompt() - : null; - var (resolvedService, _) = _llm.GetCurrentModelInfo(); - var executionMode = _chatEngine.ResolveExecutionMode( + var preparedExecution = PrepareExecutionForConversation( + conv, runTab, - _settings.Settings.Llm.Streaming, - resolvedService, - coworkSystem, - codeSystem); - var promptStack = _chatEngine.BuildPromptStack( - conv.SystemCommand, slashSystem, - executionMode.TaskSystemPrompt); - - sendMessages = _chatEngine - .PrepareTurn(conv, promptStack, fileContext, outboundImages) - .Messages; + fileContext, + outboundImages); + var executionMode = preparedExecution.Mode; + sendMessages = preparedExecution.Messages; // ── 전송 전 컨텍스트 사전 압축 ── { @@ -8502,7 +8465,7 @@ public partial class ChatWindow : Window SetStatusIdle(); } - assistantContent = BuildAssistantFallbackContent(conv, runTab, assistantContent); + assistantContent = _chatEngine.NormalizeAssistantContent(conv, runTab, assistantContent); if (runTab is "Cowork" or "Code") conv.ShowExecutionHistory = false; @@ -8596,6 +8559,34 @@ public partial class ChatWindow : Window } } + private AxAgentExecutionEngine.PreparedExecution PrepareExecutionForConversation( + ChatConversation conversation, + string runTab, + string? slashSystem, + string? fileContext = null, + IReadOnlyList? images = null) + { + var coworkSystem = string.Equals(runTab, "Cowork", StringComparison.OrdinalIgnoreCase) + ? BuildCoworkSystemPrompt() + : null; + var codeSystem = string.Equals(runTab, "Code", StringComparison.OrdinalIgnoreCase) + ? BuildCodeSystemPrompt() + : null; + var (resolvedService, _) = _llm.GetCurrentModelInfo(); + + return _chatEngine.PrepareExecution( + conversation, + runTab, + _settings.Settings.Llm.Streaming, + resolvedService, + conversation.SystemCommand, + slashSystem, + coworkSystem, + codeSystem, + fileContext, + images); + } + // ─── 코워크 에이전트 지원 ──────────────────────────────────────────── private string BuildCoworkSystemPrompt() @@ -10987,27 +10978,12 @@ public partial class ChatWindow : Window try { - var coworkSystem = string.Equals(runTab, "Cowork", StringComparison.OrdinalIgnoreCase) - ? BuildCoworkSystemPrompt() - : null; - var codeSystem = string.Equals(runTab, "Code", StringComparison.OrdinalIgnoreCase) - ? BuildCodeSystemPrompt() - : null; - var (resolvedService, _) = _llm.GetCurrentModelInfo(); - var executionMode = _chatEngine.ResolveExecutionMode( - runTab, - _settings.Settings.Llm.Streaming, - resolvedService, - coworkSystem, - codeSystem); - var promptStack = _chatEngine.BuildPromptStack( - conv.SystemCommand, - null, - executionMode.TaskSystemPrompt); - List sendMessages; + AxAgentExecutionEngine.PreparedExecution preparedExecution; lock (_convLock) - sendMessages = _chatEngine.PrepareTurn(conv, promptStack, null, null).Messages; + preparedExecution = PrepareExecutionForConversation(conv, runTab, null); + var executionMode = preparedExecution.Mode; + sendMessages = preparedExecution.Messages; var response = executionMode.UseAgentLoop ? await RunAgentLoopAsync(runTab, runTab, conv, sendMessages, _streamCts.Token) @@ -11046,7 +11022,7 @@ public partial class ChatWindow : Window SetStatusIdle(); } - assistantContent = BuildAssistantFallbackContent(conv, runTab, assistantContent); + assistantContent = _chatEngine.NormalizeAssistantContent(conv, runTab, assistantContent); if (runTab is "Cowork" or "Code") conv.ShowExecutionHistory = false; lock (_convLock)