Initial commit to new repository
This commit is contained in:
246
src/AxCopilot/Services/Agent/AgentLoopParallelExecution.cs
Normal file
246
src/AxCopilot/Services/Agent/AgentLoopParallelExecution.cs
Normal file
@@ -0,0 +1,246 @@
|
||||
using System.Text.Json;
|
||||
using AxCopilot.Models;
|
||||
using AxCopilot.Services;
|
||||
|
||||
namespace AxCopilot.Services.Agent;
|
||||
|
||||
public partial class AgentLoopService
|
||||
{
|
||||
private static int GetMaxParallelToolConcurrency()
|
||||
{
|
||||
var raw = Environment.GetEnvironmentVariable("AXCOPILOT_MAX_PARALLEL_TOOLS");
|
||||
if (int.TryParse(raw, out var parsed) && parsed > 0)
|
||||
return Math.Min(parsed, 12);
|
||||
|
||||
return 4;
|
||||
}
|
||||
|
||||
// 읽기 전용 도구 (파일 상태를 변경하지 않음)
|
||||
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>();
|
||||
var collectParallelPrefix = true;
|
||||
|
||||
foreach (var call in calls)
|
||||
{
|
||||
var requestedToolName = call.ToolName ?? "";
|
||||
var normalizedToolName = NormalizeAliasToken(requestedToolName);
|
||||
var classificationToolName = ToolAliasMap.TryGetValue(normalizedToolName, out var mappedToolName)
|
||||
? mappedToolName
|
||||
: requestedToolName;
|
||||
|
||||
if (collectParallelPrefix && ReadOnlyTools.Contains(classificationToolName))
|
||||
parallel.Add(call);
|
||||
else
|
||||
{
|
||||
collectParallelPrefix = false;
|
||||
sequential.Add(call);
|
||||
}
|
||||
}
|
||||
|
||||
// 읽기 전용 도구가 1개뿐이면 병렬화 의미 없음
|
||||
if (parallel.Count <= 1)
|
||||
{
|
||||
sequential.InsertRange(0, parallel);
|
||||
parallel.Clear();
|
||||
}
|
||||
|
||||
return (parallel, sequential);
|
||||
}
|
||||
|
||||
/// <summary>병렬 실행용 가변 상태.</summary>
|
||||
private class ParallelState
|
||||
{
|
||||
public int CurrentStep;
|
||||
public int TotalToolCalls;
|
||||
public int MaxIterations;
|
||||
public int ConsecutiveReadOnlySuccessTools;
|
||||
public int ConsecutiveNonMutatingSuccessTools;
|
||||
public int ConsecutiveErrors;
|
||||
public int StatsSuccessCount;
|
||||
public int StatsFailCount;
|
||||
public int StatsInputTokens;
|
||||
public int StatsOutputTokens;
|
||||
public int StatsRepeatedFailureBlocks;
|
||||
public int StatsRecoveredAfterFailure;
|
||||
public bool RecoveryPendingAfterFailure;
|
||||
public string? LastFailedToolSignature;
|
||||
public int RepeatedFailedToolSignatureCount;
|
||||
}
|
||||
|
||||
/// <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 activeToolNames = _tools.GetActiveTools(llm.DisabledTools)
|
||||
.Select(t => t.Name)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.OrderBy(x => x, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
|
||||
var executableCalls = new List<LlmService.ContentBlock>();
|
||||
foreach (var call in calls)
|
||||
{
|
||||
var resolvedToolName = ResolveRequestedToolName(call.ToolName, activeToolNames);
|
||||
var effectiveCall = string.Equals(call.ToolName, resolvedToolName, StringComparison.OrdinalIgnoreCase)
|
||||
? call
|
||||
: new LlmService.ContentBlock
|
||||
{
|
||||
Type = call.Type,
|
||||
Text = call.Text,
|
||||
ToolName = resolvedToolName,
|
||||
ToolId = call.ToolId,
|
||||
ToolInput = call.ToolInput,
|
||||
};
|
||||
var signature = BuildToolCallSignature(effectiveCall);
|
||||
if (ShouldBlockRepeatedFailedCall(
|
||||
signature,
|
||||
state.LastFailedToolSignature,
|
||||
state.RepeatedFailedToolSignatureCount,
|
||||
maxRetry))
|
||||
{
|
||||
messages.Add(LlmService.CreateToolResultMessage(
|
||||
call.ToolId,
|
||||
call.ToolName,
|
||||
BuildRepeatedFailureGuardMessage(call.ToolName, state.RepeatedFailedToolSignatureCount, maxRetry)));
|
||||
EmitEvent(
|
||||
AgentEventType.Thinking,
|
||||
call.ToolName,
|
||||
$"병렬 배치에서도 동일 호출 반복 실패를 감지해 실행을 건너뜁니다 ({state.RepeatedFailedToolSignatureCount}/{maxRetry})");
|
||||
state.StatsRepeatedFailureBlocks++;
|
||||
continue;
|
||||
}
|
||||
|
||||
executableCalls.Add(effectiveCall);
|
||||
}
|
||||
|
||||
if (executableCalls.Count == 0)
|
||||
return;
|
||||
|
||||
var maxConcurrency = GetMaxParallelToolConcurrency();
|
||||
using var gate = new SemaphoreSlim(maxConcurrency, maxConcurrency);
|
||||
var tasks = executableCalls.Select(async call =>
|
||||
{
|
||||
await gate.WaitAsync(ct).ConfigureAwait(false);
|
||||
var tool = _tools.Get(call.ToolName);
|
||||
try
|
||||
{
|
||||
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 ExecuteToolWithTimeoutAsync(tool, call.ToolName, input, context, ct);
|
||||
sw.Stop();
|
||||
return (call, result, sw.ElapsedMilliseconds);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
sw.Stop();
|
||||
return (call, ToolResult.Fail($"도구 실행 오류: {ex.Message}"), sw.ElapsedMilliseconds);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
gate.Release();
|
||||
}
|
||||
}).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++;
|
||||
state.ConsecutiveReadOnlySuccessTools = UpdateConsecutiveReadOnlySuccessTools(
|
||||
state.ConsecutiveReadOnlySuccessTools,
|
||||
call.ToolName,
|
||||
result.Success);
|
||||
state.ConsecutiveNonMutatingSuccessTools = UpdateConsecutiveNonMutatingSuccessTools(
|
||||
state.ConsecutiveNonMutatingSuccessTools,
|
||||
call.ToolName,
|
||||
result.Success);
|
||||
if (!statsUsedTools.Contains(call.ToolName))
|
||||
statsUsedTools.Add(call.ToolName);
|
||||
|
||||
state.TotalToolCalls++;
|
||||
messages.Add(LlmService.CreateToolResultMessage(
|
||||
call.ToolId, call.ToolName, TruncateOutput(result.Output, 4000)));
|
||||
|
||||
if (!result.Success)
|
||||
{
|
||||
var signature = BuildToolCallSignature(call);
|
||||
if (string.Equals(state.LastFailedToolSignature, signature, StringComparison.Ordinal))
|
||||
state.RepeatedFailedToolSignatureCount++;
|
||||
else
|
||||
{
|
||||
state.LastFailedToolSignature = signature;
|
||||
state.RepeatedFailedToolSignatureCount = 1;
|
||||
}
|
||||
|
||||
state.ConsecutiveErrors++;
|
||||
state.RecoveryPendingAfterFailure = true;
|
||||
if (state.ConsecutiveErrors > maxRetry)
|
||||
{
|
||||
messages.Add(LlmService.CreateToolResultMessage(
|
||||
call.ToolId, call.ToolName,
|
||||
$"[FAILED after retries] {TruncateOutput(result.Output, 500)}"));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
state.ConsecutiveErrors = 0;
|
||||
if (state.RecoveryPendingAfterFailure)
|
||||
{
|
||||
state.StatsRecoveredAfterFailure++;
|
||||
state.RecoveryPendingAfterFailure = false;
|
||||
}
|
||||
state.LastFailedToolSignature = null;
|
||||
state.RepeatedFailedToolSignatureCount = 0;
|
||||
}
|
||||
|
||||
// 감사 로그
|
||||
if (llm.EnableAuditLog)
|
||||
{
|
||||
AuditLogService.LogToolCall(
|
||||
_conversationId, ActiveTab ?? "",
|
||||
call.ToolName,
|
||||
call.ToolInput?.ToString() ?? "",
|
||||
TruncateOutput(result.Output, 500),
|
||||
result.FilePath, result.Success);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user