코드 탭 컨텍스트 격차를 해소하고 대화 영속 작업 세트를 도입함
- 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:
11
README.md
11
README.md
@@ -1,5 +1,16 @@
|
||||
# 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)
|
||||
- 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.
|
||||
|
||||
@@ -1,5 +1,31 @@
|
||||
# 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)
|
||||
|
||||
- Closed the main gaps that were still open versus the comparison checklist:
|
||||
|
||||
@@ -1908,3 +1908,22 @@ UI ?遺우쁽????域뱀뮆???귐뗫솯?醫딆춦 ???袁る퓮 ?臾믩씜 ??疫
|
||||
- 테스트: `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 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
|
||||
|
||||
@@ -95,7 +95,11 @@ public class AgentLoopLlmDispatchStageServiceTests
|
||||
InjectedToolReminder: false,
|
||||
SupplementalMessageCount: 0,
|
||||
FlattenedStructuredAssistantCount: 0,
|
||||
ConvertedOrphanToolResultCount: 0);
|
||||
ConvertedOrphanToolResultCount: 0,
|
||||
InjectedCodeWorkingSet: false,
|
||||
InjectedSemanticSummary: false,
|
||||
ProtectedActiveDiagnostics: false,
|
||||
InjectedWorkspaceContext: false);
|
||||
|
||||
var result = await service.ExecuteAsync(
|
||||
new AgentLoopLlmDispatchStageInput(
|
||||
@@ -172,7 +176,11 @@ public class AgentLoopLlmDispatchStageServiceTests
|
||||
InjectedToolReminder: false,
|
||||
SupplementalMessageCount: 0,
|
||||
FlattenedStructuredAssistantCount: 0,
|
||||
ConvertedOrphanToolResultCount: 0);
|
||||
ConvertedOrphanToolResultCount: 0,
|
||||
InjectedCodeWorkingSet: false,
|
||||
InjectedSemanticSummary: false,
|
||||
ProtectedActiveDiagnostics: false,
|
||||
InjectedWorkspaceContext: false);
|
||||
var context = new AgentContext { WorkFolder = @"E:\code", ActiveTab = "Code" };
|
||||
|
||||
await service.ExecuteAsync(
|
||||
|
||||
@@ -161,7 +161,8 @@ public class AgentLoopPreLlmStageServiceTests
|
||||
RuntimeAllowedToolCount: 0));
|
||||
|
||||
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_semantic_summary");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,8 +93,11 @@ public class AgentLoopQueryAssemblyServiceTests
|
||||
injectPreCallToolReminder: true,
|
||||
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_semantic_summary");
|
||||
result.InjectedCodeWorkingSet.Should().BeTrue();
|
||||
result.InjectedSemanticSummary.Should().BeTrue();
|
||||
result.SendMessages.Last().Content.Should().Contain("[TOOL_REQUIRED]");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,4 +173,62 @@ public class ChatStorageServiceTests
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,4 +89,41 @@ public class CodeTaskWorkingSetServiceTests
|
||||
message.Should().NotBeNull();
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,10 +54,12 @@ public class ContextCondenserTests
|
||||
changed.Should().BeTrue();
|
||||
messages.Any(m =>
|
||||
{
|
||||
var c = m.Content ?? "";
|
||||
return c.Contains("[축약됨", StringComparison.Ordinal)
|
||||
|| c.Contains("[time-based", StringComparison.Ordinal)
|
||||
|| c.Contains("이전 내용 축약됨", StringComparison.Ordinal);
|
||||
var content = m.Content ?? "";
|
||||
return content.Contains("[truncated]", StringComparison.Ordinal)
|
||||
|| content.Contains("[time-based", 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();
|
||||
}
|
||||
|
||||
@@ -69,6 +71,30 @@ public class ContextCondenserTests
|
||||
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()
|
||||
{
|
||||
var largeOutput = new string('A', 30_000);
|
||||
@@ -76,18 +102,18 @@ public class ContextCondenserTests
|
||||
|
||||
return
|
||||
[
|
||||
new ChatMessage { Role = "system", Content = "system prompt" },
|
||||
new ChatMessage { Role = "user", Content = "첫 질문" },
|
||||
new ChatMessage { Role = "assistant", Content = toolJson }, // Keep the large payload in the old segment.
|
||||
new ChatMessage { Role = "assistant", Content = "첫 답변" },
|
||||
new ChatMessage { Role = "user", Content = "둘째 질문" },
|
||||
new ChatMessage { Role = "assistant", Content = "둘째 답변" },
|
||||
new ChatMessage { Role = "user", Content = "셋째 질문" },
|
||||
new ChatMessage { Role = "assistant", Content = "셋째 답변" },
|
||||
new ChatMessage { Role = "user", Content = "넷째 질문" },
|
||||
new ChatMessage { Role = "assistant", Content = "넷째 답변" },
|
||||
new ChatMessage { Role = "user", Content = "다섯째 질문" },
|
||||
new ChatMessage { Role = "assistant", Content = "다섯째 답변" },
|
||||
new ChatMessage { MsgId = "system-1", Role = "system", Content = "system prompt" },
|
||||
new ChatMessage { MsgId = "user-1", Role = "user", Content = "first request" },
|
||||
new ChatMessage { MsgId = "assistant-large", Role = "assistant", Content = toolJson },
|
||||
new ChatMessage { MsgId = "assistant-2", Role = "assistant", Content = "first reply" },
|
||||
new ChatMessage { MsgId = "user-2", Role = "user", Content = "follow-up request" },
|
||||
new ChatMessage { MsgId = "assistant-3", Role = "assistant", Content = "follow-up reply" },
|
||||
new ChatMessage { MsgId = "user-3", Role = "user", Content = "third request" },
|
||||
new ChatMessage { MsgId = "assistant-4", Role = "assistant", Content = "third reply" },
|
||||
new ChatMessage { MsgId = "user-4", Role = "user", Content = "fourth request" },
|
||||
new ChatMessage { MsgId = "assistant-5", Role = "assistant", Content = "fourth reply" },
|
||||
new ChatMessage { MsgId = "user-5", Role = "user", Content = "fifth request" },
|
||||
new ChatMessage { MsgId = "assistant-6", Role = "assistant", Content = "fifth reply" },
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,6 +191,87 @@ public class ChatConversation
|
||||
|
||||
[JsonPropertyName("postCompactionCompletionTokens")]
|
||||
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
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,6 @@ public partial class AgentLoopService
|
||||
var toolTraceRepair = llmRequest.FlattenedStructuredAssistantCount > 0 || llmRequest.ConvertedOrphanToolResultCount > 0
|
||||
? $"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}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,11 @@ internal sealed record AgentLoopLlmRequestPreparationResult(
|
||||
bool InjectedToolReminder,
|
||||
int SupplementalMessageCount,
|
||||
int FlattenedStructuredAssistantCount,
|
||||
int ConvertedOrphanToolResultCount);
|
||||
int ConvertedOrphanToolResultCount,
|
||||
bool InjectedCodeWorkingSet,
|
||||
bool InjectedSemanticSummary,
|
||||
bool ProtectedActiveDiagnostics,
|
||||
bool InjectedWorkspaceContext);
|
||||
|
||||
/// <summary>
|
||||
/// 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 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;
|
||||
if (!forceInitialToolCall
|
||||
|| !injectPreCallToolReminder
|
||||
@@ -38,7 +50,11 @@ internal static class AgentLoopLlmRequestPreparationService
|
||||
false,
|
||||
supplementalCount,
|
||||
normalization.FlattenedAssistantCount,
|
||||
normalization.ConvertedOrphanToolResultCount);
|
||||
normalization.ConvertedOrphanToolResultCount,
|
||||
injectedCodeWorkingSet,
|
||||
injectedSemanticSummary,
|
||||
protectedActiveDiagnostics,
|
||||
injectedWorkspaceContext);
|
||||
}
|
||||
|
||||
sendMessages.Add(BuildToolReminderMessage());
|
||||
@@ -48,7 +64,11 @@ internal static class AgentLoopLlmRequestPreparationService
|
||||
true,
|
||||
supplementalCount,
|
||||
normalization.FlattenedAssistantCount,
|
||||
normalization.ConvertedOrphanToolResultCount);
|
||||
normalization.ConvertedOrphanToolResultCount,
|
||||
injectedCodeWorkingSet,
|
||||
injectedSemanticSummary,
|
||||
protectedActiveDiagnostics,
|
||||
injectedWorkspaceContext);
|
||||
}
|
||||
|
||||
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)
|
||||
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;
|
||||
|
||||
var added = 0;
|
||||
foreach (var message in supplementalMessages)
|
||||
{
|
||||
if (message == null || string.IsNullOrWhiteSpace(message.Content))
|
||||
continue;
|
||||
|
||||
sendMessages.Add(CloneMessage(message));
|
||||
added++;
|
||||
}
|
||||
|
||||
@@ -79,7 +79,41 @@ internal static class AgentLoopQueryAssemblyService
|
||||
|
||||
private static IEnumerable<ChatMessage>? BuildSupplementalMessages(CodeTaskWorkingSetService? codeWorkingSet)
|
||||
{
|
||||
var workingSetMessage = codeWorkingSet?.BuildChatMessage();
|
||||
return workingSetMessage is null ? null : [workingSetMessage];
|
||||
if (codeWorkingSet == null)
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,6 +113,10 @@ public partial class AgentLoopService
|
||||
/// </summary>
|
||||
public string? RuntimeWorkFolderOverride { get; set; }
|
||||
|
||||
public CodeWorkingSetSnapshot? InitialCodeWorkingSetSnapshot { get; set; }
|
||||
|
||||
public Action<CodeWorkingSetSnapshot?>? CodeWorkingSetSnapshotUpdated { get; set; }
|
||||
|
||||
/// <summary>현재 대화 ID (감사 로그 기록용).</summary>
|
||||
private string _conversationId = "";
|
||||
|
||||
@@ -311,7 +315,8 @@ public partial class AgentLoopService
|
||||
userQuery,
|
||||
context.WorkFolder,
|
||||
explorationState.ScaffoldProfile?.Label,
|
||||
workspaceWasInitiallyEmpty)
|
||||
workspaceWasInitiallyEmpty,
|
||||
InitialCodeWorkingSetSnapshot)
|
||||
: null;
|
||||
|
||||
var preferredInitialToolSequence = BuildPreferredInitialToolSequence(
|
||||
@@ -1525,6 +1530,7 @@ public partial class AgentLoopService
|
||||
if (!isCodeTab)
|
||||
sessionLearnings?.TryExtract(effectiveCall.ToolName, result.Output ?? "", result.Success);
|
||||
codeWorkingSet?.RecordToolResult(effectiveCall.ToolName, effectiveCall.ToolInput, result);
|
||||
PublishCodeWorkingSetSnapshot(codeWorkingSet);
|
||||
|
||||
if (!result.Success)
|
||||
{
|
||||
|
||||
@@ -19,6 +19,7 @@ public sealed class AgentQueryContextWindowResult
|
||||
public int ProtectedRecentNonSystemMessages { get; init; }
|
||||
public int ToolResultSoftCharLimit { get; init; }
|
||||
public int ToolResultAggregateBudgetChars { get; init; }
|
||||
public bool LegacyBoundaryFallbackUsed { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -79,10 +80,11 @@ public static class AgentQueryContextBuilder
|
||||
ProtectedRecentNonSystemMessages = options.ToolResultBudget.ProtectedRecentNonSystemMessages,
|
||||
ToolResultSoftCharLimit = options.ToolResultBudget.SoftCharLimit,
|
||||
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(
|
||||
sourceMessages,
|
||||
startIndex,
|
||||
@@ -124,24 +126,43 @@ public static class AgentQueryContextBuilder
|
||||
ProtectedRecentNonSystemMessages = options.ToolResultBudget.ProtectedRecentNonSystemMessages,
|
||||
ToolResultSoftCharLimit = options.ToolResultBudget.SoftCharLimit,
|
||||
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--)
|
||||
{
|
||||
if (IsQueryBoundaryMarker(sourceMessages[i]))
|
||||
if (TryIsQueryBoundaryMarker(sourceMessages[i], out var usedLegacyFallback))
|
||||
{
|
||||
boundaryApplied = true;
|
||||
legacyBoundaryFallbackUsed = usedLegacyFallback;
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
boundaryApplied = false;
|
||||
legacyBoundaryFallbackUsed = false;
|
||||
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)
|
||||
{
|
||||
var metaKind = message.MetaKind ?? "";
|
||||
@@ -286,7 +307,7 @@ public static class AgentQueryContextBuilder
|
||||
}
|
||||
else if (string.Equals(message.Role, "assistant", StringComparison.OrdinalIgnoreCase)
|
||||
&& (message.Content ?? string.Empty).StartsWith("{\"_tool_use_blocks\"", StringComparison.Ordinal)
|
||||
&& TryBuildToolUseSnippet(message, out var toolUseSnippet))
|
||||
&& TryBuildCleanToolUseSnippet(message, out var 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)
|
||||
{
|
||||
var normalized = text.Replace('\r', ' ').Replace('\n', ' ').Trim();
|
||||
|
||||
@@ -28,17 +28,22 @@ internal sealed class CodeTaskWorkingSetService
|
||||
private string _environmentSummary = "";
|
||||
private string _nextFocus = "";
|
||||
private string _lastMutationSummary = "";
|
||||
private CodeWorkingSetSemanticSummary? _latestSemanticSummary;
|
||||
private string _lastPublishedFingerprint = "";
|
||||
|
||||
public CodeTaskWorkingSetService(
|
||||
string goal,
|
||||
string? workFolder,
|
||||
string? scaffoldProfileLabel,
|
||||
bool startedFromEmptyWorkspace)
|
||||
bool startedFromEmptyWorkspace,
|
||||
CodeWorkingSetSnapshot? snapshot = null)
|
||||
{
|
||||
_goal = CollapseWhitespace(goal);
|
||||
_workFolder = workFolder?.Trim() ?? "";
|
||||
_scaffoldProfileLabel = scaffoldProfileLabel?.Trim() ?? "";
|
||||
_startedFromEmptyWorkspace = startedFromEmptyWorkspace;
|
||||
RestoreSnapshot(snapshot);
|
||||
_lastPublishedFingerprint = CreateSnapshotFingerprint(BuildSnapshotCore());
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
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()
|
||||
{
|
||||
var lines = new List<string> { "[code-working-set]" };
|
||||
@@ -150,14 +223,32 @@ internal sealed class CodeTaskWorkingSetService
|
||||
return;
|
||||
}
|
||||
|
||||
var createdDirectories = new List<string>();
|
||||
foreach (var candidate in ExtractPathCandidates(toolInput))
|
||||
{
|
||||
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))
|
||||
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)
|
||||
@@ -195,6 +286,15 @@ internal sealed class CodeTaskWorkingSetService
|
||||
|
||||
if (touchedFiles.Count > 0)
|
||||
_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)
|
||||
@@ -202,9 +302,18 @@ internal sealed class CodeTaskWorkingSetService
|
||||
if (!IsVerificationTool(toolName))
|
||||
return;
|
||||
|
||||
var hadDiagnostics = _activeDiagnostics.Count > 0;
|
||||
if (result.Success)
|
||||
{
|
||||
_activeDiagnostics.Clear();
|
||||
if (hadDiagnostics)
|
||||
{
|
||||
RecordSemanticSummary(
|
||||
"verification_resolved",
|
||||
toolName,
|
||||
"Verification passed after resolving the latest diagnostics.",
|
||||
_recentWrites);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -219,12 +328,22 @@ internal sealed class CodeTaskWorkingSetService
|
||||
{
|
||||
_activeDiagnostics.Clear();
|
||||
_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;
|
||||
}
|
||||
|
||||
_activeDiagnostics.Clear();
|
||||
_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)
|
||||
@@ -454,6 +573,113 @@ internal sealed class CodeTaskWorkingSetService
|
||||
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(
|
||||
string ToolName,
|
||||
string FilePath,
|
||||
|
||||
@@ -15,7 +15,7 @@ public sealed class ContextCompactionResult
|
||||
public int CollapsedBoundaryCount { get; set; }
|
||||
public List<string> AppliedStages { get; } = new();
|
||||
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>
|
||||
@@ -43,7 +43,7 @@ public static class ContextCondenser
|
||||
private const int RecentKeepCount = 12;
|
||||
private const int AutoCompactBufferTokens = 13_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);
|
||||
|
||||
@@ -237,7 +237,7 @@ public static class ContextCondenser
|
||||
if (didCompress)
|
||||
{
|
||||
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.AfterTokens = afterTokens;
|
||||
}
|
||||
@@ -366,7 +366,7 @@ public static class ContextCondenser
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -374,8 +374,8 @@ public static class ContextCondenser
|
||||
{
|
||||
messages[i] = CloneWithContent(
|
||||
msg,
|
||||
msg.Content[..MaxToolResultChars] + "\n\n...[이전 내용 축약됨 — 원본 " +
|
||||
$"{msg.Content.Length:N0}자 중 {MaxToolResultChars:N0}자 유지]");
|
||||
msg.Content[..MaxToolResultChars] + "\n\n...[previous content truncated - kept " +
|
||||
$"{MaxToolResultChars:N0} of {msg.Content.Length:N0} chars]");
|
||||
truncated = true;
|
||||
}
|
||||
}
|
||||
@@ -387,13 +387,18 @@ public static class ContextCondenser
|
||||
{
|
||||
return new ChatMessage
|
||||
{
|
||||
MsgId = source.MsgId,
|
||||
Role = source.Role,
|
||||
Content = content,
|
||||
Timestamp = source.Timestamp,
|
||||
MetaKind = source.MetaKind,
|
||||
MetaRunId = source.MetaRunId,
|
||||
Feedback = source.Feedback,
|
||||
ResponseElapsedMs = source.ResponseElapsedMs,
|
||||
PromptTokens = source.PromptTokens,
|
||||
CompletionTokens = source.CompletionTokens,
|
||||
AttachedFiles = source.AttachedFiles?.ToList(),
|
||||
QueryPreviewContent = source.QueryPreviewContent,
|
||||
Images = source.Images?.Select(x => new ImageAttachment
|
||||
{
|
||||
Base64 = x.Base64,
|
||||
@@ -508,9 +513,9 @@ public static class ContextCondenser
|
||||
AttachedFiles = attachedFiles.Count > 0 ? attachedFiles : null,
|
||||
Content = string.Join("\n", new[]
|
||||
{
|
||||
$"[세션 메모리 압축 - {candidates.Count}개 이전 경계 통합]",
|
||||
"- 이전 요약, 실행 경계, 오래된 메타 로그를 하나의 세션 메모로 합쳤습니다.",
|
||||
attachedFiles.Count > 0 ? $"- 관련 파일: {string.Join(", ", attachedFiles)}" : "- 관련 파일: 없음",
|
||||
$"[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 ? $"- 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)
|
||||
|| 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)
|
||||
@@ -584,7 +594,7 @@ public static class ContextCondenser
|
||||
Timestamp = group.Last().Timestamp,
|
||||
MetaKind = "collapsed_boundary",
|
||||
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;
|
||||
i = j;
|
||||
@@ -619,6 +629,9 @@ public static class ContextCondenser
|
||||
var content = message.Content ?? "";
|
||||
return string.Equals(message.MetaKind, "microcompact_boundary", 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);
|
||||
@@ -646,7 +659,7 @@ public static class ContextCondenser
|
||||
snipped = CloneWithContent(
|
||||
source,
|
||||
string.Join("\n", headLines) +
|
||||
"\n...[snip: 중간 실행 로그/출력 축약]...\n" +
|
||||
"\n...[snip: middle execution log/output removed]...\n" +
|
||||
string.Join("\n", tailLines));
|
||||
return true;
|
||||
}
|
||||
@@ -655,7 +668,7 @@ public static class ContextCondenser
|
||||
var tail = normalized.Length > SnipKeepTailChars ? normalized[^SnipKeepTailChars..] : "";
|
||||
snipped = CloneWithContent(
|
||||
source,
|
||||
head + "\n...[snip: 중간 내용 축약]...\n" + tail);
|
||||
head + "\n...[snip: middle content removed]...\n" + tail);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -685,7 +698,7 @@ public static class ContextCondenser
|
||||
{
|
||||
compacted = CloneWithContent(
|
||||
source,
|
||||
$"[이전 도구 결과 축약]\n- 상세 출력은 압축되었습니다.\n- 원본 길이: {content.Length:N0}자");
|
||||
$"[previous tool result compaction]\n- Detailed output was compacted.\n- Original length: {content.Length:N0} chars");
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -693,7 +706,7 @@ public static class ContextCondenser
|
||||
{
|
||||
compacted = CloneWithContent(
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -701,7 +714,7 @@ public static class ContextCondenser
|
||||
{
|
||||
compacted = CloneWithContent(
|
||||
source,
|
||||
$"[이전 실행 메타 축약]\n- 종류: {source.MetaKind}\n- 자세한 실행 이벤트는 압축되었습니다.");
|
||||
$"[previous execution meta compaction]\n- Kind: {source.MetaKind}\n- Detailed execution events were compacted.");
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -710,7 +723,7 @@ public static class ContextCondenser
|
||||
var head = content[..Math.Min(MicrocompactSingleKeepChars, content.Length)];
|
||||
compacted = CloneWithContent(
|
||||
source,
|
||||
head + $"\n\n...[이전 메시지 microcompact 적용 — 원본 {content.Length:N0}자]");
|
||||
head + $"\n\n...[previous message microcompact applied - original {content.Length:N0} chars]");
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -728,15 +741,15 @@ public static class ContextCondenser
|
||||
|
||||
var lines = new List<string>
|
||||
{
|
||||
$"[이전 실행 묶음 압축 — {group.Count}개 메시지]"
|
||||
$"[previous execution bundle compaction - {group.Count} messages]"
|
||||
};
|
||||
|
||||
if (toolResults > 0) lines.Add($"- 도구 결과 {toolResults}건 정리");
|
||||
if (toolCalls > 0) lines.Add($"- 도구 호출 {toolCalls}건 정리");
|
||||
if (metaEvents > 0) lines.Add($"- 실행 메타/로그 {metaEvents}건 정리");
|
||||
if (longTexts > 0) lines.Add($"- 긴 설명/출력 {longTexts}건 축약");
|
||||
if (attachmentSummary.AttachedFiles.Count > 0) lines.Add($"- 관련 파일: {string.Join(", ", attachmentSummary.AttachedFiles)}");
|
||||
if (attachmentSummary.ImageCount > 0) lines.Add($"- 관련 이미지: {attachmentSummary.ImageCount}개");
|
||||
if (toolResults > 0) lines.Add($"- Compacted tool results: {toolResults}");
|
||||
if (toolCalls > 0) lines.Add($"- Compacted tool calls: {toolCalls}");
|
||||
if (metaEvents > 0) lines.Add($"- Compacted execution meta/logs: {metaEvents}");
|
||||
if (longTexts > 0) lines.Add($"- Compacted long explanation/output blocks: {longTexts}");
|
||||
if (attachmentSummary.AttachedFiles.Count > 0) lines.Add($"- Related files: {string.Join(", ", attachmentSummary.AttachedFiles)}");
|
||||
if (attachmentSummary.ImageCount > 0) lines.Add($"- Related images: {attachmentSummary.ImageCount}");
|
||||
|
||||
return new ChatMessage
|
||||
{
|
||||
@@ -766,18 +779,18 @@ public static class ContextCondenser
|
||||
|
||||
// 요약 대상 텍스트 구성
|
||||
var sb = new System.Text.StringBuilder();
|
||||
sb.AppendLine("다음 대화 기록을 간결하게 요약하세요.");
|
||||
sb.AppendLine("반드시 유지할 정보: 사용자 요청, 핵심 결정 사항, 생성/수정된 파일 경로, 작업 진행 상황, 중요한 결과.");
|
||||
sb.AppendLine("제거할 정보: 도구 실행의 상세 출력, 반복되는 내용, 중간 사고 과정.");
|
||||
sb.AppendLine("요약은 대화와 동일한 언어로 작성하세요. 글머리 기호(-)로 핵심 사항만 나열하세요.\n---");
|
||||
sb.AppendLine("Summarize the following conversation history.");
|
||||
sb.AppendLine("Preserve the user request, key decisions, created or modified file paths, current progress, and important outcomes.");
|
||||
sb.AppendLine("Remove detailed tool output, repetition, and intermediate reasoning.");
|
||||
sb.AppendLine("Write concise bullet points using the same language as the conversation.\n---");
|
||||
|
||||
foreach (var m in oldMessages)
|
||||
{
|
||||
var content = m.Content ?? "";
|
||||
if (content.StartsWith("{\"_tool_use_blocks\""))
|
||||
content = "[도구 호출]";
|
||||
content = "[tool call]";
|
||||
else if (content.StartsWith("{\"type\":\"tool_result\""))
|
||||
content = "[도구 결과]";
|
||||
content = "[tool result]";
|
||||
else if (content.Length > 300)
|
||||
content = content[..300] + "...";
|
||||
|
||||
@@ -808,7 +821,7 @@ public static class ContextCondenser
|
||||
messages.Add(new ChatMessage
|
||||
{
|
||||
Role = "assistant",
|
||||
Content = "이전 대화 내용을 확인했습니다. 이어서 작업하겠습니다.",
|
||||
Content = "I reviewed the previous conversation summary and will continue from it.",
|
||||
Timestamp = DateTime.Now,
|
||||
});
|
||||
|
||||
@@ -817,7 +830,7 @@ public static class ContextCondenser
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogService.Warn($"Context Condenser 요약 실패: {ex.Message}");
|
||||
LogService.Warn($"Context Condenser summary failed: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -848,14 +861,14 @@ public static class ContextCondenser
|
||||
{
|
||||
var lines = new List<string>
|
||||
{
|
||||
$"[이전 대화 요약 — {oldMessageCount}개 메시지 압축]",
|
||||
$"[previous conversation summary - {oldMessageCount} messages compacted]",
|
||||
summary.Trim()
|
||||
};
|
||||
|
||||
if (attachmentSummary.AttachedFiles.Count > 0)
|
||||
lines.Add($"참고 파일: {string.Join(", ", attachmentSummary.AttachedFiles)}");
|
||||
lines.Add($"Reference files: {string.Join(", ", attachmentSummary.AttachedFiles)}");
|
||||
if (attachmentSummary.ImageCount > 0)
|
||||
lines.Add($"참고 이미지: {attachmentSummary.ImageCount}개");
|
||||
lines.Add($"Reference images: {attachmentSummary.ImageCount}");
|
||||
|
||||
return string.Join("\n", lines.Where(x => !string.IsNullOrWhiteSpace(x)));
|
||||
}
|
||||
|
||||
@@ -257,6 +257,7 @@ public sealed class ChatSessionStateService
|
||||
Category = source.Category,
|
||||
WorkFolder = source.WorkFolder,
|
||||
SystemCommand = source.SystemCommand,
|
||||
CodeWorkingSet = AxCopilot.Services.Agent.CodeTaskWorkingSetService.CloneSnapshot(source.CodeWorkingSet),
|
||||
ParentId = source.Id,
|
||||
BranchLabel = branchLabel,
|
||||
BranchAtIndex = atIndex,
|
||||
@@ -281,12 +282,16 @@ public sealed class ChatSessionStateService
|
||||
var m = source.Messages[i];
|
||||
fork.Messages.Add(new ChatMessage
|
||||
{
|
||||
MsgId = m.MsgId,
|
||||
Role = m.Role,
|
||||
Content = m.Content,
|
||||
Timestamp = m.Timestamp,
|
||||
MetaKind = m.MetaKind,
|
||||
MetaRunId = m.MetaRunId,
|
||||
Feedback = m.Feedback,
|
||||
ResponseElapsedMs = m.ResponseElapsedMs,
|
||||
PromptTokens = m.PromptTokens,
|
||||
CompletionTokens = m.CompletionTokens,
|
||||
AttachedFiles = m.AttachedFiles?.ToList(),
|
||||
QueryPreviewContent = m.QueryPreviewContent,
|
||||
Images = m.Images?.Select(img => new ImageAttachment
|
||||
|
||||
@@ -6280,6 +6280,20 @@ public partial class ChatWindow : Window
|
||||
{
|
||||
loop.ActiveTab = runTab;
|
||||
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 스레드 블록 방지
|
||||
// RunAsync 내부의 _llm.SendAsync가 SynchronizationContext로 UI 스레드에
|
||||
// 복귀할 때 WPF 메시지 펌프를 독점하여 창 드래그/최소화가 안 되는 문제 해결
|
||||
@@ -6328,8 +6342,15 @@ public partial class ChatWindow : Window
|
||||
finally
|
||||
{
|
||||
StoreAgentLoopMessagesSnapshot(runTab, msgList);
|
||||
if (string.Equals(runTab, "Code", StringComparison.OrdinalIgnoreCase)
|
||||
&& conversation.CodeWorkingSet != null)
|
||||
{
|
||||
PersistConversationSnapshot(originTab, conversation, "코드 컨텍스트 저장 실패");
|
||||
}
|
||||
ResetPermissionRulesForRun(runTab);
|
||||
loop.RuntimeWorkFolderOverride = null;
|
||||
loop.InitialCodeWorkingSetSnapshot = null;
|
||||
loop.CodeWorkingSetSnapshotUpdated = null;
|
||||
loop.EventOccurred -= agentEventHandler;
|
||||
loop.UserDecisionCallback = null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user