diff --git a/README.md b/README.md index 1e4d1b0..27ea0d7 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,22 @@ # AX Commander +- Update: 2026-04-16 02:05 (KST) +- Continued the Code-tab context reliability work with a structural refactor that moves query/history assembly closer to the staged shape used by `claw-code`. +- Added `src/AxCopilot/Services/Agent/AgentLoopQueryAssemblyService.cs` so `AgentLoopService` no longer directly owns all of the following responsibilities in one place: + - refreshing session learnings for non-Code tabs + - projecting queued commands into the message list + - preparing the query window + - appending Code working-set supplemental context before request dispatch +- `src/AxCopilot/Services/Agent/AgentLoopService.cs` now delegates staged assembly to `AgentLoopQueryAssemblyService.PrepareHistory(...)` and `PrepareRequest(...)`, leaving the loop focused more narrowly on orchestration, hooks, tool execution, and retry flow. +- Added `src/AxCopilot.Tests/Services/AgentLoopQueryAssemblyServiceTests.cs` to lock: + - non-Code session-learning injection + - Code-tab skip behavior for session learnings + - Code working-set supplemental message injection before the tool reminder +- Rewrote `src/AxCopilot.Tests/Services/SessionLearningCollectorTests.cs` in English-only comments and assertions so the test file itself also follows the new encoding/comment rule. +- Validation: + - `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_query_assembly_structure\\ -p:IntermediateOutputPath=obj\\verify_query_assembly_structure\\` warnings 0 / errors 0 + - `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "AgentLoopQueryAssemblyServiceTests|AgentLoopIterationPreparationServiceTests|AgentLoopLlmRequestPreparationServiceTests|AgentMessageInvariantHelperTests|SessionLearningCollectorTests|CodeTaskWorkingSetServiceTests|AgentLoopE2ETests" -p:OutputPath=bin\\verify_query_assembly_structure_tests\\ -p:IntermediateOutputPath=obj\\verify_query_assembly_structure_tests\\` passed 56 + - Update: 2026-04-16 01:28 (KST) - Added an encoding rule to `AGENTS.md`: comments inside code files must be written in English only, and any broken mojibake strings found in touched code files should be rewritten in English before commit. - Reviewed the recent Code tab runs again. On `2026-04-16`, the request message count still grew during long runs (`messages=7 -> 125`, and another run `118 -> 139`), so the main issue is not raw context length. The problem is context fidelity: build/file evidence is being compacted too aggressively while tool-trace repair noise is repeatedly inserted. diff --git a/docs/CODE_CONTEXT_RELIABILITY_PLAN.md b/docs/CODE_CONTEXT_RELIABILITY_PLAN.md index 5d8541f..082f6f6 100644 --- a/docs/CODE_CONTEXT_RELIABILITY_PLAN.md +++ b/docs/CODE_CONTEXT_RELIABILITY_PLAN.md @@ -288,3 +288,18 @@ Updated: 2026-04-16 01:57 (KST) - Remaining follow-up: - measure whether `tool_trace_repair` counts keep trending down in long Code runs after this preflight normalization - continue replacing older mojibake strings outside the active Code execution path + +Updated: 2026-04-16 02:05 (KST) + +- Delivered in this pass: + - structural alignment step: + - `AgentLoopQueryAssemblyService.cs` now owns the staged query/history assembly path. + - `PrepareHistory(...)` handles session-learning refresh plus queued-command/query-window preparation. + - `PrepareRequest(...)` handles Code working-set supplemental context and request-message assembly before dispatch. + - `AgentLoopService.cs` now delegates those responsibilities instead of manually stitching them together inline. + - test and encoding hygiene step: + - `AgentLoopQueryAssemblyServiceTests.cs` locks the new staged assembly behavior. + - `SessionLearningCollectorTests.cs` was rewritten to English-only comments and assertions to match the new repository rule. +- Remaining follow-up: + - keep extracting more inline AgentLoop responsibilities into smaller staged services where it improves observability or retry correctness + - continue measuring long Code runs against claw-code-style continuity scenarios diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index caec3fd..863daaf 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -1830,3 +1830,23 @@ UI ?遺우쁽????域뱀뮆???귐뗫솯?醫딆춦 ???袁る퓮 ?臾믩씜 ??疫 - 검증: - `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_tool_trace_hardening\\ -p:IntermediateOutputPath=obj\\verify_tool_trace_hardening\\` 경고 0 / 오류 0 - `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "AgentMessageInvariantHelperTests|AgentLoopLlmRequestPreparationServiceTests|AgentQueryContextBuilderTests|CodeTaskWorkingSetServiceTests|AgentLoopE2ETests" -p:OutputPath=bin\\verify_tool_trace_hardening_tests\\ -p:IntermediateOutputPath=obj\\verify_tool_trace_hardening_tests\\` 통과 34 + +업데이트: 2026-04-16 02:05 (KST) +- Code 탭 컨텍스트 조립 구조를 `claw-code`의 staged query/history 흐름에 더 가깝게 정리했습니다. + - `src/AxCopilot/Services/Agent/AgentLoopQueryAssemblyService.cs` + - 새 서비스 파일을 추가했습니다. + - 반복당 query/history 조립을 `PrepareHistory(...)`와 `PrepareRequest(...)` 두 단계로 분리했습니다. + - 비-Code 탭에서는 session learnings block refresh를 담당하고, Code 탭에서는 working-set supplemental context를 request assembly 단계에 붙입니다. + - `src/AxCopilot/Services/Agent/AgentLoopService.cs` + - 직접 가지고 있던 session learning refresh / iteration preparation / request supplemental assembly 책임 일부를 `AgentLoopQueryAssemblyService`로 위임했습니다. + - 이제 AgentLoop는 orchestration, hook, tool execution, retry 흐름 중심으로 더 좁아졌습니다. + - `src/AxCopilot.Tests/Services/AgentLoopQueryAssemblyServiceTests.cs` + - non-Code session learning injection + - Code 탭에서 session learning skip + - Code working set supplemental message injection + 를 각각 회귀 테스트로 고정했습니다. + - `src/AxCopilot.Tests/Services/SessionLearningCollectorTests.cs` + - 영어 주석/문구 기준으로 다시 정리해 새 인코딩 규칙에 맞췄습니다. +- 검증: + - `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_query_assembly_structure\\ -p:IntermediateOutputPath=obj\\verify_query_assembly_structure\\` 경고 0 / 오류 0 + - `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "AgentLoopQueryAssemblyServiceTests|AgentLoopIterationPreparationServiceTests|AgentLoopLlmRequestPreparationServiceTests|AgentMessageInvariantHelperTests|SessionLearningCollectorTests|CodeTaskWorkingSetServiceTests|AgentLoopE2ETests" -p:OutputPath=bin\\verify_query_assembly_structure_tests\\ -p:IntermediateOutputPath=obj\\verify_query_assembly_structure_tests\\` 통과 56 diff --git a/src/AxCopilot.Tests/Services/AgentLoopQueryAssemblyServiceTests.cs b/src/AxCopilot.Tests/Services/AgentLoopQueryAssemblyServiceTests.cs new file mode 100644 index 0000000..45aa739 --- /dev/null +++ b/src/AxCopilot.Tests/Services/AgentLoopQueryAssemblyServiceTests.cs @@ -0,0 +1,100 @@ +using System.Text.Json; +using AxCopilot.Models; +using AxCopilot.Services.Agent; +using FluentAssertions; +using Xunit; + +namespace AxCopilot.Tests.Services; + +public class AgentLoopQueryAssemblyServiceTests +{ + [Fact] + public void PrepareHistory_ShouldInjectSessionLearningsForNonCodeTabs() + { + var messages = new List + { + new() + { + Role = "user", + Content = "fix the build" + } + }; + var collector = new SessionLearningCollector(); + collector.TryExtract("build_run", "error CS0246: Missing type", success: false); + + var result = AgentLoopQueryAssemblyService.PrepareHistory( + messages, + new AgentCommandQueue(), + lastToolResultAtUtc: null, + lastToolResultToolName: null, + utcNow: DateTime.UtcNow, + isCodeTab: false, + sessionLearnings: collector, + queryOptions: AgentQueryContextBuilder.AgentQueryContextBuildOptions.CreateDefault()); + + result.RefreshedSessionLearnings.Should().BeTrue(); + messages[0].MetaKind.Should().Be("session_learnings"); + messages[0].Content.Should().Contain("[System:SessionLearnings]"); + result.IterationPreparation.QueryView.Messages.Should().Contain(message => message.MetaKind == "session_learnings"); + } + + [Fact] + public void PrepareHistory_ShouldSkipSessionLearningsForCodeTabs() + { + var messages = new List + { + new() + { + Role = "user", + Content = "fix the build" + } + }; + var collector = new SessionLearningCollector(); + collector.TryExtract("build_run", "error CS0246: Missing type", success: false); + + var result = AgentLoopQueryAssemblyService.PrepareHistory( + messages, + new AgentCommandQueue(), + lastToolResultAtUtc: null, + lastToolResultToolName: null, + utcNow: DateTime.UtcNow, + isCodeTab: true, + sessionLearnings: collector, + queryOptions: AgentQueryContextBuilder.AgentQueryContextBuildOptions.CreateCodeDefault()); + + result.RefreshedSessionLearnings.Should().BeFalse(); + messages.Should().OnlyContain(message => message.MetaKind != "session_learnings"); + } + + [Fact] + public void PrepareRequest_ShouldAppendWorkingSetMessageForCodeRuns() + { + var queryMessages = new List + { + new() + { + Role = "user", + Content = "create the WPF shell" + } + }; + var workingSet = new CodeTaskWorkingSetService( + "create the WPF shell", + @"E:\code", + "wpf-mvvm", + startedFromEmptyWorkspace: true); + using var fileManageInput = JsonDocument.Parse("""{"action":"mkdir","paths":["Views","ViewModels","Themes"]}"""); + workingSet.RecordToolResult("file_manage", fileManageInput.RootElement.Clone(), ToolResult.Ok("created directories")); + + var result = AgentLoopQueryAssemblyService.PrepareRequest( + queryMessages, + workingSet, + totalToolCalls: 0, + forceInitialToolCallEnabled: true, + injectPreCallToolReminder: true, + noToolCallLoopRetry: 0); + + result.SupplementalMessageCount.Should().Be(1); + result.SendMessages.Should().Contain(message => message.MetaKind == "code_working_set"); + result.SendMessages.Last().Content.Should().Contain("[TOOL_REQUIRED]"); + } +} diff --git a/src/AxCopilot.Tests/Services/SessionLearningCollectorTests.cs b/src/AxCopilot.Tests/Services/SessionLearningCollectorTests.cs index 6fafb8e..4f25017 100644 --- a/src/AxCopilot.Tests/Services/SessionLearningCollectorTests.cs +++ b/src/AxCopilot.Tests/Services/SessionLearningCollectorTests.cs @@ -6,9 +6,7 @@ namespace AxCopilot.Tests.Services; public class SessionLearningCollectorTests { - // ═══════════════════════════════════════════ - // 기본 동작 - // ═══════════════════════════════════════════ + // Basic behavior [Fact] public void Empty_BuildInjectionMessage_ReturnsNull() @@ -36,9 +34,7 @@ public class SessionLearningCollectorTests collector.BuildInjectionMessage().Should().BeNull(); } - // ═══════════════════════════════════════════ - // 빌드/테스트 실패 추출 - // ═══════════════════════════════════════════ + // Build / test failure extraction [Fact] public void TryExtract_BuildRunFailure_ExtractsBuildConfig() @@ -68,9 +64,7 @@ public class SessionLearningCollectorTests collector.Count.Should().Be(1); } - // ═══════════════════════════════════════════ - // grep/glob 결과 추출 - // ═══════════════════════════════════════════ + // grep / glob extraction [Fact] public void TryExtract_GrepSuccess_ExtractsCodeLocation() @@ -94,14 +88,11 @@ public class SessionLearningCollectorTests public void TryExtract_GrepSingleFile_NoLearning() { var collector = new SessionLearningCollector(); - // 단일 파일 경로만 있으면 학습 가치 없음 collector.TryExtract("grep", "src/file.cs:10: something", success: true); collector.Count.Should().Be(0); } - // ═══════════════════════════════════════════ - // 프로젝트 메타 파일 추출 - // ═══════════════════════════════════════════ + // Project metadata extraction [Fact] public void TryExtract_CsprojRead_ExtractsProjectStructure() @@ -128,9 +119,7 @@ public class SessionLearningCollectorTests msg.Should().Contain("xunit"); } - // ═══════════════════════════════════════════ - // 런타임 감지 추출 - // ═══════════════════════════════════════════ + // Environment detection extraction [Fact] public void TryExtract_DevEnvDetect_ExtractsDependency() @@ -149,9 +138,7 @@ public class SessionLearningCollectorTests msg.Should().Contain("[dependency]"); } - // ═══════════════════════════════════════════ - // 파일 조작 에러 추출 - // ═══════════════════════════════════════════ + // File operation error extraction [Fact] public void TryExtract_FileWriteFailure_ExtractsErrorPattern() @@ -165,9 +152,7 @@ public class SessionLearningCollectorTests msg.Should().Contain("file_write"); } - // ═══════════════════════════════════════════ - // FIFO 관리 - // ═══════════════════════════════════════════ + // FIFO management [Fact] public void TryExtract_FifoEviction_MaintainsMaxLimit() @@ -175,16 +160,12 @@ public class SessionLearningCollectorTests var collector = new SessionLearningCollector(maxLearnings: 3); for (int i = 0; i < 5; i++) - { collector.TryExtract("file_write", $"Error {i}: unique error message number {i}", success: false); - } collector.Count.Should().BeLessOrEqualTo(3); } - // ═══════════════════════════════════════════ - // 중복 방지 - // ═══════════════════════════════════════════ + // Duplicate prevention [Fact] public void TryExtract_DuplicateContent_NotAdded() @@ -198,9 +179,7 @@ public class SessionLearningCollectorTests collector.Count.Should().Be(1); } - // ═══════════════════════════════════════════ - // BuildInjectionMessage 포맷 - // ═══════════════════════════════════════════ + // BuildInjectionMessage formatting [Fact] public void BuildInjectionMessage_ContainsHeader() @@ -210,12 +189,10 @@ public class SessionLearningCollectorTests var msg = collector.BuildInjectionMessage()!; msg.Should().StartWith("[System:SessionLearnings]"); - msg.Should().Contain("위 내용을 참고하여 동일 실수를 반복하지 마세요"); + msg.Should().Contain("Use these notes to avoid repeating the same failed attempts."); } - // ═══════════════════════════════════════════ - // 안전 가드 - // ═══════════════════════════════════════════ + // Defensive argument handling [Theory] [InlineData(null, "output")] @@ -227,16 +204,13 @@ public class SessionLearningCollectorTests { var collector = new SessionLearningCollector(); collector.TryExtract(toolName!, output!, false); - // No exception = pass } [Fact] public void TryExtract_LargeOutput_TruncatesWithoutCrash() { var collector = new SessionLearningCollector(); - // 50KB의 큰 출력 var largeOutput = "error CS0001: Big error\n" + new string('x', 60_000); collector.TryExtract("build_run", largeOutput, success: false); - // 크래시 없이 완료 = 성공 } } diff --git a/src/AxCopilot/Services/Agent/AgentLoopQueryAssemblyService.cs b/src/AxCopilot/Services/Agent/AgentLoopQueryAssemblyService.cs new file mode 100644 index 0000000..7dbaf8b --- /dev/null +++ b/src/AxCopilot/Services/Agent/AgentLoopQueryAssemblyService.cs @@ -0,0 +1,85 @@ +using AxCopilot.Models; + +namespace AxCopilot.Services.Agent; + +internal sealed record AgentLoopQueryHistoryPreparationResult( + AgentLoopIterationPreparationResult IterationPreparation, + bool RefreshedSessionLearnings); + +/// +/// Assembles the staged query/history pipeline for an AgentLoop iteration. +/// This keeps AgentLoopService focused on orchestration while the assembly +/// rules live in one place. +/// +internal static class AgentLoopQueryAssemblyService +{ + public static AgentLoopQueryHistoryPreparationResult PrepareHistory( + List messages, + AgentCommandQueue pendingCommands, + DateTime? lastToolResultAtUtc, + string? lastToolResultToolName, + DateTime utcNow, + bool isCodeTab, + SessionLearningCollector? sessionLearnings, + AgentQueryContextBuilder.AgentQueryContextBuildOptions? queryOptions = null) + { + var refreshedSessionLearnings = RefreshSessionLearningsBlock(messages, isCodeTab, sessionLearnings); + var iterationPreparation = AgentLoopIterationPreparationService.Prepare( + messages, + pendingCommands, + lastToolResultAtUtc, + lastToolResultToolName, + utcNow, + queryOptions); + + return new AgentLoopQueryHistoryPreparationResult( + iterationPreparation, + refreshedSessionLearnings); + } + + public static AgentLoopLlmRequestPreparationResult PrepareRequest( + IReadOnlyList queryMessages, + CodeTaskWorkingSetService? codeWorkingSet, + int totalToolCalls, + bool forceInitialToolCallEnabled, + bool injectPreCallToolReminder, + int noToolCallLoopRetry) + { + var supplementalMessages = BuildSupplementalMessages(codeWorkingSet); + return AgentLoopLlmRequestPreparationService.Prepare( + queryMessages, + totalToolCalls, + forceInitialToolCallEnabled, + injectPreCallToolReminder, + noToolCallLoopRetry, + supplementalMessages); + } + + internal static bool RefreshSessionLearningsBlock( + List messages, + bool isCodeTab, + SessionLearningCollector? sessionLearnings) + { + if (isCodeTab || sessionLearnings is not { Count: > 0 }) + return false; + + var learningMsg = sessionLearnings.BuildInjectionMessage(); + if (string.IsNullOrWhiteSpace(learningMsg)) + return false; + + messages.RemoveAll(message => string.Equals(message.MetaKind, "session_learnings", StringComparison.OrdinalIgnoreCase)); + messages.Insert(0, new ChatMessage + { + Role = "user", + Content = learningMsg, + MetaKind = "session_learnings", + }); + return true; + } + + private static IEnumerable? BuildSupplementalMessages(CodeTaskWorkingSetService? codeWorkingSet) + { + var workingSetMessage = codeWorkingSet?.BuildChatMessage(); + return workingSetMessage is null ? null : [workingSetMessage]; + } +} diff --git a/src/AxCopilot/Services/Agent/AgentLoopService.cs b/src/AxCopilot/Services/Agent/AgentLoopService.cs index b4269f6..bb7ea77 100644 --- a/src/AxCopilot/Services/Agent/AgentLoopService.cs +++ b/src/AxCopilot/Services/Agent/AgentLoopService.cs @@ -458,13 +458,16 @@ public partial class AgentLoopService } } - var iterationPreparation = AgentLoopIterationPreparationService.Prepare( + var historyPreparation = AgentLoopQueryAssemblyService.PrepareHistory( messages, _pendingCommands, lastToolResultAtUtc, lastToolResultToolName, DateTime.UtcNow, + isCodeTab, + sessionLearnings, queryContextOptions); + var iterationPreparation = historyPreparation.IterationPreparation; if (!string.IsNullOrWhiteSpace(iterationPreparation.ToolResultWaitSummary)) { WorkflowLogService.LogTransition( @@ -530,22 +533,6 @@ public partial class AgentLoopService } } - // Refresh the session learnings block only for non-Code tabs. - if (!isCodeTab && sessionLearnings is { Count: > 0 }) - { - var learningMsg = sessionLearnings.BuildInjectionMessage(); - if (learningMsg != null) - { - messages.RemoveAll(m => m.MetaKind == "session_learnings"); - messages.Insert(0, new ChatMessage - { - Role = "user", - Content = learningMsg, - MetaKind = "session_learnings", - }); - } - } - // P1: 반복당 1회 캐시 — GetRuntimeActiveTools는 동일 파라미터로 반복 내 여러 번 호출됨 var cachedActiveTools = FilterExplorationToolsForCurrentIteration( GetRuntimeActiveTools(llm.DisabledTools, runtimeOverrides), @@ -572,14 +559,13 @@ public partial class AgentLoopService EmitEvent(AgentEventType.Error, "", $"{tabLabel} 탭에서 사용 가능한 도구가 없습니다."); return $"⚠ 현재 {tabLabel} 탭에서 사용 가능한 도구가 없어 작업을 진행할 수 없습니다. 탭별 도구 노출 정책을 확인하세요."; } - var workingSetMessage = codeWorkingSet?.BuildChatMessage(); - var llmRequest = AgentLoopLlmRequestPreparationService.Prepare( + var llmRequest = AgentLoopQueryAssemblyService.PrepareRequest( queryMessages, + codeWorkingSet, totalToolCalls, executionPolicy.ForceInitialToolCall, executionPolicy.InjectPreCallToolReminder, - runState.NoToolCallLoopRetry, - workingSetMessage is null ? null : [workingSetMessage]); + runState.NoToolCallLoopRetry); var forceFirst = llmRequest.ForceInitialToolCall; var sendMessages = llmRequest.SendMessages;