diff --git a/README.md b/README.md index 7a41ab7..20145ad 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,29 @@ Windows 전용 시맨틱 런처 & 워크스페이스 매니저 개발 참고: Claw Code 동등성 작업 추적 문서 `docs/claw-code-parity-plan.md` +- 업데이트: 2026-04-09 22:48 (KST) +- `claude-code` 기준 후속 과제를 이어서 반영했습니다. IBM 배포형 vLLM/Qwen 경로에서는 과거 `tool_calls`/`role=tool` 이력을 그대로 재전송하지 않고, `...` 중심의 평탄한 transcript로 직렬화해 엄격한 tool history 검사에 덜 걸리도록 전용 분기를 넣었습니다. +- Cowork 문서 생성 프롬프트는 `document_review`와 `format_convert`를 상시 기본 단계처럼 밀지 않도록 다시 낮췄습니다. 이제 기본 검증은 `file_read/document_read` 중심이고, `document_review`는 큰 문서 품질 점검이나 명시적 요청일 때만 권장합니다. +- Code/Cowork 공통 런타임 복구 문구도 정리했습니다. `tool_search`는 후보만으로 충분할 때는 바로 실제 도구를 고르도록 하고, 정말 모호할 때만 사용하게 바꿨습니다. `spawn_agent`도 기본 전면 노출 대신 병렬 조사가 실제로 도움이 될 때만 고려하도록 완화했습니다. +- Code의 의미 기반 탐색도 한 단계 더 보강했습니다. 정의/참조/구현/호출관계 질문은 `lsp_code_intel`을 더 앞에 두고, LSP 결과는 파일 수·대표 위치·첫 결과를 함께 요약해 다음 액션 판단에 바로 쓸 수 있게 정리했습니다. +- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\` 경고 0 / 오류 0 + +- 업데이트: 2026-04-09 22:38 (KST) +- Code 탭의 구조적 코드 읽기를 `claude-code`에 더 가깝게 확장했습니다. AX에 이미 있던 `lsp_code_intel` 도구를 `goto_definition`, `find_references`, `hover`, `goto_implementation`, `workspace_symbols`, `prepare_call_hierarchy`, `incoming_calls`, `outgoing_calls`까지 넓혀, 단순 `grep/file_read`만이 아니라 정의/참조/호출관계를 입체적으로 볼 수 있게 했습니다. +- Code 프롬프트와 탐색 우선순위도 이에 맞춰 조정했습니다. 이제 특정 심볼의 정의, 참조, 구현, 호출자/피호출자를 볼 때는 `lsp_code_intel`을 우선 후보로 제시하고, 텍스트 검색이 더 적합한 경우에만 `grep/glob`를 쓰도록 유도합니다. +- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\` 경고 0 / 오류 0 + +- 업데이트: 2026-04-09 21:48 (KST) +- Cowork/Code 생성 로직을 `claude-code` 기준으로 다시 정리했습니다. Cowork는 순수 문서 생성 요청이면 `glob/grep/document_read/folder_map`로 먼저 새지 않고 `document_plan -> 생성 도구`로 바로 가도록 프롬프트와 탐색 우선순위를 맞췄습니다. +- Code는 시작 흐름을 더 얇게 바꿨습니다. `targeted file_read` 또는 `grep/glob`로 가장 작은 범위만 확인한 뒤 바로 `file_edit/file_write`로 이어지고, `build_run/test_loop`와 `git_tool(diff)`는 필요할 때 쓰는 방향으로 정리했습니다. +- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\` 경고 0 / 오류 0 + +- 업데이트: 2026-04-09 21:58 (KST) +- Cowork/Code 기본 실행 정책을 더 `claude-code`답게 완화했습니다. `balanced`, `reasoning_first` 프로필에서 초기 도구 호출 강제와 post-tool 검증, 문서 검증 게이트를 기본 비활성화해 불필요한 재호출을 줄였습니다. +- Cowork는 문서 생성 완료 뒤 추가 검증 턴을 기본으로 붙이지 않고, 결과 파일이 만들어지면 바로 완료 경로로 빠지도록 정리했습니다. 문서 품질 확인은 가벼운 self-check 성격으로 프롬프트도 완화했습니다. +- Code는 일반 수정의 완료 근거를 `diff` 또는 최근 build/test 같은 가벼운 증거로도 인정하도록 바꿔, 고영향 수정이 아닐 때 `CodeQualityGate`가 과하게 재발동하지 않게 했습니다. +- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\` 경고 0 / 오류 0 + - 업데이트: 2026-04-09 21:03 (KST) - 핵심 엔진 계층도 `claude-code` 기준으로 더 정리했습니다. 스트리밍 도구 코디네이터는 재시도 전에 중간 스트림 상태를 끊는 `RetryReset` 이벤트를 보내도록 바꿔, 부분 응답이 누적된 채 다시 이어지는 현상을 줄였습니다. - 조기 실행 대상 읽기 도구는 `file_read`와 `document_read` 중심의 가벼운 도구로 다시 좁혔고, `folder_map` 같은 구조 탐색 도구는 더 이상 엔진 레벨 prefetch 대상에 넣지 않도록 조정했습니다. diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 80e2e67..c7d67be 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -1,8 +1,34 @@ # AX Copilot - 媛쒕컻 臾몄꽌 +## claude-code식 후속 호환/선택성 보강 + +- 업데이트: 2026-04-09 22:48 (KST) +- IBM 배포형 vLLM/Qwen 호환을 위해 `LlmService.ToolUse`의 `BuildIbmToolBody`를 다시 정리했습니다. 이전 assistant `tool_calls`와 `role=tool` 이력을 OpenAI 형식으로 재전송하던 경로를 제거하고, `_tool_use_blocks`는 `...` transcript로, `tool_result`는 plain user transcript로 평탄화합니다. 기존의 텍스트 기반 `TryExtractToolCallsFromText` 폴백과 함께 IBM 쪽의 엄격한 tool history 검사에 대응하는 방향입니다. +- `ChatWindow.SystemPromptBuilder`는 Cowork/Code 모두 “도구 호출 필수” 톤을 더 낮췄습니다. Cowork는 `document_review`와 `format_convert`를 기본 후속 단계처럼 밀지 않고, `file_read/document_read` 중심의 가벼운 검증을 기본으로 삼습니다. Code/Cowork 공통 `Sub-Agent Delegation`도 `spawn_agent`를 병렬성이 실제로 도움이 될 때만 선택하도록 바꿨습니다. +- `AgentLoopService`의 unknown/disallowed tool recovery는 `tool_search`를 항상 먼저 강제하지 않고, alias 자동 매핑 후보나 활성 도구 예시만으로 바로 선택 가능하면 그 도구를 바로 쓰도록 완화했습니다. `tool_search`는 정말 모호할 때만 쓰는 보조 수단으로 내렸습니다. +- `AgentLoopTransitions.Execution`의 문서 검증 근거도 단순화했습니다. 문서 생성 후 기본 완료 근거는 `file_read`/`document_read`면 충분하고, `document_review`는 선택적 품질 점검 도구로만 남깁니다. +- `AgentLoopExplorationPolicy`는 Code 쿼리에 정의/참조/구현/호출관계/심볼 의도가 보이면 `lsp_code_intel -> targeted file_read -> edit/verify` 순서를 더 앞세웁니다. `LspTool` 결과도 파일 수, 대표 위치, 첫 결과를 같이 요약해 `claude-code`의 LSP 결과 shaping에 더 가깝게 맞췄습니다. +- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\` 경고 0 / 오류 0 + ## claude-code식 선택 탐색 우선순위 정렬 +- 업데이트: 2026-04-09 22:38 (KST) +- AX에 이미 존재하던 `LspTool`/`LspClientService`를 `claude-code`의 `LSPTool` 수준에 더 가깝게 확장했습니다. 기존 `goto_definition`, `find_references`, `symbols` 외에 `hover`, `goto_implementation`, `workspace_symbols`, `prepare_call_hierarchy`, `incoming_calls`, `outgoing_calls`를 추가해 정의/참조/문서 심볼/워크스페이스 심볼/호출 계층을 모두 조회할 수 있게 했습니다. +- `LspClientService`는 LSP initialize capability도 확장했습니다. 이제 `implementation`, `hover`, `callHierarchy`, `workspace/symbol` 요청을 직접 보낼 수 있고, `LocationLink`, hover contents, call hierarchy 결과를 AX용 단순 모델로 파싱합니다. +- `AgentLoopExplorationPolicy`, `TaskTypePolicy`, `ChatWindow.SystemPromptBuilder`도 같이 조정해 Code 탭의 좁은 요청에서 `file_read`, `grep/glob`와 함께 `lsp_code_intel`이 자연스러운 선택지로 노출되도록 맞췄습니다. `claude-code`처럼 기본은 텍스트 탐색이되, 정의/참조/호출관계는 LSP를 더 우선적으로 쓰는 흐름입니다. + +- 업데이트: 2026-04-09 21:58 (KST) +- `ModelExecutionProfileCatalog`의 기본 프로필을 다시 완화했습니다. `balanced`, `reasoning_first`는 이제 초기 도구 호출 강제와 post-tool verification을 기본으로 켜지 않고, 문서 검증 게이트와 diff/final-report 후속 게이트도 기본적으로 줄여 `claude-code`처럼 더 얇은 반복 구조를 따릅니다. +- Cowork 문서 생성 완료 경로는 `AgentLoopTransitions.Documents`에서 한 번 더 정리했습니다. Code 탭이 아닌 경우 terminal document tool 성공 뒤 별도 post-tool verification 턴을 추가하지 않고 바로 완료 가능하도록 바꿔, 문서 생성 후 불필요한 재호출을 줄였습니다. +- Code 완료 게이트도 `AgentLoopTransitions.Verification`에서 완화했습니다. 일반 수정은 `diff` 또는 최근 build/test 같은 가벼운 완료 증거만 있어도 마무리 가능하게 하고, build/test 강제는 고영향 수정일 때 중심으로 남겨 `claude-code`의 얇은 code loop에 더 가깝게 맞췄습니다. +- `ChatWindow.SystemPromptBuilder`의 Cowork/Code 프롬프트도 같은 방향으로 손봤습니다. “모든 응답은 도구 호출 필수” 식의 과한 강제 표현을 줄이고, 필요한 경우에만 즉시 도구를 쓰되 불필요한 도구 호출은 강제하지 않도록 완화했습니다. + +- 업데이트: 2026-04-09 21:48 (KST) +- Cowork 시스템 프롬프트에서 “불확실하면 먼저 파일을 찾아라” 성향을 더 줄였습니다. 이제 순수 문서 생성 요청은 `document_plan -> docx_create/html_create/...`를 먼저 타고, `glob/grep/document_read/folder_map`은 기존 자료 참조가 명시된 경우에만 먼저 쓰도록 유도합니다. +- Code 시스템 프롬프트와 탐색 우선순위도 `claude-code`처럼 더 얇게 바꿨습니다. 기본 시작 흐름은 `specific file -> file_read`, 아니면 `grep/glob -> small targeted read`, 그 다음 `file_edit/file_write`이며, `build_run/test_loop`와 `git_tool(diff)`는 검증이 실제로 필요할 때 붙는 구조로 정리했습니다. +- `TaskTypePolicy`의 bugfix/feature/refactor/review 기본 도구 순서도 같은 기준으로 완화해, AX Code가 과하게 `git diff/build/test`를 절차적으로 앞세우지 않도록 맞췄습니다. + - 업데이트: 2026-04-09 21:03 (KST) - `StreamingToolExecutionCoordinator`에 `RetryReset` 이벤트를 추가해, 컨텍스트 복구나 일시적 LLM 오류 재시도 전에 부분 스트림 미리보기 상태를 끊도록 했습니다. `claude-code`가 fallback 시 orphaned partial state를 정리하고 executor를 새로 잡는 흐름과 비슷한 방향으로 AX도 재시도 경계를 더 명확히 가지게 됐습니다. - 엔진 레벨 `PrefetchableReadOnlyTools`는 `file_read`/`document_read` 중심으로 다시 줄였습니다. `folder_map`, `glob`, `grep`, `multi_read`, `code_search` 같은 구조 탐색/광범위 검색 도구는 prefetch 대상에서 빼서, 탐색 정책과 실행 엔진의 우선순위가 서로 어긋나지 않도록 맞췄습니다. diff --git a/src/AxCopilot/Services/Agent/AgentLoopExplorationPolicy.cs b/src/AxCopilot/Services/Agent/AgentLoopExplorationPolicy.cs index 76fde97..0bec2fc 100644 --- a/src/AxCopilot/Services/Agent/AgentLoopExplorationPolicy.cs +++ b/src/AxCopilot/Services/Agent/AgentLoopExplorationPolicy.cs @@ -11,6 +11,8 @@ public partial class AgentLoopService TopicBased, RepoWide, OpenEnded, + /// 문서 생성 요청 — 탐색 단계를 건너뛰고 바로 document_plan/생성 도구 사용. + DirectCreation, } private sealed class ExplorationTrackingState @@ -24,6 +26,196 @@ public partial class AgentLoopService public bool CorrectiveHintInjected { get; set; } } + private static IReadOnlyCollection FilterExplorationToolsForCurrentIteration( + IReadOnlyCollection tools, + ExplorationTrackingState state, + string userQuery, + string? activeTab, + int totalToolCalls) + { + if (tools.Count == 0) + return tools; + + // 문서 생성 모드: 생성 도구를 최우선, 탐색 도구를 뒤로 배치 + if (state.Scope == ExplorationScope.DirectCreation) + { + var creationFirst = new List(tools.Count); + creationFirst.AddRange(tools.Where(IsDocumentCreationTool)); + creationFirst.AddRange(tools.Where(t => !IsDocumentCreationTool(t) + && !string.Equals(t.Name, "folder_map", StringComparison.OrdinalIgnoreCase) + && !string.Equals(t.Name, "glob", StringComparison.OrdinalIgnoreCase) + && !string.Equals(t.Name, "grep", StringComparison.OrdinalIgnoreCase))); + // 탐색 도구는 마지막에 (필요 시에만 사용) + creationFirst.AddRange(tools.Where(t => + string.Equals(t.Name, "glob", StringComparison.OrdinalIgnoreCase) + || string.Equals(t.Name, "grep", StringComparison.OrdinalIgnoreCase))); + return creationFirst + .DistinctBy(t => t.Name, StringComparer.OrdinalIgnoreCase) + .ToList() + .AsReadOnly(); + } + + var allowFolderMap = ShouldAllowFolderMapForCurrentIteration(state, userQuery, activeTab, totalToolCalls); + var ordered = new List(tools.Count); + + ordered.AddRange(tools.Where(IsSelectiveDiscoveryTool)); + ordered.AddRange(tools.Where(t => !IsSelectiveDiscoveryTool(t) && + (allowFolderMap || !string.Equals(t.Name, "folder_map", StringComparison.OrdinalIgnoreCase)))); + if (allowFolderMap) + { + ordered.AddRange(tools.Where(t => string.Equals(t.Name, "folder_map", StringComparison.OrdinalIgnoreCase))); + } + + return ordered + .DistinctBy(t => t.Name, StringComparer.OrdinalIgnoreCase) + .ToList() + .AsReadOnly(); + } + + private static bool IsDocumentCreationTool(IAgentTool tool) + { + return tool.Name is "document_plan" or "docx_create" or "html_create" or "excel_create" + or "markdown_create" or "csv_create" or "pptx_create" or "chart_create" + or "document_assemble" or "file_write"; + } + + private static bool ShouldAllowFolderMapForCurrentIteration( + ExplorationTrackingState state, + string userQuery, + string? activeTab, + int totalToolCalls) + { + // 문서 생성 모드에서는 folder_map 차단 — 탐색 없이 바로 생성 + if (state.Scope == ExplorationScope.DirectCreation) + return false; + + if (state.Scope is ExplorationScope.RepoWide or ExplorationScope.OpenEnded) + return true; + + if (HasExplicitFolderIntent(userQuery)) + return true; + + if (string.Equals(activeTab, "Cowork", StringComparison.OrdinalIgnoreCase) + && IsExistingMaterialReferenceRequest(userQuery)) + return true; + + return totalToolCalls >= 2 && state.TotalFilesRead == 0 && state.FolderMapCalls == 0; + } + + private static bool HasExplicitFolderIntent(string userQuery) + { + if (string.IsNullOrWhiteSpace(userQuery)) + return false; + + return ContainsAny( + userQuery, + "folder", + "directory", + "tree", + "structure", + "list files", + "workspace layout", + "work folder", + "project structure", + "폴더", + "디렉터리", + "폴더 구조", + "디렉터리 구조", + "파일 목록", + "작업 폴더", + "폴더 안", + "구조를 보여", + "구조 확인"); + } + + private static bool IsExistingMaterialReferenceRequest(string userQuery) + { + if (string.IsNullOrWhiteSpace(userQuery)) + return false; + + return ContainsAny( + userQuery, + "existing file", + "existing files", + "existing document", + "existing documents", + "reference files", + "reference docs", + "existing materials", + "기존 파일", + "기존 문서", + "기존 자료", + "참고 파일", + "참고 문서", + "폴더 내 자료", + "안의 자료", + "작업 폴더 파일"); + } + + private static bool IsSelectiveDiscoveryTool(IAgentTool tool) + { + return tool.Name is "glob" or "grep" or "file_read" or "document_read" or "multi_read" or "lsp_code_intel"; + } + + private static bool HasSemanticNavigationIntent(string userQuery) + { + if (string.IsNullOrWhiteSpace(userQuery)) + return false; + + return ContainsAny( + userQuery, + "definition", + "reference", + "references", + "implementation", + "implementations", + "caller", + "callers", + "callee", + "call hierarchy", + "symbol", + "symbols", + "interface", + "override", + "정의", + "참조", + "구현", + "호출부", + "호출 관계", + "심볼", + "인터페이스", + "오버라이드"); + } + + private static string BuildPreferredInitialToolSequence( + ExplorationTrackingState state, + TaskTypePolicy taskPolicy, + string? activeTab, + string userQuery) + { + // 문서 생성 의도가 감지되면 탐색 없이 바로 문서 계획/생성 도구 사용 + if (state.Scope == ExplorationScope.DirectCreation) + return "document_plan -> docx_create/html_create/excel_create -> self-review"; + + if (HasExplicitFolderIntent(userQuery)) + return "folder_map -> glob/grep -> targeted read"; + + if (string.Equals(taskPolicy.TaskType, "docs", StringComparison.OrdinalIgnoreCase)) + return "glob/grep -> document_read/file_read -> drafting tool"; + + if (string.Equals(activeTab, "Code", StringComparison.OrdinalIgnoreCase) + && HasSemanticNavigationIntent(userQuery)) + return "lsp_code_intel -> targeted file_read -> file_edit/file_write -> build_run/test_loop as needed"; + + if (string.Equals(activeTab, "Code", StringComparison.OrdinalIgnoreCase)) + return "targeted file_read or grep/glob/lsp -> file_edit/file_write -> build_run/test_loop as needed"; + + if (state.Scope == ExplorationScope.TopicBased) + return "glob/grep -> targeted read -> next action"; + + return "glob/grep -> file_read/document_read -> next action"; + } + private static ExplorationScope ClassifyExplorationScope(string userQuery, string? activeTab) { if (string.IsNullOrWhiteSpace(userQuery)) @@ -32,6 +224,11 @@ public partial class AgentLoopService var q = userQuery.Trim(); var lower = q.ToLowerInvariant(); + // Cowork 탭에서 문서 생성 의도가 있으면 탐색을 건너뛰고 바로 생성 도구 사용 + if (!string.Equals(activeTab, "Code", StringComparison.OrdinalIgnoreCase) + && HasDocumentCreationIntent(lower)) + return ExplorationScope.DirectCreation; + if (lower.Contains("전체") || lower.Contains("전반") || lower.Contains("코드베이스 전체") || lower.Contains("repo-wide") || lower.Contains("repository-wide") || lower.Contains("전체 구조") || lower.Contains("아키텍처") || lower.Contains("전체 점검")) @@ -52,14 +249,47 @@ public partial class AgentLoopService : ExplorationScope.OpenEnded; } + /// + /// 사용자가 문서를 새로 생성/작성하려는 의도인지 판별합니다. + /// "보고서 작성해줘", "문서 만들어줘" 등 생성 동사가 포함된 경우 true. + /// + private static bool HasDocumentCreationIntent(string lowerQuery) + { + // 생성 동사 키워드 + var hasCreationVerb = ContainsAny(lowerQuery, + "작성해", "써줘", "써 줘", "만들어", "생성해", "작성 해", + "만들어줘", "만들어 줘", "생성해줘", "생성해 줘", + "write", "create", "draft", "generate", "compose", + "작성하", "작성을", "생성하", "생성을", + "리포트 써", "보고서 써", "문서 써", + "작성 부탁", "만들어 부탁"); + + if (!hasCreationVerb) + return false; + + // 생성 대상이 문서/보고서/자료 등인지 확인 + return ContainsAny(lowerQuery, + "보고서", "문서", "제안서", "리포트", "분석서", "기획서", + "report", "document", "proposal", "analysis", + "요약서", "발표자료", "ppt", "pptx", "docx", "xlsx", "excel", "word", + "표", "차트", "스프레드시트", "프레젠테이션", + "정리해", "정리 해"); + } + private static void InjectExplorationScopeGuidance(List messages, ExplorationScope scope) { var guidance = scope switch { + ExplorationScope.DirectCreation => + "Exploration scope = direct-creation. The user wants to CREATE a new document/report/file. " + + "Do NOT search for existing files with glob/grep/folder_map — skip exploration entirely. " + + "Call document_plan first to outline the document structure, then immediately call the appropriate creation tool " + + "(docx_create, html_create, excel_create, markdown_create, etc.) to produce the actual file. " + + "The output MUST be a real file on disk, not a text response.", ExplorationScope.Localized => - "Exploration scope = localized. Start with grep/glob and targeted file reads. Avoid folder_map unless structure is unclear.", + "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.", ExplorationScope.TopicBased => - "Exploration scope = topic-based. Identify candidate files by topic keywords first, 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 => "Exploration scope = repo-wide. Broad structure inspection is allowed when needed.", _ => @@ -93,6 +323,15 @@ public partial class AgentLoopService string toolName, string argsJson) { + // 문서 생성 모드: 탐색 도구가 1회라도 호출되면 즉시 교정 + if (state.Scope == ExplorationScope.DirectCreation) + { + return string.Equals(toolName, "folder_map", StringComparison.OrdinalIgnoreCase) + || string.Equals(toolName, "glob", StringComparison.OrdinalIgnoreCase) + || string.Equals(toolName, "grep", StringComparison.OrdinalIgnoreCase) + || state.TotalFilesRead >= 1; + } + if (state.Scope is ExplorationScope.RepoWide or ExplorationScope.OpenEnded) return false; diff --git a/src/AxCopilot/Services/Agent/AgentLoopService.cs b/src/AxCopilot/Services/Agent/AgentLoopService.cs index cc1ea26..31d4f1d 100644 --- a/src/AxCopilot/Services/Agent/AgentLoopService.cs +++ b/src/AxCopilot/Services/Agent/AgentLoopService.cs @@ -255,6 +255,7 @@ public partial class AgentLoopService "", explorationState.Scope switch { + ExplorationScope.DirectCreation => "문서 생성 모드 · 바로 문서를 만드는 중", ExplorationScope.Localized => "국소 범위 탐색 · 관련 파일만 찾는 중", ExplorationScope.TopicBased => "주제 범위 탐색 · 관련 후보만 추리는 중", ExplorationScope.RepoWide => "저장소 범위 탐색 · 구조를 확인하는 중", @@ -2832,7 +2833,7 @@ public partial class AgentLoopService ? testReviewLine + "5. grep 또는 glob으로 공용 API, 호출부, 의존성 등록, 테스트 영향을 모두 다시 확인합니다.\n" + "6. build_run으로 build와 test를 모두 실행해 통과 여부를 확인합니다.\n" + - "7. 필요하면 spawn_agent로 호출부 분석이나 관련 테스트 탐색을 병렬 조사하게 하고, wait_agents로 결과를 통합합니다.\n" + + "7. 병렬 조사가 실제로 도움이 될 때만 spawn_agent로 호출부 분석이나 관련 테스트 탐색을 위임하고, wait_agents로 결과를 통합합니다.\n" + "8. 문제가 발견되면 즉시 수정하고 다시 검증합니다.\n" + "중요: 이 변경은 영향 범위가 넓을 가능성이 큽니다. build/test와 참조 검토가 모두 끝나기 전에는 마무리하지 마세요." : testReviewLine + @@ -2983,7 +2984,7 @@ public partial class AgentLoopService var failureHint = BuildFailureTypeRecoveryHint(ClassifyFailureRecoveryKind(toolName, toolOutput), toolName); var highImpactLine = highImpactChange ? "4. 공용 API, 인터페이스, DI 등록, 모델 계약, 호출부 전파 영향까지 반드시 확인합니다.\n" + - "5. 필요하면 spawn_agent로 호출부/관련 테스트 조사를 병렬 실행해 근거를 보강합니다.\n" + + "5. 병렬 조사가 실제로 도움이 될 때만 spawn_agent로 호출부/관련 테스트 조사를 실행해 근거를 보강합니다.\n" + "6. 관련 테스트가 없다면 테스트 부재 사실과 확인 근거를 남깁니다.\n" + "7. 근본 원인을 수정한 뒤 build/test를 다시 실행합니다.\n" + "중요: 이 변경은 영향 범위가 넓을 수 있으므로 build와 test 둘 다 확인하기 전에는 종료하지 마세요." @@ -3859,7 +3860,9 @@ public partial class AgentLoopService $"- 실패 도구: {unknownToolName}\n" + aliasHint + $"- 사용 가능한 도구 예시: {string.Join(", ", suggestions)}\n" + - "- 도구가 애매하면 먼저 tool_search를 호출해 정확한 이름을 찾으세요.\n" + + (string.IsNullOrEmpty(aliasHint) + ? "- 위 후보만으로도 충분히 선택 가능하면 바로 그 도구를 호출하세요. 그래도 도구가 애매할 때만 tool_search를 사용하세요.\n" + : "- 자동 매핑 후보가 맞으면 바로 그 도구를 호출하세요. 정말 모호할 때만 tool_search를 사용하세요.\n") + "위 목록에서 실제 존재하는 도구 하나를 골라 다시 호출하세요. 같은 미등록 도구를 반복 호출하지 마세요."; } @@ -3879,7 +3882,7 @@ public partial class AgentLoopService $"- 요청 도구: {requestedToolName}\n" + policyLine + $"- 지금 사용 가능한 도구 예시: {activePreview}\n" + - "- 도구 선택이 모호하면 tool_search로 허용 가능한 대체 도구를 먼저 찾으세요.\n" + + "- 위 허용 도구 예시로 바로 대체할 수 있으면 즉시 호출하세요. 그래도 도구 선택이 모호할 때만 tool_search를 사용하세요.\n" + "허용 목록에서 대체 도구를 선택해 다시 호출하세요. 동일한 비허용 도구 재호출은 금지합니다."; } @@ -3893,7 +3896,7 @@ public partial class AgentLoopService $"- 반복 횟수: {repeatedUnknownToolCount}\n" + $"- 실패 도구: {unknownToolName}\n" + $"- 현재 사용 가능한 도구 예시: {preview}\n" + - "- 다음 실행에서는 tool_search로 도구명을 확인한 뒤 위 목록의 실제 도구 이름으로 호출하세요."; + "- 다음 실행에서는 먼저 위 목록의 실제 도구 이름을 직접 선택하고, 그래도 애매할 때만 tool_search를 사용하세요."; } private static string BuildDisallowedToolLoopAbortResponse( @@ -3906,7 +3909,7 @@ public partial class AgentLoopService $"- 반복 횟수: {repeatedCount}\n" + $"- 비허용 도구: {toolName}\n" + $"- 현재 사용 가능한 도구 예시: {preview}\n" + - "- 다음 실행에서는 tool_search로 허용 도구를 확인하고 계획을 수정하세요."; + "- 다음 실행에서는 허용 도구 예시에서 직접 고를 수 있으면 바로 바꾸고, 그래도 애매할 때만 tool_search를 사용하세요."; } private static readonly Dictionary ToolAliasMap = new(StringComparer.OrdinalIgnoreCase) diff --git a/src/AxCopilot/Services/Agent/AgentLoopTransitions.Execution.cs b/src/AxCopilot/Services/Agent/AgentLoopTransitions.Execution.cs index 0079510..bd28585 100644 --- a/src/AxCopilot/Services/Agent/AgentLoopTransitions.Execution.cs +++ b/src/AxCopilot/Services/Agent/AgentLoopTransitions.Execution.cs @@ -300,7 +300,7 @@ public partial class AgentLoopService }; var verificationTools = new HashSet(StringComparer.OrdinalIgnoreCase) { - "file_read", "document_read", "document_review" + "file_read", "document_read" }; var sawVerification = false; @@ -421,7 +421,7 @@ public partial class AgentLoopService } private bool TryHandleRepeatedFailureGuardTransition( - LlmService.ContentBlock call, + ContentBlock call, string toolCallSignature, List messages, string? lastFailedToolSignature, @@ -463,7 +463,7 @@ public partial class AgentLoopService } private bool TryHandleNoProgressReadOnlyLoopTransition( - LlmService.ContentBlock call, + ContentBlock call, string toolCallSignature, int repeatedSameSignatureCount, List messages, @@ -911,22 +911,22 @@ public partial class AgentLoopService } } - private async Task TryPrefetchReadOnlyToolAsync( - LlmService.ContentBlock block, + private async Task TryPrefetchReadOnlyToolAsync( + ContentBlock block, IReadOnlyCollection tools, AgentContext context, CancellationToken ct) => await _toolExecutionCoordinator.TryPrefetchReadOnlyToolAsync(block, tools, context, ct); - private async Task> SendWithToolsWithRecoveryAsync( + private async Task> SendWithToolsWithRecoveryAsync( List messages, IReadOnlyCollection tools, CancellationToken ct, string phaseLabel, RunState? runState = null, bool forceToolCall = false, - Func>? prefetchToolCallAsync = null, - Func? onStreamEventAsync = null) + Func>? prefetchToolCallAsync = null, + Func? onStreamEventAsync = null) => await _toolExecutionCoordinator.SendWithToolsWithRecoveryAsync( messages, tools, @@ -938,7 +938,7 @@ public partial class AgentLoopService onStreamEventAsync); private void ApplyToolPostExecutionBookkeeping( - LlmService.ContentBlock call, + ContentBlock call, ToolResult result, TokenUsage? tokenUsage, Models.LlmSettings llm, @@ -986,7 +986,7 @@ public partial class AgentLoopService } private bool TryHandleToolFailureTransition( - LlmService.ContentBlock call, + ContentBlock call, ToolResult result, AgentContext context, TaskTypePolicy taskPolicy, @@ -1120,7 +1120,7 @@ public partial class AgentLoopService } private async Task<(bool ShouldContinue, string? TerminalResponse)> TryHandleUserDecisionTransitionsAsync( - LlmService.ContentBlock call, + ContentBlock call, AgentContext context, List messages) { @@ -1193,4 +1193,3 @@ public partial class AgentLoopService } - diff --git a/src/AxCopilot/Services/Agent/LspTool.cs b/src/AxCopilot/Services/Agent/LspTool.cs index 75add39..ed3005b 100644 --- a/src/AxCopilot/Services/Agent/LspTool.cs +++ b/src/AxCopilot/Services/Agent/LspTool.cs @@ -5,18 +5,24 @@ namespace AxCopilot.Services.Agent; /// /// LSP 기반 코드 인텔리전스 도구. -/// 정의 이동(goto_definition), 참조 검색(find_references), 심볼 목록(symbols) 3가지 액션을 제공합니다. +/// 정의 이동, 참조 검색, hover, 구현 위치, 심볼 검색, 호출 계층 등 구조적 코드 탐색을 제공합니다. /// public class LspTool : IAgentTool, IDisposable { public string Name => "lsp_code_intel"; public string Description => - "코드 인텔리전스 도구. 정의 이동, 참조 검색, 심볼 목록을 제공합니다.\n" + - "- action=\"goto_definition\": 심볼의 정의 위치를 찾습니다 (파일, 라인, 컬럼)\n" + + "코드 인텔리전스 도구. 정의, 참조, hover, 구현 위치, 문서/워크스페이스 심볼, 호출 계층을 제공합니다.\n" + + "- action=\"goto_definition\": 심볼의 정의 위치를 찾습니다\n" + "- action=\"find_references\": 심볼이 사용된 모든 위치를 찾습니다\n" + - "- action=\"symbols\": 파일 내 모든 심볼(클래스, 메서드, 필드 등)을 나열합니다\n" + - "file_path, line, character 파라미터가 필요합니다 (line과 character는 0-based)."; + "- action=\"hover\": 심볼의 타입/문서 정보를 가져옵니다\n" + + "- action=\"goto_implementation\": 인터페이스/추상 멤버의 구현 위치를 찾습니다\n" + + "- action=\"symbols\": 파일 내 모든 심볼을 나열합니다\n" + + "- action=\"workspace_symbols\": 워크스페이스 전체 심볼을 검색합니다 (query 권장)\n" + + "- action=\"prepare_call_hierarchy\": 현재 위치의 호출 계층 기준 심볼을 확인합니다\n" + + "- action=\"incoming_calls\": 현재 심볼을 호출하는 상위 호출자를 찾습니다\n" + + "- action=\"outgoing_calls\": 현재 심볼이 호출하는 하위 호출 대상을 찾습니다\n" + + "line/character는 기본적으로 1-based 입력을 기대하며, 0-based 값도 호환 처리합니다."; public ToolParameterSchema Parameters => new() { @@ -25,23 +31,39 @@ public class LspTool : IAgentTool, IDisposable ["action"] = new ToolProperty { Type = "string", - Description = "수행할 작업: goto_definition | find_references | symbols", - Enum = new() { "goto_definition", "find_references", "symbols" } + Description = "수행할 작업", + Enum = new() + { + "goto_definition", + "find_references", + "hover", + "goto_implementation", + "symbols", + "workspace_symbols", + "prepare_call_hierarchy", + "incoming_calls", + "outgoing_calls" + } }, ["file_path"] = new ToolProperty { Type = "string", - Description = "대상 파일 경로 (절대 또는 작업 폴더 기준 상대 경로)" + Description = "대상 파일 경로 (절대 또는 작업 폴더 기준 상대 경로). workspace_symbols에서는 작업 폴더 기준 힌트로만 사용 가능." }, ["line"] = new ToolProperty { Type = "integer", - Description = "대상 라인 번호 (0-based). symbols 액션에서는 불필요." + Description = "대상 라인 번호. 기본 1-based 입력을 기대하며 내부에서 0-based로 변환합니다. symbols/workspace_symbols에서는 불필요." }, ["character"] = new ToolProperty { Type = "integer", - Description = "라인 내 문자 위치 (0-based). symbols 액션에서는 불필요." + Description = "라인 내 문자 위치. 기본 1-based 입력을 기대하며 내부에서 0-based로 변환합니다. symbols/workspace_symbols에서는 불필요." + }, + ["query"] = new ToolProperty + { + Type = "string", + Description = "workspace_symbols에서 사용할 심볼 검색어. 비어 있으면 file_path의 파일명/심볼 힌트를 사용합니다." }, }, Required = new() { "action", "file_path" } @@ -59,8 +81,9 @@ public class LspTool : IAgentTool, IDisposable var action = args.SafeTryGetProperty("action", out var a) ? a.SafeGetString() ?? "" : ""; var filePath = args.SafeTryGetProperty("file_path", out var f) ? f.SafeGetString() ?? "" : ""; - var line = args.SafeTryGetProperty("line", out var l) ? l.GetInt32() : 0; - var character = args.SafeTryGetProperty("character", out var ch) ? ch.GetInt32() : 0; + var line = args.SafeTryGetProperty("line", out var l) ? NormalizePosition(l.SafeGetInt32()) : 0; + var character = args.SafeTryGetProperty("character", out var ch) ? NormalizePosition(ch.SafeGetInt32()) : 0; + var query = args.SafeTryGetProperty("query", out var q) ? q.SafeGetString() ?? "" : ""; if (string.IsNullOrEmpty(filePath)) return ToolResult.Fail("file_path가 필요합니다."); @@ -88,8 +111,14 @@ public class LspTool : IAgentTool, IDisposable { "goto_definition" => await GotoDefinitionAsync(client, filePath, line, character, ct), "find_references" => await FindReferencesAsync(client, filePath, line, character, ct), + "hover" => await HoverAsync(client, filePath, line, character, ct), + "goto_implementation" => await GotoImplementationAsync(client, filePath, line, character, ct), "symbols" => await GetSymbolsAsync(client, filePath, ct), - _ => ToolResult.Fail($"알 수 없는 액션: {action}. goto_definition | find_references | symbols 중 선택하세요.") + "workspace_symbols" => await GetWorkspaceSymbolsAsync(client, filePath, query, ct), + "prepare_call_hierarchy" => await PrepareCallHierarchyAsync(client, filePath, line, character, ct), + "incoming_calls" => await GetIncomingCallsAsync(client, filePath, line, character, ct), + "outgoing_calls" => await GetOutgoingCallsAsync(client, filePath, line, character, ct), + _ => ToolResult.Fail("알 수 없는 action입니다. goto_definition | find_references | hover | goto_implementation | symbols | workspace_symbols | prepare_call_hierarchy | incoming_calls | outgoing_calls 중 선택하세요.") }; } catch (Exception ex) @@ -118,13 +147,44 @@ public class LspTool : IAgentTool, IDisposable return ToolResult.Ok("참조를 찾을 수 없습니다."); var sb = new System.Text.StringBuilder(); - sb.AppendLine($"총 {locations.Count}개 참조:"); + sb.AppendLine($"총 {locations.Count}개 참조"); + sb.AppendLine($"파일 수: {locations.Select(l => l.FilePath).Distinct(StringComparer.OrdinalIgnoreCase).Count()}"); + sb.AppendLine($"첫 참조: {locations[0]}"); + sb.AppendLine(); foreach (var loc in locations.Take(30)) sb.AppendLine($" {loc}"); if (locations.Count > 30) sb.AppendLine($" ... 외 {locations.Count - 30}개"); - return ToolResult.Ok(sb.ToString()); + return ToolResult.Ok(sb.ToString(), locations[0].FilePath); + } + + private async Task HoverAsync(LspClientService client, string filePath, int line, int character, CancellationToken ct) + { + var hover = await client.HoverAsync(filePath, line, character, ct); + if (string.IsNullOrWhiteSpace(hover)) + return ToolResult.Ok("hover 정보를 찾을 수 없습니다."); + + return ToolResult.Ok($"Hover 정보\n위치: {Path.GetFileName(filePath)}:{line + 1}:{character + 1}\n\n{hover}", filePath); + } + + private async Task GotoImplementationAsync(LspClientService client, string filePath, int line, int character, CancellationToken ct) + { + var locations = await client.GotoImplementationAsync(filePath, line, character, ct); + if (locations.Count == 0) + return ToolResult.Ok("구현 위치를 찾을 수 없습니다."); + + var sb = new System.Text.StringBuilder(); + sb.AppendLine($"총 {locations.Count}개 구현 위치"); + sb.AppendLine($"파일 수: {locations.Select(l => l.FilePath).Distinct(StringComparer.OrdinalIgnoreCase).Count()}"); + sb.AppendLine($"첫 구현: {locations[0]}"); + sb.AppendLine(); + foreach (var loc in locations.Take(20)) + sb.AppendLine($" {loc}"); + if (locations.Count > 20) + sb.AppendLine($" ... 외 {locations.Count - 20}개"); + + return ToolResult.Ok(sb.ToString(), locations[0].FilePath); } private async Task GetSymbolsAsync(LspClientService client, string filePath, CancellationToken ct) @@ -134,13 +194,92 @@ public class LspTool : IAgentTool, IDisposable return ToolResult.Ok("심볼을 찾을 수 없습니다."); var sb = new System.Text.StringBuilder(); - sb.AppendLine($"총 {symbols.Count}개 심볼:"); + sb.AppendLine($"총 {symbols.Count}개 심볼"); + sb.AppendLine($"파일: {Path.GetFileName(filePath)}"); + sb.AppendLine(); foreach (var sym in symbols) sb.AppendLine($" {sym}"); return ToolResult.Ok(sb.ToString()); } + private async Task GetWorkspaceSymbolsAsync(LspClientService client, string filePath, string query, CancellationToken ct) + { + var fallbackQuery = !string.IsNullOrWhiteSpace(query) + ? query + : Path.GetFileNameWithoutExtension(filePath); + var symbols = await client.SearchWorkspaceSymbolsAsync(fallbackQuery, ct); + if (symbols.Count == 0) + return ToolResult.Ok($"워크스페이스 심볼을 찾을 수 없습니다. query=\"{fallbackQuery}\""); + + var sb = new System.Text.StringBuilder(); + sb.AppendLine($"query=\"{fallbackQuery}\" 결과 {symbols.Count}개"); + sb.AppendLine($"파일 수: {symbols.Select(s => s.Location?.FilePath).Where(p => !string.IsNullOrWhiteSpace(p)).Distinct(StringComparer.OrdinalIgnoreCase).Count()}"); + if (symbols.FirstOrDefault() is { } firstSymbol) + sb.AppendLine($"첫 결과: {firstSymbol}"); + sb.AppendLine(); + foreach (var sym in symbols.Take(30)) + sb.AppendLine($" {sym}"); + if (symbols.Count > 30) + sb.AppendLine($" ... 외 {symbols.Count - 30}개"); + + return ToolResult.Ok(sb.ToString(), symbols.FirstOrDefault(s => s.Location != null)?.Location?.FilePath); + } + + private async Task PrepareCallHierarchyAsync(LspClientService client, string filePath, int line, int character, CancellationToken ct) + { + var items = await client.PrepareCallHierarchyAsync(filePath, line, character, ct); + if (items.Count == 0) + return ToolResult.Ok("호출 계층 기준 심볼을 찾을 수 없습니다."); + + var sb = new System.Text.StringBuilder(); + sb.AppendLine($"호출 계층 기준 {items.Count}개"); + sb.AppendLine($"대표 심볼: {items[0]}"); + sb.AppendLine(); + foreach (var item in items.Take(10)) + sb.AppendLine($" {item}"); + if (items.Count > 10) + sb.AppendLine($" ... 외 {items.Count - 10}개"); + + return ToolResult.Ok(sb.ToString(), items[0].Location.FilePath); + } + + private async Task GetIncomingCallsAsync(LspClientService client, string filePath, int line, int character, CancellationToken ct) + { + var calls = await client.GetIncomingCallsAsync(filePath, line, character, ct); + if (calls.Count == 0) + return ToolResult.Ok("incoming call을 찾을 수 없습니다."); + + var sb = new System.Text.StringBuilder(); + sb.AppendLine($"상위 호출자 {calls.Count}개"); + sb.AppendLine($"대표 호출자: {calls[0]}"); + sb.AppendLine(); + foreach (var call in calls.Take(20)) + sb.AppendLine($" {call}"); + if (calls.Count > 20) + sb.AppendLine($" ... 외 {calls.Count - 20}개"); + + return ToolResult.Ok(sb.ToString(), calls[0].Location.FilePath); + } + + private async Task GetOutgoingCallsAsync(LspClientService client, string filePath, int line, int character, CancellationToken ct) + { + var calls = await client.GetOutgoingCallsAsync(filePath, line, character, ct); + if (calls.Count == 0) + return ToolResult.Ok("outgoing call을 찾을 수 없습니다."); + + var sb = new System.Text.StringBuilder(); + sb.AppendLine($"하위 호출 대상 {calls.Count}개"); + sb.AppendLine($"첫 호출 대상: {calls[0]}"); + sb.AppendLine(); + foreach (var call in calls.Take(20)) + sb.AppendLine($" {call}"); + if (calls.Count > 20) + sb.AppendLine($" ... 외 {calls.Count - 20}개"); + + return ToolResult.Ok(sb.ToString(), calls[0].Location.FilePath); + } + private async Task GetOrCreateClientAsync(string language, string workFolder, CancellationToken ct) { if (_clients.TryGetValue(language, out var existing) && existing.IsConnected) @@ -173,6 +312,14 @@ public class LspTool : IAgentTool, IDisposable }; } + private static int NormalizePosition(int value) + { + if (value <= 0) + return 0; + + return value - 1; + } + public void Dispose() { foreach (var client in _clients.Values) diff --git a/src/AxCopilot/Services/LlmService.ToolUse.cs b/src/AxCopilot/Services/LlmService.ToolUse.cs index 17b603f..fc73795 100644 --- a/src/AxCopilot/Services/LlmService.ToolUse.cs +++ b/src/AxCopilot/Services/LlmService.ToolUse.cs @@ -870,59 +870,28 @@ public partial class LlmService { if (m.Role == "system") continue; - // tool_result → OpenAI role:"tool" 형식 (watsonx /text/chat 지원) + // IBM/Qwen 배포형은 과거 tool_calls/tool 메시지 이력을 엄격하게 검사하는 경우가 있어 + // 이전 tool-use 대화는 평탄한 transcript로 재구성한다. if (m.Role == "user" && m.Content.StartsWith("{\"type\":\"tool_result\"")) { try { using var doc = JsonDocument.Parse(m.Content); var root = doc.RootElement; - msgs.Add(new - { - role = "tool", - tool_call_id = root.GetProperty("tool_use_id").SafeGetString(), - content = root.GetProperty("content").SafeGetString(), - }); + msgs.Add(new { role = "user", content = BuildIbmToolResultTranscript(root) }); continue; } catch { } } - // _tool_use_blocks → OpenAI tool_calls 형식 + // _tool_use_blocks도 assistant + tool_calls로 다시 보내지 않고 plain assistant transcript로 평탄화 if (m.Role == "assistant" && m.Content.StartsWith("{\"_tool_use_blocks\"")) { try { using var doc = JsonDocument.Parse(m.Content); var blocksArr = doc.RootElement.GetProperty("_tool_use_blocks"); - var textContent = ""; - var toolCallsList = new List(); - foreach (var b in blocksArr.EnumerateArray()) - { - var bType = b.GetProperty("type").SafeGetString(); - if (bType == "text") - textContent = b.GetProperty("text").SafeGetString() ?? ""; - else if (bType == "tool_use") - { - var argsJson = b.SafeTryGetProperty("input", out var inp) ? inp.GetRawText() : "{}"; - toolCallsList.Add(new - { - id = b.GetProperty("id").SafeGetString() ?? "", - type = "function", - function = new - { - name = b.GetProperty("name").SafeGetString() ?? "", - arguments = argsJson, - } - }); - } - } - msgs.Add(new - { - role = "assistant", - content = string.IsNullOrEmpty(textContent) ? (string?)null : textContent, - tool_calls = toolCallsList, - }); + msgs.Add(new { role = "assistant", content = BuildIbmAssistantTranscript(blocksArr) }); continue; } catch { } @@ -999,6 +968,60 @@ public partial class LlmService }; } + private static string BuildIbmAssistantTranscript(JsonElement blocksArr) + { + var textSegments = new List(); + var toolSegments = new List(); + + foreach (var block in blocksArr.EnumerateArray()) + { + var blockType = block.GetProperty("type").SafeGetString(); + if (string.Equals(blockType, "text", StringComparison.OrdinalIgnoreCase)) + { + var text = block.SafeTryGetProperty("text", out var textEl) ? textEl.SafeGetString() ?? "" : ""; + if (!string.IsNullOrWhiteSpace(text)) + textSegments.Add(text.Trim()); + continue; + } + + if (!string.Equals(blockType, "tool_use", StringComparison.OrdinalIgnoreCase)) + continue; + + var name = block.SafeTryGetProperty("name", out var nameEl) ? nameEl.SafeGetString() ?? "" : ""; + var args = block.SafeTryGetProperty("input", out var inputEl) ? inputEl.GetRawText() : "{}"; + if (string.IsNullOrWhiteSpace(name)) + continue; + + toolSegments.Add($"\n{{\"name\":\"{name}\",\"arguments\":{args}}}\n"); + } + + var parts = new List(); + if (textSegments.Count > 0) + parts.Add(string.Join("\n\n", textSegments)); + if (toolSegments.Count > 0) + parts.Add(string.Join("\n", toolSegments)); + + return parts.Count == 0 + ? "\n{\"name\":\"unknown_tool\",\"arguments\":{}}\n" + : string.Join("\n\n", parts); + } + + private static string BuildIbmToolResultTranscript(JsonElement root) + { + var toolCallId = root.SafeTryGetProperty("tool_use_id", out var idEl) ? idEl.SafeGetString() ?? "" : ""; + var toolName = root.SafeTryGetProperty("tool_name", out var nameEl) ? nameEl.SafeGetString() ?? "" : ""; + var content = root.SafeTryGetProperty("content", out var contentEl) ? contentEl.SafeGetString() ?? "" : ""; + var header = string.IsNullOrWhiteSpace(toolName) + ? "[Tool Result]" + : $"[Tool Result: {toolName}]"; + if (!string.IsNullOrWhiteSpace(toolCallId)) + header += $" (id={toolCallId})"; + + return string.IsNullOrWhiteSpace(content) + ? $"{header}\n(no output)" + : $"{header}\n{content}"; + } + private sealed class ToolCallAccumulator { public int Index { get; init; } diff --git a/src/AxCopilot/Views/ChatWindow.SystemPromptBuilder.cs b/src/AxCopilot/Views/ChatWindow.SystemPromptBuilder.cs new file mode 100644 index 0000000..b9853dc --- /dev/null +++ b/src/AxCopilot/Views/ChatWindow.SystemPromptBuilder.cs @@ -0,0 +1,435 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using AxCopilot.Models; +using AxCopilot.Services; + +namespace AxCopilot.Views; + +public partial class ChatWindow +{ + // ─── 코워크 에이전트 지원 ──────────────────────────────────────────── + + private string BuildCoworkSystemPrompt() + { + var workFolder = GetCurrentWorkFolder(); + var llm = _settings.Settings.Llm; + var sb = new System.Text.StringBuilder(); + // ══════════════════════════════════════════════════════════════════ + // 도구 호출 절대 규칙을 시스템 프롬프트 맨 첫 줄에 배치 — 최대 강제력 + // ══════════════════════════════════════════════════════════════════ + sb.AppendLine("## [절대 규칙] 도구 우선 — USE TOOLS WHEN THEY MATERIALLY ADVANCE THE TASK"); + sb.AppendLine("이 규칙들은 다른 모든 지시보다 우선합니다. 위반 시 작업이 즉시 실패합니다."); + sb.AppendLine("Treat these as operating guidance, not as a requirement to force unnecessary tool calls."); + sb.AppendLine(""); + sb.AppendLine("### [규칙 1] 필요한 도구를 먼저 사용하라 — Tools First When Needed"); + sb.AppendLine("모든 응답에는 반드시 하나 이상의 도구 호출이 포함되어야 합니다."); + sb.AppendLine("텍스트만 있고 도구 호출이 없는 응답은 무효이며 즉시 거부됩니다."); + sb.AppendLine("Use tools whenever they materially advance the task. Do not answer with text only when inspection, file creation, editing, or verification is still needed."); + sb.AppendLine("A text-only response is fine once the requested artifact already exists or enough evidence has been gathered."); + sb.AppendLine("출력의 첫 번째 항목은 반드시 도구 호출이어야 합니다. 텍스트로 시작하는 것은 금지입니다."); + sb.AppendLine("When a tool is clearly useful, call it promptly without a long preamble. Do not force an unnecessary tool call if the task is already complete."); + sb.AppendLine(""); + sb.AppendLine("### [규칙 2] 말 먼저 하지 마라 — No Verbal Preamble"); + sb.AppendLine("절대 금지 문구 — 도구 호출 전에 절대 쓰지 말 것:"); + sb.AppendLine(" ✗ '알겠습니다' / '네' / '확인했습니다' / '작업을 시작하겠습니다' / '먼저 ... 하겠습니다'"); + sb.AppendLine(" ✗ '다음과 같이 진행하겠습니다' / '분석해보겠습니다' / '살펴보겠습니다'"); + sb.AppendLine(" ✗ 'I will ...' / 'Let me ...' / 'Sure!' / 'Of course!' / 'I'll now ...' / 'First, I need to ...'"); + sb.AppendLine("올바른 행동: 즉시 도구를 호출하세요. 간단한 설명은 도구 결과 이후에만 허용됩니다."); + sb.AppendLine("CORRECT: call the tool immediately. A brief explanation may follow AFTER the tool result, never before."); + sb.AppendLine(""); + sb.AppendLine("### [규칙 3] 한 번에 여러 도구를 호출하라 — Batch Multiple Tools"); + sb.AppendLine("독립적인 작업들은 반드시 같은 응답에서 동시에 호출해야 합니다. 순차적으로 하지 마세요."); + sb.AppendLine("Batch independent reads only when it genuinely saves time and keeps the scope narrow."); + sb.AppendLine(" 나쁜 예(BAD): 응답1: folder_map → 응답2: document_read → 응답3: html_create (순차 처리 — 금지)"); + sb.AppendLine(" 좋은 예(GOOD): 응답1: folder_map + document_read 동시 호출 → 응답2: html_create (배치 처리)"); + sb.AppendLine(""); + sb.AppendLine("### [규칙 4] 불확실해도 즉시 도구를 호출하라 — Act on Uncertainty"); + sb.AppendLine("어떤 파일을 읽어야 할지 모를 때도 사용자에게 묻지 말고 즉시 folder_map이나 document_read를 호출하세요."); + sb.AppendLine("If the task requires existing files, start with glob or grep, then use document_read or file_read on the best candidates. If the user wants a brand-new document, skip file exploration and create the artifact directly."); + sb.AppendLine(""); + sb.AppendLine("### [규칙 5] 완료까지 계속 도구를 호출하라 — Continue Until Complete"); + sb.AppendLine("사용자의 요청이 완전히 완료될 때까지 도구 호출을 계속하세요. '작업을 시작했습니다'는 완료가 아닙니다."); + sb.AppendLine("'완료'의 정의: 결과 파일이 실제로 존재하고, 검증되고, 사용자에게 보고된 상태."); + sb.AppendLine("Keep working until the artifact exists or a concrete blocker is reached. Prefer the shortest path to a complete result."); + sb.AppendLine(""); + + // 소개 및 메타 정보 + sb.AppendLine("---"); + sb.AppendLine("You are AX Copilot Agent. You can read, write, and edit files using the provided tools."); + sb.AppendLine($"Today's date: {DateTime.Now:yyyy년 M월 d일} ({DateTime.Now:yyyy-MM-dd}, {DateTime.Now:dddd})."); + sb.AppendLine("Available skills: excel_create (.xlsx), docx_create (.docx), csv_create (.csv), markdown_create (.md), html_create (.html), script_create (.bat/.ps1), document_review (품질 검증), format_convert (포맷 변환)."); + + sb.AppendLine("\nOnly present a step-by-step plan when the user explicitly asks for a plan or when the task is too ambiguous to execute safely."); + sb.AppendLine("For ordinary Cowork requests, proceed directly with the work and focus on producing the real artifact."); + sb.AppendLine("If the user asks for a brand-new report, proposal, analysis, manual, or other document and does not explicitly ask to reference workspace files, do NOT start with glob, grep, document_read, or folder_map."); + sb.AppendLine("In that case, go straight to document_plan when helpful, then immediately call the creation tool such as docx_create, html_create, markdown_create, excel_create, or document_assemble."); + sb.AppendLine("After creating files, summarize what was created and include the actual output path."); + sb.AppendLine("Do not stop after a single step. Continue autonomously until the request is completed or a concrete blocker (permission denial, missing dependency, hard error) is encountered."); + sb.AppendLine("When adapting external references, rewrite names/structure/comments to AX Copilot style. Avoid clone-like outputs."); + sb.AppendLine("IMPORTANT: When creating documents with dates, always use today's actual date above. Never use placeholder or fictional dates."); + sb.AppendLine("IMPORTANT: For reports, proposals, analyses, and manuals with multiple sections:"); + sb.AppendLine(" 1. Decide the document structure internally first."); + sb.AppendLine(" 2. Use document_plan only when it improves the actual output, not as a mandatory user-facing approval step."); + sb.AppendLine(" 3. Then immediately create the real output file with html_create, docx_create, markdown_create, or file_write."); + sb.AppendLine(" 4. Fill every section with real content. Do not stop at an outline or plan only."); + sb.AppendLine(" 5. The task is complete only after the actual document file has been created or updated."); + + // 문서 품질 검증 루프 + sb.AppendLine("\n## Document Quality Review"); + sb.AppendLine("After creating a document, verify it in the lightest way that is sufficient:"); + sb.AppendLine("1. Start with file_read or document_read to confirm the generated file exists and covers the requested sections"); + sb.AppendLine("2. Use document_review only for large generated documents, conversion-heavy output, or when the user explicitly asks for a quality check"); + sb.AppendLine("3. Check logical errors only when relevant: dates, missing sections, broken formatting, or placeholder text"); + sb.AppendLine("4. If issues are found, fix them using file_write or file_edit, then re-verify"); + sb.AppendLine("5. Report what was checked and whether corrections were needed"); + + // 문서 포맷 변환 지원 + sb.AppendLine("\n## Format Conversion"); + sb.AppendLine("When the user requests format conversion (e.g., HTML→Word, Excel→CSV, Markdown→HTML):"); + sb.AppendLine("Use format conversion tools only when the user explicitly requests conversion between formats."); + sb.AppendLine("1. Use file_read or document_read to read the source file content"); + sb.AppendLine("2. Create a new file in the target format using the appropriate skill (docx_create, html_create, etc.)"); + sb.AppendLine("3. Preserve the content structure, formatting, and data as closely as possible"); + + // 사용자 지정 출력 포맷 + var fmt = llm.DefaultOutputFormat; + if (!string.IsNullOrEmpty(fmt) && fmt != "auto") + { + var fmtMap = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["xlsx"] = "Excel (.xlsx) using excel_create", + ["docx"] = "Word (.docx) using docx_create", + ["html"] = "HTML (.html) using html_create", + ["md"] = "Markdown (.md) using markdown_create", + ["csv"] = "CSV (.csv) using csv_create", + }; + if (fmtMap.TryGetValue(fmt, out var fmtDesc)) + sb.AppendLine($"IMPORTANT: User prefers output format: {fmtDesc}. Use this format unless the user specifies otherwise."); + } + + // 디자인 무드 — HTML 문서 생성 시 mood 파라미터로 전달하도록 안내 + if (!string.IsNullOrEmpty(_selectedMood) && _selectedMood != "modern") + sb.AppendLine($"When creating HTML documents with html_create, use mood=\"{_selectedMood}\" for the design template."); + else + sb.AppendLine("When creating HTML documents with html_create, you can set 'mood' parameter: modern, professional, creative, minimal, elegant, dark, colorful, corporate, magazine, dashboard."); + + if (!string.IsNullOrEmpty(workFolder)) + sb.AppendLine($"Current work folder: {workFolder}"); + sb.AppendLine($"File permission mode: {llm.FilePermission}"); + sb.Append(BuildSubAgentDelegationSection(false)); + + // 폴더 데이터 활용 지침 (사용자 옵션이 아닌 탭별 자동 정책) + switch (GetAutomaticFolderDataUsage()) + { + case "active": + sb.AppendLine("IMPORTANT: Folder Data Usage = ACTIVE. You have 'document_read' and 'folder_map' tools available."); + sb.AppendLine("Use workspace exploration only when the user explicitly asks to reference existing materials, provides a file/path hint, or asks about folder contents."); + sb.AppendLine("For brand-new document creation requests, skip glob/grep/document_read/folder_map and proceed directly to document_plan plus the creation tool."); + sb.AppendLine("[CRITICAL] FILE SELECTION STRATEGY — DO NOT READ ALL FILES:"); + sb.AppendLine(" 1. Identify candidate files by filename or topic keywords first."); + sb.AppendLine(" 2. Read ONLY files that clearly match the user's topic. Skip unrelated topics."); + sb.AppendLine(" 3. Maximum 2-3 relevant files for the first pass. Expand only when evidence shows more files are needed."); + sb.AppendLine(" 4. Do NOT read every file 'just in case'. Broad reading without evidence is forbidden."); + sb.AppendLine(" 5. If no files match the topic, proceed WITHOUT reading any workspace files."); + sb.AppendLine("VIOLATION: Reading all files in the folder is FORBIDDEN. It wastes tokens and degrades quality."); + break; + case "passive": + sb.AppendLine("Folder Data Usage = PASSIVE. You have 'document_read' and 'folder_map' tools. " + + "Only read folder documents when the user explicitly asks you to reference or use them."); + break; + default: // "none" + sb.AppendLine("Folder Data Usage = NONE. Do NOT read or reference documents in the work folder unless the user explicitly provides a file path."); + break; + } + + // 프리셋 시스템 프롬프트가 있으면 추가 + lock (_convLock) + { + if (_currentConversation != null && !string.IsNullOrEmpty(_currentConversation.SystemCommand)) + sb.AppendLine("\n" + _currentConversation.SystemCommand); + } + + // 프로젝트 문맥 파일 (AGENTS.md) 주입 + sb.Append(LoadProjectContext(workFolder)); + + // 프로젝트 규칙 (.ax/rules/) 자동 주입 + sb.Append(BuildProjectRulesSection(workFolder)); + + // 에이전트 메모리 주입 + sb.Append(BuildMemorySection(workFolder)); + + // 피드백 학습 컨텍스트 주입 + sb.Append(BuildFeedbackContext()); + + return sb.ToString(); + } + + private string BuildCodeSystemPrompt() + { + var workFolder = GetCurrentWorkFolder(); + var llm = _settings.Settings.Llm; + var code = llm.Code; + var sb = new System.Text.StringBuilder(); + + // ══════════════════════════════════════════════════════════════════ + // 도구 호출 절대 규칙을 시스템 프롬프트 맨 첫 줄에 배치 — 최대 강제력 + // ══════════════════════════════════════════════════════════════════ + sb.AppendLine("## [절대 규칙] 도구 우선 — USE TOOLS WHEN THEY MATERIALLY ADVANCE THE TASK"); + sb.AppendLine("이 규칙들은 다른 모든 지시보다 우선합니다. 위반 시 작업이 즉시 실패합니다."); + sb.AppendLine("Treat these as operating guidance, not as a requirement to force unnecessary tool calls."); + sb.AppendLine(""); + sb.AppendLine("### [규칙 1] 필요한 도구를 먼저 사용하라 — Tools First When Needed"); + sb.AppendLine("모든 응답에는 반드시 하나 이상의 도구 호출이 포함되어야 합니다."); + sb.AppendLine("텍스트만 있고 도구 호출이 없는 응답은 무효이며 즉시 거부됩니다."); + sb.AppendLine("Use tools whenever they materially advance the task. Do not answer with text only when code inspection, editing, diff review, or verification is still needed."); + sb.AppendLine("A text-only response is fine once the requested code change is complete or enough evidence has been gathered."); + sb.AppendLine("출력의 첫 번째 항목은 반드시 도구 호출이어야 합니다. 텍스트로 시작하는 것은 금지입니다."); + sb.AppendLine("When a tool is clearly useful, call it promptly without a long preamble. Do not force an unnecessary tool call if the task is already complete."); + sb.AppendLine(""); + sb.AppendLine("### [규칙 2] 말 먼저 하지 마라 — No Verbal Preamble"); + sb.AppendLine("절대 금지 문구 — 도구 호출 전에 절대 쓰지 말 것:"); + sb.AppendLine(" ✗ '알겠습니다' / '네' / '확인했습니다' / '분석해보겠습니다' / '살펴보겠습니다'"); + sb.AppendLine(" ✗ '먼저 폴더 구조를 파악하겠습니다' — folder_map을 즉시 호출하세요, 예고하지 마세요"); + sb.AppendLine(" ✗ 'I will ...' / 'Let me ...' / 'Sure!' / 'I'll start by ...' / 'First, I need to ...'"); + sb.AppendLine("올바른 행동: 즉시 도구를 호출하세요. 한 문장 설명은 도구 결과 이후에만 허용됩니다."); + sb.AppendLine("CORRECT: call the tool immediately. One-sentence explanation may follow the tool result, never precede it."); + sb.AppendLine(""); + sb.AppendLine("### [규칙 3] 모든 독립 작업은 병렬로 호출하라 — Parallelise Everything Possible"); + sb.AppendLine("독립적인 읽기/검색 작업들은 반드시 같은 응답에서 동시에 호출해야 합니다. 절대 순차적으로 하지 마세요."); + sb.AppendLine("Batch independent reads/searches only when it genuinely saves time and keeps the scope narrow."); + sb.AppendLine(" 나쁜 예(BAD): R1: folder_map → R2: grep → R3: file_read → R4: file_edit (순차 — 금지)"); + sb.AppendLine(" 좋은 예(GOOD): R1: folder_map + grep + file_read 동시 호출 → R2: file_edit + build_run"); + sb.AppendLine("모든 독립적인 읽기/검색 작업은 같은 응답에 배치해야 합니다."); + sb.AppendLine("Prefer targeted grep/glob plus a small number of file reads over broad parallel sweeps."); + sb.AppendLine(""); + sb.AppendLine("### [규칙 4] 불확실해도 즉시 도구를 호출하라 — Act on Uncertainty, Do Not Ask"); + sb.AppendLine("어떤 파일을 읽어야 할지 모를 때: 즉시 glob이나 grep을 호출하세요."); + sb.AppendLine("의존성이 불확실할 때: 즉시 dev_env_detect를 호출하세요."); + sb.AppendLine("If unsure which file, use glob or grep to narrow the target. If unsure about dependencies or tooling, use dev_env_detect."); + sb.AppendLine("'잘 모르겠습니다, 알려주시겠어요?' — 도구 호출로 답을 구할 수 있다면 절대 사용자에게 묻지 마세요."); + sb.AppendLine("Prefer answering uncertainty with a focused tool call instead of asking avoidable clarification questions."); + sb.AppendLine(""); + sb.AppendLine("### [규칙 5] 도구로 검증하라, 가정하지 마라 — Verify with Tools, Not Assumptions"); + sb.AppendLine("편집 후에는 반드시 file_read로 최종 상태를 확인하세요. 편집이 성공했다고 가정하지 마세요."); + sb.AppendLine("빌드/테스트 후에는 build_run의 실제 출력을 인용하세요. '정상 작동할 것입니다'라고 말하지 마세요."); + sb.AppendLine("After editing, re-open the changed file when confirmation is helpful. Do not assume a risky edit succeeded."); + sb.AppendLine("When you run build/test, cite the actual build_run output instead of assuming success."); + sb.AppendLine("'완료'의 정의: 빌드 통과 + 테스트 통과 + 편집 파일 재확인 + 결과 보고 — 모두 도구 출력으로 증명된 상태."); + sb.AppendLine("'Done' = build passes + tests pass + edited files re-read + result reported — all proven by tool output."); + sb.AppendLine(""); + + // 소개 및 메타 정보 + sb.AppendLine("---"); + sb.AppendLine("You are AX Copilot Code Agent — a senior software engineer for enterprise development."); + sb.AppendLine($"Today's date: {DateTime.Now:yyyy년 M월 d일} ({DateTime.Now:yyyy-MM-dd})."); + sb.AppendLine("Available tools: file_read, file_write, file_edit (supports replace_all), glob, grep (supports context_lines, case_sensitive), lsp_code_intel, folder_map, process, dev_env_detect, build_run, git_tool."); + sb.AppendLine("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("\n## Core Workflow"); + sb.AppendLine("1. ORIENT: Pick the smallest next step that can answer the request."); + sb.AppendLine(" - If a specific file is already obvious, start with file_read directly."); + sb.AppendLine(" - If you need definitions, references, symbols, implementations, or call hierarchy, prefer lsp_code_intel before broad searching."); + sb.AppendLine(" - Otherwise use grep/glob to find the smallest relevant set of files."); + sb.AppendLine(" - Use folder_map only when the repository structure itself is unclear."); + sb.AppendLine("2. ANALYZE: Read only the files needed to understand the change."); + sb.AppendLine(" - Check impacted callers/references when changing shared code or public behavior. Use lsp_code_intel when semantic lookup will be more precise than text search."); + sb.AppendLine("3. IMPLEMENT: Apply the smallest safe edit. Use file_edit for existing files and file_write for new files."); + 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("5. REPORT: Summarize what changed, what was verified, and any remaining risk."); + + sb.AppendLine("\n## Development Environment"); + sb.AppendLine("Use dev_env_detect to check installed IDEs, runtimes, and build tools before running commands."); + sb.AppendLine("IMPORTANT: Do NOT attempt to install compilers, IDEs, or build tools. Only use what is already installed."); + + // 패키지 저장소 정보 + sb.AppendLine("\n## Package Repositories"); + if (!string.IsNullOrEmpty(code.NexusBaseUrl)) + sb.AppendLine($"Enterprise Nexus: {code.NexusBaseUrl}"); + sb.AppendLine($"NuGet (.NET): {code.NugetSource}"); + sb.AppendLine($"PyPI/Conda (Python): {code.PypiSource}"); + sb.AppendLine($"Maven (Java): {code.MavenSource}"); + sb.AppendLine($"npm (JavaScript): {code.NpmSource}"); + sb.AppendLine("When adding dependencies, use these repository URLs."); + + // IDE 정보 + if (!string.IsNullOrEmpty(code.PreferredIdePath)) + sb.AppendLine($"\nPreferred IDE: {code.PreferredIdePath}"); + + // 사용자 선택 개발 언어 + if (_selectedLanguage != "auto") + { + var langName = _selectedLanguage switch { "python" => "Python", "java" => "Java", "csharp" => "C# (.NET)", "cpp" => "C/C++", "javascript" => "JavaScript/TypeScript", _ => _selectedLanguage }; + sb.AppendLine($"\nIMPORTANT: User selected language: {langName}. Prioritize this language for code analysis and generation."); + } + + // 언어별 가이드라인 + sb.AppendLine("\n## Language Guidelines"); + sb.AppendLine("- C# (.NET): Use dotnet CLI. NuGet for packages. Follow Microsoft naming conventions."); + sb.AppendLine("- Python: Use conda/pip. Follow PEP8. Use type hints. Virtual env preferred."); + sb.AppendLine("- Java: Use Maven/Gradle. Follow Google Java Style Guide."); + sb.AppendLine("- C++: Use CMake for build. Follow C++ Core Guidelines."); + sb.AppendLine("- JavaScript/TypeScript: Use npm/yarn. Follow ESLint rules. Vue3 uses Composition API."); + + // 코드 품질 + 안전 수칙 + sb.AppendLine("\n## Code Quality & Safety"); + sb.AppendLine("- NEVER delete or overwrite files without user confirmation."); + sb.AppendLine("- ALWAYS read a file before editing it. Don't guess contents."); + sb.AppendLine("- Prefer file_edit over file_write for existing files (shows diff)."); + sb.AppendLine("- When porting/referencing external code, do not copy verbatim. Rename and re-structure to match AX Copilot conventions."); + sb.AppendLine("- Use grep to find ALL references before renaming/removing anything."); + sb.AppendLine("- After editing, re-open the changed files and nearby callers to verify the final state, not just the patch intent."); + sb.AppendLine("- Treat verification as incomplete unless you can cite build/test or direct file-read evidence."); + sb.AppendLine("- If unsure about a change's impact, ask the user first."); + sb.AppendLine("- For large refactors, do them incrementally with build verification between steps."); + sb.AppendLine("- Use git_tool action='diff' to review your changes before committing."); + + sb.AppendLine("\n## Lint & Format"); + sb.AppendLine("After code changes, check for available linters:"); + sb.AppendLine("- Python: ruff, black, flake8, pylint"); + sb.AppendLine("- JavaScript: eslint, prettier"); + sb.AppendLine("- C#: dotnet format"); + sb.AppendLine("- C++: clang-format"); + sb.AppendLine("Run the appropriate linter via process tool if detected by dev_env_detect."); + + if (!string.IsNullOrEmpty(workFolder)) + sb.AppendLine($"\nCurrent work folder: {workFolder}"); + sb.AppendLine($"File permission mode: {llm.FilePermission}"); + sb.Append(BuildSubAgentDelegationSection(true)); + + // 폴더 데이터 활용 + sb.AppendLine("\nFolder Data Usage = ACTIVE."); + sb.AppendLine("Prefer targeted file_read, grep/glob, and lsp_code_intel for narrow requests."); + sb.AppendLine("Use folder_map only when structure is unclear, the user explicitly asks for folder contents, or the request is repo-wide."); + sb.AppendLine("Read only files that are relevant to the current question. Avoid broad codebase sweeps without evidence."); + + // 프리셋 시스템 프롬프트 + lock (_convLock) + { + if (_currentConversation?.SystemCommand is { Length: > 0 } sysCmd) + sb.AppendLine("\n" + sysCmd); + } + + // 프로젝트 문맥 파일 (AGENTS.md) 주입 + sb.Append(LoadProjectContext(workFolder)); + + // 프로젝트 규칙 (.ax/rules/) 자동 주입 + sb.Append(BuildProjectRulesSection(workFolder)); + + // 에이전트 메모리 주입 + sb.Append(BuildMemorySection(workFolder)); + + // 피드백 학습 컨텍스트 주입 + sb.Append(BuildFeedbackContext()); + + return sb.ToString(); + } + + private static string BuildSubAgentDelegationSection(bool codeMode) + { + var sb = new System.Text.StringBuilder(); + sb.AppendLine("\n## Sub-Agent Delegation"); + sb.AppendLine("Use spawn_agent only when a bounded side investigation can run in parallel without blocking your own next step."); + sb.AppendLine("Good delegation targets: impact analysis, reference/caller search, test-file discovery, diff review, and bug root-cause investigation."); + sb.AppendLine("Do not delegate the final editing decision, the final report, or blocking work that you must inspect immediately yourself."); + sb.AppendLine("When spawning a sub-agent, give a concrete task with the exact question, likely file/module scope, and the output shape you want."); + sb.AppendLine("Expected sub-agent result shape: conclusion, files checked, key evidence, recommended next action, risks/unknowns."); + sb.AppendLine("Use wait_agents only when you are ready to integrate the result. Keep doing useful local work while the sub-agent runs."); + if (codeMode) + { + sb.AppendLine("For code tasks, prefer delegating read-only investigations such as caller mapping, related test discovery, or build/test failure triage."); + sb.AppendLine("For high-impact code changes, consider delegation when it quickly adds evidence, but do not treat it as mandatory."); + } + else + { + sb.AppendLine("For cowork tasks, consider delegation only when fact gathering or source cross-checking clearly speeds up the work."); + } + + return sb.ToString(); + } + + /// 프로젝트 규칙 (.ax/rules/)을 시스템 프롬프트 섹션으로 포맷합니다. + private string BuildProjectRulesSection(string? workFolder) + { + if (string.IsNullOrEmpty(workFolder)) return ""; + if (!_settings.Settings.Llm.EnableProjectRules) return ""; + + try + { + var rules = Services.Agent.ProjectRulesService.LoadRules(workFolder); + if (rules.Count == 0) return ""; + + // 컨텍스트별 필터링: Cowork=document, Code=always (기본) + var when = _activeTab == "Code" ? "always" : "always"; + var filtered = Services.Agent.ProjectRulesService.FilterRules(rules, when); + return Services.Agent.ProjectRulesService.FormatForSystemPrompt(filtered); + } + catch + { + return ""; + } + } + + /// 에이전트 메모리를 시스템 프롬프트 섹션으로 포맷합니다. + private string BuildMemorySection(string? workFolder) + { + if (!_settings.Settings.Llm.EnableAgentMemory) return ""; + + var app = System.Windows.Application.Current as App; + var memService = app?.MemoryService; + if (memService == null) return ""; + + // 메모리를 로드 (작업 폴더 변경 시 재로드) + memService.Load(workFolder ?? ""); + + var all = memService.All; + var layeredDocs = memService.InstructionDocuments; + if (all.Count == 0 && layeredDocs.Count == 0) return ""; + + var sb = new System.Text.StringBuilder(); + sb.AppendLine("\n## 메모리 계층"); + sb.AppendLine("다음 메모리는 claude-code와 비슷하게 관리형 → 사용자 → 프로젝트 → 로컬 순서로 조립됩니다."); + sb.AppendLine("현재 작업 디렉토리에 가까운 메모리가 더 높은 우선순위를 가집니다.\n"); + + const int maxLayeredDocs = 8; + const int maxDocChars = 1800; + foreach (var doc in layeredDocs.Take(maxLayeredDocs)) + { + sb.AppendLine($"[{doc.Label}] {doc.Path}"); + var text = doc.Content; + if (text.Length > maxDocChars) + text = text[..maxDocChars] + "\n...(생략)"; + sb.AppendLine(text); + sb.AppendLine(); + } + + if (all.Count > 0) + { + sb.AppendLine("## 학습 메모리 (이전 대화에서 학습한 내용)"); + sb.AppendLine("아래는 이전 대화에서 학습한 규칙과 선호도입니다. 작업 시 참고하세요."); + sb.AppendLine("새로운 규칙이나 선호도를 발견하면 memory 도구의 save 액션으로 저장하세요."); + sb.AppendLine("사용자가 이전 학습 내용과 다른 지시를 하면 memory 도구의 delete 후 새로 save 하세요.\n"); + + foreach (var group in all.GroupBy(e => e.Type)) + { + var label = group.Key switch + { + "rule" => "프로젝트 규칙", + "preference" => "사용자 선호", + "fact" => "프로젝트 사실", + "correction" => "이전 교정", + _ => group.Key, + }; + sb.AppendLine($"[{label}]"); + foreach (var e in group.OrderByDescending(e => e.UseCount).Take(15)) + sb.AppendLine($"- {e.Content}"); + sb.AppendLine(); + } + } + + return sb.ToString(); + } +}