Compare commits
3 Commits
main
...
codex/core
| Author | SHA1 | Date | |
|---|---|---|---|
| 6bd8d5bb2c | |||
| 1fab215344 | |||
| 3c6d2f1ce4 |
62
README.md
62
README.md
@@ -7,10 +7,51 @@ Windows 전용 시맨틱 런처 & 워크스페이스 매니저
|
|||||||
개발 참고: Claw Code 동등성 작업 추적 문서
|
개발 참고: Claw Code 동등성 작업 추적 문서
|
||||||
`docs/claw-code-parity-plan.md`
|
`docs/claw-code-parity-plan.md`
|
||||||
|
|
||||||
- 업데이트: 2026-04-09 13:05 (KST)
|
- 업데이트: 2026-04-09 23:02 (KST)
|
||||||
- AX Agent transcript 표시를 `claude-code`식 row 타입 기반으로 재정리했습니다. `thinking / waiting / compact / tool activity / permission / tool result / status`를 별도 row 의미로 정규화하고, process feed는 같은 활동 그룹이면 이어붙여 append 수를 줄이도록 바꿨습니다.
|
- IBM/Qwen 후속 호환을 한 단계 더 보강했습니다. 이제 IBM 배포형 응답에서 `generated_text`, `output_text`, `message.content`, `reasoning_content`가 문자열뿐 아니라 배열/블록 형태로 와도 텍스트를 추출하고, `content` 배열 안의 `tool_use/tool_call` 블록도 직접 읽어 도구 호출로 복구합니다.
|
||||||
- 권한 요청과 도구 결과 카탈로그를 다시 정리해 Cowork/Code 진행 중 무엇을 하는지, 어떤 결과 상태인지가 기본 transcript에서 더 읽히도록 맞췄습니다. 동시에 render 성능 로그에 row kind 분포와 process feed append/merge 수치를 남겨 이후 실검증 때 구조 개선 효과를 바로 판단할 수 있게 했습니다.
|
- 활성 도구 노출 순서도 다시 정리했습니다. `file_read/file_edit/glob/grep/lsp_code_intel/build_run/document_plan` 같은 기본 도구를 먼저 보여주고, `document_review/format_convert/tool_search/code_search`는 그 다음, `mcp_*`, `spawn_agent`, `wait_agents`, `task_*`는 더 뒤로 미뤄 `claude-code`처럼 기본 작업 도구가 먼저 선택되도록 했습니다.
|
||||||
- Cowork/Chat 하단 프리셋 안내 카드는 실제 메시지뿐 아니라 execution event가 생긴 뒤에도 자동으로 숨겨 결과/진행 화면을 가리지 않도록 조정했고, 입력 워터마크와 footer 기본 문구도 정상 한국어 기준으로 다시 정리했습니다.
|
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\` 경고 0 / 오류 0
|
||||||
|
|
||||||
|
- 업데이트: 2026-04-09 22:48 (KST)
|
||||||
|
- `claude-code` 기준 후속 과제를 이어서 반영했습니다. IBM 배포형 vLLM/Qwen 경로에서는 과거 `tool_calls`/`role=tool` 이력을 그대로 재전송하지 않고, `<tool_call>...</tool_call>` 중심의 평탄한 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 대상에 넣지 않도록 조정했습니다.
|
||||||
|
- Cowork/Code에서 최종 텍스트 응답이 비어 있을 때도 무조건 완료 문구를 합성하지 않고, 실행 이벤트 근거가 있을 때만 요약을 만들도록 보수적으로 바꿨습니다.
|
||||||
|
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\` 경고 0 / 오류 0
|
||||||
|
|
||||||
|
- 업데이트: 2026-04-09 20:46 (KST)
|
||||||
|
- AX Code 핵심 루프의 과한 검증 개입을 줄여 `claude-code`에 더 가깝게 정리했습니다. 일반 코드 수정 뒤에는 즉시 별도 검증 루프를 다시 돌리지 않고, 고영향 수정일 때만 post-tool verification을 유지하도록 조정했습니다.
|
||||||
|
- Code 종료 게이트도 순서를 다듬어, diff/build/test 근거가 이미 있는 일반 수정은 `CodeQualityGate`를 과하게 반복하지 않게 했고 `FinalReportGate`는 실제 검증 공백이 해소된 뒤에만 작동하도록 바꿨습니다.
|
||||||
|
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\` 경고 0 / 오류 0
|
||||||
|
|
||||||
|
- 업데이트: 2026-04-09 20:29 (KST)
|
||||||
|
- Cowork/Code 입력창에 파일명 후보 칩을 추가했습니다. 작업 폴더가 잡힌 상태에서 파일명 일부, 경로 조각, 확장자 힌트를 입력하면 관련 파일 후보를 바로 보여주고 클릭이나 `Tab`으로 삽입할 수 있어 `folder_map` 없이도 특정 파일 지정을 더 빠르게 시작할 수 있습니다.
|
||||||
|
- `folder_map`이 `0 files, 0 dirs`를 반환했는데 실제로는 보이는 파일이 있는 경우를 자동 복구하도록 에이전트 루프를 보강했습니다. 이제 같은 경로에 대해 `folder_map`을 반복하지 않고, 내부 fallback 스캔으로 후보 파일을 찾아 `glob -> file_read/document_read` 쪽으로 바로 재유도합니다.
|
||||||
|
- 워크플로우 로그에는 마지막 도구 결과 이후 다음 LLM 호출까지의 대기 구간을 `llm_wait_after_tool_result` 전이로 남기고, `folder_map_empty_recovery` 복구 전이도 함께 기록합니다. 멈춤처럼 보이는 상황을 다음부터는 로그만 보고 더 빨리 구분할 수 있습니다.
|
||||||
|
- `DocumentPlannerTool`과 스킬 예시 가이드의 탐색 문구도 다시 정리해, 내부 계획/스킬 층까지 `glob/grep -> targeted read -> folder_map(필요 시만)` 기준으로 맞췄습니다.
|
||||||
|
- Code 루프에는 같은 경로 반복 접근 가드를 추가했습니다. `file_read/document_read/grep/glob`가 동일 파일 또는 동일 프로젝트 경로를 연속으로 재확인하면 4회째부터는 그대로 읽지 않고, 호출부 탐색·`git_tool(diff)`·`build_run/test_loop` 쪽으로 전환하라는 복구 메시지를 먼저 주입합니다.
|
||||||
|
|
||||||
- 업데이트: 2026-04-09 01:33 (KST)
|
- 업데이트: 2026-04-09 01:33 (KST)
|
||||||
- AX Agent transcript 호스트를 `ObservableCollection<UIElement>` 직접 주입 구조에서 `TranscriptVisualItem + TranscriptVisualHost` 기반의 지연 materialization 구조로 올렸습니다. `MessageList`는 virtualization 설정을 유지한 채 필요한 시점에만 실제 버블 UI를 생성할 수 있는 기반을 갖습니다.
|
- AX Agent transcript 호스트를 `ObservableCollection<UIElement>` 직접 주입 구조에서 `TranscriptVisualItem + TranscriptVisualHost` 기반의 지연 materialization 구조로 올렸습니다. `MessageList`는 virtualization 설정을 유지한 채 필요한 시점에만 실제 버블 UI를 생성할 수 있는 기반을 갖습니다.
|
||||||
@@ -1546,16 +1587,3 @@ MIT License
|
|||||||
- [AgentLoopTransitions.Verification.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentLoopTransitions.Verification.cs), [AgentLoopTransitions.Documents.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentLoopTransitions.Documents.cs), [AgentLoopCompactionPolicy.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentLoopCompactionPolicy.cs)로 검증/fallback/compact 정책 메서드를 분리해 [AgentLoopService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentLoopService.cs)의 책임을 더 줄였습니다.
|
- [AgentLoopTransitions.Verification.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentLoopTransitions.Verification.cs), [AgentLoopTransitions.Documents.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentLoopTransitions.Documents.cs), [AgentLoopCompactionPolicy.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentLoopCompactionPolicy.cs)로 검증/fallback/compact 정책 메서드를 분리해 [AgentLoopService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentLoopService.cs)의 책임을 더 줄였습니다.
|
||||||
- [ChatWindow.TranscriptVirtualization.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.TranscriptVirtualization.cs)에서 off-screen 버블 캐시를 pruning하도록 바꿨고, [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml)의 transcript `ListBox`에는 deferred scrolling과 작은 cache length를 적용해 더 강한 가상화 리스트 방향으로 정리했습니다.
|
- [ChatWindow.TranscriptVirtualization.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.TranscriptVirtualization.cs)에서 off-screen 버블 캐시를 pruning하도록 바꿨고, [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml)의 transcript `ListBox`에는 deferred scrolling과 작은 cache length를 적용해 더 강한 가상화 리스트 방향으로 정리했습니다.
|
||||||
- [ChatWindow.TranscriptRendering.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.TranscriptRendering.cs)는 렌더 시간, visible message/event 수, hidden count, lightweight mode 여부를 함께 기록해 실사용 세션에서 버벅임을 실제 수치로 판단할 수 있게 됐습니다.
|
- [ChatWindow.TranscriptRendering.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.TranscriptRendering.cs)는 렌더 시간, visible message/event 수, hidden count, lightweight mode 여부를 함께 기록해 실사용 세션에서 버벅임을 실제 수치로 판단할 수 있게 됐습니다.
|
||||||
- 업데이트: 2026-04-09 10:36 (KST)
|
|
||||||
- `claude-code`의 선택적 탐색 흐름을 다시 대조해, Cowork/Code가 질문 범위와 무관한 워크스페이스 전체를 훑는 경향을 줄이기 시작했습니다.
|
|
||||||
- [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)의 Cowork/Code 시스템 프롬프트를 조정해 `folder_map`을 항상 첫 단계로 요구하지 않고, 좁은 질문에서는 `glob/grep + targeted read`를 우선하도록 바꿨습니다.
|
|
||||||
- [FolderMapTool.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/FolderMapTool.cs)는 기본 depth를 2로, `include_files` 기본값을 `false`로 조정해 첫 탐색 폭을 더 보수적으로 만들었습니다.
|
|
||||||
- [MultiReadTool.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/MultiReadTool.cs)는 한 번에 읽을 수 있는 최대 파일 수를 20개에서 8개로 낮춰 초기 과탐색 토큰 낭비를 줄이도록 했습니다.
|
|
||||||
- [AgentLoopExplorationPolicy.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentLoopExplorationPolicy.cs)와 [AgentLoopService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentLoopService.cs)에 탐색 범위 분류기와 broad-scan corrective hint를 추가해, 좁은 질문에서 반복적인 `folder_map`/대량 `multi_read`가 나오면 관련 파일만 다시 고르도록 교정합니다.
|
|
||||||
- [AgentPerformanceLogService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/AgentPerformanceLogService.cs)는 `%APPDATA%\\AxCopilot\\perf`에 `exploration_breadth` 로그를 남겨 `folder_map` 호출 수, 총 읽은 파일 수, broad scan 여부를 실사용 기준으로 확인할 수 있게 했습니다.
|
|
||||||
- 업데이트: 2026-04-09 11:12 (KST)
|
|
||||||
- `claude-code`의 `Messages.tsx`, `MessageRow.tsx`, `GroupedToolUseContent.tsx`, `UserToolResultMessage`, `PermissionRequest` 구조를 다시 대조해 AX transcript 표시 계약을 row 중심으로 정리했습니다.
|
|
||||||
- [AgentTranscriptDisplayCatalog.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentTranscriptDisplayCatalog.cs)에 `TranscriptRowKind`와 `AgentTranscriptRowPresentation`을 도입해 thinking, waiting, compact, tool activity, permission, tool result, status를 한 번에 정규화하도록 바꿨습니다.
|
|
||||||
- [ChatWindow.AgentEventRendering.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.AgentEventRendering.cs)는 이 row 메타를 사용해 고빈도 process feed를 같은 group key 기준으로 교체 렌더하도록 바꿨고, 연속 읽기/검색/단계 이벤트를 더 적은 row 수로 보여주게 했습니다.
|
|
||||||
- [ToolResultPresentationCatalog.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/ToolResultPresentationCatalog.cs)와 [PermissionRequestPresentationCatalog.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/PermissionRequestPresentationCatalog.cs)를 정리해 실패/거부/취소/승인 필요와 권한 행위를 유형별 카드 메타로 다시 맞췄습니다.
|
|
||||||
- [ChatWindow.TranscriptHost.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.TranscriptHost.cs), [ChatWindow.TranscriptRendering.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.TranscriptRendering.cs)는 grouped process feed append/merge 수를 추적하도록 바꿔, `claude-code`식 activity grouping이 실제 렌더 수를 얼마나 줄였는지 성능 로그로 확인할 수 있게 했습니다.
|
|
||||||
|
|||||||
@@ -1,6 +1,63 @@
|
|||||||
# AX Copilot - 媛쒕컻 臾몄꽌
|
# AX Copilot - 媛쒕컻 臾몄꽌
|
||||||
|
|
||||||
|
|
||||||
|
## claude-code식 후속 호환/선택성 보강
|
||||||
|
|
||||||
|
- 업데이트: 2026-04-09 23:02 (KST)
|
||||||
|
- `LlmService.ToolUse`의 메시지 파서를 더 느슨하게 확장했습니다. `TryExtractMessageToolBlocks`는 이제 `content`/`reasoning_content`가 문자열뿐 아니라 배열일 때도 `text`, `output_text`, nested `content`를 모아 텍스트를 만들고, 배열 안의 `tool_use`/`tool_call` 블록은 직접 `ContentBlock`으로 복구합니다.
|
||||||
|
- IBM 스트리밍 응답의 `results[0]`도 같은 기준으로 읽습니다. `generated_text`, `output_text`가 배열/블록이어도 텍스트를 추출하고, `message` 오브젝트가 있을 때는 그 안의 텍스트/도구 호출까지 함께 처리해 Qwen류 응답 포맷 차이에 덜 민감하게 만들었습니다.
|
||||||
|
- `ToolRegistry`에는 노출 순서 정렬을 추가했습니다. 기본 파일/검색/생성/실행 도구를 가장 앞에 두고, `document_review`·`format_convert`·`tool_search`·`code_search`는 보조 단계, `mcp_*`·`spawn_agent`·`wait_agents`, `task_*` 계열은 더 뒤에 배치해 `claude-code`처럼 기본 작업 도구가 먼저 선택되도록 조정했습니다.
|
||||||
|
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\` 경고 0 / 오류 0
|
||||||
|
|
||||||
|
- 업데이트: 2026-04-09 22:48 (KST)
|
||||||
|
- IBM 배포형 vLLM/Qwen 호환을 위해 `LlmService.ToolUse`의 `BuildIbmToolBody`를 다시 정리했습니다. 이전 assistant `tool_calls`와 `role=tool` 이력을 OpenAI 형식으로 재전송하던 경로를 제거하고, `_tool_use_blocks`는 `<tool_call>...</tool_call>` 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 대상에서 빼서, 탐색 정책과 실행 엔진의 우선순위가 서로 어긋나지 않도록 맞췄습니다.
|
||||||
|
- `AxAgentExecutionEngine`의 Cowork/Code 빈 응답 처리도 보수적으로 조정했습니다. 이제 최종 텍스트가 비어 있을 때는 실행 이벤트에 실제 파일 경로나 유의미한 완료 요약이 있을 때만 합성 메시지를 만들고, 근거가 없으면 로그 확인을 안내하는 쪽으로 바꿨습니다.
|
||||||
|
|
||||||
|
- 업데이트: 2026-04-09 20:46 (KST)
|
||||||
|
- Code 핵심 루프에서 `claude-code`와 가장 크게 달랐던 “수정 직후 과한 검증 개입”을 줄였습니다. `TryApplyPostToolVerificationTransitionAsync`는 이제 Code 탭에서 고영향 수정일 때만 별도 검증 LLM 턴을 실행하고, 일반 수정은 메인 루프의 `diff/build/test` 근거 흐름에 맡기도록 바꿨습니다.
|
||||||
|
- `ApplyCodeQualityFollowUpTransition`도 모든 코드 수정 뒤에 추가 검증 프롬프트를 넣지 않고, 고영향 수정만 즉시 후속 검증을 유도하도록 완화했습니다. 이로써 `file_edit -> file_read -> grep -> file_read` 식의 과도한 재접근이 줄어들도록 정리했습니다.
|
||||||
|
- `TryApplyCodeCompletionGateTransition`은 diff/build/test 근거가 이미 있는 일반 수정에 대해 `CodeQualityGate`를 중복 발동하지 않도록 조정했고, `FinalReportGate`는 코드 검증 공백이 남아 있을 때는 먼저 열리지 않게 순서를 정리했습니다.
|
||||||
|
|
||||||
|
- 업데이트: 2026-04-09 20:29 (KST)
|
||||||
|
- `AgentLoopExplorationPolicy`에 현재 반복 기준의 도구 필터링을 추가해, `Localized`/`TopicBased` 요청에서는 `glob`, `grep`, `file_read`, `document_read`, `multi_read`를 먼저 노출하고 `folder_map`은 기본적으로 뒤로 미루거나 제외하도록 조정했습니다.
|
||||||
|
- `folder_map`은 사용자가 폴더 구조/파일 목록/기존 자료 참조를 명시했거나, 선택 탐색이 몇 차례 실패한 뒤에만 다시 허용합니다. Cowork의 문서형 요청과 Code의 코드 수정 요청이 모두 같은 기준을 따르도록 맞췄습니다.
|
||||||
|
- `TaskTypePolicy`의 `feature`, `bugfix`, `refactor`, `review`, `docs`, `general` 가이드를 다시 정리해 AX가 `claude-code`처럼 `glob/grep -> targeted read`를 먼저 타게 만들었습니다. 기존처럼 `feature/docs`에서 `folder_map`을 선행 단계처럼 유도하던 문구를 제거했습니다.
|
||||||
|
- `AgentLoopService`의 no-tool 재시도 프롬프트와 탐색 교정 메시지, 실패 복구 우선순위도 `glob/grep -> targeted read -> folder_map(필요 시만)` 순서로 재정렬했습니다. 이로써 `folder_map` 성공 직후 멈춘 것처럼 보이던 일부 흐름과, 계획만 세우고 첫 도구 선택을 망설이던 Code 루프를 함께 보정했습니다.
|
||||||
|
- `ChatWindow.SystemPromptBuilder`의 Cowork/Code 시스템 프롬프트는 `모든 응답에 도구 호출 강제`, `첫 항목은 반드시 도구`, `애매하면 folder_map부터` 같은 문구를 제거하고, 실제 `claude-code`처럼 좁은 범위 탐색과 마지막 턴의 텍스트 응답을 허용하는 방향으로 완화했습니다.
|
||||||
|
- `code_개발`, `code_리뷰`, `code_리팩터링`, `cowork_문서작성`, `cowork_보고서` 프리셋도 같은 기준으로 갱신해 프롬프트 층과 런타임 정책 층이 서로 충돌하지 않도록 정렬했습니다.
|
||||||
|
- `ChatWindow.FileMentionSuggestions`를 추가해 Cowork/Code 입력창에서 파일명 후보 칩을 즉시 제안합니다. 사용자가 파일명 일부, 경로 조각, 확장자를 입력하면 작업 폴더 인덱스를 바탕으로 관련 파일을 추천하고 클릭 또는 `Tab`으로 삽입할 수 있습니다.
|
||||||
|
- `AgentLoopExplorationRecovery`를 추가해 `folder_map`의 빈 결과를 자동 복구합니다. `0 files, 0 dirs` 응답 뒤에 실제 파일 후보가 보이면 `folder_map_empty_recovery` 전이를 기록하고, LLM에는 `glob -> file_read/document_read`로 전환하라는 시스템 메시지를 추가합니다.
|
||||||
|
- 마지막 도구 결과 이후 다음 LLM 호출까지의 대기 시간도 `llm_wait_after_tool_result` 전이로 기록하도록 해, 멈춤 체감이 LLM 대기인지 루프 정체인지 워크플로우 로그만으로 더 빨리 판별할 수 있게 했습니다.
|
||||||
|
- `DocumentPlannerTool`, `SkillService` 예시 가이드도 같은 선택 탐색 순서로 갱신해 내부 문서 계획 지시와 스킬 샘플이 런타임 정책과 어긋나지 않도록 맞췄습니다.
|
||||||
|
- `AgentLoopPathStagnation`을 추가해 Code 탭의 동일 경로 재접근 루프를 차단합니다. 기존 가드는 “같은 도구+같은 파라미터” 반복만 강하게 막았지만, 실제 루프는 `file_read -> grep -> file_read`처럼 도구를 바꿔 같은 파일을 계속 두드리는 패턴을 허용했습니다. 이제 동일 경로 읽기 접근이 4회 이상 이어지면 읽기를 중단시키고 `grep/glob으로 호출부 탐색 -> git_tool(diff) -> build_run/test_loop` 순서로 전환하라는 복구 메시지를 주입합니다.
|
||||||
|
|
||||||
## claude-code식 transcript 표시 구조 정리
|
## claude-code식 transcript 표시 구조 정리
|
||||||
|
|
||||||
- 업데이트: 2026-04-09 13:05 (KST)
|
- 업데이트: 2026-04-09 13:05 (KST)
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ public partial class AgentLoopService
|
|||||||
TopicBased,
|
TopicBased,
|
||||||
RepoWide,
|
RepoWide,
|
||||||
OpenEnded,
|
OpenEnded,
|
||||||
|
/// <summary>문서 생성 요청 — 탐색 단계를 건너뛰고 바로 document_plan/생성 도구 사용.</summary>
|
||||||
|
DirectCreation,
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class ExplorationTrackingState
|
private sealed class ExplorationTrackingState
|
||||||
@@ -24,6 +26,196 @@ public partial class AgentLoopService
|
|||||||
public bool CorrectiveHintInjected { get; set; }
|
public bool CorrectiveHintInjected { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static IReadOnlyCollection<IAgentTool> FilterExplorationToolsForCurrentIteration(
|
||||||
|
IReadOnlyCollection<IAgentTool> tools,
|
||||||
|
ExplorationTrackingState state,
|
||||||
|
string userQuery,
|
||||||
|
string? activeTab,
|
||||||
|
int totalToolCalls)
|
||||||
|
{
|
||||||
|
if (tools.Count == 0)
|
||||||
|
return tools;
|
||||||
|
|
||||||
|
// 문서 생성 모드: 생성 도구를 최우선, 탐색 도구를 뒤로 배치
|
||||||
|
if (state.Scope == ExplorationScope.DirectCreation)
|
||||||
|
{
|
||||||
|
var creationFirst = new List<IAgentTool>(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<IAgentTool>(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)
|
private static ExplorationScope ClassifyExplorationScope(string userQuery, string? activeTab)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(userQuery))
|
if (string.IsNullOrWhiteSpace(userQuery))
|
||||||
@@ -32,6 +224,11 @@ public partial class AgentLoopService
|
|||||||
var q = userQuery.Trim();
|
var q = userQuery.Trim();
|
||||||
var lower = q.ToLowerInvariant();
|
var lower = q.ToLowerInvariant();
|
||||||
|
|
||||||
|
// Cowork 탭에서 문서 생성 의도가 있으면 탐색을 건너뛰고 바로 생성 도구 사용
|
||||||
|
if (!string.Equals(activeTab, "Code", StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& HasDocumentCreationIntent(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("전체 점검"))
|
||||||
@@ -52,14 +249,47 @@ public partial class AgentLoopService
|
|||||||
: ExplorationScope.OpenEnded;
|
: ExplorationScope.OpenEnded;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 사용자가 문서를 새로 생성/작성하려는 의도인지 판별합니다.
|
||||||
|
/// "보고서 작성해줘", "문서 만들어줘" 등 생성 동사가 포함된 경우 true.
|
||||||
|
/// </summary>
|
||||||
|
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<ChatMessage> messages, ExplorationScope scope)
|
private static void InjectExplorationScopeGuidance(List<ChatMessage> messages, ExplorationScope scope)
|
||||||
{
|
{
|
||||||
var guidance = scope switch
|
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 =>
|
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 =>
|
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 =>
|
ExplorationScope.RepoWide =>
|
||||||
"Exploration scope = repo-wide. Broad structure inspection is allowed when needed.",
|
"Exploration scope = repo-wide. Broad structure inspection is allowed when needed.",
|
||||||
_ =>
|
_ =>
|
||||||
@@ -93,6 +323,15 @@ public partial class AgentLoopService
|
|||||||
string toolName,
|
string toolName,
|
||||||
string argsJson)
|
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)
|
if (state.Scope is ExplorationScope.RepoWide or ExplorationScope.OpenEnded)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ public partial class AgentLoopService
|
|||||||
string Content,
|
string Content,
|
||||||
string? PreviousContent = null);
|
string? PreviousContent = null);
|
||||||
|
|
||||||
private readonly LlmService _llm;
|
private readonly ILlmService _llm;
|
||||||
private readonly ToolRegistry _tools;
|
private readonly ToolRegistry _tools;
|
||||||
private readonly SettingsService _settings;
|
private readonly SettingsService _settings;
|
||||||
private readonly IToolExecutionCoordinator _toolExecutionCoordinator;
|
private readonly IToolExecutionCoordinator _toolExecutionCoordinator;
|
||||||
@@ -82,7 +82,7 @@ public partial class AgentLoopService
|
|||||||
/// <summary>에이전트 이벤트 발생 시 호출되는 콜백 (UI 표시용).</summary>
|
/// <summary>에이전트 이벤트 발생 시 호출되는 콜백 (UI 표시용).</summary>
|
||||||
public event Action<AgentEvent>? EventOccurred;
|
public event Action<AgentEvent>? EventOccurred;
|
||||||
|
|
||||||
public AgentLoopService(LlmService llm, ToolRegistry tools, SettingsService settings)
|
public AgentLoopService(ILlmService llm, ToolRegistry tools, SettingsService settings)
|
||||||
{
|
{
|
||||||
_llm = llm;
|
_llm = llm;
|
||||||
_tools = tools;
|
_tools = tools;
|
||||||
@@ -180,6 +180,9 @@ public partial class AgentLoopService
|
|||||||
Scope = ClassifyExplorationScope(userQuery, ActiveTab),
|
Scope = ClassifyExplorationScope(userQuery, ActiveTab),
|
||||||
SelectiveHit = true,
|
SelectiveHit = true,
|
||||||
};
|
};
|
||||||
|
var pathAccessState = new PathAccessTrackingState();
|
||||||
|
DateTime? lastToolResultAtUtc = null;
|
||||||
|
string? lastToolResultToolName = null;
|
||||||
|
|
||||||
// 워크플로우 상세 로그: 에이전트 루프 시작
|
// 워크플로우 상세 로그: 에이전트 루프 시작
|
||||||
WorkflowLogService.LogAgentLifecycle(_conversationId, _currentRunId, "start",
|
WorkflowLogService.LogAgentLifecycle(_conversationId, _currentRunId, "start",
|
||||||
@@ -242,11 +245,17 @@ public partial class AgentLoopService
|
|||||||
var context = BuildContext();
|
var context = BuildContext();
|
||||||
InjectTaskTypeGuidance(messages, taskPolicy);
|
InjectTaskTypeGuidance(messages, taskPolicy);
|
||||||
InjectExplorationScopeGuidance(messages, explorationState.Scope);
|
InjectExplorationScopeGuidance(messages, explorationState.Scope);
|
||||||
|
var preferredInitialToolSequence = BuildPreferredInitialToolSequence(
|
||||||
|
explorationState,
|
||||||
|
taskPolicy,
|
||||||
|
ActiveTab,
|
||||||
|
userQuery);
|
||||||
EmitEvent(
|
EmitEvent(
|
||||||
AgentEventType.Thinking,
|
AgentEventType.Thinking,
|
||||||
"",
|
"",
|
||||||
explorationState.Scope switch
|
explorationState.Scope switch
|
||||||
{
|
{
|
||||||
|
ExplorationScope.DirectCreation => "문서 생성 모드 · 바로 문서를 만드는 중",
|
||||||
ExplorationScope.Localized => "국소 범위 탐색 · 관련 파일만 찾는 중",
|
ExplorationScope.Localized => "국소 범위 탐색 · 관련 파일만 찾는 중",
|
||||||
ExplorationScope.TopicBased => "주제 범위 탐색 · 관련 후보만 추리는 중",
|
ExplorationScope.TopicBased => "주제 범위 탐색 · 관련 후보만 추리는 중",
|
||||||
ExplorationScope.RepoWide => "저장소 범위 탐색 · 구조를 확인하는 중",
|
ExplorationScope.RepoWide => "저장소 범위 탐색 · 구조를 확인하는 중",
|
||||||
@@ -518,6 +527,19 @@ public partial class AgentLoopService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (lastToolResultAtUtc is { } toolResultAt)
|
||||||
|
{
|
||||||
|
var waitMs = Math.Max(0, (long)(DateTime.UtcNow - toolResultAt).TotalMilliseconds);
|
||||||
|
WorkflowLogService.LogTransition(
|
||||||
|
_conversationId,
|
||||||
|
_currentRunId,
|
||||||
|
iteration,
|
||||||
|
"llm_wait_after_tool_result",
|
||||||
|
$"{lastToolResultToolName ?? "unknown"}:{waitMs}ms");
|
||||||
|
lastToolResultAtUtc = null;
|
||||||
|
lastToolResultToolName = null;
|
||||||
|
}
|
||||||
|
|
||||||
EmitEvent(AgentEventType.Thinking, "", $"LLM에 요청 중... (반복 {iteration}/{maxIterations})");
|
EmitEvent(AgentEventType.Thinking, "", $"LLM에 요청 중... (반복 {iteration}/{maxIterations})");
|
||||||
|
|
||||||
// Gemini 무료 티어 모드: LLM 호출 간 딜레이 (RPM 한도 초과 방지)
|
// Gemini 무료 티어 모드: LLM 호출 간 딜레이 (RPM 한도 초과 방지)
|
||||||
@@ -554,10 +576,15 @@ public partial class AgentLoopService
|
|||||||
}
|
}
|
||||||
|
|
||||||
// P1: 반복당 1회 캐시 — GetRuntimeActiveTools는 동일 파라미터로 반복 내 여러 번 호출됨
|
// P1: 반복당 1회 캐시 — GetRuntimeActiveTools는 동일 파라미터로 반복 내 여러 번 호출됨
|
||||||
var cachedActiveTools = GetRuntimeActiveTools(llm.DisabledTools, runtimeOverrides);
|
var cachedActiveTools = FilterExplorationToolsForCurrentIteration(
|
||||||
|
GetRuntimeActiveTools(llm.DisabledTools, runtimeOverrides),
|
||||||
|
explorationState,
|
||||||
|
userQuery,
|
||||||
|
ActiveTab,
|
||||||
|
totalToolCalls);
|
||||||
|
|
||||||
// LLM에 도구 정의와 함께 요청
|
// LLM에 도구 정의와 함께 요청
|
||||||
List<LlmService.ContentBlock> blocks;
|
List<ContentBlock> blocks;
|
||||||
var llmCallSw = Stopwatch.StartNew();
|
var llmCallSw = Stopwatch.StartNew();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -612,7 +639,7 @@ public partial class AgentLoopService
|
|||||||
{
|
{
|
||||||
switch (evt.Kind)
|
switch (evt.Kind)
|
||||||
{
|
{
|
||||||
case LlmService.ToolStreamEventKind.TextDelta:
|
case ToolStreamEventKind.TextDelta:
|
||||||
if (!string.IsNullOrWhiteSpace(evt.Text))
|
if (!string.IsNullOrWhiteSpace(evt.Text))
|
||||||
{
|
{
|
||||||
streamedTextPreview.Append(evt.Text);
|
streamedTextPreview.Append(evt.Text);
|
||||||
@@ -626,7 +653,7 @@ public partial class AgentLoopService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case LlmService.ToolStreamEventKind.ToolCallReady:
|
case ToolStreamEventKind.ToolCallReady:
|
||||||
if (evt.ToolCall != null)
|
if (evt.ToolCall != null)
|
||||||
{
|
{
|
||||||
EmitEvent(
|
EmitEvent(
|
||||||
@@ -635,9 +662,14 @@ public partial class AgentLoopService
|
|||||||
$"스트리밍 도구 감지: {FormatToolCallSummary(evt.ToolCall)}");
|
$"스트리밍 도구 감지: {FormatToolCallSummary(evt.ToolCall)}");
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case LlmService.ToolStreamEventKind.Completed:
|
case ToolStreamEventKind.Completed:
|
||||||
await Task.CompletedTask;
|
await Task.CompletedTask;
|
||||||
break;
|
break;
|
||||||
|
case ToolStreamEventKind.RetryReset:
|
||||||
|
streamedTextPreview.Clear();
|
||||||
|
lastStreamUiUpdateAt = DateTime.UtcNow;
|
||||||
|
EmitEvent(AgentEventType.Thinking, "", "스트리밍 중간 응답을 정리하고 재시도합니다.");
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
runState.ContextRecoveryAttempts = 0;
|
runState.ContextRecoveryAttempts = 0;
|
||||||
@@ -745,7 +777,7 @@ public partial class AgentLoopService
|
|||||||
|
|
||||||
// 응답에서 텍스트와 도구 호출 분리
|
// 응답에서 텍스트와 도구 호출 분리
|
||||||
var textParts = new List<string>();
|
var textParts = new List<string>();
|
||||||
var toolCalls = new List<LlmService.ContentBlock>();
|
var toolCalls = new List<ContentBlock>();
|
||||||
|
|
||||||
foreach (var block in blocks)
|
foreach (var block in blocks)
|
||||||
{
|
{
|
||||||
@@ -851,13 +883,15 @@ public partial class AgentLoopService
|
|||||||
"[System:ToolCallRequired] " +
|
"[System:ToolCallRequired] " +
|
||||||
"⚠ 경고: 이전 응답에서 도구를 호출하지 않았습니다. " +
|
"⚠ 경고: 이전 응답에서 도구를 호출하지 않았습니다. " +
|
||||||
"텍스트 설명만 반환하는 것은 허용되지 않습니다. " +
|
"텍스트 설명만 반환하는 것은 허용되지 않습니다. " +
|
||||||
|
$"먼저 권장 순서는 {preferredInitialToolSequence} 입니다. " +
|
||||||
"지금 즉시 아래 형식으로 도구를 호출하세요:\n" +
|
"지금 즉시 아래 형식으로 도구를 호출하세요:\n" +
|
||||||
"<tool_call>\n{\"name\": \"TOOL_NAME\", \"arguments\": {\"param\": \"value\"}}\n</tool_call>\n" +
|
"<tool_call>\n{\"name\": \"TOOL_NAME\", \"arguments\": {\"param\": \"value\"}}\n</tool_call>\n" +
|
||||||
$"사용 가능한 도구: {activeToolPreview}",
|
$"사용 가능한 도구: {activeToolPreview}",
|
||||||
_ =>
|
_ =>
|
||||||
"[System:ToolCallRequired] " +
|
"[System:ToolCallRequired] " +
|
||||||
"🚨 최종 경고: 도구를 계속 호출하지 않고 있습니다. 이것이 마지막 기회입니다. " +
|
"🚨 최종 경고: 도구를 계속 호출하지 않고 있습니다. 이것이 마지막 기회입니다. " +
|
||||||
"텍스트는 한 글자도 쓰지 마세요. 반드시 아래 형식으로 도구를 호출하세요:\n" +
|
$"텍스트는 한 글자도 쓰지 말고 {preferredInitialToolSequence} 순서에 맞는 첫 도구를 고르세요. " +
|
||||||
|
"반드시 아래 형식으로 도구를 호출하세요:\n" +
|
||||||
"<tool_call>\n{\"name\": \"TOOL_NAME\", \"arguments\": {\"param\": \"value\"}}\n</tool_call>\n" +
|
"<tool_call>\n{\"name\": \"TOOL_NAME\", \"arguments\": {\"param\": \"value\"}}\n</tool_call>\n" +
|
||||||
$"반드시 사용해야 할 도구 목록: {activeToolPreview}"
|
$"반드시 사용해야 할 도구 목록: {activeToolPreview}"
|
||||||
};
|
};
|
||||||
@@ -891,12 +925,13 @@ public partial class AgentLoopService
|
|||||||
{
|
{
|
||||||
1 =>
|
1 =>
|
||||||
"[System:ToolCallRequired] 계획을 세웠지만 도구를 호출하지 않았습니다. " +
|
"[System:ToolCallRequired] 계획을 세웠지만 도구를 호출하지 않았습니다. " +
|
||||||
|
$"권장 첫 순서는 {preferredInitialToolSequence} 입니다. " +
|
||||||
"지금 당장 실행하세요. 아래 형식으로 도구를 호출하세요:\n" +
|
"지금 당장 실행하세요. 아래 형식으로 도구를 호출하세요:\n" +
|
||||||
"<tool_call>\n{\"name\": \"TOOL_NAME\", \"arguments\": {\"param\": \"value\"}}\n</tool_call>\n" +
|
"<tool_call>\n{\"name\": \"TOOL_NAME\", \"arguments\": {\"param\": \"value\"}}\n</tool_call>\n" +
|
||||||
$"사용 가능한 도구: {planToolList}",
|
$"사용 가능한 도구: {planToolList}",
|
||||||
_ =>
|
_ =>
|
||||||
"[System:ToolCallRequired] 🚨 도구 호출 없이 계획만 반복하고 있습니다. " +
|
"[System:ToolCallRequired] 🚨 도구 호출 없이 계획만 반복하고 있습니다. " +
|
||||||
"텍스트를 한 글자도 쓰지 마세요. 오직 아래 형식의 도구 호출만 출력하세요:\n" +
|
$"텍스트를 한 글자도 쓰지 말고 {preferredInitialToolSequence} 순서에 맞는 도구 호출만 출력하세요:\n" +
|
||||||
"<tool_call>\n{\"name\": \"TOOL_NAME\", \"arguments\": {\"param\": \"value\"}}\n</tool_call>\n" +
|
"<tool_call>\n{\"name\": \"TOOL_NAME\", \"arguments\": {\"param\": \"value\"}}\n</tool_call>\n" +
|
||||||
$"사용 가능한 도구: {planToolList}"
|
$"사용 가능한 도구: {planToolList}"
|
||||||
};
|
};
|
||||||
@@ -1248,7 +1283,7 @@ public partial class AgentLoopService
|
|||||||
: resolvedToolName;
|
: resolvedToolName;
|
||||||
var effectiveCall = string.Equals(call.ToolName, effectiveToolName, StringComparison.OrdinalIgnoreCase)
|
var effectiveCall = string.Equals(call.ToolName, effectiveToolName, StringComparison.OrdinalIgnoreCase)
|
||||||
? call
|
? call
|
||||||
: new LlmService.ContentBlock
|
: new ContentBlock
|
||||||
{
|
{
|
||||||
Type = call.Type,
|
Type = call.Type,
|
||||||
Text = call.Text,
|
Text = call.Text,
|
||||||
@@ -1306,6 +1341,18 @@ public partial class AgentLoopService
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (TryHandleRepeatedPathAccessTransition(
|
||||||
|
effectiveCall,
|
||||||
|
context,
|
||||||
|
pathAccessState,
|
||||||
|
messages,
|
||||||
|
lastModifiedCodeFilePath,
|
||||||
|
requireHighImpactCodeVerification,
|
||||||
|
taskPolicy))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Task Decomposition: 단계 진행률 추적
|
// Task Decomposition: 단계 진행률 추적
|
||||||
if (planSteps.Count > 0)
|
if (planSteps.Count > 0)
|
||||||
{
|
{
|
||||||
@@ -1345,7 +1392,7 @@ public partial class AgentLoopService
|
|||||||
Role = "system",
|
Role = "system",
|
||||||
Content =
|
Content =
|
||||||
"Exploration correction: The current request is narrow. Stop broad workspace scanning. " +
|
"Exploration correction: The current request is narrow. Stop broad workspace scanning. " +
|
||||||
"Use grep/glob to identify only files directly related to the user's topic, then read a very small targeted set. " +
|
"Follow this order: glob/grep -> targeted file_read/document_read -> folder_map only if the user explicitly needs folder structure. " +
|
||||||
"Do not repeat folder_map unless repository structure is genuinely required."
|
"Do not repeat folder_map unless repository structure is genuinely required."
|
||||||
});
|
});
|
||||||
EmitEvent(
|
EmitEvent(
|
||||||
@@ -1405,7 +1452,7 @@ public partial class AgentLoopService
|
|||||||
if (llm.EnableHookInputMutation &&
|
if (llm.EnableHookInputMutation &&
|
||||||
TryGetHookUpdatedInput(preResults, out var updatedInput))
|
TryGetHookUpdatedInput(preResults, out var updatedInput))
|
||||||
{
|
{
|
||||||
effectiveCall = new LlmService.ContentBlock
|
effectiveCall = new ContentBlock
|
||||||
{
|
{
|
||||||
Type = effectiveCall.Type,
|
Type = effectiveCall.Type,
|
||||||
Text = effectiveCall.Text,
|
Text = effectiveCall.Text,
|
||||||
@@ -1533,6 +1580,8 @@ public partial class AgentLoopService
|
|||||||
ref statsFailCount,
|
ref statsFailCount,
|
||||||
ref statsInputTokens,
|
ref statsInputTokens,
|
||||||
ref statsOutputTokens);
|
ref statsOutputTokens);
|
||||||
|
lastToolResultAtUtc = DateTime.UtcNow;
|
||||||
|
lastToolResultToolName = effectiveCall.ToolName;
|
||||||
|
|
||||||
if (!result.Success)
|
if (!result.Success)
|
||||||
{
|
{
|
||||||
@@ -1608,7 +1657,14 @@ public partial class AgentLoopService
|
|||||||
messages.Add(LlmService.CreateToolResultMessage(
|
messages.Add(LlmService.CreateToolResultMessage(
|
||||||
effectiveCall.ToolId,
|
effectiveCall.ToolId,
|
||||||
effectiveCall.ToolName,
|
effectiveCall.ToolName,
|
||||||
BuildLoopToolResultMessage(effectiveCall, result, runState)));
|
BuildToolResultFeedbackMessage(
|
||||||
|
effectiveCall,
|
||||||
|
result,
|
||||||
|
runState,
|
||||||
|
context,
|
||||||
|
messages,
|
||||||
|
explorationState,
|
||||||
|
iteration)));
|
||||||
|
|
||||||
if (TryHandleReadOnlyStagnationTransition(
|
if (TryHandleReadOnlyStagnationTransition(
|
||||||
consecutiveReadOnlySuccessTools,
|
consecutiveReadOnlySuccessTools,
|
||||||
@@ -1909,7 +1965,8 @@ public partial class AgentLoopService
|
|||||||
SkillRuntimeOverrides? runtimeOverrides)
|
SkillRuntimeOverrides? runtimeOverrides)
|
||||||
{
|
{
|
||||||
var mergedDisabled = MergeDisabledTools(disabledToolNames);
|
var mergedDisabled = MergeDisabledTools(disabledToolNames);
|
||||||
var active = _tools.GetActiveTools(mergedDisabled);
|
// 탭별 도구 필터링: 현재 탭에 맞는 도구만 LLM에 전송 (토큰 절약)
|
||||||
|
var active = _tools.GetActiveToolsForTab(ActiveTab, mergedDisabled);
|
||||||
if (runtimeOverrides == null || runtimeOverrides.AllowedToolNames.Count == 0)
|
if (runtimeOverrides == null || runtimeOverrides.AllowedToolNames.Count == 0)
|
||||||
return active;
|
return active;
|
||||||
|
|
||||||
@@ -2776,7 +2833,7 @@ public partial class AgentLoopService
|
|||||||
? testReviewLine +
|
? testReviewLine +
|
||||||
"5. grep 또는 glob으로 공용 API, 호출부, 의존성 등록, 테스트 영향을 모두 다시 확인합니다.\n" +
|
"5. grep 또는 glob으로 공용 API, 호출부, 의존성 등록, 테스트 영향을 모두 다시 확인합니다.\n" +
|
||||||
"6. build_run으로 build와 test를 모두 실행해 통과 여부를 확인합니다.\n" +
|
"6. build_run으로 build와 test를 모두 실행해 통과 여부를 확인합니다.\n" +
|
||||||
"7. 필요하면 spawn_agent로 호출부 분석이나 관련 테스트 탐색을 병렬 조사하게 하고, wait_agents로 결과를 통합합니다.\n" +
|
"7. 병렬 조사가 실제로 도움이 될 때만 spawn_agent로 호출부 분석이나 관련 테스트 탐색을 위임하고, wait_agents로 결과를 통합합니다.\n" +
|
||||||
"8. 문제가 발견되면 즉시 수정하고 다시 검증합니다.\n" +
|
"8. 문제가 발견되면 즉시 수정하고 다시 검증합니다.\n" +
|
||||||
"중요: 이 변경은 영향 범위가 넓을 가능성이 큽니다. build/test와 참조 검토가 모두 끝나기 전에는 마무리하지 마세요."
|
"중요: 이 변경은 영향 범위가 넓을 가능성이 큽니다. build/test와 참조 검토가 모두 끝나기 전에는 마무리하지 마세요."
|
||||||
: testReviewLine +
|
: testReviewLine +
|
||||||
@@ -2927,7 +2984,7 @@ public partial class AgentLoopService
|
|||||||
var failureHint = BuildFailureTypeRecoveryHint(ClassifyFailureRecoveryKind(toolName, toolOutput), toolName);
|
var failureHint = BuildFailureTypeRecoveryHint(ClassifyFailureRecoveryKind(toolName, toolOutput), toolName);
|
||||||
var highImpactLine = highImpactChange
|
var highImpactLine = highImpactChange
|
||||||
? "4. 공용 API, 인터페이스, DI 등록, 모델 계약, 호출부 전파 영향까지 반드시 확인합니다.\n" +
|
? "4. 공용 API, 인터페이스, DI 등록, 모델 계약, 호출부 전파 영향까지 반드시 확인합니다.\n" +
|
||||||
"5. 필요하면 spawn_agent로 호출부/관련 테스트 조사를 병렬 실행해 근거를 보강합니다.\n" +
|
"5. 병렬 조사가 실제로 도움이 될 때만 spawn_agent로 호출부/관련 테스트 조사를 실행해 근거를 보강합니다.\n" +
|
||||||
"6. 관련 테스트가 없다면 테스트 부재 사실과 확인 근거를 남깁니다.\n" +
|
"6. 관련 테스트가 없다면 테스트 부재 사실과 확인 근거를 남깁니다.\n" +
|
||||||
"7. 근본 원인을 수정한 뒤 build/test를 다시 실행합니다.\n" +
|
"7. 근본 원인을 수정한 뒤 build/test를 다시 실행합니다.\n" +
|
||||||
"중요: 이 변경은 영향 범위가 넓을 수 있으므로 build와 test 둘 다 확인하기 전에는 종료하지 마세요."
|
"중요: 이 변경은 영향 범위가 넓을 수 있으므로 build와 test 둘 다 확인하기 전에는 종료하지 마세요."
|
||||||
@@ -3803,7 +3860,9 @@ public partial class AgentLoopService
|
|||||||
$"- 실패 도구: {unknownToolName}\n" +
|
$"- 실패 도구: {unknownToolName}\n" +
|
||||||
aliasHint +
|
aliasHint +
|
||||||
$"- 사용 가능한 도구 예시: {string.Join(", ", suggestions)}\n" +
|
$"- 사용 가능한 도구 예시: {string.Join(", ", suggestions)}\n" +
|
||||||
"- 도구가 애매하면 먼저 tool_search를 호출해 정확한 이름을 찾으세요.\n" +
|
(string.IsNullOrEmpty(aliasHint)
|
||||||
|
? "- 위 후보만으로도 충분히 선택 가능하면 바로 그 도구를 호출하세요. 그래도 도구가 애매할 때만 tool_search를 사용하세요.\n"
|
||||||
|
: "- 자동 매핑 후보가 맞으면 바로 그 도구를 호출하세요. 정말 모호할 때만 tool_search를 사용하세요.\n") +
|
||||||
"위 목록에서 실제 존재하는 도구 하나를 골라 다시 호출하세요. 같은 미등록 도구를 반복 호출하지 마세요.";
|
"위 목록에서 실제 존재하는 도구 하나를 골라 다시 호출하세요. 같은 미등록 도구를 반복 호출하지 마세요.";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3823,7 +3882,7 @@ public partial class AgentLoopService
|
|||||||
$"- 요청 도구: {requestedToolName}\n" +
|
$"- 요청 도구: {requestedToolName}\n" +
|
||||||
policyLine +
|
policyLine +
|
||||||
$"- 지금 사용 가능한 도구 예시: {activePreview}\n" +
|
$"- 지금 사용 가능한 도구 예시: {activePreview}\n" +
|
||||||
"- 도구 선택이 모호하면 tool_search로 허용 가능한 대체 도구를 먼저 찾으세요.\n" +
|
"- 위 허용 도구 예시로 바로 대체할 수 있으면 즉시 호출하세요. 그래도 도구 선택이 모호할 때만 tool_search를 사용하세요.\n" +
|
||||||
"허용 목록에서 대체 도구를 선택해 다시 호출하세요. 동일한 비허용 도구 재호출은 금지합니다.";
|
"허용 목록에서 대체 도구를 선택해 다시 호출하세요. 동일한 비허용 도구 재호출은 금지합니다.";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3837,7 +3896,7 @@ public partial class AgentLoopService
|
|||||||
$"- 반복 횟수: {repeatedUnknownToolCount}\n" +
|
$"- 반복 횟수: {repeatedUnknownToolCount}\n" +
|
||||||
$"- 실패 도구: {unknownToolName}\n" +
|
$"- 실패 도구: {unknownToolName}\n" +
|
||||||
$"- 현재 사용 가능한 도구 예시: {preview}\n" +
|
$"- 현재 사용 가능한 도구 예시: {preview}\n" +
|
||||||
"- 다음 실행에서는 tool_search로 도구명을 확인한 뒤 위 목록의 실제 도구 이름으로 호출하세요.";
|
"- 다음 실행에서는 먼저 위 목록의 실제 도구 이름을 직접 선택하고, 그래도 애매할 때만 tool_search를 사용하세요.";
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string BuildDisallowedToolLoopAbortResponse(
|
private static string BuildDisallowedToolLoopAbortResponse(
|
||||||
@@ -3850,7 +3909,7 @@ public partial class AgentLoopService
|
|||||||
$"- 반복 횟수: {repeatedCount}\n" +
|
$"- 반복 횟수: {repeatedCount}\n" +
|
||||||
$"- 비허용 도구: {toolName}\n" +
|
$"- 비허용 도구: {toolName}\n" +
|
||||||
$"- 현재 사용 가능한 도구 예시: {preview}\n" +
|
$"- 현재 사용 가능한 도구 예시: {preview}\n" +
|
||||||
"- 다음 실행에서는 tool_search로 허용 도구를 확인하고 계획을 수정하세요.";
|
"- 다음 실행에서는 허용 도구 예시에서 직접 고를 수 있으면 바로 바꾸고, 그래도 애매할 때만 tool_search를 사용하세요.";
|
||||||
}
|
}
|
||||||
|
|
||||||
private static readonly Dictionary<string, string> ToolAliasMap = new(StringComparer.OrdinalIgnoreCase)
|
private static readonly Dictionary<string, string> ToolAliasMap = new(StringComparer.OrdinalIgnoreCase)
|
||||||
@@ -4136,7 +4195,7 @@ public partial class AgentLoopService
|
|||||||
|
|
||||||
// 검증 응답 처리
|
// 검증 응답 처리
|
||||||
var verifyText = new List<string>();
|
var verifyText = new List<string>();
|
||||||
var verifyToolCalls = new List<LlmService.ContentBlock>();
|
var verifyToolCalls = new List<ContentBlock>();
|
||||||
|
|
||||||
foreach (var block in verifyBlocks)
|
foreach (var block in verifyBlocks)
|
||||||
{
|
{
|
||||||
@@ -4724,7 +4783,7 @@ public partial class AgentLoopService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>영향 범위 기반 의사결정 체크. 확인이 필요하면 메시지를 반환, 불필요하면 null.</summary>
|
/// <summary>영향 범위 기반 의사결정 체크. 확인이 필요하면 메시지를 반환, 불필요하면 null.</summary>
|
||||||
private string? CheckDecisionRequired(LlmService.ContentBlock call, AgentContext context)
|
private string? CheckDecisionRequired(ContentBlock call, AgentContext context)
|
||||||
{
|
{
|
||||||
var app = System.Windows.Application.Current as App;
|
var app = System.Windows.Application.Current as App;
|
||||||
var level = app?.SettingsService?.Settings.Llm.AgentDecisionLevel ?? "normal";
|
var level = app?.SettingsService?.Settings.Llm.AgentDecisionLevel ?? "normal";
|
||||||
@@ -4811,7 +4870,7 @@ public partial class AgentLoopService
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string FormatToolCallSummary(LlmService.ContentBlock call)
|
private static string FormatToolCallSummary(ContentBlock call)
|
||||||
{
|
{
|
||||||
if (call.ToolInput == null) return call.ToolName;
|
if (call.ToolInput == null) return call.ToolName;
|
||||||
try
|
try
|
||||||
@@ -4829,7 +4888,7 @@ public partial class AgentLoopService
|
|||||||
catch { return call.ToolName; }
|
catch { return call.ToolName; }
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string BuildToolCallSignature(LlmService.ContentBlock call)
|
private static string BuildToolCallSignature(ContentBlock call)
|
||||||
{
|
{
|
||||||
var name = (call.ToolName ?? "").Trim().ToLowerInvariant();
|
var name = (call.ToolName ?? "").Trim().ToLowerInvariant();
|
||||||
if (call.ToolInput == null)
|
if (call.ToolInput == null)
|
||||||
@@ -4929,7 +4988,7 @@ public partial class AgentLoopService
|
|||||||
FailureRecoveryKind.Permission =>
|
FailureRecoveryKind.Permission =>
|
||||||
"권한설정 확인 -> 대상 경로 재선정 -> file_read/grep으로 대체 검증 -> 승인 후 재실행",
|
"권한설정 확인 -> 대상 경로 재선정 -> file_read/grep으로 대체 검증 -> 승인 후 재실행",
|
||||||
FailureRecoveryKind.Path =>
|
FailureRecoveryKind.Path =>
|
||||||
"folder_map/glob -> 절대경로 확인 -> file_read로 존재 검증 -> 원도구 재실행",
|
"glob -> file_read -> folder_map(필요 시만) -> 절대경로 확인 -> 원도구 재실행",
|
||||||
FailureRecoveryKind.Command =>
|
FailureRecoveryKind.Command =>
|
||||||
"도구 스키마 재확인 -> 최소 파라미터 호출 -> 옵션 확장 -> 필요 시 대체 도구",
|
"도구 스키마 재확인 -> 최소 파라미터 호출 -> 옵션 확장 -> 필요 시 대체 도구",
|
||||||
FailureRecoveryKind.Dependency =>
|
FailureRecoveryKind.Dependency =>
|
||||||
@@ -4940,7 +4999,7 @@ public partial class AgentLoopService
|
|||||||
{
|
{
|
||||||
"build_run" or "test_loop" => "file_read -> grep/glob -> git_tool(diff) -> file_edit -> build_run/test_loop",
|
"build_run" or "test_loop" => "file_read -> grep/glob -> git_tool(diff) -> file_edit -> build_run/test_loop",
|
||||||
"file_edit" or "file_write" => "file_read -> grep/glob -> git_tool(diff) -> file_edit -> build_run/test_loop",
|
"file_edit" or "file_write" => "file_read -> grep/glob -> git_tool(diff) -> file_edit -> build_run/test_loop",
|
||||||
"grep" or "glob" => "file_read -> folder_map -> grep/glob",
|
"grep" or "glob" => "glob/grep 재구성 -> file_read -> folder_map(필요 시만)",
|
||||||
_ => "file_read -> grep/glob -> git_tool(diff) -> targeted tool retry"
|
_ => "file_read -> grep/glob -> git_tool(diff) -> targeted tool retry"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ namespace AxCopilot.Services.Agent;
|
|||||||
public partial class AgentLoopService
|
public partial class AgentLoopService
|
||||||
{
|
{
|
||||||
private void ApplyDocumentPlanSuccessTransitions(
|
private void ApplyDocumentPlanSuccessTransitions(
|
||||||
LlmService.ContentBlock call,
|
ContentBlock call,
|
||||||
ToolResult result,
|
ToolResult result,
|
||||||
List<ChatMessage> messages,
|
List<ChatMessage> messages,
|
||||||
ref bool documentPlanCalled,
|
ref bool documentPlanCalled,
|
||||||
@@ -96,9 +96,9 @@ public partial class AgentLoopService
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async Task<(bool Completed, bool ConsumedExtraIteration)> TryHandleTerminalDocumentCompletionTransitionAsync(
|
private async Task<(bool Completed, bool ConsumedExtraIteration)> TryHandleTerminalDocumentCompletionTransitionAsync(
|
||||||
LlmService.ContentBlock call,
|
ContentBlock call,
|
||||||
ToolResult result,
|
ToolResult result,
|
||||||
List<LlmService.ContentBlock> toolCalls,
|
List<ContentBlock> toolCalls,
|
||||||
List<ChatMessage> messages,
|
List<ChatMessage> messages,
|
||||||
Models.LlmSettings llm,
|
Models.LlmSettings llm,
|
||||||
ModelExecutionProfileCatalog.ExecutionPolicy executionPolicy,
|
ModelExecutionProfileCatalog.ExecutionPolicy executionPolicy,
|
||||||
@@ -134,7 +134,7 @@ public partial class AgentLoopService
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async Task<bool> TryApplyPostToolVerificationTransitionAsync(
|
private async Task<bool> TryApplyPostToolVerificationTransitionAsync(
|
||||||
LlmService.ContentBlock call,
|
ContentBlock call,
|
||||||
ToolResult result,
|
ToolResult result,
|
||||||
List<ChatMessage> messages,
|
List<ChatMessage> messages,
|
||||||
Models.LlmSettings llm,
|
Models.LlmSettings llm,
|
||||||
@@ -156,6 +156,16 @@ public partial class AgentLoopService
|
|||||||
if (!shouldVerify)
|
if (!shouldVerify)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
if (string.Equals(ActiveTab, "Code", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var highImpactCodeChange = IsHighImpactCodeModification(ActiveTab ?? "", call.ToolName, result);
|
||||||
|
var hasDiffEvidence = HasDiffEvidenceAfterLastModification(messages);
|
||||||
|
var hasRecentBuildOrTestEvidence = HasBuildOrTestEvidenceAfterLastModification(messages);
|
||||||
|
|
||||||
|
if (!highImpactCodeChange || (hasDiffEvidence && hasRecentBuildOrTestEvidence))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
await RunPostToolVerificationAsync(messages, call.ToolName, result, context, ct);
|
await RunPostToolVerificationAsync(messages, call.ToolName, result, context, ct);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -300,7 +300,7 @@ public partial class AgentLoopService
|
|||||||
};
|
};
|
||||||
var verificationTools = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
var verificationTools = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||||
{
|
{
|
||||||
"file_read", "document_read", "document_review"
|
"file_read", "document_read"
|
||||||
};
|
};
|
||||||
|
|
||||||
var sawVerification = false;
|
var sawVerification = false;
|
||||||
@@ -421,7 +421,7 @@ public partial class AgentLoopService
|
|||||||
}
|
}
|
||||||
|
|
||||||
private bool TryHandleRepeatedFailureGuardTransition(
|
private bool TryHandleRepeatedFailureGuardTransition(
|
||||||
LlmService.ContentBlock call,
|
ContentBlock call,
|
||||||
string toolCallSignature,
|
string toolCallSignature,
|
||||||
List<ChatMessage> messages,
|
List<ChatMessage> messages,
|
||||||
string? lastFailedToolSignature,
|
string? lastFailedToolSignature,
|
||||||
@@ -463,7 +463,7 @@ public partial class AgentLoopService
|
|||||||
}
|
}
|
||||||
|
|
||||||
private bool TryHandleNoProgressReadOnlyLoopTransition(
|
private bool TryHandleNoProgressReadOnlyLoopTransition(
|
||||||
LlmService.ContentBlock call,
|
ContentBlock call,
|
||||||
string toolCallSignature,
|
string toolCallSignature,
|
||||||
int repeatedSameSignatureCount,
|
int repeatedSameSignatureCount,
|
||||||
List<ChatMessage> messages,
|
List<ChatMessage> messages,
|
||||||
@@ -911,22 +911,22 @@ public partial class AgentLoopService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<LlmService.ToolPrefetchResult?> TryPrefetchReadOnlyToolAsync(
|
private async Task<ToolPrefetchResult?> TryPrefetchReadOnlyToolAsync(
|
||||||
LlmService.ContentBlock block,
|
ContentBlock block,
|
||||||
IReadOnlyCollection<IAgentTool> tools,
|
IReadOnlyCollection<IAgentTool> tools,
|
||||||
AgentContext context,
|
AgentContext context,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
=> await _toolExecutionCoordinator.TryPrefetchReadOnlyToolAsync(block, tools, context, ct);
|
=> await _toolExecutionCoordinator.TryPrefetchReadOnlyToolAsync(block, tools, context, ct);
|
||||||
|
|
||||||
private async Task<List<LlmService.ContentBlock>> SendWithToolsWithRecoveryAsync(
|
private async Task<List<ContentBlock>> SendWithToolsWithRecoveryAsync(
|
||||||
List<ChatMessage> messages,
|
List<ChatMessage> messages,
|
||||||
IReadOnlyCollection<IAgentTool> tools,
|
IReadOnlyCollection<IAgentTool> tools,
|
||||||
CancellationToken ct,
|
CancellationToken ct,
|
||||||
string phaseLabel,
|
string phaseLabel,
|
||||||
RunState? runState = null,
|
RunState? runState = null,
|
||||||
bool forceToolCall = false,
|
bool forceToolCall = false,
|
||||||
Func<LlmService.ContentBlock, Task<LlmService.ToolPrefetchResult?>>? prefetchToolCallAsync = null,
|
Func<ContentBlock, Task<ToolPrefetchResult?>>? prefetchToolCallAsync = null,
|
||||||
Func<LlmService.ToolStreamEvent, Task>? onStreamEventAsync = null)
|
Func<ToolStreamEvent, Task>? onStreamEventAsync = null)
|
||||||
=> await _toolExecutionCoordinator.SendWithToolsWithRecoveryAsync(
|
=> await _toolExecutionCoordinator.SendWithToolsWithRecoveryAsync(
|
||||||
messages,
|
messages,
|
||||||
tools,
|
tools,
|
||||||
@@ -938,7 +938,7 @@ public partial class AgentLoopService
|
|||||||
onStreamEventAsync);
|
onStreamEventAsync);
|
||||||
|
|
||||||
private void ApplyToolPostExecutionBookkeeping(
|
private void ApplyToolPostExecutionBookkeeping(
|
||||||
LlmService.ContentBlock call,
|
ContentBlock call,
|
||||||
ToolResult result,
|
ToolResult result,
|
||||||
TokenUsage? tokenUsage,
|
TokenUsage? tokenUsage,
|
||||||
Models.LlmSettings llm,
|
Models.LlmSettings llm,
|
||||||
@@ -986,7 +986,7 @@ public partial class AgentLoopService
|
|||||||
}
|
}
|
||||||
|
|
||||||
private bool TryHandleToolFailureTransition(
|
private bool TryHandleToolFailureTransition(
|
||||||
LlmService.ContentBlock call,
|
ContentBlock call,
|
||||||
ToolResult result,
|
ToolResult result,
|
||||||
AgentContext context,
|
AgentContext context,
|
||||||
TaskTypePolicy taskPolicy,
|
TaskTypePolicy taskPolicy,
|
||||||
@@ -1120,7 +1120,7 @@ public partial class AgentLoopService
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async Task<(bool ShouldContinue, string? TerminalResponse)> TryHandleUserDecisionTransitionsAsync(
|
private async Task<(bool ShouldContinue, string? TerminalResponse)> TryHandleUserDecisionTransitionsAsync(
|
||||||
LlmService.ContentBlock call,
|
ContentBlock call,
|
||||||
AgentContext context,
|
AgentContext context,
|
||||||
List<ChatMessage> messages)
|
List<ChatMessage> messages)
|
||||||
{
|
{
|
||||||
@@ -1193,4 +1193,3 @@ public partial class AgentLoopService
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ namespace AxCopilot.Services.Agent;
|
|||||||
public partial class AgentLoopService
|
public partial class AgentLoopService
|
||||||
{
|
{
|
||||||
private void ApplyCodeQualityFollowUpTransition(
|
private void ApplyCodeQualityFollowUpTransition(
|
||||||
LlmService.ContentBlock call,
|
ContentBlock call,
|
||||||
ToolResult result,
|
ToolResult result,
|
||||||
List<ChatMessage> messages,
|
List<ChatMessage> messages,
|
||||||
TaskTypePolicy taskPolicy,
|
TaskTypePolicy taskPolicy,
|
||||||
@@ -13,9 +13,9 @@ public partial class AgentLoopService
|
|||||||
ref string? lastModifiedCodeFilePath)
|
ref string? lastModifiedCodeFilePath)
|
||||||
{
|
{
|
||||||
var highImpactCodeChange = IsHighImpactCodeModification(ActiveTab ?? "", call.ToolName, result);
|
var highImpactCodeChange = IsHighImpactCodeModification(ActiveTab ?? "", call.ToolName, result);
|
||||||
if (ShouldInjectCodeQualityFollowUp(ActiveTab ?? "", call.ToolName, result))
|
|
||||||
{
|
|
||||||
requireHighImpactCodeVerification = highImpactCodeChange;
|
requireHighImpactCodeVerification = highImpactCodeChange;
|
||||||
|
if (highImpactCodeChange && ShouldInjectCodeQualityFollowUp(ActiveTab ?? "", call.ToolName, result))
|
||||||
|
{
|
||||||
lastModifiedCodeFilePath = result.FilePath;
|
lastModifiedCodeFilePath = result.FilePath;
|
||||||
messages.Add(new ChatMessage
|
messages.Add(new ChatMessage
|
||||||
{
|
{
|
||||||
@@ -52,8 +52,12 @@ public partial class AgentLoopService
|
|||||||
var hasCodeVerificationEvidence = HasCodeVerificationEvidenceAfterLastModification(
|
var hasCodeVerificationEvidence = HasCodeVerificationEvidenceAfterLastModification(
|
||||||
messages,
|
messages,
|
||||||
requireHighImpactCodeVerification);
|
requireHighImpactCodeVerification);
|
||||||
|
var hasDiffEvidence = HasDiffEvidenceAfterLastModification(messages);
|
||||||
|
var hasRecentBuildOrTestEvidence = HasBuildOrTestEvidenceAfterLastModification(messages);
|
||||||
|
var hasSuccessfulBuildAndTestEvidence = HasSuccessfulBuildAndTestAfterLastModification(messages);
|
||||||
if (executionPolicy.CodeVerificationGateMaxRetries > 0
|
if (executionPolicy.CodeVerificationGateMaxRetries > 0
|
||||||
&& !hasCodeVerificationEvidence
|
&& !hasCodeVerificationEvidence
|
||||||
|
&& !(hasDiffEvidence && hasRecentBuildOrTestEvidence && !requireHighImpactCodeVerification)
|
||||||
&& runState.CodeVerificationGateRetry < executionPolicy.CodeVerificationGateMaxRetries)
|
&& runState.CodeVerificationGateRetry < executionPolicy.CodeVerificationGateMaxRetries)
|
||||||
{
|
{
|
||||||
runState.CodeVerificationGateRetry++;
|
runState.CodeVerificationGateRetry++;
|
||||||
@@ -89,7 +93,10 @@ public partial class AgentLoopService
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var hasBlockingCodeEvidenceGap = !hasCodeVerificationEvidence
|
||||||
|
|| (requireHighImpactCodeVerification && !hasSuccessfulBuildAndTestEvidence);
|
||||||
if (executionPolicy.FinalReportGateMaxRetries > 0
|
if (executionPolicy.FinalReportGateMaxRetries > 0
|
||||||
|
&& !hasBlockingCodeEvidenceGap
|
||||||
&& !HasSufficientFinalReportEvidence(textResponse, taskPolicy, requireHighImpactCodeVerification, messages)
|
&& !HasSufficientFinalReportEvidence(textResponse, taskPolicy, requireHighImpactCodeVerification, messages)
|
||||||
&& runState.FinalReportGateRetry < executionPolicy.FinalReportGateMaxRetries)
|
&& runState.FinalReportGateRetry < executionPolicy.FinalReportGateMaxRetries)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ public sealed class AxAgentExecutionEngine
|
|||||||
int completionTokens = 0,
|
int completionTokens = 0,
|
||||||
long? responseElapsedMs = null,
|
long? responseElapsedMs = null,
|
||||||
string? metaRunId = null,
|
string? metaRunId = null,
|
||||||
ChatStorageService? storage = null)
|
IChatStorageService? storage = null)
|
||||||
{
|
{
|
||||||
var assistant = new ChatMessage
|
var assistant = new ChatMessage
|
||||||
{
|
{
|
||||||
@@ -177,7 +177,7 @@ public sealed class AxAgentExecutionEngine
|
|||||||
int completionTokens = 0,
|
int completionTokens = 0,
|
||||||
long? responseElapsedMs = null,
|
long? responseElapsedMs = null,
|
||||||
string? metaRunId = null,
|
string? metaRunId = null,
|
||||||
ChatStorageService? storage = null)
|
IChatStorageService? storage = null)
|
||||||
{
|
{
|
||||||
var normalized = NormalizeAssistantContentForUi(conversation, tab, content);
|
var normalized = NormalizeAssistantContentForUi(conversation, tab, content);
|
||||||
if (tab is "Cowork" or "Code")
|
if (tab is "Cowork" or "Code")
|
||||||
@@ -209,6 +209,10 @@ public sealed class AxAgentExecutionEngine
|
|||||||
if (!string.IsNullOrWhiteSpace(content))
|
if (!string.IsNullOrWhiteSpace(content))
|
||||||
return content;
|
return content;
|
||||||
|
|
||||||
|
if (runTab is "Cowork" or "Code")
|
||||||
|
return TryBuildStrictExecutionCompletionMessage(conversation, runTab)
|
||||||
|
?? "(실행은 종료되었지만 최종 요약 응답이 비어 있습니다. 실행 로그를 확인하세요.)";
|
||||||
|
|
||||||
return BuildFallbackCompletionMessage(conversation, runTab);
|
return BuildFallbackCompletionMessage(conversation, runTab);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,7 +234,7 @@ public sealed class AxAgentExecutionEngine
|
|||||||
|
|
||||||
public SessionMutationResult AppendExecutionEvent(
|
public SessionMutationResult AppendExecutionEvent(
|
||||||
ChatSessionStateService session,
|
ChatSessionStateService session,
|
||||||
ChatStorageService storage,
|
IChatStorageService storage,
|
||||||
ChatConversation? activeConversation,
|
ChatConversation? activeConversation,
|
||||||
string activeTab,
|
string activeTab,
|
||||||
string targetTab,
|
string targetTab,
|
||||||
@@ -247,7 +251,7 @@ public sealed class AxAgentExecutionEngine
|
|||||||
|
|
||||||
public SessionMutationResult AppendAgentRun(
|
public SessionMutationResult AppendAgentRun(
|
||||||
ChatSessionStateService session,
|
ChatSessionStateService session,
|
||||||
ChatStorageService storage,
|
IChatStorageService storage,
|
||||||
ChatConversation? activeConversation,
|
ChatConversation? activeConversation,
|
||||||
string activeTab,
|
string activeTab,
|
||||||
string targetTab,
|
string targetTab,
|
||||||
@@ -272,6 +276,10 @@ public sealed class AxAgentExecutionEngine
|
|||||||
if (!string.IsNullOrWhiteSpace(content))
|
if (!string.IsNullOrWhiteSpace(content))
|
||||||
return content;
|
return content;
|
||||||
|
|
||||||
|
if (runTab is "Cowork" or "Code")
|
||||||
|
return TryBuildStrictExecutionCompletionMessage(conversation, runTab)
|
||||||
|
?? "(실행은 종료되었지만 최종 요약 응답이 비어 있습니다. 실행 로그를 확인하세요.)";
|
||||||
|
|
||||||
return BuildFallbackCompletionMessage(conversation, runTab);
|
return BuildFallbackCompletionMessage(conversation, runTab);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -280,6 +288,9 @@ public sealed class AxAgentExecutionEngine
|
|||||||
/// UserPromptSubmit/Paused/Resumed 같은 내부 운영 이벤트는 제외합니다.
|
/// UserPromptSubmit/Paused/Resumed 같은 내부 운영 이벤트는 제외합니다.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static string BuildFallbackCompletionMessage(ChatConversation conversation, string runTab)
|
private static string BuildFallbackCompletionMessage(ChatConversation conversation, string runTab)
|
||||||
|
=> TryBuildEvidenceBackedCompletionMessage(conversation, runTab) ?? "(빈 응답)";
|
||||||
|
|
||||||
|
private static string? TryBuildEvidenceBackedCompletionMessage(ChatConversation conversation, string runTab)
|
||||||
{
|
{
|
||||||
static bool IsSignificantEventType(string t)
|
static bool IsSignificantEventType(string t)
|
||||||
=> !string.Equals(t, "UserPromptSubmit", StringComparison.OrdinalIgnoreCase)
|
=> !string.Equals(t, "UserPromptSubmit", StringComparison.OrdinalIgnoreCase)
|
||||||
@@ -290,7 +301,8 @@ public sealed class AxAgentExecutionEngine
|
|||||||
// 완료 메시지로 표시하면 안 되는 Thinking 이벤트 요약 패턴 (내부 진행 상태 문자열)
|
// 완료 메시지로 표시하면 안 되는 Thinking 이벤트 요약 패턴 (내부 진행 상태 문자열)
|
||||||
static bool IsInternalStatusSummary(string? summary)
|
static bool IsInternalStatusSummary(string? summary)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(summary)) return false;
|
if (string.IsNullOrWhiteSpace(summary))
|
||||||
|
return false;
|
||||||
return summary.StartsWith("LLM에 요청 중", StringComparison.OrdinalIgnoreCase)
|
return summary.StartsWith("LLM에 요청 중", StringComparison.OrdinalIgnoreCase)
|
||||||
|| summary.StartsWith("도구 미호출 루프", StringComparison.OrdinalIgnoreCase)
|
|| summary.StartsWith("도구 미호출 루프", StringComparison.OrdinalIgnoreCase)
|
||||||
|| summary.StartsWith("강제 실행 유도", StringComparison.OrdinalIgnoreCase)
|
|| summary.StartsWith("강제 실행 유도", StringComparison.OrdinalIgnoreCase)
|
||||||
@@ -345,6 +357,68 @@ public sealed class AxAgentExecutionEngine
|
|||||||
return completionLine ?? "(빈 응답)";
|
return completionLine ?? "(빈 응답)";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string? TryBuildStrictExecutionCompletionMessage(ChatConversation conversation, string runTab)
|
||||||
|
{
|
||||||
|
static bool IsSignificantEventType(string t)
|
||||||
|
=> !string.Equals(t, "UserPromptSubmit", StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& !string.Equals(t, "Paused", StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& !string.Equals(t, "Resumed", StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& !string.Equals(t, "SessionStart", StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
static bool IsInternalStatusSummary(string? summary)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(summary))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return summary.StartsWith("LLM", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| summary.StartsWith("도구 미호출 루프", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| summary.StartsWith("강제 실행 유도", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| summary.StartsWith("읽기 전용 도구", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| summary.StartsWith("병렬 실행", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| summary.StartsWith("Self-Reflection", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| summary.StartsWith("일시적 LLM 오류", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| summary.StartsWith("컨텍스트 한도 초과", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| summary.Contains("[System:", StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
var completionLine = runTab switch
|
||||||
|
{
|
||||||
|
"Cowork" => "코워크 작업이 완료되었습니다.",
|
||||||
|
"Code" => "코드 작업이 완료되었습니다.",
|
||||||
|
_ => null,
|
||||||
|
};
|
||||||
|
|
||||||
|
var artifactEvent = conversation.ExecutionEvents?
|
||||||
|
.Where(evt => !string.IsNullOrWhiteSpace(evt.FilePath) && IsSignificantEventType(evt.Type))
|
||||||
|
.OrderByDescending(evt => evt.Timestamp)
|
||||||
|
.FirstOrDefault();
|
||||||
|
if (artifactEvent != null)
|
||||||
|
{
|
||||||
|
var fileLine = string.IsNullOrWhiteSpace(artifactEvent.Summary)
|
||||||
|
? $"생성된 파일: {artifactEvent.FilePath}"
|
||||||
|
: $"{artifactEvent.Summary}\n경로: {artifactEvent.FilePath}";
|
||||||
|
return completionLine != null
|
||||||
|
? $"{completionLine}\n\n{fileLine}"
|
||||||
|
: fileLine;
|
||||||
|
}
|
||||||
|
|
||||||
|
var latestSummary = conversation.ExecutionEvents?
|
||||||
|
.Where(evt => !string.IsNullOrWhiteSpace(evt.Summary)
|
||||||
|
&& IsSignificantEventType(evt.Type)
|
||||||
|
&& !IsInternalStatusSummary(evt.Summary))
|
||||||
|
.OrderByDescending(evt => evt.Timestamp)
|
||||||
|
.Select(evt => evt.Summary.Trim())
|
||||||
|
.FirstOrDefault();
|
||||||
|
if (!string.IsNullOrWhiteSpace(latestSummary))
|
||||||
|
{
|
||||||
|
return completionLine != null
|
||||||
|
? $"{completionLine}\n\n{latestSummary}"
|
||||||
|
: latestSummary;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
private static ChatMessage CloneMessage(ChatMessage source)
|
private static ChatMessage CloneMessage(ChatMessage source)
|
||||||
{
|
{
|
||||||
return new ChatMessage
|
return new ChatMessage
|
||||||
@@ -370,7 +444,7 @@ public sealed class AxAgentExecutionEngine
|
|||||||
|
|
||||||
private static SessionMutationResult ApplyConversationMutation(
|
private static SessionMutationResult ApplyConversationMutation(
|
||||||
ChatSessionStateService session,
|
ChatSessionStateService session,
|
||||||
ChatStorageService storage,
|
IChatStorageService storage,
|
||||||
ChatConversation? activeConversation,
|
ChatConversation? activeConversation,
|
||||||
string activeTab,
|
string activeTab,
|
||||||
string targetTab,
|
string targetTab,
|
||||||
|
|||||||
@@ -5,18 +5,24 @@ namespace AxCopilot.Services.Agent;
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// LSP 기반 코드 인텔리전스 도구.
|
/// LSP 기반 코드 인텔리전스 도구.
|
||||||
/// 정의 이동(goto_definition), 참조 검색(find_references), 심볼 목록(symbols) 3가지 액션을 제공합니다.
|
/// 정의 이동, 참조 검색, hover, 구현 위치, 심볼 검색, 호출 계층 등 구조적 코드 탐색을 제공합니다.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class LspTool : IAgentTool, IDisposable
|
public class LspTool : IAgentTool, IDisposable
|
||||||
{
|
{
|
||||||
public string Name => "lsp_code_intel";
|
public string Name => "lsp_code_intel";
|
||||||
|
|
||||||
public string Description =>
|
public string Description =>
|
||||||
"코드 인텔리전스 도구. 정의 이동, 참조 검색, 심볼 목록을 제공합니다.\n" +
|
"코드 인텔리전스 도구. 정의, 참조, hover, 구현 위치, 문서/워크스페이스 심볼, 호출 계층을 제공합니다.\n" +
|
||||||
"- action=\"goto_definition\": 심볼의 정의 위치를 찾습니다 (파일, 라인, 컬럼)\n" +
|
"- action=\"goto_definition\": 심볼의 정의 위치를 찾습니다\n" +
|
||||||
"- action=\"find_references\": 심볼이 사용된 모든 위치를 찾습니다\n" +
|
"- action=\"find_references\": 심볼이 사용된 모든 위치를 찾습니다\n" +
|
||||||
"- action=\"symbols\": 파일 내 모든 심볼(클래스, 메서드, 필드 등)을 나열합니다\n" +
|
"- action=\"hover\": 심볼의 타입/문서 정보를 가져옵니다\n" +
|
||||||
"file_path, line, character 파라미터가 필요합니다 (line과 character는 0-based).";
|
"- 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()
|
public ToolParameterSchema Parameters => new()
|
||||||
{
|
{
|
||||||
@@ -25,23 +31,39 @@ public class LspTool : IAgentTool, IDisposable
|
|||||||
["action"] = new ToolProperty
|
["action"] = new ToolProperty
|
||||||
{
|
{
|
||||||
Type = "string",
|
Type = "string",
|
||||||
Description = "수행할 작업: goto_definition | find_references | symbols",
|
Description = "수행할 작업",
|
||||||
Enum = new() { "goto_definition", "find_references", "symbols" }
|
Enum = new()
|
||||||
|
{
|
||||||
|
"goto_definition",
|
||||||
|
"find_references",
|
||||||
|
"hover",
|
||||||
|
"goto_implementation",
|
||||||
|
"symbols",
|
||||||
|
"workspace_symbols",
|
||||||
|
"prepare_call_hierarchy",
|
||||||
|
"incoming_calls",
|
||||||
|
"outgoing_calls"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
["file_path"] = new ToolProperty
|
["file_path"] = new ToolProperty
|
||||||
{
|
{
|
||||||
Type = "string",
|
Type = "string",
|
||||||
Description = "대상 파일 경로 (절대 또는 작업 폴더 기준 상대 경로)"
|
Description = "대상 파일 경로 (절대 또는 작업 폴더 기준 상대 경로). workspace_symbols에서는 작업 폴더 기준 힌트로만 사용 가능."
|
||||||
},
|
},
|
||||||
["line"] = new ToolProperty
|
["line"] = new ToolProperty
|
||||||
{
|
{
|
||||||
Type = "integer",
|
Type = "integer",
|
||||||
Description = "대상 라인 번호 (0-based). symbols 액션에서는 불필요."
|
Description = "대상 라인 번호. 기본 1-based 입력을 기대하며 내부에서 0-based로 변환합니다. symbols/workspace_symbols에서는 불필요."
|
||||||
},
|
},
|
||||||
["character"] = new ToolProperty
|
["character"] = new ToolProperty
|
||||||
{
|
{
|
||||||
Type = "integer",
|
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" }
|
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 action = args.SafeTryGetProperty("action", out var a) ? a.SafeGetString() ?? "" : "";
|
||||||
var filePath = args.SafeTryGetProperty("file_path", out var f) ? f.SafeGetString() ?? "" : "";
|
var filePath = args.SafeTryGetProperty("file_path", out var f) ? f.SafeGetString() ?? "" : "";
|
||||||
var line = args.SafeTryGetProperty("line", out var l) ? l.GetInt32() : 0;
|
var line = args.SafeTryGetProperty("line", out var l) ? NormalizePosition(l.SafeGetInt32()) : 0;
|
||||||
var character = args.SafeTryGetProperty("character", out var ch) ? ch.GetInt32() : 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))
|
if (string.IsNullOrEmpty(filePath))
|
||||||
return ToolResult.Fail("file_path가 필요합니다.");
|
return ToolResult.Fail("file_path가 필요합니다.");
|
||||||
@@ -88,8 +111,14 @@ public class LspTool : IAgentTool, IDisposable
|
|||||||
{
|
{
|
||||||
"goto_definition" => await GotoDefinitionAsync(client, filePath, line, character, ct),
|
"goto_definition" => await GotoDefinitionAsync(client, filePath, line, character, ct),
|
||||||
"find_references" => await FindReferencesAsync(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),
|
"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)
|
catch (Exception ex)
|
||||||
@@ -118,13 +147,44 @@ public class LspTool : IAgentTool, IDisposable
|
|||||||
return ToolResult.Ok("참조를 찾을 수 없습니다.");
|
return ToolResult.Ok("참조를 찾을 수 없습니다.");
|
||||||
|
|
||||||
var sb = new System.Text.StringBuilder();
|
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))
|
foreach (var loc in locations.Take(30))
|
||||||
sb.AppendLine($" {loc}");
|
sb.AppendLine($" {loc}");
|
||||||
if (locations.Count > 30)
|
if (locations.Count > 30)
|
||||||
sb.AppendLine($" ... 외 {locations.Count - 30}개");
|
sb.AppendLine($" ... 외 {locations.Count - 30}개");
|
||||||
|
|
||||||
return ToolResult.Ok(sb.ToString());
|
return ToolResult.Ok(sb.ToString(), locations[0].FilePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<ToolResult> 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<ToolResult> 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<ToolResult> GetSymbolsAsync(LspClientService client, string filePath, CancellationToken ct)
|
private async Task<ToolResult> GetSymbolsAsync(LspClientService client, string filePath, CancellationToken ct)
|
||||||
@@ -134,13 +194,92 @@ public class LspTool : IAgentTool, IDisposable
|
|||||||
return ToolResult.Ok("심볼을 찾을 수 없습니다.");
|
return ToolResult.Ok("심볼을 찾을 수 없습니다.");
|
||||||
|
|
||||||
var sb = new System.Text.StringBuilder();
|
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)
|
foreach (var sym in symbols)
|
||||||
sb.AppendLine($" {sym}");
|
sb.AppendLine($" {sym}");
|
||||||
|
|
||||||
return ToolResult.Ok(sb.ToString());
|
return ToolResult.Ok(sb.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<ToolResult> 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<ToolResult> 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<ToolResult> 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<ToolResult> 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<LspClientService?> GetOrCreateClientAsync(string language, string workFolder, CancellationToken ct)
|
private async Task<LspClientService?> GetOrCreateClientAsync(string language, string workFolder, CancellationToken ct)
|
||||||
{
|
{
|
||||||
if (_clients.TryGetValue(language, out var existing) && existing.IsConnected)
|
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()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
foreach (var client in _clients.Values)
|
foreach (var client in _clients.Values)
|
||||||
|
|||||||
@@ -11,13 +11,13 @@ internal sealed class StreamingToolExecutionCoordinator : IToolExecutionCoordina
|
|||||||
{
|
{
|
||||||
private static readonly HashSet<string> PrefetchableReadOnlyTools = new(StringComparer.OrdinalIgnoreCase)
|
private static readonly HashSet<string> PrefetchableReadOnlyTools = new(StringComparer.OrdinalIgnoreCase)
|
||||||
{
|
{
|
||||||
"file_read", "glob", "grep", "grep_tool", "folder_map", "document_read",
|
"file_read", "document_read",
|
||||||
"search_codebase", "code_search", "env_tool", "datetime_tool",
|
"env_tool", "datetime_tool",
|
||||||
"dev_env_detect", "memory", "json_tool", "regex_tool", "base64_tool",
|
"dev_env_detect", "memory", "json_tool", "regex_tool", "base64_tool",
|
||||||
"hash_tool", "image_analyze", "multi_read"
|
"hash_tool", "image_analyze"
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly LlmService _llm;
|
private readonly ILlmService _llm;
|
||||||
private readonly Func<string, IReadOnlyCollection<string>, string> _resolveRequestedToolName;
|
private readonly Func<string, IReadOnlyCollection<string>, string> _resolveRequestedToolName;
|
||||||
private readonly Func<string, JsonElement, AgentContext, List<ChatMessage>?, CancellationToken, Task<ToolResult>> _executeToolAsync;
|
private readonly Func<string, JsonElement, AgentContext, List<ChatMessage>?, CancellationToken, Task<ToolResult>> _executeToolAsync;
|
||||||
private readonly Action<AgentEventType, string, string> _emitEvent;
|
private readonly Action<AgentEventType, string, string> _emitEvent;
|
||||||
@@ -27,7 +27,7 @@ internal sealed class StreamingToolExecutionCoordinator : IToolExecutionCoordina
|
|||||||
private readonly Func<int, Exception, int> _computeTransientBackoffDelayMs;
|
private readonly Func<int, Exception, int> _computeTransientBackoffDelayMs;
|
||||||
|
|
||||||
public StreamingToolExecutionCoordinator(
|
public StreamingToolExecutionCoordinator(
|
||||||
LlmService llm,
|
ILlmService llm,
|
||||||
Func<string, IReadOnlyCollection<string>, string> resolveRequestedToolName,
|
Func<string, IReadOnlyCollection<string>, string> resolveRequestedToolName,
|
||||||
Func<string, JsonElement, AgentContext, List<ChatMessage>?, CancellationToken, Task<ToolResult>> executeToolAsync,
|
Func<string, JsonElement, AgentContext, List<ChatMessage>?, CancellationToken, Task<ToolResult>> executeToolAsync,
|
||||||
Action<AgentEventType, string, string> emitEvent,
|
Action<AgentEventType, string, string> emitEvent,
|
||||||
@@ -46,8 +46,8 @@ internal sealed class StreamingToolExecutionCoordinator : IToolExecutionCoordina
|
|||||||
_computeTransientBackoffDelayMs = computeTransientBackoffDelayMs;
|
_computeTransientBackoffDelayMs = computeTransientBackoffDelayMs;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<LlmService.ToolPrefetchResult?> TryPrefetchReadOnlyToolAsync(
|
public async Task<ToolPrefetchResult?> TryPrefetchReadOnlyToolAsync(
|
||||||
LlmService.ContentBlock block,
|
ContentBlock block,
|
||||||
IReadOnlyCollection<IAgentTool> tools,
|
IReadOnlyCollection<IAgentTool> tools,
|
||||||
AgentContext context,
|
AgentContext context,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
@@ -70,57 +70,60 @@ internal sealed class StreamingToolExecutionCoordinator : IToolExecutionCoordina
|
|||||||
var input = block.ToolInput ?? JsonDocument.Parse("{}").RootElement;
|
var input = block.ToolInput ?? JsonDocument.Parse("{}").RootElement;
|
||||||
var result = await _executeToolAsync(resolvedToolName, input, context, null, ct);
|
var result = await _executeToolAsync(resolvedToolName, input, context, null, ct);
|
||||||
sw.Stop();
|
sw.Stop();
|
||||||
return new LlmService.ToolPrefetchResult(result, sw.ElapsedMilliseconds, resolvedToolName);
|
return new ToolPrefetchResult(result, sw.ElapsedMilliseconds, resolvedToolName);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
sw.Stop();
|
sw.Stop();
|
||||||
return new LlmService.ToolPrefetchResult(
|
return new ToolPrefetchResult(
|
||||||
ToolResult.Fail($"조기 실행 오류: {ex.Message}"),
|
ToolResult.Fail($"조기 실행 오류: {ex.Message}"),
|
||||||
sw.ElapsedMilliseconds,
|
sw.ElapsedMilliseconds,
|
||||||
resolvedToolName);
|
resolvedToolName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<LlmService.ContentBlock>> SendWithToolsWithRecoveryAsync(
|
public async Task<List<ContentBlock>> SendWithToolsWithRecoveryAsync(
|
||||||
List<ChatMessage> messages,
|
List<ChatMessage> messages,
|
||||||
IReadOnlyCollection<IAgentTool> tools,
|
IReadOnlyCollection<IAgentTool> tools,
|
||||||
CancellationToken ct,
|
CancellationToken ct,
|
||||||
string phaseLabel,
|
string phaseLabel,
|
||||||
AgentLoopService.RunState? runState = null,
|
AgentLoopService.RunState? runState = null,
|
||||||
bool forceToolCall = false,
|
bool forceToolCall = false,
|
||||||
Func<LlmService.ContentBlock, Task<LlmService.ToolPrefetchResult?>>? prefetchToolCallAsync = null,
|
Func<ContentBlock, Task<ToolPrefetchResult?>>? prefetchToolCallAsync = null,
|
||||||
Func<LlmService.ToolStreamEvent, Task>? onStreamEventAsync = null)
|
Func<ToolStreamEvent, Task>? onStreamEventAsync = null)
|
||||||
{
|
{
|
||||||
var transientRetries = runState?.TransientLlmErrorRetries ?? 0;
|
var transientRetries = runState?.TransientLlmErrorRetries ?? 0;
|
||||||
var contextRecoveryRetries = runState?.ContextRecoveryAttempts ?? 0;
|
var contextRecoveryRetries = runState?.ContextRecoveryAttempts ?? 0;
|
||||||
|
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
|
var streamedAnyPartialState = false;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (onStreamEventAsync == null)
|
if (onStreamEventAsync == null)
|
||||||
return await _llm.SendWithToolsAsync(messages, tools, ct, forceToolCall, prefetchToolCallAsync);
|
return await _llm.SendWithToolsAsync(messages, tools, ct, forceToolCall, prefetchToolCallAsync);
|
||||||
|
|
||||||
var blocks = new List<LlmService.ContentBlock>();
|
var blocks = new List<ContentBlock>();
|
||||||
var textBuilder = new StringBuilder();
|
var textBuilder = new StringBuilder();
|
||||||
await foreach (var evt in _llm.StreamWithToolsAsync(messages, tools, forceToolCall, prefetchToolCallAsync, ct).WithCancellation(ct))
|
await foreach (var evt in _llm.StreamWithToolsAsync(messages, tools, forceToolCall, prefetchToolCallAsync, ct).WithCancellation(ct))
|
||||||
{
|
{
|
||||||
await onStreamEventAsync(evt);
|
await onStreamEventAsync(evt);
|
||||||
if (evt.Kind == LlmService.ToolStreamEventKind.TextDelta && !string.IsNullOrWhiteSpace(evt.Text))
|
if (evt.Kind == ToolStreamEventKind.TextDelta && !string.IsNullOrWhiteSpace(evt.Text))
|
||||||
{
|
{
|
||||||
|
streamedAnyPartialState = true;
|
||||||
textBuilder.Append(evt.Text);
|
textBuilder.Append(evt.Text);
|
||||||
}
|
}
|
||||||
else if (evt.Kind == LlmService.ToolStreamEventKind.ToolCallReady && evt.ToolCall != null)
|
else if (evt.Kind == ToolStreamEventKind.ToolCallReady && evt.ToolCall != null)
|
||||||
{
|
{
|
||||||
|
streamedAnyPartialState = true;
|
||||||
blocks.Add(evt.ToolCall);
|
blocks.Add(evt.ToolCall);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = new List<LlmService.ContentBlock>();
|
var result = new List<ContentBlock>();
|
||||||
var text = textBuilder.ToString().Trim();
|
var text = textBuilder.ToString().Trim();
|
||||||
if (!string.IsNullOrWhiteSpace(text))
|
if (!string.IsNullOrWhiteSpace(text))
|
||||||
result.Add(new LlmService.ContentBlock { Type = "text", Text = text });
|
result.Add(new ContentBlock { Type = "text", Text = text });
|
||||||
result.AddRange(blocks);
|
result.AddRange(blocks);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -130,6 +133,8 @@ internal sealed class StreamingToolExecutionCoordinator : IToolExecutionCoordina
|
|||||||
&& contextRecoveryRetries < 2
|
&& contextRecoveryRetries < 2
|
||||||
&& _forceContextRecovery(messages))
|
&& _forceContextRecovery(messages))
|
||||||
{
|
{
|
||||||
|
if (onStreamEventAsync != null && streamedAnyPartialState)
|
||||||
|
await onStreamEventAsync(new ToolStreamEvent(ToolStreamEventKind.RetryReset, $"{phaseLabel}:retry"));
|
||||||
contextRecoveryRetries++;
|
contextRecoveryRetries++;
|
||||||
if (runState != null)
|
if (runState != null)
|
||||||
runState.ContextRecoveryAttempts = contextRecoveryRetries;
|
runState.ContextRecoveryAttempts = contextRecoveryRetries;
|
||||||
@@ -146,6 +151,8 @@ internal sealed class StreamingToolExecutionCoordinator : IToolExecutionCoordina
|
|||||||
|
|
||||||
if (_isTransientLlmError(ex) && transientRetries < 3)
|
if (_isTransientLlmError(ex) && transientRetries < 3)
|
||||||
{
|
{
|
||||||
|
if (onStreamEventAsync != null && streamedAnyPartialState)
|
||||||
|
await onStreamEventAsync(new ToolStreamEvent(ToolStreamEventKind.RetryReset, $"{phaseLabel}:retry"));
|
||||||
transientRetries++;
|
transientRetries++;
|
||||||
if (runState != null)
|
if (runState != null)
|
||||||
runState.TransientLlmErrorRetries = transientRetries;
|
runState.TransientLlmErrorRetries = transientRetries;
|
||||||
|
|||||||
@@ -62,7 +62,174 @@ public class ToolRegistry : IDisposable
|
|||||||
if (disabledNames == null) return All;
|
if (disabledNames == null) return All;
|
||||||
var disabled = new HashSet<string>(disabledNames, StringComparer.OrdinalIgnoreCase);
|
var disabled = new HashSet<string>(disabledNames, StringComparer.OrdinalIgnoreCase);
|
||||||
if (disabled.Count == 0) return All;
|
if (disabled.Count == 0) return All;
|
||||||
return _tools.Values.Where(t => !disabled.Contains(t.Name)).ToList().AsReadOnly();
|
return OrderToolsForExposure(_tools.Values.Where(t => !disabled.Contains(t.Name)))
|
||||||
|
.ToList()
|
||||||
|
.AsReadOnly();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>비활성 도구를 제외하고 현재 탭에 해당하는 도구만 반환합니다.</summary>
|
||||||
|
public IReadOnlyCollection<IAgentTool> GetActiveToolsForTab(string activeTab, IEnumerable<string>? disabledNames = null)
|
||||||
|
{
|
||||||
|
var disabled = disabledNames != null
|
||||||
|
? new HashSet<string>(disabledNames, StringComparer.OrdinalIgnoreCase)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return OrderToolsForExposure(_tools.Values.Where(t =>
|
||||||
|
{
|
||||||
|
if (disabled != null && disabled.Contains(t.Name)) return false;
|
||||||
|
return IsToolAvailableForTab(t, activeTab);
|
||||||
|
})).ToList().AsReadOnly();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<IAgentTool> OrderToolsForExposure(IEnumerable<IAgentTool> tools)
|
||||||
|
{
|
||||||
|
return tools
|
||||||
|
.OrderBy(GetToolExposureBucket)
|
||||||
|
.ThenBy(t => t.Name, StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int GetToolExposureBucket(IAgentTool tool)
|
||||||
|
{
|
||||||
|
return tool.Name switch
|
||||||
|
{
|
||||||
|
"file_read" or "file_write" or "file_edit" or "glob" or "grep" or "folder_map" or "document_read"
|
||||||
|
or "process" or "dev_env_detect" or "build_run" or "git_tool" or "lsp_code_intel"
|
||||||
|
or "document_plan" or "document_assemble" or "docx_create" or "html_create" or "markdown_create"
|
||||||
|
or "excel_create" or "csv_create" or "pptx_create" or "chart_create" => 0,
|
||||||
|
"document_review" or "format_convert" or "tool_search" or "code_search" => 1,
|
||||||
|
"mcp_list_resources" or "mcp_read_resource" or "spawn_agent" or "wait_agents" => 2,
|
||||||
|
_ when tool.Name.StartsWith("task_", StringComparison.OrdinalIgnoreCase) => 3,
|
||||||
|
_ => 1
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>도구가 해당 탭에서 사용 가능한지 확인합니다.</summary>
|
||||||
|
private static bool IsToolAvailableForTab(IAgentTool tool, string activeTab)
|
||||||
|
{
|
||||||
|
var category = ResolveTabCategory(tool);
|
||||||
|
if (string.IsNullOrEmpty(category)) return true; // null = 모든 탭
|
||||||
|
// 쉼표 구분 복수 탭: "Cowork,Code"
|
||||||
|
foreach (var part in category.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
|
||||||
|
{
|
||||||
|
if (string.Equals(part, activeTab, StringComparison.OrdinalIgnoreCase))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 도구별 탭 카테고리 오버라이드.
|
||||||
|
/// IAgentTool.TabCategory가 null인 도구는 이 맵을 참조합니다.
|
||||||
|
/// 키: 도구 이름, 값: 허용 탭 (쉼표 구분). 맵에 없으면 = 모든 탭.
|
||||||
|
/// </summary>
|
||||||
|
private static readonly Dictionary<string, string> ToolTabOverrides = new(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
// ════════════════════════════════════════════════════════════
|
||||||
|
// Chat = 순수 대화 (도구 없음). 아래 맵에 없는 공통 도구도
|
||||||
|
// Chat에선 제외하려면 여기에 "Cowork,Code"로 등록.
|
||||||
|
// ════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ── 파일/검색 기본 도구: Cowork + Code ──
|
||||||
|
["file_read"] = "Cowork,Code",
|
||||||
|
["file_write"] = "Cowork,Code",
|
||||||
|
["file_edit"] = "Cowork,Code",
|
||||||
|
["glob"] = "Cowork,Code",
|
||||||
|
["grep"] = "Cowork,Code",
|
||||||
|
["process"] = "Cowork,Code",
|
||||||
|
["folder_map"] = "Cowork,Code",
|
||||||
|
["document_read"] = "Cowork,Code",
|
||||||
|
["file_manage"] = "Cowork,Code",
|
||||||
|
["file_info"] = "Cowork,Code",
|
||||||
|
["multi_read"] = "Cowork,Code",
|
||||||
|
["zip"] = "Cowork,Code",
|
||||||
|
["open_external"] = "Cowork,Code",
|
||||||
|
|
||||||
|
// ── 데이터/유틸리티: Cowork + Code ──
|
||||||
|
["json"] = "Cowork,Code",
|
||||||
|
["regex"] = "Cowork,Code",
|
||||||
|
["base64"] = "Cowork,Code",
|
||||||
|
["hash"] = "Cowork,Code",
|
||||||
|
["datetime"] = "Cowork,Code",
|
||||||
|
["math"] = "Cowork,Code",
|
||||||
|
["encoding"] = "Cowork,Code",
|
||||||
|
["http"] = "Cowork,Code",
|
||||||
|
["clipboard"] = "Cowork,Code",
|
||||||
|
["env"] = "Cowork,Code",
|
||||||
|
["notify"] = "Cowork,Code",
|
||||||
|
["user_ask"] = "Cowork,Code",
|
||||||
|
["memory"] = "Cowork,Code",
|
||||||
|
["skill_manager"] = "Cowork,Code",
|
||||||
|
["tool_search"] = "Cowork,Code",
|
||||||
|
["mcp_list_resources"] = "Cowork,Code",
|
||||||
|
["mcp_read_resource"] = "Cowork,Code",
|
||||||
|
|
||||||
|
// ── 문서 생성/처리: Cowork 전용 ──
|
||||||
|
["xlsx_create"] = "Cowork",
|
||||||
|
["excel_create"] = "Cowork",
|
||||||
|
["docx_create"] = "Cowork",
|
||||||
|
["csv_create"] = "Cowork",
|
||||||
|
["md_create"] = "Cowork",
|
||||||
|
["markdown_create"] = "Cowork",
|
||||||
|
["html_create"] = "Cowork",
|
||||||
|
["chart_create"] = "Cowork",
|
||||||
|
["batch_create"] = "Cowork",
|
||||||
|
["pptx_create"] = "Cowork",
|
||||||
|
["document_plan"] = "Cowork",
|
||||||
|
["document_assemble"] = "Cowork",
|
||||||
|
["document_review"] = "Cowork",
|
||||||
|
["format_convert"] = "Cowork",
|
||||||
|
["data_pivot"] = "Cowork",
|
||||||
|
["template_render"] = "Cowork",
|
||||||
|
["text_summarize"] = "Cowork",
|
||||||
|
["sql"] = "Cowork",
|
||||||
|
["xml"] = "Cowork",
|
||||||
|
["image_analyze"] = "Cowork",
|
||||||
|
|
||||||
|
// ── 개발 도구: Code 전용 ──
|
||||||
|
["dev_env_detect"] = "Code",
|
||||||
|
["build_run"] = "Code",
|
||||||
|
["git"] = "Code",
|
||||||
|
["lsp"] = "Code",
|
||||||
|
["code_search"] = "Code",
|
||||||
|
["code_review"] = "Code",
|
||||||
|
["project_rule"] = "Code",
|
||||||
|
["snippet_run"] = "Code",
|
||||||
|
["diff"] = "Code",
|
||||||
|
["diff_preview"] = "Code",
|
||||||
|
["sub_agent"] = "Code",
|
||||||
|
["wait_agents"] = "Code",
|
||||||
|
["test_loop"] = "Code",
|
||||||
|
["file_watch"] = "Code",
|
||||||
|
|
||||||
|
// ── 태스크/워크트리/팀: Code 전용 ──
|
||||||
|
["task_tracker"] = "Code",
|
||||||
|
["todo_write"] = "Code",
|
||||||
|
["task_create"] = "Code",
|
||||||
|
["task_get"] = "Code",
|
||||||
|
["task_list"] = "Code",
|
||||||
|
["task_update"] = "Code",
|
||||||
|
["task_stop"] = "Code",
|
||||||
|
["task_output"] = "Code",
|
||||||
|
["enter_worktree"] = "Code",
|
||||||
|
["exit_worktree"] = "Code",
|
||||||
|
["team_create"] = "Code",
|
||||||
|
["team_delete"] = "Code",
|
||||||
|
["cron_create"] = "Code",
|
||||||
|
["cron_delete"] = "Code",
|
||||||
|
["cron_list"] = "Code",
|
||||||
|
["checkpoint"] = "Code",
|
||||||
|
["suggest_actions"] = "Code",
|
||||||
|
["playbook"] = "Code",
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>도구의 실질 탭 카테고리를 결정합니다 (IAgentTool.TabCategory → 오버라이드 맵 순).</summary>
|
||||||
|
private static string? ResolveTabCategory(IAgentTool tool)
|
||||||
|
{
|
||||||
|
// 도구 자체에 TabCategory가 명시되어 있으면 우선
|
||||||
|
if (!string.IsNullOrEmpty(tool.TabCategory))
|
||||||
|
return tool.TabCategory;
|
||||||
|
// 오버라이드 맵에서 조회
|
||||||
|
return ToolTabOverrides.TryGetValue(tool.Name, out var cat) ? cat : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>IDisposable 도구를 모두 해제합니다.</summary>
|
/// <summary>IDisposable 도구를 모두 해제합니다.</summary>
|
||||||
|
|||||||
@@ -8,13 +8,6 @@ using AxCopilot.Services.Agent;
|
|||||||
|
|
||||||
namespace AxCopilot.Services;
|
namespace AxCopilot.Services;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// LlmService의 Function Calling (tool_use) 확장.
|
|
||||||
/// Claude tool_use, Gemini function_calling 프로토콜을 지원합니다.
|
|
||||||
/// 기존 SendAsync/StreamAsync는 변경하지 않고, 에이전트 전용 메서드를 추가합니다.
|
|
||||||
/// </summary>
|
|
||||||
public partial class LlmService
|
|
||||||
{
|
|
||||||
/// <summary>LLM 응답에서 파싱된 컨텐츠 블록.</summary>
|
/// <summary>LLM 응답에서 파싱된 컨텐츠 블록.</summary>
|
||||||
public class ContentBlock
|
public class ContentBlock
|
||||||
{
|
{
|
||||||
@@ -36,6 +29,7 @@ public partial class LlmService
|
|||||||
{
|
{
|
||||||
TextDelta,
|
TextDelta,
|
||||||
ToolCallReady,
|
ToolCallReady,
|
||||||
|
RetryReset,
|
||||||
Completed
|
Completed
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,6 +38,13 @@ public partial class LlmService
|
|||||||
string Text = "",
|
string Text = "",
|
||||||
ContentBlock? ToolCall = null);
|
ContentBlock? ToolCall = null);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// LlmService의 Function Calling (tool_use) 확장.
|
||||||
|
/// Claude tool_use, Gemini function_calling 프로토콜을 지원합니다.
|
||||||
|
/// 기존 SendAsync/StreamAsync는 변경하지 않고, 에이전트 전용 메서드를 추가합니다.
|
||||||
|
/// </summary>
|
||||||
|
public partial class LlmService
|
||||||
|
{
|
||||||
/// <summary>도구 정의를 포함하여 LLM에 요청하고, 텍스트 + tool_use 블록을 파싱하여 반환합니다.</summary>
|
/// <summary>도구 정의를 포함하여 LLM에 요청하고, 텍스트 + tool_use 블록을 파싱하여 반환합니다.</summary>
|
||||||
/// <param name="forceToolCall">
|
/// <param name="forceToolCall">
|
||||||
/// true이면 <c>tool_choice: "required"</c>를 요청에 추가하여 모델이 반드시 도구를 호출하도록 강제합니다.
|
/// true이면 <c>tool_choice: "required"</c>를 요청에 추가하여 모델이 반드시 도구를 호출하도록 강제합니다.
|
||||||
@@ -150,14 +151,14 @@ public partial class LlmService
|
|||||||
req.Headers.Add("x-api-key", apiKey);
|
req.Headers.Add("x-api-key", apiKey);
|
||||||
req.Headers.Add(SigmoidApiVersionHeader, SigmoidApiVersion);
|
req.Headers.Add(SigmoidApiVersionHeader, SigmoidApiVersion);
|
||||||
|
|
||||||
using var resp = await _http.SendAsync(req, ct);
|
using var resp = await _http.SendAsync(req, ct).ConfigureAwait(false);
|
||||||
if (!resp.IsSuccessStatusCode)
|
if (!resp.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
var errBody = await resp.Content.ReadAsStringAsync(ct);
|
var errBody = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
|
||||||
throw new HttpRequestException(ClassifyHttpError(resp, errBody));
|
throw new HttpRequestException(ClassifyHttpError(resp, errBody));
|
||||||
}
|
}
|
||||||
|
|
||||||
var respJson = await resp.Content.ReadAsStringAsync(ct);
|
var respJson = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
|
||||||
using var doc = JsonDocument.Parse(respJson);
|
using var doc = JsonDocument.Parse(respJson);
|
||||||
var root = doc.RootElement;
|
var root = doc.RootElement;
|
||||||
|
|
||||||
@@ -869,59 +870,28 @@ public partial class LlmService
|
|||||||
{
|
{
|
||||||
if (m.Role == "system") continue;
|
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\""))
|
if (m.Role == "user" && m.Content.StartsWith("{\"type\":\"tool_result\""))
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var doc = JsonDocument.Parse(m.Content);
|
using var doc = JsonDocument.Parse(m.Content);
|
||||||
var root = doc.RootElement;
|
var root = doc.RootElement;
|
||||||
msgs.Add(new
|
msgs.Add(new { role = "user", content = BuildIbmToolResultTranscript(root) });
|
||||||
{
|
|
||||||
role = "tool",
|
|
||||||
tool_call_id = root.GetProperty("tool_use_id").SafeGetString(),
|
|
||||||
content = root.GetProperty("content").SafeGetString(),
|
|
||||||
});
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
catch { }
|
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\""))
|
if (m.Role == "assistant" && m.Content.StartsWith("{\"_tool_use_blocks\""))
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var doc = JsonDocument.Parse(m.Content);
|
using var doc = JsonDocument.Parse(m.Content);
|
||||||
var blocksArr = doc.RootElement.GetProperty("_tool_use_blocks");
|
var blocksArr = doc.RootElement.GetProperty("_tool_use_blocks");
|
||||||
var textContent = "";
|
msgs.Add(new { role = "assistant", content = BuildIbmAssistantTranscript(blocksArr) });
|
||||||
var toolCallsList = new List<object>();
|
|
||||||
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,
|
|
||||||
});
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
catch { }
|
catch { }
|
||||||
@@ -998,6 +968,60 @@ public partial class LlmService
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string BuildIbmAssistantTranscript(JsonElement blocksArr)
|
||||||
|
{
|
||||||
|
var textSegments = new List<string>();
|
||||||
|
var toolSegments = new List<string>();
|
||||||
|
|
||||||
|
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($"<tool_call>\n{{\"name\":\"{name}\",\"arguments\":{args}}}\n</tool_call>");
|
||||||
|
}
|
||||||
|
|
||||||
|
var parts = new List<string>();
|
||||||
|
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
|
||||||
|
? "<tool_call>\n{\"name\":\"unknown_tool\",\"arguments\":{}}\n</tool_call>"
|
||||||
|
: 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
|
private sealed class ToolCallAccumulator
|
||||||
{
|
{
|
||||||
public int Index { get; init; }
|
public int Index { get; init; }
|
||||||
@@ -1230,11 +1254,22 @@ public partial class LlmService
|
|||||||
resultsEl.GetArrayLength() > 0)
|
resultsEl.GetArrayLength() > 0)
|
||||||
{
|
{
|
||||||
var first = resultsEl[0];
|
var first = resultsEl[0];
|
||||||
var generatedText = first.SafeTryGetProperty("generated_text", out var generatedTextEl)
|
string? generatedText = null;
|
||||||
? generatedTextEl.SafeGetString()
|
if (first.SafeTryGetProperty("generated_text", out var generatedTextEl))
|
||||||
: first.SafeTryGetProperty("output_text", out var outputTextEl)
|
generatedText = TryExtractTextContent(generatedTextEl, out var extractedGeneratedText) ? extractedGeneratedText : generatedTextEl.SafeGetString();
|
||||||
? outputTextEl.SafeGetString()
|
else if (first.SafeTryGetProperty("output_text", out var outputTextEl))
|
||||||
: null;
|
generatedText = TryExtractTextContent(outputTextEl, out var extractedOutputText) ? extractedOutputText : outputTextEl.SafeGetString();
|
||||||
|
else if (first.SafeTryGetProperty("message", out var ibmMessageEl) &&
|
||||||
|
TryExtractMessageToolBlocks(ibmMessageEl, out var ibmMessageText, out var ibmToolBlocks))
|
||||||
|
{
|
||||||
|
generatedText = ibmMessageText;
|
||||||
|
foreach (var toolBlock in ibmToolBlocks)
|
||||||
|
{
|
||||||
|
if (prefetchToolCallAsync != null)
|
||||||
|
toolBlock.PrefetchedExecutionTask = prefetchToolCallAsync(toolBlock);
|
||||||
|
yield return new ToolStreamEvent(ToolStreamEventKind.ToolCallReady, ToolCall: toolBlock);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (!string.IsNullOrEmpty(generatedText))
|
if (!string.IsNullOrEmpty(generatedText))
|
||||||
{
|
{
|
||||||
if (generatedText.StartsWith(lastIbmGeneratedText, StringComparison.Ordinal))
|
if (generatedText.StartsWith(lastIbmGeneratedText, StringComparison.Ordinal))
|
||||||
@@ -1364,28 +1399,34 @@ public partial class LlmService
|
|||||||
message = nestedMessage;
|
message = nestedMessage;
|
||||||
|
|
||||||
var consumed = false;
|
var consumed = false;
|
||||||
if (message.SafeTryGetProperty("content", out var contentEl) &&
|
if (message.SafeTryGetProperty("content", out var contentEl))
|
||||||
contentEl.ValueKind == JsonValueKind.String)
|
|
||||||
{
|
{
|
||||||
var parsedText = contentEl.SafeGetString();
|
if (TryExtractTextContent(contentEl, out var parsedText))
|
||||||
if (!string.IsNullOrWhiteSpace(parsedText))
|
|
||||||
{
|
{
|
||||||
text = parsedText;
|
text = parsedText;
|
||||||
consumed = true;
|
consumed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (contentEl.ValueKind == JsonValueKind.Array)
|
||||||
|
{
|
||||||
|
foreach (var block in contentEl.EnumerateArray())
|
||||||
|
{
|
||||||
|
if (!TryParseContentArrayToolBlock(block, out var toolBlock))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
toolBlocks.Add(toolBlock);
|
||||||
|
consumed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Qwen3.5 thinking 모드 폴백: content가 비어있으면 reasoning_content 사용
|
// Qwen3.5 thinking 모드 폴백: content가 비어있으면 reasoning_content 사용
|
||||||
if (!consumed &&
|
if (!consumed &&
|
||||||
message.SafeTryGetProperty("reasoning_content", out var reasoningContentEl) &&
|
message.SafeTryGetProperty("reasoning_content", out var reasoningContentEl) &&
|
||||||
reasoningContentEl.ValueKind == JsonValueKind.String)
|
TryExtractTextContent(reasoningContentEl, out var reasoningText))
|
||||||
{
|
|
||||||
var reasoningText = reasoningContentEl.SafeGetString();
|
|
||||||
if (!string.IsNullOrWhiteSpace(reasoningText))
|
|
||||||
{
|
{
|
||||||
text = reasoningText;
|
text = reasoningText;
|
||||||
consumed = true;
|
consumed = true;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (message.SafeTryGetProperty("tool_calls", out var toolCallsEl) &&
|
if (message.SafeTryGetProperty("tool_calls", out var toolCallsEl) &&
|
||||||
toolCallsEl.ValueKind == JsonValueKind.Array)
|
toolCallsEl.ValueKind == JsonValueKind.Array)
|
||||||
@@ -1428,6 +1469,125 @@ public partial class LlmService
|
|||||||
return consumed;
|
return consumed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool TryExtractTextContent(JsonElement element, out string text)
|
||||||
|
{
|
||||||
|
text = "";
|
||||||
|
|
||||||
|
if (element.ValueKind == JsonValueKind.String)
|
||||||
|
{
|
||||||
|
var parsed = element.SafeGetString();
|
||||||
|
if (!string.IsNullOrWhiteSpace(parsed))
|
||||||
|
{
|
||||||
|
text = parsed;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (element.ValueKind != JsonValueKind.Array)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var segments = new List<string>();
|
||||||
|
foreach (var item in element.EnumerateArray())
|
||||||
|
{
|
||||||
|
if (item.ValueKind == JsonValueKind.String)
|
||||||
|
{
|
||||||
|
var str = item.SafeGetString();
|
||||||
|
if (!string.IsNullOrWhiteSpace(str))
|
||||||
|
segments.Add(str);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.ValueKind != JsonValueKind.Object)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (item.SafeTryGetProperty("text", out var textEl) && textEl.ValueKind == JsonValueKind.String)
|
||||||
|
{
|
||||||
|
var str = textEl.SafeGetString();
|
||||||
|
if (!string.IsNullOrWhiteSpace(str))
|
||||||
|
segments.Add(str);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.SafeTryGetProperty("content", out var nestedContentEl) && TryExtractTextContent(nestedContentEl, out var nestedText))
|
||||||
|
{
|
||||||
|
segments.Add(nestedText);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.SafeTryGetProperty("reasoning_content", out var reasoningEl) && TryExtractTextContent(reasoningEl, out var reasoningText))
|
||||||
|
{
|
||||||
|
segments.Add(reasoningText);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.SafeTryGetProperty("output_text", out var outputTextEl) && outputTextEl.ValueKind == JsonValueKind.String)
|
||||||
|
{
|
||||||
|
var str = outputTextEl.SafeGetString();
|
||||||
|
if (!string.IsNullOrWhiteSpace(str))
|
||||||
|
segments.Add(str);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (segments.Count == 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
text = string.Join("\n", segments.Where(s => !string.IsNullOrWhiteSpace(s)));
|
||||||
|
return !string.IsNullOrWhiteSpace(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryParseContentArrayToolBlock(JsonElement block, out ContentBlock toolBlock)
|
||||||
|
{
|
||||||
|
toolBlock = default!;
|
||||||
|
|
||||||
|
if (block.ValueKind != JsonValueKind.Object)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var type = block.SafeTryGetProperty("type", out var typeEl) ? typeEl.SafeGetString() ?? "" : "";
|
||||||
|
if (!string.Equals(type, "tool_use", StringComparison.OrdinalIgnoreCase) &&
|
||||||
|
!string.Equals(type, "tool_call", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
JsonElement? parsedArgs = null;
|
||||||
|
if (block.SafeTryGetProperty("input", out var inputEl))
|
||||||
|
parsedArgs = inputEl.Clone();
|
||||||
|
else if (block.SafeTryGetProperty("arguments", out var argsEl))
|
||||||
|
{
|
||||||
|
if (argsEl.ValueKind == JsonValueKind.String)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var argsDoc = JsonDocument.Parse(argsEl.SafeGetString() ?? "{}");
|
||||||
|
parsedArgs = argsDoc.RootElement.Clone();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
parsedArgs = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (argsEl.ValueKind is JsonValueKind.Object or JsonValueKind.Array)
|
||||||
|
{
|
||||||
|
parsedArgs = argsEl.Clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var toolName = block.SafeTryGetProperty("name", out var nameEl) ? nameEl.SafeGetString() ?? "" : "";
|
||||||
|
if (string.IsNullOrWhiteSpace(toolName))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
toolBlock = new ContentBlock
|
||||||
|
{
|
||||||
|
Type = "tool_use",
|
||||||
|
ToolName = toolName,
|
||||||
|
ToolId = block.SafeTryGetProperty("id", out var idEl)
|
||||||
|
? idEl.SafeGetString() ?? Guid.NewGuid().ToString("N")[..12]
|
||||||
|
: Guid.NewGuid().ToString("N")[..12],
|
||||||
|
ToolInput = parsedArgs,
|
||||||
|
};
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private static bool LooksLikeCompleteJson(string json)
|
private static bool LooksLikeCompleteJson(string json)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(json))
|
if (string.IsNullOrWhiteSpace(json))
|
||||||
|
|||||||
435
src/AxCopilot/Views/ChatWindow.SystemPromptBuilder.cs
Normal file
435
src/AxCopilot/Views/ChatWindow.SystemPromptBuilder.cs
Normal file
@@ -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<string, string>(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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>프로젝트 규칙 (.ax/rules/)을 시스템 프롬프트 섹션으로 포맷합니다.</summary>
|
||||||
|
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 "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>에이전트 메모리를 시스템 프롬프트 섹션으로 포맷합니다.</summary>
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user