???? ?? ?? ?? ??? ?? fallback ???? ?? ??

- CodeLanguageCatalog? UTF-8 ???? ????? ?? fallback ???? ??? ??? manifest/build/test/lint ?? ?? ??? ????
- WorkspaceContextGenerator? ?? ??? ????? ?? Language Workflow ??? ?????? ??? no-LSP ?????? ?? ??? ?? ??? ?? ???
- AgentLoopLlmRequestPreparationService? ??? ?? tool-call ??? pre-call reminder ?? ??? AgentLoopService?? ???
- CodeLanguageCatalogTests, WorkspaceContextGeneratorTests, AgentLoopLlmRequestPreparationServiceTests? ??? ?? fallback/????/LLM ?? ?? ??? ???
- ??: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_final_batch\\ -p:IntermediateOutputPath=obj\\verify_final_batch\\ (?? 0 / ?? 0)
- ??: dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "CodeLanguageCatalogTests|WorkspaceContextGeneratorTests|AgentLoopLlmRequestPreparationServiceTests|AgentLoopIterationPreparationServiceTests|AgentMessageInvariantHelperTests|AgentToolResultBudgetTests|ChatStorageServiceTests|HtmlSkillGoldenReportTests|PptxSkillGoldenDeckTests|DocxSkillGoldenDocumentTests|ExcelSkillGoldenWorkbookTests" -p:OutputPath=bin\\verify_final_batch_tests\\ -p:IntermediateOutputPath=obj\\verify_final_batch_tests\\ (?? 54)
This commit is contained in:
2026-04-15 10:51:44 +09:00
parent 91c4dc74c3
commit 48e8c57cf3
11 changed files with 456 additions and 235 deletions

View File

@@ -0,0 +1,52 @@
using AxCopilot.Models;
namespace AxCopilot.Services.Agent;
internal sealed record AgentLoopLlmRequestPreparationResult(
List<ChatMessage> SendMessages,
bool ForceInitialToolCall,
bool InjectedToolReminder);
/// <summary>
/// query view가 만들어진 뒤 실제 LLM 요청 배열을 조립합니다.
/// 초기 tool call 강제 여부와 사전 reminder 주입을 한곳에서 결정해
/// AgentLoopService 본체가 orchestration에 더 집중하도록 분리합니다.
/// </summary>
internal static class AgentLoopLlmRequestPreparationService
{
public static AgentLoopLlmRequestPreparationResult Prepare(
IReadOnlyList<ChatMessage> queryMessages,
int totalToolCalls,
bool forceInitialToolCallEnabled,
bool injectPreCallToolReminder,
int noToolCallLoopRetry)
{
var forceInitialToolCall = totalToolCalls == 0 && forceInitialToolCallEnabled;
if (!forceInitialToolCall
|| !injectPreCallToolReminder
|| noToolCallLoopRetry > 0)
{
return new AgentLoopLlmRequestPreparationResult(
queryMessages.ToList(),
forceInitialToolCall,
false);
}
var sendMessages = queryMessages.ToList();
sendMessages.Add(BuildToolReminderMessage());
return new AgentLoopLlmRequestPreparationResult(
sendMessages,
forceInitialToolCall,
true);
}
internal static ChatMessage BuildToolReminderMessage()
{
return new ChatMessage
{
Role = "user",
Content = "[TOOL_REQUIRED] 지금 즉시 <tool_call> 형식으로 도구를 호출하세요. 텍스트만 반환하면 거부됩니다.\n" +
"Output format:\n<tool_call>\n{\"name\": \"TOOL_NAME\", \"arguments\": {\"param\": \"value\"}}\n</tool_call>"
};
}
}

View File

@@ -87,22 +87,6 @@ public partial class AgentLoopService
requestInterrupt && IsRunning);
}
private void DrainPendingCommands(List<ChatMessage> messages)
{
var queuedSnapshot = _pendingCommands.Snapshot();
if (queuedSnapshot.Count == 0)
return;
var drained = _pendingCommands.DequeuePriorityBatch();
if (drained.Count == 0)
return;
var projection = AgentQueuedCommandProjector.Project(drained, queuedSnapshot.Count - drained.Count);
messages.AddRange(projection.Messages);
foreach (var evt in projection.Events)
EmitEvent(evt.Type, evt.ToolName, evt.Summary);
}
/// <summary>에이전트 이벤트 스트림 (UI 바인딩용).</summary>
public ObservableCollection<AgentEvent> Events { get; } = new();
@@ -557,25 +541,14 @@ public partial class AgentLoopService
EmitEvent(AgentEventType.Error, "", "현재 스킬 런타임 정책으로 사용 가능한 도구가 없습니다.");
return "⚠ 현재 스킬 정책에서 허용된 도구가 없어 작업을 진행할 수 없습니다. allowed-tools 설정을 확인하세요.";
}
// 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 = queryMessages;
if (forceFirst
&& executionPolicy.InjectPreCallToolReminder
&& runState.NoToolCallLoopRetry == 0)
{
sendMessages = [.. queryMessages, new ChatMessage
{
Role = "user",
Content = "[TOOL_REQUIRED] 지금 즉시 <tool_call> 형식으로 도구를 호출하세요. 텍스트만 반환하면 거부됩니다.\n" +
"Output format:\n<tool_call>\n{\"name\": \"TOOL_NAME\", \"arguments\": {\"param\": \"value\"}}\n</tool_call>"
}];
}
var llmRequest = AgentLoopLlmRequestPreparationService.Prepare(
queryMessages,
totalToolCalls,
executionPolicy.ForceInitialToolCall,
executionPolicy.InjectPreCallToolReminder,
runState.NoToolCallLoopRetry);
var forceFirst = llmRequest.ForceInitialToolCall;
var sendMessages = llmRequest.SendMessages;
// 워크플로우 상세 로그: LLM 요청
llmCallSw.Restart();

View File

@@ -1,5 +1,6 @@
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
@@ -174,6 +175,21 @@ internal static class WorkspaceContextGenerator
catch { return null; }
}
internal static IReadOnlyList<string> DetectLanguageWorkflowHints(
string? workFolder,
string? preferredLanguage = null,
int maxLanguages = 3)
{
if (string.IsNullOrWhiteSpace(workFolder) || !Directory.Exists(workFolder))
return [];
var extDist = GetExtensionDistribution(workFolder, CancellationToken.None);
return CodeLanguageCatalog.BuildWorkspaceWorkflowSummaries(
extDist.Select(x => x.Key),
preferredLanguage,
maxLanguages);
}
// ════════════════════════════════════════════════════════════
// 분석 로직
// ════════════════════════════════════════════════════════════
@@ -450,26 +466,7 @@ internal static class WorkspaceContextGenerator
.ToList();
private static List<string> BuildLanguageWorkflow(List<KeyValuePair<string, int>> extDist)
{
var workflow = new List<string>();
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var (extension, _) in extDist)
{
var capability = Services.CodeLanguageCatalog.FindByExtension(extension);
if (capability == null || !seen.Add(capability.Key))
continue;
var summary = Services.CodeLanguageCatalog.BuildWorkflowSummary(capability.Key);
if (!string.IsNullOrWhiteSpace(summary))
workflow.Add(summary);
if (workflow.Count >= 3)
break;
}
return workflow;
}
=> CodeLanguageCatalog.BuildWorkspaceWorkflowSummaries(extDist.Select(x => x.Key)).ToList();
private static async Task<(string? Branch, string? Remote)> GetGitInfoAsync(
string folder, CancellationToken ct)