?? ? ? ?? ?? ?? ??? ???? file_write ?? ??? ??
? ?? ???? folder_map, grep, file_read, env_tool, skill_manager, mcp_list_resources ?? ?? ???? ??? ??? AgentLoopCodeRuntimeGuards? ????, ?? ??(C:\ ?) fallback? ?? ? file_write ?? ?? ??? ????? ????. Code ??? ?? ?? ???? meta tool? ????, direct-creation ????? ?? ??? ?? ??? ?? ???? file_write ?? ???? ????? AgentLoopService? SystemPromptBuilder? ????. ???? ?? <tool_call>? AgentLoopResponseClassificationService?? ??? file_write ??? ???. AgentLoopE2ETests? AgentLoopResponseClassificationServiceTests? ??? ? ?? ?? ?? fallback ??, skill_manager detour ??, text-embedded file_write ??? ??? ???? README.md? docs/DEVELOPMENT.md? 2026-04-15 14:00 (KST) ?? ??? ????. ??: - dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_empty_workspace_fix2\\ -p:IntermediateOutputPath=obj\\verify_empty_workspace_fix2\\ (?? 0 / ?? 0) - dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "FullyQualifiedName~RunAsync_EmptyWorkspace_BlocksExternalFallbackAndRecoversToFileWrite|FullyQualifiedName~RunAsync_EmptyWorkspace_DisallowsSkillManagerAndRecoversToFileWrite|FullyQualifiedName~RunAsync_TextEmbeddedToolCall_RecoversAndExecutesFileWrite|FullyQualifiedName~Classify_ShouldRecoverToolCallEmbeddedInText" -p:OutputPath=bin\\verify_empty_workspace_fix2_tests\\ -p:IntermediateOutputPath=obj\\verify_empty_workspace_fix2_tests\\ (?? 4)
This commit is contained in:
12
README.md
12
README.md
@@ -2051,3 +2051,15 @@ MIT License
|
|||||||
- 검증:
|
- 검증:
|
||||||
- `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_loop_sql_finalize\\ -p:IntermediateOutputPath=obj\\verify_loop_sql_finalize\\` 경고 0 / 오류 0
|
- `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_loop_sql_finalize\\ -p:IntermediateOutputPath=obj\\verify_loop_sql_finalize\\` 경고 0 / 오류 0
|
||||||
- `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "AgentLoopResponseClassificationServiceTests|AgentLoopLlmRequestPreparationServiceTests|AgentLoopIterationPreparationServiceTests|SqlAnalysisServiceTests|SqlReviewServiceTests|CodeLanguageCatalogTests|WorkspaceContextGeneratorTests" -p:OutputPath=bin\\verify_loop_sql_finalize_tests\\ -p:IntermediateOutputPath=obj\\verify_loop_sql_finalize_tests\\` 통과 48
|
- `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "AgentLoopResponseClassificationServiceTests|AgentLoopLlmRequestPreparationServiceTests|AgentLoopIterationPreparationServiceTests|SqlAnalysisServiceTests|SqlReviewServiceTests|CodeLanguageCatalogTests|WorkspaceContextGeneratorTests" -p:OutputPath=bin\\verify_loop_sql_finalize_tests\\ -p:IntermediateOutputPath=obj\\verify_loop_sql_finalize_tests\\` 통과 48
|
||||||
|
|
||||||
|
업데이트: 2026-04-15 14:00 (KST)
|
||||||
|
- Code 탭의 빈 작업 폴더 생성 버그를 보강했습니다. 새 [AgentLoopCodeRuntimeGuards.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentLoopCodeRuntimeGuards.cs)는 빈 작업 폴더에서 `folder_map`, `grep`, `file_read`, `env_tool`, `skill_manager`, `mcp_list_resources`, `mcp_read_resource` 같은 우회 탐색을 차단하고, 외부 루트(`C:\\` 등)로의 fallback을 막은 뒤 `file_write` 직접 생성 경로로 복구합니다.
|
||||||
|
- [AgentLoopService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentLoopService.cs)는 실행 시작 시 빈 작업 폴더를 감지하고, Code 탭의 direct-creation 요청이면 탐색보다 생성 우선 가이드를 먼저 주입하도록 정리했습니다. 같은 흐름에서 Code 기본 메타 도구 노출도 실제 런타임 활성 도구 목록에 반영되도록 연결했습니다.
|
||||||
|
- [ChatWindow.SystemPromptBuilder.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.SystemPromptBuilder.cs)는 Code/Cowork 프롬프트에 `빈 작업 폴더 + 새 파일 생성 요청`일 때 `file_write`를 바로 호출하라는 규칙과 `skill_manager`, `mcp_list_resources`, `mcp_read_resource` 비사용 규칙을 추가했습니다.
|
||||||
|
- [AgentLoopResponseClassificationService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentLoopResponseClassificationService.cs)는 text 안에 섞여 들어온 `<tool_call>` 블록을 복구해 `file_write` 같은 실제 도구 호출이 스트리밍 중 유실되지 않도록 보강했습니다.
|
||||||
|
- 테스트:
|
||||||
|
- [AgentLoopE2ETests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/AgentLoopE2ETests.cs)
|
||||||
|
- [AgentLoopResponseClassificationServiceTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/AgentLoopResponseClassificationServiceTests.cs)
|
||||||
|
- 검증:
|
||||||
|
- `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_empty_workspace_fix2\\ -p:IntermediateOutputPath=obj\\verify_empty_workspace_fix2\\` 경고 0 / 오류 0
|
||||||
|
- `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "FullyQualifiedName~RunAsync_EmptyWorkspace_BlocksExternalFallbackAndRecoversToFileWrite|FullyQualifiedName~RunAsync_EmptyWorkspace_DisallowsSkillManagerAndRecoversToFileWrite|FullyQualifiedName~RunAsync_TextEmbeddedToolCall_RecoversAndExecutesFileWrite|FullyQualifiedName~Classify_ShouldRecoverToolCallEmbeddedInText" -p:OutputPath=bin\\verify_empty_workspace_fix2_tests\\ -p:IntermediateOutputPath=obj\\verify_empty_workspace_fix2_tests\\` 통과 4
|
||||||
|
|||||||
@@ -1298,3 +1298,41 @@ UI ?붿옄???洹쒕え 由ы뙥?좊쭅 ???꾪뿕 ?묒뾽 ??湲곕줉???덉쟾
|
|||||||
### 검증
|
### 검증
|
||||||
- `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_loop_sql_finalize\\ -p:IntermediateOutputPath=obj\\verify_loop_sql_finalize\\` 경고 0 / 오류 0
|
- `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_loop_sql_finalize\\ -p:IntermediateOutputPath=obj\\verify_loop_sql_finalize\\` 경고 0 / 오류 0
|
||||||
- `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "AgentLoopResponseClassificationServiceTests|AgentLoopLlmRequestPreparationServiceTests|AgentLoopIterationPreparationServiceTests|SqlAnalysisServiceTests|SqlReviewServiceTests|CodeLanguageCatalogTests|WorkspaceContextGeneratorTests" -p:OutputPath=bin\\verify_loop_sql_finalize_tests\\ -p:IntermediateOutputPath=obj\\verify_loop_sql_finalize_tests\\` 통과 48
|
- `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "AgentLoopResponseClassificationServiceTests|AgentLoopLlmRequestPreparationServiceTests|AgentLoopIterationPreparationServiceTests|SqlAnalysisServiceTests|SqlReviewServiceTests|CodeLanguageCatalogTests|WorkspaceContextGeneratorTests" -p:OutputPath=bin\\verify_loop_sql_finalize_tests\\ -p:IntermediateOutputPath=obj\\verify_loop_sql_finalize_tests\\` 통과 48
|
||||||
|
|
||||||
|
업데이트: 2026-04-15 14:00 (KST)
|
||||||
|
|
||||||
|
### Code 탭 빈 작업 폴더 생성 버그 보강
|
||||||
|
- 새 `AgentLoopCodeRuntimeGuards.cs`
|
||||||
|
- `skill_manager`, `mcp_list_resources`, `mcp_read_resource`를 Code 기본 런타임 도구 노출에서 제외하는 `ApplyCodeDefaultMetaToolFilter()`를 추가했습니다.
|
||||||
|
- 빈 작업 폴더에서 `folder_map`, `glob`, `grep`, `code_search`, `file_read`, `env_tool`, `skill_manager`, `mcp_*` 도구로 우회 탐색을 시도하면 `TryHandleEmptyWorkspaceFallbackTransition()`이 차단하고, `file_write` 직접 생성 복구 프롬프트를 주입하도록 만들었습니다.
|
||||||
|
- 상대 경로 `.`도 작업 폴더 기준으로 해석해 외부 루트 탐색과 detour를 구분하도록 `IsExternalWorkspaceEscalationTarget()`를 보정했습니다.
|
||||||
|
- direct-creation 요청이면서 작업 폴더가 비어 있으면 시작 시점에 곧바로 `file_write` 생성 우선 가이드를 주입하는 `InjectInitialEmptyWorkspaceCreationGuidance()`를 추가했습니다.
|
||||||
|
|
||||||
|
### AgentLoop / Code 프롬프트 연동
|
||||||
|
- `AgentLoopService.cs`
|
||||||
|
- `BuildContext()` 직후 `DetectEmptyWorkspace(context.WorkFolder)`로 빈 작업 폴더를 감지해 `runState.WorkspaceAppearsEmpty`에 반영합니다.
|
||||||
|
- `GetRuntimeActiveTools()`에서 `ApplyCodeDefaultMetaToolFilter()`를 실제 런타임 도구 목록에 적용합니다.
|
||||||
|
- 도구 실행 직전 `TryHandleEmptyWorkspaceFallbackTransition()`을 호출해 외부 루트 fallback과 메타 도구 detour를 막습니다.
|
||||||
|
- direct-creation 상태 메시지는 Code 탭에서 `즉시 생성 모드 · 바로 파일을 만드는 중`으로 분리해 사용자에게 현재 의도를 더 정확히 보이도록 정리했습니다.
|
||||||
|
- `ChatWindow.SystemPromptBuilder.cs`
|
||||||
|
- Code/Cowork 프롬프트에 `빈 작업 폴더 + 새 파일/웹페이지/scaffold 생성 요청`이면 broad exploration 없이 `file_write`를 바로 호출하라는 규칙을 추가했습니다.
|
||||||
|
- Code 일반 작업에서 `skill_manager`, `mcp_list_resources`, `mcp_read_resource`를 쓰지 말라는 규칙도 함께 추가했습니다.
|
||||||
|
|
||||||
|
### Tool-call 정합성 복구
|
||||||
|
- `AgentLoopResponseClassificationService.cs`
|
||||||
|
- 텍스트 블록 안에 `<tool_call>{...}</tool_call>` 형태로 섞여 들어온 호출을 `LlmService.TryExtractToolCallsFromText()`로 복구하도록 확장했습니다.
|
||||||
|
- `LlmService.StripToolCallTokens()`로 남은 텍스트는 thinking/assistant 요약에만 남기고 실제 도구 호출은 실행 경로로 넘깁니다.
|
||||||
|
- 이 보강으로 `file_write`가 스트리밍 중 텍스트에 묻혀 유실되는 케이스를 줄였습니다.
|
||||||
|
|
||||||
|
### 테스트
|
||||||
|
- `AgentLoopE2ETests.cs`
|
||||||
|
- `RunAsync_EmptyWorkspace_BlocksExternalFallbackAndRecoversToFileWrite()`
|
||||||
|
- `RunAsync_EmptyWorkspace_DisallowsSkillManagerAndRecoversToFileWrite()`
|
||||||
|
- `RunAsync_TextEmbeddedToolCall_RecoversAndExecutesFileWrite()`
|
||||||
|
- E2E helper `BuildLoopSettings()`는 ambient project/plugin/MCP skill discovery를 꺼서 현재 저장소 스킬 상태에 영향받지 않도록 격리했습니다.
|
||||||
|
- `AgentLoopResponseClassificationServiceTests.cs`
|
||||||
|
- `Classify_ShouldRecoverToolCallEmbeddedInText()` 추가
|
||||||
|
|
||||||
|
### 검증
|
||||||
|
- `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_empty_workspace_fix2\\ -p:IntermediateOutputPath=obj\\verify_empty_workspace_fix2\\` 경고 0 / 오류 0
|
||||||
|
- `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "FullyQualifiedName~RunAsync_EmptyWorkspace_BlocksExternalFallbackAndRecoversToFileWrite|FullyQualifiedName~RunAsync_EmptyWorkspace_DisallowsSkillManagerAndRecoversToFileWrite|FullyQualifiedName~RunAsync_TextEmbeddedToolCall_RecoversAndExecutesFileWrite|FullyQualifiedName~Classify_ShouldRecoverToolCallEmbeddedInText" -p:OutputPath=bin\\verify_empty_workspace_fix2_tests\\ -p:IntermediateOutputPath=obj\\verify_empty_workspace_fix2_tests\\` 통과 4
|
||||||
|
|||||||
@@ -507,6 +507,136 @@ public class AgentLoopE2ETests
|
|||||||
events.Should().Contain(e => e.Type == AgentEventType.ToolResult && e.ToolName == "math_eval" && e.Success);
|
events.Should().Contain(e => e.Type == AgentEventType.ToolResult && e.ToolName == "math_eval" && e.Success);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RunAsync_EmptyWorkspace_BlocksExternalFallbackAndRecoversToFileWrite()
|
||||||
|
{
|
||||||
|
var tempDir = Path.Combine(Path.GetTempPath(), "axcopilot-empty-workspace-" + Guid.NewGuid().ToString("N"));
|
||||||
|
Directory.CreateDirectory(tempDir);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var server = new FakeOllamaServer(
|
||||||
|
[
|
||||||
|
BuildToolCallResponse("folder_map", new { path = ".", depth = 2, include_files = true }, "empty workspace check"),
|
||||||
|
BuildToolCallResponse("grep", new { pattern = "\\.html$", path = @"C:\\", files_only = true }, "bad external fallback"),
|
||||||
|
BuildToolCallResponse("file_write", new { path = "index.html", content = "<html><body>clock</body></html>" }, "create file directly"),
|
||||||
|
BuildTextResponse("완료"),
|
||||||
|
]);
|
||||||
|
|
||||||
|
var settings = BuildLoopSettings(server.Endpoint);
|
||||||
|
settings.Settings.Llm.WorkFolder = tempDir;
|
||||||
|
using var llm = new LlmService(settings);
|
||||||
|
using var tools = ToolRegistry.CreateDefault();
|
||||||
|
var loop = new AgentLoopService(llm, tools, settings) { ActiveTab = "Code" };
|
||||||
|
|
||||||
|
var events = new List<AgentEvent>();
|
||||||
|
loop.EventOccurred += evt => events.Add(evt);
|
||||||
|
|
||||||
|
var result = await loop.RunAsync(
|
||||||
|
[
|
||||||
|
new ChatMessage { Role = "user", Content = "실시간으로 시간을 표시하는 웹페이지 만들어줘" }
|
||||||
|
]);
|
||||||
|
|
||||||
|
|
||||||
|
result.Should().Contain("완료");
|
||||||
|
File.Exists(Path.Combine(tempDir, "index.html")).Should().BeTrue();
|
||||||
|
events.Should().Contain(e =>
|
||||||
|
e.Type == AgentEventType.Error
|
||||||
|
&& (e.ToolName == "grep" || e.ToolName == "folder_map")
|
||||||
|
&& e.Summary.Contains("Empty workspace guard blocked", StringComparison.OrdinalIgnoreCase));
|
||||||
|
events.Should().Contain(e => e.Type == AgentEventType.ToolCall && e.ToolName == "file_write");
|
||||||
|
events.Should().NotContain(e => e.Type == AgentEventType.PermissionRequest && e.ToolName == "grep");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
try { if (Directory.Exists(tempDir)) Directory.Delete(tempDir, true); } catch { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RunAsync_EmptyWorkspace_DisallowsSkillManagerAndRecoversToFileWrite()
|
||||||
|
{
|
||||||
|
var tempDir = Path.Combine(Path.GetTempPath(), "axcopilot-empty-workspace-skill-" + Guid.NewGuid().ToString("N"));
|
||||||
|
Directory.CreateDirectory(tempDir);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var server = new FakeOllamaServer(
|
||||||
|
[
|
||||||
|
BuildToolCallResponse("folder_map", new { path = ".", depth = 2, include_files = true }, "empty workspace check"),
|
||||||
|
BuildToolCallResponse("skill_manager", new { action = "list" }, "irrelevant meta tool"),
|
||||||
|
BuildToolCallResponse("file_write", new { path = "index.html", content = "<html><body>ok</body></html>" }, "create file"),
|
||||||
|
BuildTextResponse("완료"),
|
||||||
|
]);
|
||||||
|
|
||||||
|
var settings = BuildLoopSettings(server.Endpoint);
|
||||||
|
settings.Settings.Llm.WorkFolder = tempDir;
|
||||||
|
using var llm = new LlmService(settings);
|
||||||
|
using var tools = ToolRegistry.CreateDefault();
|
||||||
|
var loop = new AgentLoopService(llm, tools, settings) { ActiveTab = "Code" };
|
||||||
|
|
||||||
|
var events = new List<AgentEvent>();
|
||||||
|
loop.EventOccurred += evt => events.Add(evt);
|
||||||
|
|
||||||
|
var result = await loop.RunAsync(
|
||||||
|
[
|
||||||
|
new ChatMessage { Role = "user", Content = "빈 폴더에 index.html 만들어줘" }
|
||||||
|
]);
|
||||||
|
|
||||||
|
|
||||||
|
result.Should().Contain("완료");
|
||||||
|
File.Exists(Path.Combine(tempDir, "index.html")).Should().BeTrue();
|
||||||
|
events.Should().Contain(e => e.Type == AgentEventType.Error && e.ToolName == "skill_manager");
|
||||||
|
events.Should().Contain(e => e.Type == AgentEventType.ToolCall && e.ToolName == "file_write");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
try { if (Directory.Exists(tempDir)) Directory.Delete(tempDir, true); } catch { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RunAsync_TextEmbeddedToolCall_RecoversAndExecutesFileWrite()
|
||||||
|
{
|
||||||
|
var tempDir = Path.Combine(Path.GetTempPath(), "axcopilot-text-toolcall-" + Guid.NewGuid().ToString("N"));
|
||||||
|
Directory.CreateDirectory(tempDir);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var server = new FakeOllamaServer(
|
||||||
|
[
|
||||||
|
BuildRawResponse("""
|
||||||
|
{
|
||||||
|
"message": {
|
||||||
|
"content": "<tool_call>{\"name\":\"file_write\",\"arguments\":{\"path\":\"index.html\",\"content\":\"<html><body>clock</body></html>\"}}</tool_call>\n파일을 작성합니다."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
BuildTextResponse("완료"),
|
||||||
|
]);
|
||||||
|
|
||||||
|
var settings = BuildLoopSettings(server.Endpoint);
|
||||||
|
settings.Settings.Llm.WorkFolder = tempDir;
|
||||||
|
using var llm = new LlmService(settings);
|
||||||
|
using var tools = ToolRegistry.CreateDefault();
|
||||||
|
var loop = new AgentLoopService(llm, tools, settings) { ActiveTab = "Code" };
|
||||||
|
|
||||||
|
var events = new List<AgentEvent>();
|
||||||
|
loop.EventOccurred += evt => events.Add(evt);
|
||||||
|
|
||||||
|
var result = await loop.RunAsync(
|
||||||
|
[
|
||||||
|
new ChatMessage { Role = "user", Content = "index.html 파일 만들어줘" }
|
||||||
|
]);
|
||||||
|
|
||||||
|
|
||||||
|
result.Should().Contain("완료");
|
||||||
|
File.Exists(Path.Combine(tempDir, "index.html")).Should().BeTrue();
|
||||||
|
events.Should().Contain(e => e.Type == AgentEventType.ToolCall && e.ToolName == "file_write");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
try { if (Directory.Exists(tempDir)) Directory.Delete(tempDir, true); } catch { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static SettingsService BuildLoopSettings(string endpoint)
|
private static SettingsService BuildLoopSettings(string endpoint)
|
||||||
{
|
{
|
||||||
var settings = new SettingsService();
|
var settings = new SettingsService();
|
||||||
@@ -520,8 +650,16 @@ public class AgentLoopE2ETests
|
|||||||
settings.Settings.Llm.EnableToolHooks = false;
|
settings.Settings.Llm.EnableToolHooks = false;
|
||||||
settings.Settings.Llm.EnableAutoRouter = false;
|
settings.Settings.Llm.EnableAutoRouter = false;
|
||||||
settings.Settings.Llm.EnableForkSkillDelegationEnforcement = true;
|
settings.Settings.Llm.EnableForkSkillDelegationEnforcement = true;
|
||||||
|
settings.Settings.Llm.EnableProjectSkillDiscovery = false;
|
||||||
|
settings.Settings.Llm.EnablePluginSkillDiscovery = false;
|
||||||
|
settings.Settings.Llm.EnableMcpSkillDiscovery = false;
|
||||||
|
settings.Settings.Llm.EnableLegacyCommandSkills = false;
|
||||||
|
settings.Settings.Llm.SkillsFolderPath = "";
|
||||||
|
settings.Settings.Llm.AdditionalSkillFolders.Clear();
|
||||||
settings.Settings.Llm.FilePermission = "BypassPermissions";
|
settings.Settings.Llm.FilePermission = "BypassPermissions";
|
||||||
settings.Settings.Llm.DefaultAgentPermission = "BypassPermissions";
|
settings.Settings.Llm.DefaultAgentPermission = "BypassPermissions";
|
||||||
|
settings.Settings.Llm.BlockedPaths.Clear();
|
||||||
|
settings.Settings.Llm.BlockedExtensions.Clear();
|
||||||
return settings;
|
return settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -565,6 +703,9 @@ public class AgentLoopE2ETests
|
|||||||
private static string BuildTextOnlyResponse(string content)
|
private static string BuildTextOnlyResponse(string content)
|
||||||
=> BuildTextResponse(content);
|
=> BuildTextResponse(content);
|
||||||
|
|
||||||
|
private static string BuildRawResponse(string rawJson)
|
||||||
|
=> rawJson;
|
||||||
|
|
||||||
private sealed class FakeOllamaServer : IDisposable
|
private sealed class FakeOllamaServer : IDisposable
|
||||||
{
|
{
|
||||||
private readonly HttpListener _listener;
|
private readonly HttpListener _listener;
|
||||||
|
|||||||
@@ -62,4 +62,27 @@ public class AgentLoopResponseClassificationServiceTests
|
|||||||
|
|
||||||
result.BuildThinkingSummary().Should().Be("실제 수정 범위를 다시 확인합니다.");
|
result.BuildThinkingSummary().Should().Be("실제 수정 범위를 다시 확인합니다.");
|
||||||
}
|
}
|
||||||
|
[Fact]
|
||||||
|
public void Classify_ShouldRecoverToolCallEmbeddedInText()
|
||||||
|
{
|
||||||
|
var blocks = new List<ContentBlock>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Type = "text",
|
||||||
|
Text = """
|
||||||
|
<tool_call>{"name":"file_write","arguments":{"path":"index.html","content":"<html>clock</html>"}}</tool_call>
|
||||||
|
이제 파일을 생성합니다.
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = AgentLoopResponseClassificationService.Classify(blocks, consecutiveNoToolResponses: 0);
|
||||||
|
|
||||||
|
result.ToolCalls.Should().ContainSingle();
|
||||||
|
result.ToolCalls[0].ToolName.Should().Be("file_write");
|
||||||
|
result.TextResponse.Should().Contain("이제 파일을 생성합니다.");
|
||||||
|
result.TextResponse.Should().NotContain("<tool_call>");
|
||||||
|
result.NextConsecutiveNoToolResponses.Should().Be(0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
224
src/AxCopilot/Services/Agent/AgentLoopCodeRuntimeGuards.cs
Normal file
224
src/AxCopilot/Services/Agent/AgentLoopCodeRuntimeGuards.cs
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
using System.IO;
|
||||||
|
using AxCopilot.Models;
|
||||||
|
using AxCopilot.Services;
|
||||||
|
|
||||||
|
namespace AxCopilot.Services.Agent;
|
||||||
|
|
||||||
|
public partial class AgentLoopService
|
||||||
|
{
|
||||||
|
private static readonly HashSet<string> s_defaultSuppressedCodeMetaTools = new(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
"skill_manager",
|
||||||
|
"mcp_list_resources",
|
||||||
|
"mcp_read_resource"
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly HashSet<string> s_emptyWorkspaceSearchTools = new(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
"folder_map",
|
||||||
|
"glob",
|
||||||
|
"grep",
|
||||||
|
"code_search",
|
||||||
|
"file_read",
|
||||||
|
"env_tool",
|
||||||
|
"skill_manager",
|
||||||
|
"mcp_list_resources",
|
||||||
|
"mcp_read_resource"
|
||||||
|
};
|
||||||
|
|
||||||
|
private IReadOnlyCollection<IAgentTool> ApplyCodeDefaultMetaToolFilter(
|
||||||
|
IReadOnlyCollection<IAgentTool> tools,
|
||||||
|
SkillRuntimeOverrides? runtimeOverrides)
|
||||||
|
{
|
||||||
|
if (!string.Equals(ActiveTab, "Code", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return tools;
|
||||||
|
|
||||||
|
if (runtimeOverrides?.AllowedToolNames?.Count > 0)
|
||||||
|
return tools;
|
||||||
|
|
||||||
|
return tools
|
||||||
|
.Where(tool => !s_defaultSuppressedCodeMetaTools.Contains(tool.Name))
|
||||||
|
.ToList()
|
||||||
|
.AsReadOnly();
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryHandleEmptyWorkspaceFallbackTransition(
|
||||||
|
ContentBlock call,
|
||||||
|
AgentContext context,
|
||||||
|
RunState runState,
|
||||||
|
List<ChatMessage> messages,
|
||||||
|
IReadOnlyCollection<string> activeToolNames)
|
||||||
|
{
|
||||||
|
if (!string.Equals(ActiveTab, "Code", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!runState.WorkspaceAppearsEmpty)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var toolName = call.ToolName ?? "";
|
||||||
|
if (!s_emptyWorkspaceSearchTools.Contains(toolName))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var target = DescribeToolTarget(toolName, call.ToolInput ?? default, context);
|
||||||
|
var isExternalEscalation = IsExternalWorkspaceEscalationTarget(target, context.WorkFolder);
|
||||||
|
|
||||||
|
runState.EmptyWorkspaceGuardTriggered = true;
|
||||||
|
|
||||||
|
var blockedMessage = isExternalEscalation
|
||||||
|
? $"Empty workspace guard blocked external path search: {target}"
|
||||||
|
: $"Empty workspace guard blocked detour tool '{toolName}'. Use file_write directly.";
|
||||||
|
|
||||||
|
EmitEvent(AgentEventType.Error, toolName, blockedMessage);
|
||||||
|
EmitEvent(
|
||||||
|
AgentEventType.Thinking,
|
||||||
|
"",
|
||||||
|
"Empty workspace detected. Stopping broad exploration and switching to direct file creation.");
|
||||||
|
|
||||||
|
messages.Add(LlmService.CreateToolResultMessage(
|
||||||
|
call.ToolId,
|
||||||
|
toolName,
|
||||||
|
blockedMessage));
|
||||||
|
messages.Add(new ChatMessage
|
||||||
|
{
|
||||||
|
Role = "user",
|
||||||
|
Content = BuildEmptyWorkspaceCreationRecoveryPrompt(activeToolNames)
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string BuildEmptyWorkspaceCreationRecoveryPrompt(IReadOnlyCollection<string> activeToolNames)
|
||||||
|
{
|
||||||
|
var activeToolPreview = AgentLoopNoToolResponseRecoveryService.BuildActiveToolPreview(activeToolNames);
|
||||||
|
return "[System:EmptyWorkspaceCreation] The current work folder is empty. " +
|
||||||
|
"Do not search C:\\, other drive roots, or repeat folder_map, grep, glob, file_read, env_tool, skill_manager, or mcp tools. " +
|
||||||
|
"Call file_write immediately using a relative path in the current work folder and create the needed file directly. " +
|
||||||
|
"For example: index.html, app.js, or style.css. " +
|
||||||
|
"Do not only describe the plan. Emit the actual tool call now. " +
|
||||||
|
$"Available tools: {activeToolPreview}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InjectInitialEmptyWorkspaceCreationGuidance(
|
||||||
|
List<ChatMessage> messages,
|
||||||
|
ExplorationTrackingState explorationState,
|
||||||
|
RunState runState)
|
||||||
|
{
|
||||||
|
if (!string.Equals(ActiveTab, "Code", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!runState.WorkspaceAppearsEmpty)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (explorationState.Scope != ExplorationScope.DirectCreation)
|
||||||
|
return;
|
||||||
|
|
||||||
|
runState.EmptyWorkspaceGuardTriggered = true;
|
||||||
|
|
||||||
|
messages.Add(new ChatMessage
|
||||||
|
{
|
||||||
|
Role = "system",
|
||||||
|
Content =
|
||||||
|
"[System:EmptyWorkspaceStart] The current work folder is empty and the user asked to create a new artifact. " +
|
||||||
|
"Skip broad exploration. Do not call folder_map, glob, grep, file_read, env_tool, skill_manager, mcp_list_resources, or mcp_read_resource unless the user explicitly asks to inspect the workspace. " +
|
||||||
|
"Call file_write immediately with a relative path inside the current work folder and create the requested file directly."
|
||||||
|
});
|
||||||
|
|
||||||
|
EmitEvent(
|
||||||
|
AgentEventType.Thinking,
|
||||||
|
"",
|
||||||
|
"빈 작업 폴더 감지 · 탐색 없이 새 파일 생성을 시작합니다");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsExternalWorkspaceEscalationTarget(string? target, string? workFolder)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(target) || string.IsNullOrWhiteSpace(workFolder))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var fullWorkFolder = Path.GetFullPath(workFolder);
|
||||||
|
var fullTarget = Path.IsPathRooted(target)
|
||||||
|
? Path.GetFullPath(target)
|
||||||
|
: Path.GetFullPath(Path.Combine(fullWorkFolder, target));
|
||||||
|
var root = Path.GetPathRoot(fullTarget);
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(root)
|
||||||
|
&& string.Equals(
|
||||||
|
fullTarget.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar),
|
||||||
|
root.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar),
|
||||||
|
StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !IsSubPathOf(fullTarget, fullWorkFolder);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsSubPathOf(string candidatePath, string basePath)
|
||||||
|
{
|
||||||
|
var normalizedBase = Path.GetFullPath(basePath)
|
||||||
|
.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)
|
||||||
|
+ Path.DirectorySeparatorChar;
|
||||||
|
var normalizedCandidate = Path.GetFullPath(candidatePath)
|
||||||
|
.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
|
||||||
|
|
||||||
|
return normalizedCandidate.StartsWith(normalizedBase, StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| string.Equals(
|
||||||
|
normalizedCandidate.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar),
|
||||||
|
normalizedBase.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar),
|
||||||
|
StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void UpdateWorkspaceEmptyStateFromResult(RunState runState, string toolName, ToolResult result)
|
||||||
|
{
|
||||||
|
if (!result.Success)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (string.Equals(toolName, "folder_map", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
runState.WorkspaceAppearsEmpty = LooksLikeEmptyFolderMap(result.Output);
|
||||||
|
if (!runState.WorkspaceAppearsEmpty)
|
||||||
|
runState.EmptyWorkspaceGuardTriggered = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsFileModifyingTool(toolName) && !string.IsNullOrWhiteSpace(result.FilePath))
|
||||||
|
{
|
||||||
|
runState.WorkspaceAppearsEmpty = false;
|
||||||
|
runState.EmptyWorkspaceGuardTriggered = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool LooksLikeEmptyFolderMap(string? output)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(output))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var lower = output.ToLowerInvariant();
|
||||||
|
return lower.Contains("0 files")
|
||||||
|
&& lower.Contains("0 dirs");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool DetectEmptyWorkspace(string? workFolder)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(workFolder))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!Directory.Exists(workFolder))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
using var enumerator = Directory.EnumerateFileSystemEntries(workFolder).GetEnumerator();
|
||||||
|
return !enumerator.MoveNext();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -202,7 +202,12 @@ public partial class AgentLoopService
|
|||||||
{
|
{
|
||||||
// 문서 생성 의도가 감지되면 탐색 없이 바로 문서 계획/생성 도구 사용
|
// 문서 생성 의도가 감지되면 탐색 없이 바로 문서 계획/생성 도구 사용
|
||||||
if (state.Scope == ExplorationScope.DirectCreation)
|
if (state.Scope == ExplorationScope.DirectCreation)
|
||||||
|
{
|
||||||
|
if (string.Equals(activeTab, "Code", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return "file_write -> file_edit -> build_run/test_loop as needed";
|
||||||
|
|
||||||
return "document_plan -> docx_create/html_create/excel_create -> self-review";
|
return "document_plan -> docx_create/html_create/excel_create -> self-review";
|
||||||
|
}
|
||||||
|
|
||||||
if (HasExplicitFolderIntent(userQuery))
|
if (HasExplicitFolderIntent(userQuery))
|
||||||
return "folder_map -> glob/grep -> targeted read";
|
return "folder_map -> glob/grep -> targeted read";
|
||||||
@@ -236,6 +241,10 @@ public partial class AgentLoopService
|
|||||||
&& HasDocumentCreationIntent(lower))
|
&& HasDocumentCreationIntent(lower))
|
||||||
return ExplorationScope.DirectCreation;
|
return ExplorationScope.DirectCreation;
|
||||||
|
|
||||||
|
if (string.Equals(activeTab, "Code", StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& HasCodeArtifactCreationIntent(lower))
|
||||||
|
return ExplorationScope.DirectCreation;
|
||||||
|
|
||||||
if (lower.Contains("전체") || lower.Contains("전반") || lower.Contains("코드베이스 전체") ||
|
if (lower.Contains("전체") || lower.Contains("전반") || lower.Contains("코드베이스 전체") ||
|
||||||
lower.Contains("repo-wide") || lower.Contains("repository-wide") || lower.Contains("전체 구조") ||
|
lower.Contains("repo-wide") || lower.Contains("repository-wide") || lower.Contains("전체 구조") ||
|
||||||
lower.Contains("아키텍처") || lower.Contains("전체 점검"))
|
lower.Contains("아키텍처") || lower.Contains("전체 점검"))
|
||||||
@@ -283,6 +292,24 @@ public partial class AgentLoopService
|
|||||||
"정리해", "정리 해");
|
"정리해", "정리 해");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool HasCodeArtifactCreationIntent(string lowerQuery)
|
||||||
|
{
|
||||||
|
var hasCreationVerb = ContainsAny(
|
||||||
|
lowerQuery,
|
||||||
|
"만들", "생성", "작성", "create", "generate", "build", "write", "scaffold", "draft");
|
||||||
|
|
||||||
|
if (!hasCreationVerb)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return ContainsAny(
|
||||||
|
lowerQuery,
|
||||||
|
"html", "css", "javascript", "js", "typescript", "ts",
|
||||||
|
"web page", "webpage", "website", "landing page",
|
||||||
|
"웹페이지", "웹 페이지", "웹사이트", "페이지",
|
||||||
|
"file", "파일", "script", "스크립트", "component", "컴포넌트",
|
||||||
|
"template", "템플릿", "index.html", "app.js", "style.css");
|
||||||
|
}
|
||||||
|
|
||||||
private static void InjectExplorationScopeGuidance(List<ChatMessage> messages, ExplorationScope scope)
|
private static void InjectExplorationScopeGuidance(List<ChatMessage> messages, ExplorationScope scope)
|
||||||
{
|
{
|
||||||
var guidance = scope switch
|
var guidance = scope switch
|
||||||
@@ -303,6 +330,16 @@ public partial class AgentLoopService
|
|||||||
"Exploration scope = open-ended. Expand gradually. Prefer selective discovery before broad scans."
|
"Exploration scope = open-ended. Expand gradually. Prefer selective discovery before broad scans."
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (scope == ExplorationScope.DirectCreation)
|
||||||
|
{
|
||||||
|
guidance =
|
||||||
|
"Exploration scope = direct-creation. The user wants to CREATE a new file or document. " +
|
||||||
|
"Do NOT search for existing files with glob/grep/folder_map unless the user explicitly asked to inspect the workspace. " +
|
||||||
|
"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, call document_plan first and then the appropriate creation tool. " +
|
||||||
|
"The output MUST be a real file on disk, not a text response.";
|
||||||
|
}
|
||||||
|
|
||||||
messages.Add(new ChatMessage
|
messages.Add(new ChatMessage
|
||||||
{
|
{
|
||||||
Role = "system",
|
Role = "system",
|
||||||
|
|||||||
@@ -34,6 +34,21 @@ internal static class AgentLoopResponseClassificationService
|
|||||||
toolCalls.Add(block);
|
toolCalls.Add(block);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (toolCalls.Count == 0 && textParts.Count > 0)
|
||||||
|
{
|
||||||
|
var mergedText = string.Join("\n", textParts);
|
||||||
|
var recoveredToolCalls = LlmService.TryExtractToolCallsFromText(mergedText);
|
||||||
|
if (recoveredToolCalls.Count > 0)
|
||||||
|
{
|
||||||
|
toolCalls.AddRange(recoveredToolCalls);
|
||||||
|
textParts.Clear();
|
||||||
|
|
||||||
|
var stripped = LlmService.StripToolCallTokens(mergedText);
|
||||||
|
if (!string.IsNullOrWhiteSpace(stripped))
|
||||||
|
textParts.Add(stripped);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var nextConsecutiveNoToolResponses = toolCalls.Count == 0
|
var nextConsecutiveNoToolResponses = toolCalls.Count == 0
|
||||||
? consecutiveNoToolResponses + 1
|
? consecutiveNoToolResponses + 1
|
||||||
: 0;
|
: 0;
|
||||||
|
|||||||
@@ -289,6 +289,7 @@ public partial class AgentLoopService
|
|||||||
string? lastModifiedCodeFilePath = null;
|
string? lastModifiedCodeFilePath = null;
|
||||||
|
|
||||||
var context = BuildContext();
|
var context = BuildContext();
|
||||||
|
runState.WorkspaceAppearsEmpty = DetectEmptyWorkspace(context.WorkFolder);
|
||||||
|
|
||||||
var preferredInitialToolSequence = BuildPreferredInitialToolSequence(
|
var preferredInitialToolSequence = BuildPreferredInitialToolSequence(
|
||||||
explorationState,
|
explorationState,
|
||||||
@@ -300,12 +301,15 @@ public partial class AgentLoopService
|
|||||||
"",
|
"",
|
||||||
explorationState.Scope switch
|
explorationState.Scope switch
|
||||||
{
|
{
|
||||||
ExplorationScope.DirectCreation => "문서 생성 모드 · 바로 문서를 만드는 중",
|
ExplorationScope.DirectCreation => string.Equals(ActiveTab, "Code", StringComparison.OrdinalIgnoreCase)
|
||||||
|
? "즉시 생성 모드 · 바로 파일을 만드는 중"
|
||||||
|
: "문서 생성 모드 · 바로 문서를 만드는 중",
|
||||||
ExplorationScope.Localized => "국소 범위 탐색 · 관련 파일만 찾는 중",
|
ExplorationScope.Localized => "국소 범위 탐색 · 관련 파일만 찾는 중",
|
||||||
ExplorationScope.TopicBased => "주제 범위 탐색 · 관련 후보만 추리는 중",
|
ExplorationScope.TopicBased => "주제 범위 탐색 · 관련 후보만 추리는 중",
|
||||||
ExplorationScope.RepoWide => "저장소 범위 탐색 · 구조를 확인하는 중",
|
ExplorationScope.RepoWide => "저장소 범위 탐색 · 구조를 확인하는 중",
|
||||||
_ => "점진 탐색 · 필요한 범위부터 확인하는 중",
|
_ => "점진 탐색 · 필요한 범위부터 확인하는 중",
|
||||||
});
|
});
|
||||||
|
InjectInitialEmptyWorkspaceCreationGuidance(messages, explorationState, runState);
|
||||||
if (!executionPolicy.ReduceEarlyMemoryPressure)
|
if (!executionPolicy.ReduceEarlyMemoryPressure)
|
||||||
InjectRecentFailureGuidance(messages, context, userQuery, taskPolicy);
|
InjectRecentFailureGuidance(messages, context, userQuery, taskPolicy);
|
||||||
var runtimeOverrides = ResolveSkillRuntimeOverrides(messages);
|
var runtimeOverrides = ResolveSkillRuntimeOverrides(messages);
|
||||||
@@ -1299,6 +1303,16 @@ public partial class AgentLoopService
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (TryHandleEmptyWorkspaceFallbackTransition(
|
||||||
|
effectiveCall,
|
||||||
|
context,
|
||||||
|
runState,
|
||||||
|
messages,
|
||||||
|
activeToolNames))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Task Decomposition: 단계 진행률 추적
|
// Task Decomposition: 단계 진행률 추적
|
||||||
if (planSteps.Count > 0)
|
if (planSteps.Count > 0)
|
||||||
{
|
{
|
||||||
@@ -1526,6 +1540,7 @@ public partial class AgentLoopService
|
|||||||
ref statsFailCount,
|
ref statsFailCount,
|
||||||
ref statsInputTokens,
|
ref statsInputTokens,
|
||||||
ref statsOutputTokens);
|
ref statsOutputTokens);
|
||||||
|
UpdateWorkspaceEmptyStateFromResult(runState, effectiveCall.ToolName, result);
|
||||||
lastToolResultAtUtc = DateTime.UtcNow;
|
lastToolResultAtUtc = DateTime.UtcNow;
|
||||||
lastToolResultToolName = effectiveCall.ToolName;
|
lastToolResultToolName = effectiveCall.ToolName;
|
||||||
|
|
||||||
@@ -1872,6 +1887,7 @@ public partial class AgentLoopService
|
|||||||
// 탭별 도구 필터링: 현재 탭에 맞는 도구만 LLM에 전송 (토큰 절약)
|
// 탭별 도구 필터링: 현재 탭에 맞는 도구만 LLM에 전송 (토큰 절약)
|
||||||
var active = _tools.GetActiveToolsForTab(ActiveTab, mergedDisabled);
|
var active = _tools.GetActiveToolsForTab(ActiveTab, mergedDisabled);
|
||||||
active = ApplyPermissionExposureFilter(active);
|
active = ApplyPermissionExposureFilter(active);
|
||||||
|
active = ApplyCodeDefaultMetaToolFilter(active, runtimeOverrides);
|
||||||
if (runtimeOverrides == null || runtimeOverrides.AllowedToolNames.Count == 0)
|
if (runtimeOverrides == null || runtimeOverrides.AllowedToolNames.Count == 0)
|
||||||
return active;
|
return active;
|
||||||
|
|
||||||
|
|||||||
@@ -1189,6 +1189,8 @@ public partial class AgentLoopService
|
|||||||
public int DocumentVerificationGateRetry;
|
public int DocumentVerificationGateRetry;
|
||||||
public int NoProgressRecoveryRetry;
|
public int NoProgressRecoveryRetry;
|
||||||
public int TerminalEvidenceGateRetry;
|
public int TerminalEvidenceGateRetry;
|
||||||
|
public bool WorkspaceAppearsEmpty;
|
||||||
|
public bool EmptyWorkspaceGuardTriggered;
|
||||||
public bool PendingPostCompactionTurn;
|
public bool PendingPostCompactionTurn;
|
||||||
public int PostCompactionTurnCounter;
|
public int PostCompactionTurnCounter;
|
||||||
public string LastCompactionStageSummary = "";
|
public string LastCompactionStageSummary = "";
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ 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("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");
|
||||||
sb.AppendLine("절대 금지 문구 — 도구 호출 전에 절대 쓰지 말 것:");
|
sb.AppendLine("절대 금지 문구 — 도구 호출 전에 절대 쓰지 말 것:");
|
||||||
@@ -206,6 +208,8 @@ 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("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");
|
||||||
sb.AppendLine("절대 금지 문구 — 도구 호출 전에 절대 쓰지 말 것:");
|
sb.AppendLine("절대 금지 문구 — 도구 호출 전에 절대 쓰지 말 것:");
|
||||||
|
|||||||
Reference in New Issue
Block a user