[Phase 41] SettingsViewModel·AgentLoopService 파셜 클래스 분할
SettingsViewModel (1,855줄 → 320줄, 82.7% 감소): - SettingsViewModel.Properties.cs (837줄): 바인딩 프로퍼티 전체 - SettingsViewModel.Methods.cs (469줄): Save/Browse/Add 등 메서드 - SettingsViewModelModels.cs (265줄): 6개 모델 클래스 분리 AgentLoopService (1,823줄 → 1,334줄, 26.8% 감소): - AgentLoopService.Execution.cs (498줄): 병렬 도구 실행, ToolExecutionState - 빌드: 경고 0, 오류 0 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -10,7 +10,7 @@ namespace AxCopilot.Services.Agent;
|
||||
/// 에이전트 루프 엔진: LLM과 대화하며 도구/스킬을 실행하는 반복 루프.
|
||||
/// 계획 → 도구 실행 → 관찰 → 재평가 패턴을 구현합니다.
|
||||
/// </summary>
|
||||
public class AgentLoopService
|
||||
public partial class AgentLoopService
|
||||
{
|
||||
private static App? CurrentApp => System.Windows.Application.Current as App;
|
||||
|
||||
@@ -1331,493 +1331,4 @@ public class AgentLoopService
|
||||
}
|
||||
catch (Exception) { return call.ToolName; }
|
||||
}
|
||||
|
||||
private static string TruncateOutput(string output, int maxLength)
|
||||
{
|
||||
if (output.Length <= maxLength) return output;
|
||||
return output[..maxLength] + "\n... (출력 잘림)";
|
||||
}
|
||||
|
||||
// ─── 병렬 도구 실행 ──────────────────────────────────────────────────
|
||||
|
||||
// 읽기 전용 도구 (파일 상태를 변경하지 않음)
|
||||
private static readonly HashSet<string> ReadOnlyTools = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"file_read", "glob", "grep_tool", "folder_map", "document_read",
|
||||
"search_codebase", "code_search", "env_tool", "datetime_tool",
|
||||
"dev_env_detect", "memory", "skill_manager", "json_tool",
|
||||
"regex_tool", "base64_tool", "hash_tool", "image_analyze",
|
||||
};
|
||||
|
||||
/// <summary>도구 호출을 병렬 가능 / 순차 필수로 분류합니다.</summary>
|
||||
private static (List<LlmService.ContentBlock> Parallel, List<LlmService.ContentBlock> Sequential)
|
||||
ClassifyToolCalls(List<LlmService.ContentBlock> calls)
|
||||
{
|
||||
var parallel = new List<LlmService.ContentBlock>();
|
||||
var sequential = new List<LlmService.ContentBlock>();
|
||||
|
||||
foreach (var call in calls)
|
||||
{
|
||||
if (ReadOnlyTools.Contains(call.ToolName ?? ""))
|
||||
parallel.Add(call);
|
||||
else
|
||||
sequential.Add(call);
|
||||
}
|
||||
|
||||
// 읽기 전용 도구가 1개뿐이면 병렬화 의미 없음
|
||||
if (parallel.Count <= 1)
|
||||
{
|
||||
sequential.InsertRange(0, parallel);
|
||||
parallel.Clear();
|
||||
}
|
||||
|
||||
return (parallel, sequential);
|
||||
}
|
||||
|
||||
// ─── Phase 33-B: 도구 실행 루프 상태 (RunAsync에서 추출) ─────────────
|
||||
/// <summary>도구 실행 루프의 가변 상태. 메서드 간 공유.</summary>
|
||||
private class ToolExecutionState
|
||||
{
|
||||
public int CurrentStep;
|
||||
public int TotalToolCalls;
|
||||
public int MaxIterations;
|
||||
public int BaseMax;
|
||||
public int MaxRetry;
|
||||
public int ConsecutiveErrors;
|
||||
public int StatsSuccessCount;
|
||||
public int StatsFailCount;
|
||||
public int StatsInputTokens;
|
||||
public int StatsOutputTokens;
|
||||
public List<string> StatsUsedTools = new();
|
||||
public List<string> PlanSteps = new();
|
||||
public bool DocumentPlanCalled;
|
||||
public string? DocumentPlanPath;
|
||||
public string? DocumentPlanTitle;
|
||||
public string? DocumentPlanScaffold;
|
||||
}
|
||||
|
||||
/// <summary>병렬 실행용 가변 상태 (ToolExecutionState의 서브셋). TODO: ToolExecutionState로 통합 예정.</summary>
|
||||
private class ParallelState
|
||||
{
|
||||
public int CurrentStep;
|
||||
public int TotalToolCalls;
|
||||
public int MaxIterations;
|
||||
public int ConsecutiveErrors;
|
||||
public int StatsSuccessCount;
|
||||
public int StatsFailCount;
|
||||
public int StatsInputTokens;
|
||||
public int StatsOutputTokens;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Phase 33-B: 단일 도구 호출을 처리합니다 (RunAsync에서 추출).
|
||||
/// 훅 실행 → 도구 실행 → 결과 처리 → 검증까지 단일 도구 라이프사이클.
|
||||
/// </summary>
|
||||
/// <returns>LoopAction: Continue=다음 도구, Break=루프 중단, Return=값 반환.</returns>
|
||||
private async Task<(ToolCallAction Action, string? ReturnValue)> ProcessSingleToolCallAsync(
|
||||
LlmService.ContentBlock call,
|
||||
List<ChatMessage> messages,
|
||||
AgentContext context,
|
||||
ToolExecutionState state,
|
||||
List<LlmService.ContentBlock> toolCalls,
|
||||
string activeTabSnapshot,
|
||||
int iteration,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var llm = _settings.Settings.Llm;
|
||||
var tool = _tools.Get(call.ToolName);
|
||||
if (tool == null)
|
||||
{
|
||||
var errResult = $"알 수 없는 도구: {call.ToolName}";
|
||||
EmitEvent(AgentEventType.Error, call.ToolName, errResult);
|
||||
messages.Add(LlmService.CreateToolResultMessage(call.ToolId, call.ToolName, errResult));
|
||||
return (ToolCallAction.Continue, null);
|
||||
}
|
||||
|
||||
// Task Decomposition: 단계 진행률 추적
|
||||
if (state.PlanSteps.Count > 0)
|
||||
{
|
||||
var summary = FormatToolCallSummary(call);
|
||||
var newStep = TaskDecomposer.EstimateCurrentStep(
|
||||
state.PlanSteps, call.ToolName, summary, state.CurrentStep);
|
||||
if (newStep != state.CurrentStep)
|
||||
{
|
||||
state.CurrentStep = newStep;
|
||||
EmitEvent(AgentEventType.StepStart, "", state.PlanSteps[state.CurrentStep],
|
||||
stepCurrent: state.CurrentStep + 1, stepTotal: state.PlanSteps.Count);
|
||||
}
|
||||
}
|
||||
|
||||
// 개발자 모드: 도구 호출 파라미터 상세 표시
|
||||
if (context.DevMode)
|
||||
{
|
||||
var paramJson = call.ToolInput?.ToString() ?? "{}";
|
||||
if (paramJson.Length > 500) paramJson = paramJson[..500] + "...";
|
||||
EmitEvent(AgentEventType.Thinking, call.ToolName,
|
||||
$"[DEV] 도구 호출: {call.ToolName}\n파라미터: {paramJson}");
|
||||
}
|
||||
|
||||
// 이벤트 로그 기록
|
||||
if (_eventLog != null)
|
||||
_ = _eventLog.AppendAsync(AgentEventLogType.ToolRequest,
|
||||
JsonSerializer.Serialize(new { toolName = call.ToolName, iteration }));
|
||||
|
||||
EmitEvent(AgentEventType.ToolCall, call.ToolName, FormatToolCallSummary(call));
|
||||
|
||||
// 개발자 모드: 스텝 바이 스텝 승인
|
||||
if (context.DevModeStepApproval && UserDecisionCallback != null)
|
||||
{
|
||||
var decision = await UserDecisionCallback(
|
||||
$"[DEV] 도구 '{call.ToolName}' 실행을 승인하시겠습니까?\n{FormatToolCallSummary(call)}",
|
||||
new List<string> { "승인", "건너뛰기", "중단" });
|
||||
if (decision == "중단")
|
||||
{
|
||||
EmitEvent(AgentEventType.Complete, "", "[DEV] 사용자가 실행을 중단했습니다");
|
||||
return (ToolCallAction.Return, "사용자가 개발자 모드에서 실행을 중단했습니다.");
|
||||
}
|
||||
if (decision == "건너뛰기")
|
||||
{
|
||||
messages.Add(LlmService.CreateToolResultMessage(
|
||||
call.ToolId, call.ToolName, "[SKIPPED by developer] 사용자가 이 도구 실행을 건너뛰었습니다."));
|
||||
return (ToolCallAction.Continue, null);
|
||||
}
|
||||
}
|
||||
|
||||
// 영향 범위 기반 의사결정 체크
|
||||
var decisionRequired = CheckDecisionRequired(call, context);
|
||||
if (decisionRequired != null && UserDecisionCallback != null)
|
||||
{
|
||||
var decision = await UserDecisionCallback(
|
||||
decisionRequired,
|
||||
new List<string> { "승인", "건너뛰기", "취소" });
|
||||
if (decision == "취소")
|
||||
{
|
||||
EmitEvent(AgentEventType.Complete, "", "사용자가 작업을 취소했습니다");
|
||||
return (ToolCallAction.Return, "사용자가 작업을 취소했습니다.");
|
||||
}
|
||||
if (decision == "건너뛰기")
|
||||
{
|
||||
messages.Add(LlmService.CreateToolResultMessage(
|
||||
call.ToolId, call.ToolName, "[SKIPPED] 사용자가 이 작업을 건너뛰었습니다."));
|
||||
return (ToolCallAction.Continue, null);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Pre-Hook 실행 ──
|
||||
await RunToolHooksAsync(llm, call, context, "pre", ct);
|
||||
|
||||
// ── 도구 실행 ──
|
||||
ToolResult result;
|
||||
var sw = Stopwatch.StartNew();
|
||||
try
|
||||
{
|
||||
var input = call.ToolInput ?? JsonDocument.Parse("{}").RootElement;
|
||||
result = await tool.ExecuteAsync(input, context, ct);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
EmitEvent(AgentEventType.Complete, "", "사용자가 작업을 취소했습니다.");
|
||||
return (ToolCallAction.Return, "사용자가 작업을 취소했습니다.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
result = ToolResult.Fail($"도구 실행 오류: {ex.Message}");
|
||||
}
|
||||
sw.Stop();
|
||||
|
||||
// ── Post-Hook 실행 ──
|
||||
await RunToolHooksAsync(llm, call, context, "post", ct, result);
|
||||
|
||||
// 개발자 모드: 도구 결과 상세 표시
|
||||
if (context.DevMode)
|
||||
{
|
||||
EmitEvent(AgentEventType.Thinking, call.ToolName,
|
||||
$"[DEV] 결과: {(result.Success ? "성공" : "실패")}\n{TruncateOutput(result.Output, 500)}");
|
||||
}
|
||||
|
||||
var tokenUsage = _llm.LastTokenUsage;
|
||||
EmitEvent(
|
||||
result.Success ? AgentEventType.ToolResult : AgentEventType.Error,
|
||||
call.ToolName,
|
||||
TruncateOutput(result.Output, 200),
|
||||
result.FilePath,
|
||||
elapsedMs: sw.ElapsedMilliseconds,
|
||||
inputTokens: tokenUsage?.PromptTokens ?? 0,
|
||||
outputTokens: tokenUsage?.CompletionTokens ?? 0,
|
||||
toolInput: call.ToolInput?.ToString(),
|
||||
iteration: iteration);
|
||||
|
||||
// 통계 수집
|
||||
if (result.Success) state.StatsSuccessCount++; else state.StatsFailCount++;
|
||||
state.StatsInputTokens += tokenUsage?.PromptTokens ?? 0;
|
||||
state.StatsOutputTokens += tokenUsage?.CompletionTokens ?? 0;
|
||||
if (!state.StatsUsedTools.Contains(call.ToolName))
|
||||
state.StatsUsedTools.Add(call.ToolName);
|
||||
|
||||
// 감사 로그 기록
|
||||
if (llm.EnableAuditLog)
|
||||
{
|
||||
AuditLogService.LogToolCall(
|
||||
_conversationId, activeTabSnapshot,
|
||||
call.ToolName,
|
||||
call.ToolInput.ToString() ?? "",
|
||||
TruncateOutput(result.Output, 500),
|
||||
result.FilePath, result.Success);
|
||||
}
|
||||
|
||||
// 이벤트 로그
|
||||
if (_eventLog != null)
|
||||
_ = _eventLog.AppendAsync(AgentEventLogType.ToolResult,
|
||||
JsonSerializer.Serialize(new
|
||||
{
|
||||
toolName = call.ToolName,
|
||||
success = result.Success,
|
||||
outputLength = result.Output?.Length ?? 0
|
||||
}));
|
||||
|
||||
state.TotalToolCalls++;
|
||||
|
||||
// 동적 반복 한도 확장
|
||||
if (state.TotalToolCalls > Defaults.ToolCallThresholdForExpansion && state.MaxIterations < state.BaseMax * 2)
|
||||
state.MaxIterations = Math.Min(state.BaseMax * 2, 50);
|
||||
|
||||
// 테스트-수정 루프 예산 확장
|
||||
if (call.ToolName == "test_loop" && (result.Output?.Contains("[AUTO_FIX:") ?? false))
|
||||
{
|
||||
var testFixMax = llm.MaxTestFixIterations > 0 ? llm.MaxTestFixIterations : Defaults.MaxTestFixIterations;
|
||||
var testFixBudget = state.BaseMax + testFixMax * 3;
|
||||
if (state.MaxIterations < testFixBudget)
|
||||
state.MaxIterations = Math.Min(testFixBudget, 60);
|
||||
}
|
||||
|
||||
// UI 스레드가 이벤트를 렌더링할 시간 확보
|
||||
await Task.Delay(80, ct);
|
||||
|
||||
// Self-Reflection: 도구 실패 시 반성 프롬프트 추가
|
||||
if (!result.Success)
|
||||
{
|
||||
state.ConsecutiveErrors++;
|
||||
if (state.ConsecutiveErrors <= state.MaxRetry)
|
||||
{
|
||||
var reflectionMsg = $"[Tool '{call.ToolName}' failed: {TruncateOutput(result.Output ?? "", 500)}]\n" +
|
||||
$"Analyze why this failed. Consider: wrong parameters, wrong file path, missing prerequisites. " +
|
||||
$"Try a different approach. (Error {state.ConsecutiveErrors}/{state.MaxRetry})";
|
||||
messages.Add(LlmService.CreateToolResultMessage(call.ToolId, call.ToolName, reflectionMsg));
|
||||
EmitEvent(AgentEventType.Thinking, "", $"Self-Reflection: 실패 분석 후 재시도 ({state.ConsecutiveErrors}/{state.MaxRetry})");
|
||||
}
|
||||
else
|
||||
{
|
||||
messages.Add(LlmService.CreateToolResultMessage(
|
||||
call.ToolId, call.ToolName,
|
||||
$"[FAILED after {state.MaxRetry} retries] {TruncateOutput(result.Output ?? "", 500)}\n" +
|
||||
"Stop retrying this tool. Explain the error to the user and suggest alternative approaches."));
|
||||
|
||||
// 에이전트 메모리에 실패 원인 저장
|
||||
try
|
||||
{
|
||||
var memSvc = CurrentApp?.MemoryService;
|
||||
if (memSvc != null && (_settings.Settings.Llm.EnableAgentMemory))
|
||||
{
|
||||
memSvc.Add("correction",
|
||||
$"도구 '{call.ToolName}' 반복 실패: {TruncateOutput(result.Output ?? "", Defaults.LogSummaryMaxLength)}",
|
||||
$"conv:{_conversationId}", context.WorkFolder);
|
||||
}
|
||||
}
|
||||
catch (Exception ex) { LogService.Warn($"[AgentLoop] 에이전트 메모리 저장 실패: {ex.Message}"); }
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
state.ConsecutiveErrors = 0;
|
||||
|
||||
// ToolResultSizer 적용
|
||||
var sizedResult = ToolResultSizer.Apply(result.Output ?? "", call.ToolName);
|
||||
messages.Add(LlmService.CreateToolResultMessage(call.ToolId, call.ToolName, sizedResult.Output));
|
||||
|
||||
// document_plan 호출 플래그 + 메타데이터 저장
|
||||
if (call.ToolName == "document_plan" && result.Success)
|
||||
{
|
||||
state.DocumentPlanCalled = true;
|
||||
try
|
||||
{
|
||||
var planJson = JsonDocument.Parse(result.Output ?? "{}");
|
||||
if (planJson.RootElement.TryGetProperty("path", out var p))
|
||||
state.DocumentPlanPath = p.GetString();
|
||||
if (planJson.RootElement.TryGetProperty("title", out var t))
|
||||
state.DocumentPlanTitle = t.GetString();
|
||||
if (planJson.RootElement.TryGetProperty("body", out var b))
|
||||
state.DocumentPlanScaffold = b.GetString();
|
||||
}
|
||||
catch (Exception) { /* document_plan JSON 파싱 실패 — 선택적 메타데이터이므로 무시 */ }
|
||||
|
||||
// html_create 즉시 호출 유도
|
||||
var toolHint = (result.Output?.Contains("document_assemble") ?? false) ? "document_assemble" :
|
||||
(result.Output?.Contains("file_write") ?? false) ? "file_write" : "html_create";
|
||||
messages.Add(new ChatMessage { Role = "user",
|
||||
Content = $"document_plan이 완료되었습니다. " +
|
||||
$"위 결과의 body/sections의 [내용...] 부분을 실제 상세 내용으로 모두 채워서 " +
|
||||
$"{toolHint} 도구를 지금 즉시 호출하세요. " +
|
||||
$"각 섹션마다 반드시 충분한 내용을 작성하고, 설명 없이 도구를 바로 호출하세요." });
|
||||
EmitEvent(AgentEventType.Thinking, "", $"문서 개요 완성 — {toolHint} 호출 중...");
|
||||
}
|
||||
|
||||
// 단일 문서 생성 도구 성공 → 즉시 루프 종료
|
||||
if (result.Success && IsTerminalDocumentTool(call.ToolName) && toolCalls.Count == 1)
|
||||
{
|
||||
var shouldVerify2 = activeTabSnapshot == "Code"
|
||||
? llm.Code.EnableCodeVerification && IsCodeVerificationTarget(call.ToolName)
|
||||
: llm.EnableCoworkVerification && IsDocumentCreationTool(call.ToolName);
|
||||
if (shouldVerify2)
|
||||
{
|
||||
await RunPostToolVerificationAsync(messages, call.ToolName, result, context, ct);
|
||||
}
|
||||
EmitEvent(AgentEventType.Complete, "", "에이전트 작업 완료");
|
||||
return (ToolCallAction.Return, result.Output ?? "");
|
||||
}
|
||||
|
||||
// Post-Tool Verification: 탭별 검증 강제
|
||||
var shouldVerify = activeTabSnapshot == "Code"
|
||||
? llm.Code.EnableCodeVerification && IsCodeVerificationTarget(call.ToolName)
|
||||
: llm.EnableCoworkVerification && IsDocumentCreationTool(call.ToolName);
|
||||
if (shouldVerify && result.Success)
|
||||
{
|
||||
await RunPostToolVerificationAsync(messages, call.ToolName, result, context, ct);
|
||||
}
|
||||
}
|
||||
|
||||
return (ToolCallAction.Continue, null);
|
||||
}
|
||||
|
||||
/// <summary>도구 호출 처리 후 루프 제어 액션.</summary>
|
||||
private enum ToolCallAction { Continue, Break, Return }
|
||||
|
||||
/// <summary>Phase 33-B: 도구 훅(Pre/Post) 실행 헬퍼.</summary>
|
||||
private async Task RunToolHooksAsync(
|
||||
Models.LlmSettings llm, LlmService.ContentBlock call,
|
||||
AgentContext context, string phase, CancellationToken ct,
|
||||
ToolResult? result = null)
|
||||
{
|
||||
if (!llm.EnableToolHooks || llm.AgentHooks.Count == 0) return;
|
||||
try
|
||||
{
|
||||
var hookResults = await AgentHookRunner.RunAsync(
|
||||
llm.AgentHooks, call.ToolName, phase,
|
||||
toolInput: call.ToolInput.ToString(),
|
||||
toolOutput: result != null ? TruncateOutput(result.Output, 2048) : null,
|
||||
success: result?.Success ?? false,
|
||||
workFolder: context.WorkFolder,
|
||||
timeoutMs: llm.ToolHookTimeoutMs,
|
||||
ct: ct);
|
||||
foreach (var pr in hookResults.Where(r => !r.Success))
|
||||
EmitEvent(AgentEventType.Error, call.ToolName, $"[Hook:{pr.HookName}] {pr.Output}");
|
||||
}
|
||||
catch (Exception ex) { LogService.Warn($"[AgentLoop] {phase}ToolUse 훅 실패: {ex.Message}"); }
|
||||
}
|
||||
|
||||
/// <summary>DelegateAgentTool에서 호출하는 서브에이전트 실행기.</summary>
|
||||
private async Task<string> RunSubAgentAsync(string agentType, string task, string workFolder, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
var systemPrompt = $"당신은 전문 {agentType} 에이전트입니다. 주어진 작업을 수행하세요.";
|
||||
var msgs = new List<ChatMessage>
|
||||
{
|
||||
new() { Role = "system", Content = systemPrompt },
|
||||
new() { Role = "user", Content = task }
|
||||
};
|
||||
|
||||
// 서브에이전트는 간단한 텍스트 응답으로 처리 (도구 호출 없이)
|
||||
var response = await _llm.SendAsync(msgs, ct);
|
||||
return response;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return $"서브에이전트 실행 오류: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>읽기 전용 도구들을 병렬 실행합니다.</summary>
|
||||
private async Task ExecuteToolsInParallelAsync(
|
||||
List<LlmService.ContentBlock> calls,
|
||||
List<ChatMessage> messages,
|
||||
AgentContext context,
|
||||
List<string> planSteps,
|
||||
ParallelState state,
|
||||
int baseMax, int maxRetry,
|
||||
Models.LlmSettings llm,
|
||||
int iteration,
|
||||
CancellationToken ct,
|
||||
List<string> statsUsedTools)
|
||||
{
|
||||
EmitEvent(AgentEventType.Thinking, "", $"읽기 전용 도구 {calls.Count}개를 병렬 실행 중...");
|
||||
|
||||
var tasks = calls.Select(async call =>
|
||||
{
|
||||
var tool = _tools.Get(call.ToolName);
|
||||
if (tool == null)
|
||||
return (call, ToolResult.Fail($"알 수 없는 도구: {call.ToolName}"), 0L);
|
||||
|
||||
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||
try
|
||||
{
|
||||
var input = call.ToolInput ?? JsonDocument.Parse("{}").RootElement;
|
||||
var result = await tool.ExecuteAsync(input, context, ct);
|
||||
sw.Stop();
|
||||
return (call, result, sw.ElapsedMilliseconds);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
sw.Stop();
|
||||
return (call, ToolResult.Fail($"도구 실행 오류: {ex.Message}"), sw.ElapsedMilliseconds);
|
||||
}
|
||||
}).ToList();
|
||||
|
||||
var results = await Task.WhenAll(tasks);
|
||||
|
||||
// 결과를 순서대로 메시지에 추가
|
||||
foreach (var (call, result, elapsed) in results)
|
||||
{
|
||||
EmitEvent(
|
||||
result.Success ? AgentEventType.ToolResult : AgentEventType.Error,
|
||||
call.ToolName,
|
||||
TruncateOutput(result.Output, 200),
|
||||
result.FilePath,
|
||||
elapsedMs: elapsed,
|
||||
iteration: iteration);
|
||||
|
||||
if (result.Success) state.StatsSuccessCount++; else state.StatsFailCount++;
|
||||
if (!statsUsedTools.Contains(call.ToolName))
|
||||
statsUsedTools.Add(call.ToolName);
|
||||
|
||||
state.TotalToolCalls++;
|
||||
messages.Add(LlmService.CreateToolResultMessage(
|
||||
call.ToolId, call.ToolName, TruncateOutput(result.Output, Defaults.ToolResultTruncateLength)));
|
||||
|
||||
if (!result.Success)
|
||||
{
|
||||
state.ConsecutiveErrors++;
|
||||
if (state.ConsecutiveErrors > maxRetry)
|
||||
{
|
||||
messages.Add(LlmService.CreateToolResultMessage(
|
||||
call.ToolId, call.ToolName,
|
||||
$"[FAILED after retries] {TruncateOutput(result.Output, 500)}"));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
state.ConsecutiveErrors = 0;
|
||||
}
|
||||
|
||||
// 감사 로그
|
||||
if (llm.EnableAuditLog)
|
||||
{
|
||||
AuditLogService.LogToolCall(
|
||||
_conversationId, context.ActiveTab ?? "",
|
||||
call.ToolName,
|
||||
call.ToolInput?.ToString() ?? "",
|
||||
TruncateOutput(result.Output, 500),
|
||||
result.FilePath, result.Success);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user