코드탭 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:
18
README.md
18
README.md
@@ -1,5 +1,23 @@
|
||||
# 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)
|
||||
- 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:
|
||||
|
||||
@@ -303,3 +303,21 @@ Updated: 2026-04-16 02:05 (KST)
|
||||
- Remaining follow-up:
|
||||
- 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
|
||||
|
||||
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
|
||||
|
||||
@@ -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 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
|
||||
|
||||
167
src/AxCopilot.Tests/Services/AgentLoopPreLlmStageServiceTests.cs
Normal file
167
src/AxCopilot.Tests/Services/AgentLoopPreLlmStageServiceTests.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
174
src/AxCopilot/Services/Agent/AgentLoopPreLlmStageService.cs
Normal file
174
src/AxCopilot/Services/Agent/AgentLoopPreLlmStageService.cs
Normal 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];
|
||||
}
|
||||
}
|
||||
@@ -495,44 +495,6 @@ public partial class AgentLoopService
|
||||
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는 동일 파라미터로 반복 내 여러 번 호출됨
|
||||
var cachedActiveTools = FilterExplorationToolsForCurrentIteration(
|
||||
GetRuntimeActiveTools(llm.DisabledTools, runtimeOverrides),
|
||||
@@ -541,31 +503,56 @@ public partial class AgentLoopService
|
||||
ActiveTab,
|
||||
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에 도구 정의와 함께 요청
|
||||
List<ContentBlock> blocks;
|
||||
var llmCallSw = Stopwatch.StartNew();
|
||||
try
|
||||
{
|
||||
var activeTools = cachedActiveTools;
|
||||
if (activeTools.Count == 0)
|
||||
if (!string.IsNullOrWhiteSpace(preLlmStage.MissingToolsReturnMessage))
|
||||
{
|
||||
if (runtimeOverrides != null && runtimeOverrides.AllowedToolNames.Count > 0)
|
||||
{
|
||||
EmitEvent(AgentEventType.Error, "", "현재 스킬 런타임 정책으로 사용 가능한 도구가 없습니다.");
|
||||
return "⚠ 현재 스킬 정책에서 허용된 도구가 없어 작업을 진행할 수 없습니다. allowed-tools 설정을 확인하세요.";
|
||||
}
|
||||
|
||||
var tabLabel = string.IsNullOrWhiteSpace(ActiveTab) ? "현재" : ActiveTab;
|
||||
EmitEvent(AgentEventType.Error, "", $"{tabLabel} 탭에서 사용 가능한 도구가 없습니다.");
|
||||
return $"⚠ 현재 {tabLabel} 탭에서 사용 가능한 도구가 없어 작업을 진행할 수 없습니다. 탭별 도구 노출 정책을 확인하세요.";
|
||||
EmitEvent(AgentEventType.Error, "", preLlmStage.MissingToolsEventSummary ?? "사용 가능한 도구가 없습니다.");
|
||||
return preLlmStage.MissingToolsReturnMessage;
|
||||
}
|
||||
var llmRequest = AgentLoopQueryAssemblyService.PrepareRequest(
|
||||
queryMessages,
|
||||
codeWorkingSet,
|
||||
totalToolCalls,
|
||||
executionPolicy.ForceInitialToolCall,
|
||||
executionPolicy.InjectPreCallToolReminder,
|
||||
runState.NoToolCallLoopRetry);
|
||||
var activeTools = cachedActiveTools;
|
||||
var llmRequest = preLlmStage.LlmRequest!;
|
||||
var forceFirst = llmRequest.ForceInitialToolCall;
|
||||
var sendMessages = llmRequest.SendMessages;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user