???? ???? ?? ????????? ????? ?? ????? PPT ?? ???? ??
??: - ?? ?? ???? ?? ?? ??, ?? ? ?? ?? ??, ?????? ???? ??? ? ?? ?????. - ?? ?? ??? ??? ?? PPT? ?? ??? ?? ???? ?? ????? ????. ?? ????: - AgentCommandQueue? steering, permission continuation, resume, user decision ? ??? ???? AgentLoopService?? ?? ???? ????? ?? - CodeLanguageCatalog? LspClientService? ??? Go, Rust, PHP, Ruby, Kotlin, Swift? ?? LSP ?? ???? ?? - SettingsWindow? SettingsViewModel?? ?? ? ?? ??? ?? ?? / LSP / ?? ???? ????? ?? - WorkspaceContextGenerator? Language Snapshot, Agent Context, Key Manifests ??? ???? .claude/skills, .ax/rules, AXMEMORY.md ??? ?? - DeckRepairGuideService? ???? PptxSkill ??? Deck repair guide? ?? ?? - ?? ?? ???? ?? ???? ?? ? ?? ??: - dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_master_batch\\ -p:IntermediateOutputPath=obj\\verify_master_batch\\ - dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter AgentCommandQueueTests,CodeLanguageCatalogTests,WorkspaceContextGeneratorTests,PptxSkillConsultingDeckTests,DeckRepairGuideServiceTests -p:OutputPath=bin\\verify_master_batch_tests\\ -p:IntermediateOutputPath=obj\\verify_master_batch_tests\\
This commit is contained in:
@@ -17,6 +17,10 @@ public enum AgentCommandKind
|
||||
{
|
||||
Prompt,
|
||||
Notification,
|
||||
Steering,
|
||||
PermissionContinuation,
|
||||
Resume,
|
||||
UserDecision,
|
||||
}
|
||||
|
||||
public sealed record AgentQueuedCommand(
|
||||
@@ -45,6 +49,18 @@ public sealed class AgentCommandQueue
|
||||
public void EnqueueNotification(string content, string priority = "later")
|
||||
=> Enqueue(AgentCommandKind.Notification, content, priority, requestInterrupt: false);
|
||||
|
||||
public void EnqueueSteering(string content, string priority = "now", bool requestInterrupt = true)
|
||||
=> Enqueue(AgentCommandKind.Steering, content, priority, requestInterrupt);
|
||||
|
||||
public void EnqueuePermissionContinuation(string content, string priority = "now")
|
||||
=> Enqueue(AgentCommandKind.PermissionContinuation, content, priority, requestInterrupt: true);
|
||||
|
||||
public void EnqueueResume(string content, string priority = "now")
|
||||
=> Enqueue(AgentCommandKind.Resume, content, priority, requestInterrupt: false);
|
||||
|
||||
public void EnqueueUserDecision(string content, string priority = "next", bool requestInterrupt = false)
|
||||
=> Enqueue(AgentCommandKind.UserDecision, content, priority, requestInterrupt);
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
while (_now.TryDequeue(out _)) { }
|
||||
|
||||
@@ -49,20 +49,60 @@ public partial class AgentLoopService
|
||||
requestInterrupt: IsRunning);
|
||||
}
|
||||
|
||||
/// <summary>실행 중 사용자 지시 보강 메시지를 우선순위 높게 주입합니다.</summary>
|
||||
public void InjectSteeringMessage(string message, bool requestInterrupt = true)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(message))
|
||||
_pendingCommands.EnqueueSteering(
|
||||
message,
|
||||
IsRunning ? "now" : "next",
|
||||
requestInterrupt: IsRunning && requestInterrupt);
|
||||
}
|
||||
|
||||
/// <summary>권한 승인 후 이어서 진행할 내용을 큐에 넣습니다.</summary>
|
||||
public void EnqueuePermissionContinuation(string toolName, string? target, string decisionSummary)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(decisionSummary))
|
||||
return;
|
||||
|
||||
var targetLabel = string.IsNullOrWhiteSpace(target) ? "" : $" ({target})";
|
||||
_pendingCommands.EnqueuePermissionContinuation(
|
||||
$"[permission continuation] Continue {toolName}{targetLabel}: {decisionSummary}");
|
||||
}
|
||||
|
||||
/// <summary>도구 실행 중 참고용 시스템 알림을 큐에 넣습니다.</summary>
|
||||
public void EnqueueNotification(string message, string priority = "later")
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(message))
|
||||
_pendingCommands.EnqueueNotification(message, priority);
|
||||
}
|
||||
|
||||
/// <summary>사용자 의사결정 결과를 다음 턴 입력으로 주입합니다.</summary>
|
||||
public void EnqueueUserDecision(string message, bool requestInterrupt = false)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(message))
|
||||
_pendingCommands.EnqueueUserDecision(
|
||||
message,
|
||||
IsRunning ? "next" : "now",
|
||||
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)
|
||||
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] {interruptingPrompts} new prompt(s) arrived during execution. Prioritize the newest user direction before continuing.",
|
||||
Content = $"[queued input] {interruptingCommands} new instruction(s) arrived during execution. Prioritize the newest user direction before continuing.",
|
||||
Timestamp = DateTime.Now,
|
||||
});
|
||||
}
|
||||
@@ -82,6 +122,40 @@ public partial class AgentLoopService
|
||||
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
|
||||
{
|
||||
@@ -209,6 +283,8 @@ public partial class AgentLoopService
|
||||
{
|
||||
// 이미 릴리즈된 상태 — 무시
|
||||
}
|
||||
if (IsRunning)
|
||||
_pendingCommands.EnqueueResume("Execution resumed after pause. Re-evaluate the latest queued context before proceeding.");
|
||||
EmitEvent(AgentEventType.Resumed, "", "에이전트가 재개되었습니다");
|
||||
}
|
||||
|
||||
|
||||
55
src/AxCopilot/Services/Agent/DeckRepairGuideService.cs
Normal file
55
src/AxCopilot/Services/Agent/DeckRepairGuideService.cs
Normal file
@@ -0,0 +1,55 @@
|
||||
namespace AxCopilot.Services.Agent;
|
||||
|
||||
public static class DeckRepairGuideService
|
||||
{
|
||||
public static string BuildGuide(DeckQualityReport review)
|
||||
{
|
||||
if (review.Issues.Count == 0)
|
||||
return "Deck repair guide: none";
|
||||
|
||||
var actions = new List<string>();
|
||||
foreach (var issue in review.Issues)
|
||||
{
|
||||
var action = issue.Message switch
|
||||
{
|
||||
var message when message.Contains("Executive Summary", StringComparison.OrdinalIgnoreCase)
|
||||
=> "Add or strengthen the Executive Summary with 2-3 evidence-backed takeaways",
|
||||
var message when message.Contains("Recommendation", StringComparison.OrdinalIgnoreCase)
|
||||
=> "Add a clear recommendation or decision request slide near the end of the deck",
|
||||
var message when message.Contains("roadmap", StringComparison.OrdinalIgnoreCase)
|
||||
=> "Add a roadmap slide with phases, owners, and timing",
|
||||
var message when message.Contains("headline is too long", StringComparison.OrdinalIgnoreCase)
|
||||
=> "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("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)
|
||||
=> "Provide chart labels and values or replace the slide with a comparison/evidence layout",
|
||||
var message when message.Contains("table slide", StringComparison.OrdinalIgnoreCase)
|
||||
=> "Provide complete table headers and rows or simplify the slide to key callouts",
|
||||
var message when message.Contains("text-heavy", StringComparison.OrdinalIgnoreCase)
|
||||
=> "Convert text-heavy slides into message-led visuals with fewer bullets",
|
||||
var message when message.Contains("Evidence slides", StringComparison.OrdinalIgnoreCase)
|
||||
=> "Add evidence slides such as charts, tables, appendix evidence, or structured comparisons",
|
||||
var message when message.Contains("placeholder", StringComparison.OrdinalIgnoreCase)
|
||||
=> "Replace placeholder text before export and verify each slide has final copy",
|
||||
_ => null,
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(action) &&
|
||||
!actions.Contains(action, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
actions.Add(action);
|
||||
}
|
||||
|
||||
if (actions.Count >= 3)
|
||||
break;
|
||||
}
|
||||
|
||||
if (actions.Count == 0)
|
||||
return "Deck repair guide: review slide alerts and reinforce storyline, evidence, and decision ask";
|
||||
|
||||
return "Deck repair guide: " + string.Join(" | ", actions);
|
||||
}
|
||||
}
|
||||
@@ -1029,7 +1029,10 @@ public class PptxSkill : IAgentTool
|
||||
if (!string.IsNullOrWhiteSpace(templatePackName))
|
||||
outputParts.Add($"Template pack: {templatePackName}");
|
||||
if (deckReview != null)
|
||||
{
|
||||
outputParts.Add(deckReview.ToToolSummary());
|
||||
outputParts.Add(DeckRepairGuideService.BuildGuide(deckReview));
|
||||
}
|
||||
return ToolResult.Ok(string.Join("\n", outputParts), fullPath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@@ -84,6 +84,15 @@ internal static class WorkspaceContextGenerator
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
var languageSnapshot = BuildLanguageSnapshot(extDist);
|
||||
if (languageSnapshot.Count > 0)
|
||||
{
|
||||
sb.AppendLine("## Language Snapshot");
|
||||
foreach (var line in languageSnapshot)
|
||||
sb.AppendLine($"- {line}");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
// 4. 기존 컨텍스트 파일 감지
|
||||
var contextFiles = DetectContextFiles(workFolder);
|
||||
if (contextFiles.Count > 0)
|
||||
@@ -95,6 +104,24 @@ internal static class WorkspaceContextGenerator
|
||||
}
|
||||
|
||||
// 5. README 요약
|
||||
var agentContextSummary = DetectAgentContextSummary(workFolder);
|
||||
if (agentContextSummary.Count > 0)
|
||||
{
|
||||
sb.AppendLine("## Agent Context");
|
||||
foreach (var line in agentContextSummary)
|
||||
sb.AppendLine($"- {line}");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
var keyManifests = DetectKeyManifests(workFolder);
|
||||
if (keyManifests.Count > 0)
|
||||
{
|
||||
sb.AppendLine("## Key Manifests");
|
||||
foreach (var line in keyManifests)
|
||||
sb.AppendLine($"- {line}");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
var readmeSummary = ExtractReadmeSummary(workFolder);
|
||||
if (readmeSummary != null)
|
||||
{
|
||||
@@ -328,6 +355,91 @@ internal static class WorkspaceContextGenerator
|
||||
return files;
|
||||
}
|
||||
|
||||
private static List<string> DetectAgentContextSummary(string folder)
|
||||
{
|
||||
var lines = new List<string>();
|
||||
|
||||
try
|
||||
{
|
||||
var claudeSkillsDir = Path.Combine(folder, ".claude", "skills");
|
||||
if (Directory.Exists(claudeSkillsDir))
|
||||
{
|
||||
var skillFiles = Directory.GetFiles(claudeSkillsDir, "SKILL.md", SearchOption.AllDirectories);
|
||||
if (skillFiles.Length > 0)
|
||||
lines.Add($".claude/skills 호환 스킬 {skillFiles.Length}개 감지");
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
try
|
||||
{
|
||||
var axRulesDir = Path.Combine(folder, ".ax", "rules");
|
||||
if (Directory.Exists(axRulesDir))
|
||||
{
|
||||
var ruleFiles = Directory.GetFiles(axRulesDir, "*.md", SearchOption.TopDirectoryOnly);
|
||||
if (ruleFiles.Length > 0)
|
||||
lines.Add($".ax/rules 규칙 {ruleFiles.Length}개 감지");
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
try
|
||||
{
|
||||
var memoryFile = Path.Combine(folder, "AXMEMORY.md");
|
||||
if (File.Exists(memoryFile))
|
||||
lines.Add("AXMEMORY.md 메모리 파일 감지");
|
||||
}
|
||||
catch { }
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
private static List<string> DetectKeyManifests(string folder)
|
||||
{
|
||||
var manifests = new List<string>();
|
||||
var patterns = new (string Pattern, string Label)[]
|
||||
{
|
||||
("*.sln", "Solution"),
|
||||
("*.csproj", ".NET project"),
|
||||
("package.json", "Node package"),
|
||||
("pyproject.toml", "Python project"),
|
||||
("requirements.txt", "Python requirements"),
|
||||
("Cargo.toml", "Rust package"),
|
||||
("go.mod", "Go module"),
|
||||
("pom.xml", "Maven project"),
|
||||
("build.gradle", "Gradle build"),
|
||||
};
|
||||
|
||||
foreach (var (pattern, label) in patterns)
|
||||
{
|
||||
try
|
||||
{
|
||||
var matches = Directory.GetFiles(folder, pattern, SearchOption.TopDirectoryOnly)
|
||||
.Select(Path.GetFileName)
|
||||
.Where(name => !string.IsNullOrWhiteSpace(name))
|
||||
.Cast<string>()
|
||||
.Take(3)
|
||||
.ToList();
|
||||
if (matches.Count > 0)
|
||||
manifests.Add($"{label}: {string.Join(", ", matches)}");
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
return manifests;
|
||||
}
|
||||
|
||||
private static List<string> BuildLanguageSnapshot(List<KeyValuePair<string, int>> extDist)
|
||||
=> extDist
|
||||
.Where(kv => kv.Value > 0)
|
||||
.Select(kv => new { Language = GetLanguageName(kv.Key), kv.Value })
|
||||
.GroupBy(x => x.Language, StringComparer.OrdinalIgnoreCase)
|
||||
.Select(group => new { Language = group.Key, Count = group.Sum(item => item.Value) })
|
||||
.OrderByDescending(x => x.Count)
|
||||
.Take(6)
|
||||
.Select(x => $"{x.Language}: {x.Count} file(s)")
|
||||
.ToList();
|
||||
|
||||
private static async Task<(string? Branch, string? Remote)> GetGitInfoAsync(
|
||||
string folder, CancellationToken ct)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user