Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da11029284 | ||
|
|
58b798d3e4 | ||
|
|
c7b2bba063 | ||
|
|
4db75d46cd | ||
|
|
fb0bea41f7 | ||
|
|
b8f4df1892 | ||
|
|
d8cd04aa4f | ||
|
|
ef58e93e38 | ||
|
|
bdd4444deb | ||
|
|
1dd10e0664 | ||
|
|
0a2e5b2d49 | ||
|
|
0f83dc802c | ||
|
|
9175dfe657 | ||
|
|
5511c620de | ||
|
|
36a9af8210 | ||
|
|
d24c1f9edc | ||
|
|
6bd8d5bb2c | ||
|
|
1fab215344 | ||
|
|
3c6d2f1ce4 |
@@ -7,10 +7,87 @@ Windows 전용 시맨틱 런처 & 워크스페이스 매니저
|
||||
개발 참고: Claw Code 동등성 작업 추적 문서
|
||||
`docs/claw-code-parity-plan.md`
|
||||
|
||||
- 업데이트: 2026-04-09 13:05 (KST)
|
||||
- AX Agent transcript 표시를 `claude-code`식 row 타입 기반으로 재정리했습니다. `thinking / waiting / compact / tool activity / permission / tool result / status`를 별도 row 의미로 정규화하고, process feed는 같은 활동 그룹이면 이어붙여 append 수를 줄이도록 바꿨습니다.
|
||||
- 권한 요청과 도구 결과 카탈로그를 다시 정리해 Cowork/Code 진행 중 무엇을 하는지, 어떤 결과 상태인지가 기본 transcript에서 더 읽히도록 맞췄습니다. 동시에 render 성능 로그에 row kind 분포와 process feed append/merge 수치를 남겨 이후 실검증 때 구조 개선 효과를 바로 판단할 수 있게 했습니다.
|
||||
- Cowork/Chat 하단 프리셋 안내 카드는 실제 메시지뿐 아니라 execution event가 생긴 뒤에도 자동으로 숨겨 결과/진행 화면을 가리지 않도록 조정했고, 입력 워터마크와 footer 기본 문구도 정상 한국어 기준으로 다시 정리했습니다.
|
||||
- 업데이트: 2026-04-12 23:45 (KST)
|
||||
- `claude-code` 기준으로 남아 있던 provider/compact/UI 후속 차이를 더 줄였습니다. OpenAI 호환 도구 호출은 이제 모델 계열별 호환 프로파일을 적용해 Qwen·LLaMA·DeepSeek 같은 취약한 vLLM 계열에서 최근 structured tool history 범위를 더 좁히고, 병렬 tool call과 reasoning_effort도 더 보수적으로 전송합니다.
|
||||
- tool forcing fallback도 보강했습니다. IBM 배포형뿐 아니라 일반 OpenAI 호환 경로에서도 `tool_choice`가 400으로 거부되면, plain text 지시를 덧붙인 대체 요청으로 한 번 더 재시도해 `채팅은 되지만 Cowork/Code만 막히는` 조합을 줄였습니다.
|
||||
- compact 이후 query view에는 복원된 branch context와 최근 tool state까지 함께 다시 주입합니다. UI는 표현 수준(`rich/balanced/simple`)에 맞춰 compact 카드와 컨텍스트 사용 팝업 밀도를 다르게 보여주고, 최종 보고 프롬프트도 같은 수준에 맞춰 더 짧거나 더 구조적으로 정리되도록 맞췄습니다.
|
||||
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\` 경고 0 / 오류 0
|
||||
|
||||
- 업데이트: 2026-04-10 14:30 (KST)
|
||||
- **UI 프리징 근본 수정**: 스트리밍 중 렌더링 쓰로틀(1.5초 최소 간격), 이중 RenderMessages 제거, 타이머 Stop→Start 무한 루프 차단, 불필요 타이머 4개 일시 정지, 타이머 간격 2-10배 증가(350ms→5s, 500ms→2s 등). 에이전트 이벤트 디스패처 우선순위를 Normal→Background로 하향.
|
||||
- **모델 프로파일 도구 사용 버그 수정**: Ollama 모델에 `tool_choice: "required"` 미전달 버그 수정 — `BuildOpenAiToolBody`에서 Ollama 조기 리턴 전에 `tool_choice` 주입. `FindRegisteredModel`의 대소문자 민감 비교를 `OrdinalIgnoreCase`로 변경하여 프로파일 매칭 실패 방지.
|
||||
- **트랜스크립트 UI 개선**: "처리 중..." / "작업을 준비하는 중입니다..." 트랜스크립트 힌트를 제거하고 PulseDotBar로만 상태 표시 (Claude Desktop 스타일). 완료 이벤트의 이모지 깨짐("?챗셝?콺듦") 수정 — 서로게이트 쌍(비-BMP 유니코드) 자동 제거.
|
||||
- **PPT 기능 확장**: 이미지 삽입(BlipFill+aspect ratio), 아이콘 라이브러리(170+ 유니코드/120+ OpenXML), 슬라이드 복제(전체/개별), 네이티브 차트(bar/line/pie ChartPart), theme_file 마스터 복제.
|
||||
- **아이콘 공유**: 새 `IconLibrary.cs`를 DOCX/XLSX/HTML 스킬에서 공유. `{icon:name}` 인라인 구문 + 블록 아이콘 지원.
|
||||
- **마스코트 개선**: 3배 크기(300px), NearestNeighbor 픽셀아트 렌더, 투명 배경, 좌우 이동 애니메이션 10종.
|
||||
- 검증: `dotnet build` 경고 0 / 오류 0
|
||||
|
||||
- 업데이트: 2026-04-10 09:02 (KST)
|
||||
- `claude-code` 기준으로 Cowork/Code의 남은 차이를 더 줄였습니다. Cowork/Code 프롬프트의 텍스트-only 완료 조건을 완화해, 작업이 이미 끝났거나 충분한 근거가 있을 때는 불필요한 도구 호출을 더 강제하지 않도록 정리했습니다.
|
||||
- 에이전트 루프도 같이 손봤습니다. 텍스트-only 재시도는 이제 실제 산출물 생성이나 코드 수정처럼 구체적인 실행이 필요한 경우에만 다시 도구를 강제하고, 문서 생성 fallback도 같은 범위로 좁혔습니다.
|
||||
- Code 검증 게이트는 `diff` 또는 최근 build/test 근거가 이미 있으면 추가 재검증을 덜 하도록 줄였고, `folder_map`은 기본 노출 우선순위에서 한 단계 내려 보조 탐색 도구로 배치했습니다.
|
||||
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\` 경고 0 / 오류 0
|
||||
|
||||
- 업데이트: 2026-04-10 08:47 (KST)
|
||||
- 계획 확인 UI를 AX Agent 테마에 맞춰 다시 정리했습니다. 채팅 안 승인 카드는 `LauncherBackground`/`ItemBackground`/`BorderColor`/`AccentColor`를 기준으로 표면, 버튼 모서리, 입력 패널 간격을 통일해 기본 컨트롤 느낌을 줄였습니다.
|
||||
- 플랜 뷰어 창의 하단 승인 영역도 같은 시각 언어로 손봤습니다. 승인 전 안내 카드를 추가하고, 수정 입력 패널은 테마 배경과 테두리를 쓰도록 바꿨으며, 액션 버튼 호버도 `ItemHoverBackground` 중심으로 정리했습니다.
|
||||
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\` 경고 0 / 오류 0
|
||||
|
||||
- 업데이트: 2026-04-10 00:08 (KST)
|
||||
- `claude-code`와 비교했을 때 AX에 남아 있던 계획 선행과 후속 권유 톤을 더 줄였습니다. 기본 Cowork/Code 루프 안에 남아 있던 plan prelude/승인용 죽은 코드를 제거해, 별도 계획 생성 단계를 끼우지 않고 바로 모델+도구 실행으로 들어갑니다.
|
||||
- Code 최종 보고 재강제도 review 작업과 고영향 변경으로 좁혔습니다. 일반 수정은 변경 내용과 검증 근거가 충분하면 후속 계획이나 남은 리스크를 덧붙이도록 다시 유도하지 않고 마무리할 수 있습니다.
|
||||
- Cowork/Code 시스템 프롬프트도 같은 방향으로 정리했습니다. Cowork는 새 문서 생성 시 `document_plan`을 기본 선행 단계처럼 밀지 않고 필요할 때만 쓰게 했고, Code는 마지막 보고에서 미해결 사항이 있을 때만 리스크를 언급하게 바꿨습니다.
|
||||
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\` 경고 0 / 오류 0
|
||||
|
||||
- 업데이트: 2026-04-10 00:15 (KST)
|
||||
- Virtual DOM Diff 렌더 구현 완료: React reconciliation 방식의 키 기반 diff를 WPF에 적용. 렌더 체인이 StreamingAppend → Incremental → **DiffRender** → FullRender 4단계로 확장되어, prefix-match 실패 시에도 전체 재빌드 없이 변경분만 반영합니다.
|
||||
- 검증: `dotnet build` 경고 0 / 오류 0
|
||||
|
||||
- 업데이트: 2026-04-09 23:02 (KST)
|
||||
- IBM/Qwen 후속 호환을 한 단계 더 보강했습니다. 이제 IBM 배포형 응답에서 `generated_text`, `output_text`, `message.content`, `reasoning_content`가 문자열뿐 아니라 배열/블록 형태로 와도 텍스트를 추출하고, `content` 배열 안의 `tool_use/tool_call` 블록도 직접 읽어 도구 호출로 복구합니다.
|
||||
- 활성 도구 노출 순서도 다시 정리했습니다. `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`처럼 기본 작업 도구가 먼저 선택되도록 했습니다.
|
||||
- 검증: `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)
|
||||
- AX Agent transcript 호스트를 `ObservableCollection<UIElement>` 직접 주입 구조에서 `TranscriptVisualItem + TranscriptVisualHost` 기반의 지연 materialization 구조로 올렸습니다. `MessageList`는 virtualization 설정을 유지한 채 필요한 시점에만 실제 버블 UI를 생성할 수 있는 기반을 갖습니다.
|
||||
@@ -1546,16 +1623,60 @@ 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)의 책임을 더 줄였습니다.
|
||||
- [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 여부를 함께 기록해 실사용 세션에서 버벅임을 실제 수치로 판단할 수 있게 됐습니다.
|
||||
- 업데이트: 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이 실제 렌더 수를 얼마나 줄였는지 성능 로그로 확인할 수 있게 했습니다.
|
||||
- 업데이트: 2026-04-10 09:03 (KST)
|
||||
- Cowork/Code 실행 중 앱 화면이 끝날 때까지 멈춰 보이던 현상을 줄이기 위해, 에이전트 이벤트의 UI 전달 우선순위와 처리 경로를 다시 정리했습니다.
|
||||
- [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)의 agent dispatcher는 `DispatcherPriority.Normal` 대신 `Background`를 사용해 입력/렌더가 먼저 흐르도록 조정했습니다.
|
||||
- 같은 파일의 `OnAgentEvent()`는 이벤트마다 라이브 카드와 상태 서브아이템을 즉시 갱신하지 않고, 완료/오류 같은 종료 신호만 즉시 처리한 뒤 나머지는 기존 배치 타이머로 넘기도록 단순화했습니다.
|
||||
- [ChatWindow.AgentEventProcessor.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.AgentEventProcessor.cs)는 execution event를 UI 스레드와 백그라운드에서 두 번 append하던 구조를 제거하고, 백그라운드 단일 리더에서 한 번만 대화 히스토리를 반영하도록 바꿨습니다.
|
||||
- 업데이트: 2026-04-12 21:34 (KST)
|
||||
- `claw-code`의 `messagesForQuery`, `autoCompact`, `sessionMemoryCompact` 흐름을 기준으로 AX Agent의 컨텍스트 전송 뷰와 압축 트리거를 한 단계 더 정리했습니다.
|
||||
- [AgentQueryContextBuilder.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentQueryContextBuilder.cs)를 추가해, 저장된 전체 대화와 실제 LLM에 전송할 query view를 분리했습니다. 이 뷰는 마지막 compact boundary부터만 다시 보내고, 오래된 `tool_result`는 전송 직전에만 budget 기준으로 더 줄입니다.
|
||||
- 같은 helper에서 `tool_result`가 남아 있는 kept range를 검사해, 대응되는 assistant `_tool_use_blocks`가 잘리지 않도록 window start를 뒤로 보정합니다.
|
||||
- [AgentLoopService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentLoopService.cs)는 각 반복에서 `messagesForQuery`에 해당하는 전송 뷰를 만든 뒤 `SendWithToolsWithRecoveryAsync()`와 텍스트 fallback 호출에 사용하도록 바꿨습니다.
|
||||
- [ContextCondenser.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/ContextCondenser.cs)는 `triggerPercent`만 보던 기준에서 `effective context window - output reserve - buffer` 개념을 반영해 자동 압축 시작 지점을 더 보수적으로 계산하도록 바꿨습니다.
|
||||
- 업데이트: 2026-04-12 21:39 (KST)
|
||||
- `claw-code`의 `sessionMemoryCompact`처럼 압축 경계 자체에서도 `tool_use / tool_result` 짝이 끊기지 않도록 보정 범위를 넓혔습니다.
|
||||
- [AgentMessageInvariantHelper.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentMessageInvariantHelper.cs)를 추가해 `tool_result`가 남는 kept range에서 필요한 assistant `_tool_use_blocks`를 공용 로직으로 다시 포함시키게 했습니다.
|
||||
- [ContextCondenser.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/ContextCondenser.cs)는 `MicrocompactOlderMessages`, `SessionMemoryCompactOlderMessages`, `CollapseAndSnipOlderMessages`, `SummarizeOldMessagesAsync`가 모두 같은 split helper를 사용하도록 바꿨습니다.
|
||||
- 이제 recent window가 `tool_result`로 시작하는 경우에도, 그 결과를 참조하는 이전 `tool_use`가 요약/압축 쪽으로 잘려 나가 API pairing 오류를 만드는 상황을 줄일 수 있습니다.
|
||||
- 업데이트: 2026-04-12 22:02 (KST)
|
||||
- `claw-code`의 `applyToolResultBudget` 방향을 따라, AX도 query view와 압축 1단계가 같은 `tool_result` 예산 규칙을 공유하도록 정리했습니다.
|
||||
- [AgentToolResultBudget.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentToolResultBudget.cs)를 추가해 오래된 `tool_result`를 최근 보호 구간과 aggregate budget 기준으로 공용 축약하도록 만들었습니다.
|
||||
- [AgentQueryContextBuilder.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentQueryContextBuilder.cs)는 query view에서 이 공용 helper를 사용하게 바뀌어, 전송 직전 budget과 축약 결과가 압축 본체와 같은 규칙을 따릅니다.
|
||||
- [ContextCondenser.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/ContextCondenser.cs)는 1단계에서 `tool_result`를 별도 공용 budget으로 먼저 줄이고, 그 외 긴 assistant/user 메시지만 추가 절단하도록 역할을 분리했습니다.
|
||||
- 업데이트: 2026-04-12 22:11 (KST)
|
||||
- `claw-code`의 time-based microcompact 방향을 따라, 오래 쉬었다가 다시 호출할 때 오래된 `tool_result`를 먼저 정리하는 분기를 AX에 추가했습니다.
|
||||
- [ContextCondenser.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/ContextCondenser.cs)는 마지막 assistant 이후 20분 이상 경과한 경우, 가장 최근 `tool_result` 1개만 남기고 나머지는 작은 cleared marker로 바꾸도록 처리합니다.
|
||||
- [AgentToolResultBudget.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentToolResultBudget.cs)는 cleared `tool_result` JSON을 만드는 helper를 추가해, 기존 `tool_use_id`/`tool_name`은 유지하면서 content만 가볍게 비우도록 보강했습니다.
|
||||
- [AgentLoopCompactionPolicy.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentLoopCompactionPolicy.cs)는 compact 직후 첫 턴에서 추가 운영성 thinking 문구를 다시 띄우지 않고 내부 상태만 갱신하도록 바꿔, compact 후 흐름이 더 조용하게 이어지도록 정리했습니다.
|
||||
- 업데이트: 2026-04-12 22:19 (KST)
|
||||
- `time-based` tool result 정리 기준을 모델/서비스별로 세분화해, Claude는 더 보수적으로, Qwen/vLLM 계열은 더 빠르게 오래된 결과를 걷어내도록 조정했습니다.
|
||||
- [ContextCondenser.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/ContextCondenser.cs)는 `service:model` 조합에 따라 `gapThresholdMinutes`와 `keepRecent`를 다르게 계산하도록 바뀌었습니다.
|
||||
- [ChatWindow.TimelinePresentation.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.TimelinePresentation.cs)는 compact 메타 카드를 긴 상세 줄 목록 대신 한 줄 요약 카드로 단순화해, transcript에서 운영성 카드 밀도를 더 낮췄습니다.
|
||||
- 업데이트: 2026-04-12 22:27 (KST)
|
||||
- compact 이후 응답/컨텍스트 사용 표시에서 남아 있던 후행 운영 메타를 더 줄였습니다.
|
||||
- [ChatWindow.ResponsePresentation.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.ResponsePresentation.cs)는 응답 하단 토큰 메타에서 `compact 직후` 꼬리표를 제거해, 일반 응답과 같은 밀도로 보이도록 정리했습니다.
|
||||
- [ChatWindow.ContextUsagePresentation.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.ContextUsagePresentation.cs)는 token usage 팝업 detail에서 `compact 후 첫 응답 대기 중` 문구를 빼고 실제 컨텍스트/압축 정보만 보여주도록 단순화했습니다.
|
||||
- 업데이트: 2026-04-12 22:36 (KST)
|
||||
- `claw-code`의 post-compact attachments 흐름을 참고해, AX도 요약/경계 메시지에 오래된 첨부 참조를 다시 실어 compact 뒤 컨텍스트 연속성을 보강했습니다.
|
||||
- [ContextCondenser.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/ContextCondenser.cs)는 오래된 메시지에서 첨부 파일 이름과 이미지 개수를 수집해 `microcompact_boundary`와 요약 메시지에 함께 기록합니다.
|
||||
- 요약 메시지에는 `AttachedFiles`도 같이 보존해, compact 이후에도 관련 파일 참조가 query view로 이어질 수 있게 맞췄습니다.
|
||||
- 업데이트: 2026-04-12 22:44 (KST)
|
||||
- OpenAI/vLLM 호환 경로의 tool history 직렬화를 더 가볍게 만들어, 최근 구간만 구조화된 `tool_calls/tool` 형식을 유지하고 오래된 구간은 평탄한 transcript로 낮췄습니다.
|
||||
- [LlmService.ToolUse.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/LlmService.ToolUse.cs)는 최근 비-system 메시지 8개만 구조화된 tool history로 유지하고, 그 이전의 `_tool_use_blocks`와 `tool_result`는 plain assistant/user transcript로 변환해 재전송합니다.
|
||||
- 이로써 strict provider에서 오래된 `tool_calls/tool` 이력이 계속 누적되던 부담과 pairing 위험을 함께 줄였습니다.
|
||||
- 업데이트: 2026-04-12 23:05 (KST)
|
||||
- `claw-code`와 비교했을 때 여전히 무겁던 Cowork 문서 생성 게이트와 Code 후속 검증 게이트를 한 단계 더 줄였습니다.
|
||||
- [AgentLoopService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentLoopService.cs) 는 `document_plan` 뒤의 강제 재시도/자동 HTML fallback을 공격적인 실행 프로파일에서만 동작하도록 제한했습니다.
|
||||
- [AgentLoopTransitions.Documents.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentLoopTransitions.Documents.cs) 는 `document_plan` 성공 직후 추가 user 압박 메시지를 넣지 않고, terminal 문서 도구 성공 시 Cowork에서 바로 종료할 수 있게 정리했습니다.
|
||||
- [AgentLoopTransitions.Verification.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentLoopTransitions.Verification.cs) 는 `CodeDiffGate`, `RecentExecutionGate`, `ExecutionSuccessGate`를 review 작업 중심으로 제한해 일반 코드 수정의 과검증을 줄였습니다.
|
||||
- [TaskTypePolicy.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/TaskTypePolicy.cs), [ChatWindow.SystemPromptBuilder.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.SystemPromptBuilder.cs), [cowork_문서작성.json](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Assets/Presets/cowork_%EB%AC%B8%EC%84%9C%EC%9E%91%EC%84%B1.json), [cowork_보고서.json](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Assets/Presets/cowork_%EB%B3%B4%EA%B3%A0%EC%84%9C.json) 도 같은 기준으로 문서 생성/텍스트 종료 조건을 더 얇게 맞췄습니다.
|
||||
- 업데이트: 2026-04-12 23:14 (KST)
|
||||
- compact 이후 query view에 복원된 파일/이미지 참조를 짧게 다시 주입해, `claw-code`의 post-compact attachment continuity에 더 가깝게 맞췄습니다.
|
||||
- [AgentQueryContextBuilder.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentQueryContextBuilder.cs)는 compact boundary가 적용된 query view에 `post_compact_context` system 메시지를 추가해, 복원된 파일 참조와 이미지 참조 개수를 함께 전달합니다.
|
||||
- [AgentLoopService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentLoopService.cs)의 final-report 품질 프롬프트는 일반 작업에서는 더 짧고 명확한 3줄 요약 중심으로 축소하고, review/high-impact 작업에만 구조적 상세 보고를 유지하도록 조정했습니다.
|
||||
- 업데이트: 2026-04-12 23:23 (KST)
|
||||
- compact 뒤 query view의 복원 메모를 한 단계 더 구조화해, 요약 경계 수와 구조화된 tool history 블록 수도 함께 실어주도록 보강했습니다.
|
||||
- [AgentQueryContextBuilder.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentQueryContextBuilder.cs)는 `post_compact_context` 메시지에 `restored compact summaries`, `restored tool history blocks`, `restored file refs`, `restored image refs`를 분리해 담습니다.
|
||||
- [ChatWindow.TimelinePresentation.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.TimelinePresentation.cs)는 compact 메타 카드를 더 짧은 설명과 파일 개수 정도만 보이도록 줄였습니다.
|
||||
- [ChatWindow.ContextUsagePresentation.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.ContextUsagePresentation.cs)는 컨텍스트 사용 팝업의 compact 디테일 문구를 짧고 읽기 쉬운 표현으로 다시 정리했습니다.
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env bash
|
||||
# AX Copilot - Quick Dev Build (no encryption, no obfuscation, no installer)
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")" && pwd)"
|
||||
APP="$ROOT/src/AxCopilot/AxCopilot.csproj"
|
||||
OUT="$ROOT/dist"
|
||||
APP_OUT="$OUT/AxCopilot"
|
||||
RUNTIME="win-x64"
|
||||
|
||||
echo ""
|
||||
echo "========================================"
|
||||
echo " AX Copilot - Quick Build (dev)"
|
||||
echo "========================================"
|
||||
echo ""
|
||||
|
||||
# Stop running process (best-effort)
|
||||
if command -v taskkill &>/dev/null; then
|
||||
taskkill //IM AxCopilot.exe //F 2>/dev/null || true
|
||||
fi
|
||||
|
||||
rm -rf "$OUT"
|
||||
mkdir -p "$APP_OUT"
|
||||
|
||||
echo "[1/1] Publishing main app (self-contained $RUNTIME)..."
|
||||
dotnet publish "$APP" \
|
||||
-c Release \
|
||||
-r "$RUNTIME" \
|
||||
--self-contained true \
|
||||
-o "$APP_OUT" \
|
||||
--nologo \
|
||||
-v minimal \
|
||||
-p:DebugType=None \
|
||||
-p:DebugSymbols=false \
|
||||
-p:CopyOutputSymbolsToPublishDirectory=false \
|
||||
-p:EnableSourceLink=false \
|
||||
-p:PublishSingleFile=true \
|
||||
-p:EnableCompressionInSingleFile=true \
|
||||
-p:IncludeNativeLibrariesForSelfExtract=true \
|
||||
-p:PublishReadyToRun=true
|
||||
|
||||
# Cleanup debug/metadata artifacts
|
||||
rm -f "$APP_OUT"/*.pdb "$APP_OUT"/*.xml "$APP_OUT"/*.deps.json "$APP_OUT"/*.runtimeconfig.json 2>/dev/null || true
|
||||
|
||||
echo ""
|
||||
echo "========================================"
|
||||
echo " Quick Build Complete!"
|
||||
echo "========================================"
|
||||
echo ""
|
||||
echo " Output: $APP_OUT"
|
||||
echo ""
|
||||
echo " Skipped: encryption, obfuscation, AxKeyEncryptor, installer"
|
||||
echo ""
|
||||
@@ -1,6 +1,89 @@
|
||||
# AX Copilot - 媛쒕컻 臾몄꽌
|
||||
|
||||
|
||||
## 계획 승인 UI 테마 정렬
|
||||
|
||||
- 업데이트: 2026-04-10 08:47 (KST)
|
||||
- `ChatWindow.AgentStatusPresentation`의 계획 승인 카드를 AX Agent 테마 리소스 기반으로 다시 정리했습니다. 승인/수정/취소 버튼의 라운드, 간격, 입력 패널 배경을 `LauncherBackground`, `ItemBackground`, `BorderColor`, `AccentColor` 축으로 통일해 채팅 본문과 더 자연스럽게 이어지도록 조정했습니다.
|
||||
- `PlanViewerWindow`의 승인 단계 UI도 같은 방향으로 손봤습니다. 승인 버튼 영역 앞에 검토 안내 카드를 추가하고, 수정 피드백 입력 패널은 테마 배경/테두리를 사용하며, 액션 버튼 호버는 단순 opacity 대신 `ItemHoverBackground` 계열로 반응하도록 바꿨습니다.
|
||||
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\` 경고 0 / 오류 0
|
||||
|
||||
## claude-code식 Cowork/Code 강제 로직 추가 완화
|
||||
|
||||
- 업데이트: 2026-04-10 09:02 (KST)
|
||||
- `ChatWindow.SystemPromptBuilder`에서 Cowork/Code의 text-only 완료 조건을 더 현실적으로 완화했습니다. 작업이 이미 완료됐거나 충분한 근거가 확보된 경우에는 불필요한 도구 호출을 더 강제하지 않도록 정리했고, 새 문서 생성 시에도 `document_plan`은 선택적 구조 보조 단계로만 남겼습니다.
|
||||
- `AgentLoopService`의 no-tool recovery와 `ForceToolCallAfterPlan`도 `claude-code` 기준에 맞춰 축소했습니다. 이제 Cowork는 실제 문서 생성 요청, Code는 bugfix/feature/refactor처럼 구체 수정이 필요한 경우에만 다시 도구를 강제하고, 분석형/설명형 응답은 불필요하게 재루프시키지 않습니다.
|
||||
- 문서 fallback도 같은 기준으로 좁혔습니다. `document_plan` 이후 `html_create` 재유도나 앱 직접 생성 fallback은 실제 산출물 생성이 필요한 경우에만 작동합니다.
|
||||
- `AgentLoopTransitions.Verification`은 일반 Code 완료 조건을 더 얇게 바꿨습니다. 최근 `build/test` 또는 `git diff` 근거가 이미 있으면 `CodeDiffGate`, `RecentExecutionGate`, `ExecutionSuccessGate`가 중복 발동하지 않도록 줄였습니다.
|
||||
- `TaskTypePolicy`의 docs 가이드와 `ToolRegistry`의 노출 우선순위도 정리했습니다. docs는 `document_plan first` 고정에서 벗어나 creation tool 우선 + `document_plan` 선택형으로 바뀌었고, `folder_map`은 최상위 기본 도구에서 한 단계 내려 보조 탐색 도구로 재배치했습니다.
|
||||
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\` 경고 0 / 오류 0
|
||||
|
||||
## claude-code식 계획/후속 권유 최소화
|
||||
|
||||
- 업데이트: 2026-04-10 00:08 (KST)
|
||||
- `AgentLoopService`에서 과거 plan mode 잔재로 남아 있던 prelude/승인용 실행 계획 블록을 제거했습니다. 기본 Cowork/Code 루프는 이제 별도 “계획만 먼저 생성” 단계 없이 바로 메인 모델+도구 반복으로 들어갑니다. 사용자가 명시적으로 계획을 원할 때만 응답 텍스트 수준에서 계획을 제시하고, 루프 자체는 `claude-code`처럼 실행 중심으로 유지합니다.
|
||||
- `AgentLoopTransitions.Verification`의 `FinalReportGate`는 review 작업 또는 고영향 변경일 때만 구조화된 재정리를 요구합니다. 일반 수정은 변경 내용과 검증 근거만 충분하면 “remaining risk / next action”을 추가로 다시 쓰게 하지 않습니다.
|
||||
- `BuildFinalReportQualityPrompt`도 같은 기준으로 완화했습니다. 남은 리스크/추가 확인은 실제로 미해결 사항이 남아 있을 때만 적도록 바꾸고, 후속 권유는 기본이 아니라 조건부 정보로 낮췄습니다.
|
||||
- `ChatWindow.SystemPromptBuilder`의 Cowork/Code 프롬프트도 조정했습니다. Cowork는 새 문서 생성 시 `document_plan`을 기본 선행 단계처럼 밀지 않고 “필요할 때만” 사용하게 했고, Code의 REPORT 단계는 “변경 내용과 검증 요약”을 기본으로 하며 미해결 리스크만 선택적으로 언급하게 바꿨습니다.
|
||||
- 검증: `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 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 표시 구조 정리
|
||||
|
||||
- 업데이트: 2026-04-09 13:05 (KST)
|
||||
@@ -472,3 +555,191 @@ owKindCounts를 함께 남겨 %APPDATA%\\AxCopilot\\perf 기준으로 transcript
|
||||
- `src/AxCopilot/Views/ChatWindow.TranscriptRendering.cs`
|
||||
- performance log detail??`processFeedAppends`, `processFeedMerges`瑜?異붽???grouped activity row???④낵瑜??ㅼ궗??濡쒓렇?먯꽌 ?뺤씤?????덇쾶 ?덉뒿?덈떎.
|
||||
|
||||
## Cowork/Code 실행 중 UI 멈춤 완화 (2026-04-10 09:03 KST)
|
||||
|
||||
- `claude-code`처럼 실행 이벤트를 얇게 소비하도록, AX Agent의 Cowork/Code UI 이벤트 경로를 다시 줄였습니다.
|
||||
- `src/AxCopilot/Views/ChatWindow.xaml.cs`
|
||||
- agent loop dispatcher 우선순위를 `DispatcherPriority.Normal`에서 `DispatcherPriority.Background`로 낮춰, 대량의 에이전트 이벤트가 들어와도 사용자 입력과 렌더가 먼저 처리되도록 바꿨습니다.
|
||||
- `OnAgentEvent()`는 이제 완료/오류 같은 종료 이벤트만 즉시 처리하고, ToolCall/ToolResult/Thinking의 라이브 카드 갱신은 기존 `_agentUiEventTimer` 기반 배치 경로로 넘깁니다.
|
||||
- `src/AxCopilot/Views/ChatWindow.AgentEventProcessor.cs`
|
||||
- execution event를 UI 스레드에서 즉시 append한 뒤 백그라운드에서 다시 append하던 중복 경로를 제거했습니다.
|
||||
- 대화 히스토리 반영, agent run 기록, 저장은 백그라운드 단일 리더에서 한 번만 처리하도록 정리해 Cowork/Code 실행 중 UI 점유를 줄였습니다.
|
||||
- 기대 효과
|
||||
- 긴 tool/thinking 이벤트가 연속으로 들어와도 창 전체가 끝날 때까지 얼어붙는 느낌이 줄어듭니다.
|
||||
- transcript 저장과 실행 이력 반영이 한 번만 일어나, 실행 길이가 길수록 커지던 UI 스레드 부담이 완화됩니다.
|
||||
|
||||
## claw-code식 컨텍스트 전송 뷰 / 압축 트리거 정리 (2026-04-12 21:34 KST)
|
||||
|
||||
- `claw-code`의 `query.ts`, `autoCompact.ts`, `sessionMemoryCompact.ts`를 다시 대조해 AX도 저장용 전체 대화와 실제 API 전송용 컨텍스트 뷰를 분리하기 시작했습니다.
|
||||
- `src/AxCopilot/Services/Agent/AgentQueryContextBuilder.cs`
|
||||
- `messagesForQuery` 역할의 전송 전용 view builder를 추가했습니다.
|
||||
- 마지막 compact boundary(`microcompact_boundary`, `session_memory_compaction`, `collapsed_boundary`, 이전 대화 요약)부터만 다시 보내도록 window start를 계산합니다.
|
||||
- kept range에 `tool_result`가 남아 있는데 대응 `assistant _tool_use_blocks`가 잘릴 수 있는 경우, start index를 뒤로 보정해 pair invariant를 유지합니다.
|
||||
- 오래된 `tool_result`는 원본 `messages`를 바꾸지 않고 query view에서만 별도 budget으로 축약합니다.
|
||||
- `src/AxCopilot/Services/Agent/AgentLoopService.cs`
|
||||
- 각 반복에서 `_pendingUserMessages` 주입 뒤 `AgentQueryContextBuilder.Build(messages)`를 호출해 `queryMessages`를 만들고, 이를 메인 LLM 호출과 텍스트 fallback에 사용하도록 바꿨습니다.
|
||||
- workflow 로그에는 `query_view` 전이를 추가해 source/view 메시지 수, preserved pair 수, tool_result budget 적용 수, budget 전후 토큰을 확인할 수 있게 했습니다.
|
||||
- `src/AxCopilot/Services/Agent/ContextCondenser.cs`
|
||||
- 모델 한도 계산을 `triggerPercent` 단독 기준에서 `effective context window - summary reserve - auto compact buffer` 구조를 함께 반영하도록 변경했습니다.
|
||||
- `SummaryReserveTokens=20_000`, `AutoCompactBufferTokens=13_000` 기준을 두어 `claw-code`의 auto-compact headroom 계산에 더 가깝게 맞췄습니다.
|
||||
- 부가 정리
|
||||
- `src/AxCopilot/Views/ChatWindow.ComposerQueuePresentation.cs`의 미사용 필드를 제거해 빌드 경고를 없앴습니다.
|
||||
|
||||
## 압축 경계에서 tool pair 불변식 보정 확대 (2026-04-12 21:39 KST)
|
||||
|
||||
- 전송 직전 query view에서만 pair를 보정하던 상태에서 한 단계 더 나아가, compact split 자체도 `tool_use / tool_result` 불변식을 보존하도록 정리했습니다.
|
||||
- `src/AxCopilot/Services/Agent/AgentMessageInvariantHelper.cs`
|
||||
- `tool_result`에서 `tool_use_id`를 읽고, assistant `_tool_use_blocks`에서 대응 `tool_use`를 찾는 공용 helper를 추가했습니다.
|
||||
- query view builder와 context condenser가 같은 보정 규칙을 공유하도록 맞췄습니다.
|
||||
- `src/AxCopilot/Services/Agent/ContextCondenser.cs`
|
||||
- `SplitCompactionWindow()`를 추가해 recent window를 자를 때 `AgentMessageInvariantHelper.AdjustStartIndexForToolPairs()`를 먼저 적용합니다.
|
||||
- `MicrocompactOlderMessages`, `SessionMemoryCompactOlderMessages`, `CollapseAndSnipOlderMessages`, `SummarizeOldMessagesAsync`가 모두 같은 split helper를 사용하게 바꿨습니다.
|
||||
- 결과적으로 recent window 안에 `tool_result`가 남아 있는데 대응 `tool_use`는 old window로 밀려 요약/압축돼 버리는 상황을 줄였습니다.
|
||||
- 기대 효과
|
||||
- 자동 압축 직후 다음 API 호출에서 orphan `tool_result`로 인한 pairing 오류가 줄어듭니다.
|
||||
- query view 보정과 compaction split 보정이 같은 규칙을 쓰므로, 저장 상태와 전송 상태 사이의 불일치가 줄어듭니다.
|
||||
|
||||
## query view / 압축 1단계 tool_result budget 통일 (2026-04-12 22:02 KST)
|
||||
|
||||
- `claw-code`의 `toolResultStorage.applyToolResultBudget()`처럼, 오래된 `tool_result`를 줄이는 규칙을 query view와 본체 압축 단계가 공용으로 쓰도록 정리했습니다.
|
||||
- `src/AxCopilot/Services/Agent/AgentToolResultBudget.cs`
|
||||
- 최근 보호 구간과 aggregate character budget을 함께 적용하는 공용 helper를 추가했습니다.
|
||||
- `tool_result` JSON 축약도 같은 helper 안에서 처리해 query view와 compaction이 같은 축약 결과를 사용하도록 맞췄습니다.
|
||||
- `src/AxCopilot/Services/Agent/AgentQueryContextBuilder.cs`
|
||||
- 전송 전용 query view가 이제 `AgentToolResultBudget.Apply()`를 사용해 오래된 `tool_result`를 동일 규칙으로 줄입니다.
|
||||
- compact boundary 이후 window 계산과 pair invariant 보정은 유지하면서, budget 적용 결과만 공용화했습니다.
|
||||
- `src/AxCopilot/Services/Agent/ContextCondenser.cs`
|
||||
- 1단계 `TruncateToolResults()`가 오래된 `tool_result`를 직접 개별 문자열 규칙으로 자르지 않고, 공용 budget helper를 먼저 적용하도록 바꿨습니다.
|
||||
- 이후 단계는 긴 assistant `_tool_use_blocks`와 일반 긴 텍스트만 추가 절단해 역할이 겹치지 않게 정리했습니다.
|
||||
- 기대 효과
|
||||
- query view와 저장 상태 사이에서 `tool_result` 축약 기준이 달라 생기던 흔들림이 줄어듭니다.
|
||||
- 같은 세션을 반복 호출할 때 오래된 `tool_result`의 토큰 사용량이 더 예측 가능해집니다.
|
||||
|
||||
## time-based tool_result 정리 / post-compact 안내 축소 (2026-04-12 22:11 KST)
|
||||
|
||||
- `claw-code`의 `maybeTimeBasedMicrocompact()`처럼, 오래 쉬었다가 다시 호출할 때 오래된 `tool_result`를 먼저 비워 prompt 재전송량을 줄이는 분기를 AX에 추가했습니다.
|
||||
- `src/AxCopilot/Services/Agent/ContextCondenser.cs`
|
||||
- 마지막 assistant 응답 이후 20분 이상 경과했고 오래된 `tool_result`가 여러 개 남아 있으면, 가장 최근 1개를 제외한 나머지를 cleared marker로 교체합니다.
|
||||
- 이 단계는 일반 토큰 한계 기반 compact보다 먼저 실행되어, 긴 휴지기 뒤 첫 호출의 prompt 부피를 줄이는 역할을 합니다.
|
||||
- `src/AxCopilot/Services/Agent/AgentToolResultBudget.cs`
|
||||
- 기존 budget 축약 외에 cleared `tool_result` JSON 생성 helper를 추가했습니다.
|
||||
- `tool_use_id`/`tool_name`은 유지하고 content만 작은 marker로 바꿔 pairing 정보는 잃지 않도록 했습니다.
|
||||
- `src/AxCopilot/Services/Agent/AgentLoopCompactionPolicy.cs`
|
||||
- compact 직후 첫 턴에서 `compact 이후 n번째 턴` 같은 운영성 thinking 문구를 다시 띄우지 않고 내부 상태만 갱신하도록 바꿨습니다.
|
||||
- 이미 `컨텍스트 압축 완료` 이벤트를 한 번 내보낸 뒤라, 추가 안내를 생략해 `claw-code`처럼 compact 후 흐름이 더 자연스럽게 이어지도록 맞췄습니다.
|
||||
- 기대 효과
|
||||
- 오래된 세션을 다시 이어갈 때 과거 `tool_result` 때문에 첫 요청이 불필요하게 비대해지는 현상이 줄어듭니다.
|
||||
- compact 직후 transcript에 운영 문구가 한 번 더 끼어드는 노이즈가 줄어듭니다.
|
||||
|
||||
## 모델별 time-based 기준 / compact 메타 카드 경량화 (2026-04-12 22:19 KST)
|
||||
|
||||
- `claw-code`의 `timeBasedMCConfig`처럼 하나의 고정값 대신, AX도 모델/서비스별로 time-based tool result 정리 기준을 다르게 적용하도록 조정했습니다.
|
||||
- `src/AxCopilot/Services/Agent/ContextCondenser.cs`
|
||||
- Claude: `60분 / 최근 5개 유지`
|
||||
- Gemini, GPT-4: `45분 / 최근 3개 유지`
|
||||
- DeepSeek: `30분 / 최근 2개 유지`
|
||||
- Qwen, LLaMA, vLLM 계열: `20분 / 최근 1개 유지`
|
||||
- 기본값: `30분 / 최근 2개 유지`
|
||||
- 이렇게 분리해 긴 캐시 TTL과 큰 컨텍스트를 가진 모델은 덜 공격적으로, 로컬/vLLM 계열은 더 빠르게 오래된 `tool_result`를 비우도록 맞췄습니다.
|
||||
- `src/AxCopilot/Views/ChatWindow.TimelinePresentation.cs`
|
||||
- compact 메타 카드를 긴 line-by-line 상세 표시 대신 제목 + 짧은 한 줄 설명으로 단순화했습니다.
|
||||
- `run id`와 내부 경계성 문구를 transcript에 다시 노출하지 않아 compact 메타가 일반 assistant 응답 흐름을 덜 끊게 했습니다.
|
||||
|
||||
## compact 후행 메타 정리 (2026-04-12 22:27 KST)
|
||||
|
||||
- compact 직후 상태를 기록하는 내부 플래그는 유지하되, 사용자에게 보이는 응답 메타와 usage 팝업에서는 후행 운영 문구를 더 줄였습니다.
|
||||
- `src/AxCopilot/Views/ChatWindow.ResponsePresentation.cs`
|
||||
- 응답 하단 토큰 메타에서 `compact 직후` 꼬리표를 제거했습니다.
|
||||
- 실행/토큰 정보는 유지하면서도, 일반 응답과 운영 메타 응답이 시각적으로 다르게 보이지 않도록 맞췄습니다.
|
||||
- `src/AxCopilot/Views/ChatWindow.ContextUsagePresentation.cs`
|
||||
- token usage 팝업의 detail 줄에서 `compact 후 첫 응답 대기 중` 문구를 제거하고, 실제 컨텍스트 사용량/압축 정보만 남겼습니다.
|
||||
- 기대 효과
|
||||
- compact 이후에도 transcript와 usage UI가 일반 응답과 더 비슷한 밀도로 이어집니다.
|
||||
- 운영 상태는 내부 로직/통계에만 남기고, 사용자 화면은 더 `claw-code` 스타일의 얇은 표현에 가까워집니다.
|
||||
|
||||
## compact 후 첨부 참조 재주입 보강 (2026-04-12 22:36 KST)
|
||||
|
||||
- `claw-code`의 `buildPostCompactMessages()`가 attachments를 boundary/summary 뒤에 다시 붙이는 흐름을 참고해, AX도 compact 이후 첨부 참조가 완전히 사라지지 않도록 보강했습니다.
|
||||
- `src/AxCopilot/Services/Agent/ContextCondenser.cs`
|
||||
- 오래된 메시지 구간에서 `AttachedFiles`와 `Images` 개수를 수집하는 helper를 추가했습니다.
|
||||
- `BuildMicrocompactBoundary()`는 compact 경계 메시지에 관련 파일 목록과 이미지 개수를 함께 요약하고, `AttachedFiles`도 메타 메시지에 유지합니다.
|
||||
- `SummarizeOldMessagesAsync()`는 요약 메시지 하단에 `참고 파일`, `참고 이미지` 줄을 추가하고, `AttachedFiles`도 요약 메시지에 보존합니다.
|
||||
- 기대 효과
|
||||
- compact 이후에도 “이 대화가 어떤 파일/이미지를 참고했는지”가 요약 메시지에서 다시 드러납니다.
|
||||
- query view가 compact 이후 메시지를 다시 보낼 때, 파일 참조 continuity가 이전보다 더 자연스럽게 유지됩니다.
|
||||
|
||||
## OpenAI/vLLM tool history 직렬화 경량화 (2026-04-12 22:44 KST)
|
||||
|
||||
- `claw-code`가 최근 trajectory만 구조적으로 유지하고 오래된 tool history는 더 가볍게 다루는 방향을 참고해, AX도 OpenAI/vLLM 호환 요청 바디에서 오래된 tool history를 평탄화하도록 조정했습니다.
|
||||
- `src/AxCopilot/Services/LlmService.ToolUse.cs`
|
||||
- 최근 비-system 메시지 8개는 기존처럼 `assistant tool_calls` + `tool` 메시지 형식을 유지합니다.
|
||||
- 그보다 오래된 `_tool_use_blocks`는 plain assistant transcript로, `tool_result`는 plain user transcript로 변환해 재전송합니다.
|
||||
- 경계 계산에는 `AgentMessageInvariantHelper.AdjustStartIndexForToolPairs()`를 사용해 최근 구조화 구간의 pair invariant는 유지했습니다.
|
||||
- 기대 효과
|
||||
- OpenAI/vLLM 호환 서버에 오래된 구조화 tool history를 계속 실어 보내던 부담이 줄어듭니다.
|
||||
- strict tool sequence 검사에 걸릴 가능성을 낮추면서도 최근 실행 흐름은 그대로 유지할 수 있습니다.
|
||||
|
||||
## Cowork 문서 생성 게이트 / Code 검증 게이트 경량화 (2026-04-12 23:05 KST)
|
||||
|
||||
- `claw-code`와 비교했을 때 AX는 `document_plan` 후속 재시도와 Code 후속 증거 게이트가 여전히 더 무거웠습니다.
|
||||
- `src/AxCopilot/Services/Agent/ModelExecutionProfileCatalog.cs`
|
||||
- `balanced` 프로필의 `ForceToolCallAfterPlan`을 끄고 `DocumentPlanRetryMax`를 0으로 낮췄습니다.
|
||||
- `tool_call_strict`도 `DocumentPlanRetryMax`를 0으로 낮추고 `PreferAggressiveDocumentFallback`을 꺼, 강제 문서 fallback이 기본 경로로 개입하지 않게 했습니다.
|
||||
- `src/AxCopilot/Services/Agent/AgentLoopService.cs`
|
||||
- `document_plan` 후 `html_create` 미호출 재시도와 자동 HTML 저장 fallback은 공격적 문서 fallback 프로필에서만 실행되도록 제한했습니다.
|
||||
- 따라서 일반 Cowork 문서 생성은 `claw-code`처럼 모델의 정상적인 다음 선택을 더 많이 허용합니다.
|
||||
- `src/AxCopilot/Services/Agent/AgentLoopTransitions.Documents.cs`
|
||||
- `document_plan` 성공 직후 강제로 다음 도구 호출을 재촉하는 user 메시지를 제거했습니다.
|
||||
- terminal 문서 도구가 성공하면 Cowork에서는 추가 반복 없이 바로 완료할 수 있게 정리했습니다.
|
||||
- `src/AxCopilot/Services/Agent/AgentLoopTransitions.Verification.cs`
|
||||
- `CodeDiffGate`, `RecentExecutionGate`, `ExecutionSuccessGate`를 review 작업 중심으로 제한해, 일반 bugfix/feature/refactor 작업에서는 과한 후속 검증 턴을 줄였습니다.
|
||||
- `src/AxCopilot/Services/Agent/TaskTypePolicy.cs`
|
||||
- docs 작업은 산출물이 분명한 경우 파일 생성을 우선하되, 분석/자문 성격 요청에는 텍스트 마무리도 허용하도록 정책 문구를 조정했습니다.
|
||||
- `src/AxCopilot/Views/ChatWindow.SystemPromptBuilder.cs`
|
||||
- `document_plan`은 멀티 섹션 구조화가 실제로 도움이 될 때만 쓰도록 톤을 낮췄습니다.
|
||||
- Code 최종 보고도 review/high-impact가 아닐 때는 남은 리스크를 과하게 언급하지 않도록 정리했습니다.
|
||||
- `src/AxCopilot/Assets/Presets/cowork_문서작성.json`
|
||||
- `src/AxCopilot/Assets/Presets/cowork_보고서.json`
|
||||
- Cowork preset에서 “무조건 파일 생성” 압력을 낮추고, 새 문서/새 보고서 요청은 생성 도구 우선, 기존 자료 참조 요청은 탐색 우선, 분석형 요청은 텍스트 종료 허용으로 기준을 분리했습니다.
|
||||
- 기대 효과
|
||||
- Cowork는 문서 생성 요청에서 불필요한 재시도와 앱 강제 fallback이 줄어듭니다.
|
||||
- Code는 일반 수정 작업에서 review 수준의 무거운 follow-up gate가 덜 개입합니다.
|
||||
- 프롬프트, preset, runtime policy가 같은 방향으로 정렬되어 `claw-code`와 더 비슷한 얇은 루프를 유지합니다.
|
||||
|
||||
## post-compact context 재주입 / 일반 final-report 경량화 (2026-04-12 23:14 KST)
|
||||
|
||||
- `claw-code`는 compact 뒤 attachment/tool/memory 상태를 다시 붙여 첫 post-compact turn이 맥락을 잃지 않게 만듭니다. AX는 파일명 continuity는 있었지만, query view에 “복원된 참조 상태”를 직접 실어주는 층이 약했습니다.
|
||||
- `src/AxCopilot/Services/Agent/AgentQueryContextBuilder.cs`
|
||||
- compact boundary가 적용된 query view에 `post_compact_context` system 메시지를 삽입합니다.
|
||||
- 이 메시지는 compact 이후 다시 이어져야 하는 `AttachedFiles`와 이미지 개수를 짧게 요약해, LLM이 첫 post-compact turn에서 참조 맥락을 더 쉽게 복원하도록 돕습니다.
|
||||
- `src/AxCopilot/Services/Agent/AgentLoopService.cs`
|
||||
- `BuildFinalReportQualityPrompt()`를 일반 작업과 review/high-impact 작업으로 분기했습니다.
|
||||
- 일반 작업은 `무엇을 변경했는지 / 무엇을 확인했는지 / 실제 미해결 이슈가 있을 때만 한 줄` 중심의 짧은 요약을 요구하고, review/high-impact만 기존의 구조적 상세 보고를 유지합니다.
|
||||
- 기대 효과
|
||||
- compact 직후 첫 query turn이 복원된 파일/이미지 참조를 더 안정적으로 이어받습니다.
|
||||
- 일반 Cowork/Code 작업의 최종 응답이 `claw-code`처럼 더 짧고 메타 밀도가 낮아집니다.
|
||||
|
||||
## post-compact 복원 메모 계층화 / compact UI 메타 축소 (2026-04-12 23:23 KST)
|
||||
|
||||
- `claw-code`는 compact 뒤 첫 요청에서 attachment/tool context를 다시 붙여주고, transcript에는 compact 운영 메타를 과하게 드러내지 않습니다. AX도 그 방향으로 한 단계 더 정리했습니다.
|
||||
- `src/AxCopilot/Services/Agent/AgentQueryContextBuilder.cs`
|
||||
- `post_compact_context` system 메시지에 단순 파일/이미지 참조뿐 아니라 `restored compact summaries`, `restored tool history blocks`도 함께 넣어, compact 뒤 첫 query turn이 어떤 종류의 맥락을 이어받는지 더 명확히 전달합니다.
|
||||
- `src/AxCopilot/Views/ChatWindow.TimelinePresentation.cs`
|
||||
- compact 메타 카드는 이전보다 더 짧은 요약 한 줄과 첨부 파일 수 정도만 보여주도록 줄였습니다.
|
||||
- 운영 설명을 줄이고 transcript 안에서 일반 메시지 흐름을 덜 방해하게 맞췄습니다.
|
||||
- `src/AxCopilot/Views/ChatWindow.ContextUsagePresentation.cs`
|
||||
- 컨텍스트 사용 카드와 팝업의 compact 관련 문구를 짧고 읽기 쉬운 한국어 표현으로 정리했습니다.
|
||||
- 마지막 압축 정보는 `자동/수동 압축 A -> B`, 누적 정보는 `누적 압축 N회 · 절감 X` 형태만 남겨 과한 운영 메타를 줄였습니다.
|
||||
- 기대 효과
|
||||
- compact 뒤 첫 LLM 호출이 복원된 맥락의 종류를 더 안정적으로 전달받습니다.
|
||||
- transcript와 usage UI가 `claw-code`처럼 더 얇고 조용한 운영 메타 표현을 유지합니다.
|
||||
|
||||
## claude-code식 provider/compact/UI 후속 정렬
|
||||
|
||||
- 업데이트: 2026-04-12 23:45 (KST)
|
||||
- `LlmService.ToolUse`에 OpenAI 호환 provider별 도구 호출 호환 프로파일을 추가했습니다. Qwen·LLaMA·DeepSeek 계열 vLLM은 최근 structured tool history 범위를 더 작게 잡고, `parallel_tool_calls`와 `reasoning_effort` 전송을 더 보수적으로 사용합니다.
|
||||
- OpenAI 호환 도구 호출 재시도도 확장했습니다. IBM 배포형만이 아니라 일반 OpenAI 호환 경로에서도 `tool_choice`가 400으로 거부되면 `tool_choice` 없이 plain-text tool-only 지시를 덧붙인 fallback body로 한 번 더 재시도합니다.
|
||||
- `AgentQueryContextBuilder`의 post-compact context에는 compact summary, file/image refs 외에 `branch_context`와 최근 tool state를 함께 싣도록 보강했습니다. compact 직후 첫 턴에서 요약/분기/도구 맥락이 더 자연스럽게 이어지도록 맞춘 변경입니다.
|
||||
- `AgentLoopService`의 최종 보고 프롬프트는 `AgentUiExpressionLevel`을 반영합니다. `simple`은 매우 짧은 종료 보고, `balanced`는 기본 요약, `rich`는 review/high-impact 중심의 구조화 보고로 밀도를 달리합니다.
|
||||
- `ChatWindow.TimelinePresentation`, `ChatWindow.ContextUsagePresentation`도 표현 수준에 맞춰 compact 메타 노출량을 다르게 조정했습니다. `simple`은 짧은 한 줄 위주, `rich`는 필요한 경우 compact preview를 덧붙여 보여줍니다.
|
||||
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\` 경고 0 / 오류 0
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Forms;
|
||||
using Microsoft.Win32;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using AxCopilot.Core;
|
||||
using AxCopilot.Handlers;
|
||||
using AxCopilot.Services;
|
||||
@@ -119,6 +120,9 @@ public partial class App : System.Windows.Application
|
||||
_appState.AttachChatSession(_chatSessionState);
|
||||
_appState.LoadFromSettings(settings);
|
||||
|
||||
// ─── DI 컨테이너 초기화 (기존 인스턴스 등록, 점진적 전환) ──────────────
|
||||
ConfigureDependencyInjection(settings, _memoryService, _chatSessionState, _appState);
|
||||
|
||||
_indexService = new IndexService(settings);
|
||||
var indexService = _indexService;
|
||||
// 캐시 로드를 백그라운드에서 실행 — UI 스레드 블록 방지
|
||||
@@ -1008,4 +1012,32 @@ public partial class App : System.Windows.Application
|
||||
|
||||
_fileDialogWatcher.Stop();
|
||||
}
|
||||
|
||||
// ─── DI 컨테이너 구성 ─────────────────────────────────────────────────────
|
||||
// 기존 수동 생성 인스턴스를 DI 컨테이너에 등록합니다.
|
||||
// 향후 ViewModel/Window도 DI에서 resolve할 수 있도록 점진 확장합니다.
|
||||
private static void ConfigureDependencyInjection(
|
||||
SettingsService settings,
|
||||
AgentMemoryService memoryService,
|
||||
ChatSessionStateService chatSessionState,
|
||||
AppStateService appState)
|
||||
{
|
||||
ServiceLocator.Configure(services =>
|
||||
{
|
||||
// ── Singleton: 기존 인스턴스 등록 ────────────────────────────────
|
||||
services.AddSingleton<ISettingsService>(settings);
|
||||
services.AddSingleton(settings); // 하위 호환: 구체 타입 직접 주입도 허용
|
||||
services.AddSingleton<IChatStorageService, ChatStorageService>();
|
||||
services.AddSingleton<IAppStateService>(appState);
|
||||
services.AddSingleton(appState);
|
||||
services.AddSingleton(chatSessionState);
|
||||
services.AddSingleton(memoryService);
|
||||
|
||||
// ── Transient: 요청마다 새 인스턴스 ──────────────────────────────
|
||||
services.AddTransient<ILlmService>(sp => new LlmService(sp.GetRequiredService<SettingsService>()));
|
||||
services.AddSingleton<IModelRouterService>(sp => new ModelRouterService(sp.GetRequiredService<SettingsService>()));
|
||||
});
|
||||
|
||||
LogService.Info("DI 컨테이너 초기화 완료");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,6 @@
|
||||
"symbol": "\uE943",
|
||||
"color": "#3B82F6",
|
||||
"description": "새 기능 개발, 코드 작성, 프로젝트 구성",
|
||||
"systemPrompt": "당신은 AX Copilot Code Agent — 사내 소프트웨어 개발 전문 에이전트입니다.\n\n## 역할\n새 기능 개발, 코드 작성, 프로젝트 구성을 담당합니다.\n\n## 워크플로우\n1. dev_env_detect로 설치된 개발 도구 확인\n2. folder_map + grep으로 기존 코드베이스 구조 분석\n3. 기존 코드 패턴과 컨벤션을 파악 (네이밍, 아키텍처, 의존성)\n4. 단계별 구현 계획을 사용자에게 제시\n5. 승인 후 file_write/file_edit으로 코드 작성\n6. build_run으로 빌드 및 테스트 검증\n\n## 핵심 원칙\n- 기존 코드 스타일과 아키텍처 패턴을 따르세요\n- SOLID 원칙과 DRY 원칙을 준수하세요\n- 적절한 에러 처리와 로깅을 포함하세요\n- 의미 있는 변수/함수 이름을 사용하세요\n- 복잡한 로직에는 주석을 추가하세요\n- 새 의존성 추가 시 사내 Nexus 저장소를 우선 사용하세요",
|
||||
"systemPrompt": "당신은 AX Copilot Code Agent — 사내 소프트웨어 개발 전문 에이전트입니다.\n\n## 역할\n새 기능 개발, 코드 작성, 프로젝트 구성을 담당합니다.\n\n## 워크플로우\n1. dev_env_detect로 설치된 개발 도구 확인\n2. grep/glob으로 관련 파일과 심볼을 먼저 좁히기\n3. file_read로 기존 코드 패턴과 컨벤션을 파악 (네이밍, 아키텍처, 의존성)\n4. 필요할 때만 계획을 제시하고, 바로 file_edit/file_write로 구현\n5. git diff, build_run, test_loop로 결과 검증\n6. folder_map은 폴더 구조 자체가 필요할 때만 사용\n\n## 핵심 원칙\n- 기존 코드 스타일과 아키텍처 패턴을 따르세요\n- SOLID 원칙과 DRY 원칙을 준수하세요\n- 적절한 에러 처리와 로깅을 포함하세요\n- 의미 있는 변수/함수 이름을 사용하세요\n- 복잡한 로직에는 주석을 추가하세요\n- 새 의존성 추가 시 사내 Nexus 저장소를 우선 사용하세요",
|
||||
"placeholder": "어떤 기능을 개발할까요? (프로젝트 폴더를 먼저 선택하세요)"
|
||||
}
|
||||
|
||||
@@ -6,6 +6,6 @@
|
||||
"symbol": "\uE71B",
|
||||
"color": "#10B981",
|
||||
"description": "코드 품질 분석, 모범 사례 검토, 개선 제안",
|
||||
"systemPrompt": "당신은 AX Copilot Code Reviewer — 코드 품질 분석 전문 에이전트입니다.\n\n## 역할\n코드 리뷰를 수행하여 품질, 가독성, 유지보수성, 성능을 평가합니다.\n\n## 리뷰 관점 (Google Code Review 가이드 기반)\n1. **정확성**: 논리 오류, 경계 조건, null 처리\n2. **가독성**: 네이밍, 주석, 코드 구조\n3. **유지보수성**: 결합도, 응집도, 확장성\n4. **성능**: 불필요한 연산, 메모리 누수, N+1 쿼리\n5. **보안**: 입력 검증, SQL 인젝션, XSS, 하드코딩된 시크릿\n6. **테스트**: 테스트 커버리지, 엣지 케이스\n\n## 워크플로우\n1. folder_map으로 프로젝트 전체 구조 파악\n2. 대상 파일을 file_read로 꼼꼼히 읽기\n3. 관련 파일도 grep/glob으로 확인 (의존성, 호출 관계)\n4. 이슈별로 분류하여 리뷰 의견 제시:\n - [CRITICAL] 반드시 수정해야 하는 문제\n - [WARNING] 개선을 권장하는 부분\n - [INFO] 참고 사항\n - [GOOD] 잘 작성된 부분 (칭찬)\n5. 전체 코드 품질을 A~F 등급으로 평가\n6. 개선 우선순위 제안\n\n## 출력 형식\n리뷰 결과를 구조화된 보고서로 작성하세요:\n- 파일별 이슈 목록 (라인 번호 포함)\n- 종합 평가 및 등급\n- 개선 액션 플랜",
|
||||
"systemPrompt": "당신은 AX Copilot Code Reviewer — 코드 품질 분석 전문 에이전트입니다.\n\n## 역할\n코드 리뷰를 수행하여 품질, 가독성, 유지보수성, 성능을 평가합니다.\n\n## 리뷰 관점 (Google Code Review 가이드 기반)\n1. **정확성**: 논리 오류, 경계 조건, null 처리\n2. **가독성**: 네이밍, 주석, 코드 구조\n3. **유지보수성**: 결합도, 응집도, 확장성\n4. **성능**: 불필요한 연산, 메모리 누수, N+1 쿼리\n5. **보안**: 입력 검증, SQL 인젝션, XSS, 하드코딩된 시크릿\n6. **테스트**: 테스트 커버리지, 엣지 케이스\n\n## 워크플로우\n1. grep/glob으로 대상 파일과 관련 호출부를 먼저 좁히기\n2. 대상 파일을 file_read로 꼼꼼히 읽기\n3. 필요 시 git diff와 추가 file_read로 근거 보강\n4. 이슈별로 분류하여 리뷰 의견 제시:\n - [CRITICAL] 반드시 수정해야 하는 문제\n - [WARNING] 개선을 권장하는 부분\n - [INFO] 참고 사항\n - [GOOD] 잘 작성된 부분 (칭찬)\n5. 전체 코드 품질을 A~F 등급으로 평가\n6. 개선 우선순위 제안\n\n## 출력 형식\n리뷰 결과를 구조화된 보고서로 작성하세요:\n- 파일별 이슈 목록 (라인 번호 포함)\n- 종합 평가 및 등급\n- 개선 액션 플랜",
|
||||
"placeholder": "어떤 코드를 리뷰할까요?"
|
||||
}
|
||||
|
||||
@@ -6,6 +6,6 @@
|
||||
"symbol": "\uE777",
|
||||
"color": "#6366F1",
|
||||
"description": "코드 구조 개선, 중복 제거, 성능 최적화",
|
||||
"systemPrompt": "당신은 AX Copilot Refactoring Agent — 코드 품질 개선 전문 에이전트입니다.\n\n## 역할\n기존 코드의 구조를 개선하고 기술 부채를 줄이는 리팩터링을 수행합니다.\n\n## 리팩터링 원칙 (Martin Fowler 기반)\n- Extract Method: 긴 메서드를 의미 단위로 분리\n- Move Method/Field: 응집도가 높은 클래스로 이동\n- Replace Conditional with Polymorphism: 복잡한 조건문을 다형성으로\n- Introduce Parameter Object: 관련 파라미터 묶기\n- Replace Magic Number with Symbolic Constant\n\n## 워크플로우\n1. folder_map + grep으로 대상 코드 구조 분석\n2. 코드 스멜(Code Smell) 식별:\n - Long Method, Large Class, Feature Envy\n - Duplicate Code, Dead Code\n - God Object, Shotgun Surgery\n3. 리팩터링 계획을 사용자에게 제시 (변경 전/후 설명)\n4. 승인 후 file_edit으로 점진적 수정 (한 번에 하나의 리팩터링)\n5. 각 단계마다 build_run으로 빌드/테스트 검증\n6. 동작 변경 없이 구조만 개선되었는지 확인\n\n## 주의사항\n- 기능을 변경하지 마세요 (행동 보존 리팩터링)\n- 테스트가 있으면 테스트를 먼저 실행하여 기준선 확보\n- 대규모 변경은 단계별로 나누어 진행\n- 변경 사항을 명확히 설명하세요",
|
||||
"systemPrompt": "당신은 AX Copilot Refactoring Agent — 코드 품질 개선 전문 에이전트입니다.\n\n## 역할\n기존 코드의 구조를 개선하고 기술 부채를 줄이는 리팩터링을 수행합니다.\n\n## 리팩터링 원칙 (Martin Fowler 기반)\n- Extract Method: 긴 메서드를 의미 단위로 분리\n- Move Method/Field: 응집도가 높은 클래스로 이동\n- Replace Conditional with Polymorphism: 복잡한 조건문을 다형성으로\n- Introduce Parameter Object: 관련 파라미터 묶기\n- Replace Magic Number with Symbolic Constant\n\n## 워크플로우\n1. grep/glob으로 대상 코드와 호출 범위를 먼저 좁히기\n2. file_read로 코드 스멜(Code Smell) 식별\n - Long Method, Large Class, Feature Envy\n - Duplicate Code, Dead Code\n - God Object, Shotgun Surgery\n3. 리팩터링 계획을 내부적으로 정리하고 필요할 때만 제시\n4. file_edit으로 점진적 수정 (한 번에 하나의 리팩터링)\n5. 각 단계마다 git diff, build_run, test_loop로 검증\n6. 동작 변경 없이 구조만 개선되었는지 확인\n\n## 주의사항\n- 기능을 변경하지 마세요 (행동 보존 리팩터링)\n- 테스트가 있으면 테스트를 먼저 실행하여 기준선 확보\n- 대규모 변경은 단계별로 나누어 진행\n- 변경 사항을 명확히 설명하세요",
|
||||
"placeholder": "어떤 코드를 리팩터링할까요?"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
{
|
||||
"category": "문서",
|
||||
"tab": "Cowork",
|
||||
"order": 60,
|
||||
@@ -6,6 +6,6 @@
|
||||
"symbol": "\uE8A5",
|
||||
"color": "#F59E0B",
|
||||
"description": "Word, Markdown, HTML 문서를 작성합니다",
|
||||
"systemPrompt": "당신은 AX Copilot Agent입니다. 사용자가 요청한 문서를 작성합니다.\n\n## 핵심 원칙\n- 문서 내용을 **상세하고 완결성 있게** 작성합니다.\n- 목차, 소제목, 번호 매기기를 활용하여 구조화합니다.\n- 전문 용어에는 간단한 설명을 병기합니다.\n- 결과물은 Word(.docx), Markdown(.md), HTML(.html) 중 적합한 형식을 선택합니다.\n- 작업 전 계획을 설명하고 도구를 사용하여 파일을 생성합니다.\n\n## 문서 품질 가이드\n\n### HTML 문서 (html_create)\n- **toc: true** 로 목차 자동 생성. **numbered: true** 로 섹션 번호 자동 부여.\n- **cover** 파라미터로 커버 페이지 추가: {\"title\": \"...\", \"subtitle\": \"...\", \"author\": \"...\"}.\n- 콜아웃: <div class=\"callout callout-info\">핵심 내용</div> (info/warning/tip/danger/note).\n- 배지: <span class=\"badge badge-blue\">완료</span>.\n- 타임라인: <div class=\"timeline\"><div class=\"timeline-item\">...</div></div>.\n- mood 추천: professional(공식), elegant(격식), minimal(학술), magazine(뉴스레터).\n\n### Word 문서 (docx_create)\n- **header** 파라미터로 머리글 추가. **footer** 에 {page}로 페이지 번호 삽입.\n- sections에서 type: \"table\" 로 스타일 테이블 (파란 헤더, 줄무늬).\n- type: \"pagebreak\" 로 섹션 간 페이지 구분.\n- type: \"list\" 로 번호/불릿 목록: {\"type\": \"list\", \"style\": \"number\", \"items\": [...]}.\n- 본문에 **볼드**, *이탤릭*, `코드` 인라인 서식 지원.\n- level: 1(대제목) / 2(소제목) 로 제목 크기 구분.\n\n## 사용 가능한 도구\n- docx_create: Word 문서 생성 (테이블, 서식, 머리글/바닥글, 페이지 나누기 지원)\n- markdown_create: Markdown 문서 생성\n- html_create: HTML 문서 생성 (목차, 커버, 콜아웃, 차트, 배지 지원)\n- document_read: 기존 문서(PDF, DOCX) 읽기\n- folder_map: 작업 폴더 구조 탐색\n- file_read: 텍스트 파일 읽기\n- file_write: 파일 생성\n- glob/grep: 파일 및 내용 검색\n- document_plan: 문서 개요 구조화 (멀티패스 생성)\n- document_assemble: 섹션별 내용을 하나의 문서로 조립\n- document_review: 생성된 문서 품질 검증\n- pptx_create: PowerPoint 프레젠테이션 생성\n- template_render: 템플릿 기반 문서 렌더링\n- text_summarize: 긴 텍스트 요약\n\n## 중요: 반드시 도구를 사용하여 파일을 생성하세요\n\n문서 요청을 받으면 텍스트로만 답변하지 마세요. 반드시 html_create, docx_create 등 도구를 호출하여 실제 파일을 생성해야 합니다.\n\n### 기본 전략 (빠른 생성)\n- html_create 또는 docx_create를 직접 호출하여 완성된 문서를 한 번에 생성합니다.\n- 문서 내용을 모두 포함하여 도구를 호출하세요. 개요만 텍스트로 작성하고 끝내지 마세요.\n\n### 멀티패스 전략 (고품질 설정 ON 시, 3페이지 이상)\n1단계 — document_plan으로 문서 구조를 설계합니다.\n2단계 — 각 섹션을 개별적으로 상세하게 작성합니다.\n3단계 — document_assemble으로 하나의 문서로 결합합니다.",
|
||||
"systemPrompt": "당신은 AX Copilot Agent입니다. 사용자가 요청한 문서를 작성합니다.\n\n## 핵심 원칙\n- 문서 내용을 **상세하고 완결성 있게** 작성합니다.\n- 목차, 소제목, 번호 매기기를 활용하여 구조화합니다.\n- 전문 용어에는 간단한 설명을 병기합니다.\n- 결과물은 Word(.docx), Markdown(.md), HTML(.html) 중 적합한 형식을 선택합니다.\n- 필요할 때만 계획을 드러내고, 도구를 사용하여 실제 파일을 생성합니다.\n- 파일 탐색은 glob/grep과 document_read/file_read를 우선하고, folder_map은 폴더 구조 확인이 필요할 때만 사용합니다.\n- 새 문서를 바로 작성하는 요청이면 탐색보다 생성 도구를 우선합니다.\n\n## 문서 품질 가이드\n\n### HTML 문서 (html_create)\n- **toc: true** 로 목차 자동 생성. **numbered: true** 로 섹션 번호 자동 부여.\n- **cover** 파라미터로 커버 페이지 추가: {\"title\": \"...\", \"subtitle\": \"...\", \"author\": \"...\"}.\n- 콜아웃: <div class=\"callout callout-info\">핵심 내용</div> (info/warning/tip/danger/note).\n- 배지: <span class=\"badge badge-blue\">완료</span>.\n- 타임라인: <div class=\"timeline\"><div class=\"timeline-item\">...</div></div>.\n- mood 추천: professional(공식), elegant(격식), minimal(학술), magazine(뉴스레터).\n\n### Word 문서 (docx_create)\n- **header** 파라미터로 머리글 추가. **footer** 에 {page}로 페이지 번호 삽입.\n- sections에서 type: \"table\" 로 스타일 테이블 (파란 헤더, 줄무늬).\n- type: \"pagebreak\" 로 섹션 간 페이지 구분.\n- type: \"list\" 로 번호/불릿 목록: {\"type\": \"list\", \"style\": \"number\", \"items\": [...]}.\n- 본문에 **볼드**, *이탤릭*, `코드` 인라인 서식 지원.\n- level: 1(대제목) / 2(소제목) 로 제목 크기 구분.\n\n## 사용 가능한 도구\n- docx_create: Word 문서 생성 (테이블, 서식, 머리글/바닥글, 페이지 나누기 지원)\n- markdown_create: Markdown 문서 생성\n- html_create: HTML 문서 생성 (목차, 커버, 콜아웃, 차트, 배지 지원)\n- document_read: 기존 문서(PDF, DOCX) 읽기\n- folder_map: 작업 폴더 구조 탐색\n- file_read: 텍스트 파일 읽기\n- file_write: 파일 생성\n- glob/grep: 파일 및 내용 검색\n- document_plan: 문서 개요 구조화 (멀티패스 생성)\n- document_assemble: 섹션별 내용을 하나의 문서로 조립\n- document_review: 생성된 문서 품질 검증\n- pptx_create: PowerPoint 프레젠테이션 생성\n- template_render: 템플릿 기반 문서 렌더링\n- text_summarize: 긴 텍스트 요약\n\n## 기본 전략\n- 새 문서를 작성하는 요청이면 html_create 또는 docx_create를 직접 호출하여 완성된 문서를 한 번에 생성합니다.\n- 기존 자료를 참고하라는 요청일 때만 glob/grep/document_read/file_read를 먼저 사용합니다.\n- 분석이나 조언이 목적이고 파일 생성이 핵심이 아니라면 텍스트 응답으로 마무리할 수 있습니다.\n\n### 멀티패스 전략 (고품질 설정 ON 시, 3페이지 이상)\n1단계 — 필요할 때만 document_plan으로 문서 구조를 설계합니다.\n2단계 — 각 섹션을 개별적으로 상세하게 작성합니다.\n3단계 — document_assemble으로 하나의 문서로 결합합니다.",
|
||||
"placeholder": "어떤 문서를 작성할까요? (예: 프로젝트 기획서 작성)"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
{
|
||||
"category": "보고서",
|
||||
"tab": "Cowork",
|
||||
"order": 10,
|
||||
@@ -6,6 +6,6 @@
|
||||
"symbol": "\uE9F9",
|
||||
"color": "#3B82F6",
|
||||
"description": "Excel, Word, HTML 보고서를 상세하게 작성합니다",
|
||||
"systemPrompt": "당신은 AX Copilot Agent입니다. 사용자가 요청한 보고서를 작성합니다.\n\n## 핵심 원칙\n- 데이터를 **상세하고 구체적으로** 작성합니다. 항목을 생략하지 않습니다.\n- 표(테이블)는 가능한 많은 행과 열을 포함합니다.\n- 수치 데이터는 단위를 명확히 표기합니다.\n- 결과물은 Excel(.xlsx), Word(.docx), HTML(.html) 중 가장 적합한 형식을 선택합니다.\n- 작업 전 계획을 설명하고 도구를 사용하여 파일을 생성합니다.\n\n## 문서 품질 가이드\n\n### HTML 보고서 (html_create)\n- **toc: true** 로 목차를 자동 생성하세요.\n- **numbered: true** 로 섹션 번호(1., 1-1.)를 자동 부여하세요.\n- **cover** 파라미터로 커버 페이지를 추가하세요: {\"title\": \"...\", \"subtitle\": \"...\", \"author\": \"...\"}.\n- 콜아웃을 활용하세요: <div class=\"callout callout-info\">중요 정보</div> (info/warning/tip/danger/note).\n- 배지를 활용하세요: <span class=\"badge badge-blue\">완료</span> (blue/green/red/yellow/purple/gray/orange).\n- CSS 바 차트: <div class=\"chart-bar\"><div class=\"bar-item\"><span class=\"bar-label\">항목</span><div class=\"bar-track\"><div class=\"bar-fill blue\" style=\"width:75%\">75%</div></div></div></div>.\n- 그리드 레이아웃: <div class=\"grid-2\"> 또는 grid-3, grid-4로 카드 배치.\n- mood 파라미터: professional(비즈니스), dashboard(KPI), corporate(공식), magazine(매거진) 등 선택.\n\n### Excel (excel_create)\n- 기본 style: 'styled' — 파란 헤더, 줄무늬, 테두리 자동 적용.\n- **freeze_header: true** 로 헤더 행 틀 고정.\n- **summary_row** 로 합계/평균 행 자동 생성: {\"label\": \"합계\", \"columns\": {\"B\": \"SUM\", \"C\": \"AVERAGE\"}}.\n- 수식은 셀 값에 '=SUM(B2:B10)' 형태로 입력.\n- **col_widths** 로 열 너비 지정: [20, 15, 12].\n\n### Word (docx_create)\n- **header/footer** 파라미터로 머리글/바닥글 추가. {page}로 페이지 번호.\n- sections에서 type: \"table\" 로 스타일 테이블 삽입 (파란 헤더, 줄무늬).\n- type: \"pagebreak\" 로 페이지 나누기.\n- type: \"list\" 로 번호/불릿 목록: {\"type\": \"list\", \"style\": \"number\", \"items\": [...]}.\n- 본문 텍스트에 **볼드**, *이탤릭*, `코드` 인라인 서식 사용 가능.\n\n## 사용 가능한 도구\n- excel_create: Excel 문서 생성 (서식, 수식, 틀 고정, 요약행 지원)\n- docx_create: Word 문서 생성 (테이블, 서식, 머리글/바닥글, 페이지 나누기 지원)\n- html_create: HTML 보고서 생성 (목차, 커버, 콜아웃, 차트, 배지 지원)\n- markdown_create: Markdown 문서 생성\n- csv_create: CSV 파일 생성\n- document_read: 기존 문서(PDF, DOCX, XLSX) 읽기\n- folder_map: 작업 폴더 구조 탐색\n- file_read: 텍스트 파일 읽기\n- file_write: 파일 생성\n- glob/grep: 파일 및 내용 검색\n- document_plan: 문서 개요 구조화 (멀티패스 생성 1단계)\n- document_assemble: 섹션별 내용을 하나의 문서로 조립 (멀티패스 생성 3단계)\n- document_review: 생성된 문서 품질 검증\n- pptx_create: PowerPoint 프레젠테이션 생성\n- data_pivot: CSV/JSON 데이터 집계/피벗\n- text_summarize: 긴 텍스트 요약\n\n## 중요: 반드시 도구를 사용하여 파일을 생성하세요\n\n보고서 요청을 받으면 텍스트로만 답변하지 마세요. 반드시 html_create, docx_create, excel_create 등 도구를 호출하여 실제 파일을 생성해야 합니다.\n\n### 기본 전략 (빠른 생성)\n- html_create 또는 docx_create를 직접 호출하여 완성된 보고서를 한 번에 생성합니다.\n- 보고서 내용을 모두 포함하여 도구를 호출하세요. 개요만 텍스트로 작성하고 끝내지 마세요.\n\n### 멀티패스 전략 (고품질 설정 ON 시, 3페이지 이상)\n1단계 — document_plan 도구로 문서 구조를 설계합니다.\n2단계 — 각 섹션을 개별적으로 상세하게 작성합니다.\n3단계 — document_assemble 도구로 하나의 문서로 결합합니다.",
|
||||
"systemPrompt": "당신은 AX Copilot Agent입니다. 사용자가 요청한 보고서를 작성합니다.\n\n## 핵심 원칙\n- 데이터를 **상세하고 구체적으로** 작성합니다. 항목을 생략하지 않습니다.\n- 표(테이블)는 가능한 많은 행과 열을 포함합니다.\n- 수치 데이터는 단위를 명확히 표기합니다.\n- 결과물은 Excel(.xlsx), Word(.docx), HTML(.html) 중 가장 적합한 형식을 선택합니다.\n- 필요할 때만 계획을 드러내고, 도구를 사용하여 실제 파일을 생성합니다.\n- 파일 탐색은 glob/grep과 document_read/file_read를 우선하고, folder_map은 폴더 구조 확인이 필요할 때만 사용합니다.\n- 새 보고서를 바로 작성하는 요청이면 탐색보다 생성 도구를 우선합니다.\n\n## 문서 품질 가이드\n\n### HTML 보고서 (html_create)\n- **toc: true** 로 목차를 자동 생성하세요.\n- **numbered: true** 로 섹션 번호(1., 1-1.)를 자동 부여하세요.\n- **cover** 파라미터로 커버 페이지를 추가하세요: {\"title\": \"...\", \"subtitle\": \"...\", \"author\": \"...\"}.\n- 콜아웃을 활용하세요: <div class=\"callout callout-info\">중요 정보</div> (info/warning/tip/danger/note).\n- 배지를 활용하세요: <span class=\"badge badge-blue\">완료</span> (blue/green/red/yellow/purple/gray/orange).\n- CSS 바 차트: <div class=\"chart-bar\"><div class=\"bar-item\"><span class=\"bar-label\">항목</span><div class=\"bar-track\"><div class=\"bar-fill blue\" style=\"width:75%\">75%</div></div></div></div>.\n- 그리드 레이아웃: <div class=\"grid-2\"> 또는 grid-3, grid-4로 카드 배치.\n- mood 파라미터: professional(비즈니스), dashboard(KPI), corporate(공식), magazine(매거진) 등 선택.\n\n### Excel (excel_create)\n- 기본 style: 'styled' — 파란 헤더, 줄무늬, 테두리 자동 적용.\n- **freeze_header: true** 로 헤더 행 틀 고정.\n- **summary_row** 로 합계/평균 행 자동 생성: {\"label\": \"합계\", \"columns\": {\"B\": \"SUM\", \"C\": \"AVERAGE\"}}.\n- 수식은 셀 값에 '=SUM(B2:B10)' 형태로 입력.\n- **col_widths** 로 열 너비 지정: [20, 15, 12].\n\n### Word (docx_create)\n- **header/footer** 파라미터로 머리글/바닥글 추가. {page}로 페이지 번호.\n- sections에서 type: \"table\" 로 스타일 테이블 삽입 (파란 헤더, 줄무늬).\n- type: \"pagebreak\" 로 페이지 나누기.\n- type: \"list\" 로 번호/불릿 목록: {\"type\": \"list\", \"style\": \"number\", \"items\": [...]}.\n- 본문 텍스트에 **볼드**, *이탤릭*, `코드` 인라인 서식 사용 가능.\n\n## 사용 가능한 도구\n- excel_create: Excel 문서 생성 (서식, 수식, 틀 고정, 요약행 지원)\n- docx_create: Word 문서 생성 (테이블, 서식, 머리글/바닥글, 페이지 나누기 지원)\n- html_create: HTML 보고서 생성 (목차, 커버, 콜아웃, 차트, 배지 지원)\n- markdown_create: Markdown 문서 생성\n- csv_create: CSV 파일 생성\n- document_read: 기존 문서(PDF, DOCX, XLSX) 읽기\n- folder_map: 작업 폴더 구조 탐색\n- file_read: 텍스트 파일 읽기\n- file_write: 파일 생성\n- glob/grep: 파일 및 내용 검색\n- document_plan: 문서 개요 구조화 (멀티패스 생성 1단계)\n- document_assemble: 섹션별 내용을 하나의 문서로 조립 (멀티패스 생성 3단계)\n- document_review: 생성된 문서 품질 검증\n- pptx_create: PowerPoint 프레젠테이션 생성\n- data_pivot: CSV/JSON 데이터 집계/피벗\n- text_summarize: 긴 텍스트 요약\n\n## 기본 전략\n- 새 보고서를 작성하는 요청이면 html_create, docx_create, excel_create 중 적합한 도구로 완성된 결과를 바로 생성합니다.\n- 기존 자료를 참고하라는 요청일 때만 glob/grep/document_read/file_read를 먼저 사용합니다.\n- 분석이나 설명이 목적이고 파일 생성이 핵심이 아니라면 텍스트 응답으로 마무리할 수 있습니다.\n\n### 멀티패스 전략 (고품질 설정 ON 시, 3페이지 이상)\n1단계 — 필요할 때만 document_plan 도구로 문서 구조를 설계합니다.\n2단계 — 각 섹션을 개별적으로 상세하게 작성합니다.\n3단계 — document_assemble 도구로 하나의 문서로 결합합니다.",
|
||||
"placeholder": "어떤 보고서를 작성할까요? (예: 삼성디스플레이 연혁 보고서)"
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 662 KiB |
|
After Width: | Height: | Size: 4.2 MiB |
|
After Width: | Height: | Size: 4.6 MiB |
|
After Width: | Height: | Size: 6.7 MiB |
|
After Width: | Height: | Size: 5.3 MiB |
|
After Width: | Height: | Size: 4.3 MiB |
|
After Width: | Height: | Size: 5.3 MiB |
|
After Width: | Height: | Size: 4.8 MiB |
|
After Width: | Height: | Size: 5.7 MiB |
|
After Width: | Height: | Size: 5.1 MiB |
|
After Width: | Height: | Size: 4.6 MiB |
@@ -0,0 +1,197 @@
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
|
||||
# 1. 색상 정의 (RGB 값) - 확장된 팔레트
|
||||
B = (20, 20, 20) # Background (Dark Gray)
|
||||
G1 = (121, 210, 180) # Mint Green (Main body)
|
||||
G2 = (100, 180, 160) # Mint Green (Shadow)
|
||||
W1 = (255, 255, 255) # White (Face/Belly)
|
||||
W2 = (240, 240, 240) # White (Shadow)
|
||||
P1 = (255, 180, 210) # Pink (Main Body)
|
||||
P2 = (255, 150, 190) # Pink (Fluffy texture detail)
|
||||
P3 = (240, 130, 170) # Pink (Shadow)
|
||||
E1 = (0, 0, 0) # Eyes (Black)
|
||||
E2 = (40, 40, 40) # Eyes (Pupil detail)
|
||||
C_RED = (255, 80, 80) # Ear pattern Red
|
||||
C_YLW = (255, 230, 80) # Ear pattern Yellow
|
||||
C_BLU = (80, 150, 255) # Ear pattern Blue
|
||||
C_PUR = (180, 80, 255) # Ear pattern Purple
|
||||
C_GRN = (80, 220, 120) # Pink hair pattern Green
|
||||
ORNG = (255, 145, 77) # Feet (Orange)
|
||||
|
||||
# 2. 텍스트 그리기 함수 (픽셀 폰트)
|
||||
def draw_pixel_text(draw, text, position, font_size=16, color=(255, 255, 255)):
|
||||
# 실제 픽셀 폰트를 사용하려면 폰트 파일이 필요합니다.
|
||||
# 여기서는 Pillow의 기본 폰트를 사용하지만, 픽셀 폰트처럼 보이게 할 수 있습니다.
|
||||
# 더 좋은 결과를 위해 실제 .ttf 픽셀 폰트를 로드하는 것이 좋습니다.
|
||||
try:
|
||||
font = ImageFont.truetype("Arial.ttf", font_size) # 실제 폰트 파일 경로
|
||||
except IOError:
|
||||
font = ImageFont.load_default() # 폰트 파일이 없으면 기본 폰트 사용
|
||||
font_size = 12 # 기본 폰트 크기
|
||||
|
||||
draw.text(position, text, font=font, fill=color)
|
||||
|
||||
# 3. 캐릭터 그리기 함수 (개념적 블록 조합)
|
||||
def draw_pixel_bunny(draw, position, size, pose, is_detailed=True):
|
||||
# size: 캐릭터의 기본 크기 (예: 32x32)
|
||||
# is_detailed: 원본의 단순화된 스타일 대신 더 정교한 디테일을 추가할지 여부
|
||||
|
||||
x_off, y_off = position
|
||||
scale = size[0] // 32
|
||||
|
||||
# 기본 형태 그리기
|
||||
# 머리
|
||||
head_color = G1
|
||||
head_size = int(size[0] * 0.5)
|
||||
head_y = y_off + int(size[1] * 0.1)
|
||||
draw.ellipse((x_off + int(size[0]*0.25), head_y,
|
||||
x_off + int(size[0]*0.75), head_y + head_size),
|
||||
fill=head_color)
|
||||
|
||||
# 얼굴 흰 부분
|
||||
draw.ellipse((x_off + int(size[0]*0.3), head_y + int(head_size*0.1),
|
||||
x_off + int(size[0]*0.7), head_y + int(head_size*0.9)),
|
||||
fill=W1)
|
||||
|
||||
# 귀
|
||||
draw.rectangle((x_off + int(size[0]*0.3), head_y - int(size[1]*0.1),
|
||||
x_off + int(size[0]*0.4), head_y),
|
||||
fill=head_color)
|
||||
draw.rectangle((x_off + int(size[0]*0.6), head_y - int(size[1]*0.1),
|
||||
x_off + int(size[0]*0.7), head_y),
|
||||
fill=head_color)
|
||||
|
||||
# 귀 디테일 (원본의 X 패턴 대신 더 복잡한 줄무늬)
|
||||
if is_detailed:
|
||||
draw.rectangle((x_off + int(size[0]*0.31), head_y - int(size[1]*0.08),
|
||||
x_off + int(size[0]*0.39), head_y - int(size[1]*0.06)), fill=C_RED)
|
||||
draw.rectangle((x_off + int(size[0]*0.31), head_y - int(size[1]*0.06),
|
||||
x_off + int(size[0]*0.39), head_y - int(size[1]*0.04)), fill=C_YLW)
|
||||
draw.rectangle((x_off + int(size[0]*0.31), head_y - int(size[1]*0.04),
|
||||
x_off + int(size[0]*0.39), head_y - int(size[1]*0.02)), fill=C_BLU)
|
||||
draw.rectangle((x_off + int(size[0]*0.31), head_y - int(size[1]*0.02),
|
||||
x_off + int(size[0]*0.39), head_y), fill=C_PUR)
|
||||
|
||||
draw.rectangle((x_off + int(size[0]*0.61), head_y - int(size[1]*0.08),
|
||||
x_off + int(size[0]*0.69), head_y - int(size[1]*0.06)), fill=C_RED)
|
||||
draw.rectangle((x_off + int(size[0]*0.61), head_y - int(size[1]*0.06),
|
||||
x_off + int(size[0]*0.69), head_y - int(size[1]*0.04)), fill=C_YLW)
|
||||
draw.rectangle((x_off + int(size[0]*0.61), head_y - int(size[1]*0.04),
|
||||
x_off + int(size[0]*0.69), head_y - int(size[1]*0.02)), fill=C_BLU)
|
||||
draw.rectangle((x_off + int(size[0]*0.61), head_y - int(size[1]*0.02),
|
||||
x_off + int(size[0]*0.69), head_y), fill=C_PUR)
|
||||
|
||||
# 눈
|
||||
draw.ellipse((x_off + int(size[0]*0.4), head_y + int(head_size*0.3),
|
||||
x_off + int(size[0]*0.48), head_y + int(head_size*0.5)),
|
||||
fill=E1)
|
||||
draw.ellipse((x_off + int(size[0]*0.52), head_y + int(head_size*0.3),
|
||||
x_off + int(size[0]*0.6), head_y + int(head_size*0.5)),
|
||||
fill=E1)
|
||||
# 눈동자 디테일
|
||||
if is_detailed:
|
||||
draw.ellipse((x_off + int(size[0]*0.42), head_y + int(head_size*0.35),
|
||||
x_off + int(size[0]*0.46), head_y + int(head_size*0.45)),
|
||||
fill=W1)
|
||||
draw.ellipse((x_off + int(size[0]*0.54), head_y + int(head_size*0.35),
|
||||
x_off + int(size[0]*0.58), head_y + int(head_size*0.45)),
|
||||
fill=W1)
|
||||
|
||||
# 몸통
|
||||
draw.rectangle((x_off + int(size[0]*0.3), head_y + head_size,
|
||||
x_off + int(size[0]*0.7), head_y + head_size + int(size[1]*0.3)),
|
||||
fill=head_color)
|
||||
# 배 흰 부분
|
||||
draw.ellipse((x_off + int(size[0]*0.35), head_y + head_size + int(size[1]*0.05),
|
||||
x_off + int(size[0]*0.65), head_y + head_size + int(size[1]*0.25)),
|
||||
fill=W1)
|
||||
|
||||
# 다리
|
||||
draw.rectangle((x_off + int(size[0]*0.35), head_y + head_size + int(size[1]*0.3),
|
||||
x_off + int(size[0]*0.45), head_y + head_size + int(size[1]*0.4)),
|
||||
fill=head_color)
|
||||
draw.rectangle((x_off + int(size[0]*0.55), head_y + head_size + int(size[1]*0.3),
|
||||
x_off + int(size[0]*0.65), head_y + head_size + int(size[1]*0.4)),
|
||||
fill=head_color)
|
||||
# 발 (주황색)
|
||||
draw.rectangle((x_off + int(size[0]*0.35), head_y + head_size + int(size[1]*0.38),
|
||||
x_off + int(size[0]*0.45), head_y + head_size + int(size[1]*0.43)),
|
||||
fill=ORNG)
|
||||
draw.rectangle((x_off + int(size[0]*0.55), head_y + head_size + int(size[1]*0.38),
|
||||
x_off + int(size[0]*0.65), head_y + head_size + int(size[1]*0.43)),
|
||||
fill=ORNG)
|
||||
|
||||
# 측면 포즈일 경우 디테일 추가
|
||||
if pose == 'side':
|
||||
# 눈 모양 변경
|
||||
# ... 측면 포즈에 대한 더 많은 코드 ...
|
||||
pass
|
||||
|
||||
def draw_pixel_pink_fluffball(draw, position, size, pose, is_detailed=True):
|
||||
# draw_pixel_bunny와 유사한 구조, 하지만 털 질감을 추가
|
||||
x_off, y_off = position
|
||||
scale = size[0] // 32
|
||||
|
||||
# 머리 (털 질감)
|
||||
head_y = y_off + int(size[1] * 0.1)
|
||||
for _ in range(int(size[0]*0.5)):
|
||||
for _ in range(int(size[0]*0.5)):
|
||||
draw.point((x_off + int(size[0]*0.25) + _, head_y + _), fill=P1)
|
||||
|
||||
# ... 더 많은 털 질감과 디테일 코드 ...
|
||||
pass
|
||||
|
||||
# 4. 이미지 크기 정의 및 배경 생성
|
||||
canvas_width = 800
|
||||
canvas_height = 600
|
||||
img = Image.new('RGB', (canvas_width, canvas_height), color=B)
|
||||
draw = ImageDraw.Draw(img)
|
||||
|
||||
# 5. 이미지 구성
|
||||
|
||||
# 상단 제목 그리기
|
||||
draw_pixel_text(draw, "정교한 캐릭터 시트", (int(canvas_width*0.35), int(canvas_height*0.05)), font_size=32)
|
||||
|
||||
# 왼쪽 패널 그리기 (민트 토끼 캐릭터 '더 자세히' 업그레이드)
|
||||
panel_bunny_y = int(canvas_height*0.2)
|
||||
panel_title_bunny = "민트 토끼 슈트 캐릭터"
|
||||
draw_pixel_text(draw, panel_title_bunny, (int(canvas_width*0.1), panel_bunny_y), font_size=20)
|
||||
|
||||
bunny_size_detailed = (200, 200) # 원본보다 더 큰 크기로 그려서 디테일 표현
|
||||
bunny_pos_detailed = (int(canvas_width*0.1), panel_bunny_y + 40)
|
||||
draw_pixel_bunny(draw, bunny_pos_detailed, bunny_size_detailed, 'front', is_detailed=True)
|
||||
|
||||
draw_pixel_text(draw, "정면", (int(canvas_width*0.1 + bunny_size_detailed[0]*0.4), panel_bunny_y + 40 + bunny_size_detailed[1]), font_size=16)
|
||||
|
||||
bunny_pos_detailed_side = (int(canvas_width*0.1 + bunny_size_detailed[0] + 50), panel_bunny_y + 40)
|
||||
draw_pixel_bunny(draw, bunny_pos_detailed_side, bunny_size_detailed, 'side', is_detailed=True)
|
||||
|
||||
draw_pixel_text(draw, "측면 45도", (int(canvas_width*0.1 + bunny_size_detailed[0] + 50 + bunny_size_detailed[0]*0.3), panel_bunny_y + 40 + bunny_size_detailed[1]), font_size=16)
|
||||
|
||||
# 오른쪽 패널 그리기 (핑크 털뭉치 캐릭터 '더 자세히' 업그레이드)
|
||||
panel_pink_y = int(canvas_height*0.2)
|
||||
panel_title_pink = "핑크 털 뭉치 슈트 캐릭터"
|
||||
draw_pixel_text(draw, panel_title_pink, (int(canvas_width*0.5), panel_pink_y), font_size=20)
|
||||
|
||||
pink_size_detailed = (200, 200) # 원본보다 더 큰 크기로 그려서 디테일 표현
|
||||
pink_pos_detailed = (int(canvas_width*0.5), panel_pink_y + 40)
|
||||
draw_pixel_pink_fluffball(draw, pink_pos_detailed, pink_size_detailed, 'front', is_detailed=True)
|
||||
|
||||
draw_pixel_text(draw, "정면", (int(canvas_width*0.5 + pink_size_detailed[0]*0.4), panel_pink_y + 40 + pink_size_detailed[1]), font_size=16)
|
||||
|
||||
pink_pos_detailed_side = (int(canvas_width*0.5 + pink_size_detailed[0] + 50), panel_pink_y + 40)
|
||||
draw_pixel_pink_fluffball(draw, pink_pos_detailed_side, pink_size_detailed, 'side', is_detailed=True)
|
||||
|
||||
draw_pixel_text(draw, "측면 45도", (int(canvas_width*0.5 + pink_size_detailed[0] + 50 + pink_size_detailed[0]*0.3), panel_pink_y + 40 + pink_size_detailed[1]), font_size=16)
|
||||
|
||||
# 6. 보기 좋게 확대 및 픽셀 격자 표현 (옵션)
|
||||
# 픽셀 아트 격자 효과를 주려면, 이미지를 작게 그린 후 크게 확대합니다.
|
||||
# 이 코드는 개념적인 블록 그리기 방식을 사용하여 직접 디테일한 픽셀 아트를 생성하므로,
|
||||
# 실제 격자 확대 효과는 생략합니다.
|
||||
|
||||
# 7. 결과 저장 및 보여주기
|
||||
output_filename = "detailed_pixel_characters_sheet.png"
|
||||
img.save(output_filename)
|
||||
img.show()
|
||||
|
||||
print(f"정교한 캐릭터 시트 이미지가 '{output_filename}'으로 저장되었습니다. {canvas_width}x{canvas_height} 크기")
|
||||
@@ -68,6 +68,7 @@
|
||||
<!-- 클립보드 히스토리 DPAPI 암호화 (Windows 전용, 외부 네트워크 통신 없음) -->
|
||||
<!-- Windows 서비스 관리 (ServiceHandler — 로컬 전용, 외부 네트워크 통신 없음) -->
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
|
||||
<PackageReference Include="DocumentFormat.OpenXml" Version="3.2.0" />
|
||||
<PackageReference Include="Markdig" Version="0.37.0" />
|
||||
<PackageReference Include="Microsoft.Data.Sqlite" Version="8.0.0" />
|
||||
@@ -114,6 +115,11 @@
|
||||
<Resource Include="Assets\mascot.png" Condition="Exists('Assets\mascot.png')" />
|
||||
<Resource Include="Assets\mascot.jpg" Condition="Exists('Assets\mascot.jpg')" />
|
||||
<Resource Include="Assets\mascot.webp" Condition="Exists('Assets\mascot.webp')" />
|
||||
<Resource Include="Assets\foldy_qdy.png" Condition="Exists('Assets\foldy_qdy.png')" />
|
||||
<Resource Include="Assets\pixel_art.png" Condition="Exists('Assets\pixel_art.png')" />
|
||||
<!-- PPT 고품질 템플릿: 빌드에 포함하지 않음 (297MB).
|
||||
런타임에 Assets/ppt/ 또는 %APPDATA%/AXCopilot/templates/ppt/ 에서 검색.
|
||||
개발 환경에서는 소스의 Assets/ppt/를 자동으로 찾음. -->
|
||||
<!-- 검색 엔진 아이콘: 빌드 출력에 복사 (사내망에서 외부 favicon 다운로드 불가 대응) -->
|
||||
<Content Include="Assets\SearchEngines\*.png">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
@@ -125,6 +131,14 @@
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<!-- 마스코트 GIF 애니메이션: 빌드 출력 Assets/gif/ 폴더에 복사 -->
|
||||
<ItemGroup>
|
||||
<Content Include="Assets\gif\*.gif">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
<Link>Assets\gif\%(Filename)%(Extension)</Link>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<!-- 내장 스킬 파일: 빌드 출력 skills/ 폴더에 복사 -->
|
||||
<ItemGroup>
|
||||
<Content Include="skills\*.skill.md">
|
||||
|
||||
@@ -0,0 +1,422 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<AssemblyName>AxCopilot</AssemblyName>
|
||||
<IntermediateOutputPath>obj\Release\</IntermediateOutputPath>
|
||||
<BaseIntermediateOutputPath>obj\</BaseIntermediateOutputPath>
|
||||
<MSBuildProjectExtensionsPath>E:\AX Copilot - Claude\src\AxCopilot\obj\</MSBuildProjectExtensionsPath>
|
||||
<_TargetAssemblyProjectName>AxCopilot</_TargetAssemblyProjectName>
|
||||
<RootNamespace>AxCopilot</RootNamespace>
|
||||
</PropertyGroup>
|
||||
<Import Project="Sdk.props" Sdk="Microsoft.NET.Sdk" />
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net8.0-windows10.0.17763.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UseWPF>true</UseWPF>
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
<RootNamespace>AxCopilot</RootNamespace>
|
||||
<ApplicationIcon>Assets\icon.ico</ApplicationIcon>
|
||||
<!--
|
||||
★ 버전 관리 규칙 ★
|
||||
이 <Version> 값 하나만 변경하면 앱 전체에 자동 반영됩니다:
|
||||
- 설정 창 하단 버전 표시 (SettingsWindow.xaml.cs → SetVersionText())
|
||||
- Windows 속성 탭 파일 버전 / 제품 버전
|
||||
- DEVELOPMENT.md 변경 이력은 수동으로 함께 업데이트하세요.
|
||||
설정 스키마 버전(마이그레이션)은 Services/SettingsService.cs → CurrentSettingsVersion 을 별도로 관리합니다.
|
||||
-->
|
||||
<Version>0.7.3</Version>
|
||||
<Company>AX</Company>
|
||||
<Product>AX Copilot</Product>
|
||||
<Description>AI 기반 업무 자동화 런처 & 코파일럿</Description>
|
||||
<!-- ScreenCaptureHandler의 LockBits 포인터 연산에 필요 -->
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<!-- 배포: build.bat에서 self-contained / framework-dependent 두 종류로 publish -->
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
<!-- ─── 디컴파일 방지 ─────────────────────────────────────────────── -->
|
||||
<!-- 디버그 심볼 제거 (Release) -->
|
||||
<DebugType Condition="'$(Configuration)'=='Release'">none</DebugType>
|
||||
<DebugSymbols Condition="'$(Configuration)'=='Release'">false</DebugSymbols>
|
||||
<!-- 임베디드 소스 제거 -->
|
||||
<EmbedAllSources>false</EmbedAllSources>
|
||||
<!-- Suppress source link -->
|
||||
<EnableSourceLink>false</EnableSourceLink>
|
||||
<DeterministicSourcePaths>false</DeterministicSourcePaths>
|
||||
</PropertyGroup>
|
||||
<!-- Release 빌드 시 추가 난독화 설정 -->
|
||||
<PropertyGroup Condition="'$(Configuration)'=='Release'">
|
||||
<Optimize>true</Optimize>
|
||||
<!-- 사용하지 않는 멤버 제거 (IL trimming) -->
|
||||
<PublishTrimmed>false</PublishTrimmed>
|
||||
<!-- PDB 제거 -->
|
||||
<CopyOutputSymbolsToPublishDirectory>false</CopyOutputSymbolsToPublishDirectory>
|
||||
<!-- 배포판 보호 수준 강화 -->
|
||||
<PublishSingleFile>true</PublishSingleFile>
|
||||
<EnableCompressionInSingleFile>true</EnableCompressionInSingleFile>
|
||||
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
|
||||
<PublishReadyToRun>true</PublishReadyToRun>
|
||||
</PropertyGroup>
|
||||
<!-- UseWindowsForms의 암묵적 using(System.Windows.Forms)이 WPF의
|
||||
System.Windows.Input과 충돌(KeyEventArgs 모호성)하므로 전역 제거.
|
||||
System.Drawing도 전역 제거: System.Windows.Media의 Color/ColorConverter/FontFamily와
|
||||
모호한 참조 충돌 방지. Drawing 타입이 필요한 파일에서만 명시적 using으로 추가. -->
|
||||
<ItemGroup>
|
||||
<Using Remove="System.Windows.Forms" />
|
||||
<Using Remove="System.Drawing" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\AxCopilot.SDK\AxCopilot.SDK.csproj" />
|
||||
</ItemGroup>
|
||||
<!-- 클립보드 히스토리 DPAPI 암호화 (Windows 전용, 외부 네트워크 통신 없음) -->
|
||||
<!-- Windows 서비스 관리 (ServiceHandler — 로컬 전용, 외부 네트워크 통신 없음) -->
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
|
||||
<PackageReference Include="DocumentFormat.OpenXml" Version="3.2.0" />
|
||||
<PackageReference Include="Markdig" Version="0.37.0" />
|
||||
<PackageReference Include="Microsoft.Data.Sqlite" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.2903.40" />
|
||||
<PackageReference Include="QRCoder" Version="1.6.0" />
|
||||
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="8.0.0" />
|
||||
<PackageReference Include="System.ServiceProcess.ServiceController" Version="8.0.1" />
|
||||
<PackageReference Include="UglyToad.PdfPig" Version="1.7.0-custom-5" />
|
||||
</ItemGroup>
|
||||
<!-- Assets -->
|
||||
<ItemGroup>
|
||||
<!-- 가이드 원본 HTML: 편집용. 출력에 복사하지 않음 (암호화 버전 사용) -->
|
||||
<None Include="Assets\AX Copilot 사용가이드.htm" />
|
||||
<None Include="Assets\AX Copilot 개발자가이드.htm" />
|
||||
<!-- 암호화된 가이드: 빌드 출력에 복사 (앱 내장 뷰어로 복호화) -->
|
||||
<Content Include="Assets\guide_user.enc">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="Assets\guide_dev.enc">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<!-- 아이콘: 앱 내장 리소스 + 출력에도 복사 (트레이용) -->
|
||||
<None Update="Assets\icon.ico">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<!-- about.json: 빌드 시 exe에 내장되는 리소스. 런타임 파일 수정 불가. -->
|
||||
<!-- 격려/명언 문구: 빌드 시 내장. 인터넷 연결 불필요. -->
|
||||
<!-- 대화 주제 프리셋: 9종 시스템 프롬프트 (빌드 시 내장) -->
|
||||
<EmbeddedResource Include="Assets\Presets\*.json" />
|
||||
<!-- 마스코트 이미지: 내장 리소스 (EXE에 포함됨, 교체 시 재빌드 필요) -->
|
||||
<!-- PPT 고품질 템플릿: 빌드에 포함하지 않음 (297MB).
|
||||
런타임에 Assets/ppt/ 또는 %APPDATA%/AXCopilot/templates/ppt/ 에서 검색.
|
||||
개발 환경에서는 소스의 Assets/ppt/를 자동으로 찾음. -->
|
||||
<!-- 검색 엔진 아이콘: 빌드 출력에 복사 (사내망에서 외부 favicon 다운로드 불가 대응) -->
|
||||
<Content Include="Assets\SearchEngines\*.png">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<!-- 시스템 프롬프트: 빌드 출력에 복사 (동적 로딩) -->
|
||||
<Content Include="Assets\system_prompt.txt">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
<Link>system_prompt.txt</Link>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
<!-- 마스코트 GIF 애니메이션: 빌드 출력 Assets/gif/ 폴더에 복사 -->
|
||||
<ItemGroup>
|
||||
<Content Include="Assets\gif\*.gif">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
<Link>Assets\gif\%(Filename)%(Extension)</Link>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
<!-- 내장 스킬 파일: 빌드 출력 skills/ 폴더에 복사 -->
|
||||
<ItemGroup>
|
||||
<Content Include="skills\*.skill.md">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
<Link>skills\%(Filename)%(Extension)</Link>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
<!-- 테마 리소스 딕셔너리 -->
|
||||
<ItemGroup>
|
||||
</ItemGroup>
|
||||
<!-- 단위 테스트 프로젝트에서 internal 멤버 접근 허용 -->
|
||||
<ItemGroup>
|
||||
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
|
||||
<_Parameter1>AxCopilot.Tests</_Parameter1>
|
||||
</AssemblyAttribute>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\Accessibility.dll" />
|
||||
<ReferencePath Include="E:\AX Copilot - Claude\src\AxCopilot.SDK\bin\Release\net8.0-windows\AxCopilot.SDK.dll" />
|
||||
<ReferencePath Include="C:\Users\admin\.nuget\packages\documentformat.openxml\3.2.0\lib\net8.0\DocumentFormat.OpenXml.dll" />
|
||||
<ReferencePath Include="C:\Users\admin\.nuget\packages\documentformat.openxml.framework\3.2.0\lib\net8.0\DocumentFormat.OpenXml.Framework.dll" />
|
||||
<ReferencePath Include="C:\Users\admin\.nuget\packages\markdig\0.37.0\lib\net8.0\Markdig.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\Microsoft.CSharp.dll" />
|
||||
<ReferencePath Include="C:\Users\admin\.nuget\packages\microsoft.data.sqlite.core\8.0.0\lib\net8.0\Microsoft.Data.Sqlite.dll" />
|
||||
<ReferencePath Include="C:\Users\admin\.nuget\packages\microsoft.extensions.dependencyinjection.abstractions\8.0.2\lib\net8.0\Microsoft.Extensions.DependencyInjection.Abstractions.dll" />
|
||||
<ReferencePath Include="C:\Users\admin\.nuget\packages\microsoft.extensions.dependencyinjection\8.0.1\lib\net8.0\Microsoft.Extensions.DependencyInjection.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\Microsoft.VisualBasic.Core.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\Microsoft.VisualBasic.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\Microsoft.VisualBasic.Forms.dll" />
|
||||
<ReferencePath Include="C:\Users\admin\.nuget\packages\microsoft.web.webview2\1.0.2903.40\lib_manual\netcoreapp3.0\Microsoft.Web.WebView2.Core.dll" />
|
||||
<ReferencePath Include="C:\Users\admin\.nuget\packages\microsoft.web.webview2\1.0.2903.40\lib_manual\netcoreapp3.0\Microsoft.Web.WebView2.WinForms.dll" />
|
||||
<ReferencePath Include="C:\Users\admin\.nuget\packages\microsoft.web.webview2\1.0.2903.40\lib_manual\net5.0-windows10.0.17763.0\Microsoft.Web.WebView2.Wpf.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\Microsoft.Win32.Primitives.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\Microsoft.Win32.Registry.AccessControl.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\Microsoft.Win32.Registry.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\Microsoft.Win32.SystemEvents.dll" />
|
||||
<ReferencePath Include="C:\Users\admin\.nuget\packages\microsoft.windows.sdk.net.ref\10.0.17763.57\lib\net8.0\Microsoft.Windows.SDK.NET.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\mscorlib.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\netstandard.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\PresentationCore.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\PresentationFramework.Aero.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\PresentationFramework.Aero2.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\PresentationFramework.AeroLite.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\PresentationFramework.Classic.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\PresentationFramework.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\PresentationFramework.Luna.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\PresentationFramework.Royale.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\PresentationUI.dll" />
|
||||
<ReferencePath Include="C:\Users\admin\.nuget\packages\qrcoder\1.6.0\lib\net6.0-windows7.0\QRCoder.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\ReachFramework.dll" />
|
||||
<ReferencePath Include="C:\Users\admin\.nuget\packages\sqlitepclraw.bundle_e_sqlite3\2.1.6\lib\netstandard2.0\SQLitePCLRaw.batteries_v2.dll" />
|
||||
<ReferencePath Include="C:\Users\admin\.nuget\packages\sqlitepclraw.core\2.1.6\lib\netstandard2.0\SQLitePCLRaw.core.dll" />
|
||||
<ReferencePath Include="C:\Users\admin\.nuget\packages\sqlitepclraw.provider.e_sqlite3\2.1.6\lib\net6.0-windows7.0\SQLitePCLRaw.provider.e_sqlite3.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.AppContext.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Buffers.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\System.CodeDom.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Collections.Concurrent.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Collections.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Collections.Immutable.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Collections.NonGeneric.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Collections.Specialized.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.ComponentModel.Annotations.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.ComponentModel.DataAnnotations.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.ComponentModel.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.ComponentModel.EventBasedAsync.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.ComponentModel.Primitives.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.ComponentModel.TypeConverter.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\System.Configuration.ConfigurationManager.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Configuration.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Console.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Core.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Data.Common.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Data.DataSetExtensions.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Data.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\System.Design.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Diagnostics.Contracts.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Diagnostics.Debug.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Diagnostics.DiagnosticSource.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\System.Diagnostics.EventLog.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Diagnostics.FileVersionInfo.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\System.Diagnostics.PerformanceCounter.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Diagnostics.Process.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Diagnostics.StackTrace.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Diagnostics.TextWriterTraceListener.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Diagnostics.Tools.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Diagnostics.TraceSource.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Diagnostics.Tracing.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\System.DirectoryServices.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\System.Drawing.Common.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\System.Drawing.Design.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\System.Drawing.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Drawing.Primitives.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Dynamic.Runtime.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Formats.Asn1.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Formats.Tar.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Globalization.Calendars.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Globalization.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Globalization.Extensions.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.IO.Compression.Brotli.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.IO.Compression.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.IO.Compression.FileSystem.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.IO.Compression.ZipFile.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.IO.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.IO.FileSystem.AccessControl.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.IO.FileSystem.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.IO.FileSystem.DriveInfo.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.IO.FileSystem.Primitives.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.IO.FileSystem.Watcher.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.IO.IsolatedStorage.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.IO.MemoryMappedFiles.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\System.IO.Packaging.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.IO.Pipes.AccessControl.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.IO.Pipes.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.IO.UnmanagedMemoryStream.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Linq.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Linq.Expressions.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Linq.Parallel.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Linq.Queryable.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Memory.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Net.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Net.Http.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Net.Http.Json.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Net.HttpListener.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Net.Mail.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Net.NameResolution.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Net.NetworkInformation.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Net.Ping.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Net.Primitives.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Net.Quic.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Net.Requests.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Net.Security.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Net.ServicePoint.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Net.Sockets.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Net.WebClient.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Net.WebHeaderCollection.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Net.WebProxy.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Net.WebSockets.Client.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Net.WebSockets.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Numerics.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Numerics.Vectors.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.ObjectModel.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\System.Printing.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Reflection.DispatchProxy.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Reflection.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Reflection.Emit.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Reflection.Emit.ILGeneration.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Reflection.Emit.Lightweight.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Reflection.Extensions.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Reflection.Metadata.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Reflection.Primitives.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Reflection.TypeExtensions.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\System.Resources.Extensions.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Resources.Reader.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Resources.ResourceManager.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Resources.Writer.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Runtime.CompilerServices.Unsafe.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Runtime.CompilerServices.VisualC.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Runtime.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Runtime.Extensions.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Runtime.Handles.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Runtime.InteropServices.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Runtime.InteropServices.JavaScript.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Runtime.InteropServices.RuntimeInformation.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Runtime.Intrinsics.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Runtime.Loader.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Runtime.Numerics.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Runtime.Serialization.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Runtime.Serialization.Formatters.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Runtime.Serialization.Json.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Runtime.Serialization.Primitives.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Runtime.Serialization.Xml.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Security.AccessControl.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Security.Claims.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Security.Cryptography.Algorithms.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Security.Cryptography.Cng.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Security.Cryptography.Csp.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Security.Cryptography.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Security.Cryptography.Encoding.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Security.Cryptography.OpenSsl.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\System.Security.Cryptography.Pkcs.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Security.Cryptography.Primitives.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\System.Security.Cryptography.ProtectedData.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Security.Cryptography.X509Certificates.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\System.Security.Cryptography.Xml.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Security.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\System.Security.Permissions.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Security.Principal.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Security.Principal.Windows.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Security.SecureString.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.ServiceModel.Web.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.ServiceProcess.dll" />
|
||||
<ReferencePath Include="C:\Users\admin\.nuget\packages\system.serviceprocess.servicecontroller\8.0.1\lib\net8.0\System.ServiceProcess.ServiceController.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Text.Encoding.CodePages.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Text.Encoding.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Text.Encoding.Extensions.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Text.Encodings.Web.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Text.Json.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Text.RegularExpressions.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\System.Threading.AccessControl.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Threading.Channels.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Threading.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Threading.Overlapped.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Threading.Tasks.Dataflow.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Threading.Tasks.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Threading.Tasks.Extensions.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Threading.Tasks.Parallel.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Threading.Thread.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Threading.ThreadPool.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Threading.Timer.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Transactions.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Transactions.Local.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.ValueTuple.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Web.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Web.HttpUtility.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\System.Windows.Controls.Ribbon.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Windows.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\System.Windows.Extensions.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\System.Windows.Forms.Design.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\System.Windows.Forms.Design.Editors.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\System.Windows.Forms.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\System.Windows.Forms.Primitives.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\System.Windows.Input.Manipulations.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\System.Windows.Presentation.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\System.Xaml.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Xml.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Xml.Linq.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Xml.ReaderWriter.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Xml.Serialization.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Xml.XDocument.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Xml.XmlDocument.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Xml.XmlSerializer.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Xml.XPath.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Xml.XPath.XDocument.dll" />
|
||||
<ReferencePath Include="C:\Users\admin\.nuget\packages\uglytoad.pdfpig.core\1.7.0-custom-5\lib\net6.0\UglyToad.PdfPig.Core.dll" />
|
||||
<ReferencePath Include="C:\Users\admin\.nuget\packages\uglytoad.pdfpig\1.7.0-custom-5\lib\net6.0\UglyToad.PdfPig.dll" />
|
||||
<ReferencePath Include="C:\Users\admin\.nuget\packages\uglytoad.pdfpig.fonts\1.7.0-custom-5\lib\net6.0\UglyToad.PdfPig.Fonts.dll" />
|
||||
<ReferencePath Include="C:\Users\admin\.nuget\packages\uglytoad.pdfpig.tokenization\1.7.0-custom-5\lib\net6.0\UglyToad.PdfPig.Tokenization.dll" />
|
||||
<ReferencePath Include="C:\Users\admin\.nuget\packages\uglytoad.pdfpig.tokens\1.7.0-custom-5\lib\net6.0\UglyToad.PdfPig.Tokens.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\UIAutomationClient.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\UIAutomationClientSideProviders.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\UIAutomationProvider.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\UIAutomationTypes.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\WindowsBase.dll" />
|
||||
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\WindowsFormsIntegration.dll" />
|
||||
<ReferencePath Include="C:\Users\admin\.nuget\packages\microsoft.windows.sdk.net.ref\10.0.17763.57\lib\net8.0\WinRT.Runtime.dll" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="E:\AX Copilot - Claude\src\AxCopilot\obj\Release\net8.0-windows10.0.17763.0\win-x64\Views\AboutWindow.g.cs" />
|
||||
<Compile Include="E:\AX Copilot - Claude\src\AxCopilot\obj\Release\net8.0-windows10.0.17763.0\win-x64\Views\AgentSettingsWindow.g.cs" />
|
||||
<Compile Include="E:\AX Copilot - Claude\src\AxCopilot\obj\Release\net8.0-windows10.0.17763.0\win-x64\Views\AgentStatsDashboardWindow.g.cs" />
|
||||
<Compile Include="E:\AX Copilot - Claude\src\AxCopilot\obj\Release\net8.0-windows10.0.17763.0\win-x64\Views\BatchRenameWindow.g.cs" />
|
||||
<Compile Include="E:\AX Copilot - Claude\src\AxCopilot\obj\Release\net8.0-windows10.0.17763.0\win-x64\Views\ChatWindow.g.cs" />
|
||||
<Compile Include="E:\AX Copilot - Claude\src\AxCopilot\obj\Release\net8.0-windows10.0.17763.0\win-x64\Views\ClipboardImagePreviewWindow.g.cs" />
|
||||
<Compile Include="E:\AX Copilot - Claude\src\AxCopilot\obj\Release\net8.0-windows10.0.17763.0\win-x64\Views\ColorPickResultWindow.g.cs" />
|
||||
<Compile Include="E:\AX Copilot - Claude\src\AxCopilot\obj\Release\net8.0-windows10.0.17763.0\win-x64\Views\CommandPaletteWindow.g.cs" />
|
||||
<Compile Include="E:\AX Copilot - Claude\src\AxCopilot\obj\Release\net8.0-windows10.0.17763.0\win-x64\Views\DockBarWindow.g.cs" />
|
||||
<Compile Include="E:\AX Copilot - Claude\src\AxCopilot\obj\Release\net8.0-windows10.0.17763.0\win-x64\Views\EyeDropperWindow.g.cs" />
|
||||
<Compile Include="E:\AX Copilot - Claude\src\AxCopilot\obj\Release\net8.0-windows10.0.17763.0\win-x64\Views\GuideViewerWindow.g.cs" />
|
||||
<Compile Include="E:\AX Copilot - Claude\src\AxCopilot\obj\Release\net8.0-windows10.0.17763.0\win-x64\Views\HelpDetailWindow.g.cs" />
|
||||
<Compile Include="E:\AX Copilot - Claude\src\AxCopilot\obj\Release\net8.0-windows10.0.17763.0\win-x64\Views\LargeTypeWindow.g.cs" />
|
||||
<Compile Include="E:\AX Copilot - Claude\src\AxCopilot\obj\Release\net8.0-windows10.0.17763.0\win-x64\Views\LauncherWindow.g.cs" />
|
||||
<Compile Include="E:\AX Copilot - Claude\src\AxCopilot\obj\Release\net8.0-windows10.0.17763.0\win-x64\Views\MacroEditorWindow.g.cs" />
|
||||
<Compile Include="E:\AX Copilot - Claude\src\AxCopilot\obj\Release\net8.0-windows10.0.17763.0\win-x64\Views\PreviewWindow.g.cs" />
|
||||
<Compile Include="E:\AX Copilot - Claude\src\AxCopilot\obj\Release\net8.0-windows10.0.17763.0\win-x64\Views\QuickLookWindow.g.cs" />
|
||||
<Compile Include="E:\AX Copilot - Claude\src\AxCopilot\obj\Release\net8.0-windows10.0.17763.0\win-x64\Views\RegionSelectWindow.g.cs" />
|
||||
<Compile Include="E:\AX Copilot - Claude\src\AxCopilot\obj\Release\net8.0-windows10.0.17763.0\win-x64\Views\ReminderPopupWindow.g.cs" />
|
||||
<Compile Include="E:\AX Copilot - Claude\src\AxCopilot\obj\Release\net8.0-windows10.0.17763.0\win-x64\Views\ResourceMonitorWindow.g.cs" />
|
||||
<Compile Include="E:\AX Copilot - Claude\src\AxCopilot\obj\Release\net8.0-windows10.0.17763.0\win-x64\Views\ScheduleEditorWindow.g.cs" />
|
||||
<Compile Include="E:\AX Copilot - Claude\src\AxCopilot\obj\Release\net8.0-windows10.0.17763.0\win-x64\Views\SessionEditorWindow.g.cs" />
|
||||
<Compile Include="E:\AX Copilot - Claude\src\AxCopilot\obj\Release\net8.0-windows10.0.17763.0\win-x64\Views\SettingsWindow.g.cs" />
|
||||
<Compile Include="E:\AX Copilot - Claude\src\AxCopilot\obj\Release\net8.0-windows10.0.17763.0\win-x64\Views\ShortcutHelpWindow.g.cs" />
|
||||
<Compile Include="E:\AX Copilot - Claude\src\AxCopilot\obj\Release\net8.0-windows10.0.17763.0\win-x64\Views\SkillEditorWindow.g.cs" />
|
||||
<Compile Include="E:\AX Copilot - Claude\src\AxCopilot\obj\Release\net8.0-windows10.0.17763.0\win-x64\Views\SkillGalleryWindow.g.cs" />
|
||||
<Compile Include="E:\AX Copilot - Claude\src\AxCopilot\obj\Release\net8.0-windows10.0.17763.0\win-x64\Views\StatisticsWindow.g.cs" />
|
||||
<Compile Include="E:\AX Copilot - Claude\src\AxCopilot\obj\Release\net8.0-windows10.0.17763.0\win-x64\Views\TextActionPopup.g.cs" />
|
||||
<Compile Include="E:\AX Copilot - Claude\src\AxCopilot\obj\Release\net8.0-windows10.0.17763.0\win-x64\Views\TrayMenuWindow.g.cs" />
|
||||
<Compile Include="E:\AX Copilot - Claude\src\AxCopilot\obj\Release\net8.0-windows10.0.17763.0\win-x64\Views\WorkflowAnalyzerWindow.g.cs" />
|
||||
<Compile Include="E:\AX Copilot - Claude\src\AxCopilot\obj\Release\net8.0-windows10.0.17763.0\win-x64\App.g.cs" />
|
||||
<Compile Include="E:\AX Copilot - Claude\src\AxCopilot\obj\Release\net8.0-windows10.0.17763.0\win-x64\AxCopilot_Content.g.cs" />
|
||||
<Compile Include="E:\AX Copilot - Claude\src\AxCopilot\obj\Release\net8.0-windows10.0.17763.0\win-x64\GeneratedInternalTypeHelper.g.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Analyzer Include="C:\Program Files\dotnet\sdk\10.0.201\Sdks\Microsoft.NET.Sdk\targets\..\analyzers\Microsoft.CodeAnalysis.CSharp.NetAnalyzers.dll" />
|
||||
<Analyzer Include="C:\Program Files\dotnet\sdk\10.0.201\Sdks\Microsoft.NET.Sdk\targets\..\analyzers\Microsoft.CodeAnalysis.NetAnalyzers.dll" />
|
||||
<Analyzer Include="C:\Users\admin\.nuget\packages\microsoft.net.illink.tasks\8.0.25\analyzers\dotnet\cs\ILLink.CodeFixProvider.dll" />
|
||||
<Analyzer Include="C:\Users\admin\.nuget\packages\microsoft.net.illink.tasks\8.0.25\analyzers\dotnet\cs\ILLink.RoslynAnalyzer.dll" />
|
||||
<Analyzer Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\analyzers/dotnet/cs/Microsoft.Interop.ComInterfaceGenerator.dll" />
|
||||
<Analyzer Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\analyzers/dotnet/cs/Microsoft.Interop.JavaScript.JSImportGenerator.dll" />
|
||||
<Analyzer Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\analyzers/dotnet/cs/Microsoft.Interop.LibraryImportGenerator.dll" />
|
||||
<Analyzer Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\analyzers/dotnet/cs/Microsoft.Interop.SourceGeneration.dll" />
|
||||
<Analyzer Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\analyzers/dotnet/cs/System.Text.Json.SourceGeneration.dll" />
|
||||
<Analyzer Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\analyzers/dotnet/cs/System.Text.RegularExpressions.Generator.dll" />
|
||||
<Analyzer Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\analyzers/dotnet/System.Windows.Forms.Analyzers.dll" />
|
||||
<Analyzer Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\analyzers/dotnet/cs/System.Windows.Forms.Analyzers.CSharp.dll" />
|
||||
<Analyzer Include="C:\Users\admin\.nuget\packages\microsoft.windows.sdk.net.ref\10.0.17763.57\analyzers/dotnet/cs/WinRT.SourceGenerator.dll" />
|
||||
</ItemGroup>
|
||||
<Import Project="Sdk.targets" Sdk="Microsoft.NET.Sdk" />
|
||||
</Project>
|
||||
@@ -64,7 +64,7 @@ public class ChatHandler : IActionHandler
|
||||
|
||||
try
|
||||
{
|
||||
var storage = new ChatStorageService();
|
||||
var storage = ServiceLocator.Get<IChatStorageService>();
|
||||
var metas = storage.LoadAllMeta();
|
||||
foreach (var conv in metas.Take(5))
|
||||
{
|
||||
|
||||
@@ -185,6 +185,14 @@ public class LauncherSettings
|
||||
[JsonPropertyName("enableIconAnimation")]
|
||||
public bool EnableIconAnimation { get; set; } = true;
|
||||
|
||||
/// <summary>채팅 본문 런처 아이콘 랜덤 애니메이션. false이면 숨쉬기만. 기본 false.</summary>
|
||||
[JsonPropertyName("enableChatIconRandomAnimation")]
|
||||
public bool EnableChatIconRandomAnimation { get; set; } = false;
|
||||
|
||||
/// <summary>채팅 본문 런처 아이콘 글로우 강도. strong/medium/weak. 기본 medium.</summary>
|
||||
[JsonPropertyName("chatIconGlowIntensity")]
|
||||
public string ChatIconGlowIntensity { get; set; } = "medium";
|
||||
|
||||
/// <summary>런처 안내 문구 랜덤 출력 활성화. false이면 고정 문구. 기본 true.</summary>
|
||||
[JsonPropertyName("enableRandomPlaceholder")]
|
||||
public bool EnableRandomPlaceholder { get; set; } = true;
|
||||
@@ -804,10 +812,18 @@ public class LlmSettings
|
||||
[JsonPropertyName("recentPromptTemplates")]
|
||||
public List<string> RecentPromptTemplates { get; set; } = new();
|
||||
|
||||
/// <summary>작업 폴더 경로. 빈 문자열이면 미선택.</summary>
|
||||
/// <summary>작업 폴더 경로. 빈 문자열이면 미선택. (레거시 — 탭별 경로 미지정 시 폴백)</summary>
|
||||
[JsonPropertyName("workFolder")]
|
||||
public string WorkFolder { get; set; } = "";
|
||||
|
||||
/// <summary>코워크(Cowork) 탭 전용 작업 폴더.</summary>
|
||||
[JsonPropertyName("coworkWorkFolder")]
|
||||
public string CoworkWorkFolder { get; set; } = "";
|
||||
|
||||
/// <summary>코드(Code) 탭 전용 작업 폴더.</summary>
|
||||
[JsonPropertyName("codeWorkFolder")]
|
||||
public string CodeWorkFolder { get; set; } = "";
|
||||
|
||||
/// <summary>최근 사용한 작업 폴더 목록.</summary>
|
||||
[JsonPropertyName("recentWorkFolders")]
|
||||
public List<string> RecentWorkFolders { get; set; } = new();
|
||||
@@ -885,9 +901,9 @@ public class LlmSettings
|
||||
[JsonPropertyName("maxTestFixIterations")]
|
||||
public int MaxTestFixIterations { get; set; } = 5;
|
||||
|
||||
/// <summary>에이전트 로그 표시 수준. simple | detailed | debug</summary>
|
||||
/// <summary>에이전트 로그 표시 수준. hidden | simple | detailed | debug</summary>
|
||||
[JsonPropertyName("agentLogLevel")]
|
||||
public string AgentLogLevel { get; set; } = "simple";
|
||||
public string AgentLogLevel { get; set; } = "detailed";
|
||||
|
||||
/// <summary>AX Agent UI 표현 수준. rich | balanced | simple</summary>
|
||||
[JsonPropertyName("agentUiExpressionLevel")]
|
||||
@@ -1055,6 +1071,14 @@ public class LlmSettings
|
||||
[JsonPropertyName("enableChatRainbowGlow")]
|
||||
public bool EnableChatRainbowGlow { get; set; } = false;
|
||||
|
||||
/// <summary>새로운 계획 뷰어(V2 사이드바 레이아웃) 사용. 기본 true.</summary>
|
||||
[JsonPropertyName("enableNewPlanViewer")]
|
||||
public bool EnableNewPlanViewer { get; set; } = true;
|
||||
|
||||
/// <summary>새로운 채팅 렌더링(V2 상세 이력) 사용. 기본 false.</summary>
|
||||
[JsonPropertyName("enableNewChatRendering")]
|
||||
public bool EnableNewChatRendering { get; set; } = false;
|
||||
|
||||
/// <summary>AX Agent 전용 테마. system | light | dark</summary>
|
||||
[JsonPropertyName("agentTheme")]
|
||||
public string AgentTheme { get; set; } = "system";
|
||||
@@ -1069,6 +1093,11 @@ public class LlmSettings
|
||||
[JsonPropertyName("notifyOnComplete")]
|
||||
public bool NotifyOnComplete { get; set; } = false;
|
||||
|
||||
/// <summary>코워크 작업 완료 후 문서 자동 처리 방식.
|
||||
/// "none" = 아무것도 안하기, "open" = 문서 실행(기본 앱), "preview" = 미리보기 뷰어.</summary>
|
||||
[JsonPropertyName("coworkOnComplete")]
|
||||
public string CoworkOnComplete { get; set; } = "none";
|
||||
|
||||
/// <summary>AI 대화창에서 팁 알림 표시 여부.</summary>
|
||||
[JsonPropertyName("showTips")]
|
||||
public bool ShowTips { get; set; } = false;
|
||||
@@ -1315,6 +1344,18 @@ public class CodeSettings
|
||||
/// <summary>Code 탭에서 Cron 도구(cron create/list/delete) 사용 여부. 기본 true.</summary>
|
||||
[JsonPropertyName("enableCronTools")]
|
||||
public bool EnableCronTools { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Code 탭 빈 화면 마스코트 캐릭터 출동 수준.
|
||||
/// "none"=출동 안하기, "one"=한명만(1), "few"=적게(3), "mid"=중간(6), "all"=전부(9+)
|
||||
/// 기본 "none" (메모리 절약).
|
||||
/// </summary>
|
||||
[JsonPropertyName("mascotLevel")]
|
||||
public string MascotLevel { get; set; } = "none";
|
||||
|
||||
/// <summary>(하위호환) 이전 bool 설정 — 무시됨. MascotLevel로 대체.</summary>
|
||||
[JsonPropertyName("enableMascotCharacter")]
|
||||
public bool EnableMascotCharacter { get; set; } = false;
|
||||
}
|
||||
|
||||
/// <summary>사용자 정의 커스텀 프리셋 (settings.json에 저장).</summary>
|
||||
|
||||
@@ -63,6 +63,10 @@ public class ChatConversation
|
||||
[JsonPropertyName("pinned")]
|
||||
public bool Pinned { get; set; } = false;
|
||||
|
||||
/// <summary>아카이브된 대화는 목록에서 기본 숨김 처리.</summary>
|
||||
[JsonPropertyName("archived")]
|
||||
public bool Archived { get; set; } = false;
|
||||
|
||||
/// <summary>대화가 속한 탭. "Chat" | "Cowork" | "Code".</summary>
|
||||
[JsonPropertyName("tab")]
|
||||
public string Tab { get; set; } = "Chat";
|
||||
@@ -126,7 +130,7 @@ public class ChatConversation
|
||||
public List<DraftQueueItem> DraftQueueItems { get; set; } = new();
|
||||
|
||||
[JsonPropertyName("showExecutionHistory")]
|
||||
public bool ShowExecutionHistory { get; set; } = true;
|
||||
public bool ShowExecutionHistory { get; set; } = false;
|
||||
|
||||
[JsonPropertyName("agentRunHistory")]
|
||||
public List<ChatAgentRunRecord> AgentRunHistory { get; set; } = new();
|
||||
|
||||
@@ -19,17 +19,6 @@ public partial class AgentLoopService
|
||||
runState.PendingPostCompactionTurn = false;
|
||||
runState.PostCompactionTurnCounter++;
|
||||
SyncRunPostCompactionState(runState);
|
||||
|
||||
var stage = string.IsNullOrWhiteSpace(runState.LastCompactionStageSummary)
|
||||
? "기본"
|
||||
: runState.LastCompactionStageSummary;
|
||||
var saved = runState.LastCompactionSavedTokens > 0
|
||||
? $" · {Services.TokenEstimator.Format(runState.LastCompactionSavedTokens)} tokens 절감"
|
||||
: "";
|
||||
EmitEvent(
|
||||
AgentEventType.Thinking,
|
||||
"",
|
||||
$"compact 이후 {runState.PostCompactionTurnCounter}번째 턴 · {stage}{saved}");
|
||||
}
|
||||
|
||||
private void SyncRunPostCompactionState(RunState runState)
|
||||
@@ -59,7 +48,7 @@ public partial class AgentLoopService
|
||||
|| summary.StartsWith("컨텍스트 압축 완료", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private string BuildLoopToolResultMessage(LlmService.ContentBlock call, ToolResult result, RunState runState)
|
||||
private string BuildLoopToolResultMessage(ContentBlock call, ToolResult result, RunState runState)
|
||||
{
|
||||
var output = result.Output ?? "";
|
||||
if (!ShouldCompactToolResultForPostCompactionTurn(runState, call.ToolName, output))
|
||||
|
||||
@@ -11,6 +11,8 @@ public partial class AgentLoopService
|
||||
TopicBased,
|
||||
RepoWide,
|
||||
OpenEnded,
|
||||
/// <summary>문서 생성 요청 — 탐색 단계를 건너뛰고 바로 document_plan/생성 도구 사용.</summary>
|
||||
DirectCreation,
|
||||
}
|
||||
|
||||
private sealed class ExplorationTrackingState
|
||||
@@ -22,6 +24,203 @@ public partial class AgentLoopService
|
||||
public bool BroadScanDetected { get; set; }
|
||||
public bool SelectiveHit { get; set; }
|
||||
public bool CorrectiveHintInjected { get; set; }
|
||||
/// <summary>스킬 런타임이 allowed-tools를 명시했으면 true — 탐색 필터링을 건너뜀.</summary>
|
||||
public bool SkillAllowedToolsActive { get; set; }
|
||||
}
|
||||
|
||||
private static IReadOnlyCollection<IAgentTool> FilterExplorationToolsForCurrentIteration(
|
||||
IReadOnlyCollection<IAgentTool> tools,
|
||||
ExplorationTrackingState state,
|
||||
string userQuery,
|
||||
string? activeTab,
|
||||
int totalToolCalls)
|
||||
{
|
||||
if (tools.Count == 0)
|
||||
return tools;
|
||||
|
||||
// 스킬 런타임 정책으로 allowed-tools가 명시된 경우 탐색 필터링을 건너뜀
|
||||
// — 스킬이 의도적으로 허용한 도구(folder_map 등)를 정책이 차단하면 안 됨
|
||||
if (state.SkillAllowedToolsActive)
|
||||
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)
|
||||
@@ -32,6 +231,11 @@ public partial class AgentLoopService
|
||||
var q = userQuery.Trim();
|
||||
var lower = q.ToLowerInvariant();
|
||||
|
||||
// Cowork 탭에서 문서 생성 의도가 있으면 탐색을 건너뛰고 바로 생성 도구 사용
|
||||
if (!string.Equals(activeTab, "Code", StringComparison.OrdinalIgnoreCase)
|
||||
&& HasDocumentCreationIntent(lower))
|
||||
return ExplorationScope.DirectCreation;
|
||||
|
||||
if (lower.Contains("전체") || lower.Contains("전반") || lower.Contains("코드베이스 전체") ||
|
||||
lower.Contains("repo-wide") || lower.Contains("repository-wide") || lower.Contains("전체 구조") ||
|
||||
lower.Contains("아키텍처") || lower.Contains("전체 점검"))
|
||||
@@ -52,14 +256,47 @@ public partial class AgentLoopService
|
||||
: 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)
|
||||
{
|
||||
var guidance = scope switch
|
||||
{
|
||||
ExplorationScope.DirectCreation =>
|
||||
"Exploration scope = direct-creation. The user wants to CREATE a new document/report/file. " +
|
||||
"Do NOT search for existing files with glob/grep/folder_map — skip exploration entirely. " +
|
||||
"Call document_plan first to outline the document structure, then immediately call the appropriate creation tool " +
|
||||
"(docx_create, html_create, excel_create, markdown_create, etc.) to produce the actual file. " +
|
||||
"The output MUST be a real file on disk, not a text response.",
|
||||
ExplorationScope.Localized =>
|
||||
"Exploration scope = localized. Start with grep/glob and targeted file reads. Avoid folder_map unless structure is unclear.",
|
||||
"Exploration scope = localized. Start with lsp_code_intel when the request is about definitions/references/implementations/call hierarchy; otherwise use targeted file_read or grep/glob. Avoid folder_map unless the user explicitly asks for folder structure or file listing.",
|
||||
ExplorationScope.TopicBased =>
|
||||
"Exploration scope = topic-based. Identify candidate files by topic keywords first, then read only a small targeted set.",
|
||||
"Exploration scope = topic-based. Identify candidate files by topic keywords first with glob/grep, then read only a small targeted set.",
|
||||
ExplorationScope.RepoWide =>
|
||||
"Exploration scope = repo-wide. Broad structure inspection is allowed when needed.",
|
||||
_ =>
|
||||
@@ -93,6 +330,15 @@ public partial class AgentLoopService
|
||||
string toolName,
|
||||
string argsJson)
|
||||
{
|
||||
// 문서 생성 모드: 탐색 도구가 1회라도 호출되면 즉시 교정
|
||||
if (state.Scope == ExplorationScope.DirectCreation)
|
||||
{
|
||||
return string.Equals(toolName, "folder_map", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(toolName, "glob", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(toolName, "grep", StringComparison.OrdinalIgnoreCase)
|
||||
|| state.TotalFilesRead >= 1;
|
||||
}
|
||||
|
||||
if (state.Scope is ExplorationScope.RepoWide or ExplorationScope.OpenEnded)
|
||||
return false;
|
||||
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using AxCopilot.Models;
|
||||
|
||||
namespace AxCopilot.Services.Agent;
|
||||
|
||||
public partial class AgentLoopService
|
||||
{
|
||||
private static readonly HashSet<string> FolderMapRecoveryIgnoredDirs = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"bin", "obj", "node_modules", ".git", ".vs", ".idea", ".vscode",
|
||||
"__pycache__", ".mypy_cache", ".pytest_cache", "dist", "build",
|
||||
"packages", ".nuget", "TestResults", "coverage", ".next",
|
||||
"target", ".gradle", ".cargo",
|
||||
};
|
||||
|
||||
private string BuildToolResultFeedbackMessage(
|
||||
ContentBlock call,
|
||||
ToolResult result,
|
||||
RunState runState,
|
||||
AgentContext context,
|
||||
List<ChatMessage> messages,
|
||||
ExplorationTrackingState explorationState,
|
||||
int iteration)
|
||||
{
|
||||
var message = BuildLoopToolResultMessage(call, result, runState);
|
||||
if (!TryBuildFolderMapEmptyRecoveryMessage(
|
||||
call,
|
||||
result,
|
||||
context,
|
||||
messages,
|
||||
explorationState,
|
||||
iteration,
|
||||
out var recoveryAppendix))
|
||||
return message;
|
||||
|
||||
return $"{message}\n\n{recoveryAppendix}";
|
||||
}
|
||||
|
||||
private bool TryBuildFolderMapEmptyRecoveryMessage(
|
||||
ContentBlock call,
|
||||
ToolResult result,
|
||||
AgentContext context,
|
||||
List<ChatMessage> messages,
|
||||
ExplorationTrackingState explorationState,
|
||||
int iteration,
|
||||
out string recoveryAppendix)
|
||||
{
|
||||
recoveryAppendix = "";
|
||||
if (!result.Success
|
||||
|| !string.Equals(call.ToolName, "folder_map", StringComparison.OrdinalIgnoreCase)
|
||||
|| !LooksLikeEmptyFolderMapResult(result.Output))
|
||||
return false;
|
||||
|
||||
var baseDir = ResolveFolderMapTargetPath(call.ToolInput, context);
|
||||
if (string.IsNullOrWhiteSpace(baseDir) || !Directory.Exists(baseDir))
|
||||
return false;
|
||||
|
||||
var candidates = CollectFolderMapRecoveryCandidates(baseDir, context, maxResults: 6);
|
||||
if (candidates.Count == 0)
|
||||
return false;
|
||||
|
||||
var displayPath = Path.GetRelativePath(context.WorkFolder, baseDir).Replace('\\', '/');
|
||||
if (string.IsNullOrWhiteSpace(displayPath) || displayPath == ".")
|
||||
displayPath = ".";
|
||||
|
||||
var suggestedPatterns = candidates
|
||||
.Select(path => Path.GetExtension(path))
|
||||
.Where(ext => !string.IsNullOrWhiteSpace(ext))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.Take(2)
|
||||
.Select(ext => $"**/*{ext}")
|
||||
.ToList();
|
||||
|
||||
var candidateList = string.Join("\n", candidates.Select(path => $"- {path}"));
|
||||
var patternHint = suggestedPatterns.Count > 0
|
||||
? $"\nSuggested glob patterns:\n- {string.Join("\n- ", suggestedPatterns)}"
|
||||
: "";
|
||||
|
||||
recoveryAppendix =
|
||||
"[Recovery] folder_map returned 0 files and 0 dirs, but a fallback scan found candidate paths. " +
|
||||
"Do not repeat folder_map for the same path. Switch to glob first, then read only the best candidates.\n" +
|
||||
$"Fallback candidates under '{displayPath}':\n{candidateList}{patternHint}";
|
||||
|
||||
messages.Add(new ChatMessage
|
||||
{
|
||||
Role = "system",
|
||||
Content =
|
||||
"[System:FolderMapEmptyRecovery] folder_map returned an empty result, but direct file scanning found visible candidates.\n" +
|
||||
$"Path: {displayPath}\n" +
|
||||
"Do not call folder_map again for the same path unless the user explicitly asks for structure only.\n" +
|
||||
"Use glob to narrow the set, then use file_read or document_read on the best matches.\n" +
|
||||
$"Candidates:\n{candidateList}{patternHint}"
|
||||
});
|
||||
|
||||
explorationState.CorrectiveHintInjected = true;
|
||||
explorationState.BroadScanDetected = true;
|
||||
explorationState.SelectiveHit = false;
|
||||
|
||||
WorkflowLogService.LogTransition(
|
||||
_conversationId,
|
||||
_currentRunId,
|
||||
iteration,
|
||||
"folder_map_empty_recovery",
|
||||
$"{displayPath} -> {candidates.Count} candidates");
|
||||
|
||||
EmitEvent(
|
||||
AgentEventType.Thinking,
|
||||
"folder_map",
|
||||
"빈 folder_map 결과를 복구하는 중 · 파일 후보로 다시 좁히는 중");
|
||||
return true;
|
||||
}
|
||||
|
||||
private static string ResolveFolderMapTargetPath(JsonElement? toolInput, AgentContext context)
|
||||
{
|
||||
if (toolInput is not { ValueKind: JsonValueKind.Object } input)
|
||||
return context.WorkFolder;
|
||||
|
||||
var rawPath = input.SafeTryGetProperty("path", out var pathElement)
|
||||
? pathElement.SafeGetString() ?? ""
|
||||
: "";
|
||||
|
||||
return string.IsNullOrWhiteSpace(rawPath)
|
||||
? context.WorkFolder
|
||||
: FileReadTool.ResolvePath(rawPath, context.WorkFolder);
|
||||
}
|
||||
|
||||
private static bool LooksLikeEmptyFolderMapResult(string? output)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(output))
|
||||
return false;
|
||||
|
||||
return output.Contains("0 files, 0 dirs", StringComparison.OrdinalIgnoreCase)
|
||||
|| output.Contains("0 files, 0 directories", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static List<string> CollectFolderMapRecoveryCandidates(string baseDir, AgentContext context, int maxResults)
|
||||
{
|
||||
var candidates = new List<string>(maxResults);
|
||||
var pending = new Stack<(string Path, int Depth)>();
|
||||
pending.Push((baseDir, 0));
|
||||
|
||||
while (pending.Count > 0 && candidates.Count < maxResults)
|
||||
{
|
||||
var (current, depth) = pending.Pop();
|
||||
|
||||
try
|
||||
{
|
||||
foreach (var file in Directory.EnumerateFiles(current))
|
||||
{
|
||||
if (candidates.Count >= maxResults)
|
||||
break;
|
||||
|
||||
var fileName = Path.GetFileName(file);
|
||||
if (string.IsNullOrWhiteSpace(fileName)
|
||||
|| fileName.StartsWith('.')
|
||||
|| !context.IsPathAllowed(file))
|
||||
continue;
|
||||
|
||||
candidates.Add(Path.GetRelativePath(context.WorkFolder, file).Replace('\\', '/'));
|
||||
}
|
||||
|
||||
if (depth >= 3)
|
||||
continue;
|
||||
|
||||
foreach (var dir in Directory.EnumerateDirectories(current))
|
||||
{
|
||||
var name = Path.GetFileName(dir);
|
||||
if (string.IsNullOrWhiteSpace(name)
|
||||
|| name.StartsWith('.')
|
||||
|| FolderMapRecoveryIgnoredDirs.Contains(name)
|
||||
|| !context.IsPathAllowed(dir))
|
||||
continue;
|
||||
|
||||
pending.Push((dir, depth + 1));
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
return candidates;
|
||||
}
|
||||
}
|
||||
@@ -25,11 +25,11 @@ public partial class AgentLoopService
|
||||
};
|
||||
|
||||
/// <summary>도구 호출을 병렬 가능 / 순차 필수로 분류합니다.</summary>
|
||||
private static (List<LlmService.ContentBlock> Parallel, List<LlmService.ContentBlock> Sequential)
|
||||
ClassifyToolCalls(List<LlmService.ContentBlock> calls)
|
||||
private static (List<ContentBlock> Parallel, List<ContentBlock> Sequential)
|
||||
ClassifyToolCalls(List<ContentBlock> calls)
|
||||
{
|
||||
var parallel = new List<LlmService.ContentBlock>();
|
||||
var sequential = new List<LlmService.ContentBlock>();
|
||||
var parallel = new List<ContentBlock>();
|
||||
var sequential = new List<ContentBlock>();
|
||||
var collectParallelPrefix = true;
|
||||
|
||||
foreach (var call in calls)
|
||||
@@ -81,7 +81,7 @@ public partial class AgentLoopService
|
||||
|
||||
/// <summary>읽기 전용 도구들을 병렬 실행합니다.</summary>
|
||||
private async Task ExecuteToolsInParallelAsync(
|
||||
List<LlmService.ContentBlock> calls,
|
||||
List<ContentBlock> calls,
|
||||
List<ChatMessage> messages,
|
||||
AgentContext context,
|
||||
List<string> planSteps,
|
||||
@@ -100,13 +100,13 @@ public partial class AgentLoopService
|
||||
.OrderBy(x => x, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
|
||||
var executableCalls = new List<LlmService.ContentBlock>();
|
||||
var executableCalls = new List<ContentBlock>();
|
||||
foreach (var call in calls)
|
||||
{
|
||||
var resolvedToolName = ResolveRequestedToolName(call.ToolName, activeToolNames);
|
||||
var effectiveCall = string.Equals(call.ToolName, resolvedToolName, StringComparison.OrdinalIgnoreCase)
|
||||
? call
|
||||
: new LlmService.ContentBlock
|
||||
: new ContentBlock
|
||||
{
|
||||
Type = call.Type,
|
||||
Text = call.Text,
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using AxCopilot.Models;
|
||||
|
||||
namespace AxCopilot.Services.Agent;
|
||||
|
||||
public partial class AgentLoopService
|
||||
{
|
||||
private sealed class PathAccessTrackingState
|
||||
{
|
||||
public string? LastPath { get; set; }
|
||||
public int ConsecutiveCount { get; set; }
|
||||
}
|
||||
|
||||
private bool TryHandleRepeatedPathAccessTransition(
|
||||
ContentBlock call,
|
||||
AgentContext context,
|
||||
PathAccessTrackingState state,
|
||||
List<ChatMessage> messages,
|
||||
string? lastModifiedCodeFilePath,
|
||||
bool requireHighImpactCodeVerification,
|
||||
TaskTypePolicy taskPolicy)
|
||||
{
|
||||
if (!string.Equals(ActiveTab, "Code", StringComparison.OrdinalIgnoreCase))
|
||||
return false;
|
||||
|
||||
var currentPath = ExtractTrackedTargetPath(call, context);
|
||||
var shouldTrack = !string.IsNullOrWhiteSpace(currentPath) && IsPathBoundReadOnlyInspectionTool(call.ToolName);
|
||||
|
||||
if (!shouldTrack)
|
||||
{
|
||||
if (ResetsRepeatedPathTracking(call.ToolName))
|
||||
{
|
||||
state.LastPath = null;
|
||||
state.ConsecutiveCount = 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (string.Equals(state.LastPath, currentPath, StringComparison.OrdinalIgnoreCase))
|
||||
state.ConsecutiveCount++;
|
||||
else
|
||||
{
|
||||
state.LastPath = currentPath;
|
||||
state.ConsecutiveCount = 1;
|
||||
}
|
||||
|
||||
if (state.ConsecutiveCount < 4)
|
||||
return false;
|
||||
|
||||
messages.Add(new ChatMessage
|
||||
{
|
||||
Role = "user",
|
||||
Content =
|
||||
"[System:RepeatedPathGuard] 동일 경로를 읽기 도구로 반복 확인하고 있습니다.\n" +
|
||||
$"반복 경로: {currentPath}\n" +
|
||||
"지금은 같은 파일/경로를 다시 읽지 말고, 다음 우선순위로 전환하세요.\n" +
|
||||
"1. grep/glob으로 관련 호출부, 참조 지점, 테스트 파일을 찾기\n" +
|
||||
"2. git_tool(diff)로 실제 변경 범위를 확인하기\n" +
|
||||
"3. 필요한 경우에만 file_edit 또는 build_run/test_loop로 다음 단계 진행하기\n" +
|
||||
"같은 경로에 대한 file_read/document_read/grep 반복은 중단하세요."
|
||||
});
|
||||
EmitEvent(
|
||||
AgentEventType.Thinking,
|
||||
call.ToolName,
|
||||
$"같은 경로 반복 접근을 감지해 흐름을 전환합니다 · {Path.GetFileName(currentPath)}");
|
||||
state.ConsecutiveCount = 0;
|
||||
state.LastPath = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool IsPathBoundReadOnlyInspectionTool(string toolName)
|
||||
=> toolName is "file_read" or "document_read" or "grep" or "glob";
|
||||
|
||||
private static bool ResetsRepeatedPathTracking(string toolName)
|
||||
=> toolName is "file_edit" or "file_write" or "file_manage" or "git_tool" or "build_run" or "test_loop" or "folder_map";
|
||||
|
||||
private static string? ExtractTrackedTargetPath(ContentBlock call, AgentContext context)
|
||||
{
|
||||
if (call.ToolInput is not { ValueKind: JsonValueKind.Object } input)
|
||||
return null;
|
||||
|
||||
var path = input.SafeTryGetProperty("path", out var pathProp)
|
||||
? pathProp.SafeGetString()
|
||||
: null;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(path)
|
||||
&& input.SafeTryGetProperty("project_path", out var projectPathProp))
|
||||
path = projectPathProp.SafeGetString();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
return FileReadTool.ResolvePath(path, context.WorkFolder);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return path;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ namespace AxCopilot.Services.Agent;
|
||||
public partial class AgentLoopService
|
||||
{
|
||||
private void ApplyDocumentPlanSuccessTransitions(
|
||||
LlmService.ContentBlock call,
|
||||
ContentBlock call,
|
||||
ToolResult result,
|
||||
List<ChatMessage> messages,
|
||||
ref bool documentPlanCalled,
|
||||
@@ -28,16 +28,7 @@ public partial class AgentLoopService
|
||||
return;
|
||||
|
||||
var toolHint = ResolveDocumentPlanFollowUpTool(po);
|
||||
messages.Add(new ChatMessage
|
||||
{
|
||||
Role = "user",
|
||||
Content =
|
||||
"document_plan이 완료되었습니다. " +
|
||||
"방금 생성된 골격의 [내용...] 자리와 각 섹션 내용을 실제 상세 본문으로 모두 채운 뒤 " +
|
||||
$"{toolHint} 도구를 지금 즉시 호출하세요. " +
|
||||
"설명만 하지 말고 실제 문서 생성 도구 호출로 바로 이어가세요."
|
||||
});
|
||||
EmitEvent(AgentEventType.Thinking, "", $"문서 개요 완료 · {toolHint} 실행 유도");
|
||||
EmitEvent(AgentEventType.Thinking, "", $"Document structure ready · next creation candidate {toolHint}");
|
||||
}
|
||||
|
||||
private static string? ExtractDocumentPlanScaffold(string output)
|
||||
@@ -47,7 +38,6 @@ public partial class AgentLoopService
|
||||
|
||||
var markers = new (string Start, string End)[]
|
||||
{
|
||||
("--- body 시작 ---", "--- body 끝 ---"),
|
||||
("--- body start ---", "--- body end ---"),
|
||||
("<!-- body start marker -->", "<!-- body end marker -->"),
|
||||
};
|
||||
@@ -76,10 +66,10 @@ public partial class AgentLoopService
|
||||
if (string.IsNullOrWhiteSpace(output))
|
||||
return false;
|
||||
|
||||
return output.Contains("즉시 실행", StringComparison.OrdinalIgnoreCase)
|
||||
|| output.Contains("immediate next step", StringComparison.OrdinalIgnoreCase)
|
||||
return output.Contains("immediate next step", StringComparison.OrdinalIgnoreCase)
|
||||
|| output.Contains("call html_create", StringComparison.OrdinalIgnoreCase)
|
||||
|| output.Contains("call document_assemble", StringComparison.OrdinalIgnoreCase);
|
||||
|| output.Contains("call document_assemble", StringComparison.OrdinalIgnoreCase)
|
||||
|| output.Contains("call docx_create", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static string ResolveDocumentPlanFollowUpTool(string output)
|
||||
@@ -96,9 +86,9 @@ public partial class AgentLoopService
|
||||
}
|
||||
|
||||
private async Task<(bool Completed, bool ConsumedExtraIteration)> TryHandleTerminalDocumentCompletionTransitionAsync(
|
||||
LlmService.ContentBlock call,
|
||||
ContentBlock call,
|
||||
ToolResult result,
|
||||
List<LlmService.ContentBlock> toolCalls,
|
||||
List<ContentBlock> toolCalls,
|
||||
List<ChatMessage> messages,
|
||||
Models.LlmSettings llm,
|
||||
ModelExecutionProfileCatalog.ExecutionPolicy executionPolicy,
|
||||
@@ -109,10 +99,11 @@ public partial class AgentLoopService
|
||||
if (!result.Success || !IsTerminalDocumentTool(call.ToolName) || toolCalls.Count != 1)
|
||||
return (false, false);
|
||||
|
||||
// document_plan 없이 바로 문서 도구가 호출된 경우 — 아직 LLM이 추가 반복을 할 수 있음.
|
||||
// 한 번에 생성된 문서는 내용이 부실할 수 있으므로 조기 종료하지 않고 LLM에 판단을 맡긴다.
|
||||
if (!_docFallbackAttempted && !documentPlanWasCalled)
|
||||
return (false, false);
|
||||
if (!string.Equals(ActiveTab, "Code", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
EmitEvent(AgentEventType.Complete, "", "에이전트 작업 완료");
|
||||
return (true, false);
|
||||
}
|
||||
|
||||
var verificationEnabled = executionPolicy.EnablePostToolVerification
|
||||
&& AgentTabSettingsResolver.IsPostToolVerificationEnabled(ActiveTab, llm);
|
||||
@@ -134,7 +125,7 @@ public partial class AgentLoopService
|
||||
}
|
||||
|
||||
private async Task<bool> TryApplyPostToolVerificationTransitionAsync(
|
||||
LlmService.ContentBlock call,
|
||||
ContentBlock call,
|
||||
ToolResult result,
|
||||
List<ChatMessage> messages,
|
||||
Models.LlmSettings llm,
|
||||
@@ -156,6 +147,16 @@ public partial class AgentLoopService
|
||||
if (!shouldVerify)
|
||||
return false;
|
||||
|
||||
if (!string.Equals(ActiveTab, "Code", StringComparison.OrdinalIgnoreCase))
|
||||
return false;
|
||||
|
||||
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);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -179,7 +179,7 @@ public partial class AgentLoopService
|
||||
return sawDiff;
|
||||
}
|
||||
|
||||
// 蹂寃??꾧뎄 ?먯껜媛 ?놁쑝硫?diff 寃뚯씠???꾧뎄?몄? ?뺤씤
|
||||
// 변경 도구 자체가 없으면 diff 게이트 도구인지 확인
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -198,8 +198,8 @@ public partial class AgentLoopService
|
||||
"insertions",
|
||||
"deletions",
|
||||
"file changed",
|
||||
"異붽?",
|
||||
"?낅뜲?댄듃");
|
||||
"추가",
|
||||
"업데이트");
|
||||
}
|
||||
|
||||
private bool TryApplyDocumentArtifactGateTransition(
|
||||
@@ -300,7 +300,7 @@ public partial class AgentLoopService
|
||||
};
|
||||
var verificationTools = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"file_read", "document_read", "document_review"
|
||||
"file_read", "document_read"
|
||||
};
|
||||
|
||||
var sawVerification = false;
|
||||
@@ -392,9 +392,9 @@ public partial class AgentLoopService
|
||||
{
|
||||
var content = m.Content ?? "";
|
||||
if (content.StartsWith("{\"_tool_use_blocks\""))
|
||||
content = "[?꾧뎄 ?몄텧 ?붿빟]";
|
||||
content = "[도구 실행 요약]";
|
||||
else if (content.StartsWith("{\"type\":\"tool_result\""))
|
||||
content = "[?꾧뎄 ?ㅽ뻾 寃곌낵 ?붿빟]";
|
||||
content = "[도구 실행 결과 요약]";
|
||||
else if (content.Length > 180)
|
||||
content = content[..180] + "...";
|
||||
return $"- [{m.Role}] {content}";
|
||||
@@ -403,7 +403,7 @@ public partial class AgentLoopService
|
||||
|
||||
var summary = summaryLines.Count > 0
|
||||
? string.Join("\n", summaryLines)
|
||||
: "- ?댁쟾 ???留λ씫???먮룞 異뺤빟?섏뿀?듬땲??";
|
||||
: "- 이전 대화 맥락이 자동 축약되었습니다";
|
||||
|
||||
var tail = nonSystem.Skip(Math.Max(0, nonSystem.Count - keepTailCount)).ToList();
|
||||
|
||||
@@ -414,14 +414,14 @@ public partial class AgentLoopService
|
||||
{
|
||||
Role = "user",
|
||||
Timestamp = DateTime.Now,
|
||||
Content = $"[?쒖뒪???먮룞 留λ씫 異뺤빟]\n?꾨옒???댁쟾 ??붿쓽 ?듭떖 ?붿빟?낅땲??\n{summary}"
|
||||
Content = $"[시스템 자동 맥락 축약]\n아래는 이전 대화의 축소 요약입니다.\n{summary}"
|
||||
});
|
||||
messages.AddRange(tail);
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool TryHandleRepeatedFailureGuardTransition(
|
||||
LlmService.ContentBlock call,
|
||||
ContentBlock call,
|
||||
string toolCallSignature,
|
||||
List<ChatMessage> messages,
|
||||
string? lastFailedToolSignature,
|
||||
@@ -458,12 +458,12 @@ public partial class AgentLoopService
|
||||
EmitEvent(
|
||||
AgentEventType.Thinking,
|
||||
call.ToolName,
|
||||
$"?숈씪 ?꾧뎄/?뚮씪誘명꽣 諛섎났 ?ㅽ뙣 ?⑦꽩 媛먯? - ?ㅻⅨ ?묎렐?쇰줈 ?꾪솚?⑸땲??({repeatedFailedToolSignatureCount}/{maxRetry})");
|
||||
$"동일 도구/파라미터 반복 실패 패턴 감지 - 다른 접근으로 전환합니다({repeatedFailedToolSignatureCount}/{maxRetry})");
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool TryHandleNoProgressReadOnlyLoopTransition(
|
||||
LlmService.ContentBlock call,
|
||||
ContentBlock call,
|
||||
string toolCallSignature,
|
||||
int repeatedSameSignatureCount,
|
||||
List<ChatMessage> messages,
|
||||
@@ -479,7 +479,7 @@ public partial class AgentLoopService
|
||||
messages.Add(LlmService.CreateToolResultMessage(
|
||||
call.ToolId,
|
||||
call.ToolName,
|
||||
$"[NO_PROGRESS_LOOP_GUARD] ?숈씪???쎄린 ?꾧뎄 ?몄텧??{repeatedSameSignatureCount}??諛섎났?섏뿀?듬땲?? {toolCallSignature}\n" +
|
||||
$"[NO_PROGRESS_LOOP_GUARD] 동일한 읽기 도구 실행이 {repeatedSameSignatureCount}회 반복되었습니다. {toolCallSignature}\n" +
|
||||
"Stop repeating the same read-only call and switch to a concrete next action."));
|
||||
messages.Add(new ChatMessage
|
||||
{
|
||||
@@ -493,7 +493,7 @@ public partial class AgentLoopService
|
||||
EmitEvent(
|
||||
AgentEventType.Thinking,
|
||||
call.ToolName,
|
||||
$"臾댁쓽誘명븳 ?쎄린 ?꾧뎄 諛섎났 猷⑦봽媛 媛먯??섏뼱 ?ㅻⅨ ?꾨왂?쇰줈 ?꾪솚?⑸땲??({repeatedSameSignatureCount}??");
|
||||
$"무의미한 읽기 도구 반복 루프가 감지되어 다른 방향으로 전환합니다({repeatedSameSignatureCount}회)");
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -520,7 +520,7 @@ public partial class AgentLoopService
|
||||
EmitEvent(
|
||||
AgentEventType.Thinking,
|
||||
"",
|
||||
$"?쎄린 ?꾩슜 ?꾧뎄媛 ?곗냽 {consecutiveReadOnlySuccessTools}???ㅽ뻾?섏뼱 ?뺤껜媛 媛먯??섏뿀?듬땲?? ?ㅽ뻾 ?④퀎濡??꾪솚?⑸땲??(threshold={threshold})");
|
||||
$"읽기 전용 도구가 연속 {consecutiveReadOnlySuccessTools}회 실행되어 정체가 감지되었습니다. 실행 전략으로 전환합니다(threshold={threshold})");
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -554,7 +554,7 @@ public partial class AgentLoopService
|
||||
EmitEvent(
|
||||
AgentEventType.Thinking,
|
||||
"",
|
||||
$"?ㅽ뻾 吏꾩쟾???놁뼱 媛뺤젣 蹂듦뎄 ?④퀎濡??쒖옉?⑸땲??({runState.NoProgressRecoveryRetry}/2)");
|
||||
$"실행 진전이 없어 강제 복구 전략으로 시작합니다({runState.NoProgressRecoveryRetry}/2)");
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -629,11 +629,11 @@ public partial class AgentLoopService
|
||||
"forbidden",
|
||||
"not found",
|
||||
"invalid",
|
||||
"?ㅽ뙣",
|
||||
"?ㅻ쪟",
|
||||
"?덉쇅",
|
||||
"?쒓컙 珥덇낵",
|
||||
"沅뚰븳 嫄곕?",
|
||||
"실패",
|
||||
"오류",
|
||||
"예외",
|
||||
"시간 초과",
|
||||
"권한 거부",
|
||||
"李⑤떒",
|
||||
"찾을 수");
|
||||
}
|
||||
@@ -911,22 +911,22 @@ public partial class AgentLoopService
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<LlmService.ToolPrefetchResult?> TryPrefetchReadOnlyToolAsync(
|
||||
LlmService.ContentBlock block,
|
||||
private async Task<ToolPrefetchResult?> TryPrefetchReadOnlyToolAsync(
|
||||
ContentBlock block,
|
||||
IReadOnlyCollection<IAgentTool> tools,
|
||||
AgentContext context,
|
||||
CancellationToken ct)
|
||||
=> await _toolExecutionCoordinator.TryPrefetchReadOnlyToolAsync(block, tools, context, ct);
|
||||
|
||||
private async Task<List<LlmService.ContentBlock>> SendWithToolsWithRecoveryAsync(
|
||||
private async Task<List<ContentBlock>> SendWithToolsWithRecoveryAsync(
|
||||
List<ChatMessage> messages,
|
||||
IReadOnlyCollection<IAgentTool> tools,
|
||||
CancellationToken ct,
|
||||
string phaseLabel,
|
||||
RunState? runState = null,
|
||||
bool forceToolCall = false,
|
||||
Func<LlmService.ContentBlock, Task<LlmService.ToolPrefetchResult?>>? prefetchToolCallAsync = null,
|
||||
Func<LlmService.ToolStreamEvent, Task>? onStreamEventAsync = null)
|
||||
Func<ContentBlock, Task<ToolPrefetchResult?>>? prefetchToolCallAsync = null,
|
||||
Func<ToolStreamEvent, Task>? onStreamEventAsync = null)
|
||||
=> await _toolExecutionCoordinator.SendWithToolsWithRecoveryAsync(
|
||||
messages,
|
||||
tools,
|
||||
@@ -938,7 +938,7 @@ public partial class AgentLoopService
|
||||
onStreamEventAsync);
|
||||
|
||||
private void ApplyToolPostExecutionBookkeeping(
|
||||
LlmService.ContentBlock call,
|
||||
ContentBlock call,
|
||||
ToolResult result,
|
||||
TokenUsage? tokenUsage,
|
||||
Models.LlmSettings llm,
|
||||
@@ -986,7 +986,7 @@ public partial class AgentLoopService
|
||||
}
|
||||
|
||||
private bool TryHandleToolFailureTransition(
|
||||
LlmService.ContentBlock call,
|
||||
ContentBlock call,
|
||||
ToolResult result,
|
||||
AgentContext context,
|
||||
TaskTypePolicy taskPolicy,
|
||||
@@ -1052,7 +1052,7 @@ public partial class AgentLoopService
|
||||
result.Output)
|
||||
});
|
||||
}
|
||||
EmitEvent(AgentEventType.Thinking, "", $"Self-Reflection: ?ㅽ뙣 遺꾩꽍 ???ъ떆??({consecutiveErrors}/{maxRetry})");
|
||||
EmitEvent(AgentEventType.Thinking, "", $"Self-Reflection: 실패 분석 후 재시도({consecutiveErrors}/{maxRetry})");
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1120,7 +1120,7 @@ public partial class AgentLoopService
|
||||
}
|
||||
|
||||
private async Task<(bool ShouldContinue, string? TerminalResponse)> TryHandleUserDecisionTransitionsAsync(
|
||||
LlmService.ContentBlock call,
|
||||
ContentBlock call,
|
||||
AgentContext context,
|
||||
List<ChatMessage> messages)
|
||||
{
|
||||
@@ -1139,7 +1139,7 @@ public partial class AgentLoopService
|
||||
if (devShouldContinue)
|
||||
{
|
||||
messages.Add(LlmService.CreateToolResultMessage(
|
||||
call.ToolId, call.ToolName, devToolResultMessage ?? "[SKIPPED by developer] 媛쒕컻?먭? ???꾧뎄 ?ㅽ뻾??嫄대꼫?곗뿀?듬땲??"));
|
||||
call.ToolId, call.ToolName, devToolResultMessage ?? "[SKIPPED by developer] 개발자에 의해 도구 실행이 건너뛰어졌습니다"));
|
||||
return (true, null);
|
||||
}
|
||||
}
|
||||
@@ -1160,7 +1160,7 @@ public partial class AgentLoopService
|
||||
if (scopeShouldContinue)
|
||||
{
|
||||
messages.Add(LlmService.CreateToolResultMessage(
|
||||
call.ToolId, call.ToolName, scopeToolResultMessage ?? "[SKIPPED] 媛쒕컻?먭? ???묒뾽??嫄대꼫?곗뿀?듬땲??"));
|
||||
call.ToolId, call.ToolName, scopeToolResultMessage ?? "[SKIPPED] 사용자에 의해 작업이 건너뛰어졌습니다"));
|
||||
return (true, null);
|
||||
}
|
||||
}
|
||||
@@ -1193,4 +1193,3 @@ public partial class AgentLoopService
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace AxCopilot.Services.Agent;
|
||||
public partial class AgentLoopService
|
||||
{
|
||||
private void ApplyCodeQualityFollowUpTransition(
|
||||
LlmService.ContentBlock call,
|
||||
ContentBlock call,
|
||||
ToolResult result,
|
||||
List<ChatMessage> messages,
|
||||
TaskTypePolicy taskPolicy,
|
||||
@@ -13,9 +13,9 @@ public partial class AgentLoopService
|
||||
ref string? lastModifiedCodeFilePath)
|
||||
{
|
||||
var highImpactCodeChange = IsHighImpactCodeModification(ActiveTab ?? "", call.ToolName, result);
|
||||
if (ShouldInjectCodeQualityFollowUp(ActiveTab ?? "", call.ToolName, result))
|
||||
{
|
||||
requireHighImpactCodeVerification = highImpactCodeChange;
|
||||
if (highImpactCodeChange && ShouldInjectCodeQualityFollowUp(ActiveTab ?? "", call.ToolName, result))
|
||||
{
|
||||
lastModifiedCodeFilePath = result.FilePath;
|
||||
messages.Add(new ChatMessage
|
||||
{
|
||||
@@ -52,8 +52,14 @@ public partial class AgentLoopService
|
||||
var hasCodeVerificationEvidence = HasCodeVerificationEvidenceAfterLastModification(
|
||||
messages,
|
||||
requireHighImpactCodeVerification);
|
||||
var hasDiffEvidence = HasDiffEvidenceAfterLastModification(messages);
|
||||
var hasRecentBuildOrTestEvidence = HasBuildOrTestEvidenceAfterLastModification(messages);
|
||||
var hasSuccessfulBuildAndTestEvidence = HasSuccessfulBuildAndTestAfterLastModification(messages);
|
||||
var hasLightweightCompletionEvidence = requireHighImpactCodeVerification
|
||||
? hasCodeVerificationEvidence
|
||||
: hasDiffEvidence || hasRecentBuildOrTestEvidence || hasCodeVerificationEvidence;
|
||||
if (executionPolicy.CodeVerificationGateMaxRetries > 0
|
||||
&& !hasCodeVerificationEvidence
|
||||
&& !(requireHighImpactCodeVerification ? hasCodeVerificationEvidence : hasLightweightCompletionEvidence)
|
||||
&& runState.CodeVerificationGateRetry < executionPolicy.CodeVerificationGateMaxRetries)
|
||||
{
|
||||
runState.CodeVerificationGateRetry++;
|
||||
@@ -89,7 +95,13 @@ public partial class AgentLoopService
|
||||
return true;
|
||||
}
|
||||
|
||||
var hasBlockingCodeEvidenceGap = !(requireHighImpactCodeVerification ? hasCodeVerificationEvidence : hasLightweightCompletionEvidence)
|
||||
|| (requireHighImpactCodeVerification && !hasSuccessfulBuildAndTestEvidence);
|
||||
var shouldRequestStructuredFinalReport =
|
||||
taskPolicy.IsReviewTask || requireHighImpactCodeVerification;
|
||||
if (executionPolicy.FinalReportGateMaxRetries > 0
|
||||
&& shouldRequestStructuredFinalReport
|
||||
&& !hasBlockingCodeEvidenceGap
|
||||
&& !HasSufficientFinalReportEvidence(textResponse, taskPolicy, requireHighImpactCodeVerification, messages)
|
||||
&& runState.FinalReportGateRetry < executionPolicy.FinalReportGateMaxRetries)
|
||||
{
|
||||
@@ -111,16 +123,20 @@ public partial class AgentLoopService
|
||||
private bool TryApplyCodeDiffEvidenceGateTransition(
|
||||
List<ChatMessage> messages,
|
||||
string? textResponse,
|
||||
TaskTypePolicy taskPolicy,
|
||||
RunState runState,
|
||||
ModelExecutionProfileCatalog.ExecutionPolicy executionPolicy)
|
||||
{
|
||||
if (!string.Equals(ActiveTab, "Code", StringComparison.OrdinalIgnoreCase))
|
||||
return false;
|
||||
|
||||
if (!taskPolicy.IsReviewTask)
|
||||
return false;
|
||||
|
||||
if (executionPolicy.CodeDiffGateMaxRetries <= 0 || runState.CodeDiffGateRetry >= executionPolicy.CodeDiffGateMaxRetries)
|
||||
return false;
|
||||
|
||||
if (HasDiffEvidenceAfterLastModification(messages))
|
||||
if (HasDiffEvidenceAfterLastModification(messages) || HasBuildOrTestEvidenceAfterLastModification(messages))
|
||||
return false;
|
||||
|
||||
runState.CodeDiffGateRetry++;
|
||||
@@ -145,9 +161,15 @@ public partial class AgentLoopService
|
||||
if (!string.Equals(ActiveTab, "Code", StringComparison.OrdinalIgnoreCase))
|
||||
return false;
|
||||
|
||||
if (!taskPolicy.IsReviewTask)
|
||||
return false;
|
||||
|
||||
if (executionPolicy.RecentExecutionGateMaxRetries <= 0 || runState.RecentExecutionGateRetry >= executionPolicy.RecentExecutionGateMaxRetries)
|
||||
return false;
|
||||
|
||||
if (HasDiffEvidenceAfterLastModification(messages))
|
||||
return false;
|
||||
|
||||
if (!HasAnyBuildOrTestEvidence(messages))
|
||||
return false;
|
||||
|
||||
@@ -176,9 +198,15 @@ public partial class AgentLoopService
|
||||
if (!string.Equals(ActiveTab, "Code", StringComparison.OrdinalIgnoreCase))
|
||||
return false;
|
||||
|
||||
if (!taskPolicy.IsReviewTask)
|
||||
return false;
|
||||
|
||||
if (executionPolicy.ExecutionSuccessGateMaxRetries <= 0 || runState.ExecutionSuccessGateRetry >= executionPolicy.ExecutionSuccessGateMaxRetries)
|
||||
return false;
|
||||
|
||||
if (HasDiffEvidenceAfterLastModification(messages))
|
||||
return false;
|
||||
|
||||
if (!HasAnyBuildOrTestAttempt(messages))
|
||||
return false;
|
||||
|
||||
|
||||
@@ -4,11 +4,11 @@ namespace AxCopilot.Services.Agent;
|
||||
|
||||
public partial class AgentLoopService
|
||||
{
|
||||
private static (bool ShouldRun, List<LlmService.ContentBlock> ParallelBatch, List<LlmService.ContentBlock> SequentialBatch)
|
||||
CreateParallelExecutionPlan(bool parallelEnabled, List<LlmService.ContentBlock> toolCalls, int maxParallelBatch)
|
||||
private static (bool ShouldRun, List<ContentBlock> ParallelBatch, List<ContentBlock> SequentialBatch)
|
||||
CreateParallelExecutionPlan(bool parallelEnabled, List<ContentBlock> toolCalls, int maxParallelBatch)
|
||||
{
|
||||
if (!parallelEnabled || toolCalls.Count <= 1)
|
||||
return (false, new List<LlmService.ContentBlock>(), toolCalls);
|
||||
return (false, new List<ContentBlock>(), toolCalls);
|
||||
|
||||
var (parallelBatch, sequentialBatch) = ClassifyToolCalls(toolCalls);
|
||||
if (maxParallelBatch > 0 && parallelBatch.Count > maxParallelBatch)
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
using System.Text.Json;
|
||||
using AxCopilot.Models;
|
||||
|
||||
namespace AxCopilot.Services.Agent;
|
||||
|
||||
/// <summary>
|
||||
/// tool_use / tool_result 불변식을 유지하기 위한 공용 helper입니다.
|
||||
/// </summary>
|
||||
internal static class AgentMessageInvariantHelper
|
||||
{
|
||||
public static int AdjustStartIndexForToolPairs(
|
||||
IReadOnlyList<ChatMessage> messages,
|
||||
int startIndex,
|
||||
out int preservedToolPairs)
|
||||
{
|
||||
preservedToolPairs = 0;
|
||||
if (startIndex <= 0)
|
||||
return startIndex;
|
||||
|
||||
var toolResultIds = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
for (var i = startIndex; i < messages.Count; i++)
|
||||
{
|
||||
if (TryGetToolResultId(messages[i], out var toolResultId))
|
||||
toolResultIds.Add(toolResultId);
|
||||
}
|
||||
|
||||
if (toolResultIds.Count == 0)
|
||||
return startIndex;
|
||||
|
||||
var toolUseIdsInWindow = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
for (var i = startIndex; i < messages.Count; i++)
|
||||
{
|
||||
foreach (var toolUseId in EnumerateToolUseIds(messages[i]))
|
||||
toolUseIdsInWindow.Add(toolUseId);
|
||||
}
|
||||
|
||||
toolResultIds.ExceptWith(toolUseIdsInWindow);
|
||||
if (toolResultIds.Count == 0)
|
||||
return startIndex;
|
||||
|
||||
var adjustedStart = startIndex;
|
||||
for (var i = startIndex - 1; i >= 0 && toolResultIds.Count > 0; i--)
|
||||
{
|
||||
var foundAny = false;
|
||||
foreach (var toolUseId in EnumerateToolUseIds(messages[i]))
|
||||
{
|
||||
if (!toolResultIds.Remove(toolUseId))
|
||||
continue;
|
||||
|
||||
preservedToolPairs++;
|
||||
foundAny = true;
|
||||
}
|
||||
|
||||
if (foundAny)
|
||||
adjustedStart = i;
|
||||
}
|
||||
|
||||
return adjustedStart;
|
||||
}
|
||||
|
||||
public static bool TryGetToolResultId(ChatMessage message, out string toolResultId)
|
||||
{
|
||||
toolResultId = "";
|
||||
if (!string.Equals(message.Role, "user", StringComparison.OrdinalIgnoreCase))
|
||||
return false;
|
||||
|
||||
var content = message.Content ?? "";
|
||||
if (!content.StartsWith("{\"type\":\"tool_result\"", StringComparison.Ordinal))
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(content);
|
||||
toolResultId = doc.RootElement.TryGetProperty("tool_use_id", out var idEl)
|
||||
? idEl.GetString() ?? ""
|
||||
: "";
|
||||
return !string.IsNullOrWhiteSpace(toolResultId);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<string> EnumerateToolUseIds(ChatMessage message)
|
||||
{
|
||||
if (!string.Equals(message.Role, "assistant", StringComparison.OrdinalIgnoreCase))
|
||||
yield break;
|
||||
|
||||
var content = message.Content ?? "";
|
||||
if (!content.StartsWith("{\"_tool_use_blocks\"", StringComparison.Ordinal))
|
||||
yield break;
|
||||
|
||||
JsonDocument? doc = null;
|
||||
try
|
||||
{
|
||||
doc = JsonDocument.Parse(content);
|
||||
if (!doc.RootElement.TryGetProperty("_tool_use_blocks", out var blocksEl) || blocksEl.ValueKind != JsonValueKind.Array)
|
||||
yield break;
|
||||
|
||||
foreach (var block in blocksEl.EnumerateArray())
|
||||
{
|
||||
if (!block.TryGetProperty("type", out var typeEl)
|
||||
|| !string.Equals(typeEl.GetString(), "tool_use", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (block.TryGetProperty("id", out var idEl))
|
||||
{
|
||||
var toolUseId = idEl.GetString();
|
||||
if (!string.IsNullOrWhiteSpace(toolUseId))
|
||||
yield return toolUseId!;
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
doc?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
using AxCopilot.Models;
|
||||
|
||||
namespace AxCopilot.Services.Agent;
|
||||
|
||||
public sealed class AgentQueryContextWindowResult
|
||||
{
|
||||
public required List<ChatMessage> Messages { get; init; }
|
||||
public int SourceMessageCount { get; init; }
|
||||
public int ViewMessageCount { get; init; }
|
||||
public int WindowStartIndex { get; init; }
|
||||
public bool BoundaryApplied { get; init; }
|
||||
public bool ToolPairExpanded { get; init; }
|
||||
public int PreservedToolPairCount { get; init; }
|
||||
public int TruncatedToolResultCount { get; init; }
|
||||
public int TokensBeforeBudget { get; init; }
|
||||
public int TokensAfterBudget { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// claude-code의 messagesForQuery처럼, 저장된 전체 대화와 실제 LLM에 보내는 query view를 분리합니다.
|
||||
/// </summary>
|
||||
public static class AgentQueryContextBuilder
|
||||
{
|
||||
private const int ProtectedRecentNonSystemMessages = 8;
|
||||
private const string PostCompactContextMetaKind = "post_compact_context";
|
||||
|
||||
public static AgentQueryContextWindowResult Build(IReadOnlyList<ChatMessage> sourceMessages)
|
||||
{
|
||||
if (sourceMessages.Count == 0)
|
||||
{
|
||||
return new AgentQueryContextWindowResult
|
||||
{
|
||||
Messages = new List<ChatMessage>(),
|
||||
SourceMessageCount = 0,
|
||||
ViewMessageCount = 0,
|
||||
WindowStartIndex = 0,
|
||||
BoundaryApplied = false,
|
||||
ToolPairExpanded = false,
|
||||
PreservedToolPairCount = 0,
|
||||
TruncatedToolResultCount = 0,
|
||||
TokensBeforeBudget = 0,
|
||||
TokensAfterBudget = 0,
|
||||
};
|
||||
}
|
||||
|
||||
var startIndex = FindWindowStartIndex(sourceMessages, out var boundaryApplied);
|
||||
var adjustedStartIndex = AgentMessageInvariantHelper.AdjustStartIndexForToolPairs(
|
||||
sourceMessages,
|
||||
startIndex,
|
||||
out var preservedToolPairs);
|
||||
var toolPairExpanded = adjustedStartIndex < startIndex;
|
||||
|
||||
var windowMessages = new List<ChatMessage>(sourceMessages.Count);
|
||||
for (var i = 0; i < sourceMessages.Count; i++)
|
||||
{
|
||||
var include = string.Equals(sourceMessages[i].Role, "system", StringComparison.OrdinalIgnoreCase)
|
||||
|| i >= adjustedStartIndex;
|
||||
if (!include)
|
||||
continue;
|
||||
|
||||
windowMessages.Add(CloneMessage(sourceMessages[i]));
|
||||
}
|
||||
|
||||
if (boundaryApplied)
|
||||
InjectPostCompactContextMessage(windowMessages);
|
||||
|
||||
var tokensBeforeBudget = TokenEstimator.EstimateMessages(windowMessages);
|
||||
var budgetResult = AgentToolResultBudget.Apply(windowMessages, ProtectedRecentNonSystemMessages);
|
||||
var tokensAfterBudget = TokenEstimator.EstimateMessages(windowMessages);
|
||||
|
||||
return new AgentQueryContextWindowResult
|
||||
{
|
||||
Messages = windowMessages,
|
||||
SourceMessageCount = sourceMessages.Count,
|
||||
ViewMessageCount = windowMessages.Count,
|
||||
WindowStartIndex = adjustedStartIndex,
|
||||
BoundaryApplied = boundaryApplied,
|
||||
ToolPairExpanded = toolPairExpanded,
|
||||
PreservedToolPairCount = preservedToolPairs,
|
||||
TruncatedToolResultCount = budgetResult.TruncatedCount,
|
||||
TokensBeforeBudget = tokensBeforeBudget,
|
||||
TokensAfterBudget = tokensAfterBudget,
|
||||
};
|
||||
}
|
||||
|
||||
private static int FindWindowStartIndex(IReadOnlyList<ChatMessage> sourceMessages, out bool boundaryApplied)
|
||||
{
|
||||
for (var i = sourceMessages.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (IsQueryBoundaryMarker(sourceMessages[i]))
|
||||
{
|
||||
boundaryApplied = true;
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
boundaryApplied = false;
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static bool IsQueryBoundaryMarker(ChatMessage message)
|
||||
{
|
||||
var metaKind = message.MetaKind ?? "";
|
||||
if (metaKind.Equals("microcompact_boundary", StringComparison.OrdinalIgnoreCase)
|
||||
|| metaKind.Equals("session_memory_compaction", StringComparison.OrdinalIgnoreCase)
|
||||
|| metaKind.Equals("collapsed_boundary", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var content = message.Content ?? "";
|
||||
return content.StartsWith("[이전 대화 요약", StringComparison.Ordinal)
|
||||
|| content.StartsWith("[세션 메모리 압축", StringComparison.Ordinal)
|
||||
|| content.StartsWith("[이전 실행 묶음 압축", StringComparison.Ordinal)
|
||||
|| content.StartsWith("[이전 압축 경계 병합", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static ChatMessage CloneMessage(ChatMessage source, string? contentOverride = null)
|
||||
{
|
||||
return new ChatMessage
|
||||
{
|
||||
MsgId = source.MsgId,
|
||||
Role = source.Role,
|
||||
Content = contentOverride ?? source.Content,
|
||||
Timestamp = source.Timestamp,
|
||||
MetaKind = source.MetaKind,
|
||||
MetaRunId = source.MetaRunId,
|
||||
Feedback = source.Feedback,
|
||||
ResponseElapsedMs = source.ResponseElapsedMs,
|
||||
PromptTokens = source.PromptTokens,
|
||||
CompletionTokens = source.CompletionTokens,
|
||||
AttachedFiles = source.AttachedFiles?.ToList(),
|
||||
Images = source.Images?.Select(image => new ImageAttachment
|
||||
{
|
||||
Base64 = image.Base64,
|
||||
MimeType = image.MimeType,
|
||||
FileName = image.FileName,
|
||||
}).ToList(),
|
||||
};
|
||||
}
|
||||
|
||||
private static void InjectPostCompactContextMessage(List<ChatMessage> messages)
|
||||
{
|
||||
if (messages.Count == 0)
|
||||
return;
|
||||
|
||||
if (messages.Any(m => string.Equals(m.MetaKind, PostCompactContextMetaKind, StringComparison.OrdinalIgnoreCase)))
|
||||
return;
|
||||
|
||||
var attachedFiles = messages
|
||||
.SelectMany(m => m.AttachedFiles ?? Enumerable.Empty<string>())
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.Take(5)
|
||||
.ToList();
|
||||
var imageCount = messages.Sum(m => m.Images?.Count ?? 0);
|
||||
var compactSummaryCount = messages.Count(m =>
|
||||
string.Equals(m.MetaKind, "microcompact_boundary", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(m.MetaKind, "session_memory_compaction", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(m.MetaKind, "collapsed_boundary", StringComparison.OrdinalIgnoreCase));
|
||||
var branchContextCount = messages.Count(m =>
|
||||
string.Equals(m.MetaKind, "branch_context", StringComparison.OrdinalIgnoreCase));
|
||||
var structuredToolHistoryCount = messages.Count(m =>
|
||||
{
|
||||
var content = m.Content ?? "";
|
||||
return content.StartsWith("{\"_tool_use_blocks\"", StringComparison.Ordinal)
|
||||
|| content.StartsWith("{\"type\":\"tool_result\"", StringComparison.Ordinal);
|
||||
});
|
||||
var recentToolNames = messages
|
||||
.Where(m => m.Role == "user")
|
||||
.Select(m => TryExtractToolResultToolName(m, out var toolName) ? toolName : "")
|
||||
.Where(toolName => !string.IsNullOrWhiteSpace(toolName))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.Take(4)
|
||||
.ToList();
|
||||
|
||||
if (attachedFiles.Count == 0
|
||||
&& imageCount == 0
|
||||
&& compactSummaryCount == 0
|
||||
&& structuredToolHistoryCount == 0
|
||||
&& branchContextCount == 0
|
||||
&& recentToolNames.Count == 0)
|
||||
return;
|
||||
|
||||
var lines = new List<string> { "[post-compact context]" };
|
||||
if (compactSummaryCount > 0)
|
||||
lines.Add($"restored compact summaries: {compactSummaryCount}");
|
||||
if (branchContextCount > 0)
|
||||
lines.Add($"restored branch context: {branchContextCount}");
|
||||
if (structuredToolHistoryCount > 0)
|
||||
lines.Add($"restored tool history blocks: {structuredToolHistoryCount}");
|
||||
if (recentToolNames.Count > 0)
|
||||
lines.Add("restored recent tools: " + string.Join(", ", recentToolNames));
|
||||
if (attachedFiles.Count > 0)
|
||||
lines.Add("restored file refs: " + string.Join(", ", attachedFiles));
|
||||
if (imageCount > 0)
|
||||
lines.Add($"restored image refs: {imageCount}");
|
||||
|
||||
var insertIndex = 0;
|
||||
while (insertIndex < messages.Count && string.Equals(messages[insertIndex].Role, "system", StringComparison.OrdinalIgnoreCase))
|
||||
insertIndex++;
|
||||
|
||||
messages.Insert(insertIndex, new ChatMessage
|
||||
{
|
||||
Role = "system",
|
||||
MetaKind = PostCompactContextMetaKind,
|
||||
Content = string.Join("\n", lines),
|
||||
Timestamp = DateTime.Now,
|
||||
AttachedFiles = attachedFiles.Count > 0 ? attachedFiles : null,
|
||||
});
|
||||
}
|
||||
|
||||
private static bool TryExtractToolResultToolName(ChatMessage message, out string toolName)
|
||||
{
|
||||
toolName = "";
|
||||
if (!string.Equals(message.Role, "user", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.IsNullOrWhiteSpace(message.Content)
|
||||
|| !message.Content.StartsWith("{\"type\":\"tool_result\"", StringComparison.Ordinal))
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
using var doc = System.Text.Json.JsonDocument.Parse(message.Content);
|
||||
if (doc.RootElement.TryGetProperty("tool_name", out var toolNameEl))
|
||||
{
|
||||
toolName = toolNameEl.GetString() ?? "";
|
||||
return !string.IsNullOrWhiteSpace(toolName);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
using System.Text.Json;
|
||||
using AxCopilot.Models;
|
||||
|
||||
namespace AxCopilot.Services.Agent;
|
||||
|
||||
public sealed class AgentToolResultBudgetResult
|
||||
{
|
||||
public int TruncatedCount { get; set; }
|
||||
public int ProcessedCount { get; set; }
|
||||
public int TotalCharsBefore { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 오래된 tool_result를 query view와 압축 단계에서 같은 기준으로 줄이기 위한 공용 helper.
|
||||
/// </summary>
|
||||
public static class AgentToolResultBudget
|
||||
{
|
||||
public const int DefaultSoftCharLimit = 900;
|
||||
public const int DefaultAggregateBudgetChars = 7_500;
|
||||
|
||||
public static AgentToolResultBudgetResult Apply(
|
||||
List<ChatMessage> messages,
|
||||
int protectedRecentNonSystemMessages,
|
||||
int softCharLimit = DefaultSoftCharLimit,
|
||||
int aggregateBudgetChars = DefaultAggregateBudgetChars)
|
||||
{
|
||||
var result = new AgentToolResultBudgetResult();
|
||||
var nonSystemIndexes = messages
|
||||
.Select((message, index) => new { message, index })
|
||||
.Where(x => !string.Equals(x.message.Role, "system", StringComparison.OrdinalIgnoreCase))
|
||||
.Select(x => x.index)
|
||||
.ToList();
|
||||
|
||||
if (nonSystemIndexes.Count <= protectedRecentNonSystemMessages)
|
||||
return result;
|
||||
|
||||
var protectedStart = nonSystemIndexes[Math.Max(0, nonSystemIndexes.Count - protectedRecentNonSystemMessages)];
|
||||
var spentChars = 0;
|
||||
|
||||
for (var i = 0; i < protectedStart; i++)
|
||||
{
|
||||
var message = messages[i];
|
||||
if (!AgentMessageInvariantHelper.TryGetToolResultId(message, out _))
|
||||
continue;
|
||||
|
||||
var content = message.Content ?? "";
|
||||
if (string.IsNullOrWhiteSpace(content))
|
||||
continue;
|
||||
|
||||
result.ProcessedCount++;
|
||||
result.TotalCharsBefore += content.Length;
|
||||
spentChars += content.Length;
|
||||
if (content.Length <= softCharLimit && spentChars <= aggregateBudgetChars)
|
||||
continue;
|
||||
|
||||
var truncated = TruncateToolResultJson(content, softCharLimit);
|
||||
if (string.Equals(truncated, content, StringComparison.Ordinal))
|
||||
continue;
|
||||
|
||||
messages[i] = CloneMessage(message, truncated);
|
||||
result.TruncatedCount++;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static string TruncateToolResultJson(string json, int softCharLimit = DefaultSoftCharLimit)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
var root = doc.RootElement;
|
||||
var toolType = root.TryGetProperty("type", out var typeEl) ? typeEl.GetString() ?? "" : "";
|
||||
if (!string.Equals(toolType, "tool_result", StringComparison.Ordinal))
|
||||
return json;
|
||||
|
||||
var toolUseId = root.TryGetProperty("tool_use_id", out var idEl) ? idEl.GetString() ?? "" : "";
|
||||
var toolName = root.TryGetProperty("tool_name", out var nameEl) ? nameEl.GetString() ?? "" : "";
|
||||
var content = root.TryGetProperty("content", out var contentEl) ? contentEl.GetString() ?? "" : "";
|
||||
if (content.Length <= softCharLimit)
|
||||
return json;
|
||||
|
||||
var keepHead = Math.Min(360, content.Length);
|
||||
var keepTail = Math.Min(220, Math.Max(0, content.Length - keepHead));
|
||||
var head = content[..keepHead];
|
||||
var tail = keepTail > 0 ? content[^keepTail..] : "";
|
||||
var compacted = head +
|
||||
$"\n...[tool_result 축약: {content.Length:N0}자]...\n" +
|
||||
tail;
|
||||
|
||||
return JsonSerializer.Serialize(new
|
||||
{
|
||||
type = "tool_result",
|
||||
tool_use_id = toolUseId,
|
||||
tool_name = toolName,
|
||||
content = compacted
|
||||
});
|
||||
}
|
||||
catch
|
||||
{
|
||||
if (json.Length <= softCharLimit)
|
||||
return json;
|
||||
|
||||
var head = json[..Math.Min(softCharLimit, json.Length)];
|
||||
return head + "...[tool_result 축약]";
|
||||
}
|
||||
}
|
||||
|
||||
public static string CreateClearedToolResultJson(string json, string clearedContent)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
var root = doc.RootElement;
|
||||
var toolType = root.TryGetProperty("type", out var typeEl) ? typeEl.GetString() ?? "" : "";
|
||||
if (!string.Equals(toolType, "tool_result", StringComparison.Ordinal))
|
||||
return json;
|
||||
|
||||
var toolUseId = root.TryGetProperty("tool_use_id", out var idEl) ? idEl.GetString() ?? "" : "";
|
||||
var toolName = root.TryGetProperty("tool_name", out var nameEl) ? nameEl.GetString() ?? "" : "";
|
||||
return JsonSerializer.Serialize(new
|
||||
{
|
||||
type = "tool_result",
|
||||
tool_use_id = toolUseId,
|
||||
tool_name = toolName,
|
||||
content = clearedContent
|
||||
});
|
||||
}
|
||||
catch
|
||||
{
|
||||
return clearedContent;
|
||||
}
|
||||
}
|
||||
|
||||
private static ChatMessage CloneMessage(ChatMessage source, string content)
|
||||
{
|
||||
return new ChatMessage
|
||||
{
|
||||
MsgId = source.MsgId,
|
||||
Role = source.Role,
|
||||
Content = content,
|
||||
Timestamp = source.Timestamp,
|
||||
MetaKind = source.MetaKind,
|
||||
MetaRunId = source.MetaRunId,
|
||||
Feedback = source.Feedback,
|
||||
ResponseElapsedMs = source.ResponseElapsedMs,
|
||||
PromptTokens = source.PromptTokens,
|
||||
CompletionTokens = source.CompletionTokens,
|
||||
AttachedFiles = source.AttachedFiles?.ToList(),
|
||||
Images = source.Images?.Select(image => new ImageAttachment
|
||||
{
|
||||
Base64 = image.Base64,
|
||||
MimeType = image.MimeType,
|
||||
FileName = image.FileName,
|
||||
}).ToList(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
|
||||
namespace AxCopilot.Services.Agent;
|
||||
|
||||
@@ -125,7 +125,7 @@ internal static class AgentTranscriptDisplayCatalog
|
||||
{
|
||||
var summary = (evt.Summary ?? string.Empty).Trim();
|
||||
if (!string.IsNullOrWhiteSpace(summary))
|
||||
return summary;
|
||||
return StripNonBmpCharacters(summary);
|
||||
|
||||
return evt.Type switch
|
||||
{
|
||||
@@ -265,13 +265,16 @@ internal static class AgentTranscriptDisplayCatalog
|
||||
|
||||
if (resultPresentation != null)
|
||||
{
|
||||
// ToolResult의 GroupKey를 선행 ToolCall과 동일한 activity 그룹으로 설정
|
||||
// → ProcessFeed에서 ToolCall 카드를 ToolResult로 교체(머지)
|
||||
var resultGroup = ResolveActivityGroup(toolName, summary);
|
||||
return new AgentTranscriptRowPresentation(
|
||||
TranscriptRowKind.ToolResult,
|
||||
"결과",
|
||||
resultPresentation.Label,
|
||||
resultPresentation.Description,
|
||||
$"result:{resultPresentation.Kind}:{resultPresentation.StatusKind}",
|
||||
false,
|
||||
$"activity:{resultGroup}",
|
||||
true,
|
||||
resultPresentation.NeedsAttention);
|
||||
}
|
||||
|
||||
@@ -420,4 +423,32 @@ internal static class AgentTranscriptDisplayCatalog
|
||||
_ => "도구",
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// WPF 기본 폰트(Segoe UI)에서 렌더링되지 않는 비-BMP 유니코드 문자(이모지 등)를 제거합니다.
|
||||
/// LLM 응답에 이모지가 포함되면 깨져서 표시되는 문제를 방지합니다.
|
||||
/// </summary>
|
||||
public static string StripNonBmpCharacters(string text)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text)) return string.Empty;
|
||||
|
||||
// Consolas / Segoe UI에서 렌더링 불가한 비-BMP 유니코드(이모지, 서로게이트 쌍) 제거
|
||||
var sb = new System.Text.StringBuilder(text.Length);
|
||||
for (int i = 0; i < text.Length; i++)
|
||||
{
|
||||
var c = text[i];
|
||||
if (char.IsHighSurrogate(c))
|
||||
{
|
||||
if (i + 1 < text.Length && char.IsLowSurrogate(text[i + 1]))
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
if (char.IsLowSurrogate(c)) continue;
|
||||
if (c >= 0x2600 && c <= 0x27BF) continue; // Misc symbols, Dingbats
|
||||
if (c >= 0x2B50 && c <= 0x2B55) continue; // Additional symbols
|
||||
if (c >= 0xFE00 && c <= 0xFE0F) continue; // Variation selectors
|
||||
sb.Append(c);
|
||||
}
|
||||
return sb.ToString().Trim();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,7 +133,7 @@ public sealed class AxAgentExecutionEngine
|
||||
int completionTokens = 0,
|
||||
long? responseElapsedMs = null,
|
||||
string? metaRunId = null,
|
||||
ChatStorageService? storage = null)
|
||||
IChatStorageService? storage = null)
|
||||
{
|
||||
var assistant = new ChatMessage
|
||||
{
|
||||
@@ -177,7 +177,7 @@ public sealed class AxAgentExecutionEngine
|
||||
int completionTokens = 0,
|
||||
long? responseElapsedMs = null,
|
||||
string? metaRunId = null,
|
||||
ChatStorageService? storage = null)
|
||||
IChatStorageService? storage = null)
|
||||
{
|
||||
var normalized = NormalizeAssistantContentForUi(conversation, tab, content);
|
||||
if (tab is "Cowork" or "Code")
|
||||
@@ -209,6 +209,10 @@ public sealed class AxAgentExecutionEngine
|
||||
if (!string.IsNullOrWhiteSpace(content))
|
||||
return content;
|
||||
|
||||
if (runTab is "Cowork" or "Code")
|
||||
return TryBuildStrictExecutionCompletionMessage(conversation, runTab)
|
||||
?? "(실행은 종료되었지만 최종 요약 응답이 비어 있습니다. 실행 로그를 확인하세요.)";
|
||||
|
||||
return BuildFallbackCompletionMessage(conversation, runTab);
|
||||
}
|
||||
|
||||
@@ -230,7 +234,7 @@ public sealed class AxAgentExecutionEngine
|
||||
|
||||
public SessionMutationResult AppendExecutionEvent(
|
||||
ChatSessionStateService session,
|
||||
ChatStorageService storage,
|
||||
IChatStorageService storage,
|
||||
ChatConversation? activeConversation,
|
||||
string activeTab,
|
||||
string targetTab,
|
||||
@@ -247,7 +251,7 @@ public sealed class AxAgentExecutionEngine
|
||||
|
||||
public SessionMutationResult AppendAgentRun(
|
||||
ChatSessionStateService session,
|
||||
ChatStorageService storage,
|
||||
IChatStorageService storage,
|
||||
ChatConversation? activeConversation,
|
||||
string activeTab,
|
||||
string targetTab,
|
||||
@@ -272,6 +276,10 @@ public sealed class AxAgentExecutionEngine
|
||||
if (!string.IsNullOrWhiteSpace(content))
|
||||
return content;
|
||||
|
||||
if (runTab is "Cowork" or "Code")
|
||||
return TryBuildStrictExecutionCompletionMessage(conversation, runTab)
|
||||
?? "(실행은 종료되었지만 최종 요약 응답이 비어 있습니다. 실행 로그를 확인하세요.)";
|
||||
|
||||
return BuildFallbackCompletionMessage(conversation, runTab);
|
||||
}
|
||||
|
||||
@@ -280,6 +288,9 @@ public sealed class AxAgentExecutionEngine
|
||||
/// UserPromptSubmit/Paused/Resumed 같은 내부 운영 이벤트는 제외합니다.
|
||||
/// </summary>
|
||||
private static string BuildFallbackCompletionMessage(ChatConversation conversation, string runTab)
|
||||
=> TryBuildEvidenceBackedCompletionMessage(conversation, runTab) ?? "(빈 응답)";
|
||||
|
||||
private static string? TryBuildEvidenceBackedCompletionMessage(ChatConversation conversation, string runTab)
|
||||
{
|
||||
static bool IsSignificantEventType(string t)
|
||||
=> !string.Equals(t, "UserPromptSubmit", StringComparison.OrdinalIgnoreCase)
|
||||
@@ -290,7 +301,8 @@ public sealed class AxAgentExecutionEngine
|
||||
// 완료 메시지로 표시하면 안 되는 Thinking 이벤트 요약 패턴 (내부 진행 상태 문자열)
|
||||
static bool IsInternalStatusSummary(string? summary)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(summary)) return false;
|
||||
if (string.IsNullOrWhiteSpace(summary))
|
||||
return false;
|
||||
return summary.StartsWith("LLM에 요청 중", StringComparison.OrdinalIgnoreCase)
|
||||
|| summary.StartsWith("도구 미호출 루프", StringComparison.OrdinalIgnoreCase)
|
||||
|| summary.StartsWith("강제 실행 유도", StringComparison.OrdinalIgnoreCase)
|
||||
@@ -345,6 +357,68 @@ public sealed class AxAgentExecutionEngine
|
||||
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)
|
||||
{
|
||||
return new ChatMessage
|
||||
@@ -370,7 +444,7 @@ public sealed class AxAgentExecutionEngine
|
||||
|
||||
private static SessionMutationResult ApplyConversationMutation(
|
||||
ChatSessionStateService session,
|
||||
ChatStorageService storage,
|
||||
IChatStorageService storage,
|
||||
ChatConversation? activeConversation,
|
||||
string activeTab,
|
||||
string targetTab,
|
||||
|
||||
@@ -100,15 +100,18 @@ public class BuildRunTool : IAgentTool
|
||||
}
|
||||
else
|
||||
{
|
||||
// "run" 액션은 빌드로 대체 — 프로그램 실행은 사용자가 직접 수행
|
||||
command = action switch
|
||||
{
|
||||
"build" => project.BuildCommand,
|
||||
"test" => project.TestCommand,
|
||||
"run" => project.RunCommand,
|
||||
"run" => project.BuildCommand, // run → build로 대체 (실행은 사용자 몫)
|
||||
"lint" => string.IsNullOrEmpty(project.LintCommand) ? null : project.LintCommand,
|
||||
"format" => string.IsNullOrEmpty(project.FormatCommand) ? null : project.FormatCommand,
|
||||
_ => project.BuildCommand,
|
||||
};
|
||||
if (action == "run")
|
||||
action = "build"; // 출력 메시지에도 build로 표시
|
||||
if (command == null)
|
||||
return ToolResult.Fail($"이 프로젝트 타입({project.Type})에서 '{action}' 작업은 지원되지 않습니다.");
|
||||
}
|
||||
|
||||
@@ -40,8 +40,27 @@ public static class ContextCondenser
|
||||
|
||||
/// <summary>요약 시 유지할 최근 메시지 수</summary>
|
||||
private const int RecentKeepCount = 6;
|
||||
private const int AutoCompactBufferTokens = 13_000;
|
||||
private const int SummaryReserveTokens = 20_000;
|
||||
private const string TimeBasedClearedToolResultMessage = "[time-based microcompact] 이전 tool_result 내용이 정리되었습니다.";
|
||||
|
||||
/// <summary>모델별 입력 토큰 한도 (대략). 정확한 값은 중요하지 않음 — 안전 마진으로 70% 적용.</summary>
|
||||
private readonly record struct TimeBasedToolResultCompactionConfig(bool Enabled, int GapThresholdMinutes, int KeepRecentToolResults);
|
||||
|
||||
private sealed class CompactionWindow
|
||||
{
|
||||
public ChatMessage? SystemMessage { get; init; }
|
||||
public required List<ChatMessage> OldMessages { get; init; }
|
||||
public required List<ChatMessage> RecentMessages { get; init; }
|
||||
public int PreservedToolPairs { get; init; }
|
||||
}
|
||||
|
||||
private sealed class CompactionAttachmentSummary
|
||||
{
|
||||
public required List<string> AttachedFiles { get; init; }
|
||||
public int ImageCount { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>모델별 입력 토큰 한도 (대략).</summary>
|
||||
private static int GetModelInputLimit(string service, string model)
|
||||
{
|
||||
var key = $"{service}:{model}".ToLowerInvariant();
|
||||
@@ -59,6 +78,13 @@ public static class ContextCondenser
|
||||
};
|
||||
}
|
||||
|
||||
private static int GetEffectiveContextWindowSize(string service, string model, int configuredLimit)
|
||||
{
|
||||
var contextWindow = configuredLimit > 0 ? configuredLimit : GetModelInputLimit(service, model);
|
||||
var reservedForSummary = Math.Min(SummaryReserveTokens, Math.Max(4_000, contextWindow / 8));
|
||||
return Math.Max(8_000, contextWindow - reservedForSummary);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 메시지 목록의 토큰이 모델 한도에 근접하면 자동 압축합니다.
|
||||
/// 1단계: 도구 결과 축약 (빠르고 LLM 호출 없음)
|
||||
@@ -67,7 +93,7 @@ public static class ContextCondenser
|
||||
/// </summary>
|
||||
public static async Task<bool> CondenseIfNeededAsync(
|
||||
List<ChatMessage> messages,
|
||||
LlmService llm,
|
||||
ILlmService llm,
|
||||
int maxOutputTokens,
|
||||
bool proactiveEnabled = true,
|
||||
int triggerPercent = 80,
|
||||
@@ -80,7 +106,7 @@ public static class ContextCondenser
|
||||
|
||||
public static async Task<ContextCompactionResult> CondenseWithStatsAsync(
|
||||
List<ChatMessage> messages,
|
||||
LlmService llm,
|
||||
ILlmService llm,
|
||||
int maxOutputTokens,
|
||||
bool proactiveEnabled = true,
|
||||
int triggerPercent = 80,
|
||||
@@ -94,15 +120,25 @@ public static class ContextCondenser
|
||||
// 현재 모델의 입력 토큰 한도
|
||||
var settings = llm.GetCurrentModelInfo();
|
||||
// 사용자가 설정한 컨텍스트 크기를 우선 사용. 미설정 시 모델별 기본값 적용.
|
||||
var inputLimit = GetModelInputLimit(settings.service, settings.model);
|
||||
var effectiveMax = maxOutputTokens > 0 ? maxOutputTokens : inputLimit;
|
||||
var effectiveWindow = GetEffectiveContextWindowSize(settings.service, settings.model, maxOutputTokens);
|
||||
var percent = Math.Clamp(triggerPercent, 50, 95);
|
||||
var threshold = (int)(effectiveMax * (percent / 100.0)); // 설정 임계치에서 압축 시작
|
||||
var percentThreshold = (int)(effectiveWindow * (percent / 100.0));
|
||||
var bufferedThreshold = Math.Max(4_000, effectiveWindow - AutoCompactBufferTokens);
|
||||
var threshold = Math.Min(percentThreshold, bufferedThreshold);
|
||||
|
||||
var currentTokens = TokenEstimator.EstimateMessages(messages);
|
||||
result.BeforeTokens = currentTokens;
|
||||
|
||||
var timeBasedConfig = GetTimeBasedToolResultCompactionConfig(settings.service, settings.model);
|
||||
if (ApplyTimeBasedToolResultCompaction(messages, timeBasedConfig, out var clearedToolResults))
|
||||
{
|
||||
result.AppliedStages.Add($"time-gap-tool-result({clearedToolResults})");
|
||||
currentTokens = TokenEstimator.EstimateMessages(messages);
|
||||
}
|
||||
|
||||
if (!force && currentTokens < threshold)
|
||||
{
|
||||
result.Changed = result.AppliedStages.Count > 0;
|
||||
result.AfterTokens = currentTokens;
|
||||
return result;
|
||||
}
|
||||
@@ -202,15 +238,106 @@ public static class ContextCondenser
|
||||
return result;
|
||||
}
|
||||
|
||||
private static CompactionWindow SplitCompactionWindow(List<ChatMessage> messages, int extraTrailingProtection)
|
||||
{
|
||||
var systemMsg = messages.FirstOrDefault(m => m.Role == "system");
|
||||
var nonSystemMessages = messages.Where(m => m.Role != "system").ToList();
|
||||
var trailingCount = Math.Min(RecentKeepCount + Math.Max(0, extraTrailingProtection), nonSystemMessages.Count);
|
||||
var splitStart = Math.Max(0, nonSystemMessages.Count - trailingCount);
|
||||
var adjustedStart = AgentMessageInvariantHelper.AdjustStartIndexForToolPairs(
|
||||
nonSystemMessages,
|
||||
splitStart,
|
||||
out var preservedToolPairs);
|
||||
|
||||
return new CompactionWindow
|
||||
{
|
||||
SystemMessage = systemMsg,
|
||||
OldMessages = nonSystemMessages.Take(adjustedStart).ToList(),
|
||||
RecentMessages = nonSystemMessages.Skip(adjustedStart).ToList(),
|
||||
PreservedToolPairs = preservedToolPairs,
|
||||
};
|
||||
}
|
||||
|
||||
private static TimeBasedToolResultCompactionConfig GetTimeBasedToolResultCompactionConfig(string service, string model)
|
||||
{
|
||||
var key = $"{service}:{model}".ToLowerInvariant();
|
||||
return key switch
|
||||
{
|
||||
_ when key.Contains(string.Concat("cl", "aude")) => new TimeBasedToolResultCompactionConfig(true, 60, 5),
|
||||
_ when key.Contains("gemini") => new TimeBasedToolResultCompactionConfig(true, 45, 3),
|
||||
_ when key.Contains("gpt-4") => new TimeBasedToolResultCompactionConfig(true, 45, 3),
|
||||
_ when key.Contains("deepseek") => new TimeBasedToolResultCompactionConfig(true, 30, 2),
|
||||
_ when key.Contains("qwen") => new TimeBasedToolResultCompactionConfig(true, 20, 1),
|
||||
_ when key.Contains("llama") => new TimeBasedToolResultCompactionConfig(true, 20, 1),
|
||||
_ when key.Contains("vllm") => new TimeBasedToolResultCompactionConfig(true, 20, 1),
|
||||
_ => new TimeBasedToolResultCompactionConfig(true, 30, 2),
|
||||
};
|
||||
}
|
||||
|
||||
private static bool ApplyTimeBasedToolResultCompaction(
|
||||
List<ChatMessage> messages,
|
||||
TimeBasedToolResultCompactionConfig config,
|
||||
out int clearedCount)
|
||||
{
|
||||
clearedCount = 0;
|
||||
if (!config.Enabled)
|
||||
return false;
|
||||
|
||||
var lastAssistant = messages
|
||||
.Where(m => string.Equals(m.Role, "assistant", StringComparison.OrdinalIgnoreCase))
|
||||
.OrderByDescending(m => m.Timestamp)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (lastAssistant?.Timestamp is not DateTime lastAssistantAt)
|
||||
return false;
|
||||
|
||||
var gapMinutes = (DateTime.Now - lastAssistantAt).TotalMinutes;
|
||||
if (!double.IsFinite(gapMinutes) || gapMinutes < config.GapThresholdMinutes)
|
||||
return false;
|
||||
|
||||
var candidateIndexes = messages
|
||||
.Select((message, index) => new { message, index })
|
||||
.Where(x => AgentMessageInvariantHelper.TryGetToolResultId(x.message, out _))
|
||||
.Select(x => x.index)
|
||||
.ToList();
|
||||
|
||||
if (candidateIndexes.Count <= config.KeepRecentToolResults)
|
||||
return false;
|
||||
|
||||
var keepSet = candidateIndexes
|
||||
.TakeLast(Math.Max(1, config.KeepRecentToolResults))
|
||||
.ToHashSet();
|
||||
|
||||
foreach (var index in candidateIndexes)
|
||||
{
|
||||
if (keepSet.Contains(index))
|
||||
continue;
|
||||
|
||||
var message = messages[index];
|
||||
var content = message.Content ?? "";
|
||||
if (string.IsNullOrWhiteSpace(content))
|
||||
continue;
|
||||
|
||||
var cleared = AgentToolResultBudget.CreateClearedToolResultJson(content, TimeBasedClearedToolResultMessage);
|
||||
if (string.Equals(cleared, content, StringComparison.Ordinal))
|
||||
continue;
|
||||
|
||||
messages[index] = CloneWithContent(message, cleared);
|
||||
clearedCount++;
|
||||
}
|
||||
|
||||
return clearedCount > 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 1단계: 대용량 도구 결과를 축약합니다.
|
||||
/// tool_result JSON이나 긴 파일 내용 등을 핵심만 남기고 자릅니다.
|
||||
/// 1단계: 오래된 tool_result는 aggregate budget 기준으로 먼저 줄이고,
|
||||
/// 그 외 긴 assistant/user 메시지는 경량 절단합니다.
|
||||
/// </summary>
|
||||
private static bool TruncateToolResults(List<ChatMessage> messages)
|
||||
{
|
||||
bool truncated = false;
|
||||
var budgetResult = AgentToolResultBudget.Apply(messages, RecentKeepCount);
|
||||
bool truncated = budgetResult.TruncatedCount > 0;
|
||||
|
||||
// 최근 RecentKeepCount개는 건드리지 않음 (방금 실행한 도구 결과는 유지)
|
||||
var cutoff = Math.Max(0, messages.Count - RecentKeepCount);
|
||||
|
||||
for (int i = 0; i < cutoff; i++)
|
||||
@@ -218,25 +345,20 @@ public static class ContextCondenser
|
||||
var msg = messages[i];
|
||||
if (msg.Content == null) continue;
|
||||
|
||||
// tool_result 메시지의 대용량 출력 축약
|
||||
if (msg.Content.StartsWith("{\"type\":\"tool_result\"") && msg.Content.Length > MaxToolResultChars)
|
||||
if (AgentMessageInvariantHelper.TryGetToolResultId(msg, out _))
|
||||
{
|
||||
// JSON 구조를 유지하되 output 부분만 축약
|
||||
messages[i] = CloneWithContent(msg, TruncateToolResultJson(msg.Content));
|
||||
truncated = true;
|
||||
continue;
|
||||
}
|
||||
// assistant의 도구 호출 블록 내 긴 텍스트도 축약
|
||||
else if (msg.Role == "assistant" && msg.Content.Length > MaxToolResultChars * 2 &&
|
||||
|
||||
if (msg.Role == "assistant" && msg.Content.Length > MaxToolResultChars * 2 &&
|
||||
msg.Content.StartsWith("{\"_tool_use_blocks\""))
|
||||
{
|
||||
// 도구 호출 구조는 유지, 텍스트 블록만 축약
|
||||
if (msg.Content.Length > MaxToolResultChars * 3)
|
||||
{
|
||||
messages[i] = CloneWithContent(msg, msg.Content[..(MaxToolResultChars * 2)] + "...[축약됨]\"]}");
|
||||
truncated = true;
|
||||
}
|
||||
}
|
||||
// 일반 assistant/user 메시지 중 비정상적으로 긴 것 (예: 파일 내용 전체 붙여넣기)
|
||||
else if (msg.Content.Length > MaxToolResultChars * 3 && msg.Role != "system")
|
||||
{
|
||||
messages[i] = CloneWithContent(
|
||||
@@ -276,13 +398,10 @@ public static class ContextCondenser
|
||||
/// </summary>
|
||||
private static (int BoundaryCount, int SingleCount) MicrocompactOlderMessages(List<ChatMessage> messages)
|
||||
{
|
||||
var systemMsg = messages.FirstOrDefault(m => m.Role == "system");
|
||||
var nonSystemMessages = messages.Where(m => m.Role != "system").ToList();
|
||||
if (nonSystemMessages.Count <= RecentKeepCount + 2) return (0, 0);
|
||||
|
||||
var keepCount = Math.Min(RecentKeepCount, nonSystemMessages.Count);
|
||||
var recentMessages = nonSystemMessages.Skip(nonSystemMessages.Count - keepCount).ToList();
|
||||
var oldMessages = nonSystemMessages.Take(nonSystemMessages.Count - keepCount).ToList();
|
||||
var window = SplitCompactionWindow(messages, extraTrailingProtection: 2);
|
||||
if (window.OldMessages.Count == 0 || window.RecentMessages.Count <= RecentKeepCount) return (0, 0);
|
||||
var oldMessages = window.OldMessages;
|
||||
var recentMessages = window.RecentMessages;
|
||||
|
||||
var rewritten = new List<ChatMessage>(oldMessages.Count);
|
||||
bool changed = false;
|
||||
@@ -337,7 +456,7 @@ public static class ContextCondenser
|
||||
if (!changed) return (0, 0);
|
||||
|
||||
messages.Clear();
|
||||
if (systemMsg != null) messages.Add(systemMsg);
|
||||
if (window.SystemMessage != null) messages.Add(window.SystemMessage);
|
||||
messages.AddRange(rewritten);
|
||||
messages.AddRange(recentMessages);
|
||||
return (boundaryCount, singleCount);
|
||||
@@ -345,13 +464,10 @@ public static class ContextCondenser
|
||||
|
||||
private static int SessionMemoryCompactOlderMessages(List<ChatMessage> messages)
|
||||
{
|
||||
var systemMsg = messages.FirstOrDefault(m => m.Role == "system");
|
||||
var nonSystemMessages = messages.Where(m => m.Role != "system").ToList();
|
||||
if (nonSystemMessages.Count <= RecentKeepCount + 3) return 0;
|
||||
|
||||
var keepCount = Math.Min(RecentKeepCount, nonSystemMessages.Count);
|
||||
var recentMessages = nonSystemMessages.Skip(nonSystemMessages.Count - keepCount).ToList();
|
||||
var oldMessages = nonSystemMessages.Take(nonSystemMessages.Count - keepCount).ToList();
|
||||
var window = SplitCompactionWindow(messages, extraTrailingProtection: 3);
|
||||
if (window.OldMessages.Count == 0 || window.RecentMessages.Count <= RecentKeepCount) return 0;
|
||||
var oldMessages = window.OldMessages;
|
||||
var recentMessages = window.RecentMessages;
|
||||
|
||||
var candidates = oldMessages
|
||||
.Select((message, index) => new { message, index })
|
||||
@@ -405,7 +521,7 @@ public static class ContextCondenser
|
||||
}
|
||||
|
||||
messages.Clear();
|
||||
if (systemMsg != null) messages.Add(systemMsg);
|
||||
if (window.SystemMessage != null) messages.Add(window.SystemMessage);
|
||||
messages.AddRange(rewritten);
|
||||
messages.AddRange(recentMessages);
|
||||
return candidates.Count;
|
||||
@@ -427,13 +543,10 @@ public static class ContextCondenser
|
||||
|
||||
private static (int SnippedCount, int CollapsedCount) CollapseAndSnipOlderMessages(List<ChatMessage> messages)
|
||||
{
|
||||
var systemMsg = messages.FirstOrDefault(m => m.Role == "system");
|
||||
var nonSystemMessages = messages.Where(m => m.Role != "system").ToList();
|
||||
if (nonSystemMessages.Count <= RecentKeepCount + 2) return (0, 0);
|
||||
|
||||
var keepCount = Math.Min(RecentKeepCount, nonSystemMessages.Count);
|
||||
var recentMessages = nonSystemMessages.Skip(nonSystemMessages.Count - keepCount).ToList();
|
||||
var oldMessages = nonSystemMessages.Take(nonSystemMessages.Count - keepCount).ToList();
|
||||
var window = SplitCompactionWindow(messages, extraTrailingProtection: 2);
|
||||
if (window.OldMessages.Count == 0 || window.RecentMessages.Count <= RecentKeepCount) return (0, 0);
|
||||
var oldMessages = window.OldMessages;
|
||||
var recentMessages = window.RecentMessages;
|
||||
|
||||
var rewritten = new List<ChatMessage>(oldMessages.Count);
|
||||
int snippedCount = 0;
|
||||
@@ -484,7 +597,7 @@ public static class ContextCondenser
|
||||
if (snippedCount == 0 && collapsedCount == 0) return (0, 0);
|
||||
|
||||
messages.Clear();
|
||||
if (systemMsg != null) messages.Add(systemMsg);
|
||||
if (window.SystemMessage != null) messages.Add(window.SystemMessage);
|
||||
messages.AddRange(rewritten);
|
||||
messages.AddRange(recentMessages);
|
||||
return (snippedCount, collapsedCount);
|
||||
@@ -596,17 +709,7 @@ public static class ContextCondenser
|
||||
!(m.Content ?? "").StartsWith("{\"_tool_use_blocks\"", StringComparison.Ordinal) &&
|
||||
m.MetaKind == null);
|
||||
|
||||
var attachedFiles = group
|
||||
.SelectMany(m => m.AttachedFiles ?? Enumerable.Empty<string>())
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.Select(path =>
|
||||
{
|
||||
try { return System.IO.Path.GetFileName(path); }
|
||||
catch { return path; }
|
||||
})
|
||||
.Where(name => !string.IsNullOrWhiteSpace(name))
|
||||
.Take(4)
|
||||
.ToList();
|
||||
var attachmentSummary = CollectCompactionAttachmentSummary(group, 4);
|
||||
|
||||
var lines = new List<string>
|
||||
{
|
||||
@@ -617,7 +720,8 @@ public static class ContextCondenser
|
||||
if (toolCalls > 0) lines.Add($"- 도구 호출 {toolCalls}건 정리");
|
||||
if (metaEvents > 0) lines.Add($"- 실행 메타/로그 {metaEvents}건 정리");
|
||||
if (longTexts > 0) lines.Add($"- 긴 설명/출력 {longTexts}건 축약");
|
||||
if (attachedFiles.Count > 0) lines.Add($"- 관련 파일: {string.Join(", ", attachedFiles)}");
|
||||
if (attachmentSummary.AttachedFiles.Count > 0) lines.Add($"- 관련 파일: {string.Join(", ", attachmentSummary.AttachedFiles)}");
|
||||
if (attachmentSummary.ImageCount > 0) lines.Add($"- 관련 이미지: {attachmentSummary.ImageCount}개");
|
||||
|
||||
return new ChatMessage
|
||||
{
|
||||
@@ -626,57 +730,22 @@ public static class ContextCondenser
|
||||
Timestamp = group.Last().Timestamp,
|
||||
MetaKind = "microcompact_boundary",
|
||||
MetaRunId = group.Last().MetaRunId,
|
||||
AttachedFiles = attachmentSummary.AttachedFiles.Count > 0 ? attachmentSummary.AttachedFiles : null,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>tool_result JSON 내의 output 값을 축약합니다.</summary>
|
||||
private static string TruncateToolResultJson(string json)
|
||||
{
|
||||
// 간단한 문자열 처리로 output 부분만 축약 (JSON 파서 없이)
|
||||
const string marker = "\"output\":\"";
|
||||
var idx = json.IndexOf(marker, StringComparison.Ordinal);
|
||||
if (idx < 0) return json[..Math.Min(json.Length, MaxToolResultChars)] + "...[축약됨]}";
|
||||
|
||||
var outputStart = idx + marker.Length;
|
||||
// output 끝 찾기 (이스케이프된 따옴표 고려)
|
||||
var outputEnd = outputStart;
|
||||
while (outputEnd < json.Length)
|
||||
{
|
||||
if (json[outputEnd] == '\\') { outputEnd += 2; continue; }
|
||||
if (json[outputEnd] == '"') break;
|
||||
outputEnd++;
|
||||
}
|
||||
|
||||
var outputLen = outputEnd - outputStart;
|
||||
if (outputLen <= MaxToolResultChars) return json; // 이미 짧음
|
||||
|
||||
// 앞부분 + "...[축약됨]" + 뒷부분
|
||||
var keepLen = MaxToolResultChars / 2;
|
||||
var prefix = json[..outputStart];
|
||||
var outputText = json[outputStart..outputEnd];
|
||||
var suffix = json[outputEnd..];
|
||||
|
||||
return prefix +
|
||||
outputText[..keepLen] +
|
||||
"\\n...[축약됨: " + $"{outputLen:N0}" + "자 중 " + $"{MaxToolResultChars:N0}" + "자 유지]\\n" +
|
||||
outputText[^keepLen..] +
|
||||
suffix;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 2단계: 오래된 메시지를 LLM으로 요약합니다.
|
||||
/// 시스템 메시지 + 최근 N개는 유지하고, 나머지를 요약으로 교체합니다.
|
||||
/// </summary>
|
||||
private static async Task<bool> SummarizeOldMessagesAsync(
|
||||
List<ChatMessage> messages, LlmService llm, CancellationToken ct)
|
||||
List<ChatMessage> messages, ILlmService llm, CancellationToken ct)
|
||||
{
|
||||
var systemMsg = messages.FirstOrDefault(m => m.Role == "system");
|
||||
var systemCount = systemMsg != null ? 1 : 0;
|
||||
var nonSystemMessages = messages.Where(m => m.Role != "system").ToList();
|
||||
|
||||
var keepCount = Math.Min(RecentKeepCount, nonSystemMessages.Count);
|
||||
var recentMessages = nonSystemMessages.Skip(nonSystemMessages.Count - keepCount).ToList();
|
||||
var oldMessages = nonSystemMessages.Take(nonSystemMessages.Count - keepCount).ToList();
|
||||
var window = SplitCompactionWindow(messages, extraTrailingProtection: 0);
|
||||
var systemMsg = window.SystemMessage;
|
||||
var oldMessages = window.OldMessages;
|
||||
var recentMessages = window.RecentMessages;
|
||||
var attachmentSummary = CollectCompactionAttachmentSummary(oldMessages, 6);
|
||||
|
||||
if (oldMessages.Count < 3) return false;
|
||||
|
||||
@@ -717,8 +786,9 @@ public static class ContextCondenser
|
||||
messages.Add(new ChatMessage
|
||||
{
|
||||
Role = "user",
|
||||
Content = $"[이전 대화 요약 — {oldMessages.Count}개 메시지 압축]\n{summary}",
|
||||
Content = BuildSummaryMessageContent(oldMessages.Count, summary, attachmentSummary),
|
||||
Timestamp = DateTime.Now,
|
||||
AttachedFiles = attachmentSummary.AttachedFiles.Count > 0 ? attachmentSummary.AttachedFiles : null,
|
||||
});
|
||||
messages.Add(new ChatMessage
|
||||
{
|
||||
@@ -736,4 +806,42 @@ public static class ContextCondenser
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static CompactionAttachmentSummary CollectCompactionAttachmentSummary(IEnumerable<ChatMessage> messages, int maxFiles)
|
||||
{
|
||||
var attachedFiles = messages
|
||||
.SelectMany(m => m.AttachedFiles ?? Enumerable.Empty<string>())
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.Select(path =>
|
||||
{
|
||||
try { return System.IO.Path.GetFileName(path); }
|
||||
catch { return path; }
|
||||
})
|
||||
.Where(name => !string.IsNullOrWhiteSpace(name))
|
||||
.Take(Math.Max(1, maxFiles))
|
||||
.ToList();
|
||||
|
||||
var imageCount = messages.Sum(m => m.Images?.Count ?? 0);
|
||||
return new CompactionAttachmentSummary
|
||||
{
|
||||
AttachedFiles = attachedFiles,
|
||||
ImageCount = imageCount,
|
||||
};
|
||||
}
|
||||
|
||||
private static string BuildSummaryMessageContent(int oldMessageCount, string summary, CompactionAttachmentSummary attachmentSummary)
|
||||
{
|
||||
var lines = new List<string>
|
||||
{
|
||||
$"[이전 대화 요약 — {oldMessageCount}개 메시지 압축]",
|
||||
summary.Trim()
|
||||
};
|
||||
|
||||
if (attachmentSummary.AttachedFiles.Count > 0)
|
||||
lines.Add($"참고 파일: {string.Join(", ", attachmentSummary.AttachedFiles)}");
|
||||
if (attachmentSummary.ImageCount > 0)
|
||||
lines.Add($"참고 이미지: {attachmentSummary.ImageCount}개");
|
||||
|
||||
return string.Join("\n", lines.Where(x => !string.IsNullOrWhiteSpace(x)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,7 +135,7 @@ public class DocumentAssemblerTool : IAgentTool
|
||||
var pageEstimate = Math.Max(1, totalWords / 500);
|
||||
|
||||
return ToolResult.Ok(
|
||||
$"✅ 문서 조립 완료: {Path.GetFileName(fullPath)}\n" +
|
||||
$"✅ 문서 조립 완료: {fullPath}\n" +
|
||||
$" 섹션: {sections.Count}개 | 글자: {totalChars:N0} | 단어: ~{totalWords:N0} | 예상 페이지: ~{pageEstimate}\n" +
|
||||
$"{resultMsg}", fullPath);
|
||||
}
|
||||
@@ -241,14 +241,30 @@ public class DocumentAssemblerTool : IAgentTool
|
||||
path, DocumentFormat.OpenXml.WordprocessingDocumentType.Document);
|
||||
|
||||
var mainPart = doc.AddMainDocumentPart();
|
||||
|
||||
// 기본 스타일 파트 추가 (styles.xml — 없으면 Word에서 글꼴/서식 깨짐)
|
||||
var stylesPart = mainPart.AddNewPart<DocumentFormat.OpenXml.Packaging.StyleDefinitionsPart>();
|
||||
stylesPart.Styles = CreateDefaultDocxStyles();
|
||||
stylesPart.Styles.Save();
|
||||
|
||||
mainPart.Document = new DocumentFormat.OpenXml.Wordprocessing.Document();
|
||||
var body = mainPart.Document.AppendChild(new DocumentFormat.OpenXml.Wordprocessing.Body());
|
||||
|
||||
// 한글 호환 글꼴 설정 헬퍼
|
||||
static DocumentFormat.OpenXml.Wordprocessing.RunFonts KoreanFonts() => new()
|
||||
{
|
||||
Ascii = "맑은 고딕",
|
||||
HighAnsi = "맑은 고딕",
|
||||
EastAsia = "맑은 고딕",
|
||||
ComplexScript = "맑은 고딕"
|
||||
};
|
||||
|
||||
// 제목
|
||||
var titlePara = new DocumentFormat.OpenXml.Wordprocessing.Paragraph();
|
||||
var titleRun = new DocumentFormat.OpenXml.Wordprocessing.Run();
|
||||
titleRun.AppendChild(new DocumentFormat.OpenXml.Wordprocessing.RunProperties
|
||||
{
|
||||
RunFonts = KoreanFonts(),
|
||||
Bold = new DocumentFormat.OpenXml.Wordprocessing.Bold(),
|
||||
FontSize = new DocumentFormat.OpenXml.Wordprocessing.FontSize { Val = "48" }
|
||||
});
|
||||
@@ -267,6 +283,7 @@ public class DocumentAssemblerTool : IAgentTool
|
||||
var headRun = new DocumentFormat.OpenXml.Wordprocessing.Run();
|
||||
headRun.AppendChild(new DocumentFormat.OpenXml.Wordprocessing.RunProperties
|
||||
{
|
||||
RunFonts = KoreanFonts(),
|
||||
Bold = new DocumentFormat.OpenXml.Wordprocessing.Bold(),
|
||||
FontSize = new DocumentFormat.OpenXml.Wordprocessing.FontSize { Val = level <= 1 ? "32" : "28" },
|
||||
Color = new DocumentFormat.OpenXml.Wordprocessing.Color { Val = "2B579A" },
|
||||
@@ -281,6 +298,11 @@ public class DocumentAssemblerTool : IAgentTool
|
||||
{
|
||||
var para = new DocumentFormat.OpenXml.Wordprocessing.Paragraph();
|
||||
var run = new DocumentFormat.OpenXml.Wordprocessing.Run();
|
||||
run.AppendChild(new DocumentFormat.OpenXml.Wordprocessing.RunProperties
|
||||
{
|
||||
RunFonts = KoreanFonts(),
|
||||
FontSize = new DocumentFormat.OpenXml.Wordprocessing.FontSize { Val = "22" }
|
||||
});
|
||||
run.AppendChild(new DocumentFormat.OpenXml.Wordprocessing.Text(line.Trim())
|
||||
{
|
||||
Space = DocumentFormat.OpenXml.SpaceProcessingModeValues.Preserve
|
||||
@@ -293,9 +315,59 @@ public class DocumentAssemblerTool : IAgentTool
|
||||
body.AppendChild(new DocumentFormat.OpenXml.Wordprocessing.Paragraph());
|
||||
}
|
||||
|
||||
// ★ SectionProperties는 반드시 body의 마지막 자식이어야 함 (OOXML 규격)
|
||||
// 첫 번째에 넣으면 Word가 무시하거나 문서가 깨짐
|
||||
body.AppendChild(new DocumentFormat.OpenXml.Wordprocessing.SectionProperties(
|
||||
new DocumentFormat.OpenXml.Wordprocessing.PageSize { Width = 11906, Height = 16838 },
|
||||
new DocumentFormat.OpenXml.Wordprocessing.PageMargin { Top = 1440, Right = 1440, Bottom = 1440, Left = 1440,
|
||||
Header = 720, Footer = 720, Gutter = 0 }
|
||||
));
|
||||
|
||||
mainPart.Document.Save();
|
||||
return " ✓ DOCX 조립 완료";
|
||||
}
|
||||
|
||||
/// <summary>DOCX 기본 스타일 정의 생성 (한글 글꼴 기본 설정 포함).</summary>
|
||||
private static DocumentFormat.OpenXml.Wordprocessing.Styles CreateDefaultDocxStyles()
|
||||
{
|
||||
var styles = new DocumentFormat.OpenXml.Wordprocessing.Styles();
|
||||
|
||||
// 문서 기본 글꼴 설정
|
||||
var docDefaults = new DocumentFormat.OpenXml.Wordprocessing.DocDefaults(
|
||||
new DocumentFormat.OpenXml.Wordprocessing.RunPropertiesDefault(
|
||||
new DocumentFormat.OpenXml.Wordprocessing.RunPropertiesBaseStyle(
|
||||
new DocumentFormat.OpenXml.Wordprocessing.RunFonts
|
||||
{
|
||||
Ascii = "맑은 고딕",
|
||||
HighAnsi = "맑은 고딕",
|
||||
EastAsia = "맑은 고딕",
|
||||
ComplexScript = "맑은 고딕"
|
||||
},
|
||||
new DocumentFormat.OpenXml.Wordprocessing.FontSize { Val = "22" },
|
||||
new DocumentFormat.OpenXml.Wordprocessing.Languages { Val = "ko-KR", EastAsia = "ko-KR" }
|
||||
)
|
||||
),
|
||||
new DocumentFormat.OpenXml.Wordprocessing.ParagraphPropertiesDefault(
|
||||
new DocumentFormat.OpenXml.Wordprocessing.ParagraphPropertiesBaseStyle(
|
||||
new DocumentFormat.OpenXml.Wordprocessing.SpacingBetweenLines { After = "160", Line = "259", LineRule = DocumentFormat.OpenXml.Wordprocessing.LineSpacingRuleValues.Auto }
|
||||
)
|
||||
)
|
||||
);
|
||||
styles.AppendChild(docDefaults);
|
||||
|
||||
// Normal 스타일
|
||||
var normalStyle = new DocumentFormat.OpenXml.Wordprocessing.Style
|
||||
{
|
||||
Type = DocumentFormat.OpenXml.Wordprocessing.StyleValues.Paragraph,
|
||||
StyleId = "Normal",
|
||||
Default = true
|
||||
};
|
||||
normalStyle.AppendChild(new DocumentFormat.OpenXml.Wordprocessing.StyleName { Val = "Normal" });
|
||||
styles.AppendChild(normalStyle);
|
||||
|
||||
return styles;
|
||||
}
|
||||
|
||||
private string AssembleMarkdown(string path, string title, List<(string Heading, string Content, int Level)> sections)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
@@ -316,7 +316,7 @@ public class DocumentPlannerTool : IAgentTool
|
||||
folder_data_usage = folderDataUsage,
|
||||
step1 = hasRefData
|
||||
? "Reference data is already provided above. Skip to step 3."
|
||||
: "Use folder_map to scan the work folder, then use document_read to read RELEVANT files only.",
|
||||
: "Use glob or grep to narrow relevant files first, then use document_read or file_read on the best candidates only. Use folder_map only when the user explicitly asks for folder structure or existing materials in a folder.",
|
||||
step2 = hasRefData
|
||||
? "(skipped)"
|
||||
: "Summarize the key findings from the folder documents relevant to the topic.",
|
||||
@@ -420,6 +420,12 @@ public class DocumentPlannerTool : IAgentTool
|
||||
path, DocumentFormat.OpenXml.WordprocessingDocumentType.Document);
|
||||
|
||||
var mainPart = doc.AddMainDocumentPart();
|
||||
|
||||
// 기본 스타일 파트 추가 (styles.xml — 없으면 Word에서 글꼴/서식 깨짐)
|
||||
var stylesPart = mainPart.AddNewPart<DocumentFormat.OpenXml.Packaging.StyleDefinitionsPart>();
|
||||
stylesPart.Styles = CreateDefaultDocxStyles();
|
||||
stylesPart.Styles.Save();
|
||||
|
||||
mainPart.Document = new DocumentFormat.OpenXml.Wordprocessing.Document();
|
||||
var body = mainPart.Document.AppendChild(new DocumentFormat.OpenXml.Wordprocessing.Body());
|
||||
|
||||
@@ -438,6 +444,15 @@ public class DocumentPlannerTool : IAgentTool
|
||||
}
|
||||
AddDocxParagraph(body, ""); // 섹션 간 빈 줄
|
||||
}
|
||||
|
||||
// ★ SectionProperties는 반드시 body의 마지막 자식이어야 함 (OOXML 규격)
|
||||
body.AppendChild(new DocumentFormat.OpenXml.Wordprocessing.SectionProperties(
|
||||
new DocumentFormat.OpenXml.Wordprocessing.PageSize { Width = 11906, Height = 16838 },
|
||||
new DocumentFormat.OpenXml.Wordprocessing.PageMargin { Top = 1440, Right = 1440, Bottom = 1440, Left = 1440,
|
||||
Header = 720, Footer = 720, Gutter = 0 }
|
||||
));
|
||||
|
||||
mainPart.Document.Save();
|
||||
}
|
||||
|
||||
private static void AddDocxParagraph(DocumentFormat.OpenXml.Wordprocessing.Body body,
|
||||
@@ -447,6 +462,13 @@ public class DocumentPlannerTool : IAgentTool
|
||||
var run = new DocumentFormat.OpenXml.Wordprocessing.Run();
|
||||
var props = new DocumentFormat.OpenXml.Wordprocessing.RunProperties
|
||||
{
|
||||
RunFonts = new DocumentFormat.OpenXml.Wordprocessing.RunFonts
|
||||
{
|
||||
Ascii = "맑은 고딕",
|
||||
HighAnsi = "맑은 고딕",
|
||||
EastAsia = "맑은 고딕",
|
||||
ComplexScript = "맑은 고딕"
|
||||
},
|
||||
FontSize = new DocumentFormat.OpenXml.Wordprocessing.FontSize { Val = fontSize }
|
||||
};
|
||||
if (bold) props.Bold = new DocumentFormat.OpenXml.Wordprocessing.Bold();
|
||||
@@ -460,6 +482,45 @@ public class DocumentPlannerTool : IAgentTool
|
||||
body.AppendChild(para);
|
||||
}
|
||||
|
||||
/// <summary>DOCX 기본 스타일 정의 생성 (한글 글꼴 기본 설정 포함).</summary>
|
||||
private static DocumentFormat.OpenXml.Wordprocessing.Styles CreateDefaultDocxStyles()
|
||||
{
|
||||
var styles = new DocumentFormat.OpenXml.Wordprocessing.Styles();
|
||||
|
||||
var docDefaults = new DocumentFormat.OpenXml.Wordprocessing.DocDefaults(
|
||||
new DocumentFormat.OpenXml.Wordprocessing.RunPropertiesDefault(
|
||||
new DocumentFormat.OpenXml.Wordprocessing.RunPropertiesBaseStyle(
|
||||
new DocumentFormat.OpenXml.Wordprocessing.RunFonts
|
||||
{
|
||||
Ascii = "맑은 고딕",
|
||||
HighAnsi = "맑은 고딕",
|
||||
EastAsia = "맑은 고딕",
|
||||
ComplexScript = "맑은 고딕"
|
||||
},
|
||||
new DocumentFormat.OpenXml.Wordprocessing.FontSize { Val = "22" },
|
||||
new DocumentFormat.OpenXml.Wordprocessing.Languages { Val = "ko-KR", EastAsia = "ko-KR" }
|
||||
)
|
||||
),
|
||||
new DocumentFormat.OpenXml.Wordprocessing.ParagraphPropertiesDefault(
|
||||
new DocumentFormat.OpenXml.Wordprocessing.ParagraphPropertiesBaseStyle(
|
||||
new DocumentFormat.OpenXml.Wordprocessing.SpacingBetweenLines { After = "160", Line = "259", LineRule = DocumentFormat.OpenXml.Wordprocessing.LineSpacingRuleValues.Auto }
|
||||
)
|
||||
)
|
||||
);
|
||||
styles.AppendChild(docDefaults);
|
||||
|
||||
var normalStyle = new DocumentFormat.OpenXml.Wordprocessing.Style
|
||||
{
|
||||
Type = DocumentFormat.OpenXml.Wordprocessing.StyleValues.Paragraph,
|
||||
StyleId = "Normal",
|
||||
Default = true
|
||||
};
|
||||
normalStyle.AppendChild(new DocumentFormat.OpenXml.Wordprocessing.StyleName { Val = "Normal" });
|
||||
styles.AppendChild(normalStyle);
|
||||
|
||||
return styles;
|
||||
}
|
||||
|
||||
// ─── Markdown 생성 ──────────────────────────────────────────────────────
|
||||
|
||||
private static void GenerateMarkdown(string path, string title, List<SectionPlan> sections)
|
||||
@@ -631,7 +692,7 @@ public class DocumentPlannerTool : IAgentTool
|
||||
return normalized;
|
||||
|
||||
var intent = $"{docType} {topic}".ToLowerInvariant();
|
||||
if (ContainsAny(intent, "대시보드", "dashboard", "분석", "analysis", "지표", "통계"))
|
||||
if (ContainsAny(intent, "대시보드", "dashboard", "지표"))
|
||||
return "dashboard";
|
||||
if (ContainsAny(intent, "기획", "아이디어", "creative", "브레인스토밍"))
|
||||
return "creative";
|
||||
@@ -639,11 +700,15 @@ public class DocumentPlannerTool : IAgentTool
|
||||
return "corporate";
|
||||
if (ContainsAny(intent, "가이드", "manual", "guide", "매뉴얼"))
|
||||
return "minimal";
|
||||
// 보고서/분석: corporate 무드 (배경색 포함)
|
||||
if (ContainsAny(intent, "보고서", "report", "분석", "analysis", "통계"))
|
||||
return "corporate";
|
||||
|
||||
return docType switch
|
||||
{
|
||||
"proposal" => "corporate",
|
||||
"analysis" => "dashboard",
|
||||
"analysis" => "corporate",
|
||||
"report" => "corporate",
|
||||
"manual" or "guide" => "minimal",
|
||||
"presentation" => "creative",
|
||||
"minutes" => "professional",
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
using DocumentFormat.OpenXml.Packaging;
|
||||
using DocumentFormat.OpenXml.Spreadsheet;
|
||||
using A = DocumentFormat.OpenXml.Drawing;
|
||||
using UglyToad.PdfPig;
|
||||
|
||||
namespace AxCopilot.Services.Agent;
|
||||
@@ -17,7 +18,7 @@ public class DocumentReaderTool : IAgentTool
|
||||
public string Name => "document_read";
|
||||
public string Description =>
|
||||
"Read a document file and extract its text content. " +
|
||||
"Supports: PDF (.pdf), Word (.docx), Excel (.xlsx), CSV (.csv), text (.txt/.log/.json/.xml/.md), " +
|
||||
"Supports: PDF (.pdf), Word (.docx), PowerPoint (.pptx), Excel (.xlsx), CSV (.csv), text (.txt/.log/.json/.xml/.md), " +
|
||||
"BibTeX (.bib), RIS (.ris). " +
|
||||
"For large files, use 'offset' to read from a specific character position (chunked reading). " +
|
||||
"For large PDFs, use 'pages' parameter to read specific page ranges (e.g., '1-5', '10-20'). " +
|
||||
@@ -79,6 +80,7 @@ public class DocumentReaderTool : IAgentTool
|
||||
{
|
||||
".pdf" => await Task.Run(() => ReadPdf(fullPath, extractMax, pagesParam, sectionParam), ct),
|
||||
".docx" => await Task.Run(() => ReadDocx(fullPath, extractMax), ct),
|
||||
".pptx" => await Task.Run(() => ReadPptx(fullPath, extractMax), ct),
|
||||
".xlsx" => await Task.Run(() => ReadXlsx(fullPath, sheetParam, extractMax), ct),
|
||||
".bib" => await Task.Run(() => ReadBibTeX(fullPath, extractMax), ct),
|
||||
".ris" => await Task.Run(() => ReadRis(fullPath, extractMax), ct),
|
||||
@@ -478,6 +480,71 @@ public class DocumentReaderTool : IAgentTool
|
||||
return Truncate(sb.ToString(), maxChars);
|
||||
}
|
||||
|
||||
// ─── PPTX ───────────────────────────────────────────────────────────────
|
||||
|
||||
private static string ReadPptx(string path, int maxChars)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
using var doc = PresentationDocument.Open(path, false);
|
||||
var presentation = doc.PresentationPart;
|
||||
if (presentation?.Presentation?.SlideIdList == null)
|
||||
return "(빈 프레젠테이션)";
|
||||
|
||||
var slideIds = presentation.Presentation.SlideIdList.Elements<DocumentFormat.OpenXml.Presentation.SlideId>().ToList();
|
||||
sb.AppendLine($"PowerPoint: {slideIds.Count}개 슬라이드");
|
||||
sb.AppendLine();
|
||||
|
||||
int slideNum = 0;
|
||||
foreach (var slideId in slideIds)
|
||||
{
|
||||
if (sb.Length >= maxChars) break;
|
||||
slideNum++;
|
||||
|
||||
var relId = slideId.RelationshipId?.Value;
|
||||
if (relId == null) continue;
|
||||
|
||||
var slidePart = (SlidePart)presentation.GetPartById(relId);
|
||||
var slide = slidePart.Slide;
|
||||
if (slide == null) continue;
|
||||
|
||||
sb.AppendLine($"--- Slide {slideNum} ---");
|
||||
|
||||
// 슬라이드 내 모든 텍스트 추출 (도형, 텍스트 박스, 테이블 등)
|
||||
var texts = slide.Descendants<A.Text>()
|
||||
.Select(t => t.Text)
|
||||
.Where(t => !string.IsNullOrWhiteSpace(t));
|
||||
|
||||
// 단락(Paragraph) 단위로 그룹핑하여 줄바꿈 유지
|
||||
var paragraphs = slide.Descendants<A.Paragraph>();
|
||||
foreach (var para in paragraphs)
|
||||
{
|
||||
var paraText = string.Join("", para.Descendants<A.Text>().Select(t => t.Text));
|
||||
if (!string.IsNullOrWhiteSpace(paraText))
|
||||
sb.AppendLine(paraText);
|
||||
}
|
||||
|
||||
// 슬라이드 노트가 있으면 포함
|
||||
var notesPart = slidePart.NotesSlidePart;
|
||||
if (notesPart != null)
|
||||
{
|
||||
var noteTexts = notesPart.NotesSlide.Descendants<A.Paragraph>()
|
||||
.Select(p => string.Join("", p.Descendants<A.Text>().Select(t => t.Text)))
|
||||
.Where(t => !string.IsNullOrWhiteSpace(t))
|
||||
.ToList();
|
||||
if (noteTexts.Count > 0)
|
||||
{
|
||||
sb.AppendLine(" [노트]");
|
||||
foreach (var note in noteTexts)
|
||||
sb.AppendLine($" {note}");
|
||||
}
|
||||
}
|
||||
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
return Truncate(sb.ToString(), maxChars);
|
||||
}
|
||||
|
||||
// ─── XLSX ───────────────────────────────────────────────────────────────
|
||||
|
||||
private static string ReadXlsx(string path, string sheetParam, int maxChars)
|
||||
|
||||
@@ -38,7 +38,9 @@ public class DocxSkill : IAgentTool
|
||||
"• List: {\"type\": \"list\", \"style\": \"bullet|number\", \"items\": [\"item1\", \" - sub-item\"]}\n" +
|
||||
"• Callout: {\"type\": \"callout\", \"style\": \"info|warning|tip|danger\", \"title\": \"...\", \"body\": \"...\"}\n" +
|
||||
"• HighlightBox: {\"type\": \"highlight_box\", \"text\": \"...\", \"color\": \"blue|green|orange|red\"}\n" +
|
||||
"Body text supports inline formatting: **bold**, *italic*, `code`.",
|
||||
"• Icon: {\"type\": \"icon\", \"name\": \"checkmark|warning|rocket|star|...\", \"text\": \"optional label text\", \"size\": 28}\n" +
|
||||
"Body text supports inline formatting: **bold**, *italic*, `code`. " +
|
||||
"Inline icons in body text: use {icon:name} syntax e.g. '{icon:checkmark} 완료됨'.",
|
||||
Items = new() { Type = "object" }
|
||||
},
|
||||
["header"] = new() { Type = "string", Description = "Header text shown at top of every page (optional)." },
|
||||
@@ -136,6 +138,12 @@ public class DocxSkill : IAgentTool
|
||||
|
||||
using var doc = WordprocessingDocument.Create(fullPath, WordprocessingDocumentType.Document);
|
||||
var mainPart = doc.AddMainDocumentPart();
|
||||
|
||||
// 기본 스타일 파트 추가 (styles.xml)
|
||||
var stylesPart = mainPart.AddNewPart<StyleDefinitionsPart>();
|
||||
stylesPart.Styles = CreateDefaultDocxStyles();
|
||||
stylesPart.Styles.Save();
|
||||
|
||||
mainPart.Document = new Document();
|
||||
var body = mainPart.Document.AppendChild(new Body());
|
||||
|
||||
@@ -194,6 +202,12 @@ public class DocxSkill : IAgentTool
|
||||
continue;
|
||||
}
|
||||
|
||||
if (blockType == "icon")
|
||||
{
|
||||
body.Append(CreateIconParagraph(section));
|
||||
continue;
|
||||
}
|
||||
|
||||
// 일반 섹션 (heading + body)
|
||||
var heading = section.SafeTryGetProperty("heading", out var h) ? h.SafeGetString() ?? "" : "";
|
||||
var bodyText = section.SafeTryGetProperty("body", out var b) ? b.SafeGetString() ?? "" : "";
|
||||
@@ -212,6 +226,8 @@ public class DocxSkill : IAgentTool
|
||||
sectionCount++;
|
||||
}
|
||||
|
||||
// ★ SectionProperties는 반드시 body의 마지막 자식이어야 함 (OOXML 규격)
|
||||
EnsureSectionProperties(body);
|
||||
mainPart.Document.Save();
|
||||
|
||||
var parts = new List<string>();
|
||||
@@ -250,7 +266,7 @@ public class DocxSkill : IAgentTool
|
||||
Bold = new Bold(),
|
||||
FontSize = new FontSize { Val = "44" }, // 22pt
|
||||
Color = new Color { Val = theme.Title },
|
||||
RunFonts = new RunFonts { Ascii = "Calibri", HighAnsi = "Calibri", EastAsia = "맑은 고딕" },
|
||||
RunFonts = new RunFonts { Ascii = "Noto Sans KR", HighAnsi = "Noto Sans KR", EastAsia = "Noto Sans KR" },
|
||||
};
|
||||
para.Append(run);
|
||||
return para;
|
||||
@@ -280,7 +296,7 @@ public class DocxSkill : IAgentTool
|
||||
Bold = new Bold(),
|
||||
FontSize = new FontSize { Val = fontSize },
|
||||
Color = new Color { Val = color },
|
||||
RunFonts = new RunFonts { Ascii = "Calibri", HighAnsi = "Calibri", EastAsia = "맑은 고딕" },
|
||||
RunFonts = new RunFonts { Ascii = "Noto Sans KR", HighAnsi = "Noto Sans KR", EastAsia = "Noto Sans KR" },
|
||||
};
|
||||
para.Append(run);
|
||||
return para;
|
||||
@@ -299,9 +315,16 @@ public class DocxSkill : IAgentTool
|
||||
return para;
|
||||
}
|
||||
|
||||
/// <summary>**bold**, *italic*, `code` 인라인 서식을 Run으로 변환</summary>
|
||||
/// <summary>**bold**, *italic*, `code`, {icon:name} 인라인 서식을 Run으로 변환</summary>
|
||||
private static void AppendFormattedRuns(Paragraph para, string text)
|
||||
{
|
||||
// 먼저 {icon:name} 인라인 아이콘을 심볼로 치환
|
||||
text = System.Text.RegularExpressions.Regex.Replace(text, @"\{icon:(\w+)\}", m =>
|
||||
{
|
||||
var name = m.Groups[1].Value;
|
||||
return IconLibrary.Contains(name) ? IconLibrary.Resolve(name) : m.Value;
|
||||
});
|
||||
|
||||
// 패턴: **bold** | *italic* | `code` | 일반텍스트
|
||||
var regex = new System.Text.RegularExpressions.Regex(
|
||||
@"\*\*(.+?)\*\*|\*(.+?)\*|`(.+?)`");
|
||||
@@ -331,7 +354,7 @@ public class DocxSkill : IAgentTool
|
||||
{
|
||||
var run = CreateRun(match.Groups[3].Value);
|
||||
run.RunProperties ??= new RunProperties();
|
||||
run.RunProperties.RunFonts = new RunFonts { Ascii = "Consolas", HighAnsi = "Consolas", EastAsia = "맑은 고딕" };
|
||||
run.RunProperties.RunFonts = new RunFonts { Ascii = "Consolas", HighAnsi = "Consolas", EastAsia = "Noto Sans KR" };
|
||||
run.RunProperties.FontSize = new FontSize { Val = "20" };
|
||||
run.RunProperties.Shading = new Shading
|
||||
{
|
||||
@@ -360,7 +383,7 @@ public class DocxSkill : IAgentTool
|
||||
run.RunProperties = new RunProperties
|
||||
{
|
||||
FontSize = new FontSize { Val = "22" }, // 11pt
|
||||
RunFonts = new RunFonts { Ascii = "Calibri", HighAnsi = "Calibri", EastAsia = "맑은 고딕" },
|
||||
RunFonts = new RunFonts { Ascii = "Noto Sans KR", HighAnsi = "Noto Sans KR", EastAsia = "Noto Sans KR" },
|
||||
};
|
||||
return run;
|
||||
}
|
||||
@@ -410,7 +433,7 @@ public class DocxSkill : IAgentTool
|
||||
Bold = new Bold(),
|
||||
FontSize = new FontSize { Val = "20" },
|
||||
Color = new Color { Val = "FFFFFF" },
|
||||
RunFonts = new RunFonts { Ascii = "Calibri", HighAnsi = "Calibri", EastAsia = "맑은 고딕" },
|
||||
RunFonts = new RunFonts { Ascii = "Noto Sans KR", HighAnsi = "Noto Sans KR", EastAsia = "Noto Sans KR" },
|
||||
}
|
||||
});
|
||||
para.ParagraphProperties = new ParagraphProperties
|
||||
@@ -448,7 +471,7 @@ public class DocxSkill : IAgentTool
|
||||
RunProperties = new RunProperties
|
||||
{
|
||||
FontSize = new FontSize { Val = "20" },
|
||||
RunFonts = new RunFonts { Ascii = "Calibri", HighAnsi = "Calibri", EastAsia = "맑은 고딕" },
|
||||
RunFonts = new RunFonts { Ascii = "Noto Sans KR", HighAnsi = "Noto Sans KR", EastAsia = "Noto Sans KR" },
|
||||
}
|
||||
});
|
||||
para.ParagraphProperties = new ParagraphProperties
|
||||
@@ -503,7 +526,7 @@ public class DocxSkill : IAgentTool
|
||||
{
|
||||
FontSize = new FontSize { Val = "22" },
|
||||
Bold = (listStyle == "number" && !isSub) ? new Bold() : null,
|
||||
RunFonts = new RunFonts { Ascii = "Calibri", HighAnsi = "Calibri", EastAsia = "맑은 고딕" },
|
||||
RunFonts = new RunFonts { Ascii = "Noto Sans KR", HighAnsi = "Noto Sans KR", EastAsia = "Noto Sans KR" },
|
||||
};
|
||||
para.Append(prefixRun);
|
||||
|
||||
@@ -511,7 +534,7 @@ public class DocxSkill : IAgentTool
|
||||
textRun.RunProperties = new RunProperties
|
||||
{
|
||||
FontSize = new FontSize { Val = "22" },
|
||||
RunFonts = new RunFonts { Ascii = "Calibri", HighAnsi = "Calibri", EastAsia = "맑은 고딕" },
|
||||
RunFonts = new RunFonts { Ascii = "Noto Sans KR", HighAnsi = "Noto Sans KR", EastAsia = "Noto Sans KR" },
|
||||
};
|
||||
para.Append(textRun);
|
||||
|
||||
@@ -552,7 +575,7 @@ public class DocxSkill : IAgentTool
|
||||
Bold = new Bold(),
|
||||
FontSize = new FontSize { Val = "22" },
|
||||
Color = new Color { Val = borderColor },
|
||||
RunFonts = new RunFonts { Ascii = "Calibri", HighAnsi = "Calibri", EastAsia = "맑은 고딕" },
|
||||
RunFonts = new RunFonts { Ascii = "Noto Sans KR", HighAnsi = "Noto Sans KR", EastAsia = "Noto Sans KR" },
|
||||
};
|
||||
titlePara.Append(titleRun);
|
||||
body.Append(titlePara);
|
||||
@@ -642,7 +665,7 @@ public class DocxSkill : IAgentTool
|
||||
{
|
||||
var run = CreateRun(match.Groups[3].Value);
|
||||
run.RunProperties ??= new RunProperties();
|
||||
run.RunProperties.RunFonts = new RunFonts { Ascii = "Consolas", HighAnsi = "Consolas", EastAsia = "맑은 고딕" };
|
||||
run.RunProperties.RunFonts = new RunFonts { Ascii = "Consolas", HighAnsi = "Consolas", EastAsia = "Noto Sans KR" };
|
||||
run.RunProperties.FontSize = new FontSize { Val = "20" };
|
||||
run.RunProperties.Shading = new Shading
|
||||
{
|
||||
@@ -679,6 +702,57 @@ public class DocxSkill : IAgentTool
|
||||
// 머리글/바닥글
|
||||
// ═══════════════════════════════════════════════════
|
||||
|
||||
/// <summary>Body의 마지막에 SectionProperties를 추가합니다 (OOXML 규격: 반드시 마지막 자식).</summary>
|
||||
private static void EnsureSectionProperties(Body body)
|
||||
{
|
||||
// 기존 sectPr이 마지막이 아닌 위치에 있으면 제거 후 재추가
|
||||
var existing = body.GetFirstChild<SectionProperties>();
|
||||
if (existing != null)
|
||||
{
|
||||
// 이미 마지막 자식이면 유지
|
||||
if (existing == body.LastChild) return;
|
||||
existing.Remove();
|
||||
}
|
||||
body.AppendChild(new SectionProperties(
|
||||
new PageSize { Width = 11906, Height = 16838 }, // A4
|
||||
new PageMargin { Top = 1440, Right = 1440, Bottom = 1440, Left = 1440,
|
||||
Header = 720, Footer = 720, Gutter = 0 }
|
||||
));
|
||||
}
|
||||
|
||||
/// <summary>DOCX 기본 스타일 정의 생성 (한글 글꼴 기본 설정 포함).</summary>
|
||||
private static Styles CreateDefaultDocxStyles()
|
||||
{
|
||||
var styles = new Styles();
|
||||
var docDefaults = new DocDefaults(
|
||||
new RunPropertiesDefault(
|
||||
new RunPropertiesBaseStyle(
|
||||
new RunFonts
|
||||
{
|
||||
Ascii = "맑은 고딕",
|
||||
HighAnsi = "맑은 고딕",
|
||||
EastAsia = "맑은 고딕",
|
||||
ComplexScript = "맑은 고딕"
|
||||
},
|
||||
new FontSize { Val = "22" },
|
||||
new Languages { Val = "ko-KR", EastAsia = "ko-KR" }
|
||||
)
|
||||
),
|
||||
new ParagraphPropertiesDefault(
|
||||
new ParagraphPropertiesBaseStyle(
|
||||
new SpacingBetweenLines { After = "160", Line = "259", LineRule = LineSpacingRuleValues.Auto }
|
||||
)
|
||||
)
|
||||
);
|
||||
styles.AppendChild(docDefaults);
|
||||
|
||||
var normalStyle = new Style { Type = StyleValues.Paragraph, StyleId = "Normal", Default = true };
|
||||
normalStyle.AppendChild(new StyleName { Val = "Normal" });
|
||||
styles.AppendChild(normalStyle);
|
||||
|
||||
return styles;
|
||||
}
|
||||
|
||||
private static void AddHeaderFooter(MainDocumentPart mainPart, Body body,
|
||||
string? headerText, string? footerText, bool showPageNumbers)
|
||||
{
|
||||
@@ -693,7 +767,7 @@ public class DocxSkill : IAgentTool
|
||||
{
|
||||
FontSize = new FontSize { Val = "18" }, // 9pt
|
||||
Color = new Color { Val = "808080" },
|
||||
RunFonts = new RunFonts { Ascii = "Calibri", HighAnsi = "Calibri", EastAsia = "맑은 고딕" },
|
||||
RunFonts = new RunFonts { Ascii = "Noto Sans KR", HighAnsi = "Noto Sans KR", EastAsia = "Noto Sans KR" },
|
||||
}
|
||||
});
|
||||
para.ParagraphProperties = new ParagraphProperties
|
||||
@@ -766,10 +840,44 @@ public class DocxSkill : IAgentTool
|
||||
{
|
||||
FontSize = new FontSize { Val = "16" },
|
||||
Color = new Color { Val = "999999" },
|
||||
RunFonts = new RunFonts { Ascii = "Calibri", HighAnsi = "Calibri", EastAsia = "맑은 고딕" },
|
||||
RunFonts = new RunFonts { Ascii = "Noto Sans KR", HighAnsi = "Noto Sans KR", EastAsia = "Noto Sans KR" },
|
||||
}
|
||||
};
|
||||
|
||||
/// <summary>아이콘 블록: 큰 심볼 + 선택적 라벨 텍스트.</summary>
|
||||
private static Paragraph CreateIconParagraph(System.Text.Json.JsonElement section)
|
||||
{
|
||||
var iconName = section.SafeTryGetProperty("name", out var n) ? n.SafeGetString() ?? "" : "";
|
||||
var label = section.SafeTryGetProperty("text", out var t) ? t.SafeGetString() ?? "" : "";
|
||||
var fontSize = section.SafeTryGetProperty("size", out var sz) ? sz.GetInt32() : 28;
|
||||
var symbol = IconLibrary.Resolve(iconName);
|
||||
|
||||
var para = new Paragraph();
|
||||
para.ParagraphProperties = new ParagraphProperties
|
||||
{
|
||||
SpacingBetweenLines = new SpacingBetweenLines { Line = "360" },
|
||||
};
|
||||
|
||||
// 아이콘 심볼 (큰 폰트)
|
||||
var iconRun = new Run(new Text(symbol + " ") { Space = SpaceProcessingModeValues.Preserve });
|
||||
iconRun.RunProperties = new RunProperties
|
||||
{
|
||||
FontSize = new FontSize { Val = (fontSize * 2).ToString() }, // half-pt 단위
|
||||
RunFonts = new RunFonts { Ascii = "Segoe UI Emoji", HighAnsi = "Segoe UI Emoji", EastAsia = "Segoe UI Emoji" },
|
||||
};
|
||||
para.Append(iconRun);
|
||||
|
||||
// 라벨 텍스트 (일반 폰트)
|
||||
if (!string.IsNullOrWhiteSpace(label))
|
||||
{
|
||||
var labelRun = CreateRun(label);
|
||||
labelRun.RunProperties!.FontSize = new FontSize { Val = "24" }; // 12pt
|
||||
para.Append(labelRun);
|
||||
}
|
||||
|
||||
return para;
|
||||
}
|
||||
|
||||
private static Run CreatePageNumberRun()
|
||||
{
|
||||
var run = new Run();
|
||||
@@ -777,7 +885,7 @@ public class DocxSkill : IAgentTool
|
||||
{
|
||||
FontSize = new FontSize { Val = "16" },
|
||||
Color = new Color { Val = "999999" },
|
||||
RunFonts = new RunFonts { Ascii = "Calibri", HighAnsi = "Calibri", EastAsia = "맑은 고딕" },
|
||||
RunFonts = new RunFonts { Ascii = "Noto Sans KR", HighAnsi = "Noto Sans KR", EastAsia = "Noto Sans KR" },
|
||||
};
|
||||
run.Append(new FieldChar { FieldCharType = FieldCharValues.Begin });
|
||||
run.Append(new FieldCode(" PAGE ") { Space = SpaceProcessingModeValues.Preserve });
|
||||
|
||||
@@ -172,7 +172,7 @@ internal static class DocxToHtmlConverter
|
||||
<meta charset='UTF-8'>
|
||||
<style>
|
||||
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
|
||||
body {{ font-family: 'Malgun Gothic', 'Segoe UI', sans-serif; background: #fff; color: #1a1a1a;
|
||||
body {{ font-family: 'Noto Sans KR', 'Segoe UI', sans-serif; background: #fff; color: #1a1a1a;
|
||||
line-height: 1.7; padding: 32px 28px; font-size: 13.5px; }}
|
||||
h1 {{ font-size: 22px; font-weight: 700; margin: 24px 0 10px; color: #111; }}
|
||||
h1.title {{ font-size: 26px; text-align: center; margin-bottom: 4px; }}
|
||||
|
||||
@@ -17,7 +17,9 @@ public class ExcelSkill : IAgentTool
|
||||
"Supports: header styling (bold white text on colored background), " +
|
||||
"striped rows, column auto-width, formulas (=SUM, =AVERAGE, etc), " +
|
||||
"cell merge, freeze panes (freeze header row), number formatting, " +
|
||||
"themes (professional/modern/dark/minimal), column alignments, and multi-sheet workbooks.";
|
||||
"themes (professional/modern/dark/minimal), column alignments, and multi-sheet workbooks. " +
|
||||
"Inline icons: use {icon:name} in cell text (e.g. '{icon:checkmark} 완료', '{icon:warning} 주의'). " +
|
||||
"170+ built-in icons: checkmark, warning, star, rocket, chart_up, chart_down, etc.";
|
||||
|
||||
public ToolParameterSchema Parameters => new()
|
||||
{
|
||||
@@ -340,12 +342,16 @@ public class ExcelSkill : IAgentTool
|
||||
|
||||
var strVal = cellVal.ToString();
|
||||
|
||||
// {icon:name} 인라인 아이콘 → 유니코드 심볼로 치환
|
||||
if (strVal.Contains("{icon:"))
|
||||
strVal = ResolveInlineIcons(strVal);
|
||||
|
||||
if (strVal.StartsWith('='))
|
||||
{
|
||||
cell.CellFormula = new CellFormula(strVal);
|
||||
cell.DataType = null;
|
||||
}
|
||||
else if (cellVal.ValueKind == JsonValueKind.Number)
|
||||
else if (cellVal.ValueKind == JsonValueKind.Number && !strVal.Contains("{icon:"))
|
||||
{
|
||||
cell.DataType = CellValues.Number;
|
||||
cell.CellValue = new CellValue(cellVal.GetDouble().ToString());
|
||||
@@ -500,18 +506,18 @@ public class ExcelSkill : IAgentTool
|
||||
var fonts = new Fonts(
|
||||
new Font( // 0: default
|
||||
new FontSize { Val = 11 },
|
||||
new FontName { Val = "맑은 고딕" }
|
||||
new FontName { Val = "Noto Sans KR" }
|
||||
),
|
||||
new Font( // 1: bold white (header)
|
||||
new Bold(),
|
||||
new FontSize { Val = 11 },
|
||||
new Color { Rgb = theme.HeaderFg },
|
||||
new FontName { Val = "맑은 고딕" }
|
||||
new FontName { Val = "Noto Sans KR" }
|
||||
),
|
||||
new Font( // 2: bold (summary row)
|
||||
new Bold(),
|
||||
new FontSize { Val = 11 },
|
||||
new FontName { Val = "맑은 고딕" }
|
||||
new FontName { Val = "Noto Sans KR" }
|
||||
)
|
||||
);
|
||||
stylesheet.Append(fonts);
|
||||
@@ -799,4 +805,12 @@ public class ExcelSkill : IAgentTool
|
||||
|
||||
private static string GetCellReference(int colIndex, int rowIndex)
|
||||
=> $"{GetColumnLetter(colIndex)}{rowIndex + 1}";
|
||||
|
||||
/// <summary>{icon:name} 패턴을 유니코드 심볼로 치환합니다.</summary>
|
||||
private static string ResolveInlineIcons(string text)
|
||||
=> System.Text.RegularExpressions.Regex.Replace(text, @"\{icon:(\w+)\}", m =>
|
||||
{
|
||||
var name = m.Groups[1].Value;
|
||||
return IconLibrary.Contains(name) ? IconLibrary.Resolve(name) : m.Value;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -13,7 +13,8 @@ public class FolderMapTool : IAgentTool
|
||||
public string Name => "folder_map";
|
||||
public string Description =>
|
||||
"Generate a directory tree map of the work folder or a specified subfolder. " +
|
||||
"Shows folders and files in a tree structure. Use this to understand the project layout before reading or editing files. " +
|
||||
"Shows folders and files in a tree structure. Use this only when the user explicitly asks for folder contents, directory structure, or workspace layout. " +
|
||||
"Do not use it to locate a specific file when glob/grep or a targeted read would work. " +
|
||||
"Supports sorting, size filtering, date filtering, and multi-extension filtering.";
|
||||
|
||||
public ToolParameterSchema Parameters => new()
|
||||
@@ -22,7 +23,7 @@ public class FolderMapTool : IAgentTool
|
||||
{
|
||||
["path"] = new() { Type = "string", Description = "Subdirectory to map. Optional, defaults to work folder root." },
|
||||
["depth"] = new() { Type = "integer", Description = "Maximum depth to traverse (1-10). Default: 2." },
|
||||
["include_files"] = new() { Type = "boolean", Description = "Whether to include files. Default: false for conservative first-pass exploration." },
|
||||
["include_files"] = new() { Type = "boolean", Description = "Whether to include files. Default: true." },
|
||||
["pattern"] = new() { Type = "string", Description = "Single file extension filter (e.g. '.cs', '.py'). Optional. Use 'extensions' for multiple extensions." },
|
||||
["extensions"] = new()
|
||||
{
|
||||
@@ -65,7 +66,7 @@ public class FolderMapTool : IAgentTool
|
||||
var maxDepth = Math.Min(depth, 10);
|
||||
|
||||
// ── include_files ─────────────────────────────────────────────────
|
||||
var includeFiles = false;
|
||||
var includeFiles = true;
|
||||
if (args.SafeTryGetProperty("include_files", out var inc))
|
||||
{
|
||||
if (inc.ValueKind == JsonValueKind.True || inc.ValueKind == JsonValueKind.False)
|
||||
|
||||
@@ -46,7 +46,10 @@ public class HtmlSkill : IAgentTool
|
||||
"'list' {style:'bullet|number', items:['item', ' - sub-item']}, " +
|
||||
"'quote' {text, author}, " +
|
||||
"'divider', " +
|
||||
"'kpi' {items:[{label, value, change, positive:bool}]}. " +
|
||||
"'kpi' {items:[{label, value, change, positive:bool}]}, " +
|
||||
"'icon' {name:'checkmark|warning|rocket|...', text:'optional label', size:48}. " +
|
||||
"Inline icons in any text: use {icon:name} syntax (e.g. '{icon:checkmark} Done'). " +
|
||||
"170+ built-in icons available. " +
|
||||
"When both body and sections are provided, sections are appended after body."
|
||||
},
|
||||
["mood"] = new() { Type = "string", Description = "Design template mood: modern, professional, creative, minimal, elegant, dark, colorful, corporate, magazine, dashboard. Default: modern" },
|
||||
@@ -178,11 +181,16 @@ public class HtmlSkill : IAgentTool
|
||||
}
|
||||
else
|
||||
{
|
||||
// 커버가 없으면 기존 방식의 제목+메타
|
||||
// 커버가 없으면 header-bar로 제목 표시
|
||||
sb.AppendLine("<div class=\"header-bar\">");
|
||||
sb.AppendLine($"<h1>{Escape(title)}</h1>");
|
||||
sb.AppendLine($"<div class=\"meta\">생성: {DateTime.Now:yyyy-MM-dd HH:mm} | AX Copilot{moodLabel}</div>");
|
||||
sb.AppendLine("</div>");
|
||||
}
|
||||
|
||||
// 본문을 body-content로 감싸서 좌우 여백 확보
|
||||
sb.AppendLine("<div class=\"body-content\">");
|
||||
|
||||
// TOC
|
||||
if (!string.IsNullOrEmpty(tocHtml))
|
||||
sb.AppendLine(tocHtml);
|
||||
@@ -198,7 +206,8 @@ public class HtmlSkill : IAgentTool
|
||||
"$1</div>");
|
||||
sb.AppendLine(wrappedBody);
|
||||
|
||||
sb.AppendLine("</div>");
|
||||
sb.AppendLine("</div>"); // body-content
|
||||
sb.AppendLine("</div>"); // container
|
||||
sb.AppendLine("</body>");
|
||||
sb.AppendLine("</html>");
|
||||
|
||||
@@ -268,6 +277,9 @@ public class HtmlSkill : IAgentTool
|
||||
case "kpi":
|
||||
sb.AppendLine(RenderKpi(section));
|
||||
break;
|
||||
case "icon":
|
||||
sb.AppendLine(RenderIcon(section));
|
||||
break;
|
||||
}
|
||||
}
|
||||
return sb.ToString();
|
||||
@@ -549,6 +561,15 @@ public class HtmlSkill : IAgentTool
|
||||
// 7. 보존된 <br> 플레이스홀더를 복원
|
||||
text = text.Replace("\x00BR\x00", "<br>");
|
||||
|
||||
// 8. {icon:name} 인라인 아이콘 → 유니코드 심볼 (span으로 감싸서 크기 조절)
|
||||
text = Regex.Replace(text, @"\{icon:(\w+)\}", m =>
|
||||
{
|
||||
var name = m.Groups[1].Value;
|
||||
if (!IconLibrary.Contains(name)) return m.Value;
|
||||
var sym = IconLibrary.Resolve(name);
|
||||
return $"<span class=\"inline-icon\" title=\"{name}\">{sym}</span>";
|
||||
});
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
@@ -732,4 +753,21 @@ public class HtmlSkill : IAgentTool
|
||||
|
||||
private static string Escape(string s) =>
|
||||
s.Replace("&", "&").Replace("<", "<").Replace(">", ">");
|
||||
|
||||
/// <summary>아이콘 블록: 큰 심볼 + 선택적 라벨 텍스트.</summary>
|
||||
private static string RenderIcon(JsonElement s)
|
||||
{
|
||||
var name = s.SafeTryGetProperty("name", out var n) ? n.SafeGetString() ?? "" : "";
|
||||
var label = s.SafeTryGetProperty("text", out var t) ? t.SafeGetString() ?? "" : "";
|
||||
var size = s.SafeTryGetProperty("size", out var sz) ? sz.GetInt32() : 48;
|
||||
var symbol = IconLibrary.Resolve(name);
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.Append($"<div class=\"icon-block\" style=\"display:flex;align-items:center;gap:12px;margin:16px 0\">");
|
||||
sb.Append($"<span style=\"font-size:{size}px;line-height:1\">{symbol}</span>");
|
||||
if (!string.IsNullOrWhiteSpace(label))
|
||||
sb.Append($"<span style=\"font-size:1rem;color:#374151\">{MarkdownToHtml(label)}</span>");
|
||||
sb.Append("</div>");
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,14 @@ public interface IAgentTool
|
||||
/// <summary>LLM function calling용 파라미터 JSON Schema.</summary>
|
||||
ToolParameterSchema Parameters { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 도구가 활성화되는 탭 카테고리.
|
||||
/// null이면 모든 탭에서 사용 가능.
|
||||
/// "Cowork" = Cowork 전용, "Code" = Code 전용, "Chat" = Chat 전용.
|
||||
/// 쉼표 구분으로 복수 탭 지정 가능: "Cowork,Code".
|
||||
/// </summary>
|
||||
string? TabCategory => null;
|
||||
|
||||
/// <summary>도구를 실행하고 결과를 반환합니다.</summary>
|
||||
/// <param name="args">LLM이 생성한 JSON 파라미터</param>
|
||||
/// <param name="context">실행 컨텍스트 (작업 폴더, 권한 등)</param>
|
||||
@@ -171,16 +179,35 @@ public class AgentContext
|
||||
}
|
||||
|
||||
// 작업 폴더 제한: 작업 폴더가 설정되어 있으면 하위 경로만 허용
|
||||
// 사내 모드에서는 외부 경로도 사용자 승인 후 접근 가능 (CheckToolPermissionAsync에서 강제 승인 처리)
|
||||
if (!string.IsNullOrEmpty(WorkFolder))
|
||||
{
|
||||
var workFull = Path.GetFullPath(WorkFolder);
|
||||
if (!fullPath.StartsWith(workFull, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// 사내 모드: 외부 경로는 IsPathAllowed에서 차단하지 않고 권한 검증 단계로 위임
|
||||
if (AxCopilot.Services.OperationModePolicy.IsInternal(OperationMode))
|
||||
return true; // CheckToolPermissionAsync에서 강제 승인 요청됨
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>경로가 워크스페이스 외부인지 확인합니다.</summary>
|
||||
public bool IsOutsideWorkspace(string path)
|
||||
{
|
||||
if (string.IsNullOrEmpty(WorkFolder)) return false;
|
||||
try
|
||||
{
|
||||
var fullPath = Path.GetFullPath(path);
|
||||
var workFull = Path.GetFullPath(WorkFolder);
|
||||
return !fullPath.StartsWith(workFull, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
catch { return true; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 파일 경로에 타임스탬프를 추가합니다.
|
||||
/// 예: report.html → report_20260328_1430.html
|
||||
@@ -241,6 +268,32 @@ public class AgentContext
|
||||
|
||||
var effectivePerm = PermissionModeCatalog.NormalizeGlobalMode(GetEffectiveToolPermission(toolName, target));
|
||||
if (PermissionModeCatalog.IsDeny(effectivePerm)) return false;
|
||||
|
||||
// ── 사내 모드 보안 강화: 워크스페이스 외부 경로 접근 시 무조건 사용자 승인 ──
|
||||
// BypassPermissions / AcceptEdits 모드여도 외부 경로는 강제 승인 필요
|
||||
if (AxCopilot.Services.OperationModePolicy.IsInternal(OperationMode)
|
||||
&& !string.IsNullOrWhiteSpace(target)
|
||||
&& IsOutsideWorkspace(target))
|
||||
{
|
||||
if (AskPermission == null) return false;
|
||||
|
||||
var extTarget = target.Trim();
|
||||
var extCacheKey = $"ext_ws|{toolName}|{extTarget}";
|
||||
lock (_permissionLock)
|
||||
{
|
||||
if (_approvedPermissionCache.Contains(extCacheKey))
|
||||
return true;
|
||||
}
|
||||
|
||||
var extAllowed = await AskPermission(toolName, extTarget);
|
||||
if (extAllowed)
|
||||
{
|
||||
lock (_permissionLock)
|
||||
_approvedPermissionCache.Add(extCacheKey);
|
||||
}
|
||||
return extAllowed;
|
||||
}
|
||||
|
||||
if (PermissionModeCatalog.IsAuto(effectivePerm)) return true;
|
||||
if (AskPermission == null) return false;
|
||||
|
||||
@@ -482,4 +535,5 @@ public enum AgentEventType
|
||||
StopRequested, // 중단 요청
|
||||
Paused, // 에이전트 일시정지
|
||||
Resumed, // 에이전트 재개
|
||||
UserMessage, // 실행 중 사용자 메시지 주입
|
||||
}
|
||||
|
||||
@@ -4,19 +4,19 @@ namespace AxCopilot.Services.Agent;
|
||||
|
||||
internal interface IToolExecutionCoordinator
|
||||
{
|
||||
Task<LlmService.ToolPrefetchResult?> TryPrefetchReadOnlyToolAsync(
|
||||
LlmService.ContentBlock block,
|
||||
Task<ToolPrefetchResult?> TryPrefetchReadOnlyToolAsync(
|
||||
ContentBlock block,
|
||||
IReadOnlyCollection<IAgentTool> tools,
|
||||
AgentContext context,
|
||||
CancellationToken ct);
|
||||
|
||||
Task<List<LlmService.ContentBlock>> SendWithToolsWithRecoveryAsync(
|
||||
Task<List<ContentBlock>> SendWithToolsWithRecoveryAsync(
|
||||
List<ChatMessage> messages,
|
||||
IReadOnlyCollection<IAgentTool> tools,
|
||||
CancellationToken ct,
|
||||
string phaseLabel,
|
||||
AgentLoopService.RunState? runState = null,
|
||||
bool forceToolCall = false,
|
||||
Func<LlmService.ContentBlock, Task<LlmService.ToolPrefetchResult?>>? prefetchToolCallAsync = null,
|
||||
Func<LlmService.ToolStreamEvent, Task>? onStreamEventAsync = null);
|
||||
Func<ContentBlock, Task<ToolPrefetchResult?>>? prefetchToolCallAsync = null,
|
||||
Func<ToolStreamEvent, Task>? onStreamEventAsync = null);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
namespace AxCopilot.Services.Agent;
|
||||
|
||||
/// <summary>
|
||||
/// 모든 문서 스킬(PPTX, DOCX, XLSX, HTML)에서 공유하는 내장 아이콘/심볼 라이브러리.
|
||||
/// 유니코드 심볼 기반이므로 외부 파일 0바이트 — 폰트가 지원하는 한 어디서든 렌더링됩니다.
|
||||
///
|
||||
/// 사용 예:
|
||||
/// var symbol = IconLibrary.Resolve("checkmark"); // "✔"
|
||||
/// var symbol = IconLibrary.Resolve("rocket"); // "🚀"
|
||||
/// var symbol = IconLibrary.Resolve("없는이름"); // "❓" (폴백)
|
||||
/// </summary>
|
||||
internal static class IconLibrary
|
||||
{
|
||||
/// <summary>아이콘 이름으로 유니코드 심볼 문자열을 반환합니다. 없으면 ❓.</summary>
|
||||
public static string Resolve(string? iconName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(iconName)) return "";
|
||||
return Symbols.TryGetValue(iconName.Trim(), out var symbol) ? symbol : "❓";
|
||||
}
|
||||
|
||||
/// <summary>아이콘 이름이 라이브러리에 존재하는지 확인합니다.</summary>
|
||||
public static bool Contains(string? iconName)
|
||||
=> !string.IsNullOrWhiteSpace(iconName) && Symbols.ContainsKey(iconName.Trim());
|
||||
|
||||
/// <summary>전체 아이콘 목록 (읽기 전용).</summary>
|
||||
public static IReadOnlyDictionary<string, string> All => Symbols;
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
// 유니코드 심볼 매핑 (170종+)
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
private static readonly Dictionary<string, string> Symbols = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
// ── 상태/알림 ──
|
||||
["checkmark"] = "✔",
|
||||
["check"] = "✔",
|
||||
["check_box"] = "☑",
|
||||
["warning"] = "⚠",
|
||||
["info"] = "ℹ",
|
||||
["error"] = "✖",
|
||||
["question"] = "❓",
|
||||
["exclamation"] = "❗",
|
||||
["prohibited"] = "🚫",
|
||||
["sos"] = "🆘",
|
||||
["new"] = "🆕",
|
||||
["free"] = "🆓",
|
||||
["ok"] = "🆗",
|
||||
["cool"] = "🆒",
|
||||
["up"] = "🆙",
|
||||
["vs"] = "🆚",
|
||||
["100"] = "💯",
|
||||
// ── 사람/소통 ──
|
||||
["people"] = "👥",
|
||||
["person"] = "👤",
|
||||
["thumbs_up"] = "👍",
|
||||
["thumbs_down"] = "👎",
|
||||
["clap"] = "👏",
|
||||
["handshake"] = "🤝",
|
||||
["wave_hand"] = "👋",
|
||||
["pray"] = "🙏",
|
||||
["muscle"] = "💪",
|
||||
["brain"] = "🧠",
|
||||
["eye"] = "👁",
|
||||
["speech"] = "💬",
|
||||
["thought"] = "💭",
|
||||
// ── 사무/업무 ──
|
||||
["phone"] = "📞",
|
||||
["email"] = "📧",
|
||||
["inbox"] = "📥",
|
||||
["outbox"] = "📤",
|
||||
["document"] = "📄",
|
||||
["documents"] = "📑",
|
||||
["folder"] = "📁",
|
||||
["folder_open"] = "📂",
|
||||
["calendar"] = "📅",
|
||||
["clipboard"] = "📋",
|
||||
["pushpin"] = "📌",
|
||||
["pin"] = "📌",
|
||||
["paperclip"] = "📎",
|
||||
["ruler"] = "📏",
|
||||
["scissors"] = "✂",
|
||||
["pen"] = "🖊",
|
||||
["pencil"] = "✏",
|
||||
["memo"] = "📝",
|
||||
["book"] = "📖",
|
||||
["books"] = "📚",
|
||||
["notebook"] = "📓",
|
||||
["newspaper"] = "📰",
|
||||
["label"] = "🏷",
|
||||
["tag"] = "🏷",
|
||||
["package"] = "📦",
|
||||
["mailbox"] = "📬",
|
||||
["fax"] = "📠",
|
||||
["printer"] = "🖨",
|
||||
["computer"] = "💻",
|
||||
["desktop"] = "🖥",
|
||||
["keyboard"] = "⌨",
|
||||
["mouse"] = "🖱",
|
||||
// ── 시간/시계 ──
|
||||
["clock"] = "🕐",
|
||||
["hourglass"] = "⏳",
|
||||
["stopwatch"] = "⏱",
|
||||
["alarm"] = "⏰",
|
||||
["timer"] = "⏲",
|
||||
// ── 보안/잠금 ──
|
||||
["lock"] = "🔒",
|
||||
["unlock"] = "🔓",
|
||||
["key"] = "🔑",
|
||||
["shield"] = "🛡",
|
||||
// ── 검색/탐색 ──
|
||||
["search"] = "🔍",
|
||||
["magnify_right"] = "🔎",
|
||||
["binoculars"] = "🔭",
|
||||
["compass"] = "🧭",
|
||||
["map"] = "🗺",
|
||||
["globe"] = "🌐",
|
||||
["globe_asia"] = "🌏",
|
||||
["globe_americas"] = "🌎",
|
||||
["globe_europe"] = "🌍",
|
||||
// ── 도구/설정 ──
|
||||
["wrench"] = "🔧",
|
||||
["hammer"] = "🔨",
|
||||
["nut_bolt"] = "🔩",
|
||||
["link"] = "🔗",
|
||||
["chain"] = "⛓",
|
||||
["refresh"] = "🔄",
|
||||
["recycle"] = "♻",
|
||||
["gear"] = "⚙",
|
||||
// ── 성취/보상 ──
|
||||
["trophy"] = "🏆",
|
||||
["medal"] = "🏅",
|
||||
["medal_gold"] = "🥇",
|
||||
["medal_silver"] = "🥈",
|
||||
["medal_bronze"] = "🥉",
|
||||
["crown"] = "👑",
|
||||
["gem"] = "💎",
|
||||
// ── 감정/기호 ──
|
||||
["lightbulb"] = "💡",
|
||||
["target"] = "🎯",
|
||||
["rocket"] = "🚀",
|
||||
["fire"] = "🔥",
|
||||
["bell"] = "🔔",
|
||||
["sparkle"] = "✨",
|
||||
["sparkles"] = "✨",
|
||||
["rainbow"] = "🌈",
|
||||
["confetti"] = "🎉",
|
||||
["party"] = "🎊",
|
||||
["balloon"] = "🎈",
|
||||
["gift"] = "🎁",
|
||||
["heart"] = "❤",
|
||||
["star"] = "⭐",
|
||||
["thumbs"] = "👍",
|
||||
// ── 화살표/방향 ──
|
||||
["arrow_right"] = "➡",
|
||||
["arrow_left"] = "⬅",
|
||||
["arrow_up"] = "⬆",
|
||||
["arrow_down"] = "⬇",
|
||||
["arrow_up_right"] = "↗",
|
||||
["arrow_down_right"]= "↘",
|
||||
["arrow_curved"] = "↩",
|
||||
// ── 금융/비즈니스 ──
|
||||
["money"] = "💰",
|
||||
["money_bag"] = "💰",
|
||||
["dollar"] = "💵",
|
||||
["credit_card"] = "💳",
|
||||
["receipt"] = "🧾",
|
||||
["chart_up"] = "📈",
|
||||
["chart_down"] = "📉",
|
||||
["bar_chart"] = "📊",
|
||||
// ── 교통/이동 ──
|
||||
["car"] = "🚗",
|
||||
["bus"] = "🚌",
|
||||
["train"] = "🚆",
|
||||
["airplane"] = "✈",
|
||||
["ship"] = "🚢",
|
||||
["bicycle"] = "🚲",
|
||||
// ── 건물/장소 ──
|
||||
["building"] = "🏢",
|
||||
["hospital"] = "🏥",
|
||||
["school"] = "🏫",
|
||||
["factory"] = "🏭",
|
||||
["house"] = "🏠",
|
||||
["store"] = "🏪",
|
||||
["bank"] = "🏦",
|
||||
// ── 자연/과학 ──
|
||||
["tree"] = "🌳",
|
||||
["leaf"] = "🍃",
|
||||
["flower"] = "🌸",
|
||||
["seedling"] = "🌱",
|
||||
["earth"] = "🌍",
|
||||
["water"] = "💧",
|
||||
["snowflake"] = "❄",
|
||||
["atom"] = "⚛",
|
||||
["dna"] = "🧬",
|
||||
["microscope"] = "🔬",
|
||||
["telescope"] = "🔭",
|
||||
["test_tube"] = "🧪",
|
||||
["petri_dish"] = "🧫",
|
||||
// ── 음식 ──
|
||||
["coffee"] = "☕",
|
||||
["pizza"] = "🍕",
|
||||
// ── 수학/기호 ──
|
||||
["plus"] = "➕",
|
||||
["minus"] = "➖",
|
||||
["multiply"] = "✖",
|
||||
["divide"] = "➗",
|
||||
["infinity"] = "♾",
|
||||
["copyright"] = "©",
|
||||
["trademark"] = "™",
|
||||
["registered"] = "®",
|
||||
// ── 기타 유용 ──
|
||||
["battery"] = "🔋",
|
||||
["plug"] = "🔌",
|
||||
["magnet"] = "🧲",
|
||||
["crystal_ball"] = "🔮",
|
||||
["palette"] = "🎨",
|
||||
["movie"] = "🎬",
|
||||
["music"] = "🎵",
|
||||
["headphone"] = "🎧",
|
||||
["camera"] = "📷",
|
||||
["video"] = "📹",
|
||||
["satellite"] = "📡",
|
||||
["antenna"] = "📡",
|
||||
["megaphone"] = "📢",
|
||||
["loudspeaker"] = "📢",
|
||||
["mute"] = "🔇",
|
||||
["speaker"] = "🔊",
|
||||
["zzz"] = "💤",
|
||||
["smiley"] = "😊",
|
||||
["cloud"] = "☁",
|
||||
["sun"] = "☀",
|
||||
["moon"] = "🌙",
|
||||
["lightning"] = "⚡",
|
||||
["flag"] = "🚩",
|
||||
["home"] = "🏠",
|
||||
["cross"] = "✝",
|
||||
["diamond"] = "💠",
|
||||
["no_symbol"] = "🚫",
|
||||
};
|
||||
}
|
||||
@@ -5,18 +5,24 @@ namespace AxCopilot.Services.Agent;
|
||||
|
||||
/// <summary>
|
||||
/// LSP 기반 코드 인텔리전스 도구.
|
||||
/// 정의 이동(goto_definition), 참조 검색(find_references), 심볼 목록(symbols) 3가지 액션을 제공합니다.
|
||||
/// 정의 이동, 참조 검색, hover, 구현 위치, 심볼 검색, 호출 계층 등 구조적 코드 탐색을 제공합니다.
|
||||
/// </summary>
|
||||
public class LspTool : IAgentTool, IDisposable
|
||||
{
|
||||
public string Name => "lsp_code_intel";
|
||||
|
||||
public string Description =>
|
||||
"코드 인텔리전스 도구. 정의 이동, 참조 검색, 심볼 목록을 제공합니다.\n" +
|
||||
"- action=\"goto_definition\": 심볼의 정의 위치를 찾습니다 (파일, 라인, 컬럼)\n" +
|
||||
"코드 인텔리전스 도구. 정의, 참조, hover, 구현 위치, 문서/워크스페이스 심볼, 호출 계층을 제공합니다.\n" +
|
||||
"- action=\"goto_definition\": 심볼의 정의 위치를 찾습니다\n" +
|
||||
"- action=\"find_references\": 심볼이 사용된 모든 위치를 찾습니다\n" +
|
||||
"- action=\"symbols\": 파일 내 모든 심볼(클래스, 메서드, 필드 등)을 나열합니다\n" +
|
||||
"file_path, line, character 파라미터가 필요합니다 (line과 character는 0-based).";
|
||||
"- action=\"hover\": 심볼의 타입/문서 정보를 가져옵니다\n" +
|
||||
"- action=\"goto_implementation\": 인터페이스/추상 멤버의 구현 위치를 찾습니다\n" +
|
||||
"- action=\"symbols\": 파일 내 모든 심볼을 나열합니다\n" +
|
||||
"- action=\"workspace_symbols\": 워크스페이스 전체 심볼을 검색합니다 (query 권장)\n" +
|
||||
"- action=\"prepare_call_hierarchy\": 현재 위치의 호출 계층 기준 심볼을 확인합니다\n" +
|
||||
"- action=\"incoming_calls\": 현재 심볼을 호출하는 상위 호출자를 찾습니다\n" +
|
||||
"- action=\"outgoing_calls\": 현재 심볼이 호출하는 하위 호출 대상을 찾습니다\n" +
|
||||
"line/character는 기본적으로 1-based 입력을 기대하며, 0-based 값도 호환 처리합니다.";
|
||||
|
||||
public ToolParameterSchema Parameters => new()
|
||||
{
|
||||
@@ -25,23 +31,39 @@ public class LspTool : IAgentTool, IDisposable
|
||||
["action"] = new ToolProperty
|
||||
{
|
||||
Type = "string",
|
||||
Description = "수행할 작업: goto_definition | find_references | symbols",
|
||||
Enum = new() { "goto_definition", "find_references", "symbols" }
|
||||
Description = "수행할 작업",
|
||||
Enum = new()
|
||||
{
|
||||
"goto_definition",
|
||||
"find_references",
|
||||
"hover",
|
||||
"goto_implementation",
|
||||
"symbols",
|
||||
"workspace_symbols",
|
||||
"prepare_call_hierarchy",
|
||||
"incoming_calls",
|
||||
"outgoing_calls"
|
||||
}
|
||||
},
|
||||
["file_path"] = new ToolProperty
|
||||
{
|
||||
Type = "string",
|
||||
Description = "대상 파일 경로 (절대 또는 작업 폴더 기준 상대 경로)"
|
||||
Description = "대상 파일 경로 (절대 또는 작업 폴더 기준 상대 경로). workspace_symbols에서는 작업 폴더 기준 힌트로만 사용 가능."
|
||||
},
|
||||
["line"] = new ToolProperty
|
||||
{
|
||||
Type = "integer",
|
||||
Description = "대상 라인 번호 (0-based). symbols 액션에서는 불필요."
|
||||
Description = "대상 라인 번호. 기본 1-based 입력을 기대하며 내부에서 0-based로 변환합니다. symbols/workspace_symbols에서는 불필요."
|
||||
},
|
||||
["character"] = new ToolProperty
|
||||
{
|
||||
Type = "integer",
|
||||
Description = "라인 내 문자 위치 (0-based). symbols 액션에서는 불필요."
|
||||
Description = "라인 내 문자 위치. 기본 1-based 입력을 기대하며 내부에서 0-based로 변환합니다. symbols/workspace_symbols에서는 불필요."
|
||||
},
|
||||
["query"] = new ToolProperty
|
||||
{
|
||||
Type = "string",
|
||||
Description = "workspace_symbols에서 사용할 심볼 검색어. 비어 있으면 file_path의 파일명/심볼 힌트를 사용합니다."
|
||||
},
|
||||
},
|
||||
Required = new() { "action", "file_path" }
|
||||
@@ -59,8 +81,9 @@ public class LspTool : IAgentTool, IDisposable
|
||||
|
||||
var action = args.SafeTryGetProperty("action", out var a) ? a.SafeGetString() ?? "" : "";
|
||||
var filePath = args.SafeTryGetProperty("file_path", out var f) ? f.SafeGetString() ?? "" : "";
|
||||
var line = args.SafeTryGetProperty("line", out var l) ? l.GetInt32() : 0;
|
||||
var character = args.SafeTryGetProperty("character", out var ch) ? ch.GetInt32() : 0;
|
||||
var line = args.SafeTryGetProperty("line", out var l) ? NormalizePosition(l.SafeGetInt32()) : 0;
|
||||
var character = args.SafeTryGetProperty("character", out var ch) ? NormalizePosition(ch.SafeGetInt32()) : 0;
|
||||
var query = args.SafeTryGetProperty("query", out var q) ? q.SafeGetString() ?? "" : "";
|
||||
|
||||
if (string.IsNullOrEmpty(filePath))
|
||||
return ToolResult.Fail("file_path가 필요합니다.");
|
||||
@@ -88,8 +111,14 @@ public class LspTool : IAgentTool, IDisposable
|
||||
{
|
||||
"goto_definition" => await GotoDefinitionAsync(client, filePath, line, character, ct),
|
||||
"find_references" => await FindReferencesAsync(client, filePath, line, character, ct),
|
||||
"hover" => await HoverAsync(client, filePath, line, character, ct),
|
||||
"goto_implementation" => await GotoImplementationAsync(client, filePath, line, character, ct),
|
||||
"symbols" => await GetSymbolsAsync(client, filePath, ct),
|
||||
_ => ToolResult.Fail($"알 수 없는 액션: {action}. goto_definition | find_references | symbols 중 선택하세요.")
|
||||
"workspace_symbols" => await GetWorkspaceSymbolsAsync(client, filePath, query, ct),
|
||||
"prepare_call_hierarchy" => await PrepareCallHierarchyAsync(client, filePath, line, character, ct),
|
||||
"incoming_calls" => await GetIncomingCallsAsync(client, filePath, line, character, ct),
|
||||
"outgoing_calls" => await GetOutgoingCallsAsync(client, filePath, line, character, ct),
|
||||
_ => ToolResult.Fail("알 수 없는 action입니다. goto_definition | find_references | hover | goto_implementation | symbols | workspace_symbols | prepare_call_hierarchy | incoming_calls | outgoing_calls 중 선택하세요.")
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -118,13 +147,44 @@ public class LspTool : IAgentTool, IDisposable
|
||||
return ToolResult.Ok("참조를 찾을 수 없습니다.");
|
||||
|
||||
var sb = new System.Text.StringBuilder();
|
||||
sb.AppendLine($"총 {locations.Count}개 참조:");
|
||||
sb.AppendLine($"총 {locations.Count}개 참조");
|
||||
sb.AppendLine($"파일 수: {locations.Select(l => l.FilePath).Distinct(StringComparer.OrdinalIgnoreCase).Count()}");
|
||||
sb.AppendLine($"첫 참조: {locations[0]}");
|
||||
sb.AppendLine();
|
||||
foreach (var loc in locations.Take(30))
|
||||
sb.AppendLine($" {loc}");
|
||||
if (locations.Count > 30)
|
||||
sb.AppendLine($" ... 외 {locations.Count - 30}개");
|
||||
|
||||
return ToolResult.Ok(sb.ToString());
|
||||
return ToolResult.Ok(sb.ToString(), locations[0].FilePath);
|
||||
}
|
||||
|
||||
private async Task<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)
|
||||
@@ -134,13 +194,92 @@ public class LspTool : IAgentTool, IDisposable
|
||||
return ToolResult.Ok("심볼을 찾을 수 없습니다.");
|
||||
|
||||
var sb = new System.Text.StringBuilder();
|
||||
sb.AppendLine($"총 {symbols.Count}개 심볼:");
|
||||
sb.AppendLine($"총 {symbols.Count}개 심볼");
|
||||
sb.AppendLine($"파일: {Path.GetFileName(filePath)}");
|
||||
sb.AppendLine();
|
||||
foreach (var sym in symbols)
|
||||
sb.AppendLine($" {sym}");
|
||||
|
||||
return ToolResult.Ok(sb.ToString());
|
||||
}
|
||||
|
||||
private async Task<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)
|
||||
{
|
||||
if (_clients.TryGetValue(language, out var existing) && existing.IsConnected)
|
||||
@@ -173,6 +312,14 @@ public class LspTool : IAgentTool, IDisposable
|
||||
};
|
||||
}
|
||||
|
||||
private static int NormalizePosition(int value)
|
||||
{
|
||||
if (value <= 0)
|
||||
return 0;
|
||||
|
||||
return value - 1;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var client in _clients.Values)
|
||||
|
||||
@@ -58,8 +58,8 @@ public static class ModelExecutionProfileCatalog
|
||||
NoToolResponseThreshold: 1,
|
||||
NoToolRecoveryMaxRetries: 4, // IBM/Qwen 등 chatty 모델: 재시도 횟수 늘려 도구 호출 강제
|
||||
PlanExecutionRetryMax: 2,
|
||||
DocumentPlanRetryMax: 2,
|
||||
PreferAggressiveDocumentFallback: true,
|
||||
DocumentPlanRetryMax: 0,
|
||||
PreferAggressiveDocumentFallback: false,
|
||||
ReduceEarlyMemoryPressure: true,
|
||||
EnablePostToolVerification: false,
|
||||
EnableCodeQualityGates: true,
|
||||
@@ -87,18 +87,18 @@ public static class ModelExecutionProfileCatalog
|
||||
DocumentPlanRetryMax: 2,
|
||||
PreferAggressiveDocumentFallback: false,
|
||||
ReduceEarlyMemoryPressure: false,
|
||||
EnablePostToolVerification: true,
|
||||
EnablePostToolVerification: false,
|
||||
EnableCodeQualityGates: true,
|
||||
EnableDocumentVerificationGate: true,
|
||||
EnableDocumentVerificationGate: false,
|
||||
EnableParallelReadBatch: true,
|
||||
MaxParallelReadBatch: 6,
|
||||
CodeVerificationGateMaxRetries: 2,
|
||||
CodeVerificationGateMaxRetries: 1,
|
||||
HighImpactBuildTestGateMaxRetries: 1,
|
||||
FinalReportGateMaxRetries: 1,
|
||||
CodeDiffGateMaxRetries: 1,
|
||||
RecentExecutionGateMaxRetries: 1,
|
||||
ExecutionSuccessGateMaxRetries: 1,
|
||||
DocumentVerificationGateMaxRetries: 1,
|
||||
FinalReportGateMaxRetries: 0,
|
||||
CodeDiffGateMaxRetries: 0,
|
||||
RecentExecutionGateMaxRetries: 0,
|
||||
ExecutionSuccessGateMaxRetries: 0,
|
||||
DocumentVerificationGateMaxRetries: 0,
|
||||
TerminalEvidenceGateMaxRetries: 1),
|
||||
"fast_readonly" => new ExecutionPolicy(
|
||||
"fast_readonly",
|
||||
@@ -128,8 +128,8 @@ public static class ModelExecutionProfileCatalog
|
||||
"document_heavy" => new ExecutionPolicy(
|
||||
"document_heavy",
|
||||
"문서 생성 우선",
|
||||
ForceInitialToolCall: true,
|
||||
ForceToolCallAfterPlan: true,
|
||||
ForceInitialToolCall: false,
|
||||
ForceToolCallAfterPlan: false,
|
||||
ToolTemperatureCap: 0.35,
|
||||
NoToolResponseThreshold: 1,
|
||||
NoToolRecoveryMaxRetries: 1,
|
||||
@@ -154,26 +154,26 @@ public static class ModelExecutionProfileCatalog
|
||||
"balanced",
|
||||
"균형",
|
||||
ForceInitialToolCall: true,
|
||||
ForceToolCallAfterPlan: true,
|
||||
ForceToolCallAfterPlan: false,
|
||||
ToolTemperatureCap: 0.35,
|
||||
NoToolResponseThreshold: 2,
|
||||
NoToolRecoveryMaxRetries: 2,
|
||||
PlanExecutionRetryMax: 2,
|
||||
DocumentPlanRetryMax: 2,
|
||||
DocumentPlanRetryMax: 0,
|
||||
PreferAggressiveDocumentFallback: false,
|
||||
ReduceEarlyMemoryPressure: false,
|
||||
EnablePostToolVerification: true,
|
||||
EnablePostToolVerification: false,
|
||||
EnableCodeQualityGates: true,
|
||||
EnableDocumentVerificationGate: true,
|
||||
EnableDocumentVerificationGate: false,
|
||||
EnableParallelReadBatch: true,
|
||||
MaxParallelReadBatch: 6,
|
||||
CodeVerificationGateMaxRetries: 2,
|
||||
CodeVerificationGateMaxRetries: 1,
|
||||
HighImpactBuildTestGateMaxRetries: 1,
|
||||
FinalReportGateMaxRetries: 1,
|
||||
CodeDiffGateMaxRetries: 1,
|
||||
RecentExecutionGateMaxRetries: 1,
|
||||
ExecutionSuccessGateMaxRetries: 1,
|
||||
DocumentVerificationGateMaxRetries: 1,
|
||||
FinalReportGateMaxRetries: 0,
|
||||
CodeDiffGateMaxRetries: 0,
|
||||
RecentExecutionGateMaxRetries: 0,
|
||||
ExecutionSuccessGateMaxRetries: 0,
|
||||
DocumentVerificationGateMaxRetries: 0,
|
||||
TerminalEvidenceGateMaxRetries: 1),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -165,7 +165,7 @@ internal static class PptxToHtmlConverter
|
||||
<meta charset='UTF-8'>
|
||||
<style>
|
||||
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
|
||||
body {{ font-family: 'Malgun Gothic', 'Segoe UI', sans-serif; background: #f3f4f6; color: #1a1a1a;
|
||||
body {{ font-family: 'Noto Sans KR', 'Segoe UI', sans-serif; background: #f3f4f6; color: #1a1a1a;
|
||||
line-height: 1.6; padding: 20px; font-size: 13px; }}
|
||||
.slide-info {{ color: #6b7280; font-size: 12px; margin-bottom: 16px; text-align: center; }}
|
||||
.slide-card {{ background: #fff; border-radius: 12px; padding: 28px 32px; margin-bottom: 16px;
|
||||
|
||||
@@ -282,8 +282,9 @@ public static class SkillService
|
||||
사용자가 지정한 파일 또는 작업 폴더의 주요 파일을 읽고 상세히 설명하세요.
|
||||
|
||||
다음 도구를 사용하세요:
|
||||
1. file_read — 파일 내용 읽기
|
||||
2. folder_map — 프로젝트 구조 파악 (필요시)
|
||||
1. glob/grep — 관련 파일 후보 찾기
|
||||
2. file_read — 파일 내용 읽기
|
||||
3. folder_map — 프로젝트 구조가 꼭 필요할 때만 사용
|
||||
|
||||
설명 포함 사항:
|
||||
- 파일의 역할과 책임
|
||||
|
||||
@@ -11,13 +11,13 @@ internal sealed class StreamingToolExecutionCoordinator : IToolExecutionCoordina
|
||||
{
|
||||
private static readonly HashSet<string> PrefetchableReadOnlyTools = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"file_read", "glob", "grep", "grep_tool", "folder_map", "document_read",
|
||||
"search_codebase", "code_search", "env_tool", "datetime_tool",
|
||||
"file_read", "document_read",
|
||||
"env_tool", "datetime_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, JsonElement, AgentContext, List<ChatMessage>?, CancellationToken, Task<ToolResult>> _executeToolAsync;
|
||||
private readonly Action<AgentEventType, string, string> _emitEvent;
|
||||
@@ -27,7 +27,7 @@ internal sealed class StreamingToolExecutionCoordinator : IToolExecutionCoordina
|
||||
private readonly Func<int, Exception, int> _computeTransientBackoffDelayMs;
|
||||
|
||||
public StreamingToolExecutionCoordinator(
|
||||
LlmService llm,
|
||||
ILlmService llm,
|
||||
Func<string, IReadOnlyCollection<string>, string> resolveRequestedToolName,
|
||||
Func<string, JsonElement, AgentContext, List<ChatMessage>?, CancellationToken, Task<ToolResult>> executeToolAsync,
|
||||
Action<AgentEventType, string, string> emitEvent,
|
||||
@@ -46,8 +46,8 @@ internal sealed class StreamingToolExecutionCoordinator : IToolExecutionCoordina
|
||||
_computeTransientBackoffDelayMs = computeTransientBackoffDelayMs;
|
||||
}
|
||||
|
||||
public async Task<LlmService.ToolPrefetchResult?> TryPrefetchReadOnlyToolAsync(
|
||||
LlmService.ContentBlock block,
|
||||
public async Task<ToolPrefetchResult?> TryPrefetchReadOnlyToolAsync(
|
||||
ContentBlock block,
|
||||
IReadOnlyCollection<IAgentTool> tools,
|
||||
AgentContext context,
|
||||
CancellationToken ct)
|
||||
@@ -70,57 +70,60 @@ internal sealed class StreamingToolExecutionCoordinator : IToolExecutionCoordina
|
||||
var input = block.ToolInput ?? JsonDocument.Parse("{}").RootElement;
|
||||
var result = await _executeToolAsync(resolvedToolName, input, context, null, ct);
|
||||
sw.Stop();
|
||||
return new LlmService.ToolPrefetchResult(result, sw.ElapsedMilliseconds, resolvedToolName);
|
||||
return new ToolPrefetchResult(result, sw.ElapsedMilliseconds, resolvedToolName);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
sw.Stop();
|
||||
return new LlmService.ToolPrefetchResult(
|
||||
return new ToolPrefetchResult(
|
||||
ToolResult.Fail($"조기 실행 오류: {ex.Message}"),
|
||||
sw.ElapsedMilliseconds,
|
||||
resolvedToolName);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<LlmService.ContentBlock>> SendWithToolsWithRecoveryAsync(
|
||||
public async Task<List<ContentBlock>> SendWithToolsWithRecoveryAsync(
|
||||
List<ChatMessage> messages,
|
||||
IReadOnlyCollection<IAgentTool> tools,
|
||||
CancellationToken ct,
|
||||
string phaseLabel,
|
||||
AgentLoopService.RunState? runState = null,
|
||||
bool forceToolCall = false,
|
||||
Func<LlmService.ContentBlock, Task<LlmService.ToolPrefetchResult?>>? prefetchToolCallAsync = null,
|
||||
Func<LlmService.ToolStreamEvent, Task>? onStreamEventAsync = null)
|
||||
Func<ContentBlock, Task<ToolPrefetchResult?>>? prefetchToolCallAsync = null,
|
||||
Func<ToolStreamEvent, Task>? onStreamEventAsync = null)
|
||||
{
|
||||
var transientRetries = runState?.TransientLlmErrorRetries ?? 0;
|
||||
var contextRecoveryRetries = runState?.ContextRecoveryAttempts ?? 0;
|
||||
|
||||
while (true)
|
||||
{
|
||||
var streamedAnyPartialState = false;
|
||||
try
|
||||
{
|
||||
if (onStreamEventAsync == null)
|
||||
return await _llm.SendWithToolsAsync(messages, tools, ct, forceToolCall, prefetchToolCallAsync);
|
||||
|
||||
var blocks = new List<LlmService.ContentBlock>();
|
||||
var blocks = new List<ContentBlock>();
|
||||
var textBuilder = new StringBuilder();
|
||||
await foreach (var evt in _llm.StreamWithToolsAsync(messages, tools, forceToolCall, prefetchToolCallAsync, ct).WithCancellation(ct))
|
||||
{
|
||||
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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
var result = new List<LlmService.ContentBlock>();
|
||||
var result = new List<ContentBlock>();
|
||||
var text = textBuilder.ToString().Trim();
|
||||
if (!string.IsNullOrWhiteSpace(text))
|
||||
result.Add(new LlmService.ContentBlock { Type = "text", Text = text });
|
||||
result.Add(new ContentBlock { Type = "text", Text = text });
|
||||
result.AddRange(blocks);
|
||||
return result;
|
||||
}
|
||||
@@ -130,6 +133,8 @@ internal sealed class StreamingToolExecutionCoordinator : IToolExecutionCoordina
|
||||
&& contextRecoveryRetries < 2
|
||||
&& _forceContextRecovery(messages))
|
||||
{
|
||||
if (onStreamEventAsync != null && streamedAnyPartialState)
|
||||
await onStreamEventAsync(new ToolStreamEvent(ToolStreamEventKind.RetryReset, $"{phaseLabel}:retry"));
|
||||
contextRecoveryRetries++;
|
||||
if (runState != null)
|
||||
runState.ContextRecoveryAttempts = contextRecoveryRetries;
|
||||
@@ -146,6 +151,8 @@ internal sealed class StreamingToolExecutionCoordinator : IToolExecutionCoordina
|
||||
|
||||
if (_isTransientLlmError(ex) && transientRetries < 3)
|
||||
{
|
||||
if (onStreamEventAsync != null && streamedAnyPartialState)
|
||||
await onStreamEventAsync(new ToolStreamEvent(ToolStreamEventKind.RetryReset, $"{phaseLabel}:retry"));
|
||||
transientRetries++;
|
||||
if (runState != null)
|
||||
runState.TransientLlmErrorRetries = transientRetries;
|
||||
|
||||
@@ -22,21 +22,22 @@ internal sealed class TaskTypePolicy
|
||||
TaskType = "bugfix",
|
||||
GuidanceMessage =
|
||||
"[System:TaskType] This is a bug-fix task. Prioritize reproduction evidence, root cause linkage, smallest safe fix, and regression verification. " +
|
||||
"Preferred tool order: file_read -> grep/glob -> build_run/test_loop -> file_edit -> build_run/test_loop.",
|
||||
FailurePatternFocus = "재현 조건과 원인 연결을 먼저 확인하세요. Check reproduction conditions and root-cause linkage first.",
|
||||
FollowUpTaskLine = "작업 유형: bugfix. Task type: bugfix. Verify the fix is directly linked to the symptom and confirm non-regression.\n",
|
||||
FailureInvestigationTaskLine = "추가 점검: 재현 조건 기준으로 증상이 재현되지 않는지와 원인 연결이 타당한지 확인하세요. Extra check: confirm the symptom is no longer reproducible and root-cause linkage is valid.\n",
|
||||
FinalReportTaskLine = "버그 수정은 원인, 수정 내용, 재현/회귀 검증 근거를 포함하세요. For bug fixes, include root cause, change summary, and reproduction/regression evidence.\n",
|
||||
"Preferred tool order: targeted file_read or grep/glob/lsp -> file_edit -> build_run/test_loop as needed -> git_tool(diff) when it helps confirm the final change.",
|
||||
FailurePatternFocus = "Check reproduction conditions and root-cause linkage first.",
|
||||
FollowUpTaskLine = "Task type: bugfix. Verify the fix is directly linked to the symptom and confirm non-regression.\n",
|
||||
FailureInvestigationTaskLine = "Extra check: confirm the symptom is no longer reproducible and root-cause linkage is valid.\n",
|
||||
FinalReportTaskLine = "For bug fixes, include root cause, change summary, and reproduction/regression evidence.\n",
|
||||
},
|
||||
"feature" => new TaskTypePolicy
|
||||
{
|
||||
TaskType = "feature",
|
||||
GuidanceMessage =
|
||||
"[System:TaskType] This is a feature task. Prioritize affected interfaces/callers, data flow, validation paths, and test/documentation needs. " +
|
||||
"Preferred tool order: folder_map -> file_read -> grep/glob -> file_edit -> build_run/test_loop.",
|
||||
"Preferred tool order: targeted file_read or grep/glob/lsp -> file_edit/file_write -> build_run/test_loop as needed -> git_tool(diff). " +
|
||||
"Use folder_map only when the user explicitly needs folder structure or file listing.",
|
||||
FailurePatternFocus = "Check new behavior flow and caller linkage first.",
|
||||
FollowUpTaskLine = "작업 유형: feature. Task type: feature. Verify behavior flow, input/output path, caller impact, and test additions.\n",
|
||||
FailureInvestigationTaskLine = "추가 점검: 새 기능 경로와 호출부 연결이 의도대로 동작하는지 확인하세요. Extra check: confirm feature path and caller linkage behave as intended.\n",
|
||||
FollowUpTaskLine = "Task type: feature. Verify behavior flow, input/output path, caller impact, and test additions.\n",
|
||||
FailureInvestigationTaskLine = "Extra check: confirm feature path and caller linkage behave as intended.\n",
|
||||
FinalReportTaskLine = "For features, include behavior flow, impacted files/callers, and verification evidence.\n",
|
||||
},
|
||||
"refactor" => new TaskTypePolicy
|
||||
@@ -44,10 +45,10 @@ internal sealed class TaskTypePolicy
|
||||
TaskType = "refactor",
|
||||
GuidanceMessage =
|
||||
"[System:TaskType] This is a refactor task. Prioritize behavior preservation, reference impact, diff review, and non-regression evidence. " +
|
||||
"Preferred tool order: file_read -> grep/glob -> git_tool(diff) -> file_edit -> build_run/test_loop.",
|
||||
"Preferred tool order: targeted file_read or grep/glob/lsp -> file_edit -> build_run/test_loop as needed -> git_tool(diff).",
|
||||
FailurePatternFocus = "Check behavior preservation and impact scope first.",
|
||||
FollowUpTaskLine = "작업 유형: refactor. Task type: refactor. Prioritize behavior-preservation evidence over cosmetic cleanup.\n",
|
||||
FailureInvestigationTaskLine = "추가 점검: 동작 보존 관점에서 기존 호출 흐름이 동일하게 유지되는지 확인하세요. Extra check: validate existing call flow remains behavior-compatible.\n",
|
||||
FollowUpTaskLine = "Task type: refactor. Prioritize behavior-preservation evidence over cosmetic cleanup.\n",
|
||||
FailureInvestigationTaskLine = "Extra check: validate existing call flow remains behavior-compatible.\n",
|
||||
FinalReportTaskLine = "For refactors, include behavior-preservation evidence and impact scope.\n",
|
||||
},
|
||||
"review" => new TaskTypePolicy
|
||||
@@ -56,10 +57,10 @@ internal sealed class TaskTypePolicy
|
||||
GuidanceMessage =
|
||||
"[System:TaskType] This is a review task. Prioritize concrete defects, regressions, risky assumptions, and missing tests before summaries. " +
|
||||
"Report findings with P0-P3 severity and file evidence, then separate Fixed vs Unfixed status. " +
|
||||
"Preferred tool order: file_read -> grep/glob -> git_tool(diff) -> evidence-first findings.",
|
||||
"Preferred tool order: targeted file_read or grep/glob/lsp -> git_tool(diff) when available -> evidence-first findings.",
|
||||
FailurePatternFocus = "Review focus: severity accuracy (P0-P3), file-grounded evidence, and unresolved-risk clarity.",
|
||||
FollowUpTaskLine = "작업 유형: review-follow-up. Task type: review-follow-up. For each finding, state status as Fixed or Unfixed with verification evidence.\n",
|
||||
FailureInvestigationTaskLine = "추가 점검: 리뷰에서 지적된 위험은 반드시 수정 근거나 미해결 사유/영향을 남기세요. Extra check: every risk must have either a concrete fix or an explicit unresolved rationale and impact.\n",
|
||||
FollowUpTaskLine = "Task type: review-follow-up. For each finding, state status as Fixed or Unfixed with verification evidence.\n",
|
||||
FailureInvestigationTaskLine = "Extra check: every risk must have either a concrete fix or an explicit unresolved rationale and impact.\n",
|
||||
FinalReportTaskLine = "For review, list P0-P3 findings with file evidence and split into Fixed vs Unfixed with residual risk.\n",
|
||||
IsReviewTask = true
|
||||
},
|
||||
@@ -67,8 +68,12 @@ internal sealed class TaskTypePolicy
|
||||
{
|
||||
TaskType = "docs",
|
||||
GuidanceMessage =
|
||||
"[System:TaskType] This is a document/content task. Prioritize source evidence, completeness, consistency, and self-review. " +
|
||||
"Preferred tool order: folder_map -> document_read/file_read -> drafting tool -> self-review.",
|
||||
"[System:TaskType] This is a document/content task. " +
|
||||
"If the user asks you to create or write a new document, skip file exploration entirely and go directly to the creation tool (docx_create, html_create, excel_create, etc.). " +
|
||||
"Use document_plan only when it materially helps structure a multi-section document. " +
|
||||
"Prefer producing a real file on disk when the request is clearly for a deliverable, but do not force file creation for analysis-only or advisory requests. " +
|
||||
"If the user asks you to read or analyze existing documents, use: glob/grep -> document_read/file_read -> analysis. " +
|
||||
"Use folder_map only when the user explicitly asks for folder contents or directory structure.",
|
||||
FailurePatternFocus = "Check source evidence and document completeness first.",
|
||||
FollowUpTaskLine = "",
|
||||
FailureInvestigationTaskLine = "",
|
||||
@@ -77,7 +82,7 @@ internal sealed class TaskTypePolicy
|
||||
_ => new TaskTypePolicy
|
||||
{
|
||||
TaskType = "general",
|
||||
GuidanceMessage = "[System:TaskType] Use a cautious analyze -> implement -> verify workflow and do not finish without concrete evidence.",
|
||||
GuidanceMessage = "[System:TaskType] Use a cautious analyze -> selective discovery -> implement -> verify workflow. Prefer targeted file_read, grep/glob, or lsp_code_intel before broad scans, and do not finish without concrete evidence.",
|
||||
FailurePatternFocus = "Check recent failure patterns first.",
|
||||
FollowUpTaskLine = "",
|
||||
FailureInvestigationTaskLine = "",
|
||||
|
||||
@@ -90,7 +90,7 @@ public class TemplateRenderTool : IAgentTool
|
||||
await TextFileCodec.WriteAllTextAsync(outputPath, rendered, TextFileCodec.Utf8NoBom, ct);
|
||||
|
||||
return ToolResult.Ok(
|
||||
$"✅ 템플릿 렌더링 완료: {Path.GetFileName(outputPath)} ({rendered.Length:N0}자)",
|
||||
$"✅ 템플릿 렌더링 완료: {outputPath} ({rendered.Length:N0}자)",
|
||||
outputPath);
|
||||
}
|
||||
|
||||
|
||||
@@ -182,7 +182,7 @@ public static class TemplateService
|
||||
private const string CssModern = """
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
||||
* { margin:0; padding:0; box-sizing:border-box; }
|
||||
body { font-family: 'Inter', 'Segoe UI', 'Malgun Gothic', sans-serif;
|
||||
body { font-family: 'Inter', 'Segoe UI', 'Noto Sans KR', sans-serif;
|
||||
background: #f5f5f7; color: #1d1d1f; line-height: 1.75; padding: 48px 24px; }
|
||||
.container { max-width: 1080px; margin: 0 auto; background: #fff;
|
||||
border-radius: 16px; padding: 56px 52px;
|
||||
@@ -218,7 +218,7 @@ public static class TemplateService
|
||||
#region Professional — 전문가
|
||||
private const string CssProfessional = """
|
||||
* { margin:0; padding:0; box-sizing:border-box; }
|
||||
body { font-family: 'Segoe UI', 'Malgun Gothic', Arial, sans-serif;
|
||||
body { font-family: 'Segoe UI', 'Noto Sans KR', Arial, sans-serif;
|
||||
background: #eef1f5; color: #1e293b; line-height: 1.7; padding: 40px 20px; }
|
||||
.container { max-width: 1080px; margin: 0 auto; background: #fff;
|
||||
border-radius: 8px; padding: 48px;
|
||||
@@ -257,7 +257,7 @@ public static class TemplateService
|
||||
private const string CssCreative = """
|
||||
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap');
|
||||
* { margin:0; padding:0; box-sizing:border-box; }
|
||||
body { font-family: 'Poppins', 'Segoe UI', 'Malgun Gothic', sans-serif;
|
||||
body { font-family: 'Poppins', 'Segoe UI', 'Noto Sans KR', sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh; color: #2d3748; line-height: 1.75; padding: 48px 24px; }
|
||||
.container { max-width: 1080px; margin: 0 auto; background: rgba(255,255,255,0.95);
|
||||
@@ -336,7 +336,7 @@ public static class TemplateService
|
||||
private const string CssElegant = """
|
||||
@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;500;600;700&family=Source+Sans+3:wght@300;400;600&display=swap');
|
||||
* { margin:0; padding:0; box-sizing:border-box; }
|
||||
body { font-family: 'Source Sans 3', 'Malgun Gothic', sans-serif;
|
||||
body { font-family: 'Source Sans 3', 'Noto Sans KR', sans-serif;
|
||||
background: #faf8f5; color: #3d3929; line-height: 1.75; padding: 48px 24px; }
|
||||
.container { max-width: 1080px; margin: 0 auto; background: #fff;
|
||||
border-radius: 4px; padding: 56px 52px;
|
||||
@@ -375,7 +375,7 @@ public static class TemplateService
|
||||
private const string CssDark = """
|
||||
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&family=Inter:wght@300;400;500;600;700&display=swap');
|
||||
* { margin:0; padding:0; box-sizing:border-box; }
|
||||
body { font-family: 'Inter', 'Segoe UI', 'Malgun Gothic', sans-serif;
|
||||
body { font-family: 'Inter', 'Segoe UI', 'Noto Sans KR', sans-serif;
|
||||
background: #0d1117; color: #e6edf3; line-height: 1.75; padding: 48px 24px; }
|
||||
.container { max-width: 1080px; margin: 0 auto; background: #161b22;
|
||||
border-radius: 12px; padding: 52px;
|
||||
@@ -415,7 +415,7 @@ public static class TemplateService
|
||||
private const string CssColorful = """
|
||||
@import url('https://fonts.googleapis.com/css2?family=Nunito:wght@300;400;600;700;800&display=swap');
|
||||
* { margin:0; padding:0; box-sizing:border-box; }
|
||||
body { font-family: 'Nunito', 'Segoe UI', 'Malgun Gothic', sans-serif;
|
||||
body { font-family: 'Nunito', 'Segoe UI', 'Noto Sans KR', sans-serif;
|
||||
background: linear-gradient(135deg, #ffecd2 0%, #fcb69f 50%, #ffecd2 100%);
|
||||
min-height: 100vh; color: #2d3436; line-height: 1.75; padding: 48px 24px; }
|
||||
.container { max-width: 1080px; margin: 0 auto; background: #fff;
|
||||
@@ -455,7 +455,7 @@ public static class TemplateService
|
||||
#region Corporate — 기업 공식
|
||||
private const string CssCorporate = """
|
||||
* { margin:0; padding:0; box-sizing:border-box; }
|
||||
body { font-family: 'Segoe UI', 'Malgun Gothic', Arial, sans-serif;
|
||||
body { font-family: 'Segoe UI', 'Noto Sans KR', Arial, sans-serif;
|
||||
background: #f2f2f2; color: #333; line-height: 1.65; padding: 32px 20px; }
|
||||
.container { max-width: 1080px; margin: 0 auto; background: #fff; padding: 0;
|
||||
box-shadow: 0 1px 4px rgba(0,0,0,0.1); }
|
||||
@@ -464,6 +464,9 @@ public static class TemplateService
|
||||
.header-bar h1 { font-size: 22px; font-weight: 700; color: #fff; margin-bottom: 2px; }
|
||||
.header-bar .meta { color: rgba(255,255,255,0.7); margin-bottom: 0; font-size: 12px; }
|
||||
.body-content { padding: 36px 40px 40px; }
|
||||
.cover-page { margin: -36px -40px 40px -40px !important; border-radius: 0 !important;
|
||||
width: auto !important; box-sizing: border-box !important;
|
||||
left: 0; right: 0; }
|
||||
h1 { font-size: 22px; font-weight: 700; color: #003366; margin-bottom: 4px; }
|
||||
h2 { font-size: 17px; font-weight: 600; margin: 28px 0 10px; color: #003366;
|
||||
border-left: 4px solid #ff6600; padding-left: 12px; }
|
||||
@@ -497,7 +500,7 @@ public static class TemplateService
|
||||
private const string CssMagazine = """
|
||||
@import url('https://fonts.googleapis.com/css2?family=Merriweather:wght@300;400;700;900&family=Open+Sans:wght@300;400;600;700&display=swap');
|
||||
* { margin:0; padding:0; box-sizing:border-box; }
|
||||
body { font-family: 'Open Sans', 'Malgun Gothic', sans-serif;
|
||||
body { font-family: 'Open Sans', 'Noto Sans KR', sans-serif;
|
||||
background: #f0ece3; color: #2c2c2c; line-height: 1.7; padding: 48px 24px; }
|
||||
.container { max-width: 1080px; margin: 0 auto; background: #fff;
|
||||
border-radius: 2px; padding: 0; overflow: hidden;
|
||||
@@ -548,7 +551,7 @@ public static class TemplateService
|
||||
private const string CssDashboard = """
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
||||
* { margin:0; padding:0; box-sizing:border-box; }
|
||||
body { font-family: 'Inter', 'Segoe UI', 'Malgun Gothic', sans-serif;
|
||||
body { font-family: 'Inter', 'Segoe UI', 'Noto Sans KR', sans-serif;
|
||||
background: #f0f2f5; color: #1a1a2e; line-height: 1.6; padding: 32px 24px; }
|
||||
.container { max-width: 1000px; margin: 0 auto; padding: 0; background: transparent; }
|
||||
h1 { font-size: 26px; font-weight: 700; color: #1a1a2e; margin-bottom: 4px; }
|
||||
|
||||
@@ -62,7 +62,174 @@ public class ToolRegistry : IDisposable
|
||||
if (disabledNames == null) return All;
|
||||
var disabled = new HashSet<string>(disabledNames, StringComparer.OrdinalIgnoreCase);
|
||||
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 "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,
|
||||
"folder_map" or "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>
|
||||
|
||||
@@ -202,7 +202,7 @@ internal static class XlsxToHtmlConverter
|
||||
<meta charset='UTF-8'>
|
||||
<style>
|
||||
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
|
||||
body {{ font-family: 'Malgun Gothic', 'Segoe UI', sans-serif; background: #fff; color: #1a1a1a;
|
||||
body {{ font-family: 'Noto Sans KR', 'Segoe UI', sans-serif; background: #fff; color: #1a1a1a;
|
||||
line-height: 1.5; padding: 16px; font-size: 12.5px; }}
|
||||
.sheet-tabs {{ display: flex; gap: 2px; margin-bottom: 12px; border-bottom: 2px solid #e5e7eb; padding-bottom: 0; }}
|
||||
.sheet-tab {{ padding: 6px 14px; border: none; background: #f3f4f6; cursor: pointer;
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace AxCopilot.Services;
|
||||
/// 채팅 세션 외의 스킬, MCP, 권한, 도구 카탈로그 상태를 한 곳에 모아
|
||||
/// 창별 로컬 필드 의존도를 줄이는 1차 계층입니다.
|
||||
/// </summary>
|
||||
public sealed class AppStateService
|
||||
public sealed class AppStateService : IAppStateService
|
||||
{
|
||||
public sealed class SkillCatalogState
|
||||
{
|
||||
@@ -212,7 +212,7 @@ public sealed class AppStateService
|
||||
NotifyStateChanged();
|
||||
}
|
||||
|
||||
public void LoadFromSettings(SettingsService settings)
|
||||
public void LoadFromSettings(ISettingsService settings)
|
||||
{
|
||||
var llm = settings.Settings.Llm;
|
||||
Skills.Enabled = llm.EnableSkillSystem;
|
||||
|
||||
@@ -51,7 +51,7 @@ public sealed class ChatSessionStateService
|
||||
return CurrentConversation;
|
||||
}
|
||||
|
||||
public void Load(SettingsService settings)
|
||||
public void Load(ISettingsService settings)
|
||||
{
|
||||
var llm = settings.Settings.Llm;
|
||||
ActiveTab = NormalizeTab(llm.LastActiveTab);
|
||||
@@ -67,7 +67,7 @@ public sealed class ChatSessionStateService
|
||||
}
|
||||
}
|
||||
|
||||
public void Save(SettingsService settings)
|
||||
public void Save(ISettingsService settings)
|
||||
{
|
||||
var llm = settings.Settings.Llm;
|
||||
llm.LastActiveTab = NormalizeTab(ActiveTab);
|
||||
@@ -87,7 +87,7 @@ public sealed class ChatSessionStateService
|
||||
TabConversationIds[NormalizeTab(tab)] = string.IsNullOrWhiteSpace(conversationId) ? null : conversationId;
|
||||
}
|
||||
|
||||
public ChatConversation LoadOrCreateConversation(string tab, ChatStorageService storage, SettingsService settings)
|
||||
public ChatConversation LoadOrCreateConversation(string tab, IChatStorageService storage, ISettingsService settings)
|
||||
{
|
||||
var normalizedTab = NormalizeTab(tab);
|
||||
var rememberedId = GetConversationId(normalizedTab);
|
||||
@@ -153,13 +153,19 @@ public sealed class ChatSessionStateService
|
||||
return CreateFreshConversation(normalizedTab, settings);
|
||||
}
|
||||
|
||||
public ChatConversation CreateFreshConversation(string tab, SettingsService settings)
|
||||
public ChatConversation CreateFreshConversation(string tab, ISettingsService settings)
|
||||
{
|
||||
var normalizedTab = NormalizeTab(tab);
|
||||
var created = new ChatConversation { Tab = normalizedTab };
|
||||
var workFolder = settings.Settings.Llm.WorkFolder;
|
||||
if (!string.IsNullOrWhiteSpace(workFolder) && normalizedTab != "Chat")
|
||||
created.WorkFolder = workFolder;
|
||||
|
||||
// Code/Cowork 탭: 매 대화마다 폴더를 새로 선택하도록 빈 상태로 시작
|
||||
if (string.Equals(normalizedTab, "Code", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(normalizedTab, "Cowork", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
created.WorkFolder = "";
|
||||
CurrentConversation = created;
|
||||
return created;
|
||||
}
|
||||
|
||||
CurrentConversation = created;
|
||||
return created;
|
||||
@@ -239,7 +245,7 @@ public sealed class ChatSessionStateService
|
||||
return fork;
|
||||
}
|
||||
|
||||
public void SaveCurrentConversation(ChatStorageService storage, string tab)
|
||||
public void SaveCurrentConversation(IChatStorageService storage, string tab)
|
||||
{
|
||||
var conv = CurrentConversation;
|
||||
if (conv == null) return;
|
||||
@@ -261,7 +267,7 @@ public sealed class ChatSessionStateService
|
||||
RememberConversation(tab, null);
|
||||
}
|
||||
|
||||
public ChatConversation SetCurrentConversation(string tab, ChatConversation conversation, ChatStorageService? storage = null, bool remember = true)
|
||||
public ChatConversation SetCurrentConversation(string tab, ChatConversation conversation, IChatStorageService? storage = null, bool remember = true)
|
||||
{
|
||||
var normalizedTab = NormalizeTab(tab);
|
||||
conversation.Tab = normalizedTab;
|
||||
@@ -275,7 +281,7 @@ public sealed class ChatSessionStateService
|
||||
return conversation;
|
||||
}
|
||||
|
||||
public ChatMessage AppendMessage(string tab, ChatMessage message, ChatStorageService? storage = null, bool useForTitle = false)
|
||||
public ChatMessage AppendMessage(string tab, ChatMessage message, IChatStorageService? storage = null, bool useForTitle = false)
|
||||
{
|
||||
var conv = EnsureCurrentConversation(tab);
|
||||
conv.Messages.Add(message);
|
||||
@@ -294,7 +300,7 @@ public sealed class ChatSessionStateService
|
||||
public ChatConversation UpdateConversationMetadata(
|
||||
string tab,
|
||||
Action<ChatConversation> apply,
|
||||
ChatStorageService? storage = null,
|
||||
IChatStorageService? storage = null,
|
||||
bool ensureConversation = true)
|
||||
{
|
||||
var conv = ensureConversation ? EnsureCurrentConversation(tab) : (CurrentConversation ?? new ChatConversation { Tab = NormalizeTab(tab) });
|
||||
@@ -311,7 +317,7 @@ public sealed class ChatSessionStateService
|
||||
string? dataUsage,
|
||||
string? outputFormat,
|
||||
string? mood,
|
||||
ChatStorageService? storage = null)
|
||||
IChatStorageService? storage = null)
|
||||
{
|
||||
return UpdateConversationMetadata(tab, conv =>
|
||||
{
|
||||
@@ -322,7 +328,7 @@ public sealed class ChatSessionStateService
|
||||
}, storage);
|
||||
}
|
||||
|
||||
public bool RemoveLastAssistantMessage(string tab, ChatStorageService? storage = null)
|
||||
public bool RemoveLastAssistantMessage(string tab, IChatStorageService? storage = null)
|
||||
{
|
||||
var conv = CurrentConversation;
|
||||
if (conv == null || conv.Messages.Count == 0)
|
||||
@@ -336,7 +342,7 @@ public sealed class ChatSessionStateService
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool UpdateUserMessageAndTrim(string tab, int userMessageIndex, string newText, ChatStorageService? storage = null)
|
||||
public bool UpdateUserMessageAndTrim(string tab, int userMessageIndex, string newText, IChatStorageService? storage = null)
|
||||
{
|
||||
var conv = CurrentConversation;
|
||||
if (conv == null)
|
||||
@@ -353,7 +359,7 @@ public sealed class ChatSessionStateService
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool UpdateMessageFeedback(string tab, ChatMessage message, string? feedback, ChatStorageService? storage = null)
|
||||
public bool UpdateMessageFeedback(string tab, ChatMessage message, string? feedback, IChatStorageService? storage = null)
|
||||
{
|
||||
var conv = CurrentConversation;
|
||||
if (conv == null)
|
||||
@@ -368,7 +374,7 @@ public sealed class ChatSessionStateService
|
||||
return true;
|
||||
}
|
||||
|
||||
public ChatConversation AppendExecutionEvent(string tab, Agent.AgentEvent evt, ChatStorageService? storage = null)
|
||||
public ChatConversation AppendExecutionEvent(string tab, Agent.AgentEvent evt, IChatStorageService? storage = null)
|
||||
{
|
||||
var conv = EnsureCurrentConversation(tab);
|
||||
conv.ExecutionEvents ??= new List<ChatExecutionEvent>();
|
||||
@@ -417,7 +423,7 @@ public sealed class ChatSessionStateService
|
||||
return conv;
|
||||
}
|
||||
|
||||
public ChatConversation AppendAgentRun(string tab, Agent.AgentEvent evt, string status, string summary, ChatStorageService? storage = null)
|
||||
public ChatConversation AppendAgentRun(string tab, Agent.AgentEvent evt, string status, string summary, IChatStorageService? storage = null)
|
||||
{
|
||||
var conv = EnsureCurrentConversation(tab);
|
||||
conv.AgentRunHistory ??= new List<ChatAgentRunRecord>();
|
||||
@@ -448,7 +454,7 @@ public sealed class ChatSessionStateService
|
||||
return conv;
|
||||
}
|
||||
|
||||
public DraftQueueItem? EnqueueDraft(string tab, string text, string priority = "next", ChatStorageService? storage = null, string kind = "message")
|
||||
public DraftQueueItem? EnqueueDraft(string tab, string text, string priority = "next", IChatStorageService? storage = null, string kind = "message")
|
||||
{
|
||||
var trimmed = text?.Trim() ?? "";
|
||||
if (string.IsNullOrWhiteSpace(trimmed))
|
||||
@@ -480,16 +486,16 @@ public sealed class ChatSessionStateService
|
||||
return (conv.DraftQueueItems ?? []).ToList();
|
||||
}
|
||||
|
||||
public bool MarkDraftRunning(string tab, string draftId, ChatStorageService? storage = null)
|
||||
public bool MarkDraftRunning(string tab, string draftId, IChatStorageService? storage = null)
|
||||
=> UpdateDraftItem(tab, draftId, item => _draftQueue.MarkRunning(item), storage);
|
||||
|
||||
public bool MarkDraftCompleted(string tab, string draftId, ChatStorageService? storage = null)
|
||||
public bool MarkDraftCompleted(string tab, string draftId, IChatStorageService? storage = null)
|
||||
=> UpdateDraftItem(tab, draftId, item => _draftQueue.MarkCompleted(item), storage);
|
||||
|
||||
public bool MarkDraftFailed(string tab, string draftId, string? error, ChatStorageService? storage = null)
|
||||
public bool MarkDraftFailed(string tab, string draftId, string? error, IChatStorageService? storage = null)
|
||||
=> UpdateDraftItem(tab, draftId, item => _draftQueue.MarkFailed(item, error), storage);
|
||||
|
||||
public bool ScheduleDraftRetry(string tab, string draftId, string? error, int maxAutoRetries = 3, ChatStorageService? storage = null)
|
||||
public bool ScheduleDraftRetry(string tab, string draftId, string? error, int maxAutoRetries = 3, IChatStorageService? storage = null)
|
||||
{
|
||||
return UpdateDraftItem(tab, draftId, item =>
|
||||
{
|
||||
@@ -503,10 +509,10 @@ public sealed class ChatSessionStateService
|
||||
}, storage);
|
||||
}
|
||||
|
||||
public bool ResetDraftToQueued(string tab, string draftId, ChatStorageService? storage = null)
|
||||
public bool ResetDraftToQueued(string tab, string draftId, IChatStorageService? storage = null)
|
||||
=> UpdateDraftItem(tab, draftId, item => _draftQueue.ResetToQueued(item), storage);
|
||||
|
||||
public bool RemoveDraft(string tab, string draftId, ChatStorageService? storage = null)
|
||||
public bool RemoveDraft(string tab, string draftId, IChatStorageService? storage = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(draftId))
|
||||
return false;
|
||||
@@ -520,7 +526,7 @@ public sealed class ChatSessionStateService
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool ToggleExecutionHistory(string tab, ChatStorageService? storage = null)
|
||||
public bool ToggleExecutionHistory(string tab, IChatStorageService? storage = null)
|
||||
{
|
||||
var conv = EnsureCurrentConversation(tab);
|
||||
conv.ShowExecutionHistory = !conv.ShowExecutionHistory;
|
||||
@@ -528,7 +534,7 @@ public sealed class ChatSessionStateService
|
||||
return conv.ShowExecutionHistory;
|
||||
}
|
||||
|
||||
public void SaveConversationListPreferences(string tab, bool failedOnly, bool runningOnly, bool sortByRecent, ChatStorageService? storage = null)
|
||||
public void SaveConversationListPreferences(string tab, bool failedOnly, bool runningOnly, bool sortByRecent, IChatStorageService? storage = null)
|
||||
{
|
||||
var conv = EnsureCurrentConversation(tab);
|
||||
conv.ConversationFailedOnlyFilter = failedOnly;
|
||||
@@ -758,7 +764,7 @@ public sealed class ChatSessionStateService
|
||||
return result;
|
||||
}
|
||||
|
||||
private void TouchConversation(ChatStorageService? storage, string tab)
|
||||
private void TouchConversation(IChatStorageService? storage, string tab)
|
||||
{
|
||||
var conv = EnsureCurrentConversation(tab);
|
||||
conv.UpdatedAt = DateTime.Now;
|
||||
@@ -768,7 +774,7 @@ public sealed class ChatSessionStateService
|
||||
try { storage?.Save(conv); } catch { }
|
||||
}
|
||||
|
||||
private bool UpdateDraftItem(string tab, string draftId, Func<DraftQueueItem, bool> update, ChatStorageService? storage)
|
||||
private bool UpdateDraftItem(string tab, string draftId, Func<DraftQueueItem, bool> update, IChatStorageService? storage)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(draftId))
|
||||
return false;
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace AxCopilot.Services;
|
||||
/// 스레드 안전: ReaderWriterLockSlim으로 동시 접근 보호.
|
||||
/// 원자적 쓰기: 임시 파일 → rename 패턴으로 크래시 시 데이터 손실 방지.
|
||||
/// </summary>
|
||||
public class ChatStorageService
|
||||
public class ChatStorageService : IChatStorageService
|
||||
{
|
||||
private static readonly string ConversationsDir =
|
||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||
@@ -53,7 +53,7 @@ public class ChatStorageService
|
||||
// 원자적 교체: 기존 파일이 있으면 덮어쓰기
|
||||
if (File.Exists(path)) File.Delete(path);
|
||||
File.Move(tempPath, path);
|
||||
UpdateMetaCache(conversation);
|
||||
UpdateMetaCacheUnsafe(conversation);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -124,6 +124,20 @@ public class ChatStorageService
|
||||
|
||||
/// <summary>메타 캐시에서 특정 항목만 업데이트합니다 (전체 재로드 없이).</summary>
|
||||
public void UpdateMetaCache(ChatConversation conv)
|
||||
{
|
||||
Lock.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
UpdateMetaCacheUnsafe(conv);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Lock.ExitWriteLock();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>WriteLock이 이미 확보된 상태에서 호출. 내부 전용.</summary>
|
||||
private void UpdateMetaCacheUnsafe(ChatConversation conv)
|
||||
{
|
||||
if (_metaCache == null) return;
|
||||
var existing = _metaCache.FindIndex(c => c.Id == conv.Id);
|
||||
@@ -146,6 +160,20 @@ public class ChatStorageService
|
||||
|
||||
/// <summary>메타 캐시에서 항목을 제거합니다.</summary>
|
||||
public void RemoveFromMetaCache(string id)
|
||||
{
|
||||
Lock.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
RemoveFromMetaCacheUnsafe(id);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Lock.ExitWriteLock();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>WriteLock이 이미 확보된 상태에서 호출. 내부 전용.</summary>
|
||||
private void RemoveFromMetaCacheUnsafe(string id)
|
||||
{
|
||||
_metaCache?.RemoveAll(c => c.Id == id);
|
||||
InvalidateMetaOrderCache();
|
||||
@@ -220,7 +248,7 @@ public class ChatStorageService
|
||||
try
|
||||
{
|
||||
if (File.Exists(path)) File.Delete(path);
|
||||
RemoveFromMetaCache(id);
|
||||
RemoveFromMetaCacheUnsafe(id);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -293,7 +321,7 @@ public class ChatStorageService
|
||||
if (conv != null && !conv.Pinned && conv.UpdatedAt < cutoff)
|
||||
{
|
||||
File.Delete(file);
|
||||
RemoveFromMetaCache(conv.Id);
|
||||
RemoveFromMetaCacheUnsafe(conv.Id);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,9 +56,9 @@ internal sealed class Cp4dTokenService
|
||||
Content = JsonContent.Create(body)
|
||||
};
|
||||
|
||||
using var resp = await _http.SendAsync(req, ct);
|
||||
using var resp = await _http.SendAsync(req, ct).ConfigureAwait(false);
|
||||
statusCode = resp.StatusCode;
|
||||
var rawBody = await resp.Content.ReadAsStringAsync(ct);
|
||||
var rawBody = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
|
||||
if (!resp.IsSuccessStatusCode)
|
||||
{
|
||||
lastErrorBody = rawBody;
|
||||
@@ -71,9 +71,9 @@ internal sealed class Cp4dTokenService
|
||||
return true;
|
||||
}
|
||||
|
||||
var authorized = await TryAuthorizeAsync(new { username, password }, "username+password");
|
||||
var authorized = await TryAuthorizeAsync(new { username, password }, "username+password").ConfigureAwait(false);
|
||||
if (!authorized && !string.IsNullOrWhiteSpace(password))
|
||||
authorized = await TryAuthorizeAsync(new { username, api_key = password }, "username+api_key");
|
||||
authorized = await TryAuthorizeAsync(new { username, api_key = password }, "username+api_key").ConfigureAwait(false);
|
||||
|
||||
if (!authorized || string.IsNullOrWhiteSpace(json))
|
||||
{
|
||||
|
||||
@@ -11,7 +11,7 @@ public sealed class DraftQueueProcessorService
|
||||
public bool CanStartNext(ChatSessionStateService? session, string tab)
|
||||
=> session?.GetNextQueuedDraft(tab) != null;
|
||||
|
||||
public DraftQueueItem? TryStartNext(ChatSessionStateService? session, string tab, ChatStorageService? storage = null, string? preferredDraftId = null, TaskRunService? taskRuns = null)
|
||||
public DraftQueueItem? TryStartNext(ChatSessionStateService? session, string tab, IChatStorageService? storage = null, string? preferredDraftId = null, TaskRunService? taskRuns = null)
|
||||
{
|
||||
if (session == null)
|
||||
return null;
|
||||
@@ -46,7 +46,7 @@ public sealed class DraftQueueProcessorService
|
||||
?? next;
|
||||
}
|
||||
|
||||
public bool Complete(ChatSessionStateService? session, string tab, string draftId, ChatStorageService? storage = null, TaskRunService? taskRuns = null)
|
||||
public bool Complete(ChatSessionStateService? session, string tab, string draftId, IChatStorageService? storage = null, TaskRunService? taskRuns = null)
|
||||
{
|
||||
var completed = session?.MarkDraftCompleted(tab, draftId, storage) ?? false;
|
||||
if (completed)
|
||||
@@ -54,7 +54,7 @@ public sealed class DraftQueueProcessorService
|
||||
return completed;
|
||||
}
|
||||
|
||||
public bool HandleFailure(ChatSessionStateService? session, string tab, string draftId, string? error, bool cancelled = false, int maxAutoRetries = 3, ChatStorageService? storage = null, TaskRunService? taskRuns = null)
|
||||
public bool HandleFailure(ChatSessionStateService? session, string tab, string draftId, string? error, bool cancelled = false, int maxAutoRetries = 3, IChatStorageService? storage = null, TaskRunService? taskRuns = null)
|
||||
{
|
||||
if (session == null)
|
||||
return false;
|
||||
@@ -83,7 +83,7 @@ public sealed class DraftQueueProcessorService
|
||||
return handled;
|
||||
}
|
||||
|
||||
public int PromoteReadyBlockedItems(ChatSessionStateService? session, string tab, ChatStorageService? storage = null)
|
||||
public int PromoteReadyBlockedItems(ChatSessionStateService? session, string tab, IChatStorageService? storage = null)
|
||||
{
|
||||
if (session == null)
|
||||
return 0;
|
||||
@@ -102,18 +102,18 @@ public sealed class DraftQueueProcessorService
|
||||
return promoted;
|
||||
}
|
||||
|
||||
public int ClearCompleted(ChatSessionStateService? session, string tab, ChatStorageService? storage = null)
|
||||
public int ClearCompleted(ChatSessionStateService? session, string tab, IChatStorageService? storage = null)
|
||||
=> ClearByState(session, tab, "completed", storage);
|
||||
|
||||
public int ClearFailed(ChatSessionStateService? session, string tab, ChatStorageService? storage = null)
|
||||
public int ClearFailed(ChatSessionStateService? session, string tab, IChatStorageService? storage = null)
|
||||
=> ClearByState(session, tab, "failed", storage);
|
||||
|
||||
/// <summary>대기 중인 항목을 모두 제거합니다 (중지 시 사용).</summary>
|
||||
public int ClearQueued(ChatSessionStateService? session, string tab, ChatStorageService? storage = null)
|
||||
public int ClearQueued(ChatSessionStateService? session, string tab, IChatStorageService? storage = null)
|
||||
=> ClearByState(session, tab, "queued", storage);
|
||||
|
||||
/// <summary>실행 중인 항목을 실패로 전환합니다 (중지 시 사용).</summary>
|
||||
public int CancelRunning(ChatSessionStateService? session, string tab, ChatStorageService? storage = null)
|
||||
public int CancelRunning(ChatSessionStateService? session, string tab, IChatStorageService? storage = null)
|
||||
{
|
||||
if (session == null) return 0;
|
||||
int count = 0;
|
||||
@@ -127,7 +127,7 @@ public sealed class DraftQueueProcessorService
|
||||
return count;
|
||||
}
|
||||
|
||||
private static int ClearByState(ChatSessionStateService? session, string tab, string state, ChatStorageService? storage)
|
||||
private static int ClearByState(ChatSessionStateService? session, string tab, string state, IChatStorageService? storage)
|
||||
{
|
||||
if (session == null)
|
||||
return 0;
|
||||
|
||||
@@ -42,15 +42,15 @@ internal sealed class IbmIamTokenService
|
||||
"application/x-www-form-urlencoded");
|
||||
req.Headers.Accept.ParseAdd("application/json");
|
||||
|
||||
using var resp = await _http.SendAsync(req, ct);
|
||||
using var resp = await _http.SendAsync(req, ct).ConfigureAwait(false);
|
||||
if (!resp.IsSuccessStatusCode)
|
||||
{
|
||||
var errBody = await resp.Content.ReadAsStringAsync(ct);
|
||||
var errBody = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
|
||||
LogService.Warn($"IBM IAM 토큰 발급 실패: {resp.StatusCode} - {errBody}");
|
||||
return null;
|
||||
}
|
||||
|
||||
var json = await resp.Content.ReadAsStringAsync(ct);
|
||||
var json = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
if (!doc.RootElement.TryGetProperty("access_token", out var tokenProp))
|
||||
{
|
||||
|
||||
@@ -124,7 +124,7 @@ public class IndexService : IDisposable
|
||||
/// </summary>
|
||||
public async Task BuildAsync(CancellationToken ct = default)
|
||||
{
|
||||
await _rebuildLock.WaitAsync(ct);
|
||||
await _rebuildLock.WaitAsync(ct).ConfigureAwait(false);
|
||||
var sw = Stopwatch.StartNew();
|
||||
try
|
||||
{
|
||||
@@ -146,7 +146,7 @@ public class IndexService : IDisposable
|
||||
$"스캔 중: {Path.GetFileName(dir)}",
|
||||
EstimateRemainingText(progressBase, sw.Elapsed)));
|
||||
|
||||
await ScanDirectoryAsync(dir, fileSystemEntries, allowedExts, indexSpeed, ct);
|
||||
await ScanDirectoryAsync(dir, fileSystemEntries, allowedExts, indexSpeed, ct).ConfigureAwait(false);
|
||||
|
||||
var completedProgress = paths.Count == 0 ? 1 : (double)(index + 1) / paths.Count;
|
||||
ReportIndexProgress(LauncherIndexProgressInfo.Running(
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
using AxCopilot.Models;
|
||||
using AxCopilot.Services.Agent;
|
||||
|
||||
namespace AxCopilot.Services;
|
||||
|
||||
/// <summary>앱 전역 상태 요약 저장소 인터페이스.</summary>
|
||||
public interface IAppStateService
|
||||
{
|
||||
ChatSessionStateService? ChatSession { get; }
|
||||
|
||||
void AttachChatSession(ChatSessionStateService session);
|
||||
void LoadFromSettings(ISettingsService settings);
|
||||
|
||||
AppStateService.SkillCatalogState Skills { get; }
|
||||
AppStateService.McpCatalogState Mcp { get; }
|
||||
AppStateService.PermissionPolicyState Permissions { get; }
|
||||
AppStateService.AgentCatalogState AgentCatalog { get; }
|
||||
AppStateService.AgentRunState AgentRun { get; }
|
||||
|
||||
void UpsertTask(string id, string kind, string title, string summary, string status = "running", string? filePath = null);
|
||||
void CompleteTask(string id, string? summary = null, string status = "completed");
|
||||
void ApplyAgentEvent(AgentEvent evt);
|
||||
AppStateService.BackgroundJobSummaryState GetBackgroundJobSummary();
|
||||
AppStateService.PermissionSummaryState GetPermissionSummary(ChatConversation? conversation = null);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using AxCopilot.Models;
|
||||
|
||||
namespace AxCopilot.Services;
|
||||
|
||||
/// <summary>대화 내역 저장/로드/삭제 인터페이스.</summary>
|
||||
public interface IChatStorageService
|
||||
{
|
||||
void Save(ChatConversation conversation);
|
||||
ChatConversation? Load(string id);
|
||||
List<ChatConversation> LoadAllMeta();
|
||||
void InvalidateMetaCache();
|
||||
void UpdateMetaCache(ChatConversation conv);
|
||||
void RemoveFromMetaCache(string id);
|
||||
void Delete(string id);
|
||||
int DeleteAll();
|
||||
int DeleteAllByTab(string tab);
|
||||
int PurgeExpired(int retentionDays);
|
||||
int PurgeForDiskSpace(double threshold = 0.98);
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
using System.Text.Json;
|
||||
using AxCopilot.Models;
|
||||
using AxCopilot.Services.Agent;
|
||||
|
||||
namespace AxCopilot.Services;
|
||||
|
||||
/// <summary>런타임 연결 정보 스냅샷.</summary>
|
||||
public record RuntimeConnectionSnapshot(
|
||||
string Service,
|
||||
string Model,
|
||||
string Endpoint,
|
||||
bool AllowInsecureTls,
|
||||
bool HasApiKey);
|
||||
|
||||
/// <summary>LLM API 호출 인터페이스. 스트리밍/비스트리밍 모두 지원.</summary>
|
||||
public interface ILlmService : IDisposable
|
||||
{
|
||||
/// <summary>현재 서비스/모델 정보 조회.</summary>
|
||||
(string service, string model) GetCurrentModelInfo();
|
||||
|
||||
/// <summary>비스트리밍 전체 응답 요청.</summary>
|
||||
Task<string> SendAsync(List<ChatMessage> messages, CancellationToken ct = default);
|
||||
|
||||
/// <summary>스트리밍 응답 요청 (SSE).</summary>
|
||||
IAsyncEnumerable<string> StreamAsync(
|
||||
List<ChatMessage> messages,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>연결 테스트.</summary>
|
||||
Task<(bool ok, string message)> TestConnectionAsync();
|
||||
|
||||
/// <summary>자동 라우팅용 서비스/모델 오버라이드 설정.</summary>
|
||||
void PushRouteOverride(string service, string model);
|
||||
|
||||
/// <summary>서비스/모델 오버라이드 해제.</summary>
|
||||
void ClearRouteOverride();
|
||||
|
||||
/// <summary>모델/추론 파라미터 오버라이드 Push.</summary>
|
||||
void PushInferenceOverride(
|
||||
string? service = null,
|
||||
string? model = null,
|
||||
double? temperature = null,
|
||||
string? reasoningEffort = null);
|
||||
|
||||
/// <summary>가장 최근 Push 상태 복원.</summary>
|
||||
void PopInferenceOverride();
|
||||
|
||||
/// <summary>가장 최근 요청의 토큰 사용량.</summary>
|
||||
TokenUsage? LastTokenUsage { get; }
|
||||
|
||||
/// <summary>런타임 연결 정보 스냅샷 조회.</summary>
|
||||
RuntimeConnectionSnapshot GetRuntimeConnectionSnapshot();
|
||||
|
||||
/// <summary>도구 정의를 포함하여 LLM에 요청하고, 텍스트 + tool_use 블록을 파싱하여 반환합니다.</summary>
|
||||
Task<List<ContentBlock>> SendWithToolsAsync(
|
||||
List<ChatMessage> messages,
|
||||
IReadOnlyCollection<IAgentTool> tools,
|
||||
CancellationToken ct = default,
|
||||
bool forceToolCall = false,
|
||||
Func<ContentBlock, Task<ToolPrefetchResult?>>? prefetchToolCallAsync = null);
|
||||
|
||||
/// <summary>도구 정의를 포함하여 스트리밍 요청.</summary>
|
||||
IAsyncEnumerable<ToolStreamEvent> StreamWithToolsAsync(
|
||||
List<ChatMessage> messages,
|
||||
IReadOnlyCollection<IAgentTool> tools,
|
||||
bool forceToolCall = false,
|
||||
Func<ContentBlock, Task<ToolPrefetchResult?>>? prefetchToolCallAsync = null,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>현재 활성 실행 정책 조회.</summary>
|
||||
ModelExecutionProfileCatalog.ExecutionPolicy GetActiveExecutionPolicy();
|
||||
|
||||
/// <summary>현재 시스템 프롬프트 (컨텍스트 토큰 추정에 사용).</summary>
|
||||
string? SystemPrompt { get; }
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace AxCopilot.Services;
|
||||
|
||||
/// <summary>사용자 메시지의 인텐트를 분석하여 최적 모델을 선택하는 라우터 인터페이스.</summary>
|
||||
public interface IModelRouterService
|
||||
{
|
||||
/// <summary>사용자 메시지를 분석하여 최적 모델을 선택합니다.</summary>
|
||||
/// <returns>라우팅 결과. null이면 기본 모델 유지.</returns>
|
||||
ModelRouteResult? Route(string userMessage);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using AxCopilot.Models;
|
||||
|
||||
namespace AxCopilot.Services;
|
||||
|
||||
/// <summary>애플리케이션 설정 읽기/쓰기 인터페이스.</summary>
|
||||
public interface ISettingsService
|
||||
{
|
||||
AppSettings Settings { get; }
|
||||
string? MigrationSummary { get; }
|
||||
event EventHandler? SettingsChanged;
|
||||
|
||||
void Load();
|
||||
void Save();
|
||||
Task SaveAsync();
|
||||
}
|
||||
@@ -8,13 +8,6 @@ using AxCopilot.Services.Agent;
|
||||
|
||||
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>
|
||||
public class ContentBlock
|
||||
{
|
||||
@@ -36,6 +29,7 @@ public partial class LlmService
|
||||
{
|
||||
TextDelta,
|
||||
ToolCallReady,
|
||||
RetryReset,
|
||||
Completed
|
||||
}
|
||||
|
||||
@@ -44,6 +38,13 @@ public partial class LlmService
|
||||
string Text = "",
|
||||
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>
|
||||
/// <param name="forceToolCall">
|
||||
/// true이면 <c>tool_choice: "required"</c>를 요청에 추가하여 모델이 반드시 도구를 호출하도록 강제합니다.
|
||||
@@ -61,9 +62,9 @@ public partial class LlmService
|
||||
EnsureOperationModeAllowsLlmService(activeService);
|
||||
return NormalizeServiceName(activeService) switch
|
||||
{
|
||||
"sigmoid" => await SendSigmoidWithToolsAsync(messages, tools, ct),
|
||||
"gemini" => await SendGeminiWithToolsAsync(messages, tools, ct),
|
||||
"ollama" or "vllm" => await SendOpenAiWithToolsAsync(messages, tools, ct, forceToolCall, prefetchToolCallAsync),
|
||||
"sigmoid" => await SendSigmoidWithToolsAsync(messages, tools, ct).ConfigureAwait(false),
|
||||
"gemini" => await SendGeminiWithToolsAsync(messages, tools, ct).ConfigureAwait(false),
|
||||
"ollama" or "vllm" => await SendOpenAiWithToolsAsync(messages, tools, ct, forceToolCall, prefetchToolCallAsync).ConfigureAwait(false),
|
||||
_ => throw new NotSupportedException($"서비스 '{activeService}'는 아직 Function Calling을 지원하지 않습니다.")
|
||||
};
|
||||
}
|
||||
@@ -150,14 +151,14 @@ public partial class LlmService
|
||||
req.Headers.Add("x-api-key", apiKey);
|
||||
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)
|
||||
{
|
||||
var errBody = await resp.Content.ReadAsStringAsync(ct);
|
||||
var errBody = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
|
||||
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);
|
||||
var root = doc.RootElement;
|
||||
|
||||
@@ -544,10 +545,14 @@ public partial class LlmService
|
||||
var detail = ExtractErrorDetail(errBody);
|
||||
LogService.Warn($"[ToolUse] {activeService} API 오류 ({resp.StatusCode}): {errBody}");
|
||||
|
||||
if (isIbmDeployment && forceToolCall && (int)resp.StatusCode == 400)
|
||||
if (forceToolCall && (int)resp.StatusCode == 400)
|
||||
{
|
||||
LogService.Warn("[ToolUse] IBM 배포형 경로에서 tool_choice가 거부되어 대체 강제 전략으로 재시도합니다.");
|
||||
var fallbackBody = BuildIbmToolBody(messages, tools, forceToolCall: true, useToolChoice: false);
|
||||
LogService.Warn(isIbmDeployment
|
||||
? "[ToolUse] IBM 배포형 경로에서 tool_choice가 거부되어 대체 강제 전략으로 재시도합니다."
|
||||
: "[ToolUse] OpenAI 호환 경로에서 tool_choice가 거부되어 텍스트 지시 기반 강제 전략으로 재시도합니다.");
|
||||
var fallbackBody = isIbmDeployment
|
||||
? BuildIbmToolBody(messages, tools, forceToolCall: true, useToolChoice: false)
|
||||
: BuildOpenAiToolBody(messages, tools, forceToolCall: true, useToolChoice: false);
|
||||
var fallbackJson = JsonSerializer.Serialize(fallbackBody);
|
||||
WorkflowLogService.LogLlmRawRequestFromContext(url, fallbackJson);
|
||||
using var retryReq = new HttpRequestMessage(HttpMethod.Post, url)
|
||||
@@ -557,7 +562,7 @@ public partial class LlmService
|
||||
await ApplyAuthHeaderAsync(retryReq, ct);
|
||||
using var retryResp = await SendWithTlsAsync(retryReq, allowInsecureTls, ct, HttpCompletionOption.ResponseHeadersRead);
|
||||
if (retryResp.IsSuccessStatusCode)
|
||||
return await ReadOpenAiToolBlocksFromStreamAsync(retryResp, true, prefetchToolCallAsync, ct);
|
||||
return await ReadOpenAiToolBlocksFromStreamAsync(retryResp, isIbmDeployment, prefetchToolCallAsync, ct);
|
||||
}
|
||||
|
||||
// 400 BadRequest → 도구 없이 일반 응답으로 폴백 시도
|
||||
@@ -591,7 +596,7 @@ public partial class LlmService
|
||||
@"\{\s*""name""\s*:\s*""(\w+)""\s*,\s*""arguments""\s*:\s*(\{[\s\S]*?\})\s*\}",
|
||||
System.Text.RegularExpressions.RegexOptions.Compiled);
|
||||
|
||||
private static List<ContentBlock> TryExtractToolCallsFromText(string text)
|
||||
internal static List<ContentBlock> TryExtractToolCallsFromText(string text)
|
||||
{
|
||||
var results = new List<ContentBlock>();
|
||||
if (string.IsNullOrWhiteSpace(text)) return results;
|
||||
@@ -685,13 +690,65 @@ public partial class LlmService
|
||||
}
|
||||
}
|
||||
|
||||
private object BuildOpenAiToolBody(List<ChatMessage> messages, IReadOnlyCollection<IAgentTool> tools, bool forceToolCall = false)
|
||||
private sealed record OpenAiToolCompatibilityProfile(
|
||||
int StructuredHistoryRecentWindow,
|
||||
bool AllowParallelToolCalls,
|
||||
bool IncludeReasoningEffort,
|
||||
bool AddToolOnlyDirectiveOnFallback);
|
||||
|
||||
private OpenAiToolCompatibilityProfile GetOpenAiToolCompatibilityProfile(string service, string model)
|
||||
{
|
||||
var normalizedService = NormalizeServiceName(service);
|
||||
var normalizedModel = (model ?? "").Trim().ToLowerInvariant();
|
||||
var isFragileVllmFamily =
|
||||
normalizedService == "vllm" &&
|
||||
(normalizedModel.Contains("qwen", StringComparison.OrdinalIgnoreCase)
|
||||
|| normalizedModel.Contains("llama", StringComparison.OrdinalIgnoreCase)
|
||||
|| normalizedModel.Contains("deepseek", StringComparison.OrdinalIgnoreCase)
|
||||
|| normalizedModel.Contains("mistral", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (isFragileVllmFamily)
|
||||
{
|
||||
return new OpenAiToolCompatibilityProfile(
|
||||
StructuredHistoryRecentWindow: 4,
|
||||
AllowParallelToolCalls: false,
|
||||
IncludeReasoningEffort: false,
|
||||
AddToolOnlyDirectiveOnFallback: true);
|
||||
}
|
||||
|
||||
return new OpenAiToolCompatibilityProfile(
|
||||
StructuredHistoryRecentWindow: 8,
|
||||
AllowParallelToolCalls: true,
|
||||
IncludeReasoningEffort: true,
|
||||
AddToolOnlyDirectiveOnFallback: true);
|
||||
}
|
||||
|
||||
private static string BuildOpenAiToolOnlyDirective(IReadOnlyCollection<IAgentTool> tools)
|
||||
{
|
||||
var toolNames = string.Join(", ", tools.Select(t => t.Name).Take(12));
|
||||
return "[TOOL_ONLY] 텍스트로 설명하지 말고 지금 바로 도구를 호출하세요. " +
|
||||
$"사용 가능한 도구: {toolNames}. " +
|
||||
"plain text 대신 function/tool call을 사용하세요.";
|
||||
}
|
||||
|
||||
private object BuildOpenAiToolBody(
|
||||
List<ChatMessage> messages,
|
||||
IReadOnlyCollection<IAgentTool> tools,
|
||||
bool forceToolCall = false,
|
||||
bool useToolChoice = true)
|
||||
{
|
||||
var llm = _settings.Settings.Llm;
|
||||
var activeService = ResolveService();
|
||||
var activeModel = ResolveModel();
|
||||
var compatibilityProfile = GetOpenAiToolCompatibilityProfile(activeService, activeModel);
|
||||
var msgs = new List<object>();
|
||||
var structuredHistoryStart = GetStructuredToolHistoryStartIndex(messages, compatibilityProfile.StructuredHistoryRecentWindow);
|
||||
|
||||
foreach (var m in messages)
|
||||
for (var messageIndex = 0; messageIndex < messages.Count; messageIndex++)
|
||||
{
|
||||
var m = messages[messageIndex];
|
||||
var keepStructuredHistory = messageIndex >= structuredHistoryStart;
|
||||
|
||||
// tool_result 메시지 → OpenAI tool 응답 형식
|
||||
if (m.Role == "user" && m.Content.StartsWith("{\"type\":\"tool_result\""))
|
||||
{
|
||||
@@ -699,6 +756,16 @@ public partial class LlmService
|
||||
{
|
||||
using var doc = JsonDocument.Parse(m.Content);
|
||||
var root = doc.RootElement;
|
||||
if (!keepStructuredHistory)
|
||||
{
|
||||
msgs.Add(new
|
||||
{
|
||||
role = "user",
|
||||
content = BuildOpenAiToolResultTranscript(root),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
msgs.Add(new
|
||||
{
|
||||
role = "tool",
|
||||
@@ -717,6 +784,16 @@ public partial class LlmService
|
||||
{
|
||||
using var doc = JsonDocument.Parse(m.Content);
|
||||
var blocksArr = doc.RootElement.GetProperty("_tool_use_blocks");
|
||||
if (!keepStructuredHistory)
|
||||
{
|
||||
msgs.Add(new
|
||||
{
|
||||
role = "assistant",
|
||||
content = BuildOpenAiAssistantTranscript(blocksArr),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
var textContent = "";
|
||||
var toolCallsList = new List<object>();
|
||||
foreach (var b in blocksArr.EnumerateArray())
|
||||
@@ -765,6 +842,21 @@ public partial class LlmService
|
||||
}
|
||||
}
|
||||
|
||||
if (forceToolCall && !useToolChoice && compatibilityProfile.AddToolOnlyDirectiveOnFallback)
|
||||
{
|
||||
msgs.Add(new
|
||||
{
|
||||
role = "user",
|
||||
content = BuildOpenAiToolOnlyDirective(tools),
|
||||
});
|
||||
}
|
||||
|
||||
// ── tool_calls ↔ tool 메시지 쌍 검증 ──
|
||||
// 컨텍스트 압축 후 tool_calls assistant 메시지는 남아있는데
|
||||
// 대응하는 tool result 메시지가 누락되면 vLLM이 400 에러를 반환함.
|
||||
// 깨진 tool_calls 메시지를 일반 assistant 텍스트로 평탄화하여 방지.
|
||||
SanitizeToolCallPairs(msgs);
|
||||
|
||||
// OpenAI 도구 정의
|
||||
var toolDefs = tools.Select(t =>
|
||||
{
|
||||
@@ -791,20 +883,24 @@ public partial class LlmService
|
||||
};
|
||||
}).ToArray();
|
||||
|
||||
var activeService = ResolveService();
|
||||
var activeModel = ResolveModel();
|
||||
var executionPolicy = GetActiveExecutionPolicy();
|
||||
var isOllama = activeService.Equals("ollama", StringComparison.OrdinalIgnoreCase);
|
||||
if (isOllama)
|
||||
{
|
||||
return new
|
||||
// Ollama /api/chat 전용 바디 — stream:false로 비스트리밍 응답
|
||||
// Ollama 0.5.x+ 에서 tool_choice 파라미터 지원 (미지원 버전은 무시됨)
|
||||
var ollamaBody = new Dictionary<string, object?>
|
||||
{
|
||||
model = activeModel,
|
||||
messages = msgs,
|
||||
tools = toolDefs,
|
||||
stream = false,
|
||||
options = new { temperature = ResolveToolTemperature() }
|
||||
["model"] = activeModel,
|
||||
["messages"] = msgs,
|
||||
["tools"] = toolDefs,
|
||||
["stream"] = false,
|
||||
["options"] = new { temperature = ResolveToolTemperature() }
|
||||
};
|
||||
// Ollama에도 tool_choice 전달 — 프로파일 ForceInitialToolCall 적용
|
||||
if (forceToolCall && useToolChoice)
|
||||
ollamaBody["tool_choice"] = "required";
|
||||
return ollamaBody;
|
||||
}
|
||||
|
||||
var body = new Dictionary<string, object?>
|
||||
@@ -815,20 +911,39 @@ public partial class LlmService
|
||||
["stream"] = true,
|
||||
["temperature"] = ResolveToolTemperature(),
|
||||
["max_tokens"] = ResolveOpenAiCompatibleMaxTokens(),
|
||||
["parallel_tool_calls"] = executionPolicy.EnableParallelReadBatch,
|
||||
["parallel_tool_calls"] = executionPolicy.EnableParallelReadBatch && compatibilityProfile.AllowParallelToolCalls,
|
||||
};
|
||||
// 스트리밍 시 마지막 청크에 토큰 사용량 포함 요청 (vLLM/OpenAI 호환)
|
||||
body["stream_options"] = new { include_usage = true };
|
||||
// tool_choice: "required" — 모델이 반드시 도구를 호출하도록 강제
|
||||
// 아직 한 번도 도구를 호출하지 않은 첫 번째 요청에서만 사용 (chatty 모델 대응)
|
||||
if (forceToolCall)
|
||||
if (forceToolCall && useToolChoice)
|
||||
body["tool_choice"] = "required";
|
||||
var effort = ResolveReasoningEffort();
|
||||
if (!string.IsNullOrWhiteSpace(effort))
|
||||
if (compatibilityProfile.IncludeReasoningEffort && !string.IsNullOrWhiteSpace(effort))
|
||||
body["reasoning_effort"] = effort;
|
||||
return body;
|
||||
}
|
||||
|
||||
private static int GetStructuredToolHistoryStartIndex(IReadOnlyList<ChatMessage> messages, int protectedRecentNonSystemMessages)
|
||||
{
|
||||
var nonSystemMessages = messages
|
||||
.Select((message, index) => new { message, index })
|
||||
.Where(x => !string.Equals(x.message.Role, "system", StringComparison.OrdinalIgnoreCase))
|
||||
.ToList();
|
||||
|
||||
if (nonSystemMessages.Count <= protectedRecentNonSystemMessages)
|
||||
return 0;
|
||||
|
||||
var tentativeStart = Math.Max(0, nonSystemMessages.Count - protectedRecentNonSystemMessages);
|
||||
var adjustedStart = AgentMessageInvariantHelper.AdjustStartIndexForToolPairs(
|
||||
nonSystemMessages.Select(x => x.message).ToList(),
|
||||
tentativeStart,
|
||||
out _);
|
||||
|
||||
return nonSystemMessages[Math.Max(0, adjustedStart)].index;
|
||||
}
|
||||
|
||||
/// <summary>IBM 배포형 /text/chat 전용 도구 바디. parameters 래퍼 사용, model 필드 없음.</summary>
|
||||
private object BuildIbmToolBody(
|
||||
List<ChatMessage> messages,
|
||||
@@ -869,59 +984,28 @@ public partial class LlmService
|
||||
{
|
||||
if (m.Role == "system") continue;
|
||||
|
||||
// tool_result → OpenAI role:"tool" 형식 (watsonx /text/chat 지원)
|
||||
// IBM/Qwen 배포형은 과거 tool_calls/tool 메시지 이력을 엄격하게 검사하는 경우가 있어
|
||||
// 이전 tool-use 대화는 평탄한 transcript로 재구성한다.
|
||||
if (m.Role == "user" && m.Content.StartsWith("{\"type\":\"tool_result\""))
|
||||
{
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(m.Content);
|
||||
var root = doc.RootElement;
|
||||
msgs.Add(new
|
||||
{
|
||||
role = "tool",
|
||||
tool_call_id = root.GetProperty("tool_use_id").SafeGetString(),
|
||||
content = root.GetProperty("content").SafeGetString(),
|
||||
});
|
||||
msgs.Add(new { role = "user", content = BuildIbmToolResultTranscript(root) });
|
||||
continue;
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
// _tool_use_blocks → OpenAI tool_calls 형식
|
||||
// _tool_use_blocks도 assistant + tool_calls로 다시 보내지 않고 plain assistant transcript로 평탄화
|
||||
if (m.Role == "assistant" && m.Content.StartsWith("{\"_tool_use_blocks\""))
|
||||
{
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(m.Content);
|
||||
var blocksArr = doc.RootElement.GetProperty("_tool_use_blocks");
|
||||
var textContent = "";
|
||||
var toolCallsList = new List<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,
|
||||
});
|
||||
msgs.Add(new { role = "assistant", content = BuildIbmAssistantTranscript(blocksArr) });
|
||||
continue;
|
||||
}
|
||||
catch { }
|
||||
@@ -998,6 +1082,105 @@ 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 BuildOpenAiAssistantTranscript(JsonElement blocksArr)
|
||||
{
|
||||
var textSegments = new List<string>();
|
||||
var toolNames = 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() ?? "" : "";
|
||||
if (!string.IsNullOrWhiteSpace(name))
|
||||
toolNames.Add(name.Trim());
|
||||
}
|
||||
|
||||
var parts = new List<string>();
|
||||
if (textSegments.Count > 0)
|
||||
parts.Add(string.Join("\n\n", textSegments));
|
||||
if (toolNames.Count > 0)
|
||||
parts.Add($"[이전 도구 호출: {string.Join(", ", toolNames.Distinct(StringComparer.OrdinalIgnoreCase))}]");
|
||||
|
||||
return parts.Count == 0 ? "[이전 도구 호출]" : string.Join("\n\n", parts);
|
||||
}
|
||||
|
||||
private static string BuildOpenAiToolResultTranscript(JsonElement root)
|
||||
{
|
||||
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)
|
||||
? "[이전 도구 결과]"
|
||||
: $"[이전 도구 결과: {toolName}]";
|
||||
return string.IsNullOrWhiteSpace(content)
|
||||
? $"{header}\n(no output)"
|
||||
: $"{header}\n{content}";
|
||||
}
|
||||
|
||||
private static string BuildIbmToolResultTranscript(JsonElement root)
|
||||
{
|
||||
var toolCallId = root.SafeTryGetProperty("tool_use_id", out var idEl) ? idEl.SafeGetString() ?? "" : "";
|
||||
var toolName = root.SafeTryGetProperty("tool_name", out var nameEl) ? nameEl.SafeGetString() ?? "" : "";
|
||||
var content = root.SafeTryGetProperty("content", out var contentEl) ? contentEl.SafeGetString() ?? "" : "";
|
||||
var header = string.IsNullOrWhiteSpace(toolName)
|
||||
? "[Tool Result]"
|
||||
: $"[Tool Result: {toolName}]";
|
||||
if (!string.IsNullOrWhiteSpace(toolCallId))
|
||||
header += $" (id={toolCallId})";
|
||||
|
||||
return string.IsNullOrWhiteSpace(content)
|
||||
? $"{header}\n(no output)"
|
||||
: $"{header}\n{content}";
|
||||
}
|
||||
|
||||
private sealed class ToolCallAccumulator
|
||||
{
|
||||
public int Index { get; init; }
|
||||
@@ -1104,9 +1287,14 @@ public partial class LlmService
|
||||
{
|
||||
var errBody = await resp.Content.ReadAsStringAsync(ct);
|
||||
var detail = ExtractErrorDetail(errBody);
|
||||
if (isIbmDeployment && forceToolCall && (int)resp.StatusCode == 400)
|
||||
if (forceToolCall && (int)resp.StatusCode == 400)
|
||||
{
|
||||
var fallbackBody = BuildIbmToolBody(messages, tools, forceToolCall: true, useToolChoice: false);
|
||||
LogService.Warn(isIbmDeployment
|
||||
? "[ToolUse] IBM 배포형 스트리밍 경로에서 tool_choice가 거부되어 대체 강제 전략으로 재시도합니다."
|
||||
: "[ToolUse] OpenAI 호환 스트리밍 경로에서 tool_choice가 거부되어 텍스트 지시 기반 강제 전략으로 재시도합니다.");
|
||||
var fallbackBody = isIbmDeployment
|
||||
? BuildIbmToolBody(messages, tools, forceToolCall: true, useToolChoice: false)
|
||||
: BuildOpenAiToolBody(messages, tools, forceToolCall: true, useToolChoice: false);
|
||||
var fallbackJson = JsonSerializer.Serialize(fallbackBody);
|
||||
WorkflowLogService.LogLlmRawRequestFromContext(url, fallbackJson);
|
||||
using var retryReq = new HttpRequestMessage(HttpMethod.Post, url)
|
||||
@@ -1230,11 +1418,22 @@ public partial class LlmService
|
||||
resultsEl.GetArrayLength() > 0)
|
||||
{
|
||||
var first = resultsEl[0];
|
||||
var generatedText = first.SafeTryGetProperty("generated_text", out var generatedTextEl)
|
||||
? generatedTextEl.SafeGetString()
|
||||
: first.SafeTryGetProperty("output_text", out var outputTextEl)
|
||||
? outputTextEl.SafeGetString()
|
||||
: null;
|
||||
string? generatedText = null;
|
||||
if (first.SafeTryGetProperty("generated_text", out var generatedTextEl))
|
||||
generatedText = TryExtractTextContent(generatedTextEl, out var extractedGeneratedText) ? extractedGeneratedText : generatedTextEl.SafeGetString();
|
||||
else if (first.SafeTryGetProperty("output_text", out var outputTextEl))
|
||||
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 (generatedText.StartsWith(lastIbmGeneratedText, StringComparison.Ordinal))
|
||||
@@ -1364,28 +1563,34 @@ public partial class LlmService
|
||||
message = nestedMessage;
|
||||
|
||||
var consumed = false;
|
||||
if (message.SafeTryGetProperty("content", out var contentEl) &&
|
||||
contentEl.ValueKind == JsonValueKind.String)
|
||||
if (message.SafeTryGetProperty("content", out var contentEl))
|
||||
{
|
||||
var parsedText = contentEl.SafeGetString();
|
||||
if (!string.IsNullOrWhiteSpace(parsedText))
|
||||
if (TryExtractTextContent(contentEl, out var parsedText))
|
||||
{
|
||||
text = parsedText;
|
||||
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 사용
|
||||
if (!consumed &&
|
||||
message.SafeTryGetProperty("reasoning_content", out var reasoningContentEl) &&
|
||||
reasoningContentEl.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
var reasoningText = reasoningContentEl.SafeGetString();
|
||||
if (!string.IsNullOrWhiteSpace(reasoningText))
|
||||
TryExtractTextContent(reasoningContentEl, out var reasoningText))
|
||||
{
|
||||
text = reasoningText;
|
||||
consumed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (message.SafeTryGetProperty("tool_calls", out var toolCallsEl) &&
|
||||
toolCallsEl.ValueKind == JsonValueKind.Array)
|
||||
@@ -1428,6 +1633,125 @@ public partial class LlmService
|
||||
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)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(json))
|
||||
@@ -1514,6 +1838,93 @@ public partial class LlmService
|
||||
|
||||
// ─── 공통 헬퍼 ─────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// tool_calls가 포함된 assistant 메시지 뒤에 대응하는 tool 메시지가 없으면
|
||||
/// 해당 assistant 메시지를 일반 텍스트로 평탄화합니다.
|
||||
/// 컨텍스트 압축 후 쌍이 깨지는 경우를 방어합니다.
|
||||
/// </summary>
|
||||
private static void SanitizeToolCallPairs(List<object> msgs)
|
||||
{
|
||||
// ── 1패스: tool_calls assistant 메시지의 쌍 검증 ──
|
||||
// tool_calls가 있는 assistant 인덱스를 기록하여 2패스에서 고아 tool 판별에 사용
|
||||
var pairedToolIndices = new HashSet<int>();
|
||||
|
||||
for (int i = 0; i < msgs.Count; i++)
|
||||
{
|
||||
var msgType = msgs[i].GetType();
|
||||
var toolCallsProp = msgType.GetProperty("tool_calls");
|
||||
var roleProp = msgType.GetProperty("role");
|
||||
if (toolCallsProp == null || roleProp == null) continue;
|
||||
|
||||
var role = roleProp.GetValue(msgs[i]) as string;
|
||||
if (role != "assistant") continue;
|
||||
|
||||
var toolCalls = toolCallsProp.GetValue(msgs[i]);
|
||||
if (toolCalls == null) continue;
|
||||
|
||||
int callCount = 0;
|
||||
if (toolCalls is System.Collections.ICollection col) callCount = col.Count;
|
||||
else if (toolCalls is System.Collections.IEnumerable en)
|
||||
{
|
||||
foreach (var _ in en) callCount++;
|
||||
}
|
||||
if (callCount == 0) continue;
|
||||
|
||||
// 바로 다음에 tool 역할 메시지가 callCount개 있는지 확인
|
||||
int foundTools = 0;
|
||||
for (int j = i + 1; j < msgs.Count && foundTools < callCount; j++)
|
||||
{
|
||||
var jType = msgs[j].GetType();
|
||||
var jRole = jType.GetProperty("role")?.GetValue(msgs[j]) as string;
|
||||
if (jRole == "tool")
|
||||
{
|
||||
foundTools++;
|
||||
pairedToolIndices.Add(j);
|
||||
}
|
||||
else
|
||||
break;
|
||||
}
|
||||
|
||||
if (foundTools < callCount)
|
||||
{
|
||||
// 쌍이 불완전 → assistant를 일반 텍스트로 교체
|
||||
var contentProp = msgType.GetProperty("content");
|
||||
var contentText = contentProp?.GetValue(msgs[i]) as string ?? "";
|
||||
if (string.IsNullOrWhiteSpace(contentText))
|
||||
contentText = "[이전 도구 호출 — 결과 누락으로 생략됨]";
|
||||
msgs[i] = new { role = "assistant", content = contentText };
|
||||
|
||||
// 이 assistant에 딸린 불완전 tool 메시지도 user로 변환
|
||||
for (int j = i + 1; j < msgs.Count; j++)
|
||||
{
|
||||
var jType = msgs[j].GetType();
|
||||
var jRole = jType.GetProperty("role")?.GetValue(msgs[j]) as string;
|
||||
if (jRole != "tool") break;
|
||||
var jContent = jType.GetProperty("content")?.GetValue(msgs[j]) as string ?? "";
|
||||
msgs[j] = new { role = "user", content = $"[이전 도구 결과]\n{jContent}" };
|
||||
pairedToolIndices.Remove(j);
|
||||
}
|
||||
|
||||
LogService.Info($"[ToolUse] tool_calls/tool 쌍 불일치 감지 — assistant 메시지를 평탄화 (index={i})");
|
||||
}
|
||||
}
|
||||
|
||||
// ── 2패스: 앞에 tool_calls가 없는 고아 tool 메시지를 user로 변환 ──
|
||||
for (int i = 0; i < msgs.Count; i++)
|
||||
{
|
||||
if (pairedToolIndices.Contains(i)) continue;
|
||||
|
||||
var msgType = msgs[i].GetType();
|
||||
var roleProp = msgType.GetProperty("role");
|
||||
var role = roleProp?.GetValue(msgs[i]) as string;
|
||||
if (role != "tool") continue;
|
||||
|
||||
var content = msgType.GetProperty("content")?.GetValue(msgs[i]) as string ?? "";
|
||||
msgs[i] = new { role = "user", content = $"[이전 도구 결과]\n{content}" };
|
||||
LogService.Info($"[ToolUse] 고아 tool 메시지 감지 — user로 변환 (index={i})");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>ToolProperty를 LLM API용 스키마 객체로 변환. array/enum/items 포함.</summary>
|
||||
private static object BuildPropertySchema(Agent.ToolProperty prop, bool upperCaseType)
|
||||
{
|
||||
|
||||
@@ -18,7 +18,7 @@ public record TokenUsage(int PromptTokens, int CompletionTokens)
|
||||
/// LLM API 호출 서비스. Ollama / vLLM / Gemini / Claude 백엔드를 지원합니다.
|
||||
/// 스트리밍(SSE) 및 비스트리밍 모두 지원합니다.
|
||||
/// </summary>
|
||||
public partial class LlmService : IDisposable
|
||||
public partial class LlmService : ILlmService
|
||||
{
|
||||
private readonly HttpClient _http;
|
||||
private readonly HttpClient _httpInsecure;
|
||||
@@ -148,7 +148,7 @@ public partial class LlmService : IDisposable
|
||||
internal string GetActiveExecutionProfileKey()
|
||||
=> Agent.ModelExecutionProfileCatalog.Normalize(GetActiveRegisteredModel()?.ExecutionProfile);
|
||||
|
||||
internal Agent.ModelExecutionProfileCatalog.ExecutionPolicy GetActiveExecutionPolicy()
|
||||
public Agent.ModelExecutionProfileCatalog.ExecutionPolicy GetActiveExecutionPolicy()
|
||||
=> Agent.ModelExecutionProfileCatalog.Get(GetActiveExecutionProfileKey());
|
||||
|
||||
internal double ResolveToolTemperature()
|
||||
@@ -220,13 +220,6 @@ public partial class LlmService : IDisposable
|
||||
/// <summary>가장 최근 요청의 토큰 사용량. 스트리밍/비스트리밍 완료 후 갱신됩니다.</summary>
|
||||
public TokenUsage? LastTokenUsage { get; private set; }
|
||||
|
||||
public record RuntimeConnectionSnapshot(
|
||||
string Service,
|
||||
string Model,
|
||||
string Endpoint,
|
||||
bool AllowInsecureTls,
|
||||
bool HasApiKey);
|
||||
|
||||
public LlmService(SettingsService settings)
|
||||
{
|
||||
_settings = settings;
|
||||
@@ -342,8 +335,8 @@ public partial class LlmService : IDisposable
|
||||
{
|
||||
return llm.RegisteredModels.FirstOrDefault(m =>
|
||||
m.Service.Equals(service, StringComparison.OrdinalIgnoreCase) &&
|
||||
(CryptoService.DecryptIfEnabled(m.EncryptedModelName, llm.EncryptionEnabled) == modelName ||
|
||||
m.Alias == modelName));
|
||||
(string.Equals(CryptoService.DecryptIfEnabled(m.EncryptedModelName, llm.EncryptionEnabled), modelName, StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(m.Alias, modelName, StringComparison.OrdinalIgnoreCase)));
|
||||
}
|
||||
|
||||
private Models.RegisteredModel? GetActiveRegisteredModel()
|
||||
@@ -572,7 +565,7 @@ public partial class LlmService : IDisposable
|
||||
EnsureOperationModeAllowsLlmService(activeService);
|
||||
try
|
||||
{
|
||||
return await SendWithServiceAsync(activeService, messages, ct);
|
||||
return await SendWithServiceAsync(activeService, messages, ct).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex) when (llm.FallbackModels.Count > 0)
|
||||
{
|
||||
@@ -587,7 +580,7 @@ public partial class LlmService : IDisposable
|
||||
EnsureOperationModeAllowsLlmService(fbService);
|
||||
LogService.Warn($"모델 폴백: {activeService} → {fbService} ({ex.Message})");
|
||||
LastFallbackInfo = $"{activeService} → {fbService}";
|
||||
return await SendWithServiceAsync(fbService, messages, ct);
|
||||
return await SendWithServiceAsync(fbService, messages, ct).ConfigureAwait(false);
|
||||
}
|
||||
catch { continue; }
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using AxCopilot.Services.Agent;
|
||||
|
||||
namespace AxCopilot.Services;
|
||||
|
||||
@@ -68,8 +69,15 @@ public class LspClientService : IDisposable
|
||||
textDocument = new
|
||||
{
|
||||
definition = new { dynamicRegistration = false },
|
||||
implementation = new { dynamicRegistration = false },
|
||||
references = new { dynamicRegistration = false },
|
||||
documentSymbol = new { dynamicRegistration = false },
|
||||
hover = new { dynamicRegistration = false },
|
||||
callHierarchy = new { dynamicRegistration = false },
|
||||
},
|
||||
workspace = new
|
||||
{
|
||||
symbol = new { dynamicRegistration = false },
|
||||
}
|
||||
}
|
||||
}, ct);
|
||||
@@ -126,6 +134,79 @@ public class LspClientService : IDisposable
|
||||
return ParseSymbols(result);
|
||||
}
|
||||
|
||||
/// <summary>심볼의 hover 정보를 가져옵니다.</summary>
|
||||
public async Task<string?> HoverAsync(string filePath, int line, int character, CancellationToken ct = default)
|
||||
{
|
||||
var result = await SendRequestAsync("textDocument/hover", new
|
||||
{
|
||||
textDocument = new { uri = FileToUri(filePath) },
|
||||
position = new { line, character }
|
||||
}, ct);
|
||||
|
||||
return ParseHover(result);
|
||||
}
|
||||
|
||||
/// <summary>인터페이스/추상 메서드 등의 구현 위치를 찾습니다.</summary>
|
||||
public async Task<List<LspLocation>> GotoImplementationAsync(string filePath, int line, int character, CancellationToken ct = default)
|
||||
{
|
||||
var result = await SendRequestAsync("textDocument/implementation", new
|
||||
{
|
||||
textDocument = new { uri = FileToUri(filePath) },
|
||||
position = new { line, character }
|
||||
}, ct);
|
||||
|
||||
return ParseLocations(result);
|
||||
}
|
||||
|
||||
/// <summary>워크스페이스 전체 심볼을 검색합니다.</summary>
|
||||
public async Task<List<LspWorkspaceSymbol>> SearchWorkspaceSymbolsAsync(string query, CancellationToken ct = default)
|
||||
{
|
||||
var result = await SendRequestAsync("workspace/symbol", new { query }, ct);
|
||||
return ParseWorkspaceSymbols(result);
|
||||
}
|
||||
|
||||
/// <summary>호출 계층의 기준 아이템을 준비합니다.</summary>
|
||||
public async Task<List<LspCallHierarchyItem>> PrepareCallHierarchyAsync(string filePath, int line, int character, CancellationToken ct = default)
|
||||
{
|
||||
var result = await SendRequestAsync("textDocument/prepareCallHierarchy", new
|
||||
{
|
||||
textDocument = new { uri = FileToUri(filePath) },
|
||||
position = new { line, character }
|
||||
}, ct);
|
||||
|
||||
return ParseCallHierarchyItems(result);
|
||||
}
|
||||
|
||||
/// <summary>해당 심볼을 호출하는 상위 호출자를 찾습니다.</summary>
|
||||
public async Task<List<LspCallHierarchyEntry>> GetIncomingCallsAsync(string filePath, int line, int character, CancellationToken ct = default)
|
||||
{
|
||||
var items = await PrepareCallHierarchyAsync(filePath, line, character, ct);
|
||||
if (items.Count == 0)
|
||||
return new List<LspCallHierarchyEntry>();
|
||||
|
||||
var result = await SendRequestAsync("callHierarchy/incomingCalls", new
|
||||
{
|
||||
item = items[0].RawItem
|
||||
}, ct);
|
||||
|
||||
return ParseIncomingCalls(result);
|
||||
}
|
||||
|
||||
/// <summary>해당 심볼이 호출하는 하위 호출 대상을 찾습니다.</summary>
|
||||
public async Task<List<LspCallHierarchyEntry>> GetOutgoingCallsAsync(string filePath, int line, int character, CancellationToken ct = default)
|
||||
{
|
||||
var items = await PrepareCallHierarchyAsync(filePath, line, character, ct);
|
||||
if (items.Count == 0)
|
||||
return new List<LspCallHierarchyEntry>();
|
||||
|
||||
var result = await SendRequestAsync("callHierarchy/outgoingCalls", new
|
||||
{
|
||||
item = items[0].RawItem
|
||||
}, ct);
|
||||
|
||||
return ParseOutgoingCalls(result);
|
||||
}
|
||||
|
||||
// ─── JSON-RPC 통신 ──────────────────────────────────────────────────────
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOpts = new()
|
||||
@@ -249,16 +330,7 @@ public class LspClientService : IDisposable
|
||||
var elem = result.Value;
|
||||
if (elem.ValueKind == JsonValueKind.Array && elem.GetArrayLength() > 0)
|
||||
elem = elem[0];
|
||||
if (elem.TryGetProperty("uri", out var uri) && elem.TryGetProperty("range", out var range))
|
||||
{
|
||||
var start = range.GetProperty("start");
|
||||
return new LspLocation
|
||||
{
|
||||
FilePath = UriToFile(uri.GetString() ?? ""),
|
||||
Line = start.GetProperty("line").GetInt32(),
|
||||
Character = start.GetProperty("character").GetInt32(),
|
||||
};
|
||||
}
|
||||
return ParseLocationElement(elem);
|
||||
}
|
||||
catch { }
|
||||
return null;
|
||||
@@ -267,21 +339,56 @@ public class LspClientService : IDisposable
|
||||
private static List<LspLocation> ParseLocations(JsonElement? result)
|
||||
{
|
||||
var list = new List<LspLocation>();
|
||||
if (result?.ValueKind != JsonValueKind.Array) return list;
|
||||
if (result == null)
|
||||
return list;
|
||||
|
||||
if (result.Value.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
foreach (var elem in result.Value.EnumerateArray())
|
||||
{
|
||||
var parsed = ParseLocationElement(elem);
|
||||
if (parsed != null)
|
||||
list.Add(parsed);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var parsed = ParseLocationElement(result.Value);
|
||||
if (parsed != null)
|
||||
list.Add(parsed);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
private static LspLocation? ParseLocationElement(JsonElement elem)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (elem.TryGetProperty("targetUri", out var targetUri) && elem.TryGetProperty("targetSelectionRange", out var targetSelectionRange))
|
||||
{
|
||||
var start = targetSelectionRange.GetProperty("start");
|
||||
return new LspLocation
|
||||
{
|
||||
FilePath = UriToFile(targetUri.SafeGetString() ?? ""),
|
||||
Line = start.GetProperty("line").GetInt32(),
|
||||
Character = start.GetProperty("character").GetInt32(),
|
||||
};
|
||||
}
|
||||
|
||||
if (elem.TryGetProperty("uri", out var uri) && elem.TryGetProperty("range", out var range))
|
||||
{
|
||||
var start = range.GetProperty("start");
|
||||
list.Add(new LspLocation
|
||||
return new LspLocation
|
||||
{
|
||||
FilePath = UriToFile(uri.GetString() ?? ""),
|
||||
FilePath = UriToFile(uri.SafeGetString() ?? ""),
|
||||
Line = start.GetProperty("line").GetInt32(),
|
||||
Character = start.GetProperty("character").GetInt32(),
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
return list;
|
||||
catch { }
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static List<LspSymbol> ParseSymbols(JsonElement? result)
|
||||
@@ -311,6 +418,193 @@ public class LspClientService : IDisposable
|
||||
return list;
|
||||
}
|
||||
|
||||
private static string? ParseHover(JsonElement? result)
|
||||
{
|
||||
if (result == null || result.Value.ValueKind != JsonValueKind.Object)
|
||||
return null;
|
||||
|
||||
if (!result.Value.TryGetProperty("contents", out var contents))
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
return contents.ValueKind switch
|
||||
{
|
||||
JsonValueKind.String => contents.SafeGetString(),
|
||||
JsonValueKind.Object => contents.TryGetProperty("value", out var value)
|
||||
? value.SafeGetString()
|
||||
: contents.GetRawText(),
|
||||
JsonValueKind.Array => string.Join(
|
||||
"\n\n",
|
||||
contents.EnumerateArray()
|
||||
.Select(item => item.ValueKind == JsonValueKind.Object && item.TryGetProperty("value", out var v)
|
||||
? v.SafeGetString()
|
||||
: item.SafeGetString())
|
||||
.Where(text => !string.IsNullOrWhiteSpace(text))),
|
||||
_ => contents.SafeGetString()
|
||||
};
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static List<LspWorkspaceSymbol> ParseWorkspaceSymbols(JsonElement? result)
|
||||
{
|
||||
var list = new List<LspWorkspaceSymbol>();
|
||||
if (result?.ValueKind != JsonValueKind.Array)
|
||||
return list;
|
||||
|
||||
foreach (var elem in result.Value.EnumerateArray())
|
||||
{
|
||||
try
|
||||
{
|
||||
var name = elem.TryGetProperty("name", out var nameEl) ? nameEl.SafeGetString() ?? "" : "";
|
||||
var kind = elem.TryGetProperty("kind", out var kindEl) ? SymbolKindName(kindEl.GetInt32()) : "symbol";
|
||||
var location = elem.TryGetProperty("location", out var locationEl)
|
||||
? ParseLocationElement(locationEl)
|
||||
: null;
|
||||
|
||||
list.Add(new LspWorkspaceSymbol
|
||||
{
|
||||
Name = name,
|
||||
Kind = kind,
|
||||
Location = location,
|
||||
});
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
private static List<LspCallHierarchyItem> ParseCallHierarchyItems(JsonElement? result)
|
||||
{
|
||||
var list = new List<LspCallHierarchyItem>();
|
||||
if (result == null)
|
||||
return list;
|
||||
|
||||
if (result.Value.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
foreach (var elem in result.Value.EnumerateArray())
|
||||
{
|
||||
var item = ParseCallHierarchyItem(elem);
|
||||
if (item != null)
|
||||
list.Add(item);
|
||||
}
|
||||
}
|
||||
else if (result.Value.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
var item = ParseCallHierarchyItem(result.Value);
|
||||
if (item != null)
|
||||
list.Add(item);
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
private static LspCallHierarchyItem? ParseCallHierarchyItem(JsonElement elem)
|
||||
{
|
||||
try
|
||||
{
|
||||
var uri = elem.TryGetProperty("uri", out var uriEl) ? uriEl.SafeGetString() ?? "" : "";
|
||||
var name = elem.TryGetProperty("name", out var nameEl) ? nameEl.SafeGetString() ?? "" : "";
|
||||
var kind = elem.TryGetProperty("kind", out var kindEl) ? SymbolKindName(kindEl.GetInt32()) : "symbol";
|
||||
var selectionRange = elem.TryGetProperty("selectionRange", out var selectionRangeEl)
|
||||
? selectionRangeEl
|
||||
: elem.GetProperty("range");
|
||||
var start = selectionRange.GetProperty("start");
|
||||
|
||||
return new LspCallHierarchyItem
|
||||
{
|
||||
Name = name,
|
||||
Kind = kind,
|
||||
Location = new LspLocation
|
||||
{
|
||||
FilePath = UriToFile(uri),
|
||||
Line = start.GetProperty("line").GetInt32(),
|
||||
Character = start.GetProperty("character").GetInt32(),
|
||||
},
|
||||
RawItem = elem.Clone(),
|
||||
};
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static List<LspCallHierarchyEntry> ParseIncomingCalls(JsonElement? result)
|
||||
{
|
||||
var list = new List<LspCallHierarchyEntry>();
|
||||
if (result?.ValueKind != JsonValueKind.Array)
|
||||
return list;
|
||||
|
||||
foreach (var elem in result.Value.EnumerateArray())
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!elem.TryGetProperty("from", out var fromEl))
|
||||
continue;
|
||||
|
||||
var item = ParseCallHierarchyItem(fromEl);
|
||||
if (item == null)
|
||||
continue;
|
||||
|
||||
var count = elem.TryGetProperty("fromRanges", out var ranges) && ranges.ValueKind == JsonValueKind.Array
|
||||
? ranges.GetArrayLength()
|
||||
: 0;
|
||||
|
||||
list.Add(new LspCallHierarchyEntry
|
||||
{
|
||||
Name = item.Name,
|
||||
Kind = item.Kind,
|
||||
Location = item.Location,
|
||||
RangeCount = count,
|
||||
});
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
private static List<LspCallHierarchyEntry> ParseOutgoingCalls(JsonElement? result)
|
||||
{
|
||||
var list = new List<LspCallHierarchyEntry>();
|
||||
if (result?.ValueKind != JsonValueKind.Array)
|
||||
return list;
|
||||
|
||||
foreach (var elem in result.Value.EnumerateArray())
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!elem.TryGetProperty("to", out var toEl))
|
||||
continue;
|
||||
|
||||
var item = ParseCallHierarchyItem(toEl);
|
||||
if (item == null)
|
||||
continue;
|
||||
|
||||
var count = elem.TryGetProperty("fromRanges", out var ranges) && ranges.ValueKind == JsonValueKind.Array
|
||||
? ranges.GetArrayLength()
|
||||
: 0;
|
||||
|
||||
list.Add(new LspCallHierarchyEntry
|
||||
{
|
||||
Name = item.Name,
|
||||
Kind = item.Kind,
|
||||
Location = item.Location,
|
||||
RangeCount = count,
|
||||
});
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
private static string SymbolKindName(int kind) => kind switch
|
||||
{
|
||||
1 => "file", 2 => "module", 3 => "namespace", 4 => "package",
|
||||
@@ -408,3 +702,34 @@ public class LspSymbol
|
||||
public int Line { get; init; }
|
||||
public override string ToString() => $"[{Kind}] {Name} (line {Line + 1})";
|
||||
}
|
||||
|
||||
/// <summary>워크스페이스 심볼 검색 결과.</summary>
|
||||
public class LspWorkspaceSymbol
|
||||
{
|
||||
public string Name { get; init; } = "";
|
||||
public string Kind { get; init; } = "";
|
||||
public LspLocation? Location { get; init; }
|
||||
public override string ToString() => Location == null
|
||||
? $"[{Kind}] {Name}"
|
||||
: $"[{Kind}] {Name} @ {Location}";
|
||||
}
|
||||
|
||||
/// <summary>호출 계층 기준 아이템.</summary>
|
||||
public class LspCallHierarchyItem
|
||||
{
|
||||
public string Name { get; init; } = "";
|
||||
public string Kind { get; init; } = "";
|
||||
public LspLocation Location { get; init; } = new();
|
||||
public JsonElement RawItem { get; init; }
|
||||
public override string ToString() => $"[{Kind}] {Name} @ {Location}";
|
||||
}
|
||||
|
||||
/// <summary>호출 계층의 incoming/outgoing 엔트리.</summary>
|
||||
public class LspCallHierarchyEntry
|
||||
{
|
||||
public string Name { get; init; } = "";
|
||||
public string Kind { get; init; } = "";
|
||||
public LspLocation Location { get; init; } = new();
|
||||
public int RangeCount { get; init; }
|
||||
public override string ToString() => $"[{Kind}] {Name} @ {Location} (matches: {RangeCount})";
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ public record ModelRouteResult(
|
||||
/// - 사내 Ollama/vLLM 모델 추가: GetDefaultCapabilities()에 항목 추가
|
||||
/// - 확신도 조정: Route() 메서드의 confidence 체크 로직
|
||||
/// </summary>
|
||||
public class ModelRouterService
|
||||
public class ModelRouterService : IModelRouterService
|
||||
{
|
||||
private readonly SettingsService _settings;
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ public static class PdfExportService
|
||||
|
||||
private static string GetPrintStyles() => @"
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font-family: 'Malgun Gothic', 'Segoe UI', sans-serif; font-size: 13px; color: #222; background: #fff; padding: 20px; }
|
||||
body { font-family: 'Noto Sans KR', 'Segoe UI', sans-serif; font-size: 13px; color: #222; background: #fff; padding: 20px; }
|
||||
.header { border-bottom: 2px solid #4B5EFC; padding-bottom: 12px; margin-bottom: 20px; }
|
||||
.header h1 { font-size: 18px; font-weight: 700; color: #1a1b2e; }
|
||||
.header .meta { font-size: 11px; color: #888; margin-top: 4px; }
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace AxCopilot.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 전역 DI 서비스 로케이터.
|
||||
/// 기존 수동 new 패턴에서 DI 컨테이너로의 점진적 전환을 지원합니다.
|
||||
/// App.OnStartup()에서 Configure()를 호출하여 초기화합니다.
|
||||
///
|
||||
/// 사용법:
|
||||
/// var settings = ServiceLocator.Get<ISettingsService>();
|
||||
/// var storage = ServiceLocator.Get<IChatStorageService>();
|
||||
/// </summary>
|
||||
public static class ServiceLocator
|
||||
{
|
||||
private static IServiceProvider? _provider;
|
||||
|
||||
/// <summary>DI 컨테이너를 구성합니다. 앱 시작 시 1회만 호출합니다.</summary>
|
||||
public static IServiceProvider Configure(Action<IServiceCollection> configureServices)
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
configureServices(services);
|
||||
_provider = services.BuildServiceProvider(validateScopes: true);
|
||||
return _provider;
|
||||
}
|
||||
|
||||
/// <summary>이미 생성된 ServiceProvider를 직접 설정합니다.</summary>
|
||||
public static void SetProvider(IServiceProvider provider)
|
||||
{
|
||||
_provider = provider ?? throw new ArgumentNullException(nameof(provider));
|
||||
}
|
||||
|
||||
/// <summary>등록된 서비스를 가져옵니다.</summary>
|
||||
public static T Get<T>() where T : notnull
|
||||
{
|
||||
if (_provider == null)
|
||||
throw new InvalidOperationException("ServiceLocator가 초기화되지 않았습니다. App.OnStartup()에서 Configure()를 호출하세요.");
|
||||
return _provider.GetRequiredService<T>();
|
||||
}
|
||||
|
||||
/// <summary>등록된 서비스를 가져옵니다. 미등록 시 null을 반환합니다.</summary>
|
||||
public static T? GetOptional<T>() where T : class
|
||||
{
|
||||
return _provider?.GetService<T>();
|
||||
}
|
||||
|
||||
/// <summary>내부 ServiceProvider에 직접 접근합니다. (테스트/마이그레이션용)</summary>
|
||||
public static IServiceProvider? Provider => _provider;
|
||||
}
|
||||
@@ -5,7 +5,7 @@ using AxCopilot.Services.Agent;
|
||||
|
||||
namespace AxCopilot.Services;
|
||||
|
||||
public class SettingsService
|
||||
public class SettingsService : ISettingsService
|
||||
{
|
||||
private static readonly string AppDataDir = InitAppDataDir();
|
||||
|
||||
@@ -186,12 +186,13 @@ public class SettingsService
|
||||
}
|
||||
catch (IOException) when (attempt < 2)
|
||||
{
|
||||
Thread.Sleep(50 * (attempt + 1));
|
||||
// SpinWait으로 짧은 지연 (Thread.Sleep보다 UI 블로킹 최소화)
|
||||
Thread.SpinWait(50_000 * (attempt + 1));
|
||||
}
|
||||
catch (Exception ex) when (attempt < 2)
|
||||
{
|
||||
LogService.Warn($"settings.dat 저장 재시도 {attempt + 1}/3: {ex.Message}");
|
||||
Thread.Sleep(50 * (attempt + 1));
|
||||
Thread.SpinWait(50_000 * (attempt + 1));
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -203,6 +204,42 @@ public class SettingsService
|
||||
SettingsChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
/// <summary>비동기 설정 저장. UI 스레드에서 호출 시 블로킹 없이 저장합니다.</summary>
|
||||
public async Task SaveAsync()
|
||||
{
|
||||
EnsureDirectories();
|
||||
NormalizeRuntimeSettings();
|
||||
var json = JsonSerializer.Serialize(_settings, JsonOptions);
|
||||
var encrypted = CryptoService.PortableEncrypt(json);
|
||||
|
||||
await Task.Run(() =>
|
||||
{
|
||||
lock (_saveLock)
|
||||
{
|
||||
var tmpPath = SettingsPath + ".tmp";
|
||||
for (int attempt = 0; attempt < 3; attempt++)
|
||||
{
|
||||
try
|
||||
{
|
||||
File.WriteAllText(tmpPath, encrypted);
|
||||
File.Move(tmpPath, SettingsPath, overwrite: true);
|
||||
break;
|
||||
}
|
||||
catch (Exception ex) when (attempt < 2)
|
||||
{
|
||||
LogService.Warn($"settings.dat 비동기 저장 재시도 {attempt + 1}/3: {ex.Message}");
|
||||
Thread.Sleep(50 * (attempt + 1));
|
||||
}
|
||||
finally
|
||||
{
|
||||
try { if (File.Exists(tmpPath)) File.Delete(tmpPath); } catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
}).ConfigureAwait(false);
|
||||
SettingsChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
private void NormalizeRuntimeSettings()
|
||||
{
|
||||
var expressionLevel = (_settings.Llm.AgentUiExpressionLevel ?? "").Trim().ToLowerInvariant();
|
||||
|
||||
@@ -30,10 +30,34 @@ public static class TokenEstimator
|
||||
{
|
||||
int total = 0;
|
||||
foreach (var m in messages)
|
||||
total += Estimate(m.Content) + 4; // 메시지 오버헤드
|
||||
{
|
||||
var content = m.Content;
|
||||
// _tool_use_blocks JSON은 API 전송 시 구조화된 형식으로 변환되므로
|
||||
// 원시 JSON 길이 대비 약 40% 할인 적용 (JSON 래퍼 오버헤드 제거)
|
||||
if (m.Role == "assistant" && content != null && content.StartsWith("{\"_tool_use_blocks\""))
|
||||
total += (int)(Estimate(content) * 0.6) + 4;
|
||||
// tool_result JSON도 구조화 변환됨
|
||||
else if (m.Role == "user" && content != null && content.StartsWith("{\"type\":\"tool_result\""))
|
||||
total += (int)(Estimate(content) * 0.7) + 4;
|
||||
else
|
||||
total += Estimate(content ?? "") + 4;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
/// <summary>시스템 프롬프트 + 도구 정의에 의한 기본 오버헤드 토큰을 추정합니다.</summary>
|
||||
/// <param name="systemPromptLength">시스템 프롬프트 문자 수</param>
|
||||
/// <param name="toolCount">등록된 도구 수</param>
|
||||
/// <returns>추정 토큰 수</returns>
|
||||
public static int EstimateBaseOverhead(int systemPromptLength, int toolCount)
|
||||
{
|
||||
// 시스템 프롬프트 토큰
|
||||
var sysTokens = systemPromptLength > 0 ? (int)(systemPromptLength / 3.5) : 0;
|
||||
// 도구 정의: 이름 + 설명 + 파라미터 스키마 ≈ 평균 180토큰/도구
|
||||
var toolTokens = toolCount * 180;
|
||||
return sysTokens + toolTokens;
|
||||
}
|
||||
|
||||
/// <summary>비용을 추정합니다 (USD 기준).</summary>
|
||||
public static (double InputCost, double OutputCost) EstimateCost(
|
||||
int promptTokens, int completionTokens, string service, string model)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<SolidColorBrush x:Key="LauncherBackground" Color="#F3F4F6"/>
|
||||
<SolidColorBrush x:Key="ItemBackground" Color="#FCFCFD"/>
|
||||
<SolidColorBrush x:Key="LauncherBackground" Color="#FFFFFF"/>
|
||||
<SolidColorBrush x:Key="ItemBackground" Color="#F3F4F6"/>
|
||||
<SolidColorBrush x:Key="ItemSelectedBackground" Color="#E4E7EC"/>
|
||||
<SolidColorBrush x:Key="ItemHoverBackground" Color="#ECEFF3"/>
|
||||
<SolidColorBrush x:Key="PrimaryText" Color="#181A1F"/>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<SolidColorBrush x:Key="LauncherBackground" Color="#FAF6EF"/>
|
||||
<SolidColorBrush x:Key="ItemBackground" Color="#FFFDFC"/>
|
||||
<SolidColorBrush x:Key="LauncherBackground" Color="#FFFFFF"/>
|
||||
<SolidColorBrush x:Key="ItemBackground" Color="#FAF6EF"/>
|
||||
<SolidColorBrush x:Key="ItemSelectedBackground" Color="#F3E5D5"/>
|
||||
<SolidColorBrush x:Key="ItemHoverBackground" Color="#F6ECE0"/>
|
||||
<SolidColorBrush x:Key="PrimaryText" Color="#2B2118"/>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<SolidColorBrush x:Key="LauncherBackground" Color="#F8FAFC"/>
|
||||
<SolidColorBrush x:Key="ItemBackground" Color="#FFFFFF"/>
|
||||
<SolidColorBrush x:Key="LauncherBackground" Color="#FFFFFF"/>
|
||||
<SolidColorBrush x:Key="ItemBackground" Color="#F8FAFC"/>
|
||||
<SolidColorBrush x:Key="ItemSelectedBackground" Color="#E2E8F0"/>
|
||||
<SolidColorBrush x:Key="ItemHoverBackground" Color="#EAF2FF"/>
|
||||
<SolidColorBrush x:Key="PrimaryText" Color="#0F172A"/>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<SolidColorBrush x:Key="LauncherBackground" Color="#ECEFF4"/>
|
||||
<SolidColorBrush x:Key="ItemBackground" Color="#FFFFFF"/>
|
||||
<SolidColorBrush x:Key="LauncherBackground" Color="#FFFFFF"/>
|
||||
<SolidColorBrush x:Key="ItemBackground" Color="#ECEFF4"/>
|
||||
<SolidColorBrush x:Key="ItemSelectedBackground" Color="#E5E9F0"/>
|
||||
<SolidColorBrush x:Key="ItemHoverBackground" Color="#E5E9F0"/>
|
||||
<SolidColorBrush x:Key="PrimaryText" Color="#2E3440"/>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<SolidColorBrush x:Key="LauncherBackground" Color="#F8FAFC"/>
|
||||
<SolidColorBrush x:Key="ItemBackground" Color="#FFFFFF"/>
|
||||
<SolidColorBrush x:Key="LauncherBackground" Color="#FFFFFF"/>
|
||||
<SolidColorBrush x:Key="ItemBackground" Color="#F0F2F8"/>
|
||||
<SolidColorBrush x:Key="ItemSelectedBackground" Color="#E2E8F0"/>
|
||||
<SolidColorBrush x:Key="ItemHoverBackground" Color="#EEF2F7"/>
|
||||
<SolidColorBrush x:Key="PrimaryText" Color="#0F172A"/>
|
||||
|
||||
@@ -107,6 +107,28 @@ public class HexToColorConverter : IValueConverter
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
/// <summary>"#RRGGBB" 헥스 문자열 → SolidColorBrush. DataTemplate 아이콘 색상에 사용.</summary>
|
||||
public class HexToBrushConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type t, object p, CultureInfo c)
|
||||
{
|
||||
if (value is string hex && !string.IsNullOrEmpty(hex))
|
||||
{
|
||||
try
|
||||
{
|
||||
var color = (Color)ColorConverter.ConvertFromString(hex);
|
||||
var brush = new SolidColorBrush(color);
|
||||
brush.Freeze();
|
||||
return brush;
|
||||
}
|
||||
catch { /* 잘못된 값이면 기본 반환 */ }
|
||||
}
|
||||
return new SolidColorBrush(Color.FromRgb(0x6B, 0x72, 0x80));
|
||||
}
|
||||
public object ConvertBack(object v, Type t, object p, CultureInfo c) =>
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
/// <summary>null 또는 빈 문자열이면 Collapsed, 값이 있으면 Visible.</summary>
|
||||
public class NullToCollapsedConverter : IValueConverter
|
||||
{
|
||||
|
||||
@@ -0,0 +1,276 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using AxCopilot.Models;
|
||||
|
||||
namespace AxCopilot.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// ChatWindow의 MVVM ViewModel.
|
||||
/// XAML 바인딩 대상 속성을 노출합니다.
|
||||
/// 기존 코드비하인드와 공존하며 점진적으로 확장합니다.
|
||||
/// </summary>
|
||||
public class ChatWindowViewModel : ViewModelBase
|
||||
{
|
||||
// ── 탭 상태 ──────────────────────────────────────────────
|
||||
|
||||
private string _activeTab = "Chat";
|
||||
public string ActiveTab
|
||||
{
|
||||
get => _activeTab;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _activeTab, value))
|
||||
{
|
||||
OnPropertyChanged(nameof(IsChat));
|
||||
OnPropertyChanged(nameof(IsCowork));
|
||||
OnPropertyChanged(nameof(IsCode));
|
||||
OnPropertyChanged(nameof(TabDisplayName));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsChat => ActiveTab == "Chat";
|
||||
public bool IsCowork => ActiveTab == "Cowork";
|
||||
public bool IsCode => ActiveTab == "Code";
|
||||
public string TabDisplayName => ActiveTab switch
|
||||
{
|
||||
"Chat" => "AX Agent",
|
||||
"Cowork" => "Cowork",
|
||||
"Code" => "Code",
|
||||
_ => ActiveTab,
|
||||
};
|
||||
|
||||
// ── 대화 제목 ────────────────────────────────────────────
|
||||
|
||||
private string _chatTitle = "";
|
||||
public string ChatTitle
|
||||
{
|
||||
get => _chatTitle;
|
||||
set => SetProperty(ref _chatTitle, value);
|
||||
}
|
||||
|
||||
// ── 스트리밍 상태 ────────────────────────────────────────
|
||||
|
||||
private bool _isStreaming;
|
||||
public bool IsStreaming
|
||||
{
|
||||
get => _isStreaming;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _isStreaming, value))
|
||||
{
|
||||
OnPropertyChanged(nameof(CanSend));
|
||||
OnPropertyChanged(nameof(SendButtonIcon));
|
||||
OnPropertyChanged(nameof(StreamingStatusText));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool CanSend => !IsStreaming;
|
||||
public string SendButtonIcon => IsStreaming ? "\uE711" : "\uE724"; // Stop : Send
|
||||
public string StreamingStatusText => IsStreaming ? "응답 생성 중..." : "";
|
||||
|
||||
// ── 모델 정보 ────────────────────────────────────────────
|
||||
|
||||
private string _modelLabel = "";
|
||||
public string ModelLabel
|
||||
{
|
||||
get => _modelLabel;
|
||||
set => SetProperty(ref _modelLabel, value);
|
||||
}
|
||||
|
||||
private string _serviceLabel = "";
|
||||
public string ServiceLabel
|
||||
{
|
||||
get => _serviceLabel;
|
||||
set => SetProperty(ref _serviceLabel, value);
|
||||
}
|
||||
|
||||
// ── 토큰 사용량 ─────────────────────────────────────────
|
||||
|
||||
private int _contextTokens;
|
||||
public int ContextTokens
|
||||
{
|
||||
get => _contextTokens;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _contextTokens, value))
|
||||
{
|
||||
OnPropertyChanged(nameof(ContextUsagePercent));
|
||||
OnPropertyChanged(nameof(ContextUsageText));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int _maxContextTokens = 128000;
|
||||
public int MaxContextTokens
|
||||
{
|
||||
get => _maxContextTokens;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _maxContextTokens, value))
|
||||
{
|
||||
OnPropertyChanged(nameof(ContextUsagePercent));
|
||||
OnPropertyChanged(nameof(ContextUsageText));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public double ContextUsagePercent => MaxContextTokens > 0
|
||||
? Math.Min(100.0, ContextTokens * 100.0 / MaxContextTokens)
|
||||
: 0;
|
||||
|
||||
public string ContextUsageText => MaxContextTokens > 0
|
||||
? $"{ContextTokens:N0} / {MaxContextTokens:N0}"
|
||||
: "";
|
||||
|
||||
// ── 대화 목록 ────────────────────────────────────────────
|
||||
|
||||
public ObservableCollection<ConversationItemViewModel> Conversations { get; } = new();
|
||||
|
||||
private int _remainingConversationCount;
|
||||
public int RemainingConversationCount
|
||||
{
|
||||
get => _remainingConversationCount;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _remainingConversationCount, value))
|
||||
OnPropertyChanged(nameof(HasMoreConversations));
|
||||
}
|
||||
}
|
||||
|
||||
public bool HasMoreConversations => RemainingConversationCount > 0;
|
||||
|
||||
private string _emptyConversationText = "";
|
||||
public string EmptyConversationText
|
||||
{
|
||||
get => _emptyConversationText;
|
||||
set => SetProperty(ref _emptyConversationText, value);
|
||||
}
|
||||
|
||||
private string _selectedCategory = "";
|
||||
public string SelectedCategory
|
||||
{
|
||||
get => _selectedCategory;
|
||||
set => SetProperty(ref _selectedCategory, value);
|
||||
}
|
||||
|
||||
private bool _failedOnlyFilter;
|
||||
public bool FailedOnlyFilter
|
||||
{
|
||||
get => _failedOnlyFilter;
|
||||
set => SetProperty(ref _failedOnlyFilter, value);
|
||||
}
|
||||
|
||||
private bool _runningOnlyFilter;
|
||||
public bool RunningOnlyFilter
|
||||
{
|
||||
get => _runningOnlyFilter;
|
||||
set => SetProperty(ref _runningOnlyFilter, value);
|
||||
}
|
||||
|
||||
private bool _sortByRecent;
|
||||
public bool SortByRecent
|
||||
{
|
||||
get => _sortByRecent;
|
||||
set => SetProperty(ref _sortByRecent, value);
|
||||
}
|
||||
|
||||
// ── Git 상태 ─────────────────────────────────────────────
|
||||
|
||||
private string _gitBranchName = "";
|
||||
public string GitBranchName
|
||||
{
|
||||
get => _gitBranchName;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _gitBranchName, value))
|
||||
OnPropertyChanged(nameof(HasGitBranch));
|
||||
}
|
||||
}
|
||||
|
||||
public bool HasGitBranch => !string.IsNullOrEmpty(GitBranchName);
|
||||
|
||||
// ── 작업 폴더 ────────────────────────────────────────────
|
||||
|
||||
private string _workFolder = "";
|
||||
public string WorkFolder
|
||||
{
|
||||
get => _workFolder;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _workFolder, value))
|
||||
{
|
||||
OnPropertyChanged(nameof(HasWorkFolder));
|
||||
OnPropertyChanged(nameof(WorkFolderDisplay));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool HasWorkFolder => !string.IsNullOrEmpty(WorkFolder);
|
||||
public string WorkFolderDisplay => string.IsNullOrEmpty(WorkFolder) ? "폴더 선택" : WorkFolder;
|
||||
}
|
||||
|
||||
/// <summary>대화 목록 항목 ViewModel — DataTemplate 바인딩 대상.</summary>
|
||||
public class ConversationItemViewModel : ViewModelBase
|
||||
{
|
||||
public string Id { get; init; } = "";
|
||||
|
||||
private string _title = "";
|
||||
public string Title
|
||||
{
|
||||
get => _title;
|
||||
set => SetProperty(ref _title, value);
|
||||
}
|
||||
|
||||
private string _updatedAtText = "";
|
||||
public string UpdatedAtText
|
||||
{
|
||||
get => _updatedAtText;
|
||||
set => SetProperty(ref _updatedAtText, value);
|
||||
}
|
||||
|
||||
private bool _pinned;
|
||||
public bool Pinned
|
||||
{
|
||||
get => _pinned;
|
||||
set => SetProperty(ref _pinned, value);
|
||||
}
|
||||
|
||||
private bool _isSelected;
|
||||
public bool IsSelected
|
||||
{
|
||||
get => _isSelected;
|
||||
set => SetProperty(ref _isSelected, value);
|
||||
}
|
||||
|
||||
private bool _isRunning;
|
||||
public bool IsRunning
|
||||
{
|
||||
get => _isRunning;
|
||||
set => SetProperty(ref _isRunning, value);
|
||||
}
|
||||
|
||||
public string Category { get; init; } = "general";
|
||||
public string Symbol { get; init; } = "\uE8BD";
|
||||
public string ColorHex { get; init; } = "#6B7280";
|
||||
public string Tab { get; init; } = "Chat";
|
||||
public string Preview { get; init; } = "";
|
||||
public string? ParentId { get; init; }
|
||||
public int AgentRunCount { get; init; }
|
||||
public int FailedAgentRunCount { get; init; }
|
||||
public string LastAgentRunSummary { get; init; } = "";
|
||||
public string WorkFolder { get; init; } = "";
|
||||
|
||||
// ── 그룹 ──
|
||||
public string Group { get; init; } = "오늘";
|
||||
public int GroupOrder { get; init; }
|
||||
|
||||
// ── 표시용 computed ──
|
||||
public bool IsBranch => !string.IsNullOrEmpty(ParentId);
|
||||
public string IconText => Pinned ? "\uE718" : IsBranch ? "\uE8A5" : Symbol;
|
||||
public string RunStatusText => FailedAgentRunCount > 0
|
||||
? $"실패 {FailedAgentRunCount}"
|
||||
: AgentRunCount > 0 ? $"실행 {AgentRunCount}" : "";
|
||||
public bool HasRunStatus => AgentRunCount > 0 || FailedAgentRunCount > 0;
|
||||
public bool HasFailed => FailedAgentRunCount > 0;
|
||||
}
|
||||