에이전트 루프 반복 준비 단계와 tool_result preview 복원 경로를 안정화한다

- AgentLoopIterationPreparationService를 추가해 queued command 투영, tool_result 대기 요약, query view 생성 책임을 AgentLoopService.RunAsync의 반복 진입부에서 분리함
- AgentMessageInvariantHelper의 preview 스냅샷을 explicit id/fingerprint와 synthetic id로 나눠 저장된 preview 우선, fingerprint 재바인딩 차선, synthetic preview 마지막 순서로 복원하도록 정리함
- AgentToolResultBudget이 source query view 기준 snapshot을 먼저 사용하도록 바꿔 source preview가 local synthetic preview에 가려지지 않게 하고 첫 축약 결과도 source message에 다시 저장함
- AgentToolResultBudgetTests와 AgentLoopIterationPreparationServiceTests를 추가/확장해 같은 tool_result의 장기 세션 재사용과 반복 준비 단계 분리를 회귀로 고정함
- README.md와 docs/DEVELOPMENT.md에 2026-04-15 10:34 (KST) 기준 작업 이력과 검증 명령을 반영함

검증 결과
- dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_loop_pipeline\\ -p:IntermediateOutputPath=obj\\verify_loop_pipeline\\ : 경고 0 / 오류 0
- dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter AgentQueuedCommandProjectorTests|AgentLoopIterationPreparationServiceTests|AgentMessageInvariantHelperTests|AgentToolResultBudgetTests|AgentQueryContextBuilderTests|ChatStorageServiceTests -p:OutputPath=bin\\verify_loop_pipeline_tests\\ -p:IntermediateOutputPath=obj\\verify_loop_pipeline_tests\\ : 통과 14
This commit is contained in:
2026-04-15 10:35:56 +09:00
parent 2c1926356a
commit 91c4dc74c3
8 changed files with 282 additions and 78 deletions

View File

@@ -0,0 +1,61 @@
using AxCopilot.Models;
using AxCopilot.Services.Agent;
using FluentAssertions;
using Xunit;
namespace AxCopilot.Tests.Services;
public class AgentLoopIterationPreparationServiceTests
{
[Fact]
public void Prepare_ShouldProjectQueuedCommandsAndBuildUpdatedQueryView()
{
var messages = new List<ChatMessage>
{
new()
{
MsgId = "assistant-1",
Role = "assistant",
Content = "existing assistant message"
}
};
var queue = new AgentCommandQueue();
queue.EnqueueSteering("focus on the changed files", priority: "now", requestInterrupt: true);
queue.EnqueueNotification("low priority note", priority: "later");
var result = AgentLoopIterationPreparationService.Prepare(
messages,
queue,
lastToolResultAtUtc: null,
lastToolResultToolName: null,
utcNow: DateTime.UtcNow);
messages.Should().Contain(message =>
message.MetaKind == "queued_input_interrupt" &&
message.Role == "system");
messages.Should().Contain(message =>
message.MetaKind == "queued_steering" &&
message.Role == "user" &&
message.Content == "focus on the changed files");
result.QueueProjection.Events.Should().Contain(evt =>
evt.Type == AgentEventType.Thinking &&
evt.Summary.Contains("Deferred 1 lower-priority", StringComparison.OrdinalIgnoreCase));
result.QueryView.Messages.Should().Contain(message =>
message.MetaKind == "queued_steering" &&
message.Role == "user");
}
[Fact]
public void BuildToolResultWaitSummary_ShouldFormatToolNameAndElapsedMilliseconds()
{
var utcNow = DateTime.UtcNow;
var waitSummary = AgentLoopIterationPreparationService.BuildToolResultWaitSummary(
utcNow.AddMilliseconds(-1250),
"file_read",
utcNow);
waitSummary.Should().StartWith("file_read:");
waitSummary.Should().Contain("ms");
}
}

View File

@@ -131,4 +131,49 @@ public class AgentToolResultBudgetTests
result.ReusedPreviewCount.Should().BeGreaterThan(0);
queryView[1].QueryPreviewContent.Should().Be(queryView[0].QueryPreviewContent);
}
[Fact]
public void Apply_ShouldReuseFingerprintPreviewFromSourceMessages_WhenToolUseIdChangesAcrossSessions()
{
var longContent = new string('D', 1700);
var sourceMessages = new List<ChatMessage>
{
new()
{
MsgId = "source-tool",
Role = "user",
Content = $$"""{"type":"tool_result","tool_use_id":"call-original","tool_name":"file_read","content":"{{longContent}}"}""",
QueryPreviewContent = """{"type":"tool_result","tool_use_id":"call-original","tool_name":"file_read","content":"preview-fingerprint"}"""
},
new()
{
MsgId = "tail-1",
Role = "assistant",
Content = "recent tail"
}
};
var queryView = new List<ChatMessage>
{
new()
{
MsgId = "rebuilt-tool",
Role = "user",
Content = $$"""{"type":"tool_result","tool_use_id":"call-replayed","tool_name":"file_read","content":"{{longContent}}"}"""
},
new()
{
MsgId = "tail-2",
Role = "assistant",
Content = "recent tail"
}
};
var result = AgentToolResultBudget.Apply(queryView, protectedRecentNonSystemMessages: 1, sourceMessages: sourceMessages);
result.ReusedPreviewCount.Should().Be(1);
queryView[0].Content.Should().Contain("call-replayed");
queryView[0].Content.Should().Contain("preview-fingerprint");
queryView[0].Content.Should().NotContain("call-original");
}
}