핵심 엔진을 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:
@@ -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"
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user