IBM vLLM 도구 호출 스트리밍과 모델 프로파일 기반 실행 정책 강화
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:
2026-04-08 16:48:11 +09:00
parent a2c952879d
commit 90ef3400f6
20 changed files with 1231 additions and 241 deletions

View File

@@ -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);
}
}
}

View File

@@ -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;

View File

@@ -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);
}
}

View File

@@ -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();

View File

@@ -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",
"추론 우선",