핵심 엔진을 claude-code 기준으로 정렬하고 스트리밍 재시도 경계를 정리한다
- StreamingToolExecutionCoordinator에서 조기 실행 대상을 file_read/document_read 중심으로 축소하고 folder_map 등 구조 탐색 도구를 prefetch 대상에서 제거함 - 스트리밍 재시도 전에 RetryReset 이벤트를 추가해 중간 응답 미리보기 누적을 끊고 AgentLoopService가 재시도 경계를 명확히 표시하도록 조정함 - AxAgentExecutionEngine의 Cowork/Code 빈 응답 합성을 보수적으로 바꿔 실행 근거가 있을 때만 완료 요약을 만들고 근거가 없으면 로그 확인 안내를 반환하도록 정리함 - Code 루프의 post-tool verification과 completion gate도 직전 수정에서 함께 정리해 일반 수정의 과검증을 줄였음 - README.md, docs/DEVELOPMENT.md에 2026-04-09 21:03 (KST) 기준 변경 이력과 검증 결과를 반영함 검증 결과: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ 경고 0 / 오류 0
This commit is contained in:
@@ -11,13 +11,13 @@ internal sealed class StreamingToolExecutionCoordinator : IToolExecutionCoordina
|
||||
{
|
||||
private static readonly HashSet<string> PrefetchableReadOnlyTools = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"file_read", "glob", "grep", "grep_tool", "folder_map", "document_read",
|
||||
"search_codebase", "code_search", "env_tool", "datetime_tool",
|
||||
"file_read", "document_read",
|
||||
"env_tool", "datetime_tool",
|
||||
"dev_env_detect", "memory", "json_tool", "regex_tool", "base64_tool",
|
||||
"hash_tool", "image_analyze", "multi_read"
|
||||
"hash_tool", "image_analyze"
|
||||
};
|
||||
|
||||
private readonly LlmService _llm;
|
||||
private readonly ILlmService _llm;
|
||||
private readonly Func<string, IReadOnlyCollection<string>, string> _resolveRequestedToolName;
|
||||
private readonly Func<string, JsonElement, AgentContext, List<ChatMessage>?, CancellationToken, Task<ToolResult>> _executeToolAsync;
|
||||
private readonly Action<AgentEventType, string, string> _emitEvent;
|
||||
@@ -27,7 +27,7 @@ internal sealed class StreamingToolExecutionCoordinator : IToolExecutionCoordina
|
||||
private readonly Func<int, Exception, int> _computeTransientBackoffDelayMs;
|
||||
|
||||
public StreamingToolExecutionCoordinator(
|
||||
LlmService llm,
|
||||
ILlmService llm,
|
||||
Func<string, IReadOnlyCollection<string>, string> resolveRequestedToolName,
|
||||
Func<string, JsonElement, AgentContext, List<ChatMessage>?, CancellationToken, Task<ToolResult>> executeToolAsync,
|
||||
Action<AgentEventType, string, string> emitEvent,
|
||||
@@ -46,8 +46,8 @@ internal sealed class StreamingToolExecutionCoordinator : IToolExecutionCoordina
|
||||
_computeTransientBackoffDelayMs = computeTransientBackoffDelayMs;
|
||||
}
|
||||
|
||||
public async Task<LlmService.ToolPrefetchResult?> TryPrefetchReadOnlyToolAsync(
|
||||
LlmService.ContentBlock block,
|
||||
public async Task<ToolPrefetchResult?> TryPrefetchReadOnlyToolAsync(
|
||||
ContentBlock block,
|
||||
IReadOnlyCollection<IAgentTool> tools,
|
||||
AgentContext context,
|
||||
CancellationToken ct)
|
||||
@@ -70,57 +70,60 @@ internal sealed class StreamingToolExecutionCoordinator : IToolExecutionCoordina
|
||||
var input = block.ToolInput ?? JsonDocument.Parse("{}").RootElement;
|
||||
var result = await _executeToolAsync(resolvedToolName, input, context, null, ct);
|
||||
sw.Stop();
|
||||
return new LlmService.ToolPrefetchResult(result, sw.ElapsedMilliseconds, resolvedToolName);
|
||||
return new ToolPrefetchResult(result, sw.ElapsedMilliseconds, resolvedToolName);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
sw.Stop();
|
||||
return new LlmService.ToolPrefetchResult(
|
||||
return new ToolPrefetchResult(
|
||||
ToolResult.Fail($"조기 실행 오류: {ex.Message}"),
|
||||
sw.ElapsedMilliseconds,
|
||||
resolvedToolName);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<LlmService.ContentBlock>> SendWithToolsWithRecoveryAsync(
|
||||
public async Task<List<ContentBlock>> SendWithToolsWithRecoveryAsync(
|
||||
List<ChatMessage> messages,
|
||||
IReadOnlyCollection<IAgentTool> tools,
|
||||
CancellationToken ct,
|
||||
string phaseLabel,
|
||||
AgentLoopService.RunState? runState = null,
|
||||
bool forceToolCall = false,
|
||||
Func<LlmService.ContentBlock, Task<LlmService.ToolPrefetchResult?>>? prefetchToolCallAsync = null,
|
||||
Func<LlmService.ToolStreamEvent, Task>? onStreamEventAsync = null)
|
||||
Func<ContentBlock, Task<ToolPrefetchResult?>>? prefetchToolCallAsync = null,
|
||||
Func<ToolStreamEvent, Task>? onStreamEventAsync = null)
|
||||
{
|
||||
var transientRetries = runState?.TransientLlmErrorRetries ?? 0;
|
||||
var contextRecoveryRetries = runState?.ContextRecoveryAttempts ?? 0;
|
||||
|
||||
while (true)
|
||||
{
|
||||
var streamedAnyPartialState = false;
|
||||
try
|
||||
{
|
||||
if (onStreamEventAsync == null)
|
||||
return await _llm.SendWithToolsAsync(messages, tools, ct, forceToolCall, prefetchToolCallAsync);
|
||||
|
||||
var blocks = new List<LlmService.ContentBlock>();
|
||||
var blocks = new List<ContentBlock>();
|
||||
var textBuilder = new StringBuilder();
|
||||
await foreach (var evt in _llm.StreamWithToolsAsync(messages, tools, forceToolCall, prefetchToolCallAsync, ct).WithCancellation(ct))
|
||||
{
|
||||
await onStreamEventAsync(evt);
|
||||
if (evt.Kind == LlmService.ToolStreamEventKind.TextDelta && !string.IsNullOrWhiteSpace(evt.Text))
|
||||
if (evt.Kind == ToolStreamEventKind.TextDelta && !string.IsNullOrWhiteSpace(evt.Text))
|
||||
{
|
||||
streamedAnyPartialState = true;
|
||||
textBuilder.Append(evt.Text);
|
||||
}
|
||||
else if (evt.Kind == LlmService.ToolStreamEventKind.ToolCallReady && evt.ToolCall != null)
|
||||
else if (evt.Kind == ToolStreamEventKind.ToolCallReady && evt.ToolCall != null)
|
||||
{
|
||||
streamedAnyPartialState = true;
|
||||
blocks.Add(evt.ToolCall);
|
||||
}
|
||||
}
|
||||
|
||||
var result = new List<LlmService.ContentBlock>();
|
||||
var result = new List<ContentBlock>();
|
||||
var text = textBuilder.ToString().Trim();
|
||||
if (!string.IsNullOrWhiteSpace(text))
|
||||
result.Add(new LlmService.ContentBlock { Type = "text", Text = text });
|
||||
result.Add(new ContentBlock { Type = "text", Text = text });
|
||||
result.AddRange(blocks);
|
||||
return result;
|
||||
}
|
||||
@@ -130,6 +133,8 @@ internal sealed class StreamingToolExecutionCoordinator : IToolExecutionCoordina
|
||||
&& contextRecoveryRetries < 2
|
||||
&& _forceContextRecovery(messages))
|
||||
{
|
||||
if (onStreamEventAsync != null && streamedAnyPartialState)
|
||||
await onStreamEventAsync(new ToolStreamEvent(ToolStreamEventKind.RetryReset, $"{phaseLabel}:retry"));
|
||||
contextRecoveryRetries++;
|
||||
if (runState != null)
|
||||
runState.ContextRecoveryAttempts = contextRecoveryRetries;
|
||||
@@ -146,6 +151,8 @@ internal sealed class StreamingToolExecutionCoordinator : IToolExecutionCoordina
|
||||
|
||||
if (_isTransientLlmError(ex) && transientRetries < 3)
|
||||
{
|
||||
if (onStreamEventAsync != null && streamedAnyPartialState)
|
||||
await onStreamEventAsync(new ToolStreamEvent(ToolStreamEventKind.RetryReset, $"{phaseLabel}:retry"));
|
||||
transientRetries++;
|
||||
if (runState != null)
|
||||
runState.TransientLlmErrorRetries = transientRetries;
|
||||
|
||||
Reference in New Issue
Block a user