에이전트 루프 반복 준비 단계와 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:
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user