핵심 엔진을 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:
2026-04-09 21:07:49 +09:00
parent 227f5ab0d3
commit 3c6d2f1ce4
8 changed files with 280 additions and 100 deletions

View File

@@ -23,7 +23,7 @@ public partial class AgentLoopService
string Content,
string? PreviousContent = null);
private readonly LlmService _llm;
private readonly ILlmService _llm;
private readonly ToolRegistry _tools;
private readonly SettingsService _settings;
private readonly IToolExecutionCoordinator _toolExecutionCoordinator;
@@ -82,7 +82,7 @@ public partial class AgentLoopService
/// <summary>에이전트 이벤트 발생 시 호출되는 콜백 (UI 표시용).</summary>
public event Action<AgentEvent>? EventOccurred;
public AgentLoopService(LlmService llm, ToolRegistry tools, SettingsService settings)
public AgentLoopService(ILlmService llm, ToolRegistry tools, SettingsService settings)
{
_llm = llm;
_tools = tools;
@@ -180,6 +180,9 @@ public partial class AgentLoopService
Scope = ClassifyExplorationScope(userQuery, ActiveTab),
SelectiveHit = true,
};
var pathAccessState = new PathAccessTrackingState();
DateTime? lastToolResultAtUtc = null;
string? lastToolResultToolName = null;
// 워크플로우 상세 로그: 에이전트 루프 시작
WorkflowLogService.LogAgentLifecycle(_conversationId, _currentRunId, "start",
@@ -242,6 +245,11 @@ public partial class AgentLoopService
var context = BuildContext();
InjectTaskTypeGuidance(messages, taskPolicy);
InjectExplorationScopeGuidance(messages, explorationState.Scope);
var preferredInitialToolSequence = BuildPreferredInitialToolSequence(
explorationState,
taskPolicy,
ActiveTab,
userQuery);
EmitEvent(
AgentEventType.Thinking,
"",
@@ -518,6 +526,19 @@ public partial class AgentLoopService
}
}
if (lastToolResultAtUtc is { } toolResultAt)
{
var waitMs = Math.Max(0, (long)(DateTime.UtcNow - toolResultAt).TotalMilliseconds);
WorkflowLogService.LogTransition(
_conversationId,
_currentRunId,
iteration,
"llm_wait_after_tool_result",
$"{lastToolResultToolName ?? "unknown"}:{waitMs}ms");
lastToolResultAtUtc = null;
lastToolResultToolName = null;
}
EmitEvent(AgentEventType.Thinking, "", $"LLM에 요청 중... (반복 {iteration}/{maxIterations})");
// Gemini 무료 티어 모드: LLM 호출 간 딜레이 (RPM 한도 초과 방지)
@@ -554,10 +575,15 @@ public partial class AgentLoopService
}
// P1: 반복당 1회 캐시 — GetRuntimeActiveTools는 동일 파라미터로 반복 내 여러 번 호출됨
var cachedActiveTools = GetRuntimeActiveTools(llm.DisabledTools, runtimeOverrides);
var cachedActiveTools = FilterExplorationToolsForCurrentIteration(
GetRuntimeActiveTools(llm.DisabledTools, runtimeOverrides),
explorationState,
userQuery,
ActiveTab,
totalToolCalls);
// LLM에 도구 정의와 함께 요청
List<LlmService.ContentBlock> blocks;
List<ContentBlock> blocks;
var llmCallSw = Stopwatch.StartNew();
try
{
@@ -612,7 +638,7 @@ public partial class AgentLoopService
{
switch (evt.Kind)
{
case LlmService.ToolStreamEventKind.TextDelta:
case ToolStreamEventKind.TextDelta:
if (!string.IsNullOrWhiteSpace(evt.Text))
{
streamedTextPreview.Append(evt.Text);
@@ -626,7 +652,7 @@ public partial class AgentLoopService
}
}
break;
case LlmService.ToolStreamEventKind.ToolCallReady:
case ToolStreamEventKind.ToolCallReady:
if (evt.ToolCall != null)
{
EmitEvent(
@@ -635,9 +661,14 @@ public partial class AgentLoopService
$"스트리밍 도구 감지: {FormatToolCallSummary(evt.ToolCall)}");
}
break;
case LlmService.ToolStreamEventKind.Completed:
case ToolStreamEventKind.Completed:
await Task.CompletedTask;
break;
case ToolStreamEventKind.RetryReset:
streamedTextPreview.Clear();
lastStreamUiUpdateAt = DateTime.UtcNow;
EmitEvent(AgentEventType.Thinking, "", "스트리밍 중간 응답을 정리하고 재시도합니다.");
break;
}
});
runState.ContextRecoveryAttempts = 0;
@@ -745,7 +776,7 @@ public partial class AgentLoopService
// 응답에서 텍스트와 도구 호출 분리
var textParts = new List<string>();
var toolCalls = new List<LlmService.ContentBlock>();
var toolCalls = new List<ContentBlock>();
foreach (var block in blocks)
{
@@ -851,13 +882,15 @@ public partial class AgentLoopService
"[System:ToolCallRequired] " +
"⚠ 경고: 이전 응답에서 도구를 호출하지 않았습니다. " +
"텍스트 설명만 반환하는 것은 허용되지 않습니다. " +
$"먼저 권장 순서는 {preferredInitialToolSequence} 입니다. " +
"지금 즉시 아래 형식으로 도구를 호출하세요:\n" +
"<tool_call>\n{\"name\": \"TOOL_NAME\", \"arguments\": {\"param\": \"value\"}}\n</tool_call>\n" +
$"사용 가능한 도구: {activeToolPreview}",
_ =>
"[System:ToolCallRequired] " +
"🚨 최종 경고: 도구를 계속 호출하지 않고 있습니다. 이것이 마지막 기회입니다. " +
"텍스트는 한 글자도 쓰지 마세요. 반드시 아래 형식으로 도구를 호출하세요:\n" +
$"텍스트는 한 글자도 쓰지 말고 {preferredInitialToolSequence} 순서에 맞는 첫 도구를 고르세요. " +
"반드시 아래 형식으로 도구를 호출하세요:\n" +
"<tool_call>\n{\"name\": \"TOOL_NAME\", \"arguments\": {\"param\": \"value\"}}\n</tool_call>\n" +
$"반드시 사용해야 할 도구 목록: {activeToolPreview}"
};
@@ -891,12 +924,13 @@ public partial class AgentLoopService
{
1 =>
"[System:ToolCallRequired] 계획을 세웠지만 도구를 호출하지 않았습니다. " +
$"권장 첫 순서는 {preferredInitialToolSequence} 입니다. " +
"지금 당장 실행하세요. 아래 형식으로 도구를 호출하세요:\n" +
"<tool_call>\n{\"name\": \"TOOL_NAME\", \"arguments\": {\"param\": \"value\"}}\n</tool_call>\n" +
$"사용 가능한 도구: {planToolList}",
_ =>
"[System:ToolCallRequired] 🚨 도구 호출 없이 계획만 반복하고 있습니다. " +
"텍스트를 한 글자도 쓰지 마세요. 오직 아래 형식의 도구 호출만 출력하세요:\n" +
$"텍스트를 한 글자도 쓰지 말고 {preferredInitialToolSequence} 순서에 맞는 도구 호출만 출력하세요:\n" +
"<tool_call>\n{\"name\": \"TOOL_NAME\", \"arguments\": {\"param\": \"value\"}}\n</tool_call>\n" +
$"사용 가능한 도구: {planToolList}"
};
@@ -1248,7 +1282,7 @@ public partial class AgentLoopService
: resolvedToolName;
var effectiveCall = string.Equals(call.ToolName, effectiveToolName, StringComparison.OrdinalIgnoreCase)
? call
: new LlmService.ContentBlock
: new ContentBlock
{
Type = call.Type,
Text = call.Text,
@@ -1306,6 +1340,18 @@ public partial class AgentLoopService
continue;
}
if (TryHandleRepeatedPathAccessTransition(
effectiveCall,
context,
pathAccessState,
messages,
lastModifiedCodeFilePath,
requireHighImpactCodeVerification,
taskPolicy))
{
continue;
}
// Task Decomposition: 단계 진행률 추적
if (planSteps.Count > 0)
{
@@ -1345,7 +1391,7 @@ public partial class AgentLoopService
Role = "system",
Content =
"Exploration correction: The current request is narrow. Stop broad workspace scanning. " +
"Use grep/glob to identify only files directly related to the user's topic, then read a very small targeted set. " +
"Follow this order: glob/grep -> targeted file_read/document_read -> folder_map only if the user explicitly needs folder structure. " +
"Do not repeat folder_map unless repository structure is genuinely required."
});
EmitEvent(
@@ -1405,7 +1451,7 @@ public partial class AgentLoopService
if (llm.EnableHookInputMutation &&
TryGetHookUpdatedInput(preResults, out var updatedInput))
{
effectiveCall = new LlmService.ContentBlock
effectiveCall = new ContentBlock
{
Type = effectiveCall.Type,
Text = effectiveCall.Text,
@@ -1533,6 +1579,8 @@ public partial class AgentLoopService
ref statsFailCount,
ref statsInputTokens,
ref statsOutputTokens);
lastToolResultAtUtc = DateTime.UtcNow;
lastToolResultToolName = effectiveCall.ToolName;
if (!result.Success)
{
@@ -1608,7 +1656,14 @@ public partial class AgentLoopService
messages.Add(LlmService.CreateToolResultMessage(
effectiveCall.ToolId,
effectiveCall.ToolName,
BuildLoopToolResultMessage(effectiveCall, result, runState)));
BuildToolResultFeedbackMessage(
effectiveCall,
result,
runState,
context,
messages,
explorationState,
iteration)));
if (TryHandleReadOnlyStagnationTransition(
consecutiveReadOnlySuccessTools,
@@ -1909,7 +1964,8 @@ public partial class AgentLoopService
SkillRuntimeOverrides? runtimeOverrides)
{
var mergedDisabled = MergeDisabledTools(disabledToolNames);
var active = _tools.GetActiveTools(mergedDisabled);
// 탭별 도구 필터링: 현재 탭에 맞는 도구만 LLM에 전송 (토큰 절약)
var active = _tools.GetActiveToolsForTab(ActiveTab, mergedDisabled);
if (runtimeOverrides == null || runtimeOverrides.AllowedToolNames.Count == 0)
return active;
@@ -4136,7 +4192,7 @@ public partial class AgentLoopService
// 검증 응답 처리
var verifyText = new List<string>();
var verifyToolCalls = new List<LlmService.ContentBlock>();
var verifyToolCalls = new List<ContentBlock>();
foreach (var block in verifyBlocks)
{
@@ -4724,7 +4780,7 @@ public partial class AgentLoopService
}
/// <summary>영향 범위 기반 의사결정 체크. 확인이 필요하면 메시지를 반환, 불필요하면 null.</summary>
private string? CheckDecisionRequired(LlmService.ContentBlock call, AgentContext context)
private string? CheckDecisionRequired(ContentBlock call, AgentContext context)
{
var app = System.Windows.Application.Current as App;
var level = app?.SettingsService?.Settings.Llm.AgentDecisionLevel ?? "normal";
@@ -4811,7 +4867,7 @@ public partial class AgentLoopService
return null;
}
private static string FormatToolCallSummary(LlmService.ContentBlock call)
private static string FormatToolCallSummary(ContentBlock call)
{
if (call.ToolInput == null) return call.ToolName;
try
@@ -4829,7 +4885,7 @@ public partial class AgentLoopService
catch { return call.ToolName; }
}
private static string BuildToolCallSignature(LlmService.ContentBlock call)
private static string BuildToolCallSignature(ContentBlock call)
{
var name = (call.ToolName ?? "").Trim().ToLowerInvariant();
if (call.ToolInput == null)
@@ -4929,7 +4985,7 @@ public partial class AgentLoopService
FailureRecoveryKind.Permission =>
"권한설정 확인 -> 대상 경로 재선정 -> file_read/grep으로 대체 검증 -> 승인 후 재실행",
FailureRecoveryKind.Path =>
"folder_map/glob -> 절대경로 확인 -> file_read로 존재 검증 -> 원도구 재실행",
"glob -> file_read -> folder_map(필요 시만) -> 절대경로 확인 -> 원도구 재실행",
FailureRecoveryKind.Command =>
"도구 스키마 재확인 -> 최소 파라미터 호출 -> 옵션 확장 -> 필요 시 대체 도구",
FailureRecoveryKind.Dependency =>
@@ -4940,7 +4996,7 @@ public partial class AgentLoopService
{
"build_run" or "test_loop" => "file_read -> grep/glob -> git_tool(diff) -> file_edit -> build_run/test_loop",
"file_edit" or "file_write" => "file_read -> grep/glob -> git_tool(diff) -> file_edit -> build_run/test_loop",
"grep" or "glob" => "file_read -> folder_map -> grep/glob",
"grep" or "glob" => "glob/grep 재구성 -> file_read -> folder_map(필요 시만)",
_ => "file_read -> grep/glob -> git_tool(diff) -> targeted tool retry"
}
};