에이전트 루프와 코드 언어 지원, PPT 생성 품질을 함께 고도화

- AgentCommandQueue를 도입해 실행 중 추가 입력을 우선순위와 인터럽트 여부까지 포함해 처리하도록 정리함
- AgentToolResultBudget와 AgentQueryContextBuilder에 tool result preview 캐시를 연결해 긴 세션에서 축약 결과 재사용을 안정화함
- CodeLanguageCatalog를 추가해 코드 탭의 내장 언어 지원, 인덱싱 확장자, 시스템 프롬프트 언어 가이드, LSP 언어 판정을 한 카탈로그로 통합함
- 설정의 코드 탭에 지원 언어(LSP)와 코드 탭 기본 지원 언어를 명시적으로 표시하도록 보강함
- DocumentPlannerTool의 presentation 구조를 컨설팅형 스토리라인으로 정리하고, PptxSkill에 executive_summary/recommendation/roadmap/comparison/kpi_dashboard 레이아웃을 추가함
- pptx-creator 스킬을 AX native pptx_create 중심으로 재작성하고, 관련 회귀 테스트를 추가했으며 WorkspaceContextGeneratorTests의 nullable 경고도 정리함

검증 결과
- dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_impl\\ -p:IntermediateOutputPath=obj\\verify_impl\\
- dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "CodeLanguageCatalogTests|AgentCommandQueueTests|AgentToolResultBudgetTests|DocumentPlannerPresentationTests|PptxSkillConsultingDeckTests" -p:OutputPath=bin\\verify_impl_tests\\ -p:IntermediateOutputPath=obj\\verify_impl_tests\\
This commit is contained in:
2026-04-14 19:53:39 +09:00
parent 946c31e275
commit 0b6d60e959
23 changed files with 1837 additions and 746 deletions

View File

@@ -36,14 +36,64 @@ public partial class AgentLoopService
};
private readonly ConcurrentDictionary<string, PermissionPromptPreview> _pendingPermissionPreviews = new(StringComparer.OrdinalIgnoreCase);
/// <summary>실행 중 사용자 메시지 주입 큐 (Claude Code 스타일 mid-execution steering).</summary>
private readonly ConcurrentQueue<string> _pendingUserMessages = new();
/// <summary>실행 중 추가 입력/알림 큐. 우선순위와 종류를 함께 보존합니다.</summary>
private readonly AgentCommandQueue _pendingCommands = new();
/// <summary>실행 중인 에이전트 루프에 사용자 메시지를 주입합니다. 다음 LLM 호출 전에 반영됩니다.</summary>
public void InjectUserMessage(string message)
{
if (!string.IsNullOrWhiteSpace(message))
_pendingUserMessages.Enqueue(message);
_pendingCommands.EnqueuePrompt(
message,
IsRunning ? "now" : "next",
requestInterrupt: IsRunning);
}
private void DrainPendingCommands(List<ChatMessage> messages)
{
var drained = _pendingCommands.DrainAll();
if (drained.Count == 0)
return;
var interruptingPrompts = drained.Count(x => x.Kind == AgentCommandKind.Prompt && x.RequestInterrupt);
if (interruptingPrompts > 0)
{
messages.Add(new ChatMessage
{
Role = "system",
MetaKind = "queued_input_interrupt",
Content = $"[queued input] {interruptingPrompts} new prompt(s) arrived during execution. Prioritize the newest user direction before continuing.",
Timestamp = DateTime.Now,
});
}
foreach (var item in drained)
{
switch (item.Kind)
{
case AgentCommandKind.Notification:
messages.Add(new ChatMessage
{
Role = "system",
MetaKind = "queue_notification",
Content = item.Content,
Timestamp = item.CreatedAt,
});
EmitEvent(AgentEventType.Thinking, "", item.Content);
break;
default:
messages.Add(new ChatMessage
{
Role = "user",
MetaKind = item.RequestInterrupt ? "queued_prompt_interrupt" : "queued_prompt",
Content = item.Content,
Timestamp = item.CreatedAt,
});
EmitEvent(AgentEventType.UserMessage, "", item.Content);
break;
}
}
}
/// <summary>에이전트 이벤트 스트림 (UI 바인딩용).</summary>
@@ -179,7 +229,7 @@ public partial class AgentLoopService
_currentRunId = Guid.NewGuid().ToString("N");
_docFallbackAttempted = false;
_documentPlanApproved = false;
while (_pendingUserMessages.TryDequeue(out _)) { } // 이전 실행의 잔여 메시지 제거
_pendingCommands.Clear(); // 이전 실행의 잔여 제거
var llm = _settings.Settings.Llm;
var baseMax = llm.MaxAgentIterations > 0 ? llm.MaxAgentIterations : 25;
var maxIterations = baseMax; // 동적 조정 가능
@@ -440,11 +490,7 @@ public partial class AgentLoopService
}
// ── 실행 중 사용자 메시지 주입 (Claude Code 스타일 steering) ──
while (_pendingUserMessages.TryDequeue(out var injectedMsg))
{
messages.Add(new ChatMessage { Role = "user", Content = injectedMsg });
EmitEvent(AgentEventType.UserMessage, "", injectedMsg);
}
DrainPendingCommands(messages);
var queryView = AgentQueryContextBuilder.Build(messages);
var queryMessages = queryView.Messages;
@@ -455,6 +501,7 @@ public partial class AgentLoopService
$"start={queryView.WindowStartIndex}, " +
$"pairs={queryView.PreservedToolPairCount}, " +
$"tool_result_budget={queryView.TruncatedToolResultCount}, " +
$"tool_result_preview_reuse={queryView.ReusedToolResultPreviewCount}, " +
$"tokens {queryView.TokensBeforeBudget}->{queryView.TokensAfterBudget}";
WorkflowLogService.LogTransition(
_conversationId,