AX Agent 구조를 claw-code 기준으로 추가 정리해 transcript 렌더와 tool streaming 책임을 분리함
Some checks failed
Release Gate / gate (push) Has been cancelled

- ChatWindow.TranscriptRendering partial을 추가해 transcript windowing, 증분 렌더, 스크롤 보존 로직을 메인 ChatWindow.xaml.cs에서 분리
- StreamingToolExecutionCoordinator를 도입해 read-only 도구 prefetch, tool-use 스트리밍 수신, context overflow/transient error 복구를 별도 계층으로 이동
- AgentLoopRuntimeThresholds helper를 추가해 no-tool, plan retry, terminal evidence gate 임계값 계산을 AgentLoopService에서 분리
- AgentLoopTransitions.Execution은 coordinator thin wrapper 중심 구조로 정리해 이후 executor 고도화와 정책 변경이 덜 위험하도록 개선
- README와 docs/DEVELOPMENT.md를 2026-04-09 09:14 (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 00:16:54 +09:00
parent 74d43e701c
commit 8643562319
8 changed files with 441 additions and 343 deletions

View File

@@ -26,6 +26,7 @@ public partial class AgentLoopService
private readonly LlmService _llm;
private readonly ToolRegistry _tools;
private readonly SettingsService _settings;
private readonly StreamingToolExecutionCoordinator _toolExecutionCoordinator;
private readonly ConcurrentDictionary<string, PermissionPromptPreview> _pendingPermissionPreviews = new(StringComparer.OrdinalIgnoreCase);
/// <summary>에이전트 이벤트 스트림 (UI 바인딩용).</summary>
@@ -79,6 +80,21 @@ public partial class AgentLoopService
_llm = llm;
_tools = tools;
_settings = settings;
_toolExecutionCoordinator = new StreamingToolExecutionCoordinator(
_llm,
ResolveRequestedToolName,
async (toolName, input, context, messages, ct) =>
{
var tool = _tools.Get(toolName);
return tool == null
? ToolResult.Fail($"도구를 찾을 수 없습니다: {toolName}")
: await ExecuteToolWithTimeoutAsync(tool, toolName, input, context, messages, ct);
},
(eventType, toolName, summary) => EmitEvent(eventType, toolName, summary),
IsContextOverflowError,
ForceContextRecovery,
IsTransientLlmError,
ComputeTransientLlmBackoffDelayMs);
}
public bool TryGetPendingPermissionPreview(string toolName, string target, out PermissionPromptPreview? preview)
@@ -194,11 +210,11 @@ public partial class AgentLoopService
var taskPolicy = TaskTypePolicy.FromTaskType(taskType);
var executionPolicy = _llm.GetActiveExecutionPolicy();
var consecutiveNoToolResponses = 0;
var noToolResponseThreshold = GetNoToolCallResponseThreshold(executionPolicy.NoToolResponseThreshold);
var noToolRecoveryMaxRetries = GetNoToolCallRecoveryMaxRetries(executionPolicy.NoToolRecoveryMaxRetries);
var planExecutionRetryMax = GetPlanExecutionRetryMax(executionPolicy.PlanExecutionRetryMax);
var noToolResponseThreshold = AgentLoopRuntimeThresholds.GetNoToolCallResponseThreshold(executionPolicy.NoToolResponseThreshold);
var noToolRecoveryMaxRetries = AgentLoopRuntimeThresholds.GetNoToolCallRecoveryMaxRetries(executionPolicy.NoToolRecoveryMaxRetries);
var planExecutionRetryMax = AgentLoopRuntimeThresholds.GetPlanExecutionRetryMax(executionPolicy.PlanExecutionRetryMax);
var documentPlanRetryMax = Math.Max(0, executionPolicy.DocumentPlanRetryMax);
var terminalEvidenceGateRetryMax = GetTerminalEvidenceGateMaxRetries(executionPolicy.TerminalEvidenceGateMaxRetries);
var terminalEvidenceGateRetryMax = AgentLoopRuntimeThresholds.GetTerminalEvidenceGateMaxRetries(executionPolicy.TerminalEvidenceGateMaxRetries);
var failedToolHistogram = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
var runState = new RunState();
var requireHighImpactCodeVerification = false;
@@ -3675,58 +3691,6 @@ public partial class AgentLoopService
.Select(x => $"{x.Key}({x.Value})"));
}
private static int GetNoToolCallResponseThreshold()
=> GetNoToolCallResponseThreshold(defaultValue: 2);
private static int GetNoToolCallResponseThreshold(int defaultValue)
=> ResolveThresholdValue(
Environment.GetEnvironmentVariable("AXCOPILOT_NOTOOL_RESPONSE_THRESHOLD"),
defaultValue,
min: 1,
max: 6);
private static int GetNoToolCallRecoveryMaxRetries()
=> GetNoToolCallRecoveryMaxRetries(defaultValue: 3);
private static int GetNoToolCallRecoveryMaxRetries(int defaultValue)
=> ResolveThresholdValue(
Environment.GetEnvironmentVariable("AXCOPILOT_NOTOOL_RECOVERY_MAX_RETRIES"),
defaultValue,
min: 0,
max: 6);
private static int GetPlanExecutionRetryMax()
=> GetPlanExecutionRetryMax(defaultValue: 2);
private static int GetPlanExecutionRetryMax(int defaultValue)
=> ResolveThresholdValue(
Environment.GetEnvironmentVariable("AXCOPILOT_PLAN_EXECUTION_RETRY_MAX"),
defaultValue,
min: 0,
max: 6);
private static int ResolveNoToolCallResponseThreshold(string? envRaw)
=> ResolveThresholdValue(envRaw, defaultValue: 2, min: 1, max: 6);
private static int ResolveNoToolCallRecoveryMaxRetries(string? envRaw)
=> ResolveThresholdValue(envRaw, defaultValue: 3, min: 0, max: 6);
private static int ResolvePlanExecutionRetryMax(string? envRaw)
=> ResolveThresholdValue(envRaw, defaultValue: 2, min: 0, max: 6);
private static int GetTerminalEvidenceGateMaxRetries()
=> GetTerminalEvidenceGateMaxRetries(defaultValue: 1);
private static int GetTerminalEvidenceGateMaxRetries(int defaultValue)
=> ResolveThresholdValue(
Environment.GetEnvironmentVariable("AXCOPILOT_TERMINAL_EVIDENCE_GATE_MAX_RETRIES"),
defaultValue,
min: 0,
max: 3);
private static int ResolveTerminalEvidenceGateMaxRetries(string? envRaw)
=> ResolveThresholdValue(envRaw, defaultValue: 1, min: 0, max: 3);
private static string BuildUnknownToolRecoveryPrompt(
string unknownToolName,
IReadOnlyCollection<string> activeToolNames)