??? ???? ???? ??? ?? ???? ???? ??? ? ?????? ?? ?? ??
- 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 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
|
- `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 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
|
- `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");
|
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]
|
[Fact]
|
||||||
public void BuildFailureNextToolPriorityPrompt_IncludesOrderedPriority()
|
public void BuildFailureNextToolPriorityPrompt_IncludesOrderedPriority()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -135,6 +135,22 @@ public class IntentGateServiceTests
|
|||||||
result.SuggestedScope.Should().Be(ExplorationScope.Localized);
|
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)
|
// 복합 요청 감지 (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
|
var blockedMessage = isExternalEscalation
|
||||||
? $"Empty workspace guard blocked external path search: {target}"
|
? $"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(AgentEventType.Error, toolName, blockedMessage);
|
||||||
EmitEvent(
|
EmitEvent(
|
||||||
@@ -81,18 +81,34 @@ public partial class AgentLoopService
|
|||||||
messages.Add(new ChatMessage
|
messages.Add(new ChatMessage
|
||||||
{
|
{
|
||||||
Role = "user",
|
Role = "user",
|
||||||
Content = BuildEmptyWorkspaceCreationRecoveryPrompt(activeToolNames)
|
Content = BuildEmptyWorkspaceCreationRecoveryPrompt(activeToolNames, explorationState: null, context.InitialUserQuery)
|
||||||
});
|
});
|
||||||
return true;
|
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 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. " +
|
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. " +
|
"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. " +
|
"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. " +
|
"Do not only describe the plan. Emit the actual tool call now. " +
|
||||||
$"Available tools: {activeToolPreview}";
|
$"Available tools: {activeToolPreview}";
|
||||||
}
|
}
|
||||||
@@ -108,7 +124,7 @@ public partial class AgentLoopService
|
|||||||
if (!runState.WorkspaceAppearsEmpty)
|
if (!runState.WorkspaceAppearsEmpty)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (explorationState.Scope != ExplorationScope.DirectCreation)
|
if (explorationState.Scope is not (ExplorationScope.DirectCreation or ExplorationScope.ProjectScaffold))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
runState.EmptyWorkspaceGuardTriggered = true;
|
runState.EmptyWorkspaceGuardTriggered = true;
|
||||||
@@ -116,10 +132,7 @@ public partial class AgentLoopService
|
|||||||
messages.Add(new ChatMessage
|
messages.Add(new ChatMessage
|
||||||
{
|
{
|
||||||
Role = "system",
|
Role = "system",
|
||||||
Content =
|
Content = BuildInitialEmptyWorkspaceGuidance(explorationState, contextHint: null)
|
||||||
"[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."
|
|
||||||
});
|
});
|
||||||
|
|
||||||
EmitEvent(
|
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)
|
private static bool IsExternalWorkspaceEscalationTarget(string? target, string? workFolder)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(target) || string.IsNullOrWhiteSpace(workFolder))
|
if (string.IsNullOrWhiteSpace(target) || string.IsNullOrWhiteSpace(workFolder))
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ public partial class AgentLoopService
|
|||||||
TopicBased,
|
TopicBased,
|
||||||
RepoWide,
|
RepoWide,
|
||||||
OpenEnded,
|
OpenEnded,
|
||||||
/// <summary>문서 생성 요청 — 탐색 단계를 건너뛰고 바로 document_plan/생성 도구 사용.</summary>
|
ProjectScaffold,
|
||||||
DirectCreation,
|
DirectCreation,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,8 +24,8 @@ public partial class AgentLoopService
|
|||||||
public bool BroadScanDetected { get; set; }
|
public bool BroadScanDetected { get; set; }
|
||||||
public bool SelectiveHit { get; set; }
|
public bool SelectiveHit { get; set; }
|
||||||
public bool CorrectiveHintInjected { get; set; }
|
public bool CorrectiveHintInjected { get; set; }
|
||||||
/// <summary>스킬 런타임이 allowed-tools를 명시했으면 true — 탐색 필터링을 건너뜀.</summary>
|
|
||||||
public bool SkillAllowedToolsActive { get; set; }
|
public bool SkillAllowedToolsActive { get; set; }
|
||||||
|
public ProjectScaffoldProfile? ScaffoldProfile { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IReadOnlyCollection<IAgentTool> FilterExplorationToolsForCurrentIteration(
|
private static IReadOnlyCollection<IAgentTool> FilterExplorationToolsForCurrentIteration(
|
||||||
@@ -38,21 +38,37 @@ public partial class AgentLoopService
|
|||||||
if (tools.Count == 0)
|
if (tools.Count == 0)
|
||||||
return tools;
|
return tools;
|
||||||
|
|
||||||
// 스킬 런타임 정책으로 allowed-tools가 명시된 경우 탐색 필터링을 건너뜀
|
|
||||||
// — 스킬이 의도적으로 허용한 도구(folder_map 등)를 정책이 차단하면 안 됨
|
|
||||||
if (state.SkillAllowedToolsActive)
|
if (state.SkillAllowedToolsActive)
|
||||||
return tools;
|
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)
|
if (state.Scope == ExplorationScope.DirectCreation)
|
||||||
{
|
{
|
||||||
var creationFirst = new List<IAgentTool>(tools.Count);
|
var creationFirst = new List<IAgentTool>(tools.Count);
|
||||||
creationFirst.AddRange(tools.Where(IsDocumentCreationTool));
|
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, "folder_map", StringComparison.OrdinalIgnoreCase)
|
||||||
&& !string.Equals(t.Name, "glob", StringComparison.OrdinalIgnoreCase)
|
&& !string.Equals(t.Name, "glob", StringComparison.OrdinalIgnoreCase)
|
||||||
&& !string.Equals(t.Name, "grep", StringComparison.OrdinalIgnoreCase)));
|
&& !string.Equals(t.Name, "grep", StringComparison.OrdinalIgnoreCase)));
|
||||||
// 탐색 도구는 마지막에 (필요 시에만 사용)
|
|
||||||
creationFirst.AddRange(tools.Where(t =>
|
creationFirst.AddRange(tools.Where(t =>
|
||||||
string.Equals(t.Name, "glob", StringComparison.OrdinalIgnoreCase)
|
string.Equals(t.Name, "glob", StringComparison.OrdinalIgnoreCase)
|
||||||
|| string.Equals(t.Name, "grep", StringComparison.OrdinalIgnoreCase)));
|
|| string.Equals(t.Name, "grep", StringComparison.OrdinalIgnoreCase)));
|
||||||
@@ -66,12 +82,11 @@ public partial class AgentLoopService
|
|||||||
var ordered = new List<IAgentTool>(tools.Count);
|
var ordered = new List<IAgentTool>(tools.Count);
|
||||||
|
|
||||||
ordered.AddRange(tools.Where(IsSelectiveDiscoveryTool));
|
ordered.AddRange(tools.Where(IsSelectiveDiscoveryTool));
|
||||||
ordered.AddRange(tools.Where(t => !IsSelectiveDiscoveryTool(t) &&
|
ordered.AddRange(tools.Where(t =>
|
||||||
(allowFolderMap || !string.Equals(t.Name, "folder_map", StringComparison.OrdinalIgnoreCase))));
|
!IsSelectiveDiscoveryTool(t)
|
||||||
|
&& (allowFolderMap || !string.Equals(t.Name, "folder_map", StringComparison.OrdinalIgnoreCase))));
|
||||||
if (allowFolderMap)
|
if (allowFolderMap)
|
||||||
{
|
|
||||||
ordered.AddRange(tools.Where(t => string.Equals(t.Name, "folder_map", StringComparison.OrdinalIgnoreCase)));
|
ordered.AddRange(tools.Where(t => string.Equals(t.Name, "folder_map", StringComparison.OrdinalIgnoreCase)));
|
||||||
}
|
|
||||||
|
|
||||||
return ordered
|
return ordered
|
||||||
.DistinctBy(t => t.Name, StringComparer.OrdinalIgnoreCase)
|
.DistinctBy(t => t.Name, StringComparer.OrdinalIgnoreCase)
|
||||||
@@ -86,14 +101,19 @@ public partial class AgentLoopService
|
|||||||
or "document_assemble" or "file_write";
|
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(
|
private static bool ShouldAllowFolderMapForCurrentIteration(
|
||||||
ExplorationTrackingState state,
|
ExplorationTrackingState state,
|
||||||
string userQuery,
|
string userQuery,
|
||||||
string? activeTab,
|
string? activeTab,
|
||||||
int totalToolCalls)
|
int totalToolCalls)
|
||||||
{
|
{
|
||||||
// 문서 생성 모드에서는 folder_map 차단 — 탐색 없이 바로 생성
|
if (state.Scope is ExplorationScope.DirectCreation or ExplorationScope.ProjectScaffold)
|
||||||
if (state.Scope == ExplorationScope.DirectCreation)
|
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (state.Scope is ExplorationScope.RepoWide or ExplorationScope.OpenEnded)
|
if (state.Scope is ExplorationScope.RepoWide or ExplorationScope.OpenEnded)
|
||||||
@@ -116,23 +136,8 @@ public partial class AgentLoopService
|
|||||||
|
|
||||||
return ContainsAny(
|
return ContainsAny(
|
||||||
userQuery,
|
userQuery,
|
||||||
"folder",
|
"folder", "directory", "tree", "structure", "list files", "workspace layout", "work folder", "project structure",
|
||||||
"directory",
|
"폴더", "디렉터리", "폴더 구조", "디렉터리 구조", "파일 목록", "작업 폴더", "구조를 보여", "구조 확인");
|
||||||
"tree",
|
|
||||||
"structure",
|
|
||||||
"list files",
|
|
||||||
"workspace layout",
|
|
||||||
"work folder",
|
|
||||||
"project structure",
|
|
||||||
"폴더",
|
|
||||||
"디렉터리",
|
|
||||||
"폴더 구조",
|
|
||||||
"디렉터리 구조",
|
|
||||||
"파일 목록",
|
|
||||||
"작업 폴더",
|
|
||||||
"폴더 안",
|
|
||||||
"구조를 보여",
|
|
||||||
"구조 확인");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsExistingMaterialReferenceRequest(string userQuery)
|
private static bool IsExistingMaterialReferenceRequest(string userQuery)
|
||||||
@@ -142,21 +147,8 @@ public partial class AgentLoopService
|
|||||||
|
|
||||||
return ContainsAny(
|
return ContainsAny(
|
||||||
userQuery,
|
userQuery,
|
||||||
"existing file",
|
"existing file", "existing files", "existing document", "existing documents", "reference files", "reference docs", "existing materials",
|
||||||
"existing files",
|
"기존 파일", "기존 문서", "기존 자료", "참고 파일", "참고 문서", "작업 폴더 파일");
|
||||||
"existing document",
|
|
||||||
"existing documents",
|
|
||||||
"reference files",
|
|
||||||
"reference docs",
|
|
||||||
"existing materials",
|
|
||||||
"기존 파일",
|
|
||||||
"기존 문서",
|
|
||||||
"기존 자료",
|
|
||||||
"참고 파일",
|
|
||||||
"참고 문서",
|
|
||||||
"폴더 내 자료",
|
|
||||||
"안의 자료",
|
|
||||||
"작업 폴더 파일");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsSelectiveDiscoveryTool(IAgentTool tool)
|
private static bool IsSelectiveDiscoveryTool(IAgentTool tool)
|
||||||
@@ -171,27 +163,9 @@ public partial class AgentLoopService
|
|||||||
|
|
||||||
return ContainsAny(
|
return ContainsAny(
|
||||||
userQuery,
|
userQuery,
|
||||||
"definition",
|
"definition", "reference", "references", "implementation", "implementations",
|
||||||
"reference",
|
"caller", "callers", "callee", "call hierarchy", "symbol", "symbols", "interface", "override",
|
||||||
"references",
|
"정의", "참조", "구현", "호출부", "호출 관계", "심볼", "인터페이스", "오버라이드");
|
||||||
"implementation",
|
|
||||||
"implementations",
|
|
||||||
"caller",
|
|
||||||
"callers",
|
|
||||||
"callee",
|
|
||||||
"call hierarchy",
|
|
||||||
"symbol",
|
|
||||||
"symbols",
|
|
||||||
"interface",
|
|
||||||
"override",
|
|
||||||
"정의",
|
|
||||||
"참조",
|
|
||||||
"구현",
|
|
||||||
"호출부",
|
|
||||||
"호출 관계",
|
|
||||||
"심볼",
|
|
||||||
"인터페이스",
|
|
||||||
"오버라이드");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string BuildPreferredInitialToolSequence(
|
private static string BuildPreferredInitialToolSequence(
|
||||||
@@ -200,7 +174,14 @@ public partial class AgentLoopService
|
|||||||
string? activeTab,
|
string? activeTab,
|
||||||
string userQuery)
|
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 (state.Scope == ExplorationScope.DirectCreation)
|
||||||
{
|
{
|
||||||
if (string.Equals(activeTab, "Code", StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(activeTab, "Code", StringComparison.OrdinalIgnoreCase))
|
||||||
@@ -233,31 +214,28 @@ public partial class AgentLoopService
|
|||||||
if (string.IsNullOrWhiteSpace(userQuery))
|
if (string.IsNullOrWhiteSpace(userQuery))
|
||||||
return ExplorationScope.OpenEnded;
|
return ExplorationScope.OpenEnded;
|
||||||
|
|
||||||
var q = userQuery.Trim();
|
var lower = userQuery.Trim().ToLowerInvariant();
|
||||||
var lower = q.ToLowerInvariant();
|
|
||||||
|
|
||||||
// Cowork 탭에서 문서 생성 의도가 있으면 탐색을 건너뛰고 바로 생성 도구 사용
|
|
||||||
if (!string.Equals(activeTab, "Code", StringComparison.OrdinalIgnoreCase)
|
if (!string.Equals(activeTab, "Code", StringComparison.OrdinalIgnoreCase)
|
||||||
&& HasDocumentCreationIntent(lower))
|
&& HasDocumentCreationIntent(lower))
|
||||||
return ExplorationScope.DirectCreation;
|
return ExplorationScope.DirectCreation;
|
||||||
|
|
||||||
|
if (string.Equals(activeTab, "Code", StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& ProjectScaffoldProfileCatalog.IsStructuredProjectRequest(lower, activeTab))
|
||||||
|
return ExplorationScope.ProjectScaffold;
|
||||||
|
|
||||||
if (string.Equals(activeTab, "Code", StringComparison.OrdinalIgnoreCase)
|
if (string.Equals(activeTab, "Code", StringComparison.OrdinalIgnoreCase)
|
||||||
&& HasCodeArtifactCreationIntent(lower))
|
&& HasCodeArtifactCreationIntent(lower))
|
||||||
return ExplorationScope.DirectCreation;
|
return ExplorationScope.DirectCreation;
|
||||||
|
|
||||||
if (lower.Contains("전체") || lower.Contains("전반") || lower.Contains("코드베이스 전체") ||
|
if (ContainsAny(lower, "전체", "전반", "코드베이스 전체", "repo-wide", "repository-wide", "전체 구조", "아키텍처", "전체 맥락"))
|
||||||
lower.Contains("repo-wide") || lower.Contains("repository-wide") || lower.Contains("전체 구조") ||
|
|
||||||
lower.Contains("아키텍처") || lower.Contains("전체 점검"))
|
|
||||||
return ExplorationScope.RepoWide;
|
return ExplorationScope.RepoWide;
|
||||||
|
|
||||||
if (q.Contains('.') || q.Contains('/') || q.Contains('\\') ||
|
if (lower.Contains('.') || lower.Contains('/') || lower.Contains('\\') ||
|
||||||
lower.Contains("file ") || lower.Contains("class ") || lower.Contains("method ") ||
|
ContainsAny(lower, "file ", "class ", "method ", "function ", "line ", "bug", "오류", "버그", "예외"))
|
||||||
lower.Contains("function ") || lower.Contains("line ") || lower.Contains("bug") ||
|
|
||||||
lower.Contains("오류") || lower.Contains("버그") || lower.Contains("예외"))
|
|
||||||
return ExplorationScope.Localized;
|
return ExplorationScope.Localized;
|
||||||
|
|
||||||
if (lower.Contains("정리") || lower.Contains("요약") || lower.Contains("보고서") ||
|
if (ContainsAny(lower, "정리", "요약", "보고", "주제", "관련", "분석"))
|
||||||
lower.Contains("주제") || lower.Contains("관련") || lower.Contains("분석"))
|
|
||||||
return ExplorationScope.TopicBased;
|
return ExplorationScope.TopicBased;
|
||||||
|
|
||||||
return string.Equals(activeTab, "Code", StringComparison.OrdinalIgnoreCase)
|
return string.Equals(activeTab, "Code", StringComparison.OrdinalIgnoreCase)
|
||||||
@@ -265,38 +243,30 @@ public partial class AgentLoopService
|
|||||||
: ExplorationScope.OpenEnded;
|
: ExplorationScope.OpenEnded;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 사용자가 문서를 새로 생성/작성하려는 의도인지 판별합니다.
|
|
||||||
/// "보고서 작성해줘", "문서 만들어줘" 등 생성 동사가 포함된 경우 true.
|
|
||||||
/// </summary>
|
|
||||||
private static bool HasDocumentCreationIntent(string lowerQuery)
|
private static bool HasDocumentCreationIntent(string lowerQuery)
|
||||||
{
|
{
|
||||||
// 생성 동사 키워드
|
var hasCreationVerb = ContainsAny(
|
||||||
var hasCreationVerb = ContainsAny(lowerQuery,
|
lowerQuery,
|
||||||
"작성해", "써줘", "써 줘", "만들어", "생성해", "작성 해",
|
"작성", "써줘", "만들", "생성", "만들어줘", "생성해줘",
|
||||||
"만들어줘", "만들어 줘", "생성해줘", "생성해 줘",
|
"write", "create", "draft", "generate", "compose");
|
||||||
"write", "create", "draft", "generate", "compose",
|
|
||||||
"작성하", "작성을", "생성하", "생성을",
|
|
||||||
"리포트 써", "보고서 써", "문서 써",
|
|
||||||
"작성 부탁", "만들어 부탁");
|
|
||||||
|
|
||||||
if (!hasCreationVerb)
|
if (!hasCreationVerb)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
// 생성 대상이 문서/보고서/자료 등인지 확인
|
return ContainsAny(
|
||||||
return ContainsAny(lowerQuery,
|
lowerQuery,
|
||||||
"보고서", "문서", "제안서", "리포트", "분석서", "기획서",
|
"보고서", "문서", "제안서", "리포트", "분석서", "기획서", "요약", "발표자료", "ppt", "pptx", "docx", "xlsx", "excel", "word", "sheet", "chart",
|
||||||
"report", "document", "proposal", "analysis",
|
"report", "document", "proposal", "analysis", "presentation", "template", "spreadsheet", "excel", "memo");
|
||||||
"요약서", "발표자료", "ppt", "pptx", "docx", "xlsx", "excel", "word",
|
|
||||||
"표", "차트", "스프레드시트", "프레젠테이션",
|
|
||||||
"정리해", "정리 해");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool HasCodeArtifactCreationIntent(string lowerQuery)
|
private static bool HasCodeArtifactCreationIntent(string lowerQuery)
|
||||||
{
|
{
|
||||||
|
if (ProjectScaffoldProfileCatalog.IsStructuredProjectRequest(lowerQuery, "Code"))
|
||||||
|
return true;
|
||||||
|
|
||||||
var hasCreationVerb = ContainsAny(
|
var hasCreationVerb = ContainsAny(
|
||||||
lowerQuery,
|
lowerQuery,
|
||||||
"만들", "생성", "작성", "create", "generate", "build", "write", "scaffold", "draft");
|
"만들", "생성", "작성", "구현", "create", "generate", "build", "write", "scaffold", "draft");
|
||||||
|
|
||||||
if (!hasCreationVerb)
|
if (!hasCreationVerb)
|
||||||
return false;
|
return false;
|
||||||
@@ -310,35 +280,52 @@ public partial class AgentLoopService
|
|||||||
"template", "템플릿", "index.html", "app.js", "style.css");
|
"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
|
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 =>
|
ExplorationScope.DirectCreation =>
|
||||||
"Exploration scope = direct-creation. The user wants to CREATE a new document/report/file. " +
|
"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 — skip exploration entirely. " +
|
"Do not search for existing files with glob/grep/folder_map unless the user explicitly asked to inspect the workspace. " +
|
||||||
"Call document_plan first to outline the document structure, then immediately call the appropriate creation tool " +
|
"Produce a real file on disk instead of only describing the result.",
|
||||||
"(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.",
|
|
||||||
ExplorationScope.Localized =>
|
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 =>
|
ExplorationScope.TopicBased =>
|
||||||
"Exploration scope = topic-based. Identify candidate files by topic keywords first with glob/grep, then read only a small targeted set.",
|
"Exploration scope = topic-based. Identify candidate files by topic keywords first with glob/grep, then read only a small targeted set.",
|
||||||
ExplorationScope.RepoWide =>
|
ExplorationScope.RepoWide =>
|
||||||
"Exploration scope = repo-wide. Broad structure inspection is allowed when needed.",
|
"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 =
|
guidance =
|
||||||
"Exploration scope = direct-creation. The user wants to CREATE a new file or document. " +
|
"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. " +
|
"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 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. " +
|
"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. " +
|
"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
|
messages.Add(new ChatMessage
|
||||||
@@ -353,8 +340,8 @@ public partial class AgentLoopService
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var doc = JsonDocument.Parse(argsJson);
|
using var doc = JsonDocument.Parse(argsJson);
|
||||||
if (doc.RootElement.TryGetProperty("paths", out var pathsEl) && pathsEl.ValueKind == JsonValueKind.Array)
|
if (doc.RootElement.TryGetProperty("paths", out var pathsElement) && pathsElement.ValueKind == JsonValueKind.Array)
|
||||||
return pathsEl.GetArrayLength();
|
return pathsElement.GetArrayLength();
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
@@ -368,13 +355,12 @@ public partial class AgentLoopService
|
|||||||
string toolName,
|
string toolName,
|
||||||
string argsJson)
|
string argsJson)
|
||||||
{
|
{
|
||||||
// 문서 생성 모드: 탐색 도구가 1회라도 호출되면 즉시 교정
|
if (state.Scope is ExplorationScope.DirectCreation or ExplorationScope.ProjectScaffold)
|
||||||
if (state.Scope == ExplorationScope.DirectCreation)
|
|
||||||
{
|
{
|
||||||
return string.Equals(toolName, "folder_map", StringComparison.OrdinalIgnoreCase)
|
return string.Equals(toolName, "folder_map", StringComparison.OrdinalIgnoreCase)
|
||||||
|| string.Equals(toolName, "glob", StringComparison.OrdinalIgnoreCase)
|
|| string.Equals(toolName, "glob", StringComparison.OrdinalIgnoreCase)
|
||||||
|| string.Equals(toolName, "grep", 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)
|
if (state.Scope is ExplorationScope.RepoWide or ExplorationScope.OpenEnded)
|
||||||
@@ -391,11 +377,11 @@ public partial class AgentLoopService
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var doc = JsonDocument.Parse(argsJson);
|
using var doc = JsonDocument.Parse(argsJson);
|
||||||
var includeFiles = doc.RootElement.TryGetProperty("include_files", out var includeFilesEl) &&
|
var includeFiles = doc.RootElement.TryGetProperty("include_files", out var includeFilesElement)
|
||||||
includeFilesEl.ValueKind is JsonValueKind.True or JsonValueKind.False &&
|
&& includeFilesElement.ValueKind is JsonValueKind.True or JsonValueKind.False
|
||||||
includeFilesEl.GetBoolean();
|
&& includeFilesElement.GetBoolean();
|
||||||
var depth = doc.RootElement.TryGetProperty("depth", out var depthEl) && depthEl.ValueKind == JsonValueKind.Number
|
var depth = doc.RootElement.TryGetProperty("depth", out var depthElement) && depthElement.ValueKind == JsonValueKind.Number
|
||||||
? depthEl.GetInt32()
|
? depthElement.GetInt32()
|
||||||
: 2;
|
: 2;
|
||||||
if (includeFiles || depth >= 3)
|
if (includeFiles || depth >= 3)
|
||||||
return true;
|
return true;
|
||||||
@@ -429,8 +415,8 @@ public partial class AgentLoopService
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.Equals(toolName, "file_read", StringComparison.OrdinalIgnoreCase) ||
|
if (string.Equals(toolName, "file_read", StringComparison.OrdinalIgnoreCase)
|
||||||
string.Equals(toolName, "document_read", StringComparison.OrdinalIgnoreCase))
|
|| string.Equals(toolName, "document_read", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
state.TotalFilesRead++;
|
state.TotalFilesRead++;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,10 +60,12 @@ public partial class AgentLoopService
|
|||||||
{
|
{
|
||||||
var intentGate = new IntentGateService(_llm);
|
var intentGate = new IntentGateService(_llm);
|
||||||
var intentResult = await intentGate.ClassifyAsync(userQuery, ActiveTab, ct).ConfigureAwait(false);
|
var intentResult = await intentGate.ClassifyAsync(userQuery, ActiveTab, ct).ConfigureAwait(false);
|
||||||
|
var scaffoldProfile = ProjectScaffoldProfileCatalog.Detect(userQuery, ActiveTab);
|
||||||
var explorationState = new ExplorationTrackingState
|
var explorationState = new ExplorationTrackingState
|
||||||
{
|
{
|
||||||
Scope = intentResult.SuggestedScope,
|
Scope = scaffoldProfile == null ? intentResult.SuggestedScope : ExplorationScope.ProjectScaffold,
|
||||||
SelectiveHit = true,
|
SelectiveHit = true,
|
||||||
|
ScaffoldProfile = scaffoldProfile,
|
||||||
};
|
};
|
||||||
var pathAccessState = new PathAccessTrackingState();
|
var pathAccessState = new PathAccessTrackingState();
|
||||||
var sessionLearnings = (_settings.Settings.Llm.EnableSessionLearnings)
|
var sessionLearnings = (_settings.Settings.Llm.EnableSessionLearnings)
|
||||||
@@ -79,7 +81,7 @@ public partial class AgentLoopService
|
|||||||
maxRetry = ComputeQualityAwareMaxRetry(maxRetry, recentTaskRetryQuality, taskPolicy.TaskType);
|
maxRetry = ComputeQualityAwareMaxRetry(maxRetry, recentTaskRetryQuality, taskPolicy.TaskType);
|
||||||
|
|
||||||
InjectTaskTypeGuidance(messages, taskPolicy);
|
InjectTaskTypeGuidance(messages, taskPolicy);
|
||||||
InjectExplorationScopeGuidance(messages, explorationState.Scope);
|
InjectExplorationScopeGuidance(messages, explorationState);
|
||||||
|
|
||||||
if (intentResult.IsComplexTask && !string.IsNullOrWhiteSpace(intentResult.DecompositionHint))
|
if (intentResult.IsComplexTask && !string.IsNullOrWhiteSpace(intentResult.DecompositionHint))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -298,6 +298,7 @@ public partial class AgentLoopService
|
|||||||
var context = BuildContext();
|
var context = BuildContext();
|
||||||
context.InitialUserQuery = userQuery;
|
context.InitialUserQuery = userQuery;
|
||||||
runState.WorkspaceAppearsEmpty = DetectEmptyWorkspace(context.WorkFolder);
|
runState.WorkspaceAppearsEmpty = DetectEmptyWorkspace(context.WorkFolder);
|
||||||
|
var workspaceWasInitiallyEmpty = runState.WorkspaceAppearsEmpty;
|
||||||
|
|
||||||
var preferredInitialToolSequence = BuildPreferredInitialToolSequence(
|
var preferredInitialToolSequence = BuildPreferredInitialToolSequence(
|
||||||
explorationState,
|
explorationState,
|
||||||
@@ -309,6 +310,7 @@ public partial class AgentLoopService
|
|||||||
"",
|
"",
|
||||||
explorationState.Scope switch
|
explorationState.Scope switch
|
||||||
{
|
{
|
||||||
|
ExplorationScope.ProjectScaffold => "프로젝트 스캐폴드 모드 · 최소 폴더 구조부터 만드는 중",
|
||||||
ExplorationScope.DirectCreation => string.Equals(ActiveTab, "Code", StringComparison.OrdinalIgnoreCase)
|
ExplorationScope.DirectCreation => string.Equals(ActiveTab, "Code", StringComparison.OrdinalIgnoreCase)
|
||||||
? "즉시 생성 모드 · 바로 파일을 만드는 중"
|
? "즉시 생성 모드 · 바로 파일을 만드는 중"
|
||||||
: "문서 생성 모드 · 바로 문서를 만드는 중",
|
: "문서 생성 모드 · 바로 문서를 만드는 중",
|
||||||
@@ -1042,6 +1044,9 @@ public partial class AgentLoopService
|
|||||||
taskPolicy,
|
taskPolicy,
|
||||||
requireHighImpactCodeVerification,
|
requireHighImpactCodeVerification,
|
||||||
totalToolCalls,
|
totalToolCalls,
|
||||||
|
context,
|
||||||
|
explorationState,
|
||||||
|
workspaceWasInitiallyEmpty,
|
||||||
runState,
|
runState,
|
||||||
executionPolicy))
|
executionPolicy))
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -1238,6 +1238,7 @@ public partial class AgentLoopService
|
|||||||
public int ExecutionSuccessGateRetry;
|
public int ExecutionSuccessGateRetry;
|
||||||
public int HighImpactBuildTestGateRetry;
|
public int HighImpactBuildTestGateRetry;
|
||||||
public int CodeVerificationGateRetry;
|
public int CodeVerificationGateRetry;
|
||||||
|
public int ProjectLayoutGateRetry;
|
||||||
public int FinalReportGateRetry;
|
public int FinalReportGateRetry;
|
||||||
public int TransientLlmErrorRetries;
|
public int TransientLlmErrorRetries;
|
||||||
public int DocumentArtifactGateRetry;
|
public int DocumentArtifactGateRetry;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using AxCopilot.Models;
|
using AxCopilot.Models;
|
||||||
|
|
||||||
namespace AxCopilot.Services.Agent;
|
namespace AxCopilot.Services.Agent;
|
||||||
|
|
||||||
@@ -28,8 +28,8 @@ public partial class AgentLoopService
|
|||||||
taskPolicy)
|
taskPolicy)
|
||||||
});
|
});
|
||||||
EmitEvent(AgentEventType.Thinking, "", highImpactCodeChange
|
EmitEvent(AgentEventType.Thinking, "", highImpactCodeChange
|
||||||
? "고영향 코드 변경으로 분류돼 참조 검증과 build/test 검증을 더 엄격하게 이어갑니다."
|
? "怨좎쁺??肄붾뱶 蹂寃쎌쑝濡?遺꾨쪟??李몄“ 寃利앷낵 build/test 寃利앹쓣 ???꾧꺽?섍쾶 ?댁뼱媛묐땲??"
|
||||||
: "코드 변경 후 build/test/diff 검증을 이어갑니다.");
|
: "肄붾뱶 蹂寃???build/test/diff 寃利앹쓣 ?댁뼱媛묐땲??");
|
||||||
}
|
}
|
||||||
else if (HasCodeVerificationEvidenceAfterLastModification(messages, requireHighImpactCodeVerification))
|
else if (HasCodeVerificationEvidenceAfterLastModification(messages, requireHighImpactCodeVerification))
|
||||||
{
|
{
|
||||||
@@ -43,12 +43,24 @@ public partial class AgentLoopService
|
|||||||
TaskTypePolicy taskPolicy,
|
TaskTypePolicy taskPolicy,
|
||||||
bool requireHighImpactCodeVerification,
|
bool requireHighImpactCodeVerification,
|
||||||
int totalToolCalls,
|
int totalToolCalls,
|
||||||
|
AgentContext context,
|
||||||
|
ExplorationTrackingState explorationState,
|
||||||
|
bool workspaceWasInitiallyEmpty,
|
||||||
RunState runState,
|
RunState runState,
|
||||||
ModelExecutionProfileCatalog.ExecutionPolicy executionPolicy)
|
ModelExecutionProfileCatalog.ExecutionPolicy executionPolicy)
|
||||||
{
|
{
|
||||||
if (!string.Equals(ActiveTab, "Code", StringComparison.OrdinalIgnoreCase) || totalToolCalls <= 0)
|
if (!string.Equals(ActiveTab, "Code", StringComparison.OrdinalIgnoreCase) || totalToolCalls <= 0)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
if (TryApplyProjectLayoutGateTransition(
|
||||||
|
messages,
|
||||||
|
textResponse,
|
||||||
|
context,
|
||||||
|
explorationState,
|
||||||
|
workspaceWasInitiallyEmpty,
|
||||||
|
runState))
|
||||||
|
return true;
|
||||||
|
|
||||||
var hasCodeVerificationEvidence = HasCodeVerificationEvidenceAfterLastModification(
|
var hasCodeVerificationEvidence = HasCodeVerificationEvidenceAfterLastModification(
|
||||||
messages,
|
messages,
|
||||||
requireHighImpactCodeVerification);
|
requireHighImpactCodeVerification);
|
||||||
@@ -69,12 +81,12 @@ public partial class AgentLoopService
|
|||||||
{
|
{
|
||||||
Role = "user",
|
Role = "user",
|
||||||
Content = requireHighImpactCodeVerification
|
Content = requireHighImpactCodeVerification
|
||||||
? "[System:CodeQualityGate] 공용/핵심 코드 변경 이후 검증 근거가 부족합니다. 종료하지 말고 file_read, grep/glob, git diff, build/test까지 확인한 뒤에만 마무리하세요."
|
? "[System:CodeQualityGate] 怨듭슜/?듭떖 肄붾뱶 蹂寃??댄썑 寃利?洹쇨굅媛 遺議깊빀?덈떎. 醫낅즺?섏? 留먭퀬 file_read, grep/glob, git diff, build/test源뚯? ?뺤씤???ㅼ뿉留?留덈Т由ы븯?몄슂."
|
||||||
: "[System:CodeQualityGate] 마지막 코드 수정 이후 build/test/file_read/diff 근거가 부족합니다. 종료하지 말고 검증 근거를 보강한 뒤에만 마무리하세요."
|
: "[System:CodeQualityGate] 留덉?留?肄붾뱶 ?섏젙 ?댄썑 build/test/file_read/diff 洹쇨굅媛 遺議깊빀?덈떎. 醫낅즺?섏? 留먭퀬 寃利?洹쇨굅瑜?蹂닿컯???ㅼ뿉留?留덈Т由ы븯?몄슂."
|
||||||
});
|
});
|
||||||
EmitEvent(AgentEventType.Thinking, "", requireHighImpactCodeVerification
|
EmitEvent(AgentEventType.Thinking, "", requireHighImpactCodeVerification
|
||||||
? "핵심 코드 변경의 검증 근거가 부족해 추가 검증을 진행합니다..."
|
? "?듭떖 肄붾뱶 蹂寃쎌쓽 寃利?洹쇨굅媛 遺議깊빐 異붽? 寃利앹쓣 吏꾪뻾?⑸땲??.."
|
||||||
: "코드 결과 검증 근거가 부족해 추가 검증을 진행합니다...");
|
: "肄붾뱶 寃곌낵 寃利?洹쇨굅媛 遺議깊빐 異붽? 寃利앹쓣 吏꾪뻾?⑸땲??..");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,9 +101,9 @@ public partial class AgentLoopService
|
|||||||
messages.Add(new ChatMessage
|
messages.Add(new ChatMessage
|
||||||
{
|
{
|
||||||
Role = "user",
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,13 +127,92 @@ public partial class AgentLoopService
|
|||||||
Role = "user",
|
Role = "user",
|
||||||
Content = BuildFinalReportQualityPrompt(taskPolicy, requireHighImpactCodeVerification)
|
Content = BuildFinalReportQualityPrompt(taskPolicy, requireHighImpactCodeVerification)
|
||||||
});
|
});
|
||||||
EmitEvent(AgentEventType.Thinking, "", "최종 보고에 변경·검증·리스크 요약이 부족해 한 번 더 정리합니다...");
|
EmitEvent(AgentEventType.Thinking, "", "理쒖쥌 蹂닿퀬??蹂寃승룰?利씲룸━?ㅽ겕 ?붿빟??遺議깊빐 ??踰????뺣━?⑸땲??..");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
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(
|
private bool TryApplyCodeDiffEvidenceGateTransition(
|
||||||
List<ChatMessage> messages,
|
List<ChatMessage> messages,
|
||||||
string? textResponse,
|
string? textResponse,
|
||||||
@@ -147,9 +238,9 @@ public partial class AgentLoopService
|
|||||||
messages.Add(new ChatMessage
|
messages.Add(new ChatMessage
|
||||||
{
|
{
|
||||||
Role = "user",
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,7 +277,7 @@ public partial class AgentLoopService
|
|||||||
Role = "user",
|
Role = "user",
|
||||||
Content = BuildRecentExecutionEvidencePrompt(taskPolicy)
|
Content = BuildRecentExecutionEvidencePrompt(taskPolicy)
|
||||||
});
|
});
|
||||||
EmitEvent(AgentEventType.Thinking, "", "최근 수정 이후 실행 근거가 부족해 build/test 재검증을 수행합니다...");
|
EmitEvent(AgentEventType.Thinking, "", "理쒓렐 ?섏젙 ?댄썑 ?ㅽ뻾 洹쇨굅媛 遺議깊빐 build/test ?ш?利앹쓣 ?섑뻾?⑸땲??..");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,7 +314,7 @@ public partial class AgentLoopService
|
|||||||
Role = "user",
|
Role = "user",
|
||||||
Content = BuildExecutionSuccessGatePrompt(taskPolicy)
|
Content = BuildExecutionSuccessGatePrompt(taskPolicy)
|
||||||
});
|
});
|
||||||
EmitEvent(AgentEventType.Thinking, "", "실패한 실행 근거만 있어 build/test 성공 결과를 다시 검증합니다...");
|
EmitEvent(AgentEventType.Thinking, "", "?ㅽ뙣???ㅽ뻾 洹쇨굅留??덉뼱 build/test ?깃났 寃곌낵瑜??ㅼ떆 寃利앺빀?덈떎...");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,7 +354,7 @@ public partial class AgentLoopService
|
|||||||
Role = "user",
|
Role = "user",
|
||||||
Content = BuildTerminalEvidenceGatePrompt(taskPolicy, lastArtifactFilePath)
|
Content = BuildTerminalEvidenceGatePrompt(taskPolicy, lastArtifactFilePath)
|
||||||
});
|
});
|
||||||
EmitEvent(AgentEventType.Thinking, "", $"종료 전 실행 증거가 부족해 보강 단계를 진행합니다 ({runState.TerminalEvidenceGateRetry}/{retryMax})");
|
EmitEvent(AgentEventType.Thinking, "", $"醫낅즺 ???ㅽ뻾 利앷굅媛 遺議깊빐 蹂닿컯 ?④퀎瑜?吏꾪뻾?⑸땲??({runState.TerminalEvidenceGateRetry}/{retryMax})");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,126 +1,117 @@
|
|||||||
|
using AxCopilot.Services;
|
||||||
using static AxCopilot.Services.Agent.AgentLoopService;
|
using static AxCopilot.Services.Agent.AgentLoopService;
|
||||||
|
|
||||||
namespace AxCopilot.Services.Agent;
|
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
|
internal sealed class IntentGateService
|
||||||
{
|
{
|
||||||
private readonly ILlmService? _llm;
|
private readonly ILlmService? _llm;
|
||||||
|
|
||||||
/// <summary>DetectComplexTask에서 매번 재생성 방지용 정적 배열.</summary>
|
|
||||||
private static readonly string[] Conjunctions =
|
private static readonly string[] Conjunctions =
|
||||||
{
|
[
|
||||||
"그리고", "하고", "다음에", "이후에", "그런 다음",
|
"그리고",
|
||||||
" and then ", " after that ", " also ", " additionally "
|
"하고",
|
||||||
};
|
"다음",
|
||||||
|
"이후",
|
||||||
|
"그런 다음",
|
||||||
|
" and then ",
|
||||||
|
" after that ",
|
||||||
|
" also ",
|
||||||
|
" additionally "
|
||||||
|
];
|
||||||
|
|
||||||
private static readonly string[] ActionVerbs =
|
private static readonly string[] ActionVerbs =
|
||||||
{
|
[
|
||||||
"해줘", "해 줘", "만들어", "수정해", "분석해", "작성해",
|
"해줘",
|
||||||
"검토해", "확인해", "추가해", "삭제해", "변경해"
|
"해 줘",
|
||||||
};
|
"만들",
|
||||||
|
"수정",
|
||||||
|
"분석",
|
||||||
|
"작성",
|
||||||
|
"검토",
|
||||||
|
"확인",
|
||||||
|
"추가",
|
||||||
|
"삭제",
|
||||||
|
"변경",
|
||||||
|
"fix",
|
||||||
|
"review",
|
||||||
|
"analyze",
|
||||||
|
"write",
|
||||||
|
"create",
|
||||||
|
"implement",
|
||||||
|
"add",
|
||||||
|
"remove",
|
||||||
|
"change"
|
||||||
|
];
|
||||||
|
|
||||||
/// <summary>입력 길이 제한 — 50KB 이상은 잘라서 처리.</summary>
|
|
||||||
private const int MaxInputLength = 50_000;
|
private const int MaxInputLength = 50_000;
|
||||||
|
|
||||||
public IntentGateService(ILlmService? llm = null) => _llm = llm;
|
public IntentGateService(ILlmService? llm = null) => _llm = llm;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 사용자 쿼리를 분석하여 IntentResult를 생성합니다.
|
|
||||||
/// </summary>
|
|
||||||
public Task<IntentResult> ClassifyAsync(
|
public Task<IntentResult> ClassifyAsync(
|
||||||
string userQuery, string? activeTab, CancellationToken ct = default)
|
string userQuery, string? activeTab, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
ct.ThrowIfCancellationRequested();
|
ct.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
// 안전 가드: null/과도한 길이
|
var safeQuery = userQuery ?? string.Empty;
|
||||||
var safeQuery = userQuery ?? "";
|
|
||||||
if (safeQuery.Length > MaxInputLength)
|
if (safeQuery.Length > MaxInputLength)
|
||||||
safeQuery = safeQuery[..MaxInputLength];
|
safeQuery = safeQuery[..MaxInputLength];
|
||||||
|
|
||||||
// 한 번만 lowercase 변환 후 하위 메서드에 전달
|
|
||||||
var lowerQuery = safeQuery.ToLowerInvariant();
|
var lowerQuery = safeQuery.ToLowerInvariant();
|
||||||
|
|
||||||
// ── Stage 1: 키워드 분류 ──
|
|
||||||
var taskType = ClassifyTaskTypeKeyword(safeQuery, activeTab);
|
var taskType = ClassifyTaskTypeKeyword(safeQuery, activeTab);
|
||||||
var (intentCategory, intentConfidence) = IntentDetector.Detect(safeQuery);
|
var (intentCategory, intentConfidence) = IntentDetector.Detect(safeQuery);
|
||||||
|
|
||||||
// 종합 confidence: taskType 확정도 + IntentDetector 확신도 가중 평균
|
|
||||||
var taskTypeConfidence = ComputeTaskTypeConfidence(taskType, lowerQuery);
|
var taskTypeConfidence = ComputeTaskTypeConfidence(taskType, lowerQuery);
|
||||||
var combinedConfidence = Math.Min(1.0,
|
var combinedConfidence = Math.Min(1.0, taskTypeConfidence * 0.6 + intentConfidence * 0.4);
|
||||||
taskTypeConfidence * 0.6 + intentConfidence * 0.4);
|
|
||||||
|
|
||||||
// ── Stage 2: 프로파일 매핑 ──
|
|
||||||
var overlay = MapToOverlay(taskType, intentCategory, activeTab);
|
var overlay = MapToOverlay(taskType, intentCategory, activeTab);
|
||||||
var scope = ClassifyScopeFromIntent(lowerQuery, activeTab, taskType, intentCategory);
|
var scope = ClassifyScopeFromIntent(lowerQuery, activeTab, taskType, intentCategory);
|
||||||
|
|
||||||
// ── 복합 요청 감지 (P5 연동) ──
|
|
||||||
var (isComplex, hint) = DetectComplexTask(lowerQuery);
|
var (isComplex, hint) = DetectComplexTask(lowerQuery);
|
||||||
|
|
||||||
var result = new IntentResult(
|
return Task.FromResult(new IntentResult(
|
||||||
TaskType: taskType,
|
TaskType: taskType,
|
||||||
IntentCategory: intentCategory,
|
IntentCategory: intentCategory,
|
||||||
Confidence: Math.Round(combinedConfidence, 2, MidpointRounding.AwayFromZero),
|
Confidence: Math.Round(combinedConfidence, 2, MidpointRounding.AwayFromZero),
|
||||||
PolicyOverlay: overlay,
|
PolicyOverlay: overlay,
|
||||||
SuggestedScope: scope,
|
SuggestedScope: scope,
|
||||||
IsComplexTask: isComplex,
|
IsComplexTask: isComplex,
|
||||||
DecompositionHint: hint
|
DecompositionHint: hint));
|
||||||
);
|
|
||||||
|
|
||||||
return Task.FromResult(result);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ════════════════════════════════════════════════════════════
|
|
||||||
// Stage 1: 키워드 기반 작업 유형 분류
|
|
||||||
// ════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ClassifyTaskType 로직을 통합한 키워드 분류. 기존 AgentLoopService.ClassifyTaskType과 동일 로직.
|
|
||||||
/// </summary>
|
|
||||||
internal static string ClassifyTaskTypeKeyword(string? userQuery, string? activeTab)
|
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";
|
return "review";
|
||||||
|
|
||||||
if (ContainsAny(q, "bug", "fix", "error", "failure", "broken", "오류", "버그", "수정", "고쳐", "깨짐", "실패"))
|
if (ContainsAny(query, "bug", "fix", "error", "failure", "broken", "버그", "오류", "수정", "고쳐", "실패"))
|
||||||
return "bugfix";
|
return "bugfix";
|
||||||
|
|
||||||
if (ContainsAny(q, "refactor", "cleanup", "rename", "reorganize", "리팩터링", "정리", "개편", "구조 개선"))
|
if (ContainsAny(query, "refactor", "cleanup", "rename", "reorganize", "리팩토링", "리팩터링", "정리", "개편", "구조 개선"))
|
||||||
return "refactor";
|
return "refactor";
|
||||||
|
|
||||||
if (!string.Equals(activeTab, "Code", StringComparison.OrdinalIgnoreCase)
|
if (!string.Equals(activeTab, "Code", StringComparison.OrdinalIgnoreCase)
|
||||||
&& ContainsAny(q, "report", "document", "proposal", "분석서", "보고서", "문서", "제안서"))
|
&& ContainsAny(query, "report", "document", "proposal", "analysis", "보고서", "문서", "제안서", "기획서"))
|
||||||
return "docs";
|
return "docs";
|
||||||
|
|
||||||
if (ContainsAny(q, "feature", "implement", "add", "support", "추가", "구현", "지원", "기능"))
|
if (ContainsAny(query, "feature", "implement", "add", "support", "추가", "구현", "기능"))
|
||||||
return "feature";
|
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)
|
private static double ComputeTaskTypeConfidence(string taskType, string lowerQuery)
|
||||||
{
|
{
|
||||||
// "general"은 폴백이므로 confidence 낮음
|
if (string.Equals(taskType, "general", StringComparison.Ordinal))
|
||||||
if (string.Equals(taskType, "general", StringComparison.Ordinal)) return 0.3;
|
return 0.3;
|
||||||
|
|
||||||
// 직접 매칭 키워드 수 세기
|
|
||||||
var hitCount = taskType switch
|
var hitCount = taskType switch
|
||||||
{
|
{
|
||||||
"review" => CountHits(lowerQuery, "review", "리뷰", "검토", "code review", "점검"),
|
"review" => CountHits(lowerQuery, "review", "code review", "리뷰", "검토"),
|
||||||
"bugfix" => CountHits(lowerQuery, "bug", "fix", "error", "오류", "버그", "수정", "고쳐", "실패"),
|
"bugfix" => CountHits(lowerQuery, "bug", "fix", "error", "버그", "오류", "수정", "고쳐"),
|
||||||
"refactor" => CountHits(lowerQuery, "refactor", "cleanup", "리팩터링", "정리", "개편"),
|
"refactor" => CountHits(lowerQuery, "refactor", "cleanup", "리팩토링", "리팩터링", "정리", "개편"),
|
||||||
"docs" => CountHits(lowerQuery, "report", "document", "보고서", "문서", "제안서"),
|
"docs" => CountHits(lowerQuery, "report", "document", "proposal", "보고서", "문서", "제안서"),
|
||||||
"feature" => CountHits(lowerQuery, "feature", "implement", "add", "추가", "구현", "기능"),
|
"feature" => CountHits(lowerQuery, "feature", "implement", "add", "추가", "구현", "기능"),
|
||||||
_ => 0,
|
_ => 0,
|
||||||
};
|
};
|
||||||
@@ -137,27 +128,20 @@ internal sealed class IntentGateService
|
|||||||
private static int CountHits(string lower, params string[] keywords)
|
private static int CountHits(string lower, params string[] keywords)
|
||||||
{
|
{
|
||||||
var count = 0;
|
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++;
|
count++;
|
||||||
}
|
}
|
||||||
|
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ════════════════════════════════════════════════════════════
|
|
||||||
// Stage 2: 프로파일 매핑
|
|
||||||
// ════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// (taskType, intentCategory) 조합에 따라 ExecutionPolicy overlay를 생성합니다.
|
|
||||||
/// </summary>
|
|
||||||
private static ExecutionPolicyOverlay? MapToOverlay(
|
private static ExecutionPolicyOverlay? MapToOverlay(
|
||||||
string taskType, string intentCategory, string? activeTab)
|
string taskType, string intentCategory, string? activeTab)
|
||||||
{
|
{
|
||||||
return (taskType, intentCategory) switch
|
return (taskType, intentCategory) switch
|
||||||
{
|
{
|
||||||
// 코드 수정 관련
|
|
||||||
("bugfix", "coding" or "general") => new(
|
("bugfix", "coding" or "general") => new(
|
||||||
ToolTemperatureCap: 0.2,
|
ToolTemperatureCap: 0.2,
|
||||||
ForceInitialToolCall: true,
|
ForceInitialToolCall: true,
|
||||||
@@ -178,7 +162,6 @@ internal sealed class IntentGateService
|
|||||||
ForceInitialToolCall: true,
|
ForceInitialToolCall: true,
|
||||||
EnableCodeQualityGates: true),
|
EnableCodeQualityGates: true),
|
||||||
|
|
||||||
// 문서 생성
|
|
||||||
("docs", "document" or "creative" or "general") => new(
|
("docs", "document" or "creative" or "general") => new(
|
||||||
EnableDocumentVerificationGate: true,
|
EnableDocumentVerificationGate: true,
|
||||||
ReduceEarlyMemoryPressure: true),
|
ReduceEarlyMemoryPressure: true),
|
||||||
@@ -186,7 +169,6 @@ internal sealed class IntentGateService
|
|||||||
("docs", _) => new(
|
("docs", _) => new(
|
||||||
EnableDocumentVerificationGate: true),
|
EnableDocumentVerificationGate: true),
|
||||||
|
|
||||||
// 리뷰/분석
|
|
||||||
("review", "analysis" or "coding" or "general") => new(
|
("review", "analysis" or "coding" or "general") => new(
|
||||||
ToolTemperatureCap: 0.3,
|
ToolTemperatureCap: 0.3,
|
||||||
EnableCodeQualityGates: true,
|
EnableCodeQualityGates: true,
|
||||||
@@ -196,131 +178,110 @@ internal sealed class IntentGateService
|
|||||||
ToolTemperatureCap: 0.3,
|
ToolTemperatureCap: 0.3,
|
||||||
ForceInitialToolCall: true),
|
ForceInitialToolCall: true),
|
||||||
|
|
||||||
// general + 순수 대화 (Chat 탭)
|
|
||||||
("general", _) when string.Equals(activeTab, "Chat", StringComparison.OrdinalIgnoreCase)
|
("general", _) when string.Equals(activeTab, "Chat", StringComparison.OrdinalIgnoreCase)
|
||||||
=> null, // Chat 탭은 도구 없음, overlay 불필요
|
=> null,
|
||||||
|
|
||||||
// general + 문서 의도
|
|
||||||
("general", "document") => new(
|
("general", "document") => new(
|
||||||
EnableDocumentVerificationGate: true),
|
EnableDocumentVerificationGate: true),
|
||||||
|
|
||||||
// general + 분석 의도
|
|
||||||
("general", "analysis") => new(
|
("general", "analysis") => new(
|
||||||
ToolTemperatureCap: 0.35,
|
ToolTemperatureCap: 0.35,
|
||||||
MaxParallelReadBatch: 8),
|
MaxParallelReadBatch: 8),
|
||||||
|
|
||||||
// 기타: base policy 그대로
|
|
||||||
_ => null,
|
_ => null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// ════════════════════════════════════════════════════════════
|
|
||||||
// 탐색 범위 결정
|
|
||||||
// ════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// IntentGate 결과를 기반으로 ExplorationScope를 결정합니다.
|
|
||||||
/// <paramref name="lowerQuery"/>는 이미 ToLowerInvariant 처리된 문자열입니다.
|
|
||||||
/// </summary>
|
|
||||||
private static ExplorationScope ClassifyScopeFromIntent(
|
private static ExplorationScope ClassifyScopeFromIntent(
|
||||||
string lowerQuery, string? activeTab, string taskType, string intentCategory)
|
string lowerQuery, string? activeTab, string taskType, string intentCategory)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(lowerQuery))
|
if (string.IsNullOrWhiteSpace(lowerQuery))
|
||||||
return ExplorationScope.OpenEnded;
|
return ExplorationScope.OpenEnded;
|
||||||
|
|
||||||
// docs 타입이면서 생성 동사가 있으면 DirectCreation
|
|
||||||
if (taskType == "docs" && !string.Equals(activeTab, "Code", StringComparison.OrdinalIgnoreCase))
|
if (taskType == "docs" && !string.Equals(activeTab, "Code", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
if (HasCreationVerb(lowerQuery))
|
if (HasCreationVerb(lowerQuery))
|
||||||
return ExplorationScope.DirectCreation;
|
return ExplorationScope.DirectCreation;
|
||||||
}
|
}
|
||||||
|
|
||||||
// document 인텐트 + 생성 동사 → DirectCreation
|
|
||||||
if (intentCategory == "document"
|
if (intentCategory == "document"
|
||||||
&& !string.Equals(activeTab, "Code", StringComparison.OrdinalIgnoreCase)
|
&& !string.Equals(activeTab, "Code", StringComparison.OrdinalIgnoreCase)
|
||||||
&& HasCreationVerb(lowerQuery))
|
&& HasCreationVerb(lowerQuery))
|
||||||
return ExplorationScope.DirectCreation;
|
return ExplorationScope.DirectCreation;
|
||||||
|
|
||||||
// RepoWide
|
if (string.Equals(activeTab, "Code", StringComparison.OrdinalIgnoreCase)
|
||||||
if (ContainsAny(lowerQuery, "전체", "전반", "코드베이스 전체",
|
&& ProjectScaffoldProfileCatalog.IsStructuredProjectRequest(lowerQuery, activeTab))
|
||||||
"repo-wide", "repository-wide", "전체 구조", "아키텍처", "전체 점검"))
|
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;
|
return ExplorationScope.RepoWide;
|
||||||
|
|
||||||
// Localized
|
|
||||||
if (lowerQuery.Contains('.') || lowerQuery.Contains('/') || lowerQuery.Contains('\\') ||
|
if (lowerQuery.Contains('.') || lowerQuery.Contains('/') || lowerQuery.Contains('\\') ||
|
||||||
ContainsAny(lowerQuery, "file ", "class ", "method ", "function ", "line ",
|
ContainsAny(lowerQuery, "file ", "class ", "method ", "function ", "line ", "bug", "오류", "버그", "예외"))
|
||||||
"bug", "오류", "버그", "예외"))
|
|
||||||
return ExplorationScope.Localized;
|
return ExplorationScope.Localized;
|
||||||
|
|
||||||
// TopicBased
|
if (ContainsAny(lowerQuery, "정리", "요약", "보고", "주제", "관련", "분석"))
|
||||||
if (ContainsAny(lowerQuery, "정리", "요약", "보고서", "주제", "관련", "분석"))
|
|
||||||
return ExplorationScope.TopicBased;
|
return ExplorationScope.TopicBased;
|
||||||
|
|
||||||
// 탭 기반 기본값
|
|
||||||
return string.Equals(activeTab, "Code", StringComparison.OrdinalIgnoreCase)
|
return string.Equals(activeTab, "Code", StringComparison.OrdinalIgnoreCase)
|
||||||
? ExplorationScope.Localized
|
? ExplorationScope.Localized
|
||||||
: ExplorationScope.OpenEnded;
|
: ExplorationScope.OpenEnded;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool HasCreationVerb(string lower)
|
private static bool HasCreationVerb(string lower)
|
||||||
=> ContainsAny(lower,
|
=> ContainsAny(
|
||||||
"작성해", "써줘", "써 줘", "만들어", "생성해",
|
lower,
|
||||||
"만들어줘", "만들어 줘", "생성해줘", "생성해 줘",
|
"작성", "작성해", "써줘", "만들", "생성", "만들어줘", "만들어", "생성해줘", "생성해",
|
||||||
"write", "create", "draft", "generate", "compose",
|
"write", "create", "draft", "generate", "compose", "scaffold", "bootstrap");
|
||||||
"작성하", "작성을", "생성하", "생성을",
|
|
||||||
"작성 부탁", "만들어 부탁");
|
|
||||||
|
|
||||||
// ════════════════════════════════════════════════════════════
|
|
||||||
// 복합 요청 감지 (P5 연동)
|
|
||||||
// ════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 복합 요청을 감지합니다. <paramref name="lowerQuery"/>는 이미 lowercase 변환된 문자열입니다.
|
|
||||||
/// </summary>
|
|
||||||
private static (bool IsComplex, string? Hint) DetectComplexTask(string lowerQuery)
|
private static (bool IsComplex, string? Hint) DetectComplexTask(string lowerQuery)
|
||||||
{
|
{
|
||||||
if (lowerQuery.Length < 20)
|
if (lowerQuery.Length < 20)
|
||||||
return (false, null);
|
return (false, null);
|
||||||
|
|
||||||
// 접속사/열거 패턴 감지 (클래스 수준 static readonly 배열 사용)
|
|
||||||
var conjunctionCount = 0;
|
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++;
|
conjunctionCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 동사 열거 패턴 (클래스 수준 static readonly 배열 사용)
|
|
||||||
var verbCount = 0;
|
var verbCount = 0;
|
||||||
foreach (var verb in ActionVerbs)
|
foreach (var verb in ActionVerbs)
|
||||||
{
|
{
|
||||||
var idx = 0;
|
var index = 0;
|
||||||
while ((idx = lowerQuery.IndexOf(verb, idx, StringComparison.Ordinal)) >= 0)
|
while ((index = lowerQuery.IndexOf(verb, index, StringComparison.Ordinal)) >= 0)
|
||||||
{
|
{
|
||||||
verbCount++;
|
verbCount++;
|
||||||
idx += verb.Length;
|
index += verb.Length;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (conjunctionCount >= 2 || verbCount >= 3)
|
if (conjunctionCount >= 2 || verbCount >= 3)
|
||||||
{
|
return (true, "이 요청은 여러 단계의 작업을 포함합니다. 병렬화 가능한 하위 작업을 분리해 처리하는 것이 좋습니다.");
|
||||||
return (true, "이 요청에 여러 독립 작업이 포함되어 있습니다. spawn_agents로 병렬 처리를 고려하세요.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return (false, null);
|
return (false, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ════════════════════════════════════════════════════════════
|
|
||||||
// 공통 유틸
|
|
||||||
// ════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
private static bool ContainsAny(string text, params string[] keywords)
|
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 true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
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("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("출력의 첫 번째 항목은 반드시 도구 호출이어야 합니다. 텍스트로 시작하는 것은 금지입니다.");
|
||||||
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("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("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("");
|
||||||
sb.AppendLine("### [규칙 2] 말 먼저 하지 마라 — No Verbal Preamble");
|
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("A text-only response is fine once the requested code work is complete or enough evidence has been gathered.");
|
||||||
sb.AppendLine("출력의 첫 번째 항목은 반드시 도구 호출이어야 합니다. 텍스트로 시작하는 것은 금지입니다.");
|
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("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("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("");
|
||||||
sb.AppendLine("### [규칙 2] 말 먼저 하지 마라 — No Verbal Preamble");
|
sb.AppendLine("### [규칙 2] 말 먼저 하지 마라 — No Verbal Preamble");
|
||||||
@@ -251,7 +255,7 @@ public partial class ChatWindow
|
|||||||
sb.AppendLine("---");
|
sb.AppendLine("---");
|
||||||
sb.AppendLine("You are AX Copilot Code Agent — a senior software engineer for enterprise development.");
|
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($"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("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("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.");
|
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("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(" - 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(" - 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("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.");
|
sb.AppendLine(" - For bugfix/feature/refactor tasks, keep the final report structured and concrete rather than minimal.");
|
||||||
|
|
||||||
|
|||||||
@@ -2,28 +2,34 @@
|
|||||||
name: code-scaffold
|
name: code-scaffold
|
||||||
label: 코드 스캐폴딩
|
label: 코드 스캐폴딩
|
||||||
description: 프로젝트 구조를 분석하고 새 기능의 코드 뼈대를 자동 생성합니다.
|
description: 프로젝트 구조를 분석하고 새 기능의 코드 뼈대를 자동 생성합니다.
|
||||||
|
when_to_use: 빈 작업 폴더에서 새 프로젝트를 만들거나, WPF/MVVM, ASP.NET, React/Vue/Next, FastAPI/Django/Flask, Spring, Android, Go, Rust 같은 구조형 스캐폴드를 폴더 트리부터 잡아야 할 때
|
||||||
icon: \uE943
|
icon: \uE943
|
||||||
allowed-tools:
|
allowed-tools:
|
||||||
|
- file_manage
|
||||||
- folder_map
|
- folder_map
|
||||||
- file_read
|
- file_read
|
||||||
- grep
|
- grep
|
||||||
- file_write
|
- file_write
|
||||||
|
- file_edit
|
||||||
- search_codebase
|
- search_codebase
|
||||||
tabs: code
|
tabs: code
|
||||||
---
|
---
|
||||||
|
|
||||||
작업 폴더의 프로젝트 구조를 분석하고 새 기능의 코드 뼈대를 생성하세요.
|
작업 폴더의 프로젝트 구조를 분석하고 새 기능이나 새 프로젝트의 코드 뼈대를 생성하세요.
|
||||||
|
|
||||||
다음 도구를 사용하세요:
|
다음 도구를 사용하세요:
|
||||||
1. folder_map — 프로젝트 구조 파악
|
1. file_manage — 폴더 생성, 이동, 정리
|
||||||
2. file_read — 기존 코드 패턴 분석
|
2. folder_map — 기존 프로젝트 구조 파악
|
||||||
3. grep — 코딩 컨벤션 확인
|
3. file_read — 기존 코드 패턴 분석
|
||||||
4. file_write — 새 파일 생성
|
4. grep — 코딩 컨벤션 확인
|
||||||
|
5. file_write / file_edit — 새 파일 생성 및 보강
|
||||||
|
|
||||||
작업 순서:
|
작업 순서:
|
||||||
1. 프로젝트 타입 감지 (언어, 프레임워크, 빌드 시스템)
|
1. 프로젝트 타입 감지 (언어, 프레임워크, 빌드 시스템)
|
||||||
2. 기존 코드 패턴 분석 (네이밍, 폴더 구조, 임포트 스타일)
|
2. 빈 작업 폴더라면 최소 프로젝트 트리를 먼저 설계하고 file_manage로 폴더를 만든 뒤 파일을 배치
|
||||||
3. 사용자 요청에 맞는 코드 뼈대 생성
|
3. 기존 프로젝트가 있다면 네이밍, 폴더 구조, 임포트 스타일을 먼저 맞춤
|
||||||
|
4. 사용자 요청에 맞는 코드 뼈대 생성
|
||||||
|
5. 구조형 프로젝트는 뷰/UI, 모델, 서비스, 리소스, 테스트를 가능한 한 폴더로 분리
|
||||||
|
|
||||||
생성 항목:
|
생성 항목:
|
||||||
- 클래스/모듈 파일 (프로젝트 컨벤션에 맞춰)
|
- 클래스/모듈 파일 (프로젝트 컨벤션에 맞춰)
|
||||||
@@ -33,5 +39,8 @@ tabs: code
|
|||||||
|
|
||||||
규칙:
|
규칙:
|
||||||
- 기존 프로젝트의 코딩 스타일을 따르세요
|
- 기존 프로젝트의 코딩 스타일을 따르세요
|
||||||
|
- WPF, ASP.NET, React, Python API, Spring, Android, Go, Rust 같은 구조형 프로젝트는 구현 파일을 루트에 평평하게 두지 마세요
|
||||||
|
- 루트에는 manifest, entrypoint, README 같은 최소 파일만 두고 나머지는 적절한 폴더로 분리하세요
|
||||||
|
- 새 프로젝트를 시작할 때는 단일 파일 생성보다 폴더 트리와 대표 파일 경로를 먼저 확정하세요
|
||||||
- TODO 주석으로 구현이 필요한 부분을 표시하세요
|
- TODO 주석으로 구현이 필요한 부분을 표시하세요
|
||||||
- 한국어 주석을 추가하세요
|
- 한국어 주석을 추가하세요
|
||||||
|
|||||||
Reference in New Issue
Block a user