??? ???? ???? ??? ?? ???? ???? ??? ? ?????? ?? ?? ??
- WPF/MVVM, ASP.NET API, React/Vue/Next, Node, Python API, Spring, Android, Go, Rust CLI, generic solution ??? ProjectScaffold? ???? ???? ????? ??? - Code ?? empty workspace ??? ??? ????? ??? ?? ??? file_write? ?? ???? ??? ????? file_manage/file_write? ?? ?? ???? ???? ??? - AgentLoop ?? ? ProjectLayoutGate? ??? ??? ?? ??? ?? ??? ??? ????? ?? ??? ? ????? ??? - code-scaffold.skill.md? when_to_use? file_manage/file_edit ?????? ??? proactive auto-skill ???? ??? - IntentGate, scaffold profile, code quality ?? ???? ??? ?? ?? - dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_project_scaffold_layout\\ -p:IntermediateOutputPath=obj\\verify_project_scaffold_layout\\ ?? 0 / ?? 0 - dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "IntentGateServiceTests|ProjectScaffoldProfileCatalogTests|SkillServiceRuntimePolicyTests|AgentLoopCodeQualityTests" -p:OutputPath=bin\\verify_project_scaffold_layout_tests\\ -p:IntermediateOutputPath=obj\\verify_project_scaffold_layout_tests\\ ?? 183
This commit is contained in:
12
README.md
12
README.md
@@ -2326,3 +2326,15 @@ MIT License
|
||||
- 검증:
|
||||
- `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_build_failure_recovery\\ -p:IntermediateOutputPath=obj\\verify_build_failure_recovery\\` 경고 0 / 오류 0
|
||||
- `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "AgentLoopCodeQualityTests" -p:OutputPath=bin\\verify_build_failure_recovery_tests\\ -p:IntermediateOutputPath=obj\\verify_build_failure_recovery_tests\\` 통과 134
|
||||
업데이트: 2026-04-15 23:44 (KST)
|
||||
- Code 탭의 구조형 프로젝트 생성 흐름을 프로젝트 스캐폴드 모드로 승격했습니다. `src/AxCopilot/Services/Agent/ProjectScaffoldProfileCatalog.cs`를 추가해 WPF/MVVM, ASP.NET API, React/Vue/Next, Node, Python API, Spring, Android, Go, Rust CLI, generic solution 요청을 감지하고 최소 폴더 구조/대표 파일 경로를 프로파일 단위로 제안합니다.
|
||||
- `src/AxCopilot/Services/Agent/IntentGateService.cs`, `src/AxCopilot/Services/Agent/AgentLoopExplorationPolicy.cs`, `src/AxCopilot/Services/Agent/AgentLoopRunLifecycle.cs`, `src/AxCopilot/Services/Agent/AgentLoopCodeRuntimeGuards.cs`, `src/AxCopilot/Views/ChatWindow.SystemPromptBuilder.cs`를 함께 조정해 빈 작업 폴더에서 `index.html/app.js/style.css` 같은 평면 예시 대신 `file_manage(mkdir)`와 중첩 `file_write`로 최소 프로젝트 트리를 먼저 만들도록 유도합니다.
|
||||
- `src/AxCopilot/Services/Agent/AgentLoopTransitions.Verification.cs`에는 프로젝트 레이아웃 게이트를 추가했습니다. 초기 워크스페이스가 비어 있었고 구조형 스캐폴드 요청이 감지된 경우, 루트에 구현 파일이 평면으로 쌓인 채 종료하려 하면 `Views/ViewModels/Models/Services/Themes`, `src/components/pages/services`, `app/api/models/services`, `src/main/java/resources/test` 같은 프레임워크별 기본 분리를 다시 요구합니다.
|
||||
- `src/AxCopilot/skills/code-scaffold.skill.md`는 `when_to_use`와 `file_manage/file_edit` 메타데이터를 추가해 proactive auto-skill 대상으로 승격했습니다. 이제 새 프로젝트/프레임워크 앱 생성에서는 기존 코드 탐색보다 폴더 트리 설계와 스캐폴딩 규칙이 먼저 주입됩니다.
|
||||
- 테스트:
|
||||
- `src/AxCopilot.Tests/Services/IntentGateServiceTests.cs`
|
||||
- `src/AxCopilot.Tests/Services/ProjectScaffoldProfileCatalogTests.cs`
|
||||
- `src/AxCopilot.Tests/Services/AgentLoopCodeQualityTests.cs`
|
||||
- 검증:
|
||||
- `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_project_scaffold_layout\\ -p:IntermediateOutputPath=obj\\verify_project_scaffold_layout\\` 경고 0 / 오류 0
|
||||
- `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "IntentGateServiceTests|ProjectScaffoldProfileCatalogTests|SkillServiceRuntimePolicyTests|AgentLoopCodeQualityTests" -p:OutputPath=bin\\verify_project_scaffold_layout_tests\\ -p:IntermediateOutputPath=obj\\verify_project_scaffold_layout_tests\\` 통과 183
|
||||
|
||||
@@ -1657,3 +1657,41 @@ UI ?遺우쁽????域뱀뮆???귐뗫솯?醫딆춦 ???袁る퓮 ?臾믩씜 ??疫
|
||||
- 검증:
|
||||
- `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_build_failure_recovery\\ -p:IntermediateOutputPath=obj\\verify_build_failure_recovery\\` 경고 0 / 오류 0
|
||||
- `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "AgentLoopCodeQualityTests" -p:OutputPath=bin\\verify_build_failure_recovery_tests\\ -p:IntermediateOutputPath=obj\\verify_build_failure_recovery_tests\\` 통과 134
|
||||
업데이트: 2026-04-15 23:44 (KST)
|
||||
- 구조형 프로젝트 스캐폴드 정책을 추가했다. `src/AxCopilot/Services/Agent/ProjectScaffoldProfileCatalog.cs`
|
||||
- WPF/MVVM, ASP.NET API, React/Vue/Next, Node backend, Python API, Spring Boot, Android/Kotlin, Go service, Rust CLI, generic solution 프로파일을 정의한다.
|
||||
- 프로파일마다 트리거 토큰, 기대 폴더, 대표 시작 파일, 루트 허용 파일, 최소 디렉터리 적중 기준을 관리한다.
|
||||
- `AssessLayout(...)`로 초기 빈 워크스페이스에서 생성된 결과가 평면 루트 파일 위주인지 검사한다.
|
||||
- 인텐트/탐색 흐름을 프로젝트 스캐폴드 중심으로 재구성했다.
|
||||
- `src/AxCopilot/Services/Agent/IntentGateService.cs`
|
||||
- Code 탭에서 구조형 프로젝트 요청을 `ExplorationScope.ProjectScaffold`로 분류한다.
|
||||
- WPF, XAML, csproj, MVVM, ViewModel, ResourceDictionary뿐 아니라 React, Python API, Spring, Android, Go, Rust 같은 다른 언어/개발 유형도 같은 기준으로 감지한다.
|
||||
- `src/AxCopilot/Services/Agent/AgentLoopExplorationPolicy.cs`
|
||||
- `ProjectScaffold` scope를 추가하고 `file_manage/file_write/file_edit`를 우선 노출한다.
|
||||
- 빈 워크스페이스의 구조형 요청은 broad 탐색 대신 최소 트리 생성 후 구현 파일 배치를 기본 순서로 안내한다.
|
||||
- `src/AxCopilot/Services/Agent/AgentLoopRunLifecycle.cs`
|
||||
- 탐색 상태 초기화 시 `ProjectScaffoldProfileCatalog.Detect(...)` 결과를 붙여 run 단위 스캐폴드 메타를 유지한다.
|
||||
- `src/AxCopilot/Services/Agent/AgentLoopCodeRuntimeGuards.cs`
|
||||
- empty workspace 복구/초기 가이드를 `file_write 즉시 호출`에서 `single file direct create / multi-file scaffold tree-first` 규칙으로 확장했다.
|
||||
- `src/AxCopilot/Views/ChatWindow.SystemPromptBuilder.cs`
|
||||
- Code 시스템 프롬프트에 `file_manage`를 실제 available tools로 반영하고, 프레임워크/멀티파일 스캐폴드는 트리 생성부터 시작하라고 명시했다.
|
||||
- 코드 품질 게이트에 프로젝트 레이아웃 검사를 추가했다.
|
||||
- `src/AxCopilot/Services/Agent/AgentLoopTransitions.Execution.cs`
|
||||
- `RunState.ProjectLayoutGateRetry`를 추가했다.
|
||||
- `src/AxCopilot/Services/Agent/AgentLoopTransitions.Verification.cs`
|
||||
- 구조형 스캐폴드 요청이었고 초기 워크스페이스가 비어 있었던 경우, 루트에 구현 파일이 평면으로 남아 있으면 종료 전 `ProjectLayoutGate`를 발동한다.
|
||||
- `file_manage(mkdir/move)`와 `file_edit/file_write`로 폴더 재배치를 먼저 수행하도록 요구한다.
|
||||
- 스킬 메타데이터도 함께 보강했다.
|
||||
- `src/AxCopilot/skills/code-scaffold.skill.md`
|
||||
- `when_to_use`를 추가해 auto-skill 후보로 포함되게 했다.
|
||||
- `file_manage`, `file_edit`를 허용 도구에 추가하고, 빈 작업 폴더에서는 최소 폴더 트리부터 설계하도록 명시했다.
|
||||
- 테스트:
|
||||
- `src/AxCopilot.Tests/Services/IntentGateServiceTests.cs`
|
||||
- WPF/React 구조형 요청이 `ProjectScaffold`로 분류되는지 확인한다.
|
||||
- `src/AxCopilot.Tests/Services/ProjectScaffoldProfileCatalogTests.cs`
|
||||
- WPF/FastAPI 프로파일 감지와 flat-root vs structured-layout 평가를 검증한다.
|
||||
- `src/AxCopilot.Tests/Services/AgentLoopCodeQualityTests.cs`
|
||||
- `BuildProjectLayoutGatePrompt(...)`가 폴더 재배치 지시를 포함하는지 검증한다.
|
||||
- 검증:
|
||||
- `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_project_scaffold_layout\\ -p:IntermediateOutputPath=obj\\verify_project_scaffold_layout\\` 경고 0 / 오류 0
|
||||
- `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "IntentGateServiceTests|ProjectScaffoldProfileCatalogTests|SkillServiceRuntimePolicyTests|AgentLoopCodeQualityTests" -p:OutputPath=bin\\verify_project_scaffold_layout_tests\\ -p:IntermediateOutputPath=obj\\verify_project_scaffold_layout_tests\\` 통과 183
|
||||
|
||||
@@ -258,6 +258,31 @@ public class AgentLoopCodeQualityTests
|
||||
prompt.Should().NotContain("git_tool(diff) -> targeted tool retry");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildProjectLayoutGatePrompt_CallsForFolderReorganization()
|
||||
{
|
||||
var profile = ProjectScaffoldProfileCatalog.Detect(
|
||||
"Create a C# WPF MVVM desktop app with ResourceDictionary themes",
|
||||
"Code")!;
|
||||
var assessment = new ProjectScaffoldLayoutAssessment(
|
||||
IsSatisfied: false,
|
||||
MatchedDirectoryCount: 0,
|
||||
ExistingDirectories: [],
|
||||
MissingDirectories: ["Views", "ViewModels", "Themes"],
|
||||
SuspiciousRootFiles: ["MainWindow.xaml", "MainWindowViewModel.cs"]);
|
||||
|
||||
var prompt = InvokePrivateStatic<string>(
|
||||
"BuildProjectLayoutGatePrompt",
|
||||
profile,
|
||||
assessment);
|
||||
|
||||
prompt.Should().Contain("[System:ProjectLayoutGate]");
|
||||
prompt.Should().Contain("Views");
|
||||
prompt.Should().Contain("ViewModels");
|
||||
prompt.Should().Contain("MainWindow.xaml");
|
||||
prompt.Should().Contain("file_manage(mkdir/move)");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildFailureNextToolPriorityPrompt_IncludesOrderedPriority()
|
||||
{
|
||||
|
||||
@@ -135,6 +135,22 @@ public class IntentGateServiceTests
|
||||
result.SuggestedScope.Should().Be(ExplorationScope.Localized);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ClassifyAsync_WpfProjectQuery_SuggestsProjectScaffold()
|
||||
{
|
||||
var result = await _sut.ClassifyAsync("Create a new C# WPF MVVM project with ResourceDictionary themes", "Code");
|
||||
|
||||
result.SuggestedScope.Should().Be(ExplorationScope.ProjectScaffold);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ClassifyAsync_ReactProjectQuery_SuggestsProjectScaffold()
|
||||
{
|
||||
var result = await _sut.ClassifyAsync("Generate a React TypeScript frontend starter with pages and services", "Code");
|
||||
|
||||
result.SuggestedScope.Should().Be(ExplorationScope.ProjectScaffold);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
// 복합 요청 감지 (P5)
|
||||
// ═══════════════════════════════════════════
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
using AxCopilot.Services.Agent;
|
||||
using FluentAssertions;
|
||||
using System.IO;
|
||||
using Xunit;
|
||||
|
||||
namespace AxCopilot.Tests.Services;
|
||||
|
||||
public class ProjectScaffoldProfileCatalogTests
|
||||
{
|
||||
[Fact]
|
||||
public void Detect_ReturnsWpfProfile_ForWpfQuery()
|
||||
{
|
||||
var profile = ProjectScaffoldProfileCatalog.Detect(
|
||||
"Create a C# WPF MVVM desktop app with ResourceDictionary themes",
|
||||
"Code");
|
||||
|
||||
profile.Should().NotBeNull();
|
||||
profile!.Key.Should().Be("wpf-mvvm");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Detect_ReturnsPythonServiceProfile_ForFastApiQuery()
|
||||
{
|
||||
var profile = ProjectScaffoldProfileCatalog.Detect(
|
||||
"Generate a Python FastAPI backend service scaffold",
|
||||
"Code");
|
||||
|
||||
profile.Should().NotBeNull();
|
||||
profile!.Key.Should().Be("python-service");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AssessLayout_Fails_WhenStructuredProjectFilesAreFlatInRoot()
|
||||
{
|
||||
var tempDir = Path.Combine(Path.GetTempPath(), "axcopilot-scaffold-flat-" + Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(tempDir);
|
||||
|
||||
try
|
||||
{
|
||||
File.WriteAllText(Path.Combine(tempDir, "MainWindow.xaml"), "<Window />");
|
||||
File.WriteAllText(Path.Combine(tempDir, "MainWindowViewModel.cs"), "class MainWindowViewModel {}");
|
||||
|
||||
var profile = ProjectScaffoldProfileCatalog.Detect(
|
||||
"Create a C# WPF MVVM desktop app with ResourceDictionary themes",
|
||||
"Code")!;
|
||||
var assessment = ProjectScaffoldProfileCatalog.AssessLayout(profile, tempDir);
|
||||
|
||||
assessment.IsSatisfied.Should().BeFalse();
|
||||
assessment.SuspiciousRootFiles.Should().Contain("MainWindow.xaml");
|
||||
assessment.SuspiciousRootFiles.Should().Contain("MainWindowViewModel.cs");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempDir, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AssessLayout_Passes_WhenExpectedDirectoriesExist()
|
||||
{
|
||||
var tempDir = Path.Combine(Path.GetTempPath(), "axcopilot-scaffold-structured-" + Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(tempDir);
|
||||
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(Path.Combine(tempDir, "Views"));
|
||||
Directory.CreateDirectory(Path.Combine(tempDir, "ViewModels"));
|
||||
Directory.CreateDirectory(Path.Combine(tempDir, "Themes"));
|
||||
File.WriteAllText(Path.Combine(tempDir, "App.xaml"), "<Application />");
|
||||
|
||||
var profile = ProjectScaffoldProfileCatalog.Detect(
|
||||
"Create a C# WPF MVVM desktop app with ResourceDictionary themes",
|
||||
"Code")!;
|
||||
var assessment = ProjectScaffoldProfileCatalog.AssessLayout(profile, tempDir);
|
||||
|
||||
assessment.IsSatisfied.Should().BeTrue();
|
||||
assessment.MatchedDirectoryCount.Should().BeGreaterThanOrEqualTo(2);
|
||||
assessment.SuspiciousRootFiles.Should().BeEmpty();
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempDir, recursive: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -66,7 +66,7 @@ public partial class AgentLoopService
|
||||
|
||||
var blockedMessage = isExternalEscalation
|
||||
? $"Empty workspace guard blocked external path search: {target}"
|
||||
: $"Empty workspace guard blocked detour tool '{toolName}'. Use file_write directly.";
|
||||
: $"Empty workspace guard blocked detour tool '{toolName}'. Use file_manage/file_write directly.";
|
||||
|
||||
EmitEvent(AgentEventType.Error, toolName, blockedMessage);
|
||||
EmitEvent(
|
||||
@@ -81,18 +81,34 @@ public partial class AgentLoopService
|
||||
messages.Add(new ChatMessage
|
||||
{
|
||||
Role = "user",
|
||||
Content = BuildEmptyWorkspaceCreationRecoveryPrompt(activeToolNames)
|
||||
Content = BuildEmptyWorkspaceCreationRecoveryPrompt(activeToolNames, explorationState: null, context.InitialUserQuery)
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
private static string BuildEmptyWorkspaceCreationRecoveryPrompt(IReadOnlyCollection<string> activeToolNames)
|
||||
private static string BuildEmptyWorkspaceCreationRecoveryPrompt(
|
||||
IReadOnlyCollection<string> activeToolNames,
|
||||
ExplorationTrackingState? explorationState,
|
||||
string? userQuery)
|
||||
{
|
||||
var activeToolPreview = AgentLoopNoToolResponseRecoveryService.BuildActiveToolPreview(activeToolNames);
|
||||
var scaffoldProfile = explorationState?.ScaffoldProfile ?? ProjectScaffoldProfileCatalog.Detect(userQuery, "Code");
|
||||
if (scaffoldProfile != null)
|
||||
{
|
||||
var layoutPreview = ProjectScaffoldProfileCatalog.BuildDirectoryPreview(scaffoldProfile);
|
||||
var starterPreview = ProjectScaffoldProfileCatalog.BuildStarterPathPreview(scaffoldProfile);
|
||||
return "[System:EmptyWorkspaceCreation] The current work folder is empty and the request is a structured project scaffold. " +
|
||||
"Do not search C:\\, other drive roots, or repeat folder_map, grep, glob, file_read, env_tool, skill_manager, or mcp tools. " +
|
||||
$"First establish a minimal folder layout such as {layoutPreview}, then create representative files directly in those paths. " +
|
||||
$"Use file_manage(mkdir) and file_write with relative paths like {starterPreview}. " +
|
||||
"Do not flatten implementation files in the workspace root, and do not only describe the plan. Emit the actual tool calls now. " +
|
||||
$"Available tools: {activeToolPreview}";
|
||||
}
|
||||
|
||||
return "[System:EmptyWorkspaceCreation] The current work folder is empty. " +
|
||||
"Do not search C:\\, other drive roots, or repeat folder_map, grep, glob, file_read, env_tool, skill_manager, or mcp tools. " +
|
||||
"Call file_write immediately using a relative path in the current work folder and create the needed file directly. " +
|
||||
"For example: index.html, app.js, or style.css. " +
|
||||
"If the user asked for a multi-file app or framework scaffold, create a minimal project tree first instead of flattening files in the root. " +
|
||||
"Do not only describe the plan. Emit the actual tool call now. " +
|
||||
$"Available tools: {activeToolPreview}";
|
||||
}
|
||||
@@ -108,7 +124,7 @@ public partial class AgentLoopService
|
||||
if (!runState.WorkspaceAppearsEmpty)
|
||||
return;
|
||||
|
||||
if (explorationState.Scope != ExplorationScope.DirectCreation)
|
||||
if (explorationState.Scope is not (ExplorationScope.DirectCreation or ExplorationScope.ProjectScaffold))
|
||||
return;
|
||||
|
||||
runState.EmptyWorkspaceGuardTriggered = true;
|
||||
@@ -116,10 +132,7 @@ public partial class AgentLoopService
|
||||
messages.Add(new ChatMessage
|
||||
{
|
||||
Role = "system",
|
||||
Content =
|
||||
"[System:EmptyWorkspaceStart] The current work folder is empty and the user asked to create a new artifact. " +
|
||||
"Skip broad exploration. Do not call folder_map, glob, grep, file_read, env_tool, skill_manager, mcp_list_resources, or mcp_read_resource unless the user explicitly asks to inspect the workspace. " +
|
||||
"Call file_write immediately with a relative path inside the current work folder and create the requested file directly."
|
||||
Content = BuildInitialEmptyWorkspaceGuidance(explorationState, contextHint: null)
|
||||
});
|
||||
|
||||
EmitEvent(
|
||||
@@ -128,6 +141,28 @@ public partial class AgentLoopService
|
||||
"빈 작업 폴더 감지 · 탐색 없이 새 파일 생성을 시작합니다");
|
||||
}
|
||||
|
||||
private static string BuildInitialEmptyWorkspaceGuidance(
|
||||
ExplorationTrackingState explorationState,
|
||||
string? contextHint)
|
||||
{
|
||||
var scaffoldProfile = explorationState.ScaffoldProfile
|
||||
?? ProjectScaffoldProfileCatalog.Detect(contextHint, "Code");
|
||||
if (explorationState.Scope == ExplorationScope.ProjectScaffold && scaffoldProfile != null)
|
||||
{
|
||||
var layoutPreview = ProjectScaffoldProfileCatalog.BuildDirectoryPreview(scaffoldProfile);
|
||||
var starterPreview = ProjectScaffoldProfileCatalog.BuildStarterPathPreview(scaffoldProfile);
|
||||
return "[System:EmptyWorkspaceStart] The current work folder is empty and the user asked for a structured project scaffold. " +
|
||||
"Skip broad exploration. Do not call folder_map, glob, grep, file_read, env_tool, skill_manager, mcp_list_resources, or mcp_read_resource unless the user explicitly asks to inspect the workspace. " +
|
||||
$"Create a minimal folder layout such as {layoutPreview} first, then place code into relative paths like {starterPreview}. " +
|
||||
"Use file_manage(mkdir) and nested file_write/file_edit calls, and do not dump implementation files into the workspace root.";
|
||||
}
|
||||
|
||||
return "[System:EmptyWorkspaceStart] The current work folder is empty and the user asked to create a new artifact. " +
|
||||
"Skip broad exploration. Do not call folder_map, glob, grep, file_read, env_tool, skill_manager, mcp_list_resources, or mcp_read_resource unless the user explicitly asks to inspect the workspace. " +
|
||||
"Call file_write immediately with a relative path inside the current work folder and create the requested file directly. " +
|
||||
"If the request is a framework or multi-file scaffold, create a minimal project tree first instead of flattening files in the root.";
|
||||
}
|
||||
|
||||
private static bool IsExternalWorkspaceEscalationTarget(string? target, string? workFolder)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(target) || string.IsNullOrWhiteSpace(workFolder))
|
||||
|
||||
@@ -11,7 +11,7 @@ public partial class AgentLoopService
|
||||
TopicBased,
|
||||
RepoWide,
|
||||
OpenEnded,
|
||||
/// <summary>문서 생성 요청 — 탐색 단계를 건너뛰고 바로 document_plan/생성 도구 사용.</summary>
|
||||
ProjectScaffold,
|
||||
DirectCreation,
|
||||
}
|
||||
|
||||
@@ -24,8 +24,8 @@ public partial class AgentLoopService
|
||||
public bool BroadScanDetected { get; set; }
|
||||
public bool SelectiveHit { get; set; }
|
||||
public bool CorrectiveHintInjected { get; set; }
|
||||
/// <summary>스킬 런타임이 allowed-tools를 명시했으면 true — 탐색 필터링을 건너뜀.</summary>
|
||||
public bool SkillAllowedToolsActive { get; set; }
|
||||
public ProjectScaffoldProfile? ScaffoldProfile { get; set; }
|
||||
}
|
||||
|
||||
private static IReadOnlyCollection<IAgentTool> FilterExplorationToolsForCurrentIteration(
|
||||
@@ -38,21 +38,37 @@ public partial class AgentLoopService
|
||||
if (tools.Count == 0)
|
||||
return tools;
|
||||
|
||||
// 스킬 런타임 정책으로 allowed-tools가 명시된 경우 탐색 필터링을 건너뜀
|
||||
// — 스킬이 의도적으로 허용한 도구(folder_map 등)를 정책이 차단하면 안 됨
|
||||
if (state.SkillAllowedToolsActive)
|
||||
return tools;
|
||||
|
||||
// 문서 생성 모드: 생성 도구를 최우선, 탐색 도구를 뒤로 배치
|
||||
if (state.Scope == ExplorationScope.ProjectScaffold)
|
||||
{
|
||||
var scaffoldFirst = new List<IAgentTool>(tools.Count);
|
||||
scaffoldFirst.AddRange(tools.Where(IsProjectScaffoldTool));
|
||||
scaffoldFirst.AddRange(tools.Where(t =>
|
||||
!IsProjectScaffoldTool(t)
|
||||
&& !string.Equals(t.Name, "folder_map", StringComparison.OrdinalIgnoreCase)
|
||||
&& !string.Equals(t.Name, "glob", StringComparison.OrdinalIgnoreCase)
|
||||
&& !string.Equals(t.Name, "grep", StringComparison.OrdinalIgnoreCase)));
|
||||
scaffoldFirst.AddRange(tools.Where(t =>
|
||||
string.Equals(t.Name, "glob", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(t.Name, "grep", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(t.Name, "folder_map", StringComparison.OrdinalIgnoreCase)));
|
||||
return scaffoldFirst
|
||||
.DistinctBy(t => t.Name, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList()
|
||||
.AsReadOnly();
|
||||
}
|
||||
|
||||
if (state.Scope == ExplorationScope.DirectCreation)
|
||||
{
|
||||
var creationFirst = new List<IAgentTool>(tools.Count);
|
||||
creationFirst.AddRange(tools.Where(IsDocumentCreationTool));
|
||||
creationFirst.AddRange(tools.Where(t => !IsDocumentCreationTool(t)
|
||||
creationFirst.AddRange(tools.Where(t =>
|
||||
!IsDocumentCreationTool(t)
|
||||
&& !string.Equals(t.Name, "folder_map", StringComparison.OrdinalIgnoreCase)
|
||||
&& !string.Equals(t.Name, "glob", StringComparison.OrdinalIgnoreCase)
|
||||
&& !string.Equals(t.Name, "grep", StringComparison.OrdinalIgnoreCase)));
|
||||
// 탐색 도구는 마지막에 (필요 시에만 사용)
|
||||
creationFirst.AddRange(tools.Where(t =>
|
||||
string.Equals(t.Name, "glob", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(t.Name, "grep", StringComparison.OrdinalIgnoreCase)));
|
||||
@@ -66,12 +82,11 @@ public partial class AgentLoopService
|
||||
var ordered = new List<IAgentTool>(tools.Count);
|
||||
|
||||
ordered.AddRange(tools.Where(IsSelectiveDiscoveryTool));
|
||||
ordered.AddRange(tools.Where(t => !IsSelectiveDiscoveryTool(t) &&
|
||||
(allowFolderMap || !string.Equals(t.Name, "folder_map", StringComparison.OrdinalIgnoreCase))));
|
||||
ordered.AddRange(tools.Where(t =>
|
||||
!IsSelectiveDiscoveryTool(t)
|
||||
&& (allowFolderMap || !string.Equals(t.Name, "folder_map", StringComparison.OrdinalIgnoreCase))));
|
||||
if (allowFolderMap)
|
||||
{
|
||||
ordered.AddRange(tools.Where(t => string.Equals(t.Name, "folder_map", StringComparison.OrdinalIgnoreCase)));
|
||||
}
|
||||
|
||||
return ordered
|
||||
.DistinctBy(t => t.Name, StringComparer.OrdinalIgnoreCase)
|
||||
@@ -86,14 +101,19 @@ public partial class AgentLoopService
|
||||
or "document_assemble" or "file_write";
|
||||
}
|
||||
|
||||
private static bool IsProjectScaffoldTool(IAgentTool tool)
|
||||
{
|
||||
return tool.Name is "file_manage" or "file_write" or "file_edit" or "file_read"
|
||||
or "build_run" or "test_loop" or "dev_env_detect" or "process";
|
||||
}
|
||||
|
||||
private static bool ShouldAllowFolderMapForCurrentIteration(
|
||||
ExplorationTrackingState state,
|
||||
string userQuery,
|
||||
string? activeTab,
|
||||
int totalToolCalls)
|
||||
{
|
||||
// 문서 생성 모드에서는 folder_map 차단 — 탐색 없이 바로 생성
|
||||
if (state.Scope == ExplorationScope.DirectCreation)
|
||||
if (state.Scope is ExplorationScope.DirectCreation or ExplorationScope.ProjectScaffold)
|
||||
return false;
|
||||
|
||||
if (state.Scope is ExplorationScope.RepoWide or ExplorationScope.OpenEnded)
|
||||
@@ -116,23 +136,8 @@ public partial class AgentLoopService
|
||||
|
||||
return ContainsAny(
|
||||
userQuery,
|
||||
"folder",
|
||||
"directory",
|
||||
"tree",
|
||||
"structure",
|
||||
"list files",
|
||||
"workspace layout",
|
||||
"work folder",
|
||||
"project structure",
|
||||
"폴더",
|
||||
"디렉터리",
|
||||
"폴더 구조",
|
||||
"디렉터리 구조",
|
||||
"파일 목록",
|
||||
"작업 폴더",
|
||||
"폴더 안",
|
||||
"구조를 보여",
|
||||
"구조 확인");
|
||||
"folder", "directory", "tree", "structure", "list files", "workspace layout", "work folder", "project structure",
|
||||
"폴더", "디렉터리", "폴더 구조", "디렉터리 구조", "파일 목록", "작업 폴더", "구조를 보여", "구조 확인");
|
||||
}
|
||||
|
||||
private static bool IsExistingMaterialReferenceRequest(string userQuery)
|
||||
@@ -142,21 +147,8 @@ public partial class AgentLoopService
|
||||
|
||||
return ContainsAny(
|
||||
userQuery,
|
||||
"existing file",
|
||||
"existing files",
|
||||
"existing document",
|
||||
"existing documents",
|
||||
"reference files",
|
||||
"reference docs",
|
||||
"existing materials",
|
||||
"기존 파일",
|
||||
"기존 문서",
|
||||
"기존 자료",
|
||||
"참고 파일",
|
||||
"참고 문서",
|
||||
"폴더 내 자료",
|
||||
"안의 자료",
|
||||
"작업 폴더 파일");
|
||||
"existing file", "existing files", "existing document", "existing documents", "reference files", "reference docs", "existing materials",
|
||||
"기존 파일", "기존 문서", "기존 자료", "참고 파일", "참고 문서", "작업 폴더 파일");
|
||||
}
|
||||
|
||||
private static bool IsSelectiveDiscoveryTool(IAgentTool tool)
|
||||
@@ -171,27 +163,9 @@ public partial class AgentLoopService
|
||||
|
||||
return ContainsAny(
|
||||
userQuery,
|
||||
"definition",
|
||||
"reference",
|
||||
"references",
|
||||
"implementation",
|
||||
"implementations",
|
||||
"caller",
|
||||
"callers",
|
||||
"callee",
|
||||
"call hierarchy",
|
||||
"symbol",
|
||||
"symbols",
|
||||
"interface",
|
||||
"override",
|
||||
"정의",
|
||||
"참조",
|
||||
"구현",
|
||||
"호출부",
|
||||
"호출 관계",
|
||||
"심볼",
|
||||
"인터페이스",
|
||||
"오버라이드");
|
||||
"definition", "reference", "references", "implementation", "implementations",
|
||||
"caller", "callers", "callee", "call hierarchy", "symbol", "symbols", "interface", "override",
|
||||
"정의", "참조", "구현", "호출부", "호출 관계", "심볼", "인터페이스", "오버라이드");
|
||||
}
|
||||
|
||||
private static string BuildPreferredInitialToolSequence(
|
||||
@@ -200,7 +174,14 @@ public partial class AgentLoopService
|
||||
string? activeTab,
|
||||
string userQuery)
|
||||
{
|
||||
// 문서 생성 의도가 감지되면 탐색 없이 바로 문서 계획/생성 도구 사용
|
||||
if (state.Scope == ExplorationScope.ProjectScaffold)
|
||||
{
|
||||
var layoutPreview = state.ScaffoldProfile == null
|
||||
? "src, tests, config"
|
||||
: ProjectScaffoldProfileCatalog.BuildDirectoryPreview(state.ScaffoldProfile);
|
||||
return $"file_manage(mkdir)/file_write with a minimal project tree ({layoutPreview}) -> file_read/file_edit -> build_run/test_loop as needed";
|
||||
}
|
||||
|
||||
if (state.Scope == ExplorationScope.DirectCreation)
|
||||
{
|
||||
if (string.Equals(activeTab, "Code", StringComparison.OrdinalIgnoreCase))
|
||||
@@ -233,31 +214,28 @@ public partial class AgentLoopService
|
||||
if (string.IsNullOrWhiteSpace(userQuery))
|
||||
return ExplorationScope.OpenEnded;
|
||||
|
||||
var q = userQuery.Trim();
|
||||
var lower = q.ToLowerInvariant();
|
||||
var lower = userQuery.Trim().ToLowerInvariant();
|
||||
|
||||
// Cowork 탭에서 문서 생성 의도가 있으면 탐색을 건너뛰고 바로 생성 도구 사용
|
||||
if (!string.Equals(activeTab, "Code", StringComparison.OrdinalIgnoreCase)
|
||||
&& HasDocumentCreationIntent(lower))
|
||||
return ExplorationScope.DirectCreation;
|
||||
|
||||
if (string.Equals(activeTab, "Code", StringComparison.OrdinalIgnoreCase)
|
||||
&& ProjectScaffoldProfileCatalog.IsStructuredProjectRequest(lower, activeTab))
|
||||
return ExplorationScope.ProjectScaffold;
|
||||
|
||||
if (string.Equals(activeTab, "Code", StringComparison.OrdinalIgnoreCase)
|
||||
&& HasCodeArtifactCreationIntent(lower))
|
||||
return ExplorationScope.DirectCreation;
|
||||
|
||||
if (lower.Contains("전체") || lower.Contains("전반") || lower.Contains("코드베이스 전체") ||
|
||||
lower.Contains("repo-wide") || lower.Contains("repository-wide") || lower.Contains("전체 구조") ||
|
||||
lower.Contains("아키텍처") || lower.Contains("전체 점검"))
|
||||
if (ContainsAny(lower, "전체", "전반", "코드베이스 전체", "repo-wide", "repository-wide", "전체 구조", "아키텍처", "전체 맥락"))
|
||||
return ExplorationScope.RepoWide;
|
||||
|
||||
if (q.Contains('.') || q.Contains('/') || q.Contains('\\') ||
|
||||
lower.Contains("file ") || lower.Contains("class ") || lower.Contains("method ") ||
|
||||
lower.Contains("function ") || lower.Contains("line ") || lower.Contains("bug") ||
|
||||
lower.Contains("오류") || lower.Contains("버그") || lower.Contains("예외"))
|
||||
if (lower.Contains('.') || lower.Contains('/') || lower.Contains('\\') ||
|
||||
ContainsAny(lower, "file ", "class ", "method ", "function ", "line ", "bug", "오류", "버그", "예외"))
|
||||
return ExplorationScope.Localized;
|
||||
|
||||
if (lower.Contains("정리") || lower.Contains("요약") || lower.Contains("보고서") ||
|
||||
lower.Contains("주제") || lower.Contains("관련") || lower.Contains("분석"))
|
||||
if (ContainsAny(lower, "정리", "요약", "보고", "주제", "관련", "분석"))
|
||||
return ExplorationScope.TopicBased;
|
||||
|
||||
return string.Equals(activeTab, "Code", StringComparison.OrdinalIgnoreCase)
|
||||
@@ -265,38 +243,30 @@ public partial class AgentLoopService
|
||||
: ExplorationScope.OpenEnded;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 사용자가 문서를 새로 생성/작성하려는 의도인지 판별합니다.
|
||||
/// "보고서 작성해줘", "문서 만들어줘" 등 생성 동사가 포함된 경우 true.
|
||||
/// </summary>
|
||||
private static bool HasDocumentCreationIntent(string lowerQuery)
|
||||
{
|
||||
// 생성 동사 키워드
|
||||
var hasCreationVerb = ContainsAny(lowerQuery,
|
||||
"작성해", "써줘", "써 줘", "만들어", "생성해", "작성 해",
|
||||
"만들어줘", "만들어 줘", "생성해줘", "생성해 줘",
|
||||
"write", "create", "draft", "generate", "compose",
|
||||
"작성하", "작성을", "생성하", "생성을",
|
||||
"리포트 써", "보고서 써", "문서 써",
|
||||
"작성 부탁", "만들어 부탁");
|
||||
var hasCreationVerb = ContainsAny(
|
||||
lowerQuery,
|
||||
"작성", "써줘", "만들", "생성", "만들어줘", "생성해줘",
|
||||
"write", "create", "draft", "generate", "compose");
|
||||
|
||||
if (!hasCreationVerb)
|
||||
return false;
|
||||
|
||||
// 생성 대상이 문서/보고서/자료 등인지 확인
|
||||
return ContainsAny(lowerQuery,
|
||||
"보고서", "문서", "제안서", "리포트", "분석서", "기획서",
|
||||
"report", "document", "proposal", "analysis",
|
||||
"요약서", "발표자료", "ppt", "pptx", "docx", "xlsx", "excel", "word",
|
||||
"표", "차트", "스프레드시트", "프레젠테이션",
|
||||
"정리해", "정리 해");
|
||||
return ContainsAny(
|
||||
lowerQuery,
|
||||
"보고서", "문서", "제안서", "리포트", "분석서", "기획서", "요약", "발표자료", "ppt", "pptx", "docx", "xlsx", "excel", "word", "sheet", "chart",
|
||||
"report", "document", "proposal", "analysis", "presentation", "template", "spreadsheet", "excel", "memo");
|
||||
}
|
||||
|
||||
private static bool HasCodeArtifactCreationIntent(string lowerQuery)
|
||||
{
|
||||
if (ProjectScaffoldProfileCatalog.IsStructuredProjectRequest(lowerQuery, "Code"))
|
||||
return true;
|
||||
|
||||
var hasCreationVerb = ContainsAny(
|
||||
lowerQuery,
|
||||
"만들", "생성", "작성", "create", "generate", "build", "write", "scaffold", "draft");
|
||||
"만들", "생성", "작성", "구현", "create", "generate", "build", "write", "scaffold", "draft");
|
||||
|
||||
if (!hasCreationVerb)
|
||||
return false;
|
||||
@@ -310,35 +280,52 @@ public partial class AgentLoopService
|
||||
"template", "템플릿", "index.html", "app.js", "style.css");
|
||||
}
|
||||
|
||||
private static void InjectExplorationScopeGuidance(List<ChatMessage> messages, ExplorationScope scope)
|
||||
private static void InjectExplorationScopeGuidance(List<ChatMessage> messages, ExplorationTrackingState state)
|
||||
{
|
||||
var scope = state.Scope;
|
||||
var guidance = scope switch
|
||||
{
|
||||
ExplorationScope.ProjectScaffold =>
|
||||
"Exploration scope = project-scaffold. The user wants a framework or multi-file project scaffold. " +
|
||||
"Create a minimal project tree first with file_manage(mkdir) or nested file_write paths, then place source files into the proper folders. " +
|
||||
"Do not flatten implementation files in the workspace root, and do not waste turns on folder_map/glob/grep unless the user explicitly asked to inspect the workspace.",
|
||||
ExplorationScope.DirectCreation =>
|
||||
"Exploration scope = direct-creation. The user wants to CREATE a new document/report/file. " +
|
||||
"Do NOT search for existing files with glob/grep/folder_map — skip exploration entirely. " +
|
||||
"Call document_plan first to outline the document structure, then immediately call the appropriate creation tool " +
|
||||
"(docx_create, html_create, excel_create, markdown_create, etc.) to produce the actual file. " +
|
||||
"The output MUST be a real file on disk, not a text response.",
|
||||
"Exploration scope = direct-creation. The user wants to create a new document or file. " +
|
||||
"Do not search for existing files with glob/grep/folder_map unless the user explicitly asked to inspect the workspace. " +
|
||||
"Produce a real file on disk instead of only describing the result.",
|
||||
ExplorationScope.Localized =>
|
||||
"Exploration scope = localized. Start with lsp_code_intel when the request is about definitions/references/implementations/call hierarchy; otherwise use targeted file_read or grep/glob. Avoid folder_map unless the user explicitly asks for folder structure or file listing.",
|
||||
"Exploration scope = localized. Start with lsp_code_intel for definitions, references, and implementations; otherwise use targeted file_read or grep/glob. Avoid folder_map unless the user explicitly asked for folder structure.",
|
||||
ExplorationScope.TopicBased =>
|
||||
"Exploration scope = topic-based. Identify candidate files by topic keywords first with glob/grep, then read only a small targeted set.",
|
||||
ExplorationScope.RepoWide =>
|
||||
"Exploration scope = repo-wide. Broad structure inspection is allowed when needed.",
|
||||
_ =>
|
||||
"Exploration scope = open-ended. Expand gradually. Prefer selective discovery before broad scans."
|
||||
"Exploration scope = open-ended. Expand gradually and prefer selective discovery before broad scans."
|
||||
};
|
||||
|
||||
if (scope == ExplorationScope.DirectCreation)
|
||||
if (scope == ExplorationScope.ProjectScaffold)
|
||||
{
|
||||
var layoutPreview = state.ScaffoldProfile == null
|
||||
? "src, tests, config"
|
||||
: ProjectScaffoldProfileCatalog.BuildDirectoryPreview(state.ScaffoldProfile);
|
||||
var starterPreview = state.ScaffoldProfile == null
|
||||
? "'src/main.ext', 'tests/main.spec.ext'"
|
||||
: ProjectScaffoldProfileCatalog.BuildStarterPathPreview(state.ScaffoldProfile);
|
||||
guidance =
|
||||
"Exploration scope = project-scaffold. The user wants to create a structured project. " +
|
||||
$"Establish a minimal folder layout such as {layoutPreview} before filling in implementation files. " +
|
||||
$"Use relative scaffold paths like {starterPreview}. " +
|
||||
"Prefer file_manage(mkdir) and nested file_write/file_edit calls. Do not dump every generated file into the workspace root.";
|
||||
}
|
||||
else if (scope == ExplorationScope.DirectCreation)
|
||||
{
|
||||
guidance =
|
||||
"Exploration scope = direct-creation. The user wants to CREATE a new file or document. " +
|
||||
"Do NOT search for existing files with glob/grep/folder_map unless the user explicitly asked to inspect the workspace. " +
|
||||
"Exploration scope = direct-creation. The user wants to create a new file or document. " +
|
||||
"Do not search for existing files with glob/grep/folder_map unless the user explicitly asked to inspect the workspace. " +
|
||||
"If you are in the Code tab, call file_write immediately with a relative path inside the current work folder, then use file_edit/build_run/test_loop only if needed. " +
|
||||
"If you are in Cowork and the user asked for a presentation, deck, slide pack, or PPT, call pptx_create directly unless the user explicitly asked for a plan or outline. " +
|
||||
"Use document_plan first only when you are drafting a multi-section written document or when structure planning materially improves the result. " +
|
||||
"The output MUST be a real file on disk, not a text response.";
|
||||
"The output must be a real file on disk, not a text response.";
|
||||
}
|
||||
|
||||
messages.Add(new ChatMessage
|
||||
@@ -353,8 +340,8 @@ public partial class AgentLoopService
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(argsJson);
|
||||
if (doc.RootElement.TryGetProperty("paths", out var pathsEl) && pathsEl.ValueKind == JsonValueKind.Array)
|
||||
return pathsEl.GetArrayLength();
|
||||
if (doc.RootElement.TryGetProperty("paths", out var pathsElement) && pathsElement.ValueKind == JsonValueKind.Array)
|
||||
return pathsElement.GetArrayLength();
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -368,13 +355,12 @@ public partial class AgentLoopService
|
||||
string toolName,
|
||||
string argsJson)
|
||||
{
|
||||
// 문서 생성 모드: 탐색 도구가 1회라도 호출되면 즉시 교정
|
||||
if (state.Scope == ExplorationScope.DirectCreation)
|
||||
if (state.Scope is ExplorationScope.DirectCreation or ExplorationScope.ProjectScaffold)
|
||||
{
|
||||
return string.Equals(toolName, "folder_map", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(toolName, "glob", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(toolName, "grep", StringComparison.OrdinalIgnoreCase)
|
||||
|| state.TotalFilesRead >= 1;
|
||||
|| (state.Scope == ExplorationScope.ProjectScaffold && state.TotalFilesRead >= 3);
|
||||
}
|
||||
|
||||
if (state.Scope is ExplorationScope.RepoWide or ExplorationScope.OpenEnded)
|
||||
@@ -391,11 +377,11 @@ public partial class AgentLoopService
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(argsJson);
|
||||
var includeFiles = doc.RootElement.TryGetProperty("include_files", out var includeFilesEl) &&
|
||||
includeFilesEl.ValueKind is JsonValueKind.True or JsonValueKind.False &&
|
||||
includeFilesEl.GetBoolean();
|
||||
var depth = doc.RootElement.TryGetProperty("depth", out var depthEl) && depthEl.ValueKind == JsonValueKind.Number
|
||||
? depthEl.GetInt32()
|
||||
var includeFiles = doc.RootElement.TryGetProperty("include_files", out var includeFilesElement)
|
||||
&& includeFilesElement.ValueKind is JsonValueKind.True or JsonValueKind.False
|
||||
&& includeFilesElement.GetBoolean();
|
||||
var depth = doc.RootElement.TryGetProperty("depth", out var depthElement) && depthElement.ValueKind == JsonValueKind.Number
|
||||
? depthElement.GetInt32()
|
||||
: 2;
|
||||
if (includeFiles || depth >= 3)
|
||||
return true;
|
||||
@@ -429,8 +415,8 @@ public partial class AgentLoopService
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.Equals(toolName, "file_read", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(toolName, "document_read", StringComparison.OrdinalIgnoreCase))
|
||||
if (string.Equals(toolName, "file_read", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(toolName, "document_read", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
state.TotalFilesRead++;
|
||||
}
|
||||
|
||||
@@ -60,10 +60,12 @@ public partial class AgentLoopService
|
||||
{
|
||||
var intentGate = new IntentGateService(_llm);
|
||||
var intentResult = await intentGate.ClassifyAsync(userQuery, ActiveTab, ct).ConfigureAwait(false);
|
||||
var scaffoldProfile = ProjectScaffoldProfileCatalog.Detect(userQuery, ActiveTab);
|
||||
var explorationState = new ExplorationTrackingState
|
||||
{
|
||||
Scope = intentResult.SuggestedScope,
|
||||
Scope = scaffoldProfile == null ? intentResult.SuggestedScope : ExplorationScope.ProjectScaffold,
|
||||
SelectiveHit = true,
|
||||
ScaffoldProfile = scaffoldProfile,
|
||||
};
|
||||
var pathAccessState = new PathAccessTrackingState();
|
||||
var sessionLearnings = (_settings.Settings.Llm.EnableSessionLearnings)
|
||||
@@ -79,7 +81,7 @@ public partial class AgentLoopService
|
||||
maxRetry = ComputeQualityAwareMaxRetry(maxRetry, recentTaskRetryQuality, taskPolicy.TaskType);
|
||||
|
||||
InjectTaskTypeGuidance(messages, taskPolicy);
|
||||
InjectExplorationScopeGuidance(messages, explorationState.Scope);
|
||||
InjectExplorationScopeGuidance(messages, explorationState);
|
||||
|
||||
if (intentResult.IsComplexTask && !string.IsNullOrWhiteSpace(intentResult.DecompositionHint))
|
||||
{
|
||||
|
||||
@@ -298,6 +298,7 @@ public partial class AgentLoopService
|
||||
var context = BuildContext();
|
||||
context.InitialUserQuery = userQuery;
|
||||
runState.WorkspaceAppearsEmpty = DetectEmptyWorkspace(context.WorkFolder);
|
||||
var workspaceWasInitiallyEmpty = runState.WorkspaceAppearsEmpty;
|
||||
|
||||
var preferredInitialToolSequence = BuildPreferredInitialToolSequence(
|
||||
explorationState,
|
||||
@@ -309,6 +310,7 @@ public partial class AgentLoopService
|
||||
"",
|
||||
explorationState.Scope switch
|
||||
{
|
||||
ExplorationScope.ProjectScaffold => "프로젝트 스캐폴드 모드 · 최소 폴더 구조부터 만드는 중",
|
||||
ExplorationScope.DirectCreation => string.Equals(ActiveTab, "Code", StringComparison.OrdinalIgnoreCase)
|
||||
? "즉시 생성 모드 · 바로 파일을 만드는 중"
|
||||
: "문서 생성 모드 · 바로 문서를 만드는 중",
|
||||
@@ -1042,6 +1044,9 @@ public partial class AgentLoopService
|
||||
taskPolicy,
|
||||
requireHighImpactCodeVerification,
|
||||
totalToolCalls,
|
||||
context,
|
||||
explorationState,
|
||||
workspaceWasInitiallyEmpty,
|
||||
runState,
|
||||
executionPolicy))
|
||||
continue;
|
||||
|
||||
@@ -1238,6 +1238,7 @@ public partial class AgentLoopService
|
||||
public int ExecutionSuccessGateRetry;
|
||||
public int HighImpactBuildTestGateRetry;
|
||||
public int CodeVerificationGateRetry;
|
||||
public int ProjectLayoutGateRetry;
|
||||
public int FinalReportGateRetry;
|
||||
public int TransientLlmErrorRetries;
|
||||
public int DocumentArtifactGateRetry;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using AxCopilot.Models;
|
||||
using AxCopilot.Models;
|
||||
|
||||
namespace AxCopilot.Services.Agent;
|
||||
|
||||
@@ -28,8 +28,8 @@ public partial class AgentLoopService
|
||||
taskPolicy)
|
||||
});
|
||||
EmitEvent(AgentEventType.Thinking, "", highImpactCodeChange
|
||||
? "고영향 코드 변경으로 분류돼 참조 검증과 build/test 검증을 더 엄격하게 이어갑니다."
|
||||
: "코드 변경 후 build/test/diff 검증을 이어갑니다.");
|
||||
? "怨좎쁺??肄붾뱶 蹂寃쎌쑝濡?遺꾨쪟??李몄“ 寃利앷낵 build/test 寃利앹쓣 ???꾧꺽?섍쾶 ?댁뼱媛묐땲??"
|
||||
: "肄붾뱶 蹂寃???build/test/diff 寃利앹쓣 ?댁뼱媛묐땲??");
|
||||
}
|
||||
else if (HasCodeVerificationEvidenceAfterLastModification(messages, requireHighImpactCodeVerification))
|
||||
{
|
||||
@@ -43,12 +43,24 @@ public partial class AgentLoopService
|
||||
TaskTypePolicy taskPolicy,
|
||||
bool requireHighImpactCodeVerification,
|
||||
int totalToolCalls,
|
||||
AgentContext context,
|
||||
ExplorationTrackingState explorationState,
|
||||
bool workspaceWasInitiallyEmpty,
|
||||
RunState runState,
|
||||
ModelExecutionProfileCatalog.ExecutionPolicy executionPolicy)
|
||||
{
|
||||
if (!string.Equals(ActiveTab, "Code", StringComparison.OrdinalIgnoreCase) || totalToolCalls <= 0)
|
||||
return false;
|
||||
|
||||
if (TryApplyProjectLayoutGateTransition(
|
||||
messages,
|
||||
textResponse,
|
||||
context,
|
||||
explorationState,
|
||||
workspaceWasInitiallyEmpty,
|
||||
runState))
|
||||
return true;
|
||||
|
||||
var hasCodeVerificationEvidence = HasCodeVerificationEvidenceAfterLastModification(
|
||||
messages,
|
||||
requireHighImpactCodeVerification);
|
||||
@@ -69,12 +81,12 @@ public partial class AgentLoopService
|
||||
{
|
||||
Role = "user",
|
||||
Content = requireHighImpactCodeVerification
|
||||
? "[System:CodeQualityGate] 공용/핵심 코드 변경 이후 검증 근거가 부족합니다. 종료하지 말고 file_read, grep/glob, git diff, build/test까지 확인한 뒤에만 마무리하세요."
|
||||
: "[System:CodeQualityGate] 마지막 코드 수정 이후 build/test/file_read/diff 근거가 부족합니다. 종료하지 말고 검증 근거를 보강한 뒤에만 마무리하세요."
|
||||
? "[System:CodeQualityGate] 怨듭슜/?듭떖 肄붾뱶 蹂寃??댄썑 寃利?洹쇨굅媛 遺議깊빀?덈떎. 醫낅즺?섏? 留먭퀬 file_read, grep/glob, git diff, build/test源뚯? ?뺤씤???ㅼ뿉留?留덈Т由ы븯?몄슂."
|
||||
: "[System:CodeQualityGate] 留덉?留?肄붾뱶 ?섏젙 ?댄썑 build/test/file_read/diff 洹쇨굅媛 遺議깊빀?덈떎. 醫낅즺?섏? 留먭퀬 寃利?洹쇨굅瑜?蹂닿컯???ㅼ뿉留?留덈Т由ы븯?몄슂."
|
||||
});
|
||||
EmitEvent(AgentEventType.Thinking, "", requireHighImpactCodeVerification
|
||||
? "핵심 코드 변경의 검증 근거가 부족해 추가 검증을 진행합니다..."
|
||||
: "코드 결과 검증 근거가 부족해 추가 검증을 진행합니다...");
|
||||
? "?듭떖 肄붾뱶 蹂寃쎌쓽 寃利?洹쇨굅媛 遺議깊빐 異붽? 寃利앹쓣 吏꾪뻾?⑸땲??.."
|
||||
: "肄붾뱶 寃곌낵 寃利?洹쇨굅媛 遺議깊빐 異붽? 寃利앹쓣 吏꾪뻾?⑸땲??..");
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -89,9 +101,9 @@ public partial class AgentLoopService
|
||||
messages.Add(new ChatMessage
|
||||
{
|
||||
Role = "user",
|
||||
Content = "[System:HighImpactBuildTestGate] 핵심 코드 변경입니다. 종료하지 말고 build_run과 test_loop를 모두 실행해 성공 근거를 확보한 뒤에만 마무리하세요."
|
||||
Content = "[System:HighImpactBuildTestGate] ?듭떖 肄붾뱶 蹂寃쎌엯?덈떎. 醫낅즺?섏? 留먭퀬 build_run怨?test_loop瑜?紐⑤몢 ?ㅽ뻾???깃났 洹쇨굅瑜??뺣낫???ㅼ뿉留?留덈Т由ы븯?몄슂."
|
||||
});
|
||||
EmitEvent(AgentEventType.Thinking, "", "핵심 변경이라 build+test 성공 근거를 모두 확보할 때까지 진행합니다...");
|
||||
EmitEvent(AgentEventType.Thinking, "", "?듭떖 蹂寃쎌씠??build+test ?깃났 洹쇨굅瑜?紐⑤몢 ?뺣낫???뚭퉴吏 吏꾪뻾?⑸땲??..");
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -115,13 +127,92 @@ public partial class AgentLoopService
|
||||
Role = "user",
|
||||
Content = BuildFinalReportQualityPrompt(taskPolicy, requireHighImpactCodeVerification)
|
||||
});
|
||||
EmitEvent(AgentEventType.Thinking, "", "최종 보고에 변경·검증·리스크 요약이 부족해 한 번 더 정리합니다...");
|
||||
EmitEvent(AgentEventType.Thinking, "", "理쒖쥌 蹂닿퀬??蹂寃승룰?利씲룸━?ㅽ겕 ?붿빟??遺議깊빐 ??踰????뺣━?⑸땲??..");
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool TryApplyProjectLayoutGateTransition(
|
||||
List<ChatMessage> messages,
|
||||
string? textResponse,
|
||||
AgentContext context,
|
||||
ExplorationTrackingState explorationState,
|
||||
bool workspaceWasInitiallyEmpty,
|
||||
RunState runState)
|
||||
{
|
||||
if (!string.Equals(ActiveTab, "Code", StringComparison.OrdinalIgnoreCase))
|
||||
return false;
|
||||
|
||||
var scaffoldProfile = explorationState.ScaffoldProfile;
|
||||
if (!workspaceWasInitiallyEmpty || scaffoldProfile == null)
|
||||
return false;
|
||||
|
||||
if (runState.ProjectLayoutGateRetry >= 1)
|
||||
return false;
|
||||
|
||||
if (!HasProjectScaffoldModificationEvidence(messages))
|
||||
return false;
|
||||
|
||||
var assessment = ProjectScaffoldProfileCatalog.AssessLayout(scaffoldProfile, context.WorkFolder);
|
||||
if (assessment.IsSatisfied)
|
||||
return false;
|
||||
|
||||
runState.ProjectLayoutGateRetry++;
|
||||
if (!string.IsNullOrEmpty(textResponse))
|
||||
messages.Add(new ChatMessage { Role = "assistant", Content = textResponse });
|
||||
|
||||
messages.Add(new ChatMessage
|
||||
{
|
||||
Role = "user",
|
||||
Content = BuildProjectLayoutGatePrompt(scaffoldProfile, assessment)
|
||||
});
|
||||
EmitEvent(
|
||||
AgentEventType.Thinking,
|
||||
"",
|
||||
$"?꾨줈?앺듃 援ъ“媛 ?됰㈃?곸쑝濡??앹꽦???대뜑 ?덉씠?꾩썐??癒쇱? ?뺣━?⑸땲??({runState.ProjectLayoutGateRetry}/1)");
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool HasProjectScaffoldModificationEvidence(List<ChatMessage> messages)
|
||||
{
|
||||
foreach (var message in messages)
|
||||
{
|
||||
if (!TryGetToolResultToolName(message, out var toolName))
|
||||
continue;
|
||||
|
||||
if (toolName is "file_write" or "file_edit" or "file_manage" or "script_create")
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string BuildProjectLayoutGatePrompt(
|
||||
ProjectScaffoldProfile profile,
|
||||
ProjectScaffoldLayoutAssessment assessment)
|
||||
{
|
||||
var missingDirectories = assessment.MissingDirectories.Count == 0
|
||||
? "none"
|
||||
: string.Join(", ", assessment.MissingDirectories.Take(6));
|
||||
var suspiciousRootFiles = assessment.SuspiciousRootFiles.Count == 0
|
||||
? "none"
|
||||
: string.Join(", ", assessment.SuspiciousRootFiles.Take(8));
|
||||
var allowedRootFiles = profile.AllowedRootFiles.Count == 0
|
||||
? "only manifest and entry files"
|
||||
: string.Join(", ", profile.AllowedRootFiles);
|
||||
|
||||
return "[System:ProjectLayoutGate] This looks like a structured scaffold request for "
|
||||
+ $"{profile.Label}, but the current workspace layout is still too flat. "
|
||||
+ $"Create or complete folders such as {ProjectScaffoldProfileCatalog.BuildDirectoryPreview(profile)}, "
|
||||
+ $"especially the missing ones: {missingDirectories}. "
|
||||
+ $"Move implementation files that are still sitting in the workspace root into the proper folders: {suspiciousRootFiles}. "
|
||||
+ $"Keep only appropriate root files such as {allowedRootFiles}. "
|
||||
+ "Use file_manage(mkdir/move) plus file_edit/file_write to reorganize the scaffold before finishing, "
|
||||
+ "then rerun build/test if relevant and only after that summarize the result.";
|
||||
}
|
||||
|
||||
private bool TryApplyCodeDiffEvidenceGateTransition(
|
||||
List<ChatMessage> messages,
|
||||
string? textResponse,
|
||||
@@ -147,9 +238,9 @@ public partial class AgentLoopService
|
||||
messages.Add(new ChatMessage
|
||||
{
|
||||
Role = "user",
|
||||
Content = "[System:CodeDiffGate] 코드 변경 이후 diff 근거가 부족합니다. git_tool 도구로 변경 파일과 핵심 diff를 먼저 확인하고 요약하세요. 지금 즉시 git_tool 도구를 호출하세요."
|
||||
Content = "[System:CodeDiffGate] 肄붾뱶 蹂寃??댄썑 diff 洹쇨굅媛 遺議깊빀?덈떎. git_tool ?꾧뎄濡?蹂寃??뚯씪怨??듭떖 diff瑜?癒쇱? ?뺤씤?섍퀬 ?붿빟?섏꽭?? 吏湲?利됱떆 git_tool ?꾧뎄瑜??몄텧?섏꽭??"
|
||||
});
|
||||
EmitEvent(AgentEventType.Thinking, "", "코드 diff 근거가 부족해 git diff 검증을 추가합니다...");
|
||||
EmitEvent(AgentEventType.Thinking, "", "肄붾뱶 diff 洹쇨굅媛 遺議깊빐 git diff 寃利앹쓣 異붽??⑸땲??..");
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -186,7 +277,7 @@ public partial class AgentLoopService
|
||||
Role = "user",
|
||||
Content = BuildRecentExecutionEvidencePrompt(taskPolicy)
|
||||
});
|
||||
EmitEvent(AgentEventType.Thinking, "", "최근 수정 이후 실행 근거가 부족해 build/test 재검증을 수행합니다...");
|
||||
EmitEvent(AgentEventType.Thinking, "", "理쒓렐 ?섏젙 ?댄썑 ?ㅽ뻾 洹쇨굅媛 遺議깊빐 build/test ?ш?利앹쓣 ?섑뻾?⑸땲??..");
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -223,7 +314,7 @@ public partial class AgentLoopService
|
||||
Role = "user",
|
||||
Content = BuildExecutionSuccessGatePrompt(taskPolicy)
|
||||
});
|
||||
EmitEvent(AgentEventType.Thinking, "", "실패한 실행 근거만 있어 build/test 성공 결과를 다시 검증합니다...");
|
||||
EmitEvent(AgentEventType.Thinking, "", "?ㅽ뙣???ㅽ뻾 洹쇨굅留??덉뼱 build/test ?깃났 寃곌낵瑜??ㅼ떆 寃利앺빀?덈떎...");
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -263,7 +354,7 @@ public partial class AgentLoopService
|
||||
Role = "user",
|
||||
Content = BuildTerminalEvidenceGatePrompt(taskPolicy, lastArtifactFilePath)
|
||||
});
|
||||
EmitEvent(AgentEventType.Thinking, "", $"종료 전 실행 증거가 부족해 보강 단계를 진행합니다 ({runState.TerminalEvidenceGateRetry}/{retryMax})");
|
||||
EmitEvent(AgentEventType.Thinking, "", $"醫낅즺 ???ㅽ뻾 利앷굅媛 遺議깊빐 蹂닿컯 ?④퀎瑜?吏꾪뻾?⑸땲??({runState.TerminalEvidenceGateRetry}/{retryMax})");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,126 +1,117 @@
|
||||
using AxCopilot.Services;
|
||||
using static AxCopilot.Services.Agent.AgentLoopService;
|
||||
|
||||
namespace AxCopilot.Services.Agent;
|
||||
|
||||
/// <summary>
|
||||
/// 사용자 입력을 분석하여 최적 실행 프로파일을 결정하는 2단계 의도 분류기.
|
||||
/// Stage 1: 키워드 기반 빠른 분류 (ClassifyTaskType + IntentDetector 통합)
|
||||
/// Stage 2: (taskType, intentCategory) 조합별 ExecutionPolicyOverlay 매핑
|
||||
/// Stage 3: (선택적) LLM 1-shot 분류 — confidence가 낮을 때만 발동
|
||||
/// </summary>
|
||||
internal sealed class IntentGateService
|
||||
{
|
||||
private readonly ILlmService? _llm;
|
||||
|
||||
/// <summary>DetectComplexTask에서 매번 재생성 방지용 정적 배열.</summary>
|
||||
private static readonly string[] Conjunctions =
|
||||
{
|
||||
"그리고", "하고", "다음에", "이후에", "그런 다음",
|
||||
" and then ", " after that ", " also ", " additionally "
|
||||
};
|
||||
[
|
||||
"그리고",
|
||||
"하고",
|
||||
"다음",
|
||||
"이후",
|
||||
"그런 다음",
|
||||
" and then ",
|
||||
" after that ",
|
||||
" also ",
|
||||
" additionally "
|
||||
];
|
||||
|
||||
private static readonly string[] ActionVerbs =
|
||||
{
|
||||
"해줘", "해 줘", "만들어", "수정해", "분석해", "작성해",
|
||||
"검토해", "확인해", "추가해", "삭제해", "변경해"
|
||||
};
|
||||
[
|
||||
"해줘",
|
||||
"해 줘",
|
||||
"만들",
|
||||
"수정",
|
||||
"분석",
|
||||
"작성",
|
||||
"검토",
|
||||
"확인",
|
||||
"추가",
|
||||
"삭제",
|
||||
"변경",
|
||||
"fix",
|
||||
"review",
|
||||
"analyze",
|
||||
"write",
|
||||
"create",
|
||||
"implement",
|
||||
"add",
|
||||
"remove",
|
||||
"change"
|
||||
];
|
||||
|
||||
/// <summary>입력 길이 제한 — 50KB 이상은 잘라서 처리.</summary>
|
||||
private const int MaxInputLength = 50_000;
|
||||
|
||||
public IntentGateService(ILlmService? llm = null) => _llm = llm;
|
||||
|
||||
/// <summary>
|
||||
/// 사용자 쿼리를 분석하여 IntentResult를 생성합니다.
|
||||
/// </summary>
|
||||
public Task<IntentResult> ClassifyAsync(
|
||||
string userQuery, string? activeTab, CancellationToken ct = default)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
// 안전 가드: null/과도한 길이
|
||||
var safeQuery = userQuery ?? "";
|
||||
var safeQuery = userQuery ?? string.Empty;
|
||||
if (safeQuery.Length > MaxInputLength)
|
||||
safeQuery = safeQuery[..MaxInputLength];
|
||||
|
||||
// 한 번만 lowercase 변환 후 하위 메서드에 전달
|
||||
var lowerQuery = safeQuery.ToLowerInvariant();
|
||||
|
||||
// ── Stage 1: 키워드 분류 ──
|
||||
var taskType = ClassifyTaskTypeKeyword(safeQuery, activeTab);
|
||||
var (intentCategory, intentConfidence) = IntentDetector.Detect(safeQuery);
|
||||
|
||||
// 종합 confidence: taskType 확정도 + IntentDetector 확신도 가중 평균
|
||||
var taskTypeConfidence = ComputeTaskTypeConfidence(taskType, lowerQuery);
|
||||
var combinedConfidence = Math.Min(1.0,
|
||||
taskTypeConfidence * 0.6 + intentConfidence * 0.4);
|
||||
|
||||
// ── Stage 2: 프로파일 매핑 ──
|
||||
var combinedConfidence = Math.Min(1.0, taskTypeConfidence * 0.6 + intentConfidence * 0.4);
|
||||
var overlay = MapToOverlay(taskType, intentCategory, activeTab);
|
||||
var scope = ClassifyScopeFromIntent(lowerQuery, activeTab, taskType, intentCategory);
|
||||
|
||||
// ── 복합 요청 감지 (P5 연동) ──
|
||||
var (isComplex, hint) = DetectComplexTask(lowerQuery);
|
||||
|
||||
var result = new IntentResult(
|
||||
return Task.FromResult(new IntentResult(
|
||||
TaskType: taskType,
|
||||
IntentCategory: intentCategory,
|
||||
Confidence: Math.Round(combinedConfidence, 2, MidpointRounding.AwayFromZero),
|
||||
PolicyOverlay: overlay,
|
||||
SuggestedScope: scope,
|
||||
IsComplexTask: isComplex,
|
||||
DecompositionHint: hint
|
||||
);
|
||||
|
||||
return Task.FromResult(result);
|
||||
DecompositionHint: hint));
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════
|
||||
// Stage 1: 키워드 기반 작업 유형 분류
|
||||
// ════════════════════════════════════════════════════════════
|
||||
|
||||
/// <summary>
|
||||
/// ClassifyTaskType 로직을 통합한 키워드 분류. 기존 AgentLoopService.ClassifyTaskType과 동일 로직.
|
||||
/// </summary>
|
||||
internal static string ClassifyTaskTypeKeyword(string? userQuery, string? activeTab)
|
||||
{
|
||||
var q = userQuery ?? "";
|
||||
var query = userQuery ?? string.Empty;
|
||||
|
||||
if (ContainsAny(q, "review", "리뷰", "검토", "code review", "점검"))
|
||||
if (ContainsAny(query, "review", "code review", "리뷰", "검토"))
|
||||
return "review";
|
||||
|
||||
if (ContainsAny(q, "bug", "fix", "error", "failure", "broken", "오류", "버그", "수정", "고쳐", "깨짐", "실패"))
|
||||
if (ContainsAny(query, "bug", "fix", "error", "failure", "broken", "버그", "오류", "수정", "고쳐", "실패"))
|
||||
return "bugfix";
|
||||
|
||||
if (ContainsAny(q, "refactor", "cleanup", "rename", "reorganize", "리팩터링", "정리", "개편", "구조 개선"))
|
||||
if (ContainsAny(query, "refactor", "cleanup", "rename", "reorganize", "리팩토링", "리팩터링", "정리", "개편", "구조 개선"))
|
||||
return "refactor";
|
||||
|
||||
if (!string.Equals(activeTab, "Code", StringComparison.OrdinalIgnoreCase)
|
||||
&& ContainsAny(q, "report", "document", "proposal", "분석서", "보고서", "문서", "제안서"))
|
||||
&& ContainsAny(query, "report", "document", "proposal", "analysis", "보고서", "문서", "제안서", "기획서"))
|
||||
return "docs";
|
||||
|
||||
if (ContainsAny(q, "feature", "implement", "add", "support", "추가", "구현", "지원", "기능"))
|
||||
if (ContainsAny(query, "feature", "implement", "add", "support", "추가", "구현", "기능"))
|
||||
return "feature";
|
||||
|
||||
return string.Equals(activeTab, "Code", StringComparison.OrdinalIgnoreCase) ? "feature" : "general";
|
||||
return string.Equals(activeTab, "Code", StringComparison.OrdinalIgnoreCase)
|
||||
? "feature"
|
||||
: "general";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// taskType 키워드 매칭 강도로 confidence를 산출합니다.
|
||||
/// <paramref name="lowerQuery"/>는 이미 ToLowerInvariant 처리된 문자열입니다.
|
||||
/// </summary>
|
||||
private static double ComputeTaskTypeConfidence(string taskType, string lowerQuery)
|
||||
{
|
||||
// "general"은 폴백이므로 confidence 낮음
|
||||
if (string.Equals(taskType, "general", StringComparison.Ordinal)) return 0.3;
|
||||
if (string.Equals(taskType, "general", StringComparison.Ordinal))
|
||||
return 0.3;
|
||||
|
||||
// 직접 매칭 키워드 수 세기
|
||||
var hitCount = taskType switch
|
||||
{
|
||||
"review" => CountHits(lowerQuery, "review", "리뷰", "검토", "code review", "점검"),
|
||||
"bugfix" => CountHits(lowerQuery, "bug", "fix", "error", "오류", "버그", "수정", "고쳐", "실패"),
|
||||
"refactor" => CountHits(lowerQuery, "refactor", "cleanup", "리팩터링", "정리", "개편"),
|
||||
"docs" => CountHits(lowerQuery, "report", "document", "보고서", "문서", "제안서"),
|
||||
"review" => CountHits(lowerQuery, "review", "code review", "리뷰", "검토"),
|
||||
"bugfix" => CountHits(lowerQuery, "bug", "fix", "error", "버그", "오류", "수정", "고쳐"),
|
||||
"refactor" => CountHits(lowerQuery, "refactor", "cleanup", "리팩토링", "리팩터링", "정리", "개편"),
|
||||
"docs" => CountHits(lowerQuery, "report", "document", "proposal", "보고서", "문서", "제안서"),
|
||||
"feature" => CountHits(lowerQuery, "feature", "implement", "add", "추가", "구현", "기능"),
|
||||
_ => 0,
|
||||
};
|
||||
@@ -137,27 +128,20 @@ internal sealed class IntentGateService
|
||||
private static int CountHits(string lower, params string[] keywords)
|
||||
{
|
||||
var count = 0;
|
||||
foreach (var kw in keywords)
|
||||
foreach (var keyword in keywords)
|
||||
{
|
||||
if (lower.Contains(kw, StringComparison.OrdinalIgnoreCase))
|
||||
if (lower.Contains(keyword, StringComparison.OrdinalIgnoreCase))
|
||||
count++;
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════
|
||||
// Stage 2: 프로파일 매핑
|
||||
// ════════════════════════════════════════════════════════════
|
||||
|
||||
/// <summary>
|
||||
/// (taskType, intentCategory) 조합에 따라 ExecutionPolicy overlay를 생성합니다.
|
||||
/// </summary>
|
||||
private static ExecutionPolicyOverlay? MapToOverlay(
|
||||
string taskType, string intentCategory, string? activeTab)
|
||||
{
|
||||
return (taskType, intentCategory) switch
|
||||
{
|
||||
// 코드 수정 관련
|
||||
("bugfix", "coding" or "general") => new(
|
||||
ToolTemperatureCap: 0.2,
|
||||
ForceInitialToolCall: true,
|
||||
@@ -178,7 +162,6 @@ internal sealed class IntentGateService
|
||||
ForceInitialToolCall: true,
|
||||
EnableCodeQualityGates: true),
|
||||
|
||||
// 문서 생성
|
||||
("docs", "document" or "creative" or "general") => new(
|
||||
EnableDocumentVerificationGate: true,
|
||||
ReduceEarlyMemoryPressure: true),
|
||||
@@ -186,7 +169,6 @@ internal sealed class IntentGateService
|
||||
("docs", _) => new(
|
||||
EnableDocumentVerificationGate: true),
|
||||
|
||||
// 리뷰/분석
|
||||
("review", "analysis" or "coding" or "general") => new(
|
||||
ToolTemperatureCap: 0.3,
|
||||
EnableCodeQualityGates: true,
|
||||
@@ -196,131 +178,110 @@ internal sealed class IntentGateService
|
||||
ToolTemperatureCap: 0.3,
|
||||
ForceInitialToolCall: true),
|
||||
|
||||
// general + 순수 대화 (Chat 탭)
|
||||
("general", _) when string.Equals(activeTab, "Chat", StringComparison.OrdinalIgnoreCase)
|
||||
=> null, // Chat 탭은 도구 없음, overlay 불필요
|
||||
=> null,
|
||||
|
||||
// general + 문서 의도
|
||||
("general", "document") => new(
|
||||
EnableDocumentVerificationGate: true),
|
||||
|
||||
// general + 분석 의도
|
||||
("general", "analysis") => new(
|
||||
ToolTemperatureCap: 0.35,
|
||||
MaxParallelReadBatch: 8),
|
||||
|
||||
// 기타: base policy 그대로
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════
|
||||
// 탐색 범위 결정
|
||||
// ════════════════════════════════════════════════════════════
|
||||
|
||||
/// <summary>
|
||||
/// IntentGate 결과를 기반으로 ExplorationScope를 결정합니다.
|
||||
/// <paramref name="lowerQuery"/>는 이미 ToLowerInvariant 처리된 문자열입니다.
|
||||
/// </summary>
|
||||
private static ExplorationScope ClassifyScopeFromIntent(
|
||||
string lowerQuery, string? activeTab, string taskType, string intentCategory)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(lowerQuery))
|
||||
return ExplorationScope.OpenEnded;
|
||||
|
||||
// docs 타입이면서 생성 동사가 있으면 DirectCreation
|
||||
if (taskType == "docs" && !string.Equals(activeTab, "Code", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (HasCreationVerb(lowerQuery))
|
||||
return ExplorationScope.DirectCreation;
|
||||
}
|
||||
|
||||
// document 인텐트 + 생성 동사 → DirectCreation
|
||||
if (intentCategory == "document"
|
||||
&& !string.Equals(activeTab, "Code", StringComparison.OrdinalIgnoreCase)
|
||||
&& HasCreationVerb(lowerQuery))
|
||||
return ExplorationScope.DirectCreation;
|
||||
|
||||
// RepoWide
|
||||
if (ContainsAny(lowerQuery, "전체", "전반", "코드베이스 전체",
|
||||
"repo-wide", "repository-wide", "전체 구조", "아키텍처", "전체 점검"))
|
||||
if (string.Equals(activeTab, "Code", StringComparison.OrdinalIgnoreCase)
|
||||
&& ProjectScaffoldProfileCatalog.IsStructuredProjectRequest(lowerQuery, activeTab))
|
||||
return ExplorationScope.ProjectScaffold;
|
||||
|
||||
if (string.Equals(activeTab, "Code", StringComparison.OrdinalIgnoreCase)
|
||||
&& HasCreationVerb(lowerQuery)
|
||||
&& ContainsAny(
|
||||
lowerQuery,
|
||||
"html", "css", "javascript", "js", "typescript", "ts",
|
||||
"web page", "webpage", "website", "landing page",
|
||||
"file", "script", "component", "template",
|
||||
"index.html", "app.js", "style.css",
|
||||
"파일", "스크립트", "컴포넌트", "템플릿"))
|
||||
return ExplorationScope.DirectCreation;
|
||||
|
||||
if (ContainsAny(lowerQuery, "전체", "전반", "코드베이스 전체", "repo-wide", "repository-wide", "전체 구조", "아키텍처", "전체 맥락"))
|
||||
return ExplorationScope.RepoWide;
|
||||
|
||||
// Localized
|
||||
if (lowerQuery.Contains('.') || lowerQuery.Contains('/') || lowerQuery.Contains('\\') ||
|
||||
ContainsAny(lowerQuery, "file ", "class ", "method ", "function ", "line ",
|
||||
"bug", "오류", "버그", "예외"))
|
||||
ContainsAny(lowerQuery, "file ", "class ", "method ", "function ", "line ", "bug", "오류", "버그", "예외"))
|
||||
return ExplorationScope.Localized;
|
||||
|
||||
// TopicBased
|
||||
if (ContainsAny(lowerQuery, "정리", "요약", "보고서", "주제", "관련", "분석"))
|
||||
if (ContainsAny(lowerQuery, "정리", "요약", "보고", "주제", "관련", "분석"))
|
||||
return ExplorationScope.TopicBased;
|
||||
|
||||
// 탭 기반 기본값
|
||||
return string.Equals(activeTab, "Code", StringComparison.OrdinalIgnoreCase)
|
||||
? ExplorationScope.Localized
|
||||
: ExplorationScope.OpenEnded;
|
||||
}
|
||||
|
||||
private static bool HasCreationVerb(string lower)
|
||||
=> ContainsAny(lower,
|
||||
"작성해", "써줘", "써 줘", "만들어", "생성해",
|
||||
"만들어줘", "만들어 줘", "생성해줘", "생성해 줘",
|
||||
"write", "create", "draft", "generate", "compose",
|
||||
"작성하", "작성을", "생성하", "생성을",
|
||||
"작성 부탁", "만들어 부탁");
|
||||
=> ContainsAny(
|
||||
lower,
|
||||
"작성", "작성해", "써줘", "만들", "생성", "만들어줘", "만들어", "생성해줘", "생성해",
|
||||
"write", "create", "draft", "generate", "compose", "scaffold", "bootstrap");
|
||||
|
||||
// ════════════════════════════════════════════════════════════
|
||||
// 복합 요청 감지 (P5 연동)
|
||||
// ════════════════════════════════════════════════════════════
|
||||
|
||||
/// <summary>
|
||||
/// 복합 요청을 감지합니다. <paramref name="lowerQuery"/>는 이미 lowercase 변환된 문자열입니다.
|
||||
/// </summary>
|
||||
private static (bool IsComplex, string? Hint) DetectComplexTask(string lowerQuery)
|
||||
{
|
||||
if (lowerQuery.Length < 20)
|
||||
return (false, null);
|
||||
|
||||
// 접속사/열거 패턴 감지 (클래스 수준 static readonly 배열 사용)
|
||||
var conjunctionCount = 0;
|
||||
foreach (var conj in Conjunctions)
|
||||
foreach (var conjunction in Conjunctions)
|
||||
{
|
||||
if (lowerQuery.Contains(conj, StringComparison.Ordinal))
|
||||
if (lowerQuery.Contains(conjunction, StringComparison.Ordinal))
|
||||
conjunctionCount++;
|
||||
}
|
||||
|
||||
// 동사 열거 패턴 (클래스 수준 static readonly 배열 사용)
|
||||
var verbCount = 0;
|
||||
foreach (var verb in ActionVerbs)
|
||||
{
|
||||
var idx = 0;
|
||||
while ((idx = lowerQuery.IndexOf(verb, idx, StringComparison.Ordinal)) >= 0)
|
||||
var index = 0;
|
||||
while ((index = lowerQuery.IndexOf(verb, index, StringComparison.Ordinal)) >= 0)
|
||||
{
|
||||
verbCount++;
|
||||
idx += verb.Length;
|
||||
index += verb.Length;
|
||||
}
|
||||
}
|
||||
|
||||
if (conjunctionCount >= 2 || verbCount >= 3)
|
||||
{
|
||||
return (true, "이 요청에 여러 독립 작업이 포함되어 있습니다. spawn_agents로 병렬 처리를 고려하세요.");
|
||||
}
|
||||
return (true, "이 요청은 여러 단계의 작업을 포함합니다. 병렬화 가능한 하위 작업을 분리해 처리하는 것이 좋습니다.");
|
||||
|
||||
return (false, null);
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════
|
||||
// 공통 유틸
|
||||
// ════════════════════════════════════════════════════════════
|
||||
|
||||
private static bool ContainsAny(string text, params string[] keywords)
|
||||
{
|
||||
foreach (var kw in keywords)
|
||||
foreach (var keyword in keywords)
|
||||
{
|
||||
if (text.Contains(kw, StringComparison.OrdinalIgnoreCase))
|
||||
if (text.Contains(keyword, StringComparison.OrdinalIgnoreCase))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
583
src/AxCopilot/Services/Agent/ProjectScaffoldProfileCatalog.cs
Normal file
583
src/AxCopilot/Services/Agent/ProjectScaffoldProfileCatalog.cs
Normal file
@@ -0,0 +1,583 @@
|
||||
using System.IO;
|
||||
|
||||
namespace AxCopilot.Services.Agent;
|
||||
|
||||
internal sealed record ProjectScaffoldProfile(
|
||||
string Key,
|
||||
string Label,
|
||||
IReadOnlyList<string> TriggerTokens,
|
||||
IReadOnlyList<string> ExpectedDirectories,
|
||||
IReadOnlyList<string> StarterPaths,
|
||||
IReadOnlyList<string> AllowedRootFiles,
|
||||
int MinimumDirectoryHits = 2);
|
||||
|
||||
internal sealed record ProjectScaffoldLayoutAssessment(
|
||||
bool IsSatisfied,
|
||||
int MatchedDirectoryCount,
|
||||
IReadOnlyList<string> ExistingDirectories,
|
||||
IReadOnlyList<string> MissingDirectories,
|
||||
IReadOnlyList<string> SuspiciousRootFiles);
|
||||
|
||||
internal static class ProjectScaffoldProfileCatalog
|
||||
{
|
||||
private static readonly string[] s_creationVerbs =
|
||||
[
|
||||
"create",
|
||||
"generate",
|
||||
"build",
|
||||
"write",
|
||||
"scaffold",
|
||||
"bootstrap",
|
||||
"set up",
|
||||
"setup",
|
||||
"make",
|
||||
"implement",
|
||||
"start",
|
||||
"만들",
|
||||
"생성",
|
||||
"작성",
|
||||
"구현",
|
||||
"구축",
|
||||
"세팅",
|
||||
"스캐폴드"
|
||||
];
|
||||
|
||||
private static readonly string[] s_projectNouns =
|
||||
[
|
||||
"project",
|
||||
"app",
|
||||
"application",
|
||||
"service",
|
||||
"api",
|
||||
"solution",
|
||||
"starter",
|
||||
"template",
|
||||
"boilerplate",
|
||||
"workspace",
|
||||
"repo",
|
||||
"repository",
|
||||
"library",
|
||||
"package",
|
||||
"module",
|
||||
"프로젝트",
|
||||
"앱",
|
||||
"애플리케이션",
|
||||
"서비스",
|
||||
"솔루션",
|
||||
"템플릿",
|
||||
"보일러플레이트",
|
||||
"워크스페이스",
|
||||
"저장소",
|
||||
"라이브러리",
|
||||
"패키지",
|
||||
"모듈"
|
||||
];
|
||||
|
||||
private static readonly string[] s_languageTokens =
|
||||
[
|
||||
"c#",
|
||||
"dotnet",
|
||||
".net",
|
||||
"csproj",
|
||||
"wpf",
|
||||
"xaml",
|
||||
"mvvm",
|
||||
"viewmodel",
|
||||
"resourcedictionary",
|
||||
"resource dictionary",
|
||||
"javascript",
|
||||
"typescript",
|
||||
"react",
|
||||
"next",
|
||||
"vue",
|
||||
"nuxt",
|
||||
"node",
|
||||
"express",
|
||||
"python",
|
||||
"fastapi",
|
||||
"flask",
|
||||
"django",
|
||||
"java",
|
||||
"spring",
|
||||
"spring boot",
|
||||
"kotlin",
|
||||
"android",
|
||||
"go",
|
||||
"golang",
|
||||
"rust",
|
||||
"cli",
|
||||
"web",
|
||||
"desktop",
|
||||
"mobile",
|
||||
"frontend",
|
||||
"backend",
|
||||
"api",
|
||||
"웹",
|
||||
"데스크톱",
|
||||
"모바일",
|
||||
"프론트엔드",
|
||||
"백엔드",
|
||||
"파이썬",
|
||||
"자바",
|
||||
"코틀린",
|
||||
"안드로이드",
|
||||
"리액트",
|
||||
"뷰",
|
||||
"고",
|
||||
"러스트",
|
||||
"스프링",
|
||||
"장고",
|
||||
"플라스크",
|
||||
"패스트api"
|
||||
];
|
||||
|
||||
private static readonly string[] s_solutionShapeTokens =
|
||||
[
|
||||
"frontend",
|
||||
"backend",
|
||||
"desktop",
|
||||
"mobile",
|
||||
"cli",
|
||||
"tool",
|
||||
"worker",
|
||||
"daemon",
|
||||
"game",
|
||||
"프론트엔드",
|
||||
"백엔드",
|
||||
"데스크톱",
|
||||
"모바일",
|
||||
"도구",
|
||||
"워커",
|
||||
"게임"
|
||||
];
|
||||
|
||||
private static readonly HashSet<string> s_commonAllowedRootFiles = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"README.md",
|
||||
".gitignore",
|
||||
".editorconfig",
|
||||
".gitattributes",
|
||||
".env",
|
||||
".env.example",
|
||||
"package.json",
|
||||
"package-lock.json",
|
||||
"pnpm-lock.yaml",
|
||||
"yarn.lock",
|
||||
"tsconfig.json",
|
||||
"vite.config.ts",
|
||||
"vite.config.js",
|
||||
"next.config.js",
|
||||
"next.config.mjs",
|
||||
"nuxt.config.ts",
|
||||
"requirements.txt",
|
||||
"pyproject.toml",
|
||||
"poetry.lock",
|
||||
"Pipfile",
|
||||
"Pipfile.lock",
|
||||
"pom.xml",
|
||||
"build.gradle",
|
||||
"build.gradle.kts",
|
||||
"settings.gradle",
|
||||
"settings.gradle.kts",
|
||||
"Cargo.toml",
|
||||
"go.mod",
|
||||
"go.sum",
|
||||
"composer.json",
|
||||
"Gemfile",
|
||||
"index.html"
|
||||
};
|
||||
|
||||
private static readonly string[] s_relevantRootExtensions =
|
||||
[
|
||||
".cs",
|
||||
".xaml",
|
||||
".razor",
|
||||
".ts",
|
||||
".tsx",
|
||||
".js",
|
||||
".jsx",
|
||||
".css",
|
||||
".scss",
|
||||
".py",
|
||||
".java",
|
||||
".kt",
|
||||
".go",
|
||||
".rs",
|
||||
".php",
|
||||
".rb",
|
||||
".json",
|
||||
".xml",
|
||||
".yaml",
|
||||
".yml"
|
||||
];
|
||||
|
||||
private static readonly IReadOnlyList<ProjectScaffoldProfile> s_profiles =
|
||||
[
|
||||
new(
|
||||
Key: "wpf-mvvm",
|
||||
Label: "WPF / MVVM desktop app",
|
||||
TriggerTokens:
|
||||
[
|
||||
"wpf",
|
||||
"xaml",
|
||||
"mvvm",
|
||||
"viewmodel",
|
||||
"resourcedictionary",
|
||||
"resource dictionary",
|
||||
"csproj",
|
||||
"desktop app",
|
||||
"window",
|
||||
"usercontrol",
|
||||
"user control"
|
||||
],
|
||||
ExpectedDirectories: ["Views", "ViewModels", "Models", "Services", "Themes", "Assets"],
|
||||
StarterPaths:
|
||||
[
|
||||
"App.xaml",
|
||||
"App.xaml.cs",
|
||||
"Views/MainWindow.xaml",
|
||||
"Views/MainWindow.xaml.cs",
|
||||
"ViewModels/MainWindowViewModel.cs",
|
||||
"Themes/Colors.xaml"
|
||||
],
|
||||
AllowedRootFiles: ["App.xaml", "App.xaml.cs"],
|
||||
MinimumDirectoryHits: 2),
|
||||
new(
|
||||
Key: "aspnet-api",
|
||||
Label: "ASP.NET / Web API service",
|
||||
TriggerTokens:
|
||||
[
|
||||
"asp.net",
|
||||
"aspnet",
|
||||
"web api",
|
||||
"webapi",
|
||||
"minimal api",
|
||||
"mvc",
|
||||
"blazor",
|
||||
"razor",
|
||||
"controller",
|
||||
"swagger"
|
||||
],
|
||||
ExpectedDirectories: ["Controllers", "Services", "Models", "Contracts", "Data", "Properties"],
|
||||
StarterPaths:
|
||||
[
|
||||
"Program.cs",
|
||||
"Controllers/HealthController.cs",
|
||||
"Services/HealthService.cs",
|
||||
"Models/HealthStatus.cs",
|
||||
"appsettings.json"
|
||||
],
|
||||
AllowedRootFiles: ["Program.cs", "appsettings.json", "appsettings.Development.json"],
|
||||
MinimumDirectoryHits: 2),
|
||||
new(
|
||||
Key: "react-web",
|
||||
Label: "React / Next / Vue frontend",
|
||||
TriggerTokens:
|
||||
[
|
||||
"react",
|
||||
"next",
|
||||
"next.js",
|
||||
"nextjs",
|
||||
"vue",
|
||||
"nuxt",
|
||||
"svelte",
|
||||
"vite",
|
||||
"frontend",
|
||||
"front-end",
|
||||
"spa",
|
||||
"component"
|
||||
],
|
||||
ExpectedDirectories: ["src/components", "src/pages", "src/services", "src/styles", "public"],
|
||||
StarterPaths:
|
||||
[
|
||||
"src/App.tsx",
|
||||
"src/main.tsx",
|
||||
"src/components/AppShell.tsx",
|
||||
"src/pages/HomePage.tsx",
|
||||
"src/services/api.ts"
|
||||
],
|
||||
AllowedRootFiles: ["index.html"],
|
||||
MinimumDirectoryHits: 2),
|
||||
new(
|
||||
Key: "node-service",
|
||||
Label: "Node / Express / Nest backend",
|
||||
TriggerTokens:
|
||||
[
|
||||
"node",
|
||||
"express",
|
||||
"nestjs",
|
||||
"nest",
|
||||
"koa",
|
||||
"backend",
|
||||
"back-end",
|
||||
"rest api",
|
||||
"server app",
|
||||
"middleware"
|
||||
],
|
||||
ExpectedDirectories: ["src/controllers", "src/routes", "src/services", "src/models", "tests"],
|
||||
StarterPaths:
|
||||
[
|
||||
"src/index.ts",
|
||||
"src/controllers/HealthController.ts",
|
||||
"src/routes/index.ts",
|
||||
"src/services/HealthService.ts",
|
||||
"tests/health.spec.ts"
|
||||
],
|
||||
AllowedRootFiles: [],
|
||||
MinimumDirectoryHits: 2),
|
||||
new(
|
||||
Key: "python-service",
|
||||
Label: "Python API service",
|
||||
TriggerTokens:
|
||||
[
|
||||
"python",
|
||||
"fastapi",
|
||||
"flask",
|
||||
"django",
|
||||
"uvicorn",
|
||||
"backend api",
|
||||
"python api"
|
||||
],
|
||||
ExpectedDirectories: ["app/api", "app/models", "app/services", "app/core", "tests"],
|
||||
StarterPaths:
|
||||
[
|
||||
"app/main.py",
|
||||
"app/api/routes.py",
|
||||
"app/services/health_service.py",
|
||||
"app/models/health.py",
|
||||
"tests/test_health.py"
|
||||
],
|
||||
AllowedRootFiles: [],
|
||||
MinimumDirectoryHits: 2),
|
||||
new(
|
||||
Key: "spring-service",
|
||||
Label: "Java / Spring Boot service",
|
||||
TriggerTokens:
|
||||
[
|
||||
"spring",
|
||||
"spring boot",
|
||||
"java api",
|
||||
"maven",
|
||||
"gradle",
|
||||
"jpa",
|
||||
"controller",
|
||||
"repository"
|
||||
],
|
||||
ExpectedDirectories: ["src/main/java", "src/main/resources", "src/test/java"],
|
||||
StarterPaths:
|
||||
[
|
||||
"src/main/java/com/example/Application.java",
|
||||
"src/main/java/com/example/controller/HealthController.java",
|
||||
"src/main/java/com/example/service/HealthService.java",
|
||||
"src/main/resources/application.yml",
|
||||
"src/test/java/com/example/ApplicationTests.java"
|
||||
],
|
||||
AllowedRootFiles: [],
|
||||
MinimumDirectoryHits: 2),
|
||||
new(
|
||||
Key: "android-kotlin",
|
||||
Label: "Android / Kotlin app",
|
||||
TriggerTokens:
|
||||
[
|
||||
"android",
|
||||
"kotlin",
|
||||
"jetpack compose",
|
||||
"compose",
|
||||
"xml layout",
|
||||
"activity",
|
||||
"fragment"
|
||||
],
|
||||
ExpectedDirectories: ["app/src/main/java", "app/src/main/res/layout", "app/src/main/res/values", "app/src/test"],
|
||||
StarterPaths:
|
||||
[
|
||||
"app/src/main/java/com/example/MainActivity.kt",
|
||||
"app/src/main/res/layout/activity_main.xml",
|
||||
"app/src/main/res/values/colors.xml",
|
||||
"app/src/test/java/com/example/MainActivityTest.kt"
|
||||
],
|
||||
AllowedRootFiles: [],
|
||||
MinimumDirectoryHits: 2),
|
||||
new(
|
||||
Key: "go-service",
|
||||
Label: "Go service",
|
||||
TriggerTokens:
|
||||
[
|
||||
"golang",
|
||||
"go api",
|
||||
"go service",
|
||||
"gin",
|
||||
"echo",
|
||||
"fiber",
|
||||
"handler"
|
||||
],
|
||||
ExpectedDirectories: ["cmd", "internal", "pkg", "api", "tests"],
|
||||
StarterPaths:
|
||||
[
|
||||
"cmd/server/main.go",
|
||||
"internal/handler/health.go",
|
||||
"internal/service/health.go",
|
||||
"pkg/config/config.go",
|
||||
"tests/health_test.go"
|
||||
],
|
||||
AllowedRootFiles: [],
|
||||
MinimumDirectoryHits: 2),
|
||||
new(
|
||||
Key: "rust-cli",
|
||||
Label: "Rust CLI / tool",
|
||||
TriggerTokens:
|
||||
[
|
||||
"rust",
|
||||
"cargo",
|
||||
"cli",
|
||||
"command line",
|
||||
"terminal tool",
|
||||
"binary",
|
||||
"subcommand"
|
||||
],
|
||||
ExpectedDirectories: ["src/bin", "src/commands", "src/domain", "tests"],
|
||||
StarterPaths:
|
||||
[
|
||||
"src/main.rs",
|
||||
"src/commands/root.rs",
|
||||
"src/domain/config.rs",
|
||||
"tests/cli_tests.rs"
|
||||
],
|
||||
AllowedRootFiles: [],
|
||||
MinimumDirectoryHits: 2),
|
||||
new(
|
||||
Key: "generic-solution",
|
||||
Label: "Generic structured solution",
|
||||
TriggerTokens:
|
||||
[
|
||||
"project",
|
||||
"solution",
|
||||
"starter",
|
||||
"template",
|
||||
"boilerplate",
|
||||
"library",
|
||||
"module",
|
||||
"workspace"
|
||||
],
|
||||
ExpectedDirectories: ["src", "tests", "docs", "config"],
|
||||
StarterPaths:
|
||||
[
|
||||
"src/main.ext",
|
||||
"src/services/service.ext",
|
||||
"tests/main.spec.ext",
|
||||
"docs/README.md"
|
||||
],
|
||||
AllowedRootFiles: [],
|
||||
MinimumDirectoryHits: 2)
|
||||
];
|
||||
|
||||
internal static ProjectScaffoldProfile? Detect(string? userQuery, string? activeTab)
|
||||
{
|
||||
if (!string.Equals(activeTab, "Code", StringComparison.OrdinalIgnoreCase))
|
||||
return null;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(userQuery))
|
||||
return null;
|
||||
|
||||
var query = userQuery.ToLowerInvariant();
|
||||
if (!ContainsAny(query, s_creationVerbs))
|
||||
return null;
|
||||
|
||||
ProjectScaffoldProfile? bestProfile = null;
|
||||
var bestScore = 0;
|
||||
foreach (var profile in s_profiles)
|
||||
{
|
||||
var score = profile.TriggerTokens.Count(token => query.Contains(token, StringComparison.OrdinalIgnoreCase));
|
||||
if (score > bestScore)
|
||||
{
|
||||
bestScore = score;
|
||||
bestProfile = profile;
|
||||
}
|
||||
}
|
||||
|
||||
if (bestProfile != null && bestScore > 0)
|
||||
return bestProfile;
|
||||
|
||||
if (ContainsAny(query, s_projectNouns) && ContainsAny(query, s_languageTokens))
|
||||
return GetProfile("generic-solution");
|
||||
|
||||
if (ContainsAny(query, s_projectNouns) && ContainsAny(query, s_solutionShapeTokens))
|
||||
return GetProfile("generic-solution");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
internal static bool IsStructuredProjectRequest(string? userQuery, string? activeTab)
|
||||
=> Detect(userQuery, activeTab) != null;
|
||||
|
||||
internal static string BuildDirectoryPreview(ProjectScaffoldProfile profile, int maxCount = 5)
|
||||
=> string.Join(", ", profile.ExpectedDirectories.Take(Math.Max(1, maxCount)));
|
||||
|
||||
internal static string BuildStarterPathPreview(ProjectScaffoldProfile profile, int maxCount = 5)
|
||||
=> string.Join(", ", profile.StarterPaths.Take(Math.Max(1, maxCount)).Select(path => $"'{path}'"));
|
||||
|
||||
internal static ProjectScaffoldLayoutAssessment AssessLayout(ProjectScaffoldProfile profile, string? workFolder)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(workFolder) || !Directory.Exists(workFolder))
|
||||
{
|
||||
return new ProjectScaffoldLayoutAssessment(
|
||||
IsSatisfied: false,
|
||||
MatchedDirectoryCount: 0,
|
||||
ExistingDirectories: [],
|
||||
MissingDirectories: profile.ExpectedDirectories.ToList(),
|
||||
SuspiciousRootFiles: []);
|
||||
}
|
||||
|
||||
var existingDirectories = profile.ExpectedDirectories
|
||||
.Where(dir => Directory.Exists(Path.Combine(workFolder, NormalizeDirectory(dir))))
|
||||
.ToList();
|
||||
var missingDirectories = profile.ExpectedDirectories
|
||||
.Where(dir => !existingDirectories.Contains(dir, StringComparer.OrdinalIgnoreCase))
|
||||
.ToList();
|
||||
|
||||
var allowedRootFiles = new HashSet<string>(s_commonAllowedRootFiles, StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var file in profile.AllowedRootFiles)
|
||||
allowedRootFiles.Add(file);
|
||||
|
||||
var suspiciousRootFiles = Directory.EnumerateFiles(workFolder)
|
||||
.Select(Path.GetFileName)
|
||||
.Where(name => !string.IsNullOrWhiteSpace(name))
|
||||
.Select(name => name!)
|
||||
.Where(name =>
|
||||
{
|
||||
if (allowedRootFiles.Contains(name))
|
||||
return false;
|
||||
|
||||
var extension = Path.GetExtension(name);
|
||||
return s_relevantRootExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase);
|
||||
})
|
||||
.OrderBy(name => name, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
|
||||
var isSatisfied = existingDirectories.Count >= profile.MinimumDirectoryHits
|
||||
&& suspiciousRootFiles.Count == 0;
|
||||
|
||||
return new ProjectScaffoldLayoutAssessment(
|
||||
IsSatisfied: isSatisfied,
|
||||
MatchedDirectoryCount: existingDirectories.Count,
|
||||
ExistingDirectories: existingDirectories,
|
||||
MissingDirectories: missingDirectories,
|
||||
SuspiciousRootFiles: suspiciousRootFiles);
|
||||
}
|
||||
|
||||
private static ProjectScaffoldProfile GetProfile(string key)
|
||||
=> s_profiles.First(profile => string.Equals(profile.Key, key, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
private static string NormalizeDirectory(string value)
|
||||
=> value.Replace('/', Path.DirectorySeparatorChar);
|
||||
|
||||
private static bool ContainsAny(string text, IEnumerable<string> tokens)
|
||||
{
|
||||
foreach (var token in tokens)
|
||||
{
|
||||
if (text.Contains(token, StringComparison.OrdinalIgnoreCase))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,9 @@ public partial class ChatWindow
|
||||
sb.AppendLine("A text-only response is fine once the requested artifact already exists, the requested analysis is complete, or enough evidence has been gathered.");
|
||||
sb.AppendLine("출력의 첫 번째 항목은 반드시 도구 호출이어야 합니다. 텍스트로 시작하는 것은 금지입니다.");
|
||||
sb.AppendLine("When a tool is clearly useful, call it promptly without a long preamble. Do not force an unnecessary tool call if the task is already complete.");
|
||||
sb.AppendLine("If the current work folder is empty and the user is asking to create a new file, webpage, or scaffold, skip broad exploration and call file_write directly with a relative path inside the current work folder.");
|
||||
sb.AppendLine("If the current work folder is empty and the user is asking to create a new file, webpage, or scaffold, skip broad exploration and move directly to artifact creation.");
|
||||
sb.AppendLine("For single-file outputs, call file_write directly with a relative path inside the current work folder.");
|
||||
sb.AppendLine("For framework or multi-file scaffolds such as WPF/MVVM, ASP.NET, React/Vue/Next, FastAPI/Django/Flask, Spring, Android, Go, or Rust, first establish a minimal project tree and place files inside framework-appropriate folders instead of flattening everything in the workspace root.");
|
||||
sb.AppendLine("Do not call skill_manager, mcp_list_resources, or mcp_read_resource for normal Code tasks unless the user explicitly asked about skills or MCP resources.");
|
||||
sb.AppendLine("");
|
||||
sb.AppendLine("### [규칙 2] 말 먼저 하지 마라 — No Verbal Preamble");
|
||||
@@ -212,7 +214,9 @@ public partial class ChatWindow
|
||||
sb.AppendLine("A text-only response is fine once the requested code work is complete or enough evidence has been gathered.");
|
||||
sb.AppendLine("출력의 첫 번째 항목은 반드시 도구 호출이어야 합니다. 텍스트로 시작하는 것은 금지입니다.");
|
||||
sb.AppendLine("When a tool is clearly useful, call it promptly without a long preamble. Do not force an unnecessary tool call if the task is already complete.");
|
||||
sb.AppendLine("If the current work folder is empty and the user is asking to create a new file, webpage, or scaffold, skip broad exploration and call file_write directly with a relative path inside the current work folder.");
|
||||
sb.AppendLine("If the current work folder is empty and the user is asking to create a new file, webpage, or scaffold, skip broad exploration and move directly to artifact creation.");
|
||||
sb.AppendLine("For a single-file artifact, call file_write directly with a relative path inside the current work folder.");
|
||||
sb.AppendLine("For framework or multi-file scaffolds such as WPF/MVVM, ASP.NET, React/Vue/Next, FastAPI/Django/Flask, Spring, Android, Go, or Rust, create a minimal project tree first and place files inside framework-appropriate folders instead of dumping all source files in the workspace root.");
|
||||
sb.AppendLine("Do not call skill_manager, mcp_list_resources, or mcp_read_resource for normal Code tasks unless the user explicitly asked about skills or MCP resources.");
|
||||
sb.AppendLine("");
|
||||
sb.AppendLine("### [규칙 2] 말 먼저 하지 마라 — No Verbal Preamble");
|
||||
@@ -251,7 +255,7 @@ public partial class ChatWindow
|
||||
sb.AppendLine("---");
|
||||
sb.AppendLine("You are AX Copilot Code Agent — a senior software engineer for enterprise development.");
|
||||
sb.AppendLine($"Today's date: {DateTime.Now:yyyy년 M월 d일} ({DateTime.Now:yyyy-MM-dd}).");
|
||||
sb.AppendLine("Available tools: file_read, file_write, file_edit (supports replace_all), glob, grep (supports context_lines, case_sensitive), lsp_code_intel, folder_map, process, dev_env_detect, build_run, git_tool.");
|
||||
sb.AppendLine("Available tools: file_read, file_write, file_edit (supports replace_all), file_manage, glob, grep (supports context_lines, case_sensitive), lsp_code_intel, folder_map, process, dev_env_detect, build_run, git_tool.");
|
||||
sb.AppendLine("Do not pause after partial progress. Keep executing consecutive steps until completion or a concrete blocker is reached.");
|
||||
sb.AppendLine("IMPORTANT: When creating documents with dates, always use today's actual date above.");
|
||||
sb.AppendLine("Do NOT call open_external, launch browsers, or start preview/server commands unless the user explicitly asks to open, preview, serve, launch, or run the result.");
|
||||
@@ -268,6 +272,7 @@ public partial class ChatWindow
|
||||
sb.AppendLine("4. VERIFY: Run build_run/test_loop when the change affects buildable or testable behavior, or when the user explicitly asks for verification.");
|
||||
sb.AppendLine(" - Use git_tool(diff) when it helps confirm the final change set or explain what changed.");
|
||||
sb.AppendLine(" - After editing code, do not stop until you have enough evidence from file_read, diff, build_run, or test_loop.");
|
||||
sb.AppendLine(" - If the task is a new framework/project scaffold, verify the layout too: keep views/components/routes/services/models/resources under appropriate folders instead of leaving implementation files flat in the root.");
|
||||
sb.AppendLine("5. REPORT: Summarize what changed, which files/callers were affected, and what verification evidence was collected.");
|
||||
sb.AppendLine(" - For bugfix/feature/refactor tasks, keep the final report structured and concrete rather than minimal.");
|
||||
|
||||
|
||||
@@ -2,28 +2,34 @@
|
||||
name: code-scaffold
|
||||
label: 코드 스캐폴딩
|
||||
description: 프로젝트 구조를 분석하고 새 기능의 코드 뼈대를 자동 생성합니다.
|
||||
when_to_use: 빈 작업 폴더에서 새 프로젝트를 만들거나, WPF/MVVM, ASP.NET, React/Vue/Next, FastAPI/Django/Flask, Spring, Android, Go, Rust 같은 구조형 스캐폴드를 폴더 트리부터 잡아야 할 때
|
||||
icon: \uE943
|
||||
allowed-tools:
|
||||
- file_manage
|
||||
- folder_map
|
||||
- file_read
|
||||
- grep
|
||||
- file_write
|
||||
- file_edit
|
||||
- search_codebase
|
||||
tabs: code
|
||||
---
|
||||
|
||||
작업 폴더의 프로젝트 구조를 분석하고 새 기능의 코드 뼈대를 생성하세요.
|
||||
작업 폴더의 프로젝트 구조를 분석하고 새 기능이나 새 프로젝트의 코드 뼈대를 생성하세요.
|
||||
|
||||
다음 도구를 사용하세요:
|
||||
1. folder_map — 프로젝트 구조 파악
|
||||
2. file_read — 기존 코드 패턴 분석
|
||||
3. grep — 코딩 컨벤션 확인
|
||||
4. file_write — 새 파일 생성
|
||||
1. file_manage — 폴더 생성, 이동, 정리
|
||||
2. folder_map — 기존 프로젝트 구조 파악
|
||||
3. file_read — 기존 코드 패턴 분석
|
||||
4. grep — 코딩 컨벤션 확인
|
||||
5. file_write / file_edit — 새 파일 생성 및 보강
|
||||
|
||||
작업 순서:
|
||||
1. 프로젝트 타입 감지 (언어, 프레임워크, 빌드 시스템)
|
||||
2. 기존 코드 패턴 분석 (네이밍, 폴더 구조, 임포트 스타일)
|
||||
3. 사용자 요청에 맞는 코드 뼈대 생성
|
||||
2. 빈 작업 폴더라면 최소 프로젝트 트리를 먼저 설계하고 file_manage로 폴더를 만든 뒤 파일을 배치
|
||||
3. 기존 프로젝트가 있다면 네이밍, 폴더 구조, 임포트 스타일을 먼저 맞춤
|
||||
4. 사용자 요청에 맞는 코드 뼈대 생성
|
||||
5. 구조형 프로젝트는 뷰/UI, 모델, 서비스, 리소스, 테스트를 가능한 한 폴더로 분리
|
||||
|
||||
생성 항목:
|
||||
- 클래스/모듈 파일 (프로젝트 컨벤션에 맞춰)
|
||||
@@ -33,5 +39,8 @@ tabs: code
|
||||
|
||||
규칙:
|
||||
- 기존 프로젝트의 코딩 스타일을 따르세요
|
||||
- WPF, ASP.NET, React, Python API, Spring, Android, Go, Rust 같은 구조형 프로젝트는 구현 파일을 루트에 평평하게 두지 마세요
|
||||
- 루트에는 manifest, entrypoint, README 같은 최소 파일만 두고 나머지는 적절한 폴더로 분리하세요
|
||||
- 새 프로젝트를 시작할 때는 단일 파일 생성보다 폴더 트리와 대표 파일 경로를 먼저 확정하세요
|
||||
- TODO 주석으로 구현이 필요한 부분을 표시하세요
|
||||
- 한국어 주석을 추가하세요
|
||||
|
||||
Reference in New Issue
Block a user