IBM vLLM 도구 호출 스트리밍과 모델 프로파일 기반 실행 정책 강화
Some checks failed
Release Gate / gate (push) Has been cancelled
Some checks failed
Release Gate / gate (push) Has been cancelled
- IBM 배포형 도구 호출 바디에 프로파일 기반 tool temperature를 적용하고 tool_call_strict 프로파일에서 더 직접적인 tool-only 지시를 추가함 - IBM 경로가 tool_choice를 거부할 때 tool_choice만 제거한 대체 강제 재시도 경로를 추가함 - OpenAI/vLLM tool-use 응답을 SSE로 수신하고 delta.tool_calls를 부분 조립해 도구 호출을 더 빨리 감지하도록 변경함 - read-only 도구 조기 실행과 결과 재사용 경로를 도입해 Cowork/Code 도구 착수 속도를 개선함 - README와 DEVELOPMENT 문서를 2026-04-08 11: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:
@@ -143,10 +143,15 @@ public partial class AgentLoopService
|
||||
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);
|
||||
// gate.WaitAsync를 try 안에서 호출: ct 취소 시 WaitAsync가 OperationCanceledException을
|
||||
// 던져도 Release()가 잘못 호출되지 않도록 보호 (SemaphoreFullException 방지)
|
||||
var acquired = false;
|
||||
try
|
||||
{
|
||||
await gate.WaitAsync(ct).ConfigureAwait(false);
|
||||
acquired = true;
|
||||
|
||||
var tool = _tools.Get(call.ToolName);
|
||||
if (tool == null)
|
||||
return (call, ToolResult.Fail($"알 수 없는 도구: {call.ToolName}"), 0L);
|
||||
|
||||
@@ -154,19 +159,32 @@ public partial class AgentLoopService
|
||||
try
|
||||
{
|
||||
var input = call.ToolInput ?? JsonDocument.Parse("{}").RootElement;
|
||||
var result = await ExecuteToolWithTimeoutAsync(tool, call.ToolName, input, context, messages, ct);
|
||||
// 병렬 실행 중에는 messages를 null로 전달:
|
||||
// 훅이 messages.Add()를 동시 호출하면 List<T> race condition 발생.
|
||||
// 읽기 전용 도구이므로 hook 추가 컨텍스트는 이 배치 후 순차로 처리됨.
|
||||
var result = await ExecuteToolWithTimeoutAsync(tool, call.ToolName, input, context, null, ct);
|
||||
sw.Stop();
|
||||
return (call, result, sw.ElapsedMilliseconds);
|
||||
}
|
||||
catch (OperationCanceledException) when (ct.IsCancellationRequested)
|
||||
{
|
||||
sw.Stop();
|
||||
return (call, ToolResult.Fail($"도구 실행이 취소되었습니다: {call.ToolName}"), sw.ElapsedMilliseconds);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
sw.Stop();
|
||||
return (call, ToolResult.Fail($"도구 실행 오류: {ex.Message}"), sw.ElapsedMilliseconds);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) when (ct.IsCancellationRequested)
|
||||
{
|
||||
// WaitAsync 도중 취소됨 — 세마포어 미취득 상태이므로 Release 하지 않음
|
||||
return (call, ToolResult.Fail($"도구 실행 대기 중 취소됨: {call.ToolName}"), 0L);
|
||||
}
|
||||
finally
|
||||
{
|
||||
gate.Release();
|
||||
if (acquired) gate.Release();
|
||||
}
|
||||
}).ToList();
|
||||
|
||||
@@ -241,6 +259,11 @@ public partial class AgentLoopService
|
||||
TruncateOutput(result.Output, 500),
|
||||
result.FilePath, result.Success);
|
||||
}
|
||||
|
||||
// 워크플로우 상세 로그: 병렬 도구 실행 결과
|
||||
WorkflowLogService.LogToolResult(_conversationId, _currentRunId, 0,
|
||||
call.ToolName, TruncateOutput(result.Output, 2000),
|
||||
result.Success, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,6 +150,10 @@ public partial class AgentLoopService
|
||||
|
||||
// 사용자 원본 요청 캡처 (문서 생성 폴백 판단용)
|
||||
var userQuery = messages.LastOrDefault(m => m.Role == "user")?.Content ?? "";
|
||||
|
||||
// 워크플로우 상세 로그: 에이전트 루프 시작
|
||||
WorkflowLogService.LogAgentLifecycle(_conversationId, _currentRunId, "start",
|
||||
userPrompt: userQuery);
|
||||
var consecutiveErrors = 0; // Self-Reflection: 연속 오류 카운터
|
||||
var totalToolCalls = 0; // 복잡도 추정용
|
||||
string? lastFailedToolSignature = null;
|
||||
@@ -510,6 +514,7 @@ public partial class AgentLoopService
|
||||
|
||||
// LLM에 도구 정의와 함께 요청
|
||||
List<LlmService.ContentBlock> blocks;
|
||||
var llmCallSw = Stopwatch.StartNew();
|
||||
try
|
||||
{
|
||||
var activeTools = GetRuntimeActiveTools(llm.DisabledTools, runtimeOverrides);
|
||||
@@ -521,14 +526,43 @@ public partial class AgentLoopService
|
||||
// totalToolCalls == 0: 아직 한 번도 도구를 안 불렀으면 tool_choice:"required" 강제
|
||||
// → chatty 모델(Qwen 등)이 텍스트 설명만 하고 도구를 안 부르는 현상 방지
|
||||
var forceFirst = totalToolCalls == 0 && executionPolicy.ForceInitialToolCall;
|
||||
|
||||
// IBM/Qwen 등 chatty 모델 대응: 첫 번째 호출 직전 마지막 user 메시지로 도구 호출 강제 reminder 주입.
|
||||
// recovery 메시지가 이미 추가된 경우(NoToolCallLoopRetry > 0)에는 중복 주입하지 않음.
|
||||
// 임시 메시지이므로 실제 messages 목록은 수정하지 않고, 별도 sendMessages로 전달.
|
||||
List<ChatMessage> sendMessages = messages;
|
||||
if (forceFirst
|
||||
&& executionPolicy.InjectPreCallToolReminder
|
||||
&& runState.NoToolCallLoopRetry == 0)
|
||||
{
|
||||
sendMessages = [.. messages, new ChatMessage
|
||||
{
|
||||
Role = "user",
|
||||
Content = "[TOOL_REQUIRED] 지금 즉시 도구를 1개 이상 호출하세요. 텍스트만 반환하면 거부됩니다. " +
|
||||
"Call at least one tool RIGHT NOW. Text-only response is rejected."
|
||||
}];
|
||||
}
|
||||
|
||||
// 워크플로우 상세 로그: LLM 요청
|
||||
llmCallSw.Restart();
|
||||
var (_, currentModel) = _llm.GetCurrentModelInfo();
|
||||
WorkflowLogService.LogLlmRequest(_conversationId, _currentRunId, iteration,
|
||||
currentModel, sendMessages.Count, activeTools.Count, forceFirst);
|
||||
|
||||
blocks = await SendWithToolsWithRecoveryAsync(
|
||||
messages,
|
||||
sendMessages,
|
||||
activeTools,
|
||||
ct,
|
||||
$"메인 루프 {iteration}",
|
||||
runState,
|
||||
forceToolCall: forceFirst);
|
||||
forceToolCall: forceFirst,
|
||||
prefetchToolCallAsync: block => TryPrefetchReadOnlyToolAsync(
|
||||
block,
|
||||
activeTools,
|
||||
context,
|
||||
ct));
|
||||
runState.ContextRecoveryAttempts = 0;
|
||||
llmCallSw.Stop();
|
||||
runState.TransientLlmErrorRetries = 0;
|
||||
NotifyPostCompactionTurnIfNeeded(runState);
|
||||
}
|
||||
@@ -646,6 +680,13 @@ public partial class AgentLoopService
|
||||
var textResponse = string.Join("\n", textParts);
|
||||
consecutiveNoToolResponses = toolCalls.Count == 0 ? consecutiveNoToolResponses + 1 : 0;
|
||||
|
||||
// 워크플로우 상세 로그: LLM 응답
|
||||
WorkflowLogService.LogLlmResponse(_conversationId, _currentRunId, iteration,
|
||||
textResponse, toolCalls.Count,
|
||||
_llm.LastTokenUsage?.PromptTokens ?? 0,
|
||||
_llm.LastTokenUsage?.CompletionTokens ?? 0,
|
||||
llmCallSw.ElapsedMilliseconds);
|
||||
|
||||
// Task Decomposition: 첫 번째 텍스트 응답에서 계획 단계 추출
|
||||
if (!planExtracted && !string.IsNullOrEmpty(textResponse))
|
||||
{
|
||||
@@ -1126,22 +1167,27 @@ public partial class AgentLoopService
|
||||
repeatedUnknownToolCount = 0;
|
||||
lastDisallowedToolName = null;
|
||||
repeatedDisallowedToolCount = 0;
|
||||
var effectiveCall = string.Equals(call.ToolName, resolvedToolName, StringComparison.OrdinalIgnoreCase)
|
||||
var effectiveToolName = !string.IsNullOrWhiteSpace(call.ResolvedToolName)
|
||||
? call.ResolvedToolName
|
||||
: resolvedToolName;
|
||||
var effectiveCall = string.Equals(call.ToolName, effectiveToolName, StringComparison.OrdinalIgnoreCase)
|
||||
? call
|
||||
: new LlmService.ContentBlock
|
||||
{
|
||||
Type = call.Type,
|
||||
Text = call.Text,
|
||||
ToolName = resolvedToolName,
|
||||
ToolName = effectiveToolName,
|
||||
ToolId = call.ToolId,
|
||||
ToolInput = call.ToolInput,
|
||||
ResolvedToolName = effectiveToolName,
|
||||
PrefetchedExecutionTask = call.PrefetchedExecutionTask,
|
||||
};
|
||||
if (!string.Equals(call.ToolName, resolvedToolName, StringComparison.OrdinalIgnoreCase))
|
||||
if (!string.Equals(call.ToolName, effectiveToolName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
EmitEvent(
|
||||
AgentEventType.Thinking,
|
||||
resolvedToolName,
|
||||
$"도구명 정규화 적용: '{call.ToolName}' → '{resolvedToolName}'");
|
||||
effectiveToolName,
|
||||
$"도구명 정규화 적용: '{call.ToolName}' → '{effectiveToolName}'");
|
||||
}
|
||||
|
||||
var toolCallSignature = BuildToolCallSignature(effectiveCall);
|
||||
@@ -1279,11 +1325,37 @@ public partial class AgentLoopService
|
||||
}
|
||||
|
||||
ToolResult result;
|
||||
long elapsedMs;
|
||||
var sw = Stopwatch.StartNew();
|
||||
try
|
||||
{
|
||||
var input = effectiveCall.ToolInput ?? JsonDocument.Parse("{}").RootElement;
|
||||
result = await ExecuteToolWithTimeoutAsync(tool, effectiveCall.ToolName, input, context, messages, ct);
|
||||
if (effectiveCall.PrefetchedExecutionTask != null)
|
||||
{
|
||||
var prefetched = await effectiveCall.PrefetchedExecutionTask.ConfigureAwait(false);
|
||||
if (prefetched != null)
|
||||
{
|
||||
result = prefetched.Result;
|
||||
elapsedMs = prefetched.ElapsedMilliseconds;
|
||||
EmitEvent(
|
||||
AgentEventType.Thinking,
|
||||
effectiveCall.ToolName,
|
||||
$"조기 실행 결과 재사용: {effectiveCall.ToolName}");
|
||||
}
|
||||
else
|
||||
{
|
||||
var input = effectiveCall.ToolInput ?? JsonDocument.Parse("{}").RootElement;
|
||||
result = await ExecuteToolWithTimeoutAsync(tool, effectiveCall.ToolName, input, context, messages, ct);
|
||||
sw.Stop();
|
||||
elapsedMs = sw.ElapsedMilliseconds;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var input = effectiveCall.ToolInput ?? JsonDocument.Parse("{}").RootElement;
|
||||
result = await ExecuteToolWithTimeoutAsync(tool, effectiveCall.ToolName, input, context, messages, ct);
|
||||
sw.Stop();
|
||||
elapsedMs = sw.ElapsedMilliseconds;
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
@@ -1300,8 +1372,9 @@ public partial class AgentLoopService
|
||||
catch (Exception ex)
|
||||
{
|
||||
result = ToolResult.Fail($"도구 실행 오류: {ex.Message}");
|
||||
sw.Stop();
|
||||
elapsedMs = sw.ElapsedMilliseconds;
|
||||
}
|
||||
sw.Stop();
|
||||
|
||||
// ── Post-Hook 실행 ──
|
||||
if (llm.EnableToolHooks && runtimeHooks.Count > 0)
|
||||
@@ -1340,7 +1413,7 @@ public partial class AgentLoopService
|
||||
effectiveCall.ToolName,
|
||||
TruncateOutput(result.Output, 200),
|
||||
result.FilePath,
|
||||
elapsedMs: sw.ElapsedMilliseconds,
|
||||
elapsedMs: elapsedMs,
|
||||
inputTokens: tokenUsage?.PromptTokens ?? 0,
|
||||
outputTokens: tokenUsage?.CompletionTokens ?? 0,
|
||||
toolInput: effectiveCall.ToolInput?.ToString(),
|
||||
@@ -1533,6 +1606,10 @@ public partial class AgentLoopService
|
||||
if (runtimeOverrideApplied)
|
||||
_llm.PopInferenceOverride();
|
||||
|
||||
// 워크플로우 상세 로그: 에이전트 루프 종료
|
||||
WorkflowLogService.LogAgentLifecycle(_conversationId, _currentRunId, "end",
|
||||
summary: $"iterations={iteration}, tools={totalToolCalls}, success={statsSuccessCount}, fail={statsFailCount}");
|
||||
|
||||
IsRunning = false;
|
||||
_currentRunId = "";
|
||||
_runPendingPostCompactionTurn = false;
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
using AxCopilot.Services;
|
||||
using System.Text.Json;
|
||||
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace AxCopilot.Services.Agent;
|
||||
|
||||
public partial class AgentLoopService
|
||||
@@ -375,7 +377,7 @@ public partial class AgentLoopService
|
||||
return sawDiff;
|
||||
}
|
||||
|
||||
// 蹂寃??꾧뎄 ?먯껜媛 ?놁쑝硫?diff 寃뚯씠?몃? ?붽뎄?섏? ?딆쓬
|
||||
// 蹂寃??꾧뎄 ?먯껜媛 ?놁쑝硫?diff 寃뚯씠???꾧뎄?몄? ?뺤씤
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -395,7 +397,7 @@ public partial class AgentLoopService
|
||||
"deletions",
|
||||
"file changed",
|
||||
"異붽?",
|
||||
"??젣");
|
||||
"?낅뜲?댄듃");
|
||||
}
|
||||
|
||||
private bool TryApplyDocumentArtifactGateTransition(
|
||||
@@ -413,18 +415,18 @@ public partial class AgentLoopService
|
||||
if (!string.IsNullOrEmpty(textResponse))
|
||||
messages.Add(new ChatMessage { Role = "assistant", Content = textResponse });
|
||||
|
||||
var targetHint = string.IsNullOrWhiteSpace(suggestedPath) ? "?붿껌???곗텧臾?臾몄꽌 ?뚯씪" : $"'{suggestedPath}'";
|
||||
var targetHint = string.IsNullOrWhiteSpace(suggestedPath) ? "요청한 출력 문서 파일" : $"'{suggestedPath}'";
|
||||
messages.Add(new ChatMessage
|
||||
{
|
||||
Role = "user",
|
||||
Content = "[System:DocumentArtifactGate] 寃곌낵 ?ㅻ챸留뚯쑝濡?醫낅즺?????놁뒿?덈떎. " +
|
||||
$"吏湲?利됱떆 {targetHint}???ㅼ젣 ?뚯씪濡??앹꽦?섏꽭?? " +
|
||||
"html_create/markdown_create/document_assemble/file_write 以??곸젅???꾧뎄瑜?諛섎뱶???몄텧?섏꽭??"
|
||||
Content = "[System:DocumentArtifactGate] 결과 파일이 누락되었습니다. " +
|
||||
$"즉시 {targetHint} 파일을 실제로 생성하세요. " +
|
||||
"html_create/markdown_create/document_assemble/file_write 중 적절한 도구를 호출하세요."
|
||||
});
|
||||
EmitEvent(
|
||||
AgentEventType.Thinking,
|
||||
"",
|
||||
$"臾몄꽌 ?묒뾽 ?곗텧臾??뚯씪???놁뼱 ?꾧뎄 ?ㅽ뻾???ъ슂泥?빀?덈떎 ({runState.DocumentArtifactGateRetry}/2)");
|
||||
$"문서 생성 결과 파일이 없어 재생성이 필요합니다. ({runState.DocumentArtifactGateRetry}/2)");
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -689,7 +691,7 @@ public partial class AgentLoopService
|
||||
EmitEvent(
|
||||
AgentEventType.Thinking,
|
||||
call.ToolName,
|
||||
$"臾댁쓽誘명븳 ?쎄린 ?꾧뎄 諛섎났 猷⑦봽瑜?媛먯????ㅻⅨ ?꾨왂?쇰줈 ?꾪솚?⑸땲??({repeatedSameSignatureCount}??");
|
||||
$"臾댁쓽誘명븳 ?쎄린 ?꾧뎄 諛섎났 猷⑦봽媛 媛먯??섏뼱 ?ㅻⅨ ?꾨왂?쇰줈 ?꾪솚?⑸땲??({repeatedSameSignatureCount}??");
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -716,7 +718,7 @@ public partial class AgentLoopService
|
||||
EmitEvent(
|
||||
AgentEventType.Thinking,
|
||||
"",
|
||||
$"?쎄린 ?꾩슜 ?꾧뎄留??곗냽 {consecutiveReadOnlySuccessTools}???ㅽ뻾?섏뼱 ?뺤껜瑜?媛먯??덉뒿?덈떎. ?ㅽ뻾 ?④퀎濡??꾪솚?⑸땲?? (threshold={threshold})");
|
||||
$"?쎄린 ?꾩슜 ?꾧뎄媛 ?곗냽 {consecutiveReadOnlySuccessTools}???ㅽ뻾?섏뼱 ?뺤껜媛 媛먯??섏뿀?듬땲?? ?ㅽ뻾 ?④퀎濡??꾪솚?⑸땲??(threshold={threshold})");
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -750,7 +752,7 @@ public partial class AgentLoopService
|
||||
EmitEvent(
|
||||
AgentEventType.Thinking,
|
||||
"",
|
||||
$"?ㅽ뻾 吏꾩쟾???놁뼱 媛뺤젣 蹂듦뎄 ?④퀎瑜??쒖옉?⑸땲??({runState.NoProgressRecoveryRetry}/2)");
|
||||
$"?ㅽ뻾 吏꾩쟾???놁뼱 媛뺤젣 蹂듦뎄 ?④퀎濡??쒖옉?⑸땲??({runState.NoProgressRecoveryRetry}/2)");
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -809,8 +811,8 @@ public partial class AgentLoopService
|
||||
return false;
|
||||
|
||||
var lower = content.ToLowerInvariant();
|
||||
if (ContainsAny(lower, "success", "succeeded", "passed", "?듦낵", "?깃났", "鍮뚮뱶?덉뒿?덈떎", "tests passed", "build succeeded")
|
||||
&& !ContainsAny(lower, "fail", "failed", "error", "?ㅻ쪟", "?ㅽ뙣", "exception", "denied", "not found"))
|
||||
if (ContainsAny(lower, "success", "succeeded", "passed", "성공", "tests passed", "build succeeded")
|
||||
&& !ContainsAny(lower, "fail", "failed", "error", "오류", "실패", "exception", "denied", "not found"))
|
||||
return false;
|
||||
|
||||
return ContainsAny(
|
||||
@@ -829,7 +831,7 @@ public partial class AgentLoopService
|
||||
"?ㅻ쪟",
|
||||
"?덉쇅",
|
||||
"?쒓컙 珥덇낵",
|
||||
"沅뚰븳",
|
||||
"沅뚰븳 嫄곕?",
|
||||
"李⑤떒",
|
||||
"찾을 수");
|
||||
}
|
||||
@@ -1103,7 +1105,55 @@ public partial class AgentLoopService
|
||||
}
|
||||
catch (OperationCanceledException) when (!ct.IsCancellationRequested)
|
||||
{
|
||||
return ToolResult.Fail($"?꾧뎄 ?ㅽ뻾 ??꾩븘??({timeoutMs}ms): {toolName}");
|
||||
return ToolResult.Fail($"도구 실행 시간 초과 ({timeoutMs}ms): {toolName}");
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly HashSet<string> PrefetchableReadOnlyTools = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"file_read", "glob", "grep", "grep_tool", "folder_map", "document_read",
|
||||
"search_codebase", "code_search", "env_tool", "datetime_tool",
|
||||
"dev_env_detect", "memory", "json_tool", "regex_tool", "base64_tool",
|
||||
"hash_tool", "image_analyze", "multi_read"
|
||||
};
|
||||
|
||||
private async Task<LlmService.ToolPrefetchResult?> TryPrefetchReadOnlyToolAsync(
|
||||
LlmService.ContentBlock block,
|
||||
IReadOnlyCollection<IAgentTool> tools,
|
||||
AgentContext context,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var activeToolNames = tools.Select(t => t.Name).Distinct(StringComparer.OrdinalIgnoreCase).ToList();
|
||||
var resolvedToolName = ResolveRequestedToolName(block.ToolName, activeToolNames);
|
||||
block.ResolvedToolName = resolvedToolName;
|
||||
|
||||
if (!PrefetchableReadOnlyTools.Contains(resolvedToolName))
|
||||
return null;
|
||||
|
||||
var tool = _tools.Get(resolvedToolName);
|
||||
if (tool == null)
|
||||
return null;
|
||||
|
||||
EmitEvent(
|
||||
AgentEventType.Thinking,
|
||||
resolvedToolName,
|
||||
$"읽기 도구 조기 실행 준비: {resolvedToolName}");
|
||||
|
||||
var sw = Stopwatch.StartNew();
|
||||
try
|
||||
{
|
||||
var input = block.ToolInput ?? JsonDocument.Parse("{}").RootElement;
|
||||
var result = await ExecuteToolWithTimeoutAsync(tool, resolvedToolName, input, context, null, ct);
|
||||
sw.Stop();
|
||||
return new LlmService.ToolPrefetchResult(result, sw.ElapsedMilliseconds, resolvedToolName);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
sw.Stop();
|
||||
return new LlmService.ToolPrefetchResult(
|
||||
ToolResult.Fail($"조기 실행 오류: {ex.Message}"),
|
||||
sw.ElapsedMilliseconds,
|
||||
resolvedToolName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1113,7 +1163,8 @@ public partial class AgentLoopService
|
||||
CancellationToken ct,
|
||||
string phaseLabel,
|
||||
RunState? runState = null,
|
||||
bool forceToolCall = false)
|
||||
bool forceToolCall = false,
|
||||
Func<LlmService.ContentBlock, Task<LlmService.ToolPrefetchResult?>>? prefetchToolCallAsync = null)
|
||||
{
|
||||
var transientRetries = runState?.TransientLlmErrorRetries ?? 0;
|
||||
var contextRecoveryRetries = runState?.ContextRecoveryAttempts ?? 0;
|
||||
@@ -1121,7 +1172,7 @@ public partial class AgentLoopService
|
||||
{
|
||||
try
|
||||
{
|
||||
return await _llm.SendWithToolsAsync(messages, tools, ct, forceToolCall);
|
||||
return await _llm.SendWithToolsAsync(messages, tools, ct, forceToolCall, prefetchToolCallAsync);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -1191,6 +1242,11 @@ public partial class AgentLoopService
|
||||
result.FilePath, result.Success);
|
||||
}
|
||||
|
||||
// 워크플로우 상세 로그: 도구 실행 결과
|
||||
WorkflowLogService.LogToolResult(_conversationId, _currentRunId, 0,
|
||||
call.ToolName, TruncateOutput(result.Output, 2000),
|
||||
result.Success, 0);
|
||||
|
||||
totalToolCalls++;
|
||||
if (totalToolCalls > 15 && maxIterations < baseMax * 2)
|
||||
maxIterations = Math.Min(baseMax * 2, 50);
|
||||
@@ -1294,7 +1350,7 @@ public partial class AgentLoopService
|
||||
result.Output)
|
||||
});
|
||||
}
|
||||
EmitEvent(AgentEventType.Thinking, "", "鍮꾩옱?쒕룄 ?ㅽ뙣濡?遺꾨쪟?섏뼱 ?숈씪 ?몄텧 諛섎났??以묐떒?섍퀬 ?고쉶 ?꾨왂?쇰줈 ?꾪솚?⑸땲??");
|
||||
EmitEvent(AgentEventType.Thinking, "", "현재도 실패로 오류 발생, 다시 실행 경로로 전환합니다.");
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1310,7 +1366,7 @@ public partial class AgentLoopService
|
||||
if (memSvc != null && (app?.SettingsService?.Settings.Llm.EnableAgentMemory ?? false))
|
||||
{
|
||||
memSvc.Add("correction",
|
||||
$"?꾧뎄 '{call.ToolName}' 諛섎났 ?ㅽ뙣: {TruncateOutput(result.Output, 200)}",
|
||||
$"도구 '{call.ToolName}' 반복 실패: {TruncateOutput(result.Output, 200)}",
|
||||
$"conv:{_conversationId}", context.WorkFolder);
|
||||
}
|
||||
}
|
||||
@@ -1453,8 +1509,8 @@ public partial class AgentLoopService
|
||||
taskPolicy)
|
||||
});
|
||||
EmitEvent(AgentEventType.Thinking, "", highImpactCodeChange
|
||||
? "怨좎쁺??肄붾뱶 蹂寃쎌씠??李몄“ 寃?됯낵 build/test 寃利앹쓣 ???꾧꺽?섍쾶 ?댁뼱媛묐땲??.."
|
||||
: "肄붾뱶 蹂寃???build/test/diff 寃利앹쓣 ?댁뼱媛묐땲??..");
|
||||
? "怨좎쁺??肄붾뱶 蹂寃쎌쑝濡?李몄“ 寃利앷낵 build/test 寃利앹쓣 ???꾧꺽?섍쾶 ?댁뼱媛묐땲??."
|
||||
: "肄붾뱶 蹂寃???build/test/diff 寃利앹쓣 ?댁뼱媛묐땲??.");
|
||||
}
|
||||
else if (HasCodeVerificationEvidenceAfterLastModification(messages, requireHighImpactCodeVerification))
|
||||
{
|
||||
@@ -1541,7 +1597,7 @@ public partial class AgentLoopService
|
||||
if (devShouldContinue)
|
||||
{
|
||||
messages.Add(LlmService.CreateToolResultMessage(
|
||||
call.ToolId, call.ToolName, devToolResultMessage ?? "[SKIPPED by developer] 사용자가 이 도구 실행을 건너뛰었습니다."));
|
||||
call.ToolId, call.ToolName, devToolResultMessage ?? "[SKIPPED by developer] 媛쒕컻?먭? ???꾧뎄 ?ㅽ뻾??嫄대꼫?곗뿀?듬땲??"));
|
||||
return (true, null);
|
||||
}
|
||||
}
|
||||
@@ -1562,7 +1618,7 @@ public partial class AgentLoopService
|
||||
if (scopeShouldContinue)
|
||||
{
|
||||
messages.Add(LlmService.CreateToolResultMessage(
|
||||
call.ToolId, call.ToolName, scopeToolResultMessage ?? "[SKIPPED] 사용자가 이 작업을 건너뛰었습니다."));
|
||||
call.ToolId, call.ToolName, scopeToolResultMessage ?? "[SKIPPED] 媛쒕컻?먭? ???묒뾽??嫄대꼫?곗뿀?듬땲??"));
|
||||
return (true, null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -287,6 +287,22 @@ public sealed class AxAgentExecutionEngine
|
||||
&& !string.Equals(t, "Resumed", StringComparison.OrdinalIgnoreCase)
|
||||
&& !string.Equals(t, "SessionStart", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
// 완료 메시지로 표시하면 안 되는 Thinking 이벤트 요약 패턴 (내부 진행 상태 문자열)
|
||||
static bool IsInternalStatusSummary(string? summary)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(summary)) return false;
|
||||
return summary.StartsWith("LLM에 요청 중", StringComparison.OrdinalIgnoreCase)
|
||||
|| summary.StartsWith("도구 미호출 루프", StringComparison.OrdinalIgnoreCase)
|
||||
|| summary.StartsWith("강제 실행 유도", StringComparison.OrdinalIgnoreCase)
|
||||
|| summary.StartsWith("읽기 전용 도구", StringComparison.OrdinalIgnoreCase)
|
||||
|| summary.StartsWith("병렬 실행", StringComparison.OrdinalIgnoreCase)
|
||||
|| summary.StartsWith("Self-Reflection", StringComparison.OrdinalIgnoreCase)
|
||||
|| summary.StartsWith("일시적 LLM 오류", StringComparison.OrdinalIgnoreCase)
|
||||
|| summary.StartsWith("컨텍스트 한도 초과", StringComparison.OrdinalIgnoreCase)
|
||||
|| summary.Contains("반복 ") && summary.Contains('/')
|
||||
|| summary.Contains("[System:");
|
||||
}
|
||||
|
||||
var completionLine = runTab switch
|
||||
{
|
||||
"Cowork" => "코워크 작업이 완료되었습니다.",
|
||||
@@ -310,9 +326,11 @@ public sealed class AxAgentExecutionEngine
|
||||
: fileLine;
|
||||
}
|
||||
|
||||
// 파일 없으면 가장 최근 의미 있는 이벤트 요약 사용
|
||||
// 파일 없으면 가장 최근 의미 있는 이벤트 요약 사용 (내부 상태 문자열 제외)
|
||||
var latestSummary = conversation.ExecutionEvents?
|
||||
.Where(evt => !string.IsNullOrWhiteSpace(evt.Summary) && IsSignificantEventType(evt.Type))
|
||||
.Where(evt => !string.IsNullOrWhiteSpace(evt.Summary)
|
||||
&& IsSignificantEventType(evt.Type)
|
||||
&& !IsInternalStatusSummary(evt.Summary))
|
||||
.OrderByDescending(evt => evt.Timestamp)
|
||||
.Select(evt => evt.Summary.Trim())
|
||||
.FirstOrDefault();
|
||||
|
||||
@@ -26,7 +26,12 @@ public static class ModelExecutionProfileCatalog
|
||||
int RecentExecutionGateMaxRetries,
|
||||
int ExecutionSuccessGateMaxRetries,
|
||||
int DocumentVerificationGateMaxRetries,
|
||||
int TerminalEvidenceGateMaxRetries);
|
||||
int TerminalEvidenceGateMaxRetries,
|
||||
/// <summary>
|
||||
/// true이면 첫 번째 LLM 호출 직전 마지막 user 메시지로 도구 호출 강제 reminder를 주입합니다.
|
||||
/// IBM Qwen 등 system prompt instruction을 약하게 따르는 chatty 모델용.
|
||||
/// </summary>
|
||||
bool InjectPreCallToolReminder = false);
|
||||
|
||||
public static string Normalize(string? key)
|
||||
{
|
||||
@@ -51,9 +56,9 @@ public static class ModelExecutionProfileCatalog
|
||||
ForceToolCallAfterPlan: true,
|
||||
ToolTemperatureCap: 0.2,
|
||||
NoToolResponseThreshold: 1,
|
||||
NoToolRecoveryMaxRetries: 1,
|
||||
PlanExecutionRetryMax: 1,
|
||||
DocumentPlanRetryMax: 1,
|
||||
NoToolRecoveryMaxRetries: 4, // IBM/Qwen 등 chatty 모델: 재시도 횟수 늘려 도구 호출 강제
|
||||
PlanExecutionRetryMax: 2,
|
||||
DocumentPlanRetryMax: 2,
|
||||
PreferAggressiveDocumentFallback: true,
|
||||
ReduceEarlyMemoryPressure: true,
|
||||
EnablePostToolVerification: false,
|
||||
@@ -68,7 +73,8 @@ public static class ModelExecutionProfileCatalog
|
||||
RecentExecutionGateMaxRetries: 0,
|
||||
ExecutionSuccessGateMaxRetries: 0,
|
||||
DocumentVerificationGateMaxRetries: 0,
|
||||
TerminalEvidenceGateMaxRetries: 1),
|
||||
TerminalEvidenceGateMaxRetries: 1,
|
||||
InjectPreCallToolReminder: true), // IBM/Qwen: 첫 호출 직전 reminder 주입으로 이중 강제
|
||||
"reasoning_first" => new ExecutionPolicy(
|
||||
"reasoning_first",
|
||||
"추론 우선",
|
||||
|
||||
Reference in New Issue
Block a user