???? ?? ? ??? ???? workbook/deck ?? ??? ??
?? ?? - AgentLoopService? ?? ?? ? ?? ??? ?? ??? ??? ?? ??? ? ??? ??? ??? ?? - XLSX dashboard workbook? PPT deck? ?? ?? ??? ??? ?? dashboard, storyline ?? ????? ? ? ????? ?? ?? ???? - AgentQueuedCommandProjector? ??? queued command ??? queued_input_interrupt, queue_notification, queue_resume, queued_prompt ?? ???? thinking/user ???? ???? ?? helper? ?? - AgentLoopService? drain? ? ??? ?? switch? ?? ?? projector ??? ???? ??? ??? - ArtifactQualityReviewService? dashboard sheet? KPI, trend, decision ?? ?? ??? ???? workbook review ??? ?? - ArtifactRepairGuideService? ? dashboard ??? core story ?? ?? ???? ????? ?? - DeckQualityReviewService? storyline? Options, Roadmap, Appendix? ????? ?? ????? ?? ? ?? ?? ??? ????? ?? - DeckRepairGuideService? storyline ?? ??? deck storyline ?? ???? ????? ?? - AgentQueuedCommandProjectorTests, DeckQualityReviewServiceTests, ArtifactQualityReviewServiceTests, ArtifactRepairGuideServiceTests, DeckRepairGuideServiceTests? ??? ??? ?? - README.md? docs/DEVELOPMENT.md? 2026-04-15 09:24 (KST) ?? ??? ?? ??? ?? ?? ?? - dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_loop_doc_finish2\\ -p:IntermediateOutputPath=obj\\verify_loop_doc_finish2\\ : ?? 0 / ?? 0 - dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "AgentQueuedCommandProjectorTests|AgentCommandQueueTests|ArtifactQualityReviewServiceTests|ArtifactRepairGuideServiceTests|DeckQualityReviewServiceTests|DeckRepairGuideServiceTests|PptxSkillGoldenDeckTests|ExcelSkillDashboardSummaryTests" -p:OutputPath=bin\\verify_loop_doc_finish2_tests\\ -p:IntermediateOutputPath=obj\\verify_loop_doc_finish2_tests\\ : ?? 25
This commit is contained in:
@@ -1917,3 +1917,11 @@ MIT License
|
||||
- 테스트로 [ArtifactQualityReviewServiceTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/ArtifactQualityReviewServiceTests.cs), [ArtifactRepairGuideServiceTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/ArtifactRepairGuideServiceTests.cs), [DeckRepairGuideServiceTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/DeckRepairGuideServiceTests.cs), [DocxSkillTemplateFeaturesTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/DocxSkillTemplateFeaturesTests.cs)를 확장했습니다.
|
||||
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_doc_repair_finalize\\ -p:IntermediateOutputPath=obj\\verify_doc_repair_finalize\\` 경고 0 / 오류 0
|
||||
- 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "ArtifactQualityReviewServiceTests|ArtifactRepairGuideServiceTests|DeckRepairGuideServiceTests|DocxSkillTemplateFeaturesTests" -p:OutputPath=bin\\verify_doc_repair_finalize_tests\\ -p:IntermediateOutputPath=obj\\verify_doc_repair_finalize_tests\\` 통과 11
|
||||
|
||||
업데이트: 2026-04-15 09:24 (KST)
|
||||
- 에이전틱 루프의 큐 소비 책임을 더 잘게 분리했습니다. [AgentQueuedCommandProjector.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentQueuedCommandProjector.cs)를 추가해 queued command 배치를 `대화 메시지 + 이벤트`로 투영하는 로직을 별도 helper로 옮겼고, [AgentLoopService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentLoopService.cs)는 투영 결과를 적용하는 얇은 orchestration 역할에 더 가깝게 정리했습니다.
|
||||
- workbook/dashboard 리뷰도 더 엄격해졌습니다. [ArtifactQualityReviewService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/ArtifactQualityReviewService.cs)는 dashboard sheet가 있어도 KPI·trend·decision 정보가 비어 있으면 얇은 dashboard로 경고하고, [ArtifactRepairGuideService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/ArtifactRepairGuideService.cs)는 이를 `core story`가 보이도록 KPI/decision 블록을 추가하라는 문장으로 바꿉니다.
|
||||
- PPT deck 리뷰는 storyline 힌트까지 검사합니다. [DeckQualityReviewService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/DeckQualityReviewService.cs)는 storyline에 `Options`, `Roadmap`, `Appendix`가 있는데 실제 슬라이드가 빠진 경우 별도 이슈를 만들고, [DeckRepairGuideService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/DeckRepairGuideService.cs)는 이를 deck storyline 보강 액션으로 연결합니다.
|
||||
- 테스트로 [AgentQueuedCommandProjectorTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/AgentQueuedCommandProjectorTests.cs), [DeckQualityReviewServiceTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/DeckQualityReviewServiceTests.cs), [ArtifactQualityReviewServiceTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/ArtifactQualityReviewServiceTests.cs), [ArtifactRepairGuideServiceTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/ArtifactRepairGuideServiceTests.cs), [DeckRepairGuideServiceTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/DeckRepairGuideServiceTests.cs)를 확장했습니다.
|
||||
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_loop_doc_finish2\\ -p:IntermediateOutputPath=obj\\verify_loop_doc_finish2\\` 경고 0 / 오류 0
|
||||
- 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "AgentQueuedCommandProjectorTests|AgentCommandQueueTests|ArtifactQualityReviewServiceTests|ArtifactRepairGuideServiceTests|DeckQualityReviewServiceTests|DeckRepairGuideServiceTests|PptxSkillGoldenDeckTests|ExcelSkillDashboardSummaryTests" -p:OutputPath=bin\\verify_loop_doc_finish2_tests\\ -p:IntermediateOutputPath=obj\\verify_loop_doc_finish2_tests\\` 통과 25
|
||||
|
||||
@@ -988,3 +988,11 @@ UI ?붿옄???洹쒕え 由ы뙥?좊쭅 ???꾪뿕 ?묒뾽 ??湲곕줉???덉쟾
|
||||
- 테스트는 [ArtifactQualityReviewServiceTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/ArtifactQualityReviewServiceTests.cs), [ArtifactRepairGuideServiceTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/ArtifactRepairGuideServiceTests.cs), [DeckRepairGuideServiceTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/DeckRepairGuideServiceTests.cs), [DocxSkillTemplateFeaturesTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/DocxSkillTemplateFeaturesTests.cs)를 확장해 회귀를 고정했습니다.
|
||||
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_doc_repair_finalize\\ -p:IntermediateOutputPath=obj\\verify_doc_repair_finalize\\` 경고 0 / 오류 0
|
||||
- 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "ArtifactQualityReviewServiceTests|ArtifactRepairGuideServiceTests|DeckRepairGuideServiceTests|DocxSkillTemplateFeaturesTests" -p:OutputPath=bin\\verify_doc_repair_finalize_tests\\ -p:IntermediateOutputPath=obj\\verify_doc_repair_finalize_tests\\` 통과 11
|
||||
|
||||
업데이트: 2026-04-15 09:24 (KST)
|
||||
- 에이전틱 루프의 queued command 소비 로직을 helper로 분리했습니다. [AgentQueuedCommandProjector.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentQueuedCommandProjector.cs)는 drain된 큐 배치를 `queued_input_interrupt`, `queue_notification`, `queue_resume`, `queued_prompt` 같은 대화 메시지와 thinking/user 이벤트로 투영합니다. [AgentLoopService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentLoopService.cs)는 이 결과를 적용하는 역할만 남겨 루프 본체의 책임을 더 줄였습니다.
|
||||
- workbook/dashboard 품질 리뷰도 강화했습니다. [ArtifactQualityReviewService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/ArtifactQualityReviewService.cs)는 dashboard sheet가 있어도 KPI·trend·decision 내용이 부족하면 별도 이슈를 만들고, [ArtifactRepairGuideService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/ArtifactRepairGuideService.cs)는 이를 `core story`가 보이도록 KPI/decision 블록을 추가하라는 가이드로 연결합니다.
|
||||
- deck 품질 리뷰는 storyline 힌트까지 보기 시작했습니다. [DeckQualityReviewService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/DeckQualityReviewService.cs)는 storyline에 `Options`, `Roadmap`, `Appendix`가 있는데 실제 슬라이드가 빠진 경우 별도 이슈를 만들고, [DeckRepairGuideService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/DeckRepairGuideService.cs)는 이를 storyline 보강 액션으로 바꿉니다.
|
||||
- 테스트는 [AgentQueuedCommandProjectorTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/AgentQueuedCommandProjectorTests.cs), [DeckQualityReviewServiceTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/DeckQualityReviewServiceTests.cs), [ArtifactQualityReviewServiceTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/ArtifactQualityReviewServiceTests.cs), [ArtifactRepairGuideServiceTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/ArtifactRepairGuideServiceTests.cs), [DeckRepairGuideServiceTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/DeckRepairGuideServiceTests.cs)를 확장해 회귀를 고정했습니다.
|
||||
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_loop_doc_finish2\\ -p:IntermediateOutputPath=obj\\verify_loop_doc_finish2\\` 경고 0 / 오류 0
|
||||
- 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "AgentQueuedCommandProjectorTests|AgentCommandQueueTests|ArtifactQualityReviewServiceTests|ArtifactRepairGuideServiceTests|DeckQualityReviewServiceTests|DeckRepairGuideServiceTests|PptxSkillGoldenDeckTests|ExcelSkillDashboardSummaryTests" -p:OutputPath=bin\\verify_loop_doc_finish2_tests\\ -p:IntermediateOutputPath=obj\\verify_loop_doc_finish2_tests\\` 통과 25
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
using AxCopilot.Models;
|
||||
using AxCopilot.Services.Agent;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
namespace AxCopilot.Tests.Services;
|
||||
|
||||
public class AgentQueuedCommandProjectorTests
|
||||
{
|
||||
[Fact]
|
||||
public void Project_ShouldCreateInterruptMessageAndDeferredEvent()
|
||||
{
|
||||
var now = DateTime.Now;
|
||||
var commands = new List<AgentQueuedCommand>
|
||||
{
|
||||
new(1, AgentCommandKind.Steering, AgentQueuePriority.Now, "steer-now", now, true),
|
||||
new(2, AgentCommandKind.Notification, AgentQueuePriority.Now, "later-note", now.AddSeconds(1), false),
|
||||
};
|
||||
|
||||
var projection = AgentQueuedCommandProjector.Project(commands, deferredCount: 2);
|
||||
|
||||
projection.Messages.Should().ContainSingle(message =>
|
||||
message.MetaKind == "queued_input_interrupt" &&
|
||||
message.Role == "system");
|
||||
projection.Messages.Should().Contain(message =>
|
||||
message.MetaKind == "queued_steering" &&
|
||||
message.Role == "user" &&
|
||||
message.Content == "steer-now");
|
||||
projection.Messages.Should().Contain(message =>
|
||||
message.MetaKind == "queue_notification" &&
|
||||
message.Role == "system" &&
|
||||
message.Content == "later-note");
|
||||
projection.Events.Should().Contain(evt =>
|
||||
evt.Type == AgentEventType.Thinking &&
|
||||
evt.Summary.Contains("Deferred 2 lower-priority", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Project_ShouldMapPermissionResumeAndPromptKindsConsistently()
|
||||
{
|
||||
var now = DateTime.Now;
|
||||
var commands = new List<AgentQueuedCommand>
|
||||
{
|
||||
new(1, AgentCommandKind.PermissionContinuation, AgentQueuePriority.Now, "continue-tool", now, true),
|
||||
new(2, AgentCommandKind.Resume, AgentQueuePriority.Now, "resume-run", now.AddSeconds(1), false),
|
||||
new(3, AgentCommandKind.Prompt, AgentQueuePriority.Now, "follow-up", now.AddSeconds(2), false),
|
||||
new(4, AgentCommandKind.UserDecision, AgentQueuePriority.Now, "choose-option-b", now.AddSeconds(3), false),
|
||||
};
|
||||
|
||||
var projection = AgentQueuedCommandProjector.Project(commands, deferredCount: 0);
|
||||
|
||||
projection.Messages.Should().Contain(message =>
|
||||
message.MetaKind == "queue_permission_continuation" &&
|
||||
message.Role == "system");
|
||||
projection.Messages.Should().Contain(message =>
|
||||
message.MetaKind == "queue_resume" &&
|
||||
message.Role == "system");
|
||||
projection.Messages.Should().Contain(message =>
|
||||
message.MetaKind == "queued_prompt" &&
|
||||
message.Role == "user");
|
||||
projection.Messages.Should().Contain(message =>
|
||||
message.MetaKind == "queued_user_decision" &&
|
||||
message.Role == "user");
|
||||
projection.Events.Should().Contain(evt =>
|
||||
evt.Type == AgentEventType.UserMessage &&
|
||||
evt.Summary == "follow-up");
|
||||
projection.Events.Should().Contain(evt =>
|
||||
evt.Type == AgentEventType.Thinking &&
|
||||
evt.Summary == "continue-tool");
|
||||
}
|
||||
}
|
||||
@@ -106,5 +106,6 @@ public class ArtifactQualityReviewServiceTests
|
||||
review.Issues.Should().Contain(issue => issue.Message.Contains("highlights or actions", StringComparison.OrdinalIgnoreCase));
|
||||
review.Issues.Should().Contain(issue => issue.Message.Contains("supporting detail sheets", StringComparison.OrdinalIgnoreCase));
|
||||
review.Issues.Should().Contain(issue => issue.Message.Contains("trend or variance formulas", StringComparison.OrdinalIgnoreCase));
|
||||
review.Issues.Should().Contain(issue => issue.Message.Contains("KPI, trend, or decision content", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ public class ArtifactRepairGuideServiceTests
|
||||
68,
|
||||
["Includes summary sheet"],
|
||||
[
|
||||
new ArtifactReviewIssue("Dashboard sheet lacks KPI, trend, or decision content.", ArtifactReviewSeverity.Info),
|
||||
new ArtifactReviewIssue("Workbook could benefit from a dashboard sheet to summarize multi-sheet trends.", ArtifactReviewSeverity.Info),
|
||||
new ArtifactReviewIssue("Summary sheet does not link to detail sheets.", ArtifactReviewSeverity.Warning)
|
||||
]);
|
||||
@@ -21,6 +22,7 @@ public class ArtifactRepairGuideServiceTests
|
||||
var guide = ArtifactRepairGuideService.BuildGuide(review);
|
||||
|
||||
guide.Should().Contain("dashboard sheet");
|
||||
guide.Should().Contain("core story");
|
||||
guide.Should().Contain("detail sheets");
|
||||
}
|
||||
|
||||
|
||||
@@ -83,4 +83,28 @@ public class DeckQualityReviewServiceTests
|
||||
review.Issues.Should().Contain(issue => issue.Message.Contains("headline is too long"));
|
||||
review.Issues.Should().Contain(issue => issue.Message.Contains("comparison slide needs at least two options"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReviewDeck_ShouldFlagMissingStorylineSlides_WhenHintsAreProvided()
|
||||
{
|
||||
using var slides = JsonDocument.Parse(
|
||||
"""
|
||||
[
|
||||
{ "layout": "title", "title": "PMO Steering" },
|
||||
{ "layout": "executive_summary", "title": "Executive Summary", "headline": "Message", "summary_points": ["A", "B"], "recommendation": "Proceed" },
|
||||
{ "layout": "recommendation", "title": "Recommendation", "recommendation": "Proceed", "summary_points": ["Reason"] }
|
||||
]
|
||||
""");
|
||||
|
||||
var review = DeckQualityReviewService.ReviewDeck(
|
||||
"Storyline Deck",
|
||||
slides.RootElement,
|
||||
hasTemplate: false,
|
||||
autoRepairCount: 0,
|
||||
storyline: ["Executive Summary", "Options", "Roadmap", "Appendix"]);
|
||||
|
||||
review.Issues.Should().Contain(issue => issue.Message.Contains("Storyline expects an options or comparison slide", StringComparison.OrdinalIgnoreCase));
|
||||
review.Issues.Should().Contain(issue => issue.Message.Contains("Storyline expects a roadmap slide", StringComparison.OrdinalIgnoreCase));
|
||||
review.Issues.Should().Contain(issue => issue.Message.Contains("Storyline expects appendix or evidence support", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,4 +53,21 @@ public class DeckRepairGuideServiceTests
|
||||
guide.Should().Contain("appendix");
|
||||
guide.Should().Contain("distinct message");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildGuide_ShouldTranslateStorylineGapIntoConcreteAction()
|
||||
{
|
||||
var report = new DeckQualityReport(
|
||||
63,
|
||||
[],
|
||||
[
|
||||
new DeckReviewIssue("Storyline expects an options or comparison slide but none is present.", DeckReviewSeverity.Warning)
|
||||
],
|
||||
["Executive Summary", "Options"]);
|
||||
|
||||
var guide = DeckRepairGuideService.BuildGuide(report);
|
||||
|
||||
guide.Should().Contain("comparison slide");
|
||||
guide.Should().Contain("storyline");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,90 +97,10 @@ public partial class AgentLoopService
|
||||
if (drained.Count == 0)
|
||||
return;
|
||||
|
||||
var interruptingCommands = drained.Count(x =>
|
||||
x.RequestInterrupt &&
|
||||
x.Kind is AgentCommandKind.Prompt or AgentCommandKind.Steering or AgentCommandKind.UserDecision or AgentCommandKind.PermissionContinuation);
|
||||
if (interruptingCommands > 0)
|
||||
{
|
||||
messages.Add(new ChatMessage
|
||||
{
|
||||
Role = "system",
|
||||
MetaKind = "queued_input_interrupt",
|
||||
Content = $"[queued input] {interruptingCommands} new instruction(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;
|
||||
|
||||
case AgentCommandKind.PermissionContinuation:
|
||||
messages.Add(new ChatMessage
|
||||
{
|
||||
Role = "system",
|
||||
MetaKind = "queue_permission_continuation",
|
||||
Content = item.Content,
|
||||
Timestamp = item.CreatedAt,
|
||||
});
|
||||
EmitEvent(AgentEventType.Thinking, "", item.Content);
|
||||
break;
|
||||
|
||||
case AgentCommandKind.Resume:
|
||||
messages.Add(new ChatMessage
|
||||
{
|
||||
Role = "system",
|
||||
MetaKind = "queue_resume",
|
||||
Content = item.Content,
|
||||
Timestamp = item.CreatedAt,
|
||||
});
|
||||
EmitEvent(AgentEventType.Thinking, "", item.Content);
|
||||
break;
|
||||
|
||||
case AgentCommandKind.Steering:
|
||||
case AgentCommandKind.UserDecision:
|
||||
messages.Add(new ChatMessage
|
||||
{
|
||||
Role = "user",
|
||||
MetaKind = item.Kind == AgentCommandKind.Steering ? "queued_steering" : "queued_user_decision",
|
||||
Content = item.Content,
|
||||
Timestamp = item.CreatedAt,
|
||||
});
|
||||
EmitEvent(AgentEventType.UserMessage, "", 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;
|
||||
}
|
||||
}
|
||||
|
||||
var deferredCount = queuedSnapshot.Count - drained.Count;
|
||||
if (deferredCount > 0)
|
||||
{
|
||||
EmitEvent(
|
||||
AgentEventType.Thinking,
|
||||
"",
|
||||
$"Deferred {deferredCount} lower-priority queued item(s) for a later turn.");
|
||||
}
|
||||
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>
|
||||
|
||||
116
src/AxCopilot/Services/Agent/AgentQueuedCommandProjector.cs
Normal file
116
src/AxCopilot/Services/Agent/AgentQueuedCommandProjector.cs
Normal file
@@ -0,0 +1,116 @@
|
||||
using AxCopilot.Models;
|
||||
|
||||
namespace AxCopilot.Services.Agent;
|
||||
|
||||
internal sealed record AgentQueuedCommandProjectionEvent(
|
||||
AgentEventType Type,
|
||||
string ToolName,
|
||||
string Summary);
|
||||
|
||||
internal sealed record AgentQueuedCommandProjectionResult(
|
||||
IReadOnlyList<ChatMessage> Messages,
|
||||
IReadOnlyList<AgentQueuedCommandProjectionEvent> Events);
|
||||
|
||||
/// <summary>
|
||||
/// 큐에서 꺼낸 명령 배치를 실제 대화 메시지와 이벤트로 투영합니다.
|
||||
/// AgentLoopService 내부 분기 부담을 줄이고 테스트 가능한 형태로 분리합니다.
|
||||
/// </summary>
|
||||
internal static class AgentQueuedCommandProjector
|
||||
{
|
||||
public static AgentQueuedCommandProjectionResult Project(
|
||||
IReadOnlyList<AgentQueuedCommand> drained,
|
||||
int deferredCount)
|
||||
{
|
||||
var messages = new List<ChatMessage>();
|
||||
var events = new List<AgentQueuedCommandProjectionEvent>();
|
||||
if (drained.Count == 0)
|
||||
return new AgentQueuedCommandProjectionResult(messages, events);
|
||||
|
||||
var interruptingCommands = drained.Count(x =>
|
||||
x.RequestInterrupt &&
|
||||
x.Kind is AgentCommandKind.Prompt or AgentCommandKind.Steering or AgentCommandKind.UserDecision or AgentCommandKind.PermissionContinuation);
|
||||
|
||||
if (interruptingCommands > 0)
|
||||
{
|
||||
messages.Add(new ChatMessage
|
||||
{
|
||||
Role = "system",
|
||||
MetaKind = "queued_input_interrupt",
|
||||
Content = $"[queued input] {interruptingCommands} new instruction(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,
|
||||
});
|
||||
events.Add(new AgentQueuedCommandProjectionEvent(AgentEventType.Thinking, "", item.Content));
|
||||
break;
|
||||
|
||||
case AgentCommandKind.PermissionContinuation:
|
||||
messages.Add(new ChatMessage
|
||||
{
|
||||
Role = "system",
|
||||
MetaKind = "queue_permission_continuation",
|
||||
Content = item.Content,
|
||||
Timestamp = item.CreatedAt,
|
||||
});
|
||||
events.Add(new AgentQueuedCommandProjectionEvent(AgentEventType.Thinking, "", item.Content));
|
||||
break;
|
||||
|
||||
case AgentCommandKind.Resume:
|
||||
messages.Add(new ChatMessage
|
||||
{
|
||||
Role = "system",
|
||||
MetaKind = "queue_resume",
|
||||
Content = item.Content,
|
||||
Timestamp = item.CreatedAt,
|
||||
});
|
||||
events.Add(new AgentQueuedCommandProjectionEvent(AgentEventType.Thinking, "", item.Content));
|
||||
break;
|
||||
|
||||
case AgentCommandKind.Steering:
|
||||
case AgentCommandKind.UserDecision:
|
||||
messages.Add(new ChatMessage
|
||||
{
|
||||
Role = "user",
|
||||
MetaKind = item.Kind == AgentCommandKind.Steering ? "queued_steering" : "queued_user_decision",
|
||||
Content = item.Content,
|
||||
Timestamp = item.CreatedAt,
|
||||
});
|
||||
events.Add(new AgentQueuedCommandProjectionEvent(AgentEventType.UserMessage, "", item.Content));
|
||||
break;
|
||||
|
||||
default:
|
||||
messages.Add(new ChatMessage
|
||||
{
|
||||
Role = "user",
|
||||
MetaKind = item.RequestInterrupt ? "queued_prompt_interrupt" : "queued_prompt",
|
||||
Content = item.Content,
|
||||
Timestamp = item.CreatedAt,
|
||||
});
|
||||
events.Add(new AgentQueuedCommandProjectionEvent(AgentEventType.UserMessage, "", item.Content));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (deferredCount > 0)
|
||||
{
|
||||
events.Add(new AgentQueuedCommandProjectionEvent(
|
||||
AgentEventType.Thinking,
|
||||
"",
|
||||
$"Deferred {deferredCount} lower-priority queued item(s) for a later turn."));
|
||||
}
|
||||
|
||||
return new AgentQueuedCommandProjectionResult(messages, events);
|
||||
}
|
||||
}
|
||||
@@ -240,6 +240,8 @@ public static class ArtifactQualityReviewService
|
||||
issues.Add(new("Summary sheet could better surface KPIs, decisions, or highlights.", ArtifactReviewSeverity.Info));
|
||||
if (input.HasSummarySheet && input.DetailSheetCount >= 2 && !input.HasDashboardSheet)
|
||||
issues.Add(new("Workbook could benefit from a dashboard sheet to summarize multi-sheet trends.", ArtifactReviewSeverity.Info));
|
||||
if (input.HasDashboardSheet && !input.HasScorecardSection && !input.HasDecisionSection)
|
||||
issues.Add(new("Dashboard sheet lacks KPI, trend, or decision content.", ArtifactReviewSeverity.Info));
|
||||
if (input.HasDashboardSheet && !input.HasHighlightSection && !input.HasActionSection)
|
||||
issues.Add(new("Dashboard sheet could better call out highlights or actions.", ArtifactReviewSeverity.Info));
|
||||
if (input.HasDashboardSheet && input.DetailSheetCount >= 2 && input.HyperlinkCount == 0)
|
||||
|
||||
@@ -53,6 +53,8 @@ public static class ArtifactRepairGuideService
|
||||
|
||||
private static string? BuildWorkbookAction(string message)
|
||||
{
|
||||
if (message.Contains("Dashboard sheet lacks KPI, trend, or decision content", StringComparison.OrdinalIgnoreCase))
|
||||
return "Add KPI, trend, or decision blocks so the dashboard communicates the core story at a glance";
|
||||
if (message.Contains("dashboard sheet", StringComparison.OrdinalIgnoreCase))
|
||||
return "Add a dashboard sheet with KPI tiles, trends, and links to detail sheets";
|
||||
if (message.Contains("Summary sheet could better surface", StringComparison.OrdinalIgnoreCase))
|
||||
|
||||
@@ -157,6 +157,12 @@ public static class DeckQualityReviewService
|
||||
issues.Add(new("Too many slides are text-heavy.", DeckReviewSeverity.Warning));
|
||||
if (chartSlides + tableSlides + comparisonSlides == 0 && slideCount >= 4)
|
||||
issues.Add(new("Evidence slides such as charts, tables, or comparisons are limited.", DeckReviewSeverity.Warning));
|
||||
if (StorylineExpects(storylineSteps, "option", "options", "comparison", "alternatives") && comparisonSlides == 0)
|
||||
issues.Add(new("Storyline expects an options or comparison slide but none is present.", DeckReviewSeverity.Warning));
|
||||
if (StorylineExpects(storylineSteps, "roadmap", "plan", "execution") && roadmapSlides == 0)
|
||||
issues.Add(new("Storyline expects a roadmap slide but none is present.", DeckReviewSeverity.Warning));
|
||||
if (StorylineExpects(storylineSteps, "appendix", "evidence", "reference", "부록", "참고") && appendixSlides == 0)
|
||||
issues.Add(new("Storyline expects appendix or evidence support but none is present.", DeckReviewSeverity.Info));
|
||||
if (placeholderCount > 0)
|
||||
issues.Add(new($"Found {placeholderCount} placeholder or unfinished marker(s).", DeckReviewSeverity.Critical));
|
||||
var score = 70;
|
||||
@@ -300,5 +306,19 @@ public static class DeckQualityReviewService
|
||||
|
||||
return patterns.Sum(pattern => Regex.Matches(text, pattern, RegexOptions.IgnoreCase).Count);
|
||||
}
|
||||
|
||||
private static bool StorylineExpects(IEnumerable<string> storylineSteps, params string[] keywords)
|
||||
{
|
||||
foreach (var step in storylineSteps)
|
||||
{
|
||||
foreach (var keyword in keywords)
|
||||
{
|
||||
if (step.Contains(keyword, StringComparison.OrdinalIgnoreCase))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,12 @@ public static class DeckRepairGuideService
|
||||
=> "Tighten slide headlines to one clear message sentence",
|
||||
var message when message.Contains("content density is high", StringComparison.OrdinalIgnoreCase)
|
||||
=> "Reduce text density and convert bullets into cards, visuals, or sharper evidence points",
|
||||
var message when message.Contains("Storyline expects an options or comparison slide", StringComparison.OrdinalIgnoreCase)
|
||||
=> "Add an options or comparison slide so the deck matches the intended storyline",
|
||||
var message when message.Contains("Storyline expects a roadmap slide", StringComparison.OrdinalIgnoreCase)
|
||||
=> "Add a roadmap slide that turns the storyline into a delivery sequence",
|
||||
var message when message.Contains("Storyline expects appendix or evidence support", StringComparison.OrdinalIgnoreCase)
|
||||
=> "Add appendix or evidence support slides to back the main storyline",
|
||||
var message when message.Contains("comparison slide", StringComparison.OrdinalIgnoreCase)
|
||||
=> "Expand the comparison slide to show at least two real options and a verdict",
|
||||
var message when message.Contains("chart slide", StringComparison.OrdinalIgnoreCase)
|
||||
|
||||
Reference in New Issue
Block a user