코드탭 LLM 디스패치 단계를 별도 서비스로 분리
AgentLoopLlmDispatchStageService를 추가해 query_context와 llm_request 로그, 스트리밍 미리보기, read-only prefetch 연결, recovery-capable tool dispatch를 한 단계로 분리했다. AgentLoopService는 pre-LLM stage 이후 dispatch stage를 호출하는 오케스트레이터 형태로 정리했고 function-calling 미지원 fallback도 staged request를 재사용하도록 보정했다. StreamingToolExecutionCoordinator와 AgentLoopPreLlmStageService의 깨진 문자열을 영어로 정리하고 AgentLoopLlmDispatchStageServiceTests를 추가했다. 검증: Release build 경고 0 오류 0, 관련 AgentLoop 회귀 테스트 43개 통과
This commit is contained in:
18
README.md
18
README.md
@@ -1,5 +1,23 @@
|
|||||||
# AX Commander
|
# AX Commander
|
||||||
|
|
||||||
|
- Update: 2026-04-16 06:41 (KST)
|
||||||
|
- Continued the Code-tab structural refactor by extracting the dispatch/stream stage into [AgentLoopLlmDispatchStageService.cs](</E:/AX Copilot - Codex/src/AxCopilot/Services/Agent/AgentLoopLlmDispatchStageService.cs>).
|
||||||
|
- The new dispatch stage now owns:
|
||||||
|
- workflow `query_context` / `llm_request` logging
|
||||||
|
- stream preview and tool-ready thinking updates
|
||||||
|
- read-only tool prefetch handoff
|
||||||
|
- the recovery-capable `SendWithToolsWithRecoveryAsync(...)` dispatch itself
|
||||||
|
- [AgentLoopService.cs](</E:/AX Copilot - Codex/src/AxCopilot/Services/Agent/AgentLoopService.cs>) now treats LLM dispatch as a staged service call instead of building the stream callback inline, which keeps the loop closer to the staged `claw-code` shape.
|
||||||
|
- [StreamingToolExecutionCoordinator.cs](</E:/AX Copilot - Codex/src/AxCopilot/Services/Agent/StreamingToolExecutionCoordinator.cs>) was also rewritten with English-only status strings so the active stream/wait path no longer carries mojibake text, and [AgentLoopPreLlmStageService.cs](</E:/AX Copilot - Codex/src/AxCopilot/Services/Agent/AgentLoopPreLlmStageService.cs>) now returns clean English missing-tool failures.
|
||||||
|
- Added [AgentLoopLlmDispatchStageServiceTests.cs](</E:/AX Copilot - Codex/src/AxCopilot.Tests/Services/AgentLoopLlmDispatchStageServiceTests.cs>) to lock:
|
||||||
|
- staged dispatch argument handoff
|
||||||
|
- run-state retry reset after a successful dispatch
|
||||||
|
- streaming preview / tool-ready / retry-reset event emission
|
||||||
|
- read-only prefetch delegation through the coordinator
|
||||||
|
- Validation:
|
||||||
|
- `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_llm_dispatch_stage_escalated2\\ -p:IntermediateOutputPath=obj\\verify_llm_dispatch_stage_escalated2\\` warnings 0 / errors 0
|
||||||
|
- `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "AgentLoopLlmDispatchStageServiceTests|AgentLoopPreLlmStageServiceTests|AgentLoopQueryAssemblyServiceTests|AgentLoopIterationPreparationServiceTests|AgentLoopLlmRequestPreparationServiceTests|AgentMessageInvariantHelperTests|CodeTaskWorkingSetServiceTests|AgentLoopE2ETests" -p:OutputPath=bin\\verify_llm_dispatch_stage_tests_escalated2\\ -p:IntermediateOutputPath=obj\\verify_llm_dispatch_stage_tests_escalated2\\` passed 43
|
||||||
|
|
||||||
- Update: 2026-04-16 02:13 (KST)
|
- Update: 2026-04-16 02:13 (KST)
|
||||||
- Continued the Code-tab structural refactor by extracting the pre-LLM stage into [AgentLoopPreLlmStageService.cs](</E:/AX Copilot - Codex/src/AxCopilot/Services/Agent/AgentLoopPreLlmStageService.cs>).
|
- Continued the Code-tab structural refactor by extracting the pre-LLM stage into [AgentLoopPreLlmStageService.cs](</E:/AX Copilot - Codex/src/AxCopilot/Services/Agent/AgentLoopPreLlmStageService.cs>).
|
||||||
- The new stage service now owns:
|
- The new stage service now owns:
|
||||||
|
|||||||
@@ -1,5 +1,18 @@
|
|||||||
# Code Context Reliability Plan
|
# Code Context Reliability Plan
|
||||||
|
|
||||||
|
Update: 2026-04-16 06:41 (KST)
|
||||||
|
|
||||||
|
- Added `src/AxCopilot/Services/Agent/AgentLoopLlmDispatchStageService.cs` so the LLM dispatch path is now split into:
|
||||||
|
- history/query assembly
|
||||||
|
- pre-LLM stage planning
|
||||||
|
- dispatch/stream stage execution
|
||||||
|
- tool execution and recovery
|
||||||
|
- `AgentLoopService` no longer owns the inline stream preview callback or the direct `SendWithToolsWithRecoveryAsync(...)` setup for the primary loop.
|
||||||
|
- `StreamingToolExecutionCoordinator.cs` was also normalized to English-only active-path status strings so the staged dispatch path no longer reintroduces mojibake text during wait/retry handling.
|
||||||
|
- Remaining structural gap versus the target `claw-code` shape:
|
||||||
|
- the `NotSupportedException` / `ToolCallNotSupportedException` fallback branch still lives in `AgentLoopService`
|
||||||
|
- the next extraction target should be a narrower fallback policy stage so the main loop keeps shrinking toward a pure orchestrator
|
||||||
|
|
||||||
Update: 2026-04-16 01:37 (KST)
|
Update: 2026-04-16 01:37 (KST)
|
||||||
|
|
||||||
## Background
|
## Background
|
||||||
|
|||||||
@@ -1873,3 +1873,11 @@ UI ?遺우쁽????域뱀뮆???귐뗫솯?醫딆춦 ???袁る퓮 ?臾믩씜 ??疫
|
|||||||
- 검증:
|
- 검증:
|
||||||
- `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_pre_llm_stage_structure\\ -p:IntermediateOutputPath=obj\\verify_pre_llm_stage_structure\\` 경고 0 / 오류 0
|
- `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_pre_llm_stage_structure\\ -p:IntermediateOutputPath=obj\\verify_pre_llm_stage_structure\\` 경고 0 / 오류 0
|
||||||
- `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "AgentLoopPreLlmStageServiceTests|AgentLoopQueryAssemblyServiceTests|AgentLoopIterationPreparationServiceTests|AgentLoopLlmRequestPreparationServiceTests|AgentMessageInvariantHelperTests|SessionLearningCollectorTests|CodeTaskWorkingSetServiceTests|AgentLoopE2ETests" -p:OutputPath=bin\\verify_pre_llm_stage_structure_tests\\ -p:IntermediateOutputPath=obj\\verify_pre_llm_stage_structure_tests\\` 통과 60
|
- `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "AgentLoopPreLlmStageServiceTests|AgentLoopQueryAssemblyServiceTests|AgentLoopIterationPreparationServiceTests|AgentLoopLlmRequestPreparationServiceTests|AgentMessageInvariantHelperTests|SessionLearningCollectorTests|CodeTaskWorkingSetServiceTests|AgentLoopE2ETests" -p:OutputPath=bin\\verify_pre_llm_stage_structure_tests\\ -p:IntermediateOutputPath=obj\\verify_pre_llm_stage_structure_tests\\` 통과 60
|
||||||
|
업데이트: 2026-04-16 06:41 (KST)
|
||||||
|
- Code 탭 LLM 호출 경로를 한 단계 더 분리했습니다. `src/AxCopilot/Services/Agent/AgentLoopLlmDispatchStageService.cs`를 추가해 `query_context`/`llm_request` 워크플로 로그, 스트리밍 thinking 미리보기, read-only prefetch 연결, recovery-capable tool dispatch를 한 서비스로 모았습니다.
|
||||||
|
- `src/AxCopilot/Services/Agent/AgentLoopService.cs`는 이제 pre-LLM stage 결과를 받아 dispatch stage 서비스에 넘기고, 메인 루프는 오케스트레이션과 fallback 분기 중심으로 유지합니다. 이 과정에서 function-calling 미지원 fallback도 staged request(`code_working_set` 포함)를 재사용하도록 보정했습니다.
|
||||||
|
- `src/AxCopilot/Services/Agent/StreamingToolExecutionCoordinator.cs`는 로직은 유지하되 깨진 상태 문자열을 모두 영어로 정리해 스트리밍 대기/재시도 경로에서 mojibake가 다시 나오지 않게 했고, `src/AxCopilot/Services/Agent/AgentLoopPreLlmStageService.cs`의 missing-tool 실패 문구도 영어로 정리했습니다.
|
||||||
|
- `src/AxCopilot/Services/Agent/AgentLoopContextReliability.cs`의 `BuildQueryContextDetail(...)`는 dispatch stage에서도 재사용할 수 있게 내부 공개 범위로 조정했습니다.
|
||||||
|
- 테스트: `src/AxCopilot.Tests/Services/AgentLoopLlmDispatchStageServiceTests.cs`
|
||||||
|
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_llm_dispatch_stage_escalated2\\ -p:IntermediateOutputPath=obj\\verify_llm_dispatch_stage_escalated2\\` 경고 0 / 오류 0
|
||||||
|
- 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "AgentLoopLlmDispatchStageServiceTests|AgentLoopPreLlmStageServiceTests|AgentLoopQueryAssemblyServiceTests|AgentLoopIterationPreparationServiceTests|AgentLoopLlmRequestPreparationServiceTests|AgentMessageInvariantHelperTests|CodeTaskWorkingSetServiceTests|AgentLoopE2ETests" -p:OutputPath=bin\\verify_llm_dispatch_stage_tests_escalated2\\ -p:IntermediateOutputPath=obj\\verify_llm_dispatch_stage_tests_escalated2\\` 통과 43
|
||||||
|
|||||||
@@ -0,0 +1,198 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using AxCopilot.Models;
|
||||||
|
using AxCopilot.Services;
|
||||||
|
using AxCopilot.Services.Agent;
|
||||||
|
using FluentAssertions;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace AxCopilot.Tests.Services;
|
||||||
|
|
||||||
|
public class AgentLoopLlmDispatchStageServiceTests
|
||||||
|
{
|
||||||
|
private sealed class FakeAgentTool : IAgentTool
|
||||||
|
{
|
||||||
|
public string Name { get; init; } = "file_read";
|
||||||
|
public string Description { get; init; } = "Reads a file";
|
||||||
|
public ToolParameterSchema Parameters { get; init; } = new();
|
||||||
|
|
||||||
|
public Task<ToolResult> ExecuteAsync(JsonElement args, AgentContext context, CancellationToken ct = default)
|
||||||
|
=> Task.FromResult(ToolResult.Ok("ok"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class FakeToolExecutionCoordinator : IToolExecutionCoordinator
|
||||||
|
{
|
||||||
|
public List<ChatMessage>? CapturedMessages { get; private set; }
|
||||||
|
public IReadOnlyCollection<IAgentTool>? CapturedTools { get; private set; }
|
||||||
|
public string? CapturedPhaseLabel { get; private set; }
|
||||||
|
public bool CapturedForceToolCall { get; private set; }
|
||||||
|
public AgentLoopService.RunState? CapturedRunState { get; private set; }
|
||||||
|
public int PrefetchCallCount { get; private set; }
|
||||||
|
public AgentContext? PrefetchContext { get; private set; }
|
||||||
|
public Func<Func<ToolStreamEvent, Task>?, Func<ContentBlock, Task<ToolPrefetchResult?>>?, Task<List<ContentBlock>>>? DispatchAsync { get; init; }
|
||||||
|
|
||||||
|
public Task<ToolPrefetchResult?> TryPrefetchReadOnlyToolAsync(
|
||||||
|
ContentBlock block,
|
||||||
|
IReadOnlyCollection<IAgentTool> tools,
|
||||||
|
AgentContext context,
|
||||||
|
CancellationToken ct)
|
||||||
|
{
|
||||||
|
PrefetchCallCount++;
|
||||||
|
PrefetchContext = context;
|
||||||
|
return Task.FromResult<ToolPrefetchResult?>(
|
||||||
|
new ToolPrefetchResult(ToolResult.Ok("prefetched"), 5, block.ToolName));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<ContentBlock>> SendWithToolsWithRecoveryAsync(
|
||||||
|
List<ChatMessage> messages,
|
||||||
|
IReadOnlyCollection<IAgentTool> tools,
|
||||||
|
CancellationToken ct,
|
||||||
|
string phaseLabel,
|
||||||
|
AgentLoopService.RunState? runState = null,
|
||||||
|
bool forceToolCall = false,
|
||||||
|
Func<ContentBlock, Task<ToolPrefetchResult?>>? prefetchToolCallAsync = null,
|
||||||
|
Func<ToolStreamEvent, Task>? onStreamEventAsync = null)
|
||||||
|
{
|
||||||
|
CapturedMessages = messages;
|
||||||
|
CapturedTools = tools;
|
||||||
|
CapturedPhaseLabel = phaseLabel;
|
||||||
|
CapturedForceToolCall = forceToolCall;
|
||||||
|
CapturedRunState = runState;
|
||||||
|
|
||||||
|
if (DispatchAsync != null)
|
||||||
|
return await DispatchAsync(onStreamEventAsync, prefetchToolCallAsync);
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ExecuteAsync_ShouldDispatchPreparedRequestAndResetRunState()
|
||||||
|
{
|
||||||
|
var coordinator = new FakeToolExecutionCoordinator
|
||||||
|
{
|
||||||
|
DispatchAsync = (_, _) => Task.FromResult(new List<ContentBlock>
|
||||||
|
{
|
||||||
|
new() { Type = "tool_use", ToolName = "file_read", ToolId = "tool-1" }
|
||||||
|
})
|
||||||
|
};
|
||||||
|
var events = new List<(AgentEventType Type, string Tool, string Summary)>();
|
||||||
|
var service = new AgentLoopLlmDispatchStageService(
|
||||||
|
coordinator,
|
||||||
|
block => $"{block.ToolName}:{block.ToolId}",
|
||||||
|
(type, tool, summary) => events.Add((type, tool, summary)));
|
||||||
|
|
||||||
|
var runState = new AgentLoopService.RunState
|
||||||
|
{
|
||||||
|
ContextRecoveryAttempts = 2,
|
||||||
|
TransientLlmErrorRetries = 1,
|
||||||
|
};
|
||||||
|
var request = new AgentLoopLlmRequestPreparationResult(
|
||||||
|
SendMessages:
|
||||||
|
[
|
||||||
|
new ChatMessage { Role = "user", Content = "fix the build" }
|
||||||
|
],
|
||||||
|
ForceInitialToolCall: true,
|
||||||
|
InjectedToolReminder: false,
|
||||||
|
SupplementalMessageCount: 0,
|
||||||
|
FlattenedStructuredAssistantCount: 0,
|
||||||
|
ConvertedOrphanToolResultCount: 0);
|
||||||
|
|
||||||
|
var result = await service.ExecuteAsync(
|
||||||
|
new AgentLoopLlmDispatchStageInput(
|
||||||
|
Iteration: 3,
|
||||||
|
ConversationId: "conv-1",
|
||||||
|
RunId: "run-1",
|
||||||
|
PhaseLabel: "Main loop 3",
|
||||||
|
CurrentModel: "model-a",
|
||||||
|
QueryContextDetail: "context=ready",
|
||||||
|
LlmRequest: request,
|
||||||
|
ActiveTools: [new FakeAgentTool()],
|
||||||
|
Context: new AgentContext { WorkFolder = @"E:\code", ActiveTab = "Code" },
|
||||||
|
RunState: runState),
|
||||||
|
CancellationToken.None);
|
||||||
|
|
||||||
|
result.Blocks.Should().ContainSingle();
|
||||||
|
coordinator.CapturedMessages.Should().BeSameAs(request.SendMessages);
|
||||||
|
coordinator.CapturedPhaseLabel.Should().Be("Main loop 3");
|
||||||
|
coordinator.CapturedForceToolCall.Should().BeTrue();
|
||||||
|
runState.ContextRecoveryAttempts.Should().Be(0);
|
||||||
|
runState.TransientLlmErrorRetries.Should().Be(0);
|
||||||
|
events.Should().BeEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ExecuteAsync_ShouldEmitStreamPreviewToolReadyAndRetryResetEvents()
|
||||||
|
{
|
||||||
|
var coordinator = new FakeToolExecutionCoordinator
|
||||||
|
{
|
||||||
|
DispatchAsync = async (onStreamEventAsync, prefetchToolCallAsync) =>
|
||||||
|
{
|
||||||
|
using var readInput = JsonDocument.Parse("""{"path":"Views/MainWindow.xaml"}""");
|
||||||
|
if (prefetchToolCallAsync != null)
|
||||||
|
{
|
||||||
|
await prefetchToolCallAsync(new ContentBlock
|
||||||
|
{
|
||||||
|
Type = "tool_use",
|
||||||
|
ToolName = "file_read",
|
||||||
|
ToolId = "prefetch-1",
|
||||||
|
ToolInput = readInput.RootElement.Clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onStreamEventAsync != null)
|
||||||
|
{
|
||||||
|
await onStreamEventAsync(new ToolStreamEvent(ToolStreamEventKind.TextDelta, "Inspecting WPF files"));
|
||||||
|
await onStreamEventAsync(new ToolStreamEvent(
|
||||||
|
ToolStreamEventKind.ToolCallReady,
|
||||||
|
ToolCall: new ContentBlock
|
||||||
|
{
|
||||||
|
Type = "tool_use",
|
||||||
|
ToolName = "file_manage",
|
||||||
|
ToolId = "tool-9",
|
||||||
|
}));
|
||||||
|
await onStreamEventAsync(new ToolStreamEvent(ToolStreamEventKind.RetryReset, "retry"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var events = new List<(AgentEventType Type, string Tool, string Summary)>();
|
||||||
|
var service = new AgentLoopLlmDispatchStageService(
|
||||||
|
coordinator,
|
||||||
|
block => $"{block.ToolName}:{block.ToolId}",
|
||||||
|
(type, tool, summary) => events.Add((type, tool, summary)));
|
||||||
|
|
||||||
|
var request = new AgentLoopLlmRequestPreparationResult(
|
||||||
|
SendMessages:
|
||||||
|
[
|
||||||
|
new ChatMessage { Role = "user", Content = "create WPF shell" }
|
||||||
|
],
|
||||||
|
ForceInitialToolCall: false,
|
||||||
|
InjectedToolReminder: false,
|
||||||
|
SupplementalMessageCount: 0,
|
||||||
|
FlattenedStructuredAssistantCount: 0,
|
||||||
|
ConvertedOrphanToolResultCount: 0);
|
||||||
|
var context = new AgentContext { WorkFolder = @"E:\code", ActiveTab = "Code" };
|
||||||
|
|
||||||
|
await service.ExecuteAsync(
|
||||||
|
new AgentLoopLlmDispatchStageInput(
|
||||||
|
Iteration: 1,
|
||||||
|
ConversationId: "conv-2",
|
||||||
|
RunId: "run-2",
|
||||||
|
PhaseLabel: "Main loop 1",
|
||||||
|
CurrentModel: "model-b",
|
||||||
|
QueryContextDetail: "context=ready",
|
||||||
|
LlmRequest: request,
|
||||||
|
ActiveTools: [new FakeAgentTool(), new FakeAgentTool { Name = "file_manage" }],
|
||||||
|
Context: context,
|
||||||
|
RunState: new AgentLoopService.RunState()),
|
||||||
|
CancellationToken.None);
|
||||||
|
|
||||||
|
coordinator.PrefetchCallCount.Should().Be(1);
|
||||||
|
coordinator.PrefetchContext.Should().BeSameAs(context);
|
||||||
|
events.Should().Contain(entry => entry.Type == AgentEventType.Thinking && entry.Summary.Contains("Inspecting WPF files"));
|
||||||
|
events.Should().Contain(entry => entry.Type == AgentEventType.Thinking && entry.Summary.Contains("Streaming tool ready: file_manage:tool-9"));
|
||||||
|
events.Should().Contain(entry => entry.Type == AgentEventType.Thinking && entry.Summary.Contains("Resetting the streamed partial response before retry."));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@ public partial class AgentLoopService
|
|||||||
? AgentQueryContextBuilder.AgentQueryContextBuildOptions.CreateCodeDefault()
|
? AgentQueryContextBuilder.AgentQueryContextBuildOptions.CreateCodeDefault()
|
||||||
: AgentQueryContextBuilder.AgentQueryContextBuildOptions.CreateDefault();
|
: AgentQueryContextBuilder.AgentQueryContextBuildOptions.CreateDefault();
|
||||||
|
|
||||||
private static string BuildQueryContextDetail(
|
internal static string BuildQueryContextDetail(
|
||||||
AgentQueryContextWindowResult queryView,
|
AgentQueryContextWindowResult queryView,
|
||||||
AgentLoopLlmRequestPreparationResult llmRequest,
|
AgentLoopLlmRequestPreparationResult llmRequest,
|
||||||
CodeTaskWorkingSetService? codeWorkingSet)
|
CodeTaskWorkingSetService? codeWorkingSet)
|
||||||
|
|||||||
152
src/AxCopilot/Services/Agent/AgentLoopLlmDispatchStageService.cs
Normal file
152
src/AxCopilot/Services/Agent/AgentLoopLlmDispatchStageService.cs
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using System.Text;
|
||||||
|
using AxCopilot.Models;
|
||||||
|
using AxCopilot.Services;
|
||||||
|
|
||||||
|
namespace AxCopilot.Services.Agent;
|
||||||
|
|
||||||
|
internal sealed record AgentLoopLlmDispatchStageInput(
|
||||||
|
int Iteration,
|
||||||
|
string ConversationId,
|
||||||
|
string RunId,
|
||||||
|
string PhaseLabel,
|
||||||
|
string CurrentModel,
|
||||||
|
string QueryContextDetail,
|
||||||
|
AgentLoopLlmRequestPreparationResult LlmRequest,
|
||||||
|
IReadOnlyCollection<IAgentTool> ActiveTools,
|
||||||
|
AgentContext Context,
|
||||||
|
AgentLoopService.RunState RunState);
|
||||||
|
|
||||||
|
internal sealed record AgentLoopLlmDispatchStageResult(
|
||||||
|
List<ContentBlock> Blocks,
|
||||||
|
long ElapsedMilliseconds);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the staged LLM dispatch for one loop iteration:
|
||||||
|
/// workflow logging, stream preview updates, read-only prefetch, and the
|
||||||
|
/// recovery-capable tool-calling request itself.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class AgentLoopLlmDispatchStageService
|
||||||
|
{
|
||||||
|
private static readonly TimeSpan StreamPreviewUiInterval = TimeSpan.FromMilliseconds(450);
|
||||||
|
|
||||||
|
private readonly IToolExecutionCoordinator _toolExecutionCoordinator;
|
||||||
|
private readonly Func<ContentBlock, string> _formatToolCallSummary;
|
||||||
|
private readonly Action<AgentEventType, string, string> _emitEvent;
|
||||||
|
|
||||||
|
public AgentLoopLlmDispatchStageService(
|
||||||
|
IToolExecutionCoordinator toolExecutionCoordinator,
|
||||||
|
Func<ContentBlock, string> formatToolCallSummary,
|
||||||
|
Action<AgentEventType, string, string> emitEvent)
|
||||||
|
{
|
||||||
|
_toolExecutionCoordinator = toolExecutionCoordinator;
|
||||||
|
_formatToolCallSummary = formatToolCallSummary;
|
||||||
|
_emitEvent = emitEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<AgentLoopLlmDispatchStageResult> ExecuteAsync(
|
||||||
|
AgentLoopLlmDispatchStageInput input,
|
||||||
|
CancellationToken ct)
|
||||||
|
{
|
||||||
|
WorkflowLogService.SetCallContext(input.ConversationId, input.RunId, input.Iteration);
|
||||||
|
WorkflowLogService.LogTransition(
|
||||||
|
input.ConversationId,
|
||||||
|
input.RunId,
|
||||||
|
input.Iteration,
|
||||||
|
"query_context",
|
||||||
|
input.QueryContextDetail);
|
||||||
|
WorkflowLogService.LogLlmRequest(
|
||||||
|
input.ConversationId,
|
||||||
|
input.RunId,
|
||||||
|
input.Iteration,
|
||||||
|
input.CurrentModel,
|
||||||
|
input.LlmRequest.SendMessages.Count,
|
||||||
|
input.ActiveTools.Count,
|
||||||
|
input.LlmRequest.ForceInitialToolCall);
|
||||||
|
|
||||||
|
var streamPreviewObserver = new StreamPreviewObserver(_formatToolCallSummary, _emitEvent);
|
||||||
|
var dispatchStopwatch = Stopwatch.StartNew();
|
||||||
|
var blocks = await _toolExecutionCoordinator.SendWithToolsWithRecoveryAsync(
|
||||||
|
input.LlmRequest.SendMessages,
|
||||||
|
input.ActiveTools,
|
||||||
|
ct,
|
||||||
|
input.PhaseLabel,
|
||||||
|
input.RunState,
|
||||||
|
forceToolCall: input.LlmRequest.ForceInitialToolCall,
|
||||||
|
prefetchToolCallAsync: block => _toolExecutionCoordinator.TryPrefetchReadOnlyToolAsync(
|
||||||
|
block,
|
||||||
|
input.ActiveTools,
|
||||||
|
input.Context,
|
||||||
|
ct),
|
||||||
|
onStreamEventAsync: streamPreviewObserver.HandleAsync).ConfigureAwait(false);
|
||||||
|
dispatchStopwatch.Stop();
|
||||||
|
|
||||||
|
input.RunState.ContextRecoveryAttempts = 0;
|
||||||
|
input.RunState.TransientLlmErrorRetries = 0;
|
||||||
|
|
||||||
|
return new AgentLoopLlmDispatchStageResult(blocks, dispatchStopwatch.ElapsedMilliseconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class StreamPreviewObserver
|
||||||
|
{
|
||||||
|
private readonly Func<ContentBlock, string> _formatToolCallSummary;
|
||||||
|
private readonly Action<AgentEventType, string, string> _emitEvent;
|
||||||
|
private readonly StringBuilder _streamedTextPreview = new();
|
||||||
|
private DateTime _lastStreamUiUpdateAt = DateTime.MinValue;
|
||||||
|
|
||||||
|
public StreamPreviewObserver(
|
||||||
|
Func<ContentBlock, string> formatToolCallSummary,
|
||||||
|
Action<AgentEventType, string, string> emitEvent)
|
||||||
|
{
|
||||||
|
_formatToolCallSummary = formatToolCallSummary;
|
||||||
|
_emitEvent = emitEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task HandleAsync(ToolStreamEvent evt)
|
||||||
|
{
|
||||||
|
switch (evt.Kind)
|
||||||
|
{
|
||||||
|
case ToolStreamEventKind.TextDelta:
|
||||||
|
HandleTextDelta(evt.Text);
|
||||||
|
break;
|
||||||
|
case ToolStreamEventKind.ToolCallReady:
|
||||||
|
if (evt.ToolCall != null)
|
||||||
|
{
|
||||||
|
_emitEvent(
|
||||||
|
AgentEventType.Thinking,
|
||||||
|
evt.ToolCall.ToolName,
|
||||||
|
$"Streaming tool ready: {_formatToolCallSummary(evt.ToolCall)}");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ToolStreamEventKind.RetryReset:
|
||||||
|
_streamedTextPreview.Clear();
|
||||||
|
_lastStreamUiUpdateAt = DateTime.UtcNow;
|
||||||
|
_emitEvent(AgentEventType.Thinking, "", "Resetting the streamed partial response before retry.");
|
||||||
|
break;
|
||||||
|
case ToolStreamEventKind.Completed:
|
||||||
|
await Task.CompletedTask.ConfigureAwait(false);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleTextDelta(string text)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(text))
|
||||||
|
return;
|
||||||
|
|
||||||
|
_streamedTextPreview.Append(text);
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
if ((now - _lastStreamUiUpdateAt) < StreamPreviewUiInterval || _streamedTextPreview.Length == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var preview = AgentProgressSummarySanitizer.NormalizeThinkingSummary(
|
||||||
|
_streamedTextPreview.ToString(),
|
||||||
|
maxLength: 140);
|
||||||
|
if (string.IsNullOrWhiteSpace(preview))
|
||||||
|
return;
|
||||||
|
|
||||||
|
_emitEvent(AgentEventType.Thinking, "", preview);
|
||||||
|
_lastStreamUiUpdateAt = now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -155,13 +155,13 @@ internal static class AgentLoopPreLlmStageService
|
|||||||
{
|
{
|
||||||
return (
|
return (
|
||||||
"No tools remain available under the active skill runtime policy.",
|
"No tools remain available under the active skill runtime policy.",
|
||||||
"⚠ No tools are currently allowed by the active skill policy, so the task cannot continue. Check the allowed-tools configuration.");
|
"No tools are currently allowed by the active skill policy, so the task cannot continue. Check the allowed-tools configuration.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var tabLabel = string.IsNullOrWhiteSpace(activeTab) ? "current" : activeTab;
|
var tabLabel = string.IsNullOrWhiteSpace(activeTab) ? "current" : activeTab;
|
||||||
return (
|
return (
|
||||||
$"No tools are available in the {tabLabel} tab.",
|
$"No tools are available in the {tabLabel} tab.",
|
||||||
$"⚠ No tools are available in the {tabLabel} tab, so the task cannot continue. Check the tab-specific tool exposure policy.");
|
$"No tools are available in the {tabLabel} tab, so the task cannot continue. Check the tab-specific tool exposure policy.");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string Truncate(string text, int maxLength)
|
private static string Truncate(string text, int maxLength)
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ public partial class AgentLoopService
|
|||||||
private readonly ToolRegistry _tools;
|
private readonly ToolRegistry _tools;
|
||||||
private readonly SettingsService _settings;
|
private readonly SettingsService _settings;
|
||||||
private readonly IToolExecutionCoordinator _toolExecutionCoordinator;
|
private readonly IToolExecutionCoordinator _toolExecutionCoordinator;
|
||||||
|
private readonly AgentLoopLlmDispatchStageService _llmDispatchStageService;
|
||||||
|
|
||||||
// P4: JsonSerializer 옵션 공유 — 익명 객체 직렬화 시 기본 옵션 재생성 방지
|
// P4: JsonSerializer 옵션 공유 — 익명 객체 직렬화 시 기본 옵션 재생성 방지
|
||||||
private static readonly JsonSerializerOptions s_jsonOpts = new()
|
private static readonly JsonSerializerOptions s_jsonOpts = new()
|
||||||
@@ -161,6 +162,10 @@ public partial class AgentLoopService
|
|||||||
ForceContextRecovery,
|
ForceContextRecovery,
|
||||||
IsTransientLlmError,
|
IsTransientLlmError,
|
||||||
ComputeTransientLlmBackoffDelayMs);
|
ComputeTransientLlmBackoffDelayMs);
|
||||||
|
_llmDispatchStageService = new AgentLoopLlmDispatchStageService(
|
||||||
|
_toolExecutionCoordinator,
|
||||||
|
FormatToolCallSummary,
|
||||||
|
(eventType, toolName, summary) => EmitEvent(eventType, toolName, summary));
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TryGetPendingPermissionPreview(string toolName, string target, out PermissionPromptPreview? preview)
|
public bool TryGetPendingPermissionPreview(string toolName, string target, out PermissionPromptPreview? preview)
|
||||||
@@ -543,7 +548,8 @@ public partial class AgentLoopService
|
|||||||
|
|
||||||
// LLM에 도구 정의와 함께 요청
|
// LLM에 도구 정의와 함께 요청
|
||||||
List<ContentBlock> blocks;
|
List<ContentBlock> blocks;
|
||||||
var llmCallSw = Stopwatch.StartNew();
|
var llmRequest = preLlmStage.LlmRequest;
|
||||||
|
long llmCallElapsedMs = 0;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrWhiteSpace(preLlmStage.MissingToolsReturnMessage))
|
if (!string.IsNullOrWhiteSpace(preLlmStage.MissingToolsReturnMessage))
|
||||||
@@ -551,88 +557,34 @@ public partial class AgentLoopService
|
|||||||
EmitEvent(AgentEventType.Error, "", preLlmStage.MissingToolsEventSummary ?? "사용 가능한 도구가 없습니다.");
|
EmitEvent(AgentEventType.Error, "", preLlmStage.MissingToolsEventSummary ?? "사용 가능한 도구가 없습니다.");
|
||||||
return preLlmStage.MissingToolsReturnMessage;
|
return preLlmStage.MissingToolsReturnMessage;
|
||||||
}
|
}
|
||||||
var activeTools = cachedActiveTools;
|
|
||||||
var llmRequest = preLlmStage.LlmRequest!;
|
|
||||||
var forceFirst = llmRequest.ForceInitialToolCall;
|
|
||||||
var sendMessages = llmRequest.SendMessages;
|
|
||||||
|
|
||||||
// 워크플로우 상세 로그: LLM 요청
|
|
||||||
llmCallSw.Restart();
|
|
||||||
var (_, currentModel) = _llm.GetCurrentModelInfo();
|
var (_, currentModel) = _llm.GetCurrentModelInfo();
|
||||||
WorkflowLogService.SetCallContext(_conversationId, _currentRunId, iteration);
|
var queryContextDetail = BuildQueryContextDetail(queryView, llmRequest!, codeWorkingSet);
|
||||||
WorkflowLogService.LogTransition(
|
var dispatchResult = await _llmDispatchStageService.ExecuteAsync(
|
||||||
|
new AgentLoopLlmDispatchStageInput(
|
||||||
|
iteration,
|
||||||
_conversationId,
|
_conversationId,
|
||||||
_currentRunId,
|
_currentRunId,
|
||||||
iteration,
|
|
||||||
"query_context",
|
|
||||||
BuildQueryContextDetail(queryView, llmRequest, codeWorkingSet));
|
|
||||||
WorkflowLogService.LogLlmRequest(_conversationId, _currentRunId, iteration,
|
|
||||||
currentModel, sendMessages.Count, activeTools.Count, forceFirst);
|
|
||||||
var streamedTextPreview = new StringBuilder();
|
|
||||||
var lastStreamUiUpdateAt = DateTime.MinValue;
|
|
||||||
|
|
||||||
blocks = await SendWithToolsWithRecoveryAsync(
|
|
||||||
sendMessages,
|
|
||||||
activeTools,
|
|
||||||
ct,
|
|
||||||
$"메인 루프 {iteration}",
|
$"메인 루프 {iteration}",
|
||||||
runState,
|
currentModel,
|
||||||
forceToolCall: forceFirst,
|
queryContextDetail,
|
||||||
prefetchToolCallAsync: block => TryPrefetchReadOnlyToolAsync(
|
llmRequest!,
|
||||||
block,
|
cachedActiveTools,
|
||||||
activeTools,
|
|
||||||
context,
|
context,
|
||||||
ct),
|
runState),
|
||||||
onStreamEventAsync: async evt =>
|
ct).ConfigureAwait(false);
|
||||||
{
|
blocks = dispatchResult.Blocks;
|
||||||
switch (evt.Kind)
|
llmCallElapsedMs = dispatchResult.ElapsedMilliseconds;
|
||||||
{
|
|
||||||
case ToolStreamEventKind.TextDelta:
|
|
||||||
if (!string.IsNullOrWhiteSpace(evt.Text))
|
|
||||||
{
|
|
||||||
streamedTextPreview.Append(evt.Text);
|
|
||||||
var now = DateTime.UtcNow;
|
|
||||||
if ((now - lastStreamUiUpdateAt).TotalMilliseconds >= 450 && streamedTextPreview.Length > 0)
|
|
||||||
{
|
|
||||||
var preview = AgentProgressSummarySanitizer.NormalizeThinkingSummary(
|
|
||||||
streamedTextPreview.ToString(),
|
|
||||||
maxLength: 140);
|
|
||||||
if (!string.IsNullOrWhiteSpace(preview))
|
|
||||||
{
|
|
||||||
EmitEvent(AgentEventType.Thinking, "", preview);
|
|
||||||
lastStreamUiUpdateAt = now;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case ToolStreamEventKind.ToolCallReady:
|
|
||||||
if (evt.ToolCall != null)
|
|
||||||
{
|
|
||||||
EmitEvent(
|
|
||||||
AgentEventType.Thinking,
|
|
||||||
evt.ToolCall.ToolName,
|
|
||||||
$"스트리밍 도구 감지: {FormatToolCallSummary(evt.ToolCall)}");
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case ToolStreamEventKind.Completed:
|
|
||||||
await Task.CompletedTask;
|
|
||||||
break;
|
|
||||||
case ToolStreamEventKind.RetryReset:
|
|
||||||
streamedTextPreview.Clear();
|
|
||||||
lastStreamUiUpdateAt = DateTime.UtcNow;
|
|
||||||
EmitEvent(AgentEventType.Thinking, "", "스트리밍 중간 응답을 정리하고 재시도합니다.");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
runState.ContextRecoveryAttempts = 0;
|
|
||||||
llmCallSw.Stop();
|
|
||||||
runState.TransientLlmErrorRetries = 0;
|
|
||||||
NotifyPostCompactionTurnIfNeeded(runState);
|
NotifyPostCompactionTurnIfNeeded(runState);
|
||||||
}
|
}
|
||||||
catch (NotSupportedException)
|
catch (NotSupportedException)
|
||||||
{
|
{
|
||||||
// Function Calling 미지원 서비스 → 일반 텍스트 응답으로 대체
|
// Function Calling 미지원 서비스 → 일반 텍스트 응답으로 대체
|
||||||
var textResp = await _llm.SendAsync(queryMessages, ct);
|
var textFallbackMessages = llmRequest == null
|
||||||
|
? queryMessages
|
||||||
|
: llmRequest.InjectedToolReminder && llmRequest.SendMessages.Count > 0
|
||||||
|
? llmRequest.SendMessages.Take(llmRequest.SendMessages.Count - 1).ToList()
|
||||||
|
: llmRequest.SendMessages;
|
||||||
|
var textResp = await _llm.SendAsync(textFallbackMessages, ct);
|
||||||
EmitEvent(AgentEventType.Complete, "", "에이전트 작업 완료");
|
EmitEvent(AgentEventType.Complete, "", "에이전트 작업 완료");
|
||||||
return textResp;
|
return textResp;
|
||||||
}
|
}
|
||||||
@@ -754,7 +706,7 @@ public partial class AgentLoopService
|
|||||||
textResponse, toolCalls.Count,
|
textResponse, toolCalls.Count,
|
||||||
_llm.LastTokenUsage?.PromptTokens ?? 0,
|
_llm.LastTokenUsage?.PromptTokens ?? 0,
|
||||||
_llm.LastTokenUsage?.CompletionTokens ?? 0,
|
_llm.LastTokenUsage?.CompletionTokens ?? 0,
|
||||||
llmCallSw.ElapsedMilliseconds);
|
llmCallElapsedMs);
|
||||||
|
|
||||||
// Task Decomposition: 첫 번째 텍스트 응답에서 계획 단계 추출
|
// Task Decomposition: 첫 번째 텍스트 응답에서 계획 단계 추출
|
||||||
if (!planExtracted && !string.IsNullOrEmpty(textResponse))
|
if (!planExtracted && !string.IsNullOrEmpty(textResponse))
|
||||||
|
|||||||
@@ -68,13 +68,13 @@ internal sealed class StreamingToolExecutionCoordinator : IToolExecutionCoordina
|
|||||||
_emitEvent(
|
_emitEvent(
|
||||||
AgentEventType.Thinking,
|
AgentEventType.Thinking,
|
||||||
resolvedToolName,
|
resolvedToolName,
|
||||||
$"읽기 도구 조기 실행 준비: {resolvedToolName}");
|
$"Preparing early execution for {resolvedToolName}");
|
||||||
|
|
||||||
var sw = Stopwatch.StartNew();
|
var sw = Stopwatch.StartNew();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var input = block.ToolInput ?? JsonDocument.Parse("{}").RootElement;
|
var input = block.ToolInput ?? JsonDocument.Parse("{}").RootElement;
|
||||||
var result = await _executeToolAsync(resolvedToolName, input, context, null, ct);
|
var result = await _executeToolAsync(resolvedToolName, input, context, null, ct).ConfigureAwait(false);
|
||||||
sw.Stop();
|
sw.Stop();
|
||||||
return new ToolPrefetchResult(result, sw.ElapsedMilliseconds, resolvedToolName);
|
return new ToolPrefetchResult(result, sw.ElapsedMilliseconds, resolvedToolName);
|
||||||
}
|
}
|
||||||
@@ -82,7 +82,7 @@ internal sealed class StreamingToolExecutionCoordinator : IToolExecutionCoordina
|
|||||||
{
|
{
|
||||||
sw.Stop();
|
sw.Stop();
|
||||||
return new ToolPrefetchResult(
|
return new ToolPrefetchResult(
|
||||||
ToolResult.Fail($"조기 실행 오류: {ex.Message}"),
|
ToolResult.Fail($"Early execution failed: {ex.Message}"),
|
||||||
sw.ElapsedMilliseconds,
|
sw.ElapsedMilliseconds,
|
||||||
resolvedToolName);
|
resolvedToolName);
|
||||||
}
|
}
|
||||||
@@ -107,14 +107,14 @@ internal sealed class StreamingToolExecutionCoordinator : IToolExecutionCoordina
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (onStreamEventAsync == null)
|
if (onStreamEventAsync == null)
|
||||||
return await _llm.SendWithToolsAsync(messages, tools, ct, forceToolCall, prefetchToolCallAsync);
|
return await _llm.SendWithToolsAsync(messages, tools, ct, forceToolCall, prefetchToolCallAsync).ConfigureAwait(false);
|
||||||
|
|
||||||
var blocks = new List<ContentBlock>();
|
var blocks = new List<ContentBlock>();
|
||||||
var textBuilder = new StringBuilder();
|
var textBuilder = new StringBuilder();
|
||||||
var (service, model) = _llm.GetCurrentModelInfo();
|
var (service, model) = _llm.GetCurrentModelInfo();
|
||||||
LogService.Info(
|
LogService.Info(
|
||||||
$"[AgentLoopWait] {phaseLabel}: LLM 요청 시작 (service={service}, model={model}, messages={messages.Count}, tools={tools.Count}, forceToolCall={forceToolCall})");
|
$"[AgentLoopWait] {phaseLabel}: starting LLM request (service={service}, model={model}, messages={messages.Count}, tools={tools.Count}, forceToolCall={forceToolCall})");
|
||||||
_emitEvent(AgentEventType.Thinking, "", $"{phaseLabel}: 모델에 요청하는 중입니다...");
|
_emitEvent(AgentEventType.Thinking, "", $"{phaseLabel}: requesting the model...");
|
||||||
|
|
||||||
var waitStopwatch = Stopwatch.StartNew();
|
var waitStopwatch = Stopwatch.StartNew();
|
||||||
var firstEventReceived = false;
|
var firstEventReceived = false;
|
||||||
@@ -151,12 +151,12 @@ internal sealed class StreamingToolExecutionCoordinator : IToolExecutionCoordina
|
|||||||
{
|
{
|
||||||
firstEventReceived = true;
|
firstEventReceived = true;
|
||||||
LogService.Debug(
|
LogService.Debug(
|
||||||
$"[AgentLoopWait] {phaseLabel}: 첫 응답 수신 ({waitStopwatch.ElapsedMilliseconds}ms, kind={evt.Kind})");
|
$"[AgentLoopWait] {phaseLabel}: first response received ({waitStopwatch.ElapsedMilliseconds}ms, kind={evt.Kind})");
|
||||||
if (waitStopwatch.Elapsed >= _firstResponseHeartbeatDelay)
|
if (waitStopwatch.Elapsed >= _firstResponseHeartbeatDelay)
|
||||||
_emitEvent(AgentEventType.Thinking, "", $"{phaseLabel}: 모델 첫 응답을 받아 계속 진행합니다.");
|
_emitEvent(AgentEventType.Thinking, "", $"{phaseLabel}: received the first response and continuing.");
|
||||||
}
|
}
|
||||||
|
|
||||||
await onStreamEventAsync(evt);
|
await onStreamEventAsync(evt).ConfigureAwait(false);
|
||||||
if (evt.Kind == ToolStreamEventKind.TextDelta && !string.IsNullOrWhiteSpace(evt.Text))
|
if (evt.Kind == ToolStreamEventKind.TextDelta && !string.IsNullOrWhiteSpace(evt.Text))
|
||||||
{
|
{
|
||||||
streamedAnyPartialState = true;
|
streamedAnyPartialState = true;
|
||||||
@@ -183,7 +183,11 @@ internal sealed class StreamingToolExecutionCoordinator : IToolExecutionCoordina
|
|||||||
&& _forceContextRecovery(messages))
|
&& _forceContextRecovery(messages))
|
||||||
{
|
{
|
||||||
if (onStreamEventAsync != null && streamedAnyPartialState)
|
if (onStreamEventAsync != null && streamedAnyPartialState)
|
||||||
await onStreamEventAsync(new ToolStreamEvent(ToolStreamEventKind.RetryReset, $"{phaseLabel}:retry"));
|
{
|
||||||
|
await onStreamEventAsync(new ToolStreamEvent(ToolStreamEventKind.RetryReset, $"{phaseLabel}:retry"))
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
contextRecoveryRetries++;
|
contextRecoveryRetries++;
|
||||||
if (runState != null)
|
if (runState != null)
|
||||||
runState.ContextRecoveryAttempts = contextRecoveryRetries;
|
runState.ContextRecoveryAttempts = contextRecoveryRetries;
|
||||||
@@ -191,7 +195,7 @@ internal sealed class StreamingToolExecutionCoordinator : IToolExecutionCoordina
|
|||||||
_emitEvent(
|
_emitEvent(
|
||||||
AgentEventType.Thinking,
|
AgentEventType.Thinking,
|
||||||
"",
|
"",
|
||||||
$"{phaseLabel}: 컨텍스트 한도 초과로 대화를 압축한 후 재시도합니다 ({contextRecoveryRetries}/2)");
|
$"{phaseLabel}: context overflow detected. Recovering and retrying ({contextRecoveryRetries}/2)");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,7 +205,11 @@ internal sealed class StreamingToolExecutionCoordinator : IToolExecutionCoordina
|
|||||||
if (_isTransientLlmError(ex) && transientRetries < 3)
|
if (_isTransientLlmError(ex) && transientRetries < 3)
|
||||||
{
|
{
|
||||||
if (onStreamEventAsync != null && streamedAnyPartialState)
|
if (onStreamEventAsync != null && streamedAnyPartialState)
|
||||||
await onStreamEventAsync(new ToolStreamEvent(ToolStreamEventKind.RetryReset, $"{phaseLabel}:retry"));
|
{
|
||||||
|
await onStreamEventAsync(new ToolStreamEvent(ToolStreamEventKind.RetryReset, $"{phaseLabel}:retry"))
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
transientRetries++;
|
transientRetries++;
|
||||||
if (runState != null)
|
if (runState != null)
|
||||||
runState.TransientLlmErrorRetries = transientRetries;
|
runState.TransientLlmErrorRetries = transientRetries;
|
||||||
@@ -210,8 +218,8 @@ internal sealed class StreamingToolExecutionCoordinator : IToolExecutionCoordina
|
|||||||
_emitEvent(
|
_emitEvent(
|
||||||
AgentEventType.Thinking,
|
AgentEventType.Thinking,
|
||||||
"",
|
"",
|
||||||
$"{phaseLabel}: 일시적 LLM 오류로 {delayMs}ms 후 재시도합니다 ({transientRetries}/3)");
|
$"{phaseLabel}: transient LLM error. Retrying after {delayMs}ms ({transientRetries}/3)");
|
||||||
await Task.Delay(delayMs, ct);
|
await Task.Delay(delayMs, ct).ConfigureAwait(false);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,8 +232,8 @@ internal sealed class StreamingToolExecutionCoordinator : IToolExecutionCoordina
|
|||||||
{
|
{
|
||||||
var seconds = Math.Max(1, (int)Math.Round(waited.TotalSeconds));
|
var seconds = Math.Max(1, (int)Math.Round(waited.TotalSeconds));
|
||||||
var summary = firstEventReceived
|
var summary = firstEventReceived
|
||||||
? $"{phaseLabel}: 모델 응답이 길어져 계속 기다리는 중입니다... ({seconds}초)"
|
? $"{phaseLabel}: model response is slow; still waiting... ({seconds}s)"
|
||||||
: $"{phaseLabel}: 모델 첫 응답을 기다리는 중입니다... ({seconds}초)";
|
: $"{phaseLabel}: waiting for the first model response... ({seconds}s)";
|
||||||
LogService.Debug($"[AgentLoopWait] {summary}");
|
LogService.Debug($"[AgentLoopWait] {summary}");
|
||||||
_emitEvent(AgentEventType.Thinking, "", summary);
|
_emitEvent(AgentEventType.Thinking, "", summary);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user