핵심 엔진을 claude-code 기준으로 정렬하고 스트리밍 재시도 경계를 정리한다
- StreamingToolExecutionCoordinator에서 조기 실행 대상을 file_read/document_read 중심으로 축소하고 folder_map 등 구조 탐색 도구를 prefetch 대상에서 제거함 - 스트리밍 재시도 전에 RetryReset 이벤트를 추가해 중간 응답 미리보기 누적을 끊고 AgentLoopService가 재시도 경계를 명확히 표시하도록 조정함 - AxAgentExecutionEngine의 Cowork/Code 빈 응답 합성을 보수적으로 바꿔 실행 근거가 있을 때만 완료 요약을 만들고 근거가 없으면 로그 확인 안내를 반환하도록 정리함 - Code 루프의 post-tool verification과 completion gate도 직전 수정에서 함께 정리해 일반 수정의 과검증을 줄였음 - README.md, docs/DEVELOPMENT.md에 2026-04-09 21:03 (KST) 기준 변경 이력과 검증 결과를 반영함 검증 결과: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ 경고 0 / 오류 0
This commit is contained in:
34
README.md
34
README.md
@@ -7,10 +7,23 @@ 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-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 +1559,3 @@ MIT License
|
||||
- [AgentLoopTransitions.Verification.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentLoopTransitions.Verification.cs), [AgentLoopTransitions.Documents.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentLoopTransitions.Documents.cs), [AgentLoopCompactionPolicy.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentLoopCompactionPolicy.cs)로 검증/fallback/compact 정책 메서드를 분리해 [AgentLoopService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentLoopService.cs)의 책임을 더 줄였습니다.
|
||||
- [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이 실제 렌더 수를 얼마나 줄였는지 성능 로그로 확인할 수 있게 했습니다.
|
||||
|
||||
@@ -1,6 +1,31 @@
|
||||
# AX Copilot - 媛쒕컻 臾몄꽌
|
||||
|
||||
|
||||
## claude-code식 선택 탐색 우선순위 정렬
|
||||
|
||||
- 업데이트: 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)
|
||||
|
||||
@@ -23,7 +23,7 @@ public partial class AgentLoopService
|
||||
string Content,
|
||||
string? PreviousContent = null);
|
||||
|
||||
private readonly LlmService _llm;
|
||||
private readonly ILlmService _llm;
|
||||
private readonly ToolRegistry _tools;
|
||||
private readonly SettingsService _settings;
|
||||
private readonly IToolExecutionCoordinator _toolExecutionCoordinator;
|
||||
@@ -82,7 +82,7 @@ public partial class AgentLoopService
|
||||
/// <summary>에이전트 이벤트 발생 시 호출되는 콜백 (UI 표시용).</summary>
|
||||
public event Action<AgentEvent>? EventOccurred;
|
||||
|
||||
public AgentLoopService(LlmService llm, ToolRegistry tools, SettingsService settings)
|
||||
public AgentLoopService(ILlmService llm, ToolRegistry tools, SettingsService settings)
|
||||
{
|
||||
_llm = llm;
|
||||
_tools = tools;
|
||||
@@ -180,6 +180,9 @@ public partial class AgentLoopService
|
||||
Scope = ClassifyExplorationScope(userQuery, ActiveTab),
|
||||
SelectiveHit = true,
|
||||
};
|
||||
var pathAccessState = new PathAccessTrackingState();
|
||||
DateTime? lastToolResultAtUtc = null;
|
||||
string? lastToolResultToolName = null;
|
||||
|
||||
// 워크플로우 상세 로그: 에이전트 루프 시작
|
||||
WorkflowLogService.LogAgentLifecycle(_conversationId, _currentRunId, "start",
|
||||
@@ -242,6 +245,11 @@ public partial class AgentLoopService
|
||||
var context = BuildContext();
|
||||
InjectTaskTypeGuidance(messages, taskPolicy);
|
||||
InjectExplorationScopeGuidance(messages, explorationState.Scope);
|
||||
var preferredInitialToolSequence = BuildPreferredInitialToolSequence(
|
||||
explorationState,
|
||||
taskPolicy,
|
||||
ActiveTab,
|
||||
userQuery);
|
||||
EmitEvent(
|
||||
AgentEventType.Thinking,
|
||||
"",
|
||||
@@ -518,6 +526,19 @@ public partial class AgentLoopService
|
||||
}
|
||||
}
|
||||
|
||||
if (lastToolResultAtUtc is { } toolResultAt)
|
||||
{
|
||||
var waitMs = Math.Max(0, (long)(DateTime.UtcNow - toolResultAt).TotalMilliseconds);
|
||||
WorkflowLogService.LogTransition(
|
||||
_conversationId,
|
||||
_currentRunId,
|
||||
iteration,
|
||||
"llm_wait_after_tool_result",
|
||||
$"{lastToolResultToolName ?? "unknown"}:{waitMs}ms");
|
||||
lastToolResultAtUtc = null;
|
||||
lastToolResultToolName = null;
|
||||
}
|
||||
|
||||
EmitEvent(AgentEventType.Thinking, "", $"LLM에 요청 중... (반복 {iteration}/{maxIterations})");
|
||||
|
||||
// Gemini 무료 티어 모드: LLM 호출 간 딜레이 (RPM 한도 초과 방지)
|
||||
@@ -554,10 +575,15 @@ public partial class AgentLoopService
|
||||
}
|
||||
|
||||
// P1: 반복당 1회 캐시 — GetRuntimeActiveTools는 동일 파라미터로 반복 내 여러 번 호출됨
|
||||
var cachedActiveTools = GetRuntimeActiveTools(llm.DisabledTools, runtimeOverrides);
|
||||
var cachedActiveTools = FilterExplorationToolsForCurrentIteration(
|
||||
GetRuntimeActiveTools(llm.DisabledTools, runtimeOverrides),
|
||||
explorationState,
|
||||
userQuery,
|
||||
ActiveTab,
|
||||
totalToolCalls);
|
||||
|
||||
// LLM에 도구 정의와 함께 요청
|
||||
List<LlmService.ContentBlock> blocks;
|
||||
List<ContentBlock> blocks;
|
||||
var llmCallSw = Stopwatch.StartNew();
|
||||
try
|
||||
{
|
||||
@@ -612,7 +638,7 @@ public partial class AgentLoopService
|
||||
{
|
||||
switch (evt.Kind)
|
||||
{
|
||||
case LlmService.ToolStreamEventKind.TextDelta:
|
||||
case ToolStreamEventKind.TextDelta:
|
||||
if (!string.IsNullOrWhiteSpace(evt.Text))
|
||||
{
|
||||
streamedTextPreview.Append(evt.Text);
|
||||
@@ -626,7 +652,7 @@ public partial class AgentLoopService
|
||||
}
|
||||
}
|
||||
break;
|
||||
case LlmService.ToolStreamEventKind.ToolCallReady:
|
||||
case ToolStreamEventKind.ToolCallReady:
|
||||
if (evt.ToolCall != null)
|
||||
{
|
||||
EmitEvent(
|
||||
@@ -635,9 +661,14 @@ public partial class AgentLoopService
|
||||
$"스트리밍 도구 감지: {FormatToolCallSummary(evt.ToolCall)}");
|
||||
}
|
||||
break;
|
||||
case LlmService.ToolStreamEventKind.Completed:
|
||||
case ToolStreamEventKind.Completed:
|
||||
await Task.CompletedTask;
|
||||
break;
|
||||
case ToolStreamEventKind.RetryReset:
|
||||
streamedTextPreview.Clear();
|
||||
lastStreamUiUpdateAt = DateTime.UtcNow;
|
||||
EmitEvent(AgentEventType.Thinking, "", "스트리밍 중간 응답을 정리하고 재시도합니다.");
|
||||
break;
|
||||
}
|
||||
});
|
||||
runState.ContextRecoveryAttempts = 0;
|
||||
@@ -745,7 +776,7 @@ public partial class AgentLoopService
|
||||
|
||||
// 응답에서 텍스트와 도구 호출 분리
|
||||
var textParts = new List<string>();
|
||||
var toolCalls = new List<LlmService.ContentBlock>();
|
||||
var toolCalls = new List<ContentBlock>();
|
||||
|
||||
foreach (var block in blocks)
|
||||
{
|
||||
@@ -851,13 +882,15 @@ public partial class AgentLoopService
|
||||
"[System:ToolCallRequired] " +
|
||||
"⚠ 경고: 이전 응답에서 도구를 호출하지 않았습니다. " +
|
||||
"텍스트 설명만 반환하는 것은 허용되지 않습니다. " +
|
||||
$"먼저 권장 순서는 {preferredInitialToolSequence} 입니다. " +
|
||||
"지금 즉시 아래 형식으로 도구를 호출하세요:\n" +
|
||||
"<tool_call>\n{\"name\": \"TOOL_NAME\", \"arguments\": {\"param\": \"value\"}}\n</tool_call>\n" +
|
||||
$"사용 가능한 도구: {activeToolPreview}",
|
||||
_ =>
|
||||
"[System:ToolCallRequired] " +
|
||||
"🚨 최종 경고: 도구를 계속 호출하지 않고 있습니다. 이것이 마지막 기회입니다. " +
|
||||
"텍스트는 한 글자도 쓰지 마세요. 반드시 아래 형식으로 도구를 호출하세요:\n" +
|
||||
$"텍스트는 한 글자도 쓰지 말고 {preferredInitialToolSequence} 순서에 맞는 첫 도구를 고르세요. " +
|
||||
"반드시 아래 형식으로 도구를 호출하세요:\n" +
|
||||
"<tool_call>\n{\"name\": \"TOOL_NAME\", \"arguments\": {\"param\": \"value\"}}\n</tool_call>\n" +
|
||||
$"반드시 사용해야 할 도구 목록: {activeToolPreview}"
|
||||
};
|
||||
@@ -891,12 +924,13 @@ public partial class AgentLoopService
|
||||
{
|
||||
1 =>
|
||||
"[System:ToolCallRequired] 계획을 세웠지만 도구를 호출하지 않았습니다. " +
|
||||
$"권장 첫 순서는 {preferredInitialToolSequence} 입니다. " +
|
||||
"지금 당장 실행하세요. 아래 형식으로 도구를 호출하세요:\n" +
|
||||
"<tool_call>\n{\"name\": \"TOOL_NAME\", \"arguments\": {\"param\": \"value\"}}\n</tool_call>\n" +
|
||||
$"사용 가능한 도구: {planToolList}",
|
||||
_ =>
|
||||
"[System:ToolCallRequired] 🚨 도구 호출 없이 계획만 반복하고 있습니다. " +
|
||||
"텍스트를 한 글자도 쓰지 마세요. 오직 아래 형식의 도구 호출만 출력하세요:\n" +
|
||||
$"텍스트를 한 글자도 쓰지 말고 {preferredInitialToolSequence} 순서에 맞는 도구 호출만 출력하세요:\n" +
|
||||
"<tool_call>\n{\"name\": \"TOOL_NAME\", \"arguments\": {\"param\": \"value\"}}\n</tool_call>\n" +
|
||||
$"사용 가능한 도구: {planToolList}"
|
||||
};
|
||||
@@ -1248,7 +1282,7 @@ public partial class AgentLoopService
|
||||
: resolvedToolName;
|
||||
var effectiveCall = string.Equals(call.ToolName, effectiveToolName, StringComparison.OrdinalIgnoreCase)
|
||||
? call
|
||||
: new LlmService.ContentBlock
|
||||
: new ContentBlock
|
||||
{
|
||||
Type = call.Type,
|
||||
Text = call.Text,
|
||||
@@ -1306,6 +1340,18 @@ public partial class AgentLoopService
|
||||
continue;
|
||||
}
|
||||
|
||||
if (TryHandleRepeatedPathAccessTransition(
|
||||
effectiveCall,
|
||||
context,
|
||||
pathAccessState,
|
||||
messages,
|
||||
lastModifiedCodeFilePath,
|
||||
requireHighImpactCodeVerification,
|
||||
taskPolicy))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Task Decomposition: 단계 진행률 추적
|
||||
if (planSteps.Count > 0)
|
||||
{
|
||||
@@ -1345,7 +1391,7 @@ public partial class AgentLoopService
|
||||
Role = "system",
|
||||
Content =
|
||||
"Exploration correction: The current request is narrow. Stop broad workspace scanning. " +
|
||||
"Use grep/glob to identify only files directly related to the user's topic, then read a very small targeted set. " +
|
||||
"Follow this order: glob/grep -> targeted file_read/document_read -> folder_map only if the user explicitly needs folder structure. " +
|
||||
"Do not repeat folder_map unless repository structure is genuinely required."
|
||||
});
|
||||
EmitEvent(
|
||||
@@ -1405,7 +1451,7 @@ public partial class AgentLoopService
|
||||
if (llm.EnableHookInputMutation &&
|
||||
TryGetHookUpdatedInput(preResults, out var updatedInput))
|
||||
{
|
||||
effectiveCall = new LlmService.ContentBlock
|
||||
effectiveCall = new ContentBlock
|
||||
{
|
||||
Type = effectiveCall.Type,
|
||||
Text = effectiveCall.Text,
|
||||
@@ -1533,6 +1579,8 @@ public partial class AgentLoopService
|
||||
ref statsFailCount,
|
||||
ref statsInputTokens,
|
||||
ref statsOutputTokens);
|
||||
lastToolResultAtUtc = DateTime.UtcNow;
|
||||
lastToolResultToolName = effectiveCall.ToolName;
|
||||
|
||||
if (!result.Success)
|
||||
{
|
||||
@@ -1608,7 +1656,14 @@ public partial class AgentLoopService
|
||||
messages.Add(LlmService.CreateToolResultMessage(
|
||||
effectiveCall.ToolId,
|
||||
effectiveCall.ToolName,
|
||||
BuildLoopToolResultMessage(effectiveCall, result, runState)));
|
||||
BuildToolResultFeedbackMessage(
|
||||
effectiveCall,
|
||||
result,
|
||||
runState,
|
||||
context,
|
||||
messages,
|
||||
explorationState,
|
||||
iteration)));
|
||||
|
||||
if (TryHandleReadOnlyStagnationTransition(
|
||||
consecutiveReadOnlySuccessTools,
|
||||
@@ -1909,7 +1964,8 @@ public partial class AgentLoopService
|
||||
SkillRuntimeOverrides? runtimeOverrides)
|
||||
{
|
||||
var mergedDisabled = MergeDisabledTools(disabledToolNames);
|
||||
var active = _tools.GetActiveTools(mergedDisabled);
|
||||
// 탭별 도구 필터링: 현재 탭에 맞는 도구만 LLM에 전송 (토큰 절약)
|
||||
var active = _tools.GetActiveToolsForTab(ActiveTab, mergedDisabled);
|
||||
if (runtimeOverrides == null || runtimeOverrides.AllowedToolNames.Count == 0)
|
||||
return active;
|
||||
|
||||
@@ -4136,7 +4192,7 @@ public partial class AgentLoopService
|
||||
|
||||
// 검증 응답 처리
|
||||
var verifyText = new List<string>();
|
||||
var verifyToolCalls = new List<LlmService.ContentBlock>();
|
||||
var verifyToolCalls = new List<ContentBlock>();
|
||||
|
||||
foreach (var block in verifyBlocks)
|
||||
{
|
||||
@@ -4724,7 +4780,7 @@ public partial class AgentLoopService
|
||||
}
|
||||
|
||||
/// <summary>영향 범위 기반 의사결정 체크. 확인이 필요하면 메시지를 반환, 불필요하면 null.</summary>
|
||||
private string? CheckDecisionRequired(LlmService.ContentBlock call, AgentContext context)
|
||||
private string? CheckDecisionRequired(ContentBlock call, AgentContext context)
|
||||
{
|
||||
var app = System.Windows.Application.Current as App;
|
||||
var level = app?.SettingsService?.Settings.Llm.AgentDecisionLevel ?? "normal";
|
||||
@@ -4811,7 +4867,7 @@ public partial class AgentLoopService
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string FormatToolCallSummary(LlmService.ContentBlock call)
|
||||
private static string FormatToolCallSummary(ContentBlock call)
|
||||
{
|
||||
if (call.ToolInput == null) return call.ToolName;
|
||||
try
|
||||
@@ -4829,7 +4885,7 @@ public partial class AgentLoopService
|
||||
catch { return call.ToolName; }
|
||||
}
|
||||
|
||||
private static string BuildToolCallSignature(LlmService.ContentBlock call)
|
||||
private static string BuildToolCallSignature(ContentBlock call)
|
||||
{
|
||||
var name = (call.ToolName ?? "").Trim().ToLowerInvariant();
|
||||
if (call.ToolInput == null)
|
||||
@@ -4929,7 +4985,7 @@ public partial class AgentLoopService
|
||||
FailureRecoveryKind.Permission =>
|
||||
"권한설정 확인 -> 대상 경로 재선정 -> file_read/grep으로 대체 검증 -> 승인 후 재실행",
|
||||
FailureRecoveryKind.Path =>
|
||||
"folder_map/glob -> 절대경로 확인 -> file_read로 존재 검증 -> 원도구 재실행",
|
||||
"glob -> file_read -> folder_map(필요 시만) -> 절대경로 확인 -> 원도구 재실행",
|
||||
FailureRecoveryKind.Command =>
|
||||
"도구 스키마 재확인 -> 최소 파라미터 호출 -> 옵션 확장 -> 필요 시 대체 도구",
|
||||
FailureRecoveryKind.Dependency =>
|
||||
@@ -4940,7 +4996,7 @@ public partial class AgentLoopService
|
||||
{
|
||||
"build_run" or "test_loop" => "file_read -> grep/glob -> git_tool(diff) -> file_edit -> build_run/test_loop",
|
||||
"file_edit" or "file_write" => "file_read -> grep/glob -> git_tool(diff) -> file_edit -> build_run/test_loop",
|
||||
"grep" or "glob" => "file_read -> folder_map -> grep/glob",
|
||||
"grep" or "glob" => "glob/grep 재구성 -> file_read -> folder_map(필요 시만)",
|
||||
_ => "file_read -> grep/glob -> git_tool(diff) -> targeted tool retry"
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
@@ -96,9 +96,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,
|
||||
@@ -134,7 +134,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 +156,16 @@ public partial class AgentLoopService
|
||||
if (!shouldVerify)
|
||||
return false;
|
||||
|
||||
if (string.Equals(ActiveTab, "Code", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var highImpactCodeChange = IsHighImpactCodeModification(ActiveTab ?? "", call.ToolName, result);
|
||||
var hasDiffEvidence = HasDiffEvidenceAfterLastModification(messages);
|
||||
var hasRecentBuildOrTestEvidence = HasBuildOrTestEvidenceAfterLastModification(messages);
|
||||
|
||||
if (!highImpactCodeChange || (hasDiffEvidence && hasRecentBuildOrTestEvidence))
|
||||
return false;
|
||||
}
|
||||
|
||||
await RunPostToolVerificationAsync(messages, call.ToolName, result, context, ct);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
{
|
||||
requireHighImpactCodeVerification = highImpactCodeChange;
|
||||
lastModifiedCodeFilePath = result.FilePath;
|
||||
messages.Add(new ChatMessage
|
||||
{
|
||||
@@ -52,8 +52,12 @@ public partial class AgentLoopService
|
||||
var hasCodeVerificationEvidence = HasCodeVerificationEvidenceAfterLastModification(
|
||||
messages,
|
||||
requireHighImpactCodeVerification);
|
||||
var hasDiffEvidence = HasDiffEvidenceAfterLastModification(messages);
|
||||
var hasRecentBuildOrTestEvidence = HasBuildOrTestEvidenceAfterLastModification(messages);
|
||||
var hasSuccessfulBuildAndTestEvidence = HasSuccessfulBuildAndTestAfterLastModification(messages);
|
||||
if (executionPolicy.CodeVerificationGateMaxRetries > 0
|
||||
&& !hasCodeVerificationEvidence
|
||||
&& !(hasDiffEvidence && hasRecentBuildOrTestEvidence && !requireHighImpactCodeVerification)
|
||||
&& runState.CodeVerificationGateRetry < executionPolicy.CodeVerificationGateMaxRetries)
|
||||
{
|
||||
runState.CodeVerificationGateRetry++;
|
||||
@@ -89,7 +93,10 @@ public partial class AgentLoopService
|
||||
return true;
|
||||
}
|
||||
|
||||
var hasBlockingCodeEvidenceGap = !hasCodeVerificationEvidence
|
||||
|| (requireHighImpactCodeVerification && !hasSuccessfulBuildAndTestEvidence);
|
||||
if (executionPolicy.FinalReportGateMaxRetries > 0
|
||||
&& !hasBlockingCodeEvidenceGap
|
||||
&& !HasSufficientFinalReportEvidence(textResponse, taskPolicy, requireHighImpactCodeVerification, messages)
|
||||
&& runState.FinalReportGateRetry < executionPolicy.FinalReportGateMaxRetries)
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -8,6 +8,36 @@ using AxCopilot.Services.Agent;
|
||||
|
||||
namespace AxCopilot.Services;
|
||||
|
||||
/// <summary>LLM 응답에서 파싱된 컨텐츠 블록.</summary>
|
||||
public class ContentBlock
|
||||
{
|
||||
public string Type { get; init; } = "text"; // "text" | "tool_use"
|
||||
public string Text { get; init; } = ""; // text 타입일 때
|
||||
public string ToolName { get; init; } = ""; // tool_use 타입일 때
|
||||
public string ToolId { get; init; } = ""; // tool_use ID
|
||||
public JsonElement? ToolInput { get; init; } // tool_use 파라미터
|
||||
public string? ResolvedToolName { get; set; }
|
||||
public Task<ToolPrefetchResult?>? PrefetchedExecutionTask { get; set; }
|
||||
}
|
||||
|
||||
public sealed record ToolPrefetchResult(
|
||||
Agent.ToolResult Result,
|
||||
long ElapsedMilliseconds,
|
||||
string? ResolvedToolName = null);
|
||||
|
||||
public enum ToolStreamEventKind
|
||||
{
|
||||
TextDelta,
|
||||
ToolCallReady,
|
||||
RetryReset,
|
||||
Completed
|
||||
}
|
||||
|
||||
public sealed record ToolStreamEvent(
|
||||
ToolStreamEventKind Kind,
|
||||
string Text = "",
|
||||
ContentBlock? ToolCall = null);
|
||||
|
||||
/// <summary>
|
||||
/// LlmService의 Function Calling (tool_use) 확장.
|
||||
/// Claude tool_use, Gemini function_calling 프로토콜을 지원합니다.
|
||||
@@ -15,35 +45,6 @@ namespace AxCopilot.Services;
|
||||
/// </summary>
|
||||
public partial class LlmService
|
||||
{
|
||||
/// <summary>LLM 응답에서 파싱된 컨텐츠 블록.</summary>
|
||||
public class ContentBlock
|
||||
{
|
||||
public string Type { get; init; } = "text"; // "text" | "tool_use"
|
||||
public string Text { get; init; } = ""; // text 타입일 때
|
||||
public string ToolName { get; init; } = ""; // tool_use 타입일 때
|
||||
public string ToolId { get; init; } = ""; // tool_use ID
|
||||
public JsonElement? ToolInput { get; init; } // tool_use 파라미터
|
||||
public string? ResolvedToolName { get; set; }
|
||||
public Task<ToolPrefetchResult?>? PrefetchedExecutionTask { get; set; }
|
||||
}
|
||||
|
||||
public sealed record ToolPrefetchResult(
|
||||
Agent.ToolResult Result,
|
||||
long ElapsedMilliseconds,
|
||||
string? ResolvedToolName = null);
|
||||
|
||||
public enum ToolStreamEventKind
|
||||
{
|
||||
TextDelta,
|
||||
ToolCallReady,
|
||||
Completed
|
||||
}
|
||||
|
||||
public sealed record ToolStreamEvent(
|
||||
ToolStreamEventKind Kind,
|
||||
string Text = "",
|
||||
ContentBlock? ToolCall = null);
|
||||
|
||||
/// <summary>도구 정의를 포함하여 LLM에 요청하고, 텍스트 + tool_use 블록을 파싱하여 반환합니다.</summary>
|
||||
/// <param name="forceToolCall">
|
||||
/// true이면 <c>tool_choice: "required"</c>를 요청에 추가하여 모델이 반드시 도구를 호출하도록 강제합니다.
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user