코드 탭 컨텍스트 격차를 해소하고 대화 영속 작업 세트를 도입함

- ChatConversation에 CodeWorkingSetSnapshot/진단/시맨틱 요약 상태를 추가하고 AgentLoop가 실행 중 갱신한 작업 세트를 대화에 다시 저장하도록 연결함
- CodeTaskWorkingSetService를 스냅샷 복원/시맨틱 연속성 요약/활성 진단 보호 구조로 확장하고 query assembly에서 working set·semantic summary·workspace bootstrap을 protected evidence로 주입하도록 보강함
- ContextCondenser가 compact 중 MsgId, preview, 토큰 메타데이터를 유지하도록 수정하고 신규 compact marker와 요약 문자열을 영어 안정형으로 정리함
- ChatSessionStateService의 분기 대화가 MsgId와 CodeWorkingSet을 유지하도록 보강하고 관련 회귀 테스트(ChatStorage/ContextCondenser/PreLlmStage/QueryAssembly/WorkingSet)를 추가 및 갱신함
- 검증: dotnet build 경고 0 오류 0, CodeTaskWorkingSetServiceTests|AgentLoopQueryAssemblyServiceTests|AgentQueryContextBuilderTests|ContextCondenserTests|AxAgentExecutionEngineTests|AgentLoopLlmDispatchStageServiceTests|ChatStorageServiceTests 26개 통과, AgentLoopE2ETests 포함 관련 컨텍스트 회귀 56개 통과
This commit is contained in:
2026-04-16 09:01:00 +09:00
parent d5dbaa6e4a
commit 998f0c9fd5
20 changed files with 725 additions and 74 deletions

View File

@@ -1,5 +1,16 @@
# AX Commander # AX Commander
- Update: 2026-04-16 09:12 (KST)
- Closed the remaining Code-tab context gap by persisting a durable conversation-owned `CodeWorkingSetSnapshot` and rehydrating it on the next Code run.
- `src/AxCopilot/Models/ChatModels.cs`, `src/AxCopilot/Services/Agent/CodeTaskWorkingSetService.cs`, `src/AxCopilot/Services/Agent/AgentLoopService.cs`, `src/AxCopilot/Services/Agent/AgentLoopCodeWorkingSetPersistence.cs`, and `src/AxCopilot/Views/ChatWindow.xaml.cs` now keep Code continuity across follow-up turns, tab switches, resume, and post-build recovery loops.
- `src/AxCopilot/Services/Agent/AgentLoopQueryAssemblyService.cs`, `src/AxCopilot/Services/Agent/AgentLoopLlmRequestPreparationService.cs`, `src/AxCopilot/Services/Agent/AgentQueryContextBuilder.cs`, and `src/AxCopilot/Services/Agent/AgentLoopContextReliability.cs` now inject and log protected Code evidence layers for the working set, semantic summary, active diagnostics, and workspace bootstrap context.
- `src/AxCopilot/Services/Agent/ContextCondenser.cs` now preserves `MsgId`, preview metadata, and token metadata when compacting messages, and newly written compact markers are emitted with stable English strings instead of mixed legacy markers.
- `src/AxCopilot/Services/ChatSessionStateService.cs` now preserves `CodeWorkingSet` and `MsgId` when branching conversations so forked Code threads do not regress to flat-root scaffolding.
- Verification:
- `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_code_context_gap_closure_final\\ -p:IntermediateOutputPath=obj\\verify_code_context_gap_closure_final\\` warning 0 / error 0
- `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "CodeTaskWorkingSetServiceTests|AgentLoopQueryAssemblyServiceTests|AgentQueryContextBuilderTests|ContextCondenserTests|AxAgentExecutionEngineTests|AgentLoopLlmDispatchStageServiceTests|ChatStorageServiceTests" -p:OutputPath=bin\\verify_code_context_gap_closure_tests\\ -p:IntermediateOutputPath=obj\\verify_code_context_gap_closure_tests\\` passed 26
- `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "AgentLoopE2ETests|AgentMessageInvariantHelperTests|AgentLoopPreLlmStageServiceTests|AgentLoopLlmRequestPreparationServiceTests|AgentLoopQueryAssemblyServiceTests|CodeTaskWorkingSetServiceTests|ContextCondenserTests|AxAgentExecutionEngineTests|ChatStorageServiceTests|AgentQueryContextBuilderTests" -p:OutputPath=bin\\verify_code_context_gap_closure_tests2\\ -p:IntermediateOutputPath=obj\\verify_code_context_gap_closure_tests2\\` passed 56
- Update: 2026-04-16 08:10 (KST) - Update: 2026-04-16 08:10 (KST)
- Merged the external work-log changes recorded in `E:\AX Copilot - Claude\.claude\worktrees\vibrant-mendeleev\docs\WORK_LOG_2026-04-15_22h_to_2026-04-16.md` into the local Code tab implementation. - Merged the external work-log changes recorded in `E:\AX Copilot - Claude\.claude\worktrees\vibrant-mendeleev\docs\WORK_LOG_2026-04-15_22h_to_2026-04-16.md` into the local Code tab implementation.
- The current branch already superseded the external context-persistence track in `AxAgentExecutionEngine` and `AgentQueryContextBuilder`, so the merge focused on the missing Code stats surface instead of overwriting the newer local context pipeline. - The current branch already superseded the external context-persistence track in `AxAgentExecutionEngine` and `AgentQueryContextBuilder`, so the merge focused on the missing Code stats surface instead of overwriting the newer local context pipeline.

View File

@@ -1,5 +1,31 @@
# Code Context Reliability Plan # Code Context Reliability Plan
Update: 2026-04-16 09:12 (KST)
- Implemented the remaining continuity gap-closure items that were still open after the earlier staged loop refactor:
- durable conversation-owned `CodeWorkingSetSnapshot`
- semantic Code tool-batch continuity summaries
- compact-safe `MsgId` and preview metadata preservation
- query-context diagnostics for working-set injection, semantic-summary injection, protected diagnostics, compact-boundary use, and legacy-boundary fallback detection
- workspace-context bootstrap injection for Code requests when `.ax-context.md` is already available
- The active implementation now spans:
- `src/AxCopilot/Models/ChatModels.cs`
- `src/AxCopilot/Services/Agent/CodeTaskWorkingSetService.cs`
- `src/AxCopilot/Services/Agent/AgentLoopService.cs`
- `src/AxCopilot/Services/Agent/AgentLoopCodeWorkingSetPersistence.cs`
- `src/AxCopilot/Services/Agent/AgentLoopQueryAssemblyService.cs`
- `src/AxCopilot/Services/Agent/AgentLoopLlmRequestPreparationService.cs`
- `src/AxCopilot/Services/Agent/AgentQueryContextBuilder.cs`
- `src/AxCopilot/Services/Agent/AgentLoopContextReliability.cs`
- `src/AxCopilot/Services/Agent/ContextCondenser.cs`
- `src/AxCopilot/Services/ChatSessionStateService.cs`
- `src/AxCopilot/Views/ChatWindow.xaml.cs`
- Remaining follow-up after this wave is now smaller and mainly optional:
- broaden the workspace bootstrap path from `LoadContext(...)` reuse into a fuller stale-check refresh only if Code runs show it is still needed
- continue cleaning low-traffic legacy mojibake comments and dormant compatibility strings as those files are touched again
- evaluate whether the durable Code working set should surface in future debug tooling or exports
- keep measuring tool-trace repair frequency in longer real Code sessions to confirm the new durable snapshot and semantic summary blocks reduce repair churn
Update: 2026-04-16 07:40 (KST) Update: 2026-04-16 07:40 (KST)
- Closed the main gaps that were still open versus the comparison checklist: - Closed the main gaps that were still open versus the comparison checklist:

View File

@@ -1908,3 +1908,22 @@ UI ?遺우쁽????域뱀뮆???귐뗫솯?醫딆춦 ???袁る퓮 ?臾믩씜 ??疫
- 테스트: `src/AxCopilot.Tests/Services/CodeStatsAggregatorTests.cs` - 테스트: `src/AxCopilot.Tests/Services/CodeStatsAggregatorTests.cs`
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_code_stats_merge\\ -p:IntermediateOutputPath=obj\\verify_code_stats_merge\\` 경고 0 / 오류 0 - 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_code_stats_merge\\ -p:IntermediateOutputPath=obj\\verify_code_stats_merge\\` 경고 0 / 오류 0
- 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "CodeStatsAggregatorTests|AxAgentExecutionEngineTests|AgentQueryContextBuilderTests|ContextCondenserTests|SettingsServiceTests" -p:OutputPath=bin\\verify_code_stats_merge_tests\\ -p:IntermediateOutputPath=obj\\verify_code_stats_merge_tests\\` 통과 56 - 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "CodeStatsAggregatorTests|AxAgentExecutionEngineTests|AgentQueryContextBuilderTests|ContextCondenserTests|SettingsServiceTests" -p:OutputPath=bin\\verify_code_stats_merge_tests\\ -p:IntermediateOutputPath=obj\\verify_code_stats_merge_tests\\` 통과 56
업데이트: 2026-04-16 09:12 (KST)
- Code context continuity was hardened with a durable `CodeWorkingSetSnapshot` that is now owned by `ChatConversation` and refreshed throughout Code runs.
- `src/AxCopilot/Models/ChatModels.cs` added `CodeWorkingSetSnapshot`, `CodeWorkingSetDiagnosticSnapshot`, and `CodeWorkingSetSemanticSummary`, and `ChatConversation` now persists the snapshot through the normal encrypted conversation save path.
- `src/AxCopilot/Services/Agent/CodeTaskWorkingSetService.cs` now restores snapshots, emits a dedicated semantic summary block, tracks active diagnostics and recent writes as protected evidence, and publishes only materially changed snapshots.
- `src/AxCopilot/Services/Agent/AgentLoopService.cs`, `src/AxCopilot/Services/Agent/AgentLoopCodeWorkingSetPersistence.cs`, and `src/AxCopilot/Views/ChatWindow.xaml.cs` now hydrate the Code working set at loop start, publish updates during tool execution, and persist the latest Code snapshot back into the owning conversation.
- `src/AxCopilot/Services/Agent/AgentLoopQueryAssemblyService.cs`, `src/AxCopilot/Services/Agent/AgentLoopLlmRequestPreparationService.cs`, `src/AxCopilot/Services/Agent/AgentQueryContextBuilder.cs`, and `src/AxCopilot/Services/Agent/AgentLoopContextReliability.cs` now inject and log Code-specific protected evidence for the working set, semantic continuity, active diagnostics, compact-boundary state, legacy marker fallback use, and workspace bootstrap context.
- `src/AxCopilot/Services/Agent/ContextCondenser.cs` now preserves `MsgId`, preview metadata, response timing, and token metadata during compaction, while newly written compaction markers and summaries use stable English strings.
- `src/AxCopilot/Services/ChatSessionStateService.cs` now preserves `CodeWorkingSet` and `MsgId` when branching conversations so forked Code threads keep the same repair chain.
- Tests:
- `src/AxCopilot.Tests/Services/CodeTaskWorkingSetServiceTests.cs`
- `src/AxCopilot.Tests/Services/ChatStorageServiceTests.cs`
- `src/AxCopilot.Tests/Services/ContextCondenserTests.cs`
- `src/AxCopilot.Tests/Services/AgentLoopQueryAssemblyServiceTests.cs`
- `src/AxCopilot.Tests/Services/AgentLoopPreLlmStageServiceTests.cs`
- `src/AxCopilot.Tests/Services/AgentLoopLlmDispatchStageServiceTests.cs`
- Verification:
- `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_code_context_gap_closure_final\\ -p:IntermediateOutputPath=obj\\verify_code_context_gap_closure_final\\` warning 0 / error 0
- `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "CodeTaskWorkingSetServiceTests|AgentLoopQueryAssemblyServiceTests|AgentQueryContextBuilderTests|ContextCondenserTests|AxAgentExecutionEngineTests|AgentLoopLlmDispatchStageServiceTests|ChatStorageServiceTests" -p:OutputPath=bin\\verify_code_context_gap_closure_tests\\ -p:IntermediateOutputPath=obj\\verify_code_context_gap_closure_tests\\` passed 26
- `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "AgentLoopE2ETests|AgentMessageInvariantHelperTests|AgentLoopPreLlmStageServiceTests|AgentLoopLlmRequestPreparationServiceTests|AgentLoopQueryAssemblyServiceTests|CodeTaskWorkingSetServiceTests|ContextCondenserTests|AxAgentExecutionEngineTests|ChatStorageServiceTests|AgentQueryContextBuilderTests" -p:OutputPath=bin\\verify_code_context_gap_closure_tests2\\ -p:IntermediateOutputPath=obj\\verify_code_context_gap_closure_tests2\\` passed 56

View File

@@ -95,7 +95,11 @@ public class AgentLoopLlmDispatchStageServiceTests
InjectedToolReminder: false, InjectedToolReminder: false,
SupplementalMessageCount: 0, SupplementalMessageCount: 0,
FlattenedStructuredAssistantCount: 0, FlattenedStructuredAssistantCount: 0,
ConvertedOrphanToolResultCount: 0); ConvertedOrphanToolResultCount: 0,
InjectedCodeWorkingSet: false,
InjectedSemanticSummary: false,
ProtectedActiveDiagnostics: false,
InjectedWorkspaceContext: false);
var result = await service.ExecuteAsync( var result = await service.ExecuteAsync(
new AgentLoopLlmDispatchStageInput( new AgentLoopLlmDispatchStageInput(
@@ -172,7 +176,11 @@ public class AgentLoopLlmDispatchStageServiceTests
InjectedToolReminder: false, InjectedToolReminder: false,
SupplementalMessageCount: 0, SupplementalMessageCount: 0,
FlattenedStructuredAssistantCount: 0, FlattenedStructuredAssistantCount: 0,
ConvertedOrphanToolResultCount: 0); ConvertedOrphanToolResultCount: 0,
InjectedCodeWorkingSet: false,
InjectedSemanticSummary: false,
ProtectedActiveDiagnostics: false,
InjectedWorkspaceContext: false);
var context = new AgentContext { WorkFolder = @"E:\code", ActiveTab = "Code" }; var context = new AgentContext { WorkFolder = @"E:\code", ActiveTab = "Code" };
await service.ExecuteAsync( await service.ExecuteAsync(

View File

@@ -161,7 +161,8 @@ public class AgentLoopPreLlmStageServiceTests
RuntimeAllowedToolCount: 0)); RuntimeAllowedToolCount: 0));
result.LlmRequest.Should().NotBeNull(); result.LlmRequest.Should().NotBeNull();
result.LlmRequest!.SupplementalMessageCount.Should().Be(1); result.LlmRequest!.SupplementalMessageCount.Should().Be(2);
result.LlmRequest.SendMessages.Should().Contain(message => message.MetaKind == "code_working_set"); result.LlmRequest.SendMessages.Should().Contain(message => message.MetaKind == "code_working_set");
result.LlmRequest.SendMessages.Should().Contain(message => message.MetaKind == "code_semantic_summary");
} }
} }

View File

@@ -93,8 +93,11 @@ public class AgentLoopQueryAssemblyServiceTests
injectPreCallToolReminder: true, injectPreCallToolReminder: true,
noToolCallLoopRetry: 0); noToolCallLoopRetry: 0);
result.SupplementalMessageCount.Should().Be(1); result.SupplementalMessageCount.Should().Be(2);
result.SendMessages.Should().Contain(message => message.MetaKind == "code_working_set"); result.SendMessages.Should().Contain(message => message.MetaKind == "code_working_set");
result.SendMessages.Should().Contain(message => message.MetaKind == "code_semantic_summary");
result.InjectedCodeWorkingSet.Should().BeTrue();
result.InjectedSemanticSummary.Should().BeTrue();
result.SendMessages.Last().Content.Should().Contain("[TOOL_REQUIRED]"); result.SendMessages.Last().Content.Should().Contain("[TOOL_REQUIRED]");
} }
} }

View File

@@ -173,4 +173,62 @@ public class ChatStorageServiceTests
DeleteConversationFile(conversationId); DeleteConversationFile(conversationId);
} }
} }
[Fact]
public void SaveAndLoad_ShouldRoundTripCodeWorkingSetSnapshot()
{
var conversationId = "code-working-set-" + Guid.NewGuid().ToString("N");
try
{
var storage = new ChatStorageService();
var conversation = new ChatConversation
{
Id = conversationId,
Title = "Code working set persistence",
Tab = "Code",
CodeWorkingSet = new CodeWorkingSetSnapshot
{
Goal = "Fix the WPF build",
WorkFolder = @"E:\code",
ScaffoldProfileLabel = "WPF MVVM",
StartedFromEmptyWorkspace = true,
CreatedDirectories = ["Views", "ViewModels"],
RecentWrites = ["Views/MainWindow.xaml"],
ActiveDiagnostics =
[
new CodeWorkingSetDiagnosticSnapshot
{
ToolName = "build_run",
FilePath = "Views/MainWindow.xaml",
LineNumber = 10,
Code = "MC3089",
Message = "Duplicate property."
}
],
LatestSemanticSummary = new CodeWorkingSetSemanticSummary
{
Kind = "verification_failed",
ToolName = "build_run",
Summary = "Verification failed with MC3089.",
Paths = ["Views/MainWindow.xaml"]
}
}
};
storage.Save(conversation);
var loaded = storage.Load(conversationId);
loaded.Should().NotBeNull();
loaded!.CodeWorkingSet.Should().NotBeNull();
loaded.CodeWorkingSet!.Goal.Should().Be("Fix the WPF build");
loaded.CodeWorkingSet.ActiveDiagnostics.Should().ContainSingle();
loaded.CodeWorkingSet.ActiveDiagnostics[0].Code.Should().Be("MC3089");
loaded.CodeWorkingSet.LatestSemanticSummary.Should().NotBeNull();
loaded.CodeWorkingSet.LatestSemanticSummary!.Summary.Should().Contain("MC3089");
}
finally
{
DeleteConversationFile(conversationId);
}
}
} }

View File

@@ -89,4 +89,41 @@ public class CodeTaskWorkingSetServiceTests
message.Should().NotBeNull(); message.Should().NotBeNull();
message!.Content.Should().NotContain("CS0017"); message!.Content.Should().NotContain("CS0017");
} }
[Fact]
public void SnapshotRoundTrip_ShouldPreserveSemanticSummaryAndDiagnostics()
{
var original = new CodeTaskWorkingSetService(
"Create a WPF app",
@"E:\code",
"WPF MVVM",
startedFromEmptyWorkspace: true);
using var mkdirDoc = JsonDocument.Parse("""{"action":"mkdir","paths":["Views","ViewModels"]}""");
original.RecordToolResult("file_manage", mkdirDoc.RootElement, ToolResult.Ok("created structure"));
original.RecordToolResult("build_run", null, new ToolResult
{
Success = false,
Output = @"E:\code\Views\MainWindow.xaml(10,5): error MC3089: Duplicate property"
});
original.TryBuildUpdatedSnapshot(out var snapshot).Should().BeTrue();
snapshot.Should().NotBeNull();
var restored = new CodeTaskWorkingSetService(
"Create a WPF app",
@"E:\code",
"WPF MVVM",
startedFromEmptyWorkspace: true,
snapshot);
var message = restored.BuildChatMessage();
var semanticMessage = restored.BuildSemanticSummaryMessage();
message.Should().NotBeNull();
message!.Content.Should().Contain("Views");
message.Content.Should().Contain("MC3089");
semanticMessage.Should().NotBeNull();
semanticMessage!.Content.Should().Contain("verification_failed");
}
} }

View File

@@ -54,10 +54,12 @@ public class ContextCondenserTests
changed.Should().BeTrue(); changed.Should().BeTrue();
messages.Any(m => messages.Any(m =>
{ {
var c = m.Content ?? ""; var content = m.Content ?? "";
return c.Contains("[축약됨", StringComparison.Ordinal) return content.Contains("[truncated]", StringComparison.Ordinal)
|| c.Contains("[time-based", StringComparison.Ordinal) || content.Contains("[time-based", StringComparison.Ordinal)
|| c.Contains("이전 내용 축약됨", StringComparison.Ordinal); || content.Contains("[previous content truncated", StringComparison.Ordinal)
|| content.Contains("[previous tool result compaction", StringComparison.Ordinal)
|| content.Contains("[previous execution bundle compaction", StringComparison.Ordinal);
}).Should().BeTrue(); }).Should().BeTrue();
} }
@@ -69,6 +71,30 @@ public class ContextCondenserTests
resolved.Should().Be(900_000); resolved.Should().Be(900_000);
} }
[Fact]
public async Task CondenseIfNeededAsync_ShouldPreserveMessageIdentityForCompactedMessages()
{
var settings = new SettingsService();
settings.Settings.Llm.Service = "ollama";
settings.Settings.Llm.Model = "test-model";
using var llm = new LlmService(settings);
var messages = BuildLargeConversation();
var originalId = messages[2].MsgId;
var changed = await ContextCondenser.CondenseIfNeededAsync(
messages,
llm,
maxOutputTokens: 2_000,
proactiveEnabled: true,
triggerPercent: 80,
force: false,
CancellationToken.None);
changed.Should().BeTrue();
messages.Should().Contain(message => message.MsgId == originalId);
}
private static List<ChatMessage> BuildLargeConversation() private static List<ChatMessage> BuildLargeConversation()
{ {
var largeOutput = new string('A', 30_000); var largeOutput = new string('A', 30_000);
@@ -76,18 +102,18 @@ public class ContextCondenserTests
return return
[ [
new ChatMessage { Role = "system", Content = "system prompt" }, new ChatMessage { MsgId = "system-1", Role = "system", Content = "system prompt" },
new ChatMessage { Role = "user", Content = "첫 질문" }, new ChatMessage { MsgId = "user-1", Role = "user", Content = "first request" },
new ChatMessage { Role = "assistant", Content = toolJson }, // Keep the large payload in the old segment. new ChatMessage { MsgId = "assistant-large", Role = "assistant", Content = toolJson },
new ChatMessage { Role = "assistant", Content = "첫 답변" }, new ChatMessage { MsgId = "assistant-2", Role = "assistant", Content = "first reply" },
new ChatMessage { Role = "user", Content = "둘째 질문" }, new ChatMessage { MsgId = "user-2", Role = "user", Content = "follow-up request" },
new ChatMessage { Role = "assistant", Content = "둘째 답변" }, new ChatMessage { MsgId = "assistant-3", Role = "assistant", Content = "follow-up reply" },
new ChatMessage { Role = "user", Content = "셋째 질문" }, new ChatMessage { MsgId = "user-3", Role = "user", Content = "third request" },
new ChatMessage { Role = "assistant", Content = "셋째 답변" }, new ChatMessage { MsgId = "assistant-4", Role = "assistant", Content = "third reply" },
new ChatMessage { Role = "user", Content = "넷째 질문" }, new ChatMessage { MsgId = "user-4", Role = "user", Content = "fourth request" },
new ChatMessage { Role = "assistant", Content = "넷째 답변" }, new ChatMessage { MsgId = "assistant-5", Role = "assistant", Content = "fourth reply" },
new ChatMessage { Role = "user", Content = "다섯째 질문" }, new ChatMessage { MsgId = "user-5", Role = "user", Content = "fifth request" },
new ChatMessage { Role = "assistant", Content = "다섯째 답변" }, new ChatMessage { MsgId = "assistant-6", Role = "assistant", Content = "fifth reply" },
]; ];
} }
} }

View File

@@ -191,6 +191,87 @@ public class ChatConversation
[JsonPropertyName("postCompactionCompletionTokens")] [JsonPropertyName("postCompactionCompletionTokens")]
public int PostCompactionCompletionTokens { get; set; } public int PostCompactionCompletionTokens { get; set; }
[JsonPropertyName("codeWorkingSet")]
public CodeWorkingSetSnapshot? CodeWorkingSet { get; set; }
}
public class CodeWorkingSetSnapshot
{
[JsonPropertyName("goal")]
public string Goal { get; set; } = "";
[JsonPropertyName("workFolder")]
public string WorkFolder { get; set; } = "";
[JsonPropertyName("scaffoldProfileLabel")]
public string? ScaffoldProfileLabel { get; set; }
[JsonPropertyName("startedFromEmptyWorkspace")]
public bool StartedFromEmptyWorkspace { get; set; }
[JsonPropertyName("createdDirectories")]
public List<string> CreatedDirectories { get; set; } = new();
[JsonPropertyName("recentReads")]
public List<string> RecentReads { get; set; } = new();
[JsonPropertyName("recentWrites")]
public List<string> RecentWrites { get; set; } = new();
[JsonPropertyName("activeDiagnostics")]
public List<CodeWorkingSetDiagnosticSnapshot> ActiveDiagnostics { get; set; } = new();
[JsonPropertyName("environmentSummary")]
public string? EnvironmentSummary { get; set; }
[JsonPropertyName("nextFocus")]
public string? NextFocus { get; set; }
[JsonPropertyName("lastMutationSummary")]
public string? LastMutationSummary { get; set; }
[JsonPropertyName("latestSemanticSummary")]
public CodeWorkingSetSemanticSummary? LatestSemanticSummary { get; set; }
[JsonPropertyName("updatedAt")]
public DateTime UpdatedAt { get; set; } = DateTime.Now;
}
public class CodeWorkingSetDiagnosticSnapshot
{
[JsonPropertyName("toolName")]
public string ToolName { get; set; } = "";
[JsonPropertyName("filePath")]
public string FilePath { get; set; } = "";
[JsonPropertyName("lineNumber")]
public int? LineNumber { get; set; }
[JsonPropertyName("code")]
public string Code { get; set; } = "";
[JsonPropertyName("message")]
public string Message { get; set; } = "";
}
public class CodeWorkingSetSemanticSummary
{
[JsonPropertyName("kind")]
public string Kind { get; set; } = "";
[JsonPropertyName("toolName")]
public string ToolName { get; set; } = "";
[JsonPropertyName("summary")]
public string Summary { get; set; } = "";
[JsonPropertyName("paths")]
public List<string> Paths { get; set; } = new();
[JsonPropertyName("updatedAt")]
public DateTime UpdatedAt { get; set; } = DateTime.Now;
} }
public class ChatAgentRunRecord public class ChatAgentRunRecord

View File

@@ -0,0 +1,17 @@
using AxCopilot.Models;
namespace AxCopilot.Services.Agent;
public partial class AgentLoopService
{
private void PublishCodeWorkingSetSnapshot(CodeTaskWorkingSetService? codeWorkingSet)
{
if (codeWorkingSet == null || CodeWorkingSetSnapshotUpdated == null)
return;
if (!codeWorkingSet.TryBuildUpdatedSnapshot(out var snapshot))
return;
CodeWorkingSetSnapshotUpdated(CodeTaskWorkingSetService.CloneSnapshot(snapshot));
}
}

View File

@@ -17,6 +17,6 @@ public partial class AgentLoopService
var toolTraceRepair = llmRequest.FlattenedStructuredAssistantCount > 0 || llmRequest.ConvertedOrphanToolResultCount > 0 var toolTraceRepair = llmRequest.FlattenedStructuredAssistantCount > 0 || llmRequest.ConvertedOrphanToolResultCount > 0
? $"tool_trace_repair=assistants:{llmRequest.FlattenedStructuredAssistantCount}/orphan_results:{llmRequest.ConvertedOrphanToolResultCount}, " ? $"tool_trace_repair=assistants:{llmRequest.FlattenedStructuredAssistantCount}/orphan_results:{llmRequest.ConvertedOrphanToolResultCount}, "
: ""; : "";
return $"{AgentLoopDiagnosticsFormatter.BuildQueryViewSummary(queryView)}, profile={queryView.ProfileName}, protected_recent={queryView.ProtectedRecentNonSystemMessages}, send={llmRequest.SendMessages.Count}, send_tokens={estimatedSendTokens}, supplemental={llmRequest.SupplementalMessageCount}, {toolTraceRepair}tool_reminder={llmRequest.InjectedToolReminder}, working_set={workingSetSummary}"; return $"{AgentLoopDiagnosticsFormatter.BuildQueryViewSummary(queryView)}, profile={queryView.ProfileName}, protected_recent={queryView.ProtectedRecentNonSystemMessages}, send={llmRequest.SendMessages.Count}, send_tokens={estimatedSendTokens}, supplemental={llmRequest.SupplementalMessageCount}, {toolTraceRepair}tool_reminder={llmRequest.InjectedToolReminder}, working_set={workingSetSummary}, injected_working_set={llmRequest.InjectedCodeWorkingSet}, injected_semantic_summary={llmRequest.InjectedSemanticSummary}, protected_diagnostics={llmRequest.ProtectedActiveDiagnostics}, compact_boundary={queryView.BoundaryApplied}, legacy_boundary_fallback={queryView.LegacyBoundaryFallbackUsed}, workspace_bootstrap={llmRequest.InjectedWorkspaceContext}";
} }
} }

View File

@@ -8,7 +8,11 @@ internal sealed record AgentLoopLlmRequestPreparationResult(
bool InjectedToolReminder, bool InjectedToolReminder,
int SupplementalMessageCount, int SupplementalMessageCount,
int FlattenedStructuredAssistantCount, int FlattenedStructuredAssistantCount,
int ConvertedOrphanToolResultCount); int ConvertedOrphanToolResultCount,
bool InjectedCodeWorkingSet,
bool InjectedSemanticSummary,
bool ProtectedActiveDiagnostics,
bool InjectedWorkspaceContext);
/// <summary> /// <summary>
/// Builds the final LLM request array from the query window and optional /// Builds the final LLM request array from the query window and optional
@@ -26,7 +30,15 @@ internal static class AgentLoopLlmRequestPreparationService
{ {
var sendMessages = queryMessages.Select(CloneMessage).ToList(); var sendMessages = queryMessages.Select(CloneMessage).ToList();
var normalization = AgentMessageInvariantHelper.NormalizeHistoricalToolTrace(sendMessages); var normalization = AgentMessageInvariantHelper.NormalizeHistoricalToolTrace(sendMessages);
var supplementalCount = AppendSupplementalMessages(sendMessages, supplementalMessages); var supplementalList = PrepareSupplementalMessages(supplementalMessages);
var supplementalCount = AppendSupplementalMessages(sendMessages, supplementalList);
var injectedCodeWorkingSet = supplementalList.Any(message => string.Equals(message.MetaKind, "code_working_set", StringComparison.OrdinalIgnoreCase));
var injectedSemanticSummary = supplementalList.Any(message => string.Equals(message.MetaKind, "code_semantic_summary", StringComparison.OrdinalIgnoreCase));
var protectedActiveDiagnostics = supplementalList.Any(message =>
string.Equals(message.MetaKind, "code_working_set", StringComparison.OrdinalIgnoreCase)
&& (message.Content ?? string.Empty).Contains("Active diagnostic:", StringComparison.Ordinal));
var injectedWorkspaceContext = supplementalList.Any(message =>
string.Equals(message.MetaKind, "workspace_context_bootstrap", StringComparison.OrdinalIgnoreCase));
var forceInitialToolCall = totalToolCalls == 0 && forceInitialToolCallEnabled; var forceInitialToolCall = totalToolCalls == 0 && forceInitialToolCallEnabled;
if (!forceInitialToolCall if (!forceInitialToolCall
|| !injectPreCallToolReminder || !injectPreCallToolReminder
@@ -38,7 +50,11 @@ internal static class AgentLoopLlmRequestPreparationService
false, false,
supplementalCount, supplementalCount,
normalization.FlattenedAssistantCount, normalization.FlattenedAssistantCount,
normalization.ConvertedOrphanToolResultCount); normalization.ConvertedOrphanToolResultCount,
injectedCodeWorkingSet,
injectedSemanticSummary,
protectedActiveDiagnostics,
injectedWorkspaceContext);
} }
sendMessages.Add(BuildToolReminderMessage()); sendMessages.Add(BuildToolReminderMessage());
@@ -48,7 +64,11 @@ internal static class AgentLoopLlmRequestPreparationService
true, true,
supplementalCount, supplementalCount,
normalization.FlattenedAssistantCount, normalization.FlattenedAssistantCount,
normalization.ConvertedOrphanToolResultCount); normalization.ConvertedOrphanToolResultCount,
injectedCodeWorkingSet,
injectedSemanticSummary,
protectedActiveDiagnostics,
injectedWorkspaceContext);
} }
internal static ChatMessage BuildToolReminderMessage() internal static ChatMessage BuildToolReminderMessage()
@@ -61,17 +81,25 @@ internal static class AgentLoopLlmRequestPreparationService
}; };
} }
private static int AppendSupplementalMessages(List<ChatMessage> sendMessages, IEnumerable<ChatMessage>? supplementalMessages) private static List<ChatMessage> PrepareSupplementalMessages(IEnumerable<ChatMessage>? supplementalMessages)
{ {
if (supplementalMessages == null) if (supplementalMessages == null)
return [];
return supplementalMessages
.Where(message => message != null && !string.IsNullOrWhiteSpace(message.Content))
.Select(CloneMessage)
.ToList();
}
private static int AppendSupplementalMessages(List<ChatMessage> sendMessages, IReadOnlyList<ChatMessage> supplementalMessages)
{
if (supplementalMessages.Count == 0)
return 0; return 0;
var added = 0; var added = 0;
foreach (var message in supplementalMessages) foreach (var message in supplementalMessages)
{ {
if (message == null || string.IsNullOrWhiteSpace(message.Content))
continue;
sendMessages.Add(CloneMessage(message)); sendMessages.Add(CloneMessage(message));
added++; added++;
} }

View File

@@ -79,7 +79,41 @@ internal static class AgentLoopQueryAssemblyService
private static IEnumerable<ChatMessage>? BuildSupplementalMessages(CodeTaskWorkingSetService? codeWorkingSet) private static IEnumerable<ChatMessage>? BuildSupplementalMessages(CodeTaskWorkingSetService? codeWorkingSet)
{ {
var workingSetMessage = codeWorkingSet?.BuildChatMessage(); if (codeWorkingSet == null)
return workingSetMessage is null ? null : [workingSetMessage]; return null;
var messages = new List<ChatMessage>();
var workingSetMessage = codeWorkingSet.BuildChatMessage();
if (workingSetMessage != null)
messages.Add(workingSetMessage);
var semanticSummaryMessage = codeWorkingSet.BuildSemanticSummaryMessage();
if (semanticSummaryMessage != null)
messages.Add(semanticSummaryMessage);
var workspaceContextMessage = BuildWorkspaceContextMessage(codeWorkingSet.WorkFolder);
if (workspaceContextMessage != null)
messages.Add(workspaceContextMessage);
return messages.Count == 0 ? null : messages;
}
private static ChatMessage? BuildWorkspaceContextMessage(string? workFolder)
{
var workspaceContext = WorkspaceContextGenerator.LoadContext(workFolder);
if (string.IsNullOrWhiteSpace(workspaceContext))
return null;
var normalized = workspaceContext.Length <= 1200
? workspaceContext
: workspaceContext[..1200] + "\n...(truncated)";
return new ChatMessage
{
Role = "system",
MetaKind = "workspace_context_bootstrap",
Content = "[workspace-context-bootstrap]\n" + normalized,
Timestamp = DateTime.Now,
};
} }
} }

View File

@@ -113,6 +113,10 @@ public partial class AgentLoopService
/// </summary> /// </summary>
public string? RuntimeWorkFolderOverride { get; set; } public string? RuntimeWorkFolderOverride { get; set; }
public CodeWorkingSetSnapshot? InitialCodeWorkingSetSnapshot { get; set; }
public Action<CodeWorkingSetSnapshot?>? CodeWorkingSetSnapshotUpdated { get; set; }
/// <summary>현재 대화 ID (감사 로그 기록용).</summary> /// <summary>현재 대화 ID (감사 로그 기록용).</summary>
private string _conversationId = ""; private string _conversationId = "";
@@ -311,7 +315,8 @@ public partial class AgentLoopService
userQuery, userQuery,
context.WorkFolder, context.WorkFolder,
explorationState.ScaffoldProfile?.Label, explorationState.ScaffoldProfile?.Label,
workspaceWasInitiallyEmpty) workspaceWasInitiallyEmpty,
InitialCodeWorkingSetSnapshot)
: null; : null;
var preferredInitialToolSequence = BuildPreferredInitialToolSequence( var preferredInitialToolSequence = BuildPreferredInitialToolSequence(
@@ -1525,6 +1530,7 @@ public partial class AgentLoopService
if (!isCodeTab) if (!isCodeTab)
sessionLearnings?.TryExtract(effectiveCall.ToolName, result.Output ?? "", result.Success); sessionLearnings?.TryExtract(effectiveCall.ToolName, result.Output ?? "", result.Success);
codeWorkingSet?.RecordToolResult(effectiveCall.ToolName, effectiveCall.ToolInput, result); codeWorkingSet?.RecordToolResult(effectiveCall.ToolName, effectiveCall.ToolInput, result);
PublishCodeWorkingSetSnapshot(codeWorkingSet);
if (!result.Success) if (!result.Success)
{ {

View File

@@ -19,6 +19,7 @@ public sealed class AgentQueryContextWindowResult
public int ProtectedRecentNonSystemMessages { get; init; } public int ProtectedRecentNonSystemMessages { get; init; }
public int ToolResultSoftCharLimit { get; init; } public int ToolResultSoftCharLimit { get; init; }
public int ToolResultAggregateBudgetChars { get; init; } public int ToolResultAggregateBudgetChars { get; init; }
public bool LegacyBoundaryFallbackUsed { get; init; }
} }
/// <summary> /// <summary>
@@ -79,10 +80,11 @@ public static class AgentQueryContextBuilder
ProtectedRecentNonSystemMessages = options.ToolResultBudget.ProtectedRecentNonSystemMessages, ProtectedRecentNonSystemMessages = options.ToolResultBudget.ProtectedRecentNonSystemMessages,
ToolResultSoftCharLimit = options.ToolResultBudget.SoftCharLimit, ToolResultSoftCharLimit = options.ToolResultBudget.SoftCharLimit,
ToolResultAggregateBudgetChars = options.ToolResultBudget.AggregateBudgetChars, ToolResultAggregateBudgetChars = options.ToolResultBudget.AggregateBudgetChars,
LegacyBoundaryFallbackUsed = false,
}; };
} }
var startIndex = FindWindowStartIndex(sourceMessages, out var boundaryApplied); var startIndex = FindWindowStartIndex(sourceMessages, out var boundaryApplied, out var legacyBoundaryFallbackUsed);
var adjustedStartIndex = AgentMessageInvariantHelper.AdjustStartIndexForToolPairs( var adjustedStartIndex = AgentMessageInvariantHelper.AdjustStartIndexForToolPairs(
sourceMessages, sourceMessages,
startIndex, startIndex,
@@ -124,24 +126,43 @@ public static class AgentQueryContextBuilder
ProtectedRecentNonSystemMessages = options.ToolResultBudget.ProtectedRecentNonSystemMessages, ProtectedRecentNonSystemMessages = options.ToolResultBudget.ProtectedRecentNonSystemMessages,
ToolResultSoftCharLimit = options.ToolResultBudget.SoftCharLimit, ToolResultSoftCharLimit = options.ToolResultBudget.SoftCharLimit,
ToolResultAggregateBudgetChars = options.ToolResultBudget.AggregateBudgetChars, ToolResultAggregateBudgetChars = options.ToolResultBudget.AggregateBudgetChars,
LegacyBoundaryFallbackUsed = legacyBoundaryFallbackUsed,
}; };
} }
private static int FindWindowStartIndex(IReadOnlyList<ChatMessage> sourceMessages, out bool boundaryApplied) private static int FindWindowStartIndex(IReadOnlyList<ChatMessage> sourceMessages, out bool boundaryApplied, out bool legacyBoundaryFallbackUsed)
{ {
for (var i = sourceMessages.Count - 1; i >= 0; i--) for (var i = sourceMessages.Count - 1; i >= 0; i--)
{ {
if (IsQueryBoundaryMarker(sourceMessages[i])) if (TryIsQueryBoundaryMarker(sourceMessages[i], out var usedLegacyFallback))
{ {
boundaryApplied = true; boundaryApplied = true;
legacyBoundaryFallbackUsed = usedLegacyFallback;
return i; return i;
} }
} }
boundaryApplied = false; boundaryApplied = false;
legacyBoundaryFallbackUsed = false;
return 0; return 0;
} }
private static bool TryIsQueryBoundaryMarker(ChatMessage message, out bool usedLegacyFallback)
{
usedLegacyFallback = false;
if (IsQueryBoundaryMarker(message))
{
var content = message.Content ?? "";
usedLegacyFallback = content.StartsWith("[?댁쟾 ?€???붿빟", StringComparison.Ordinal)
|| content.StartsWith("[?몄뀡 硫붾え由??뺤텞", StringComparison.Ordinal)
|| content.StartsWith("[?댁쟾 ?ㅽ뻾 臾띠쓬 ?뺤텞", StringComparison.Ordinal)
|| content.StartsWith("[?댁쟾 ?뺤텞 寃쎄퀎 蹂묓빀", StringComparison.Ordinal);
return true;
}
return false;
}
private static bool IsQueryBoundaryMarker(ChatMessage message) private static bool IsQueryBoundaryMarker(ChatMessage message)
{ {
var metaKind = message.MetaKind ?? ""; var metaKind = message.MetaKind ?? "";
@@ -286,7 +307,7 @@ public static class AgentQueryContextBuilder
} }
else if (string.Equals(message.Role, "assistant", StringComparison.OrdinalIgnoreCase) else if (string.Equals(message.Role, "assistant", StringComparison.OrdinalIgnoreCase)
&& (message.Content ?? string.Empty).StartsWith("{\"_tool_use_blocks\"", StringComparison.Ordinal) && (message.Content ?? string.Empty).StartsWith("{\"_tool_use_blocks\"", StringComparison.Ordinal)
&& TryBuildToolUseSnippet(message, out var toolUseSnippet)) && TryBuildCleanToolUseSnippet(message, out var toolUseSnippet))
{ {
snippet = toolUseSnippet; snippet = toolUseSnippet;
} }
@@ -384,6 +405,16 @@ public static class AgentQueryContextBuilder
} }
} }
private static bool TryBuildCleanToolUseSnippet(ChatMessage message, out string snippet)
{
snippet = "";
if (!TryBuildToolUseSnippet(message, out var legacySnippet))
return false;
snippet = legacySnippet.Replace(" ??", " - ", StringComparison.Ordinal);
return true;
}
private static string TruncateSnippet(string text) private static string TruncateSnippet(string text)
{ {
var normalized = text.Replace('\r', ' ').Replace('\n', ' ').Trim(); var normalized = text.Replace('\r', ' ').Replace('\n', ' ').Trim();

View File

@@ -28,17 +28,22 @@ internal sealed class CodeTaskWorkingSetService
private string _environmentSummary = ""; private string _environmentSummary = "";
private string _nextFocus = ""; private string _nextFocus = "";
private string _lastMutationSummary = ""; private string _lastMutationSummary = "";
private CodeWorkingSetSemanticSummary? _latestSemanticSummary;
private string _lastPublishedFingerprint = "";
public CodeTaskWorkingSetService( public CodeTaskWorkingSetService(
string goal, string goal,
string? workFolder, string? workFolder,
string? scaffoldProfileLabel, string? scaffoldProfileLabel,
bool startedFromEmptyWorkspace) bool startedFromEmptyWorkspace,
CodeWorkingSetSnapshot? snapshot = null)
{ {
_goal = CollapseWhitespace(goal); _goal = CollapseWhitespace(goal);
_workFolder = workFolder?.Trim() ?? ""; _workFolder = workFolder?.Trim() ?? "";
_scaffoldProfileLabel = scaffoldProfileLabel?.Trim() ?? ""; _scaffoldProfileLabel = scaffoldProfileLabel?.Trim() ?? "";
_startedFromEmptyWorkspace = startedFromEmptyWorkspace; _startedFromEmptyWorkspace = startedFromEmptyWorkspace;
RestoreSnapshot(snapshot);
_lastPublishedFingerprint = CreateSnapshotFingerprint(BuildSnapshotCore());
} }
public void RecordToolResult(string toolName, JsonElement? toolInput, ToolResult result) public void RecordToolResult(string toolName, JsonElement? toolInput, ToolResult result)
@@ -77,14 +82,82 @@ internal sealed class CodeTaskWorkingSetService
}; };
} }
public ChatMessage? BuildSemanticSummaryMessage()
{
CodeWorkingSetSemanticSummary? semanticSummary;
lock (_lock)
{
semanticSummary = CloneSemanticSummary(_latestSemanticSummary);
}
if (semanticSummary == null || string.IsNullOrWhiteSpace(semanticSummary.Summary))
return null;
var lines = new List<string> { "[code-semantic-summary]" };
lines.Add($"- Kind: {semanticSummary.Kind}");
lines.Add($"- Summary: {semanticSummary.Summary}");
if (semanticSummary.Paths.Count > 0)
lines.Add("- Paths: " + string.Join(", ", semanticSummary.Paths));
return new ChatMessage
{
Role = "system",
MetaKind = "code_semantic_summary",
Content = string.Join("\n", lines),
Timestamp = semanticSummary.UpdatedAt,
};
}
public bool TryBuildUpdatedSnapshot(out CodeWorkingSetSnapshot? snapshot)
{
lock (_lock)
{
snapshot = BuildSnapshotCore();
var fingerprint = CreateSnapshotFingerprint(snapshot);
if (string.Equals(fingerprint, _lastPublishedFingerprint, StringComparison.Ordinal))
{
snapshot = null;
return false;
}
_lastPublishedFingerprint = fingerprint;
return true;
}
}
public static CodeWorkingSetSnapshot? CloneSnapshot(CodeWorkingSetSnapshot? snapshot)
{
if (snapshot == null)
return null;
return new CodeWorkingSetSnapshot
{
Goal = snapshot.Goal,
WorkFolder = snapshot.WorkFolder,
ScaffoldProfileLabel = snapshot.ScaffoldProfileLabel,
StartedFromEmptyWorkspace = snapshot.StartedFromEmptyWorkspace,
CreatedDirectories = snapshot.CreatedDirectories.ToList(),
RecentReads = snapshot.RecentReads.ToList(),
RecentWrites = snapshot.RecentWrites.ToList(),
ActiveDiagnostics = snapshot.ActiveDiagnostics.Select(CloneDiagnosticSnapshot).ToList(),
EnvironmentSummary = snapshot.EnvironmentSummary,
NextFocus = snapshot.NextFocus,
LastMutationSummary = snapshot.LastMutationSummary,
LatestSemanticSummary = CloneSemanticSummary(snapshot.LatestSemanticSummary),
UpdatedAt = snapshot.UpdatedAt,
};
}
public string DescribeForLog() public string DescribeForLog()
{ {
lock (_lock) lock (_lock)
{ {
return $"writes={_recentWrites.Count}, reads={_recentReads.Count}, dirs={_createdDirectories.Count}, diagnostics={_activeDiagnostics.Count}, next={TruncateSingleLine(_nextFocus, 80)}"; return $"writes={_recentWrites.Count}, reads={_recentReads.Count}, dirs={_createdDirectories.Count}, diagnostics={_activeDiagnostics.Count}, semantic={_latestSemanticSummary?.Kind ?? "none"}, next={TruncateSingleLine(_nextFocus, 80)}";
} }
} }
public string WorkFolder => _workFolder;
private string BuildContentCore() private string BuildContentCore()
{ {
var lines = new List<string> { "[code-working-set]" }; var lines = new List<string> { "[code-working-set]" };
@@ -150,14 +223,32 @@ internal sealed class CodeTaskWorkingSetService
return; return;
} }
var createdDirectories = new List<string>();
foreach (var candidate in ExtractPathCandidates(toolInput)) foreach (var candidate in ExtractPathCandidates(toolInput))
{ {
if (LooksLikeDirectory(candidate)) if (LooksLikeDirectory(candidate))
AddRecentUnique(_createdDirectories, NormalizePath(candidate), MaxListItems); {
var normalized = NormalizePath(candidate);
AddRecentUnique(_createdDirectories, normalized, MaxListItems);
createdDirectories.Add(normalized);
}
} }
if (!string.IsNullOrWhiteSpace(result.FilePath) && LooksLikeDirectory(result.FilePath)) if (!string.IsNullOrWhiteSpace(result.FilePath) && LooksLikeDirectory(result.FilePath))
AddRecentUnique(_createdDirectories, NormalizePath(result.FilePath), MaxListItems); {
var normalized = NormalizePath(result.FilePath);
AddRecentUnique(_createdDirectories, normalized, MaxListItems);
createdDirectories.Add(normalized);
}
if (result.Success && createdDirectories.Count > 0)
{
RecordSemanticSummary(
"structure",
toolName,
"Created project structure: " + string.Join(", ", createdDirectories.Distinct(StringComparer.OrdinalIgnoreCase).Take(4)),
createdDirectories);
}
} }
private void TrackReads(string toolName, JsonElement? toolInput, ToolResult result) private void TrackReads(string toolName, JsonElement? toolInput, ToolResult result)
@@ -195,6 +286,15 @@ internal sealed class CodeTaskWorkingSetService
if (touchedFiles.Count > 0) if (touchedFiles.Count > 0)
_lastMutationSummary = "Updated " + string.Join(", ", touchedFiles.Distinct(StringComparer.OrdinalIgnoreCase).Take(3)); _lastMutationSummary = "Updated " + string.Join(", ", touchedFiles.Distinct(StringComparer.OrdinalIgnoreCase).Take(3));
if (result.Success && touchedFiles.Count > 0)
{
RecordSemanticSummary(
"mutation",
toolName,
"Applied code changes to " + string.Join(", ", touchedFiles.Distinct(StringComparer.OrdinalIgnoreCase).Take(4)),
touchedFiles);
}
} }
private void TrackDiagnostics(string toolName, ToolResult result) private void TrackDiagnostics(string toolName, ToolResult result)
@@ -202,9 +302,18 @@ internal sealed class CodeTaskWorkingSetService
if (!IsVerificationTool(toolName)) if (!IsVerificationTool(toolName))
return; return;
var hadDiagnostics = _activeDiagnostics.Count > 0;
if (result.Success) if (result.Success)
{ {
_activeDiagnostics.Clear(); _activeDiagnostics.Clear();
if (hadDiagnostics)
{
RecordSemanticSummary(
"verification_resolved",
toolName,
"Verification passed after resolving the latest diagnostics.",
_recentWrites);
}
return; return;
} }
@@ -219,12 +328,22 @@ internal sealed class CodeTaskWorkingSetService
{ {
_activeDiagnostics.Clear(); _activeDiagnostics.Clear();
_activeDiagnostics.Add(new CodeDiagnostic(toolName, "", null, "", TruncateSingleLine(fallbackSummary, 180))); _activeDiagnostics.Add(new CodeDiagnostic(toolName, "", null, "", TruncateSingleLine(fallbackSummary, 180)));
RecordSemanticSummary(
"verification_failed",
toolName,
"Verification failed and introduced a new active diagnostic: " + TruncateSingleLine(fallbackSummary, 140),
_recentWrites);
} }
return; return;
} }
_activeDiagnostics.Clear(); _activeDiagnostics.Clear();
_activeDiagnostics.AddRange(diagnostics.Take(3)); _activeDiagnostics.AddRange(diagnostics.Take(3));
RecordSemanticSummary(
"verification_failed",
toolName,
"Verification failed with " + _activeDiagnostics[0].ToSummary(),
_activeDiagnostics.Select(diagnostic => diagnostic.FilePath).Where(path => !string.IsNullOrWhiteSpace(path)));
} }
private void UpdateNextFocus(string toolName, ToolResult result) private void UpdateNextFocus(string toolName, ToolResult result)
@@ -454,6 +573,113 @@ internal sealed class CodeTaskWorkingSetService
return collapsed[..maxLength].TrimEnd() + "..."; return collapsed[..maxLength].TrimEnd() + "...";
} }
private void RestoreSnapshot(CodeWorkingSetSnapshot? snapshot)
{
if (snapshot == null)
return;
foreach (var directory in snapshot.CreatedDirectories)
AddRecentUnique(_createdDirectories, directory, MaxListItems);
foreach (var file in snapshot.RecentReads)
AddRecentUnique(_recentReads, file, MaxListItems);
foreach (var file in snapshot.RecentWrites)
AddRecentUnique(_recentWrites, file, MaxListItems);
_activeDiagnostics.Clear();
_activeDiagnostics.AddRange(snapshot.ActiveDiagnostics.Select(diagnostic =>
new CodeDiagnostic(
diagnostic.ToolName,
diagnostic.FilePath,
diagnostic.LineNumber,
diagnostic.Code,
diagnostic.Message)));
_environmentSummary = CollapseWhitespace(snapshot.EnvironmentSummary);
_nextFocus = CollapseWhitespace(snapshot.NextFocus);
_lastMutationSummary = CollapseWhitespace(snapshot.LastMutationSummary);
_latestSemanticSummary = CloneSemanticSummary(snapshot.LatestSemanticSummary);
}
private CodeWorkingSetSnapshot BuildSnapshotCore()
{
return new CodeWorkingSetSnapshot
{
Goal = _goal,
WorkFolder = _workFolder,
ScaffoldProfileLabel = _scaffoldProfileLabel,
StartedFromEmptyWorkspace = _startedFromEmptyWorkspace,
CreatedDirectories = _createdDirectories.ToList(),
RecentReads = _recentReads.ToList(),
RecentWrites = _recentWrites.ToList(),
ActiveDiagnostics = _activeDiagnostics.Select(diagnostic => new CodeWorkingSetDiagnosticSnapshot
{
ToolName = diagnostic.ToolName,
FilePath = diagnostic.FilePath,
LineNumber = diagnostic.LineNumber,
Code = diagnostic.Code,
Message = diagnostic.Message,
}).ToList(),
EnvironmentSummary = _environmentSummary,
NextFocus = _nextFocus,
LastMutationSummary = _lastMutationSummary,
LatestSemanticSummary = CloneSemanticSummary(_latestSemanticSummary),
UpdatedAt = DateTime.Now,
};
}
private static string CreateSnapshotFingerprint(CodeWorkingSetSnapshot snapshot)
{
return JsonSerializer.Serialize(snapshot);
}
private void RecordSemanticSummary(string kind, string toolName, string summary, IEnumerable<string>? paths)
{
var normalizedSummary = TruncateSingleLine(summary, 220);
if (string.IsNullOrWhiteSpace(normalizedSummary))
return;
_latestSemanticSummary = new CodeWorkingSetSemanticSummary
{
Kind = kind,
ToolName = toolName,
Summary = normalizedSummary,
Paths = paths?
.Where(path => !string.IsNullOrWhiteSpace(path))
.Distinct(StringComparer.OrdinalIgnoreCase)
.Take(4)
.ToList()
?? new List<string>(),
UpdatedAt = DateTime.Now,
};
}
private static CodeWorkingSetDiagnosticSnapshot CloneDiagnosticSnapshot(CodeWorkingSetDiagnosticSnapshot snapshot)
{
return new CodeWorkingSetDiagnosticSnapshot
{
ToolName = snapshot.ToolName,
FilePath = snapshot.FilePath,
LineNumber = snapshot.LineNumber,
Code = snapshot.Code,
Message = snapshot.Message,
};
}
private static CodeWorkingSetSemanticSummary? CloneSemanticSummary(CodeWorkingSetSemanticSummary? summary)
{
if (summary == null)
return null;
return new CodeWorkingSetSemanticSummary
{
Kind = summary.Kind,
ToolName = summary.ToolName,
Summary = summary.Summary,
Paths = summary.Paths.ToList(),
UpdatedAt = summary.UpdatedAt,
};
}
private sealed record CodeDiagnostic( private sealed record CodeDiagnostic(
string ToolName, string ToolName,
string FilePath, string FilePath,

View File

@@ -15,7 +15,7 @@ public sealed class ContextCompactionResult
public int CollapsedBoundaryCount { get; set; } public int CollapsedBoundaryCount { get; set; }
public List<string> AppliedStages { get; } = new(); public List<string> AppliedStages { get; } = new();
public int SavedTokens => Math.Max(0, BeforeTokens - AfterTokens); public int SavedTokens => Math.Max(0, BeforeTokens - AfterTokens);
public string StageSummary => AppliedStages.Count == 0 ? "없음" : string.Join(" -> ", AppliedStages); public string StageSummary => AppliedStages.Count == 0 ? "none" : string.Join(" -> ", AppliedStages);
} }
/// <summary> /// <summary>
@@ -43,7 +43,7 @@ public static class ContextCondenser
private const int RecentKeepCount = 12; private const int RecentKeepCount = 12;
private const int AutoCompactBufferTokens = 13_000; private const int AutoCompactBufferTokens = 13_000;
private const int SummaryReserveTokens = 20_000; private const int SummaryReserveTokens = 20_000;
private const string TimeBasedClearedToolResultMessage = "[time-based microcompact] 이전 tool_result 내용이 정리되었습니다."; private const string TimeBasedClearedToolResultMessage = "[time-based microcompact] Previous tool_result content was cleared.";
private readonly record struct TimeBasedToolResultCompactionConfig(bool Enabled, int GapThresholdMinutes, int KeepRecentToolResults); private readonly record struct TimeBasedToolResultCompactionConfig(bool Enabled, int GapThresholdMinutes, int KeepRecentToolResults);
@@ -237,7 +237,7 @@ public static class ContextCondenser
if (didCompress) if (didCompress)
{ {
var afterTokens = TokenEstimator.EstimateMessages(messages); var afterTokens = TokenEstimator.EstimateMessages(messages);
LogService.Info($"Context Condenser: {originalTokens} {afterTokens} 토큰 (절감 {originalTokens - afterTokens})"); LogService.Info($"Context Condenser: {originalTokens} -> {afterTokens} tokens (saved {originalTokens - afterTokens})");
result.Changed = true; result.Changed = true;
result.AfterTokens = afterTokens; result.AfterTokens = afterTokens;
} }
@@ -366,7 +366,7 @@ public static class ContextCondenser
{ {
if (msg.Content.Length > MaxToolResultChars * 3) if (msg.Content.Length > MaxToolResultChars * 3)
{ {
messages[i] = CloneWithContent(msg, msg.Content[..(MaxToolResultChars * 2)] + "...[축약됨]\"]}"); messages[i] = CloneWithContent(msg, msg.Content[..(MaxToolResultChars * 2)] + "...[truncated]\"]}");
truncated = true; truncated = true;
} }
} }
@@ -374,8 +374,8 @@ public static class ContextCondenser
{ {
messages[i] = CloneWithContent( messages[i] = CloneWithContent(
msg, msg,
msg.Content[..MaxToolResultChars] + "\n\n...[이전 내용 축약됨 — 원본 " + msg.Content[..MaxToolResultChars] + "\n\n...[previous content truncated - kept " +
$"{msg.Content.Length:N0}자 중 {MaxToolResultChars:N0}자 유지]"); $"{MaxToolResultChars:N0} of {msg.Content.Length:N0} chars]");
truncated = true; truncated = true;
} }
} }
@@ -387,13 +387,18 @@ public static class ContextCondenser
{ {
return new ChatMessage return new ChatMessage
{ {
MsgId = source.MsgId,
Role = source.Role, Role = source.Role,
Content = content, Content = content,
Timestamp = source.Timestamp, Timestamp = source.Timestamp,
MetaKind = source.MetaKind, MetaKind = source.MetaKind,
MetaRunId = source.MetaRunId, MetaRunId = source.MetaRunId,
Feedback = source.Feedback, Feedback = source.Feedback,
ResponseElapsedMs = source.ResponseElapsedMs,
PromptTokens = source.PromptTokens,
CompletionTokens = source.CompletionTokens,
AttachedFiles = source.AttachedFiles?.ToList(), AttachedFiles = source.AttachedFiles?.ToList(),
QueryPreviewContent = source.QueryPreviewContent,
Images = source.Images?.Select(x => new ImageAttachment Images = source.Images?.Select(x => new ImageAttachment
{ {
Base64 = x.Base64, Base64 = x.Base64,
@@ -508,9 +513,9 @@ public static class ContextCondenser
AttachedFiles = attachedFiles.Count > 0 ? attachedFiles : null, AttachedFiles = attachedFiles.Count > 0 ? attachedFiles : null,
Content = string.Join("\n", new[] Content = string.Join("\n", new[]
{ {
$"[세션 메모리 압축 - {candidates.Count}개 이전 경계 통합]", $"[session memory compaction - merged {candidates.Count} earlier boundaries]",
"- 이전 요약, 실행 경계, 오래된 메타 로그를 하나의 세션 메모로 합쳤습니다.", "- Earlier summaries, execution boundaries, and stale meta logs were merged into one session memory block.",
attachedFiles.Count > 0 ? $"- 관련 파일: {string.Join(", ", attachedFiles)}" : "- 관련 파일: 없음", attachedFiles.Count > 0 ? $"- Related files: {string.Join(", ", attachedFiles)}" : "- Related files: none",
}) })
}; };
@@ -545,6 +550,11 @@ public static class ContextCondenser
return string.Equals(message.MetaKind, "microcompact_boundary", StringComparison.OrdinalIgnoreCase) return string.Equals(message.MetaKind, "microcompact_boundary", StringComparison.OrdinalIgnoreCase)
|| string.Equals(message.MetaKind, "session_memory_compaction", StringComparison.OrdinalIgnoreCase) || string.Equals(message.MetaKind, "session_memory_compaction", StringComparison.OrdinalIgnoreCase)
|| content.StartsWith("[previous conversation summary", StringComparison.OrdinalIgnoreCase)
|| content.StartsWith("[previous execution bundle compaction", StringComparison.OrdinalIgnoreCase)
|| content.StartsWith("[previous tool result compaction", StringComparison.OrdinalIgnoreCase)
|| content.StartsWith("[previous tool call bundle compaction", StringComparison.OrdinalIgnoreCase)
|| content.StartsWith("[previous execution meta compaction", StringComparison.OrdinalIgnoreCase)
|| content.StartsWith("[이전 대화 요약", StringComparison.Ordinal) || content.StartsWith("[이전 대화 요약", StringComparison.Ordinal)
|| content.StartsWith("[이전 실행 묶음 압축", StringComparison.Ordinal) || content.StartsWith("[이전 실행 묶음 압축", StringComparison.Ordinal)
|| content.StartsWith("[이전 도구 결과 축약", StringComparison.Ordinal) || content.StartsWith("[이전 도구 결과 축약", StringComparison.Ordinal)
@@ -584,7 +594,7 @@ public static class ContextCondenser
Timestamp = group.Last().Timestamp, Timestamp = group.Last().Timestamp,
MetaKind = "collapsed_boundary", MetaKind = "collapsed_boundary",
MetaRunId = group.Last().MetaRunId, MetaRunId = group.Last().MetaRunId,
Content = $"[이전 압축 경계 병합 - {group.Count}개]\n- 이전 compact/snippet 경계를 하나로 합쳤습니다." Content = $"[previous compaction boundary merge - {group.Count} boundaries]\n- Earlier compact/snippet boundaries were merged into one marker."
}); });
collapsedCount += group.Count; collapsedCount += group.Count;
i = j; i = j;
@@ -619,6 +629,9 @@ public static class ContextCondenser
var content = message.Content ?? ""; var content = message.Content ?? "";
return string.Equals(message.MetaKind, "microcompact_boundary", StringComparison.OrdinalIgnoreCase) return string.Equals(message.MetaKind, "microcompact_boundary", StringComparison.OrdinalIgnoreCase)
|| string.Equals(message.MetaKind, "session_memory_compaction", StringComparison.OrdinalIgnoreCase) || string.Equals(message.MetaKind, "session_memory_compaction", StringComparison.OrdinalIgnoreCase)
|| content.StartsWith("[previous execution bundle compaction", StringComparison.OrdinalIgnoreCase)
|| content.StartsWith("[session memory compaction", StringComparison.OrdinalIgnoreCase)
|| content.StartsWith("[previous compaction boundary merge", StringComparison.OrdinalIgnoreCase)
|| content.StartsWith("[이전 실행 묶음 압축", StringComparison.Ordinal) || content.StartsWith("[이전 실행 묶음 압축", StringComparison.Ordinal)
|| content.StartsWith("[세션 메모리 압축", StringComparison.Ordinal) || content.StartsWith("[세션 메모리 압축", StringComparison.Ordinal)
|| content.StartsWith("[이전 압축 경계 병합", StringComparison.Ordinal); || content.StartsWith("[이전 압축 경계 병합", StringComparison.Ordinal);
@@ -646,7 +659,7 @@ public static class ContextCondenser
snipped = CloneWithContent( snipped = CloneWithContent(
source, source,
string.Join("\n", headLines) + string.Join("\n", headLines) +
"\n...[snip: 중간 실행 로그/출력 축약]...\n" + "\n...[snip: middle execution log/output removed]...\n" +
string.Join("\n", tailLines)); string.Join("\n", tailLines));
return true; return true;
} }
@@ -655,7 +668,7 @@ public static class ContextCondenser
var tail = normalized.Length > SnipKeepTailChars ? normalized[^SnipKeepTailChars..] : ""; var tail = normalized.Length > SnipKeepTailChars ? normalized[^SnipKeepTailChars..] : "";
snipped = CloneWithContent( snipped = CloneWithContent(
source, source,
head + "\n...[snip: 중간 내용 축약]...\n" + tail); head + "\n...[snip: middle content removed]...\n" + tail);
return true; return true;
} }
@@ -685,7 +698,7 @@ public static class ContextCondenser
{ {
compacted = CloneWithContent( compacted = CloneWithContent(
source, source,
$"[이전 도구 결과 축약]\n- 상세 출력은 압축되었습니다.\n- 원본 길이: {content.Length:N0}"); $"[previous tool result compaction]\n- Detailed output was compacted.\n- Original length: {content.Length:N0} chars");
return true; return true;
} }
@@ -693,7 +706,7 @@ public static class ContextCondenser
{ {
compacted = CloneWithContent( compacted = CloneWithContent(
source, source,
$"[이전 도구 호출 묶음 축약]\n- 도구 호출 구조는 유지하지 않고 요약만 남겼습니다.\n- 원본 길이: {content.Length:N0}"); $"[previous tool call bundle compaction]\n- The detailed tool call structure was compacted into a short marker.\n- Original length: {content.Length:N0} chars");
return true; return true;
} }
@@ -701,7 +714,7 @@ public static class ContextCondenser
{ {
compacted = CloneWithContent( compacted = CloneWithContent(
source, source,
$"[이전 실행 메타 축약]\n- 종류: {source.MetaKind}\n- 자세한 실행 이벤트는 압축되었습니다."); $"[previous execution meta compaction]\n- Kind: {source.MetaKind}\n- Detailed execution events were compacted.");
return true; return true;
} }
@@ -710,7 +723,7 @@ public static class ContextCondenser
var head = content[..Math.Min(MicrocompactSingleKeepChars, content.Length)]; var head = content[..Math.Min(MicrocompactSingleKeepChars, content.Length)];
compacted = CloneWithContent( compacted = CloneWithContent(
source, source,
head + $"\n\n...[이전 메시지 microcompact 적용 — 원본 {content.Length:N0}]"); head + $"\n\n...[previous message microcompact applied - original {content.Length:N0} chars]");
return true; return true;
} }
@@ -728,15 +741,15 @@ public static class ContextCondenser
var lines = new List<string> var lines = new List<string>
{ {
$"[이전 실행 묶음 압축 — {group.Count}개 메시지]" $"[previous execution bundle compaction - {group.Count} messages]"
}; };
if (toolResults > 0) lines.Add($"- 도구 결과 {toolResults}건 정리"); if (toolResults > 0) lines.Add($"- Compacted tool results: {toolResults}");
if (toolCalls > 0) lines.Add($"- 도구 호출 {toolCalls}건 정리"); if (toolCalls > 0) lines.Add($"- Compacted tool calls: {toolCalls}");
if (metaEvents > 0) lines.Add($"- 실행 메타/로그 {metaEvents}건 정리"); if (metaEvents > 0) lines.Add($"- Compacted execution meta/logs: {metaEvents}");
if (longTexts > 0) lines.Add($"- 긴 설명/출력 {longTexts}건 축약"); if (longTexts > 0) lines.Add($"- Compacted long explanation/output blocks: {longTexts}");
if (attachmentSummary.AttachedFiles.Count > 0) lines.Add($"- 관련 파일: {string.Join(", ", attachmentSummary.AttachedFiles)}"); if (attachmentSummary.AttachedFiles.Count > 0) lines.Add($"- Related files: {string.Join(", ", attachmentSummary.AttachedFiles)}");
if (attachmentSummary.ImageCount > 0) lines.Add($"- 관련 이미지: {attachmentSummary.ImageCount}"); if (attachmentSummary.ImageCount > 0) lines.Add($"- Related images: {attachmentSummary.ImageCount}");
return new ChatMessage return new ChatMessage
{ {
@@ -766,18 +779,18 @@ public static class ContextCondenser
// 요약 대상 텍스트 구성 // 요약 대상 텍스트 구성
var sb = new System.Text.StringBuilder(); var sb = new System.Text.StringBuilder();
sb.AppendLine("다음 대화 기록을 간결하게 요약하세요."); sb.AppendLine("Summarize the following conversation history.");
sb.AppendLine("반드시 유지할 정보: 사용자 요청, 핵심 결정 사항, 생성/수정된 파일 경로, 작업 진행 상황, 중요한 결과."); sb.AppendLine("Preserve the user request, key decisions, created or modified file paths, current progress, and important outcomes.");
sb.AppendLine("제거할 정보: 도구 실행의 상세 출력, 반복되는 내용, 중간 사고 과정."); sb.AppendLine("Remove detailed tool output, repetition, and intermediate reasoning.");
sb.AppendLine("요약은 대화와 동일한 언어로 작성하세요. 글머리 기호(-)로 핵심 사항만 나열하세요.\n---"); sb.AppendLine("Write concise bullet points using the same language as the conversation.\n---");
foreach (var m in oldMessages) foreach (var m in oldMessages)
{ {
var content = m.Content ?? ""; var content = m.Content ?? "";
if (content.StartsWith("{\"_tool_use_blocks\"")) if (content.StartsWith("{\"_tool_use_blocks\""))
content = "[도구 호출]"; content = "[tool call]";
else if (content.StartsWith("{\"type\":\"tool_result\"")) else if (content.StartsWith("{\"type\":\"tool_result\""))
content = "[도구 결과]"; content = "[tool result]";
else if (content.Length > 300) else if (content.Length > 300)
content = content[..300] + "..."; content = content[..300] + "...";
@@ -808,7 +821,7 @@ public static class ContextCondenser
messages.Add(new ChatMessage messages.Add(new ChatMessage
{ {
Role = "assistant", Role = "assistant",
Content = "이전 대화 내용을 확인했습니다. 이어서 작업하겠습니다.", Content = "I reviewed the previous conversation summary and will continue from it.",
Timestamp = DateTime.Now, Timestamp = DateTime.Now,
}); });
@@ -817,7 +830,7 @@ public static class ContextCondenser
} }
catch (Exception ex) catch (Exception ex)
{ {
LogService.Warn($"Context Condenser 요약 실패: {ex.Message}"); LogService.Warn($"Context Condenser summary failed: {ex.Message}");
return false; return false;
} }
} }
@@ -848,14 +861,14 @@ public static class ContextCondenser
{ {
var lines = new List<string> var lines = new List<string>
{ {
$"[이전 대화 요약 — {oldMessageCount}개 메시지 압축]", $"[previous conversation summary - {oldMessageCount} messages compacted]",
summary.Trim() summary.Trim()
}; };
if (attachmentSummary.AttachedFiles.Count > 0) if (attachmentSummary.AttachedFiles.Count > 0)
lines.Add($"참고 파일: {string.Join(", ", attachmentSummary.AttachedFiles)}"); lines.Add($"Reference files: {string.Join(", ", attachmentSummary.AttachedFiles)}");
if (attachmentSummary.ImageCount > 0) if (attachmentSummary.ImageCount > 0)
lines.Add($"참고 이미지: {attachmentSummary.ImageCount}"); lines.Add($"Reference images: {attachmentSummary.ImageCount}");
return string.Join("\n", lines.Where(x => !string.IsNullOrWhiteSpace(x))); return string.Join("\n", lines.Where(x => !string.IsNullOrWhiteSpace(x)));
} }

View File

@@ -257,6 +257,7 @@ public sealed class ChatSessionStateService
Category = source.Category, Category = source.Category,
WorkFolder = source.WorkFolder, WorkFolder = source.WorkFolder,
SystemCommand = source.SystemCommand, SystemCommand = source.SystemCommand,
CodeWorkingSet = AxCopilot.Services.Agent.CodeTaskWorkingSetService.CloneSnapshot(source.CodeWorkingSet),
ParentId = source.Id, ParentId = source.Id,
BranchLabel = branchLabel, BranchLabel = branchLabel,
BranchAtIndex = atIndex, BranchAtIndex = atIndex,
@@ -281,12 +282,16 @@ public sealed class ChatSessionStateService
var m = source.Messages[i]; var m = source.Messages[i];
fork.Messages.Add(new ChatMessage fork.Messages.Add(new ChatMessage
{ {
MsgId = m.MsgId,
Role = m.Role, Role = m.Role,
Content = m.Content, Content = m.Content,
Timestamp = m.Timestamp, Timestamp = m.Timestamp,
MetaKind = m.MetaKind, MetaKind = m.MetaKind,
MetaRunId = m.MetaRunId, MetaRunId = m.MetaRunId,
Feedback = m.Feedback, Feedback = m.Feedback,
ResponseElapsedMs = m.ResponseElapsedMs,
PromptTokens = m.PromptTokens,
CompletionTokens = m.CompletionTokens,
AttachedFiles = m.AttachedFiles?.ToList(), AttachedFiles = m.AttachedFiles?.ToList(),
QueryPreviewContent = m.QueryPreviewContent, QueryPreviewContent = m.QueryPreviewContent,
Images = m.Images?.Select(img => new ImageAttachment Images = m.Images?.Select(img => new ImageAttachment

View File

@@ -6280,6 +6280,20 @@ public partial class ChatWindow : Window
{ {
loop.ActiveTab = runTab; loop.ActiveTab = runTab;
loop.RuntimeWorkFolderOverride = conversation.WorkFolder; loop.RuntimeWorkFolderOverride = conversation.WorkFolder;
loop.InitialCodeWorkingSetSnapshot = CodeTaskWorkingSetService.CloneSnapshot(conversation.CodeWorkingSet);
loop.CodeWorkingSetSnapshotUpdated = snapshot =>
{
var clonedSnapshot = CodeTaskWorkingSetService.CloneSnapshot(snapshot);
lock (_convLock)
{
conversation.CodeWorkingSet = clonedSnapshot;
if (_currentConversation != null
&& string.Equals(_currentConversation.Id, conversation.Id, StringComparison.Ordinal))
{
_currentConversation.CodeWorkingSet = CodeTaskWorkingSetService.CloneSnapshot(clonedSnapshot);
}
}
};
// 에이전트 루프를 백그라운드 스레드에서 실행 — UI 스레드 블록 방지 // 에이전트 루프를 백그라운드 스레드에서 실행 — UI 스레드 블록 방지
// RunAsync 내부의 _llm.SendAsync가 SynchronizationContext로 UI 스레드에 // RunAsync 내부의 _llm.SendAsync가 SynchronizationContext로 UI 스레드에
// 복귀할 때 WPF 메시지 펌프를 독점하여 창 드래그/최소화가 안 되는 문제 해결 // 복귀할 때 WPF 메시지 펌프를 독점하여 창 드래그/최소화가 안 되는 문제 해결
@@ -6328,8 +6342,15 @@ public partial class ChatWindow : Window
finally finally
{ {
StoreAgentLoopMessagesSnapshot(runTab, msgList); StoreAgentLoopMessagesSnapshot(runTab, msgList);
if (string.Equals(runTab, "Code", StringComparison.OrdinalIgnoreCase)
&& conversation.CodeWorkingSet != null)
{
PersistConversationSnapshot(originTab, conversation, "코드 컨텍스트 저장 실패");
}
ResetPermissionRulesForRun(runTab); ResetPermissionRulesForRun(runTab);
loop.RuntimeWorkFolderOverride = null; loop.RuntimeWorkFolderOverride = null;
loop.InitialCodeWorkingSetSnapshot = null;
loop.CodeWorkingSetSnapshotUpdated = null;
loop.EventOccurred -= agentEventHandler; loop.EventOccurred -= agentEventHandler;
loop.UserDecisionCallback = null; loop.UserDecisionCallback = null;
} }