코드탭 pre-LLM 단계 결정을 별도 서비스로 분리

- AgentLoopPreLlmStageService를 추가해 thinking summary, Gemini free-tier delay, user prompt submit hook, missing-tool guard, request assembly handoff를 한 단계로 정리함

- AgentLoopService는 pre-LLM stage 결과를 소비하는 형태로 단순화해 history/query assembly 다음 단계가 claw-code처럼 더 선명하게 보이도록 구조를 개선함

- AgentLoopPreLlmStageServiceTests를 추가하고 관련 구조 테스트 60개를 통과시켰으며 dotnet build 경고/오류 0으로 검증함
This commit is contained in:
2026-04-16 02:18:05 +09:00
parent 2e1c7be8c3
commit 1ec529ed1c
6 changed files with 443 additions and 56 deletions

View File

@@ -1,5 +1,23 @@
# AX Commander # AX Commander
- Update: 2026-04-16 02:13 (KST)
- Continued the Code-tab structural refactor by extracting the pre-LLM stage into [AgentLoopPreLlmStageService.cs](</E:/AX Copilot - Codex/src/AxCopilot/Services/Agent/AgentLoopPreLlmStageService.cs>).
- The new stage service now owns:
- iteration thinking-summary selection
- Gemini free-tier delay planning
- user-prompt submit hook planning and payload generation
- missing-tool guard construction
- final request assembly handoff through the staged query/history pipeline
- [AgentLoopService.cs](</E:/AX Copilot - Codex/src/AxCopilot/Services/Agent/AgentLoopService.cs>) now reads more like an orchestrator: history/query assembly, pre-LLM stage planning, LLM dispatch, tool execution, and recovery.
- Added [AgentLoopPreLlmStageServiceTests.cs](</E:/AX Copilot - Codex/src/AxCopilot.Tests/Services/AgentLoopPreLlmStageServiceTests.cs>) to lock:
- free-tier delay planning
- prompt-submit hook dedup by fingerprint
- runtime-policy missing-tool failure shaping
- working-set-backed request assembly
- Validation:
- `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_pre_llm_stage_structure\\ -p:IntermediateOutputPath=obj\\verify_pre_llm_stage_structure\\` warnings 0 / errors 0
- `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "AgentLoopPreLlmStageServiceTests|AgentLoopQueryAssemblyServiceTests|AgentLoopIterationPreparationServiceTests|AgentLoopLlmRequestPreparationServiceTests|AgentMessageInvariantHelperTests|SessionLearningCollectorTests|CodeTaskWorkingSetServiceTests|AgentLoopE2ETests" -p:OutputPath=bin\\verify_pre_llm_stage_structure_tests\\ -p:IntermediateOutputPath=obj\\verify_pre_llm_stage_structure_tests\\` passed 60
- Update: 2026-04-16 02:05 (KST) - Update: 2026-04-16 02:05 (KST)
- Continued the Code-tab context reliability work with a structural refactor that moves query/history assembly closer to the staged shape used by `claw-code`. - Continued the Code-tab context reliability work with a structural refactor that moves query/history assembly closer to the staged shape used by `claw-code`.
- Added `src/AxCopilot/Services/Agent/AgentLoopQueryAssemblyService.cs` so `AgentLoopService` no longer directly owns all of the following responsibilities in one place: - Added `src/AxCopilot/Services/Agent/AgentLoopQueryAssemblyService.cs` so `AgentLoopService` no longer directly owns all of the following responsibilities in one place:

View File

@@ -303,3 +303,21 @@ Updated: 2026-04-16 02:05 (KST)
- Remaining follow-up: - Remaining follow-up:
- keep extracting more inline AgentLoop responsibilities into smaller staged services where it improves observability or retry correctness - keep extracting more inline AgentLoop responsibilities into smaller staged services where it improves observability or retry correctness
- continue measuring long Code runs against claw-code-style continuity scenarios - continue measuring long Code runs against claw-code-style continuity scenarios
Updated: 2026-04-16 02:13 (KST)
- Delivered in this pass:
- structural alignment step:
- `AgentLoopPreLlmStageService.cs` now owns the iteration decisions immediately before the LLM call.
- the service centralizes:
- thinking-summary selection
- Gemini free-tier delay planning
- user-prompt submit hook fingerprint/payload planning
- missing-tool guard shaping
- request assembly handoff
- `AgentLoopService.cs` now consumes that stage result instead of computing those branches inline.
- test coverage step:
- `AgentLoopPreLlmStageServiceTests.cs` now locks the new pre-LLM decision layer.
- Remaining follow-up:
- continue extracting the actual LLM dispatch / streaming callback branch into a narrower execution service
- compare long-running Code traces against claw-code-style staged transitions and keep reducing inline loop logic

View File

@@ -1850,3 +1850,26 @@ UI ?遺우쁽????域뱀뮆???귐뗫솯?醫딆춦 ???袁る퓮 ?臾믩씜 ??疫
- 검증: - 검증:
- `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_query_assembly_structure\\ -p:IntermediateOutputPath=obj\\verify_query_assembly_structure\\` 경고 0 / 오류 0 - `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_query_assembly_structure\\ -p:IntermediateOutputPath=obj\\verify_query_assembly_structure\\` 경고 0 / 오류 0
- `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "AgentLoopQueryAssemblyServiceTests|AgentLoopIterationPreparationServiceTests|AgentLoopLlmRequestPreparationServiceTests|AgentMessageInvariantHelperTests|SessionLearningCollectorTests|CodeTaskWorkingSetServiceTests|AgentLoopE2ETests" -p:OutputPath=bin\\verify_query_assembly_structure_tests\\ -p:IntermediateOutputPath=obj\\verify_query_assembly_structure_tests\\` 통과 56 - `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "AgentLoopQueryAssemblyServiceTests|AgentLoopIterationPreparationServiceTests|AgentLoopLlmRequestPreparationServiceTests|AgentMessageInvariantHelperTests|SessionLearningCollectorTests|CodeTaskWorkingSetServiceTests|AgentLoopE2ETests" -p:OutputPath=bin\\verify_query_assembly_structure_tests\\ -p:IntermediateOutputPath=obj\\verify_query_assembly_structure_tests\\` 통과 56
업데이트: 2026-04-16 02:13 (KST)
- Code 탭 pre-LLM stage 구조를 단계형 서비스로 추가 분리했습니다.
- `src/AxCopilot/Services/Agent/AgentLoopPreLlmStageService.cs`
- 반복당 LLM 호출 직전 결정을 한 곳으로 모았습니다.
- thinking summary 선택
- Gemini free-tier delay 계획
- user prompt submit hook fingerprint/payload 생성
- missing-tool guard 문구 생성
- final request assembly handoff
를 담당합니다.
- `src/AxCopilot/Services/Agent/AgentLoopService.cs`
- 위 결정들을 직접 계산하던 인라인 코드를 제거하고, pre-LLM stage 결과를 해석해 이벤트/대기/훅 실행만 수행하도록 단순화했습니다.
- 결과적으로 AgentLoop는 `history/query assembly -> pre-LLM stage -> dispatch -> tool execution/recovery` 흐름으로 읽히게 됐습니다.
- `src/AxCopilot.Tests/Services/AgentLoopPreLlmStageServiceTests.cs`
- free-tier delay 계획
- prompt hook fingerprint dedup
- runtime policy missing-tool failure
- working set supplemental request assembly
를 회귀 테스트로 고정했습니다.
- 검증:
- `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_pre_llm_stage_structure\\ -p:IntermediateOutputPath=obj\\verify_pre_llm_stage_structure\\` 경고 0 / 오류 0
- `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "AgentLoopPreLlmStageServiceTests|AgentLoopQueryAssemblyServiceTests|AgentLoopIterationPreparationServiceTests|AgentLoopLlmRequestPreparationServiceTests|AgentMessageInvariantHelperTests|SessionLearningCollectorTests|CodeTaskWorkingSetServiceTests|AgentLoopE2ETests" -p:OutputPath=bin\\verify_pre_llm_stage_structure_tests\\ -p:IntermediateOutputPath=obj\\verify_pre_llm_stage_structure_tests\\` 통과 60

View File

@@ -0,0 +1,167 @@
using System.Text.Json;
using AxCopilot.Models;
using AxCopilot.Services.Agent;
using FluentAssertions;
using Xunit;
namespace AxCopilot.Tests.Services;
public class AgentLoopPreLlmStageServiceTests
{
private sealed class FakeAgentTool : IAgentTool
{
public string Name { get; init; } = "file_read";
public string Description { get; init; } = "Reads a file";
public ToolParameterSchema Parameters { get; init; } = new();
public Task<ToolResult> ExecuteAsync(JsonElement args, AgentContext context, CancellationToken ct = default)
=> Task.FromResult(ToolResult.Ok("ok"));
}
[Fact]
public void Prepare_ShouldPlanFreeTierDelayForGeminiAfterFirstIteration()
{
var result = AgentLoopPreLlmStageService.Prepare(new AgentLoopPreLlmStageInput(
QueryMessages: [new ChatMessage { Role = "user", Content = "inspect" }],
ActiveTools: [new FakeAgentTool()],
CodeWorkingSet: null,
ActiveTab: "Code",
AgentLogLevel: "info",
Iteration: 2,
MaxIterations: 40,
FreeTierMode: true,
ActiveService: "gemini",
FreeTierDelaySeconds: 5,
LatestUserPrompt: "inspect",
LastUserPromptHookFingerprint: null,
RunId: "run-1",
TotalToolCalls: 1,
ForceInitialToolCallEnabled: true,
InjectPreCallToolReminder: true,
NoToolCallLoopRetry: 0,
HasRuntimeOverride: false,
RuntimeAllowedToolCount: 0));
result.FreeTierDelayMilliseconds.Should().Be(5000);
result.FreeTierDelaySummary.Should().Contain("5s");
}
[Fact]
public void Prepare_ShouldEmitUserPromptHookOnlyWhenFingerprintChanges()
{
var unchanged = AgentLoopPreLlmStageService.Prepare(new AgentLoopPreLlmStageInput(
QueryMessages: [new ChatMessage { Role = "user", Content = "fix build" }],
ActiveTools: [new FakeAgentTool()],
CodeWorkingSet: null,
ActiveTab: "Code",
AgentLogLevel: "debug",
Iteration: 1,
MaxIterations: 20,
FreeTierMode: false,
ActiveService: "vllm",
FreeTierDelaySeconds: 0,
LatestUserPrompt: "fix build",
LastUserPromptHookFingerprint: $"{ "fix build".Length}:{ "fix build".GetHashCode()}",
RunId: "run-2",
TotalToolCalls: 0,
ForceInitialToolCallEnabled: true,
InjectPreCallToolReminder: true,
NoToolCallLoopRetry: 0,
HasRuntimeOverride: false,
RuntimeAllowedToolCount: 0));
unchanged.ShouldRunUserPromptSubmitHook.Should().BeFalse();
unchanged.UserPromptSubmitHookPayloadJson.Should().BeNull();
var changed = AgentLoopPreLlmStageService.Prepare(new AgentLoopPreLlmStageInput(
QueryMessages: [new ChatMessage { Role = "user", Content = "fix build" }],
ActiveTools: [new FakeAgentTool()],
CodeWorkingSet: null,
ActiveTab: "Code",
AgentLogLevel: "debug",
Iteration: 1,
MaxIterations: 20,
FreeTierMode: false,
ActiveService: "vllm",
FreeTierDelaySeconds: 0,
LatestUserPrompt: "fix build",
LastUserPromptHookFingerprint: null,
RunId: "run-3",
TotalToolCalls: 0,
ForceInitialToolCallEnabled: true,
InjectPreCallToolReminder: true,
NoToolCallLoopRetry: 0,
HasRuntimeOverride: false,
RuntimeAllowedToolCount: 0));
changed.ShouldRunUserPromptSubmitHook.Should().BeTrue();
changed.UserPromptSubmitHookPayloadJson.Should().Contain("\"runId\":\"run-3\"");
changed.ThinkingSummary.Should().Contain("iteration 1/20");
}
[Fact]
public void Prepare_ShouldReturnRuntimeSpecificMissingToolsFailure()
{
var result = AgentLoopPreLlmStageService.Prepare(new AgentLoopPreLlmStageInput(
QueryMessages: [new ChatMessage { Role = "user", Content = "fix build" }],
ActiveTools: [],
CodeWorkingSet: null,
ActiveTab: "Code",
AgentLogLevel: "info",
Iteration: 1,
MaxIterations: 20,
FreeTierMode: false,
ActiveService: "vllm",
FreeTierDelaySeconds: 0,
LatestUserPrompt: "fix build",
LastUserPromptHookFingerprint: null,
RunId: "run-4",
TotalToolCalls: 0,
ForceInitialToolCallEnabled: true,
InjectPreCallToolReminder: true,
NoToolCallLoopRetry: 0,
HasRuntimeOverride: true,
RuntimeAllowedToolCount: 3));
result.LlmRequest.Should().BeNull();
result.MissingToolsEventSummary.Should().Contain("skill runtime policy");
result.MissingToolsReturnMessage.Should().Contain("allowed-tools");
}
[Fact]
public void Prepare_ShouldBuildLlmRequestWhenToolsExist()
{
var workingSet = new CodeTaskWorkingSetService(
"create WPF shell",
@"E:\code",
"wpf-mvvm",
startedFromEmptyWorkspace: true);
using var mkdirDoc = JsonDocument.Parse("""{"action":"mkdir","paths":["Views","ViewModels"]}""");
workingSet.RecordToolResult("file_manage", mkdirDoc.RootElement.Clone(), ToolResult.Ok("created directories"));
var result = AgentLoopPreLlmStageService.Prepare(new AgentLoopPreLlmStageInput(
QueryMessages: [new ChatMessage { Role = "user", Content = "create WPF shell" }],
ActiveTools: [new FakeAgentTool()],
CodeWorkingSet: workingSet,
ActiveTab: "Code",
AgentLogLevel: "info",
Iteration: 1,
MaxIterations: 30,
FreeTierMode: false,
ActiveService: "vllm",
FreeTierDelaySeconds: 0,
LatestUserPrompt: "create WPF shell",
LastUserPromptHookFingerprint: null,
RunId: "run-5",
TotalToolCalls: 0,
ForceInitialToolCallEnabled: true,
InjectPreCallToolReminder: true,
NoToolCallLoopRetry: 0,
HasRuntimeOverride: false,
RuntimeAllowedToolCount: 0));
result.LlmRequest.Should().NotBeNull();
result.LlmRequest!.SupplementalMessageCount.Should().Be(1);
result.LlmRequest.SendMessages.Should().Contain(message => message.MetaKind == "code_working_set");
}
}

View File

@@ -0,0 +1,174 @@
using System.Text.Json;
using AxCopilot.Models;
namespace AxCopilot.Services.Agent;
internal sealed record AgentLoopPreLlmStageInput(
IReadOnlyList<ChatMessage> QueryMessages,
IReadOnlyCollection<IAgentTool> ActiveTools,
CodeTaskWorkingSetService? CodeWorkingSet,
string? ActiveTab,
string? AgentLogLevel,
int Iteration,
int MaxIterations,
bool FreeTierMode,
string? ActiveService,
int FreeTierDelaySeconds,
string? LatestUserPrompt,
string? LastUserPromptHookFingerprint,
string RunId,
int TotalToolCalls,
bool ForceInitialToolCallEnabled,
bool InjectPreCallToolReminder,
int NoToolCallLoopRetry,
bool HasRuntimeOverride,
int RuntimeAllowedToolCount);
internal sealed record AgentLoopPreLlmStageResult(
string ThinkingSummary,
string? FreeTierDelaySummary,
int FreeTierDelayMilliseconds,
bool ShouldRunUserPromptSubmitHook,
string? UpdatedUserPromptFingerprint,
string? UserPromptSubmitHookPayloadJson,
AgentLoopLlmRequestPreparationResult? LlmRequest,
string? MissingToolsEventSummary,
string? MissingToolsReturnMessage);
/// <summary>
/// Builds the pre-LLM stage decisions for an iteration:
/// status text, free-tier wait, prompt-submit hook plan, missing-tool guard,
/// and the final request payload assembly.
/// </summary>
internal static class AgentLoopPreLlmStageService
{
public static AgentLoopPreLlmStageResult Prepare(AgentLoopPreLlmStageInput input)
{
var thinkingSummary = BuildThinkingSummary(
input.AgentLogLevel,
input.Iteration,
input.MaxIterations);
var freeTierDelay = BuildFreeTierDelayPlan(
input.FreeTierMode,
input.ActiveService,
input.Iteration,
input.FreeTierDelaySeconds);
var promptHookPlan = BuildUserPromptHookPlan(
input.LatestUserPrompt,
input.LastUserPromptHookFingerprint,
input.RunId,
input.ActiveTab);
if (input.ActiveTools.Count == 0)
{
var (eventSummary, returnMessage) = BuildMissingToolsFailure(
input.ActiveTab,
input.HasRuntimeOverride,
input.RuntimeAllowedToolCount);
return new AgentLoopPreLlmStageResult(
thinkingSummary,
freeTierDelay.Summary,
freeTierDelay.DelayMilliseconds,
promptHookPlan.ShouldRunHook,
promptHookPlan.UpdatedFingerprint,
promptHookPlan.PayloadJson,
null,
eventSummary,
returnMessage);
}
var llmRequest = AgentLoopQueryAssemblyService.PrepareRequest(
input.QueryMessages,
input.CodeWorkingSet,
input.TotalToolCalls,
input.ForceInitialToolCallEnabled,
input.InjectPreCallToolReminder,
input.NoToolCallLoopRetry);
return new AgentLoopPreLlmStageResult(
thinkingSummary,
freeTierDelay.Summary,
freeTierDelay.DelayMilliseconds,
promptHookPlan.ShouldRunHook,
promptHookPlan.UpdatedFingerprint,
promptHookPlan.PayloadJson,
llmRequest,
null,
null);
}
private static string BuildThinkingSummary(string? agentLogLevel, int iteration, int maxIterations)
{
var isDebugLog = string.Equals(agentLogLevel, "debug", StringComparison.OrdinalIgnoreCase);
return isDebugLog
? $"Requesting the LLM... (iteration {iteration}/{maxIterations})"
: "Requesting the LLM...";
}
private static (string? Summary, int DelayMilliseconds) BuildFreeTierDelayPlan(
bool freeTierMode,
string? activeService,
int iteration,
int configuredDelaySeconds)
{
var shouldDelay =
freeTierMode
&& iteration > 1
&& string.Equals((activeService ?? "").Trim(), "gemini", StringComparison.OrdinalIgnoreCase);
if (!shouldDelay)
return (null, 0);
var delaySeconds = configuredDelaySeconds > 0 ? configuredDelaySeconds : 4;
return ($"Gemini free-tier wait: continue after {delaySeconds}s", delaySeconds * 1000);
}
private static (bool ShouldRunHook, string? UpdatedFingerprint, string? PayloadJson) BuildUserPromptHookPlan(
string? latestUserPrompt,
string? lastUserPromptHookFingerprint,
string runId,
string? activeTab)
{
if (string.IsNullOrWhiteSpace(latestUserPrompt))
return (false, lastUserPromptHookFingerprint, null);
var fingerprint = $"{latestUserPrompt.Length}:{latestUserPrompt.GetHashCode()}";
if (string.Equals(fingerprint, lastUserPromptHookFingerprint, StringComparison.Ordinal))
return (false, lastUserPromptHookFingerprint, null);
var payload = JsonSerializer.Serialize(new
{
prompt = Truncate(latestUserPrompt, 4000),
runId,
tab = activeTab
});
return (true, fingerprint, payload);
}
private static (string EventSummary, string ReturnMessage) BuildMissingToolsFailure(
string? activeTab,
bool hasRuntimeOverride,
int runtimeAllowedToolCount)
{
if (hasRuntimeOverride && runtimeAllowedToolCount > 0)
{
return (
"No tools remain available under the active skill runtime policy.",
"⚠ No tools are currently allowed by the active skill policy, so the task cannot continue. Check the allowed-tools configuration.");
}
var tabLabel = string.IsNullOrWhiteSpace(activeTab) ? "current" : activeTab;
return (
$"No tools are available in the {tabLabel} tab.",
$"⚠ No tools are available in the {tabLabel} tab, so the task cannot continue. Check the tab-specific tool exposure policy.");
}
private static string Truncate(string text, int maxLength)
{
if (string.IsNullOrWhiteSpace(text) || text.Length <= maxLength)
return text;
return text[..maxLength];
}
}

View File

@@ -495,44 +495,6 @@ public partial class AgentLoopService
AgentLoopDiagnosticsFormatter.BuildQueryViewSummary(queryView)); AgentLoopDiagnosticsFormatter.BuildQueryViewSummary(queryView));
} }
var isDebugLog = string.Equals(_settings.Settings.Llm.AgentLogLevel, "debug", StringComparison.OrdinalIgnoreCase);
EmitEvent(AgentEventType.Thinking, "", isDebugLog
? $"LLM에 요청 중... (반복 {iteration}/{maxIterations})"
: "LLM에 요청 중...");
// Gemini 무료 티어 모드: LLM 호출 간 딜레이 (RPM 한도 초과 방지)
var activeService = (_settings.Settings.Llm.Service ?? "").Trim();
var shouldApplyFreeTierDelay =
llm.FreeTierMode
&& iteration > 1
&& string.Equals(activeService, "gemini", StringComparison.OrdinalIgnoreCase);
if (shouldApplyFreeTierDelay)
{
var delaySec = llm.FreeTierDelaySeconds > 0 ? llm.FreeTierDelaySeconds : 4;
EmitEvent(AgentEventType.Thinking, "", $"Gemini 무료 티어 대기: {delaySec}초 후 다음 호출을 진행합니다...");
await Task.Delay(delaySec * 1000, ct);
}
var latestUserPrompt = messages.LastOrDefault(m => string.Equals(m.Role, "user", StringComparison.OrdinalIgnoreCase))?.Content;
if (!string.IsNullOrWhiteSpace(latestUserPrompt))
{
var fingerprint = $"{latestUserPrompt.Length}:{latestUserPrompt.GetHashCode()}";
if (!string.Equals(fingerprint, lastUserPromptHookFingerprint, StringComparison.Ordinal))
{
lastUserPromptHookFingerprint = fingerprint;
EmitEvent(AgentEventType.UserPromptSubmit, "", "사용자 프롬프트 제출");
await RunRuntimeHooksAsync(
"__user_prompt_submit__",
"pre",
JsonSerializer.Serialize(new
{
prompt = TruncateOutput(latestUserPrompt, 4000),
runId = _currentRunId,
tab = ActiveTab
}));
}
}
// P1: 반복당 1회 캐시 — GetRuntimeActiveTools는 동일 파라미터로 반복 내 여러 번 호출됨 // P1: 반복당 1회 캐시 — GetRuntimeActiveTools는 동일 파라미터로 반복 내 여러 번 호출됨
var cachedActiveTools = FilterExplorationToolsForCurrentIteration( var cachedActiveTools = FilterExplorationToolsForCurrentIteration(
GetRuntimeActiveTools(llm.DisabledTools, runtimeOverrides), GetRuntimeActiveTools(llm.DisabledTools, runtimeOverrides),
@@ -541,31 +503,56 @@ public partial class AgentLoopService
ActiveTab, ActiveTab,
totalToolCalls); totalToolCalls);
var preLlmStage = AgentLoopPreLlmStageService.Prepare(new AgentLoopPreLlmStageInput(
queryMessages,
cachedActiveTools,
codeWorkingSet,
ActiveTab,
_settings.Settings.Llm.AgentLogLevel,
iteration,
maxIterations,
llm.FreeTierMode,
_settings.Settings.Llm.Service,
llm.FreeTierDelaySeconds,
messages.LastOrDefault(m => string.Equals(m.Role, "user", StringComparison.OrdinalIgnoreCase))?.Content,
lastUserPromptHookFingerprint,
_currentRunId,
totalToolCalls,
executionPolicy.ForceInitialToolCall,
executionPolicy.InjectPreCallToolReminder,
runState.NoToolCallLoopRetry,
runtimeOverrides != null,
runtimeOverrides?.AllowedToolNames.Count ?? 0));
EmitEvent(AgentEventType.Thinking, "", preLlmStage.ThinkingSummary);
if (!string.IsNullOrWhiteSpace(preLlmStage.FreeTierDelaySummary) && preLlmStage.FreeTierDelayMilliseconds > 0)
{
EmitEvent(AgentEventType.Thinking, "", preLlmStage.FreeTierDelaySummary);
await Task.Delay(preLlmStage.FreeTierDelayMilliseconds, ct);
}
if (preLlmStage.ShouldRunUserPromptSubmitHook && !string.IsNullOrWhiteSpace(preLlmStage.UserPromptSubmitHookPayloadJson))
{
lastUserPromptHookFingerprint = preLlmStage.UpdatedUserPromptFingerprint;
EmitEvent(AgentEventType.UserPromptSubmit, "", "사용자 프롬프트 제출");
await RunRuntimeHooksAsync(
"__user_prompt_submit__",
"pre",
preLlmStage.UserPromptSubmitHookPayloadJson);
}
// LLM에 도구 정의와 함께 요청 // LLM에 도구 정의와 함께 요청
List<ContentBlock> blocks; List<ContentBlock> blocks;
var llmCallSw = Stopwatch.StartNew(); var llmCallSw = Stopwatch.StartNew();
try try
{ {
var activeTools = cachedActiveTools; if (!string.IsNullOrWhiteSpace(preLlmStage.MissingToolsReturnMessage))
if (activeTools.Count == 0)
{ {
if (runtimeOverrides != null && runtimeOverrides.AllowedToolNames.Count > 0) EmitEvent(AgentEventType.Error, "", preLlmStage.MissingToolsEventSummary ?? "사용 가능한 도구가 없습니다.");
{ return preLlmStage.MissingToolsReturnMessage;
EmitEvent(AgentEventType.Error, "", "현재 스킬 런타임 정책으로 사용 가능한 도구가 없습니다.");
return "⚠ 현재 스킬 정책에서 허용된 도구가 없어 작업을 진행할 수 없습니다. allowed-tools 설정을 확인하세요.";
}
var tabLabel = string.IsNullOrWhiteSpace(ActiveTab) ? "현재" : ActiveTab;
EmitEvent(AgentEventType.Error, "", $"{tabLabel} 탭에서 사용 가능한 도구가 없습니다.");
return $"⚠ 현재 {tabLabel} 탭에서 사용 가능한 도구가 없어 작업을 진행할 수 없습니다. 탭별 도구 노출 정책을 확인하세요.";
} }
var llmRequest = AgentLoopQueryAssemblyService.PrepareRequest( var activeTools = cachedActiveTools;
queryMessages, var llmRequest = preLlmStage.LlmRequest!;
codeWorkingSet,
totalToolCalls,
executionPolicy.ForceInitialToolCall,
executionPolicy.InjectPreCallToolReminder,
runState.NoToolCallLoopRetry);
var forceFirst = llmRequest.ForceInitialToolCall; var forceFirst = llmRequest.ForceInitialToolCall;
var sendMessages = llmRequest.SendMessages; var sendMessages = llmRequest.SendMessages;