diff --git a/README.md b/README.md
index 7a41ab7..20145ad 100644
--- a/README.md
+++ b/README.md
@@ -7,6 +7,29 @@ Windows 전용 시맨틱 런처 & 워크스페이스 매니저
개발 참고: Claw Code 동등성 작업 추적 문서
`docs/claw-code-parity-plan.md`
+- 업데이트: 2026-04-09 22:48 (KST)
+- `claude-code` 기준 후속 과제를 이어서 반영했습니다. IBM 배포형 vLLM/Qwen 경로에서는 과거 `tool_calls`/`role=tool` 이력을 그대로 재전송하지 않고, `...` 중심의 평탄한 transcript로 직렬화해 엄격한 tool history 검사에 덜 걸리도록 전용 분기를 넣었습니다.
+- Cowork 문서 생성 프롬프트는 `document_review`와 `format_convert`를 상시 기본 단계처럼 밀지 않도록 다시 낮췄습니다. 이제 기본 검증은 `file_read/document_read` 중심이고, `document_review`는 큰 문서 품질 점검이나 명시적 요청일 때만 권장합니다.
+- Code/Cowork 공통 런타임 복구 문구도 정리했습니다. `tool_search`는 후보만으로 충분할 때는 바로 실제 도구를 고르도록 하고, 정말 모호할 때만 사용하게 바꿨습니다. `spawn_agent`도 기본 전면 노출 대신 병렬 조사가 실제로 도움이 될 때만 고려하도록 완화했습니다.
+- Code의 의미 기반 탐색도 한 단계 더 보강했습니다. 정의/참조/구현/호출관계 질문은 `lsp_code_intel`을 더 앞에 두고, LSP 결과는 파일 수·대표 위치·첫 결과를 함께 요약해 다음 액션 판단에 바로 쓸 수 있게 정리했습니다.
+- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\` 경고 0 / 오류 0
+
+- 업데이트: 2026-04-09 22:38 (KST)
+- Code 탭의 구조적 코드 읽기를 `claude-code`에 더 가깝게 확장했습니다. AX에 이미 있던 `lsp_code_intel` 도구를 `goto_definition`, `find_references`, `hover`, `goto_implementation`, `workspace_symbols`, `prepare_call_hierarchy`, `incoming_calls`, `outgoing_calls`까지 넓혀, 단순 `grep/file_read`만이 아니라 정의/참조/호출관계를 입체적으로 볼 수 있게 했습니다.
+- Code 프롬프트와 탐색 우선순위도 이에 맞춰 조정했습니다. 이제 특정 심볼의 정의, 참조, 구현, 호출자/피호출자를 볼 때는 `lsp_code_intel`을 우선 후보로 제시하고, 텍스트 검색이 더 적합한 경우에만 `grep/glob`를 쓰도록 유도합니다.
+- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\` 경고 0 / 오류 0
+
+- 업데이트: 2026-04-09 21:48 (KST)
+- Cowork/Code 생성 로직을 `claude-code` 기준으로 다시 정리했습니다. Cowork는 순수 문서 생성 요청이면 `glob/grep/document_read/folder_map`로 먼저 새지 않고 `document_plan -> 생성 도구`로 바로 가도록 프롬프트와 탐색 우선순위를 맞췄습니다.
+- Code는 시작 흐름을 더 얇게 바꿨습니다. `targeted file_read` 또는 `grep/glob`로 가장 작은 범위만 확인한 뒤 바로 `file_edit/file_write`로 이어지고, `build_run/test_loop`와 `git_tool(diff)`는 필요할 때 쓰는 방향으로 정리했습니다.
+- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\` 경고 0 / 오류 0
+
+- 업데이트: 2026-04-09 21:58 (KST)
+- Cowork/Code 기본 실행 정책을 더 `claude-code`답게 완화했습니다. `balanced`, `reasoning_first` 프로필에서 초기 도구 호출 강제와 post-tool 검증, 문서 검증 게이트를 기본 비활성화해 불필요한 재호출을 줄였습니다.
+- Cowork는 문서 생성 완료 뒤 추가 검증 턴을 기본으로 붙이지 않고, 결과 파일이 만들어지면 바로 완료 경로로 빠지도록 정리했습니다. 문서 품질 확인은 가벼운 self-check 성격으로 프롬프트도 완화했습니다.
+- Code는 일반 수정의 완료 근거를 `diff` 또는 최근 build/test 같은 가벼운 증거로도 인정하도록 바꿔, 고영향 수정이 아닐 때 `CodeQualityGate`가 과하게 재발동하지 않게 했습니다.
+- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\` 경고 0 / 오류 0
+
- 업데이트: 2026-04-09 21:03 (KST)
- 핵심 엔진 계층도 `claude-code` 기준으로 더 정리했습니다. 스트리밍 도구 코디네이터는 재시도 전에 중간 스트림 상태를 끊는 `RetryReset` 이벤트를 보내도록 바꿔, 부분 응답이 누적된 채 다시 이어지는 현상을 줄였습니다.
- 조기 실행 대상 읽기 도구는 `file_read`와 `document_read` 중심의 가벼운 도구로 다시 좁혔고, `folder_map` 같은 구조 탐색 도구는 더 이상 엔진 레벨 prefetch 대상에 넣지 않도록 조정했습니다.
diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md
index 80e2e67..c7d67be 100644
--- a/docs/DEVELOPMENT.md
+++ b/docs/DEVELOPMENT.md
@@ -1,8 +1,34 @@
# AX Copilot - 媛쒕컻 臾몄꽌
+## claude-code식 후속 호환/선택성 보강
+
+- 업데이트: 2026-04-09 22:48 (KST)
+- IBM 배포형 vLLM/Qwen 호환을 위해 `LlmService.ToolUse`의 `BuildIbmToolBody`를 다시 정리했습니다. 이전 assistant `tool_calls`와 `role=tool` 이력을 OpenAI 형식으로 재전송하던 경로를 제거하고, `_tool_use_blocks`는 `...` transcript로, `tool_result`는 plain user transcript로 평탄화합니다. 기존의 텍스트 기반 `TryExtractToolCallsFromText` 폴백과 함께 IBM 쪽의 엄격한 tool history 검사에 대응하는 방향입니다.
+- `ChatWindow.SystemPromptBuilder`는 Cowork/Code 모두 “도구 호출 필수” 톤을 더 낮췄습니다. Cowork는 `document_review`와 `format_convert`를 기본 후속 단계처럼 밀지 않고, `file_read/document_read` 중심의 가벼운 검증을 기본으로 삼습니다. Code/Cowork 공통 `Sub-Agent Delegation`도 `spawn_agent`를 병렬성이 실제로 도움이 될 때만 선택하도록 바꿨습니다.
+- `AgentLoopService`의 unknown/disallowed tool recovery는 `tool_search`를 항상 먼저 강제하지 않고, alias 자동 매핑 후보나 활성 도구 예시만으로 바로 선택 가능하면 그 도구를 바로 쓰도록 완화했습니다. `tool_search`는 정말 모호할 때만 쓰는 보조 수단으로 내렸습니다.
+- `AgentLoopTransitions.Execution`의 문서 검증 근거도 단순화했습니다. 문서 생성 후 기본 완료 근거는 `file_read`/`document_read`면 충분하고, `document_review`는 선택적 품질 점검 도구로만 남깁니다.
+- `AgentLoopExplorationPolicy`는 Code 쿼리에 정의/참조/구현/호출관계/심볼 의도가 보이면 `lsp_code_intel -> targeted file_read -> edit/verify` 순서를 더 앞세웁니다. `LspTool` 결과도 파일 수, 대표 위치, 첫 결과를 같이 요약해 `claude-code`의 LSP 결과 shaping에 더 가깝게 맞췄습니다.
+- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\` 경고 0 / 오류 0
+
## claude-code식 선택 탐색 우선순위 정렬
+- 업데이트: 2026-04-09 22:38 (KST)
+- AX에 이미 존재하던 `LspTool`/`LspClientService`를 `claude-code`의 `LSPTool` 수준에 더 가깝게 확장했습니다. 기존 `goto_definition`, `find_references`, `symbols` 외에 `hover`, `goto_implementation`, `workspace_symbols`, `prepare_call_hierarchy`, `incoming_calls`, `outgoing_calls`를 추가해 정의/참조/문서 심볼/워크스페이스 심볼/호출 계층을 모두 조회할 수 있게 했습니다.
+- `LspClientService`는 LSP initialize capability도 확장했습니다. 이제 `implementation`, `hover`, `callHierarchy`, `workspace/symbol` 요청을 직접 보낼 수 있고, `LocationLink`, hover contents, call hierarchy 결과를 AX용 단순 모델로 파싱합니다.
+- `AgentLoopExplorationPolicy`, `TaskTypePolicy`, `ChatWindow.SystemPromptBuilder`도 같이 조정해 Code 탭의 좁은 요청에서 `file_read`, `grep/glob`와 함께 `lsp_code_intel`이 자연스러운 선택지로 노출되도록 맞췄습니다. `claude-code`처럼 기본은 텍스트 탐색이되, 정의/참조/호출관계는 LSP를 더 우선적으로 쓰는 흐름입니다.
+
+- 업데이트: 2026-04-09 21:58 (KST)
+- `ModelExecutionProfileCatalog`의 기본 프로필을 다시 완화했습니다. `balanced`, `reasoning_first`는 이제 초기 도구 호출 강제와 post-tool verification을 기본으로 켜지 않고, 문서 검증 게이트와 diff/final-report 후속 게이트도 기본적으로 줄여 `claude-code`처럼 더 얇은 반복 구조를 따릅니다.
+- Cowork 문서 생성 완료 경로는 `AgentLoopTransitions.Documents`에서 한 번 더 정리했습니다. Code 탭이 아닌 경우 terminal document tool 성공 뒤 별도 post-tool verification 턴을 추가하지 않고 바로 완료 가능하도록 바꿔, 문서 생성 후 불필요한 재호출을 줄였습니다.
+- Code 완료 게이트도 `AgentLoopTransitions.Verification`에서 완화했습니다. 일반 수정은 `diff` 또는 최근 build/test 같은 가벼운 완료 증거만 있어도 마무리 가능하게 하고, build/test 강제는 고영향 수정일 때 중심으로 남겨 `claude-code`의 얇은 code loop에 더 가깝게 맞췄습니다.
+- `ChatWindow.SystemPromptBuilder`의 Cowork/Code 프롬프트도 같은 방향으로 손봤습니다. “모든 응답은 도구 호출 필수” 식의 과한 강제 표현을 줄이고, 필요한 경우에만 즉시 도구를 쓰되 불필요한 도구 호출은 강제하지 않도록 완화했습니다.
+
+- 업데이트: 2026-04-09 21:48 (KST)
+- Cowork 시스템 프롬프트에서 “불확실하면 먼저 파일을 찾아라” 성향을 더 줄였습니다. 이제 순수 문서 생성 요청은 `document_plan -> docx_create/html_create/...`를 먼저 타고, `glob/grep/document_read/folder_map`은 기존 자료 참조가 명시된 경우에만 먼저 쓰도록 유도합니다.
+- Code 시스템 프롬프트와 탐색 우선순위도 `claude-code`처럼 더 얇게 바꿨습니다. 기본 시작 흐름은 `specific file -> file_read`, 아니면 `grep/glob -> small targeted read`, 그 다음 `file_edit/file_write`이며, `build_run/test_loop`와 `git_tool(diff)`는 검증이 실제로 필요할 때 붙는 구조로 정리했습니다.
+- `TaskTypePolicy`의 bugfix/feature/refactor/review 기본 도구 순서도 같은 기준으로 완화해, AX Code가 과하게 `git diff/build/test`를 절차적으로 앞세우지 않도록 맞췄습니다.
+
- 업데이트: 2026-04-09 21:03 (KST)
- `StreamingToolExecutionCoordinator`에 `RetryReset` 이벤트를 추가해, 컨텍스트 복구나 일시적 LLM 오류 재시도 전에 부분 스트림 미리보기 상태를 끊도록 했습니다. `claude-code`가 fallback 시 orphaned partial state를 정리하고 executor를 새로 잡는 흐름과 비슷한 방향으로 AX도 재시도 경계를 더 명확히 가지게 됐습니다.
- 엔진 레벨 `PrefetchableReadOnlyTools`는 `file_read`/`document_read` 중심으로 다시 줄였습니다. `folder_map`, `glob`, `grep`, `multi_read`, `code_search` 같은 구조 탐색/광범위 검색 도구는 prefetch 대상에서 빼서, 탐색 정책과 실행 엔진의 우선순위가 서로 어긋나지 않도록 맞췄습니다.
diff --git a/src/AxCopilot/Services/Agent/AgentLoopExplorationPolicy.cs b/src/AxCopilot/Services/Agent/AgentLoopExplorationPolicy.cs
index 76fde97..0bec2fc 100644
--- a/src/AxCopilot/Services/Agent/AgentLoopExplorationPolicy.cs
+++ b/src/AxCopilot/Services/Agent/AgentLoopExplorationPolicy.cs
@@ -11,6 +11,8 @@ public partial class AgentLoopService
TopicBased,
RepoWide,
OpenEnded,
+ /// 문서 생성 요청 — 탐색 단계를 건너뛰고 바로 document_plan/생성 도구 사용.
+ DirectCreation,
}
private sealed class ExplorationTrackingState
@@ -24,6 +26,196 @@ public partial class AgentLoopService
public bool CorrectiveHintInjected { get; set; }
}
+ private static IReadOnlyCollection FilterExplorationToolsForCurrentIteration(
+ IReadOnlyCollection tools,
+ ExplorationTrackingState state,
+ string userQuery,
+ string? activeTab,
+ int totalToolCalls)
+ {
+ if (tools.Count == 0)
+ return tools;
+
+ // 문서 생성 모드: 생성 도구를 최우선, 탐색 도구를 뒤로 배치
+ if (state.Scope == ExplorationScope.DirectCreation)
+ {
+ var creationFirst = new List(tools.Count);
+ creationFirst.AddRange(tools.Where(IsDocumentCreationTool));
+ creationFirst.AddRange(tools.Where(t => !IsDocumentCreationTool(t)
+ && !string.Equals(t.Name, "folder_map", StringComparison.OrdinalIgnoreCase)
+ && !string.Equals(t.Name, "glob", StringComparison.OrdinalIgnoreCase)
+ && !string.Equals(t.Name, "grep", StringComparison.OrdinalIgnoreCase)));
+ // 탐색 도구는 마지막에 (필요 시에만 사용)
+ creationFirst.AddRange(tools.Where(t =>
+ string.Equals(t.Name, "glob", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(t.Name, "grep", StringComparison.OrdinalIgnoreCase)));
+ return creationFirst
+ .DistinctBy(t => t.Name, StringComparer.OrdinalIgnoreCase)
+ .ToList()
+ .AsReadOnly();
+ }
+
+ var allowFolderMap = ShouldAllowFolderMapForCurrentIteration(state, userQuery, activeTab, totalToolCalls);
+ var ordered = new List(tools.Count);
+
+ ordered.AddRange(tools.Where(IsSelectiveDiscoveryTool));
+ ordered.AddRange(tools.Where(t => !IsSelectiveDiscoveryTool(t) &&
+ (allowFolderMap || !string.Equals(t.Name, "folder_map", StringComparison.OrdinalIgnoreCase))));
+ if (allowFolderMap)
+ {
+ ordered.AddRange(tools.Where(t => string.Equals(t.Name, "folder_map", StringComparison.OrdinalIgnoreCase)));
+ }
+
+ return ordered
+ .DistinctBy(t => t.Name, StringComparer.OrdinalIgnoreCase)
+ .ToList()
+ .AsReadOnly();
+ }
+
+ private static bool IsDocumentCreationTool(IAgentTool tool)
+ {
+ return tool.Name is "document_plan" or "docx_create" or "html_create" or "excel_create"
+ or "markdown_create" or "csv_create" or "pptx_create" or "chart_create"
+ or "document_assemble" or "file_write";
+ }
+
+ private static bool ShouldAllowFolderMapForCurrentIteration(
+ ExplorationTrackingState state,
+ string userQuery,
+ string? activeTab,
+ int totalToolCalls)
+ {
+ // 문서 생성 모드에서는 folder_map 차단 — 탐색 없이 바로 생성
+ if (state.Scope == ExplorationScope.DirectCreation)
+ return false;
+
+ if (state.Scope is ExplorationScope.RepoWide or ExplorationScope.OpenEnded)
+ return true;
+
+ if (HasExplicitFolderIntent(userQuery))
+ return true;
+
+ if (string.Equals(activeTab, "Cowork", StringComparison.OrdinalIgnoreCase)
+ && IsExistingMaterialReferenceRequest(userQuery))
+ return true;
+
+ return totalToolCalls >= 2 && state.TotalFilesRead == 0 && state.FolderMapCalls == 0;
+ }
+
+ private static bool HasExplicitFolderIntent(string userQuery)
+ {
+ if (string.IsNullOrWhiteSpace(userQuery))
+ return false;
+
+ return ContainsAny(
+ userQuery,
+ "folder",
+ "directory",
+ "tree",
+ "structure",
+ "list files",
+ "workspace layout",
+ "work folder",
+ "project structure",
+ "폴더",
+ "디렉터리",
+ "폴더 구조",
+ "디렉터리 구조",
+ "파일 목록",
+ "작업 폴더",
+ "폴더 안",
+ "구조를 보여",
+ "구조 확인");
+ }
+
+ private static bool IsExistingMaterialReferenceRequest(string userQuery)
+ {
+ if (string.IsNullOrWhiteSpace(userQuery))
+ return false;
+
+ return ContainsAny(
+ userQuery,
+ "existing file",
+ "existing files",
+ "existing document",
+ "existing documents",
+ "reference files",
+ "reference docs",
+ "existing materials",
+ "기존 파일",
+ "기존 문서",
+ "기존 자료",
+ "참고 파일",
+ "참고 문서",
+ "폴더 내 자료",
+ "안의 자료",
+ "작업 폴더 파일");
+ }
+
+ private static bool IsSelectiveDiscoveryTool(IAgentTool tool)
+ {
+ return tool.Name is "glob" or "grep" or "file_read" or "document_read" or "multi_read" or "lsp_code_intel";
+ }
+
+ private static bool HasSemanticNavigationIntent(string userQuery)
+ {
+ if (string.IsNullOrWhiteSpace(userQuery))
+ return false;
+
+ return ContainsAny(
+ userQuery,
+ "definition",
+ "reference",
+ "references",
+ "implementation",
+ "implementations",
+ "caller",
+ "callers",
+ "callee",
+ "call hierarchy",
+ "symbol",
+ "symbols",
+ "interface",
+ "override",
+ "정의",
+ "참조",
+ "구현",
+ "호출부",
+ "호출 관계",
+ "심볼",
+ "인터페이스",
+ "오버라이드");
+ }
+
+ private static string BuildPreferredInitialToolSequence(
+ ExplorationTrackingState state,
+ TaskTypePolicy taskPolicy,
+ string? activeTab,
+ string userQuery)
+ {
+ // 문서 생성 의도가 감지되면 탐색 없이 바로 문서 계획/생성 도구 사용
+ if (state.Scope == ExplorationScope.DirectCreation)
+ return "document_plan -> docx_create/html_create/excel_create -> self-review";
+
+ if (HasExplicitFolderIntent(userQuery))
+ return "folder_map -> glob/grep -> targeted read";
+
+ if (string.Equals(taskPolicy.TaskType, "docs", StringComparison.OrdinalIgnoreCase))
+ return "glob/grep -> document_read/file_read -> drafting tool";
+
+ if (string.Equals(activeTab, "Code", StringComparison.OrdinalIgnoreCase)
+ && HasSemanticNavigationIntent(userQuery))
+ return "lsp_code_intel -> targeted file_read -> file_edit/file_write -> build_run/test_loop as needed";
+
+ if (string.Equals(activeTab, "Code", StringComparison.OrdinalIgnoreCase))
+ return "targeted file_read or grep/glob/lsp -> file_edit/file_write -> build_run/test_loop as needed";
+
+ if (state.Scope == ExplorationScope.TopicBased)
+ return "glob/grep -> targeted read -> next action";
+
+ return "glob/grep -> file_read/document_read -> next action";
+ }
+
private static ExplorationScope ClassifyExplorationScope(string userQuery, string? activeTab)
{
if (string.IsNullOrWhiteSpace(userQuery))
@@ -32,6 +224,11 @@ public partial class AgentLoopService
var q = userQuery.Trim();
var lower = q.ToLowerInvariant();
+ // Cowork 탭에서 문서 생성 의도가 있으면 탐색을 건너뛰고 바로 생성 도구 사용
+ if (!string.Equals(activeTab, "Code", StringComparison.OrdinalIgnoreCase)
+ && HasDocumentCreationIntent(lower))
+ return ExplorationScope.DirectCreation;
+
if (lower.Contains("전체") || lower.Contains("전반") || lower.Contains("코드베이스 전체") ||
lower.Contains("repo-wide") || lower.Contains("repository-wide") || lower.Contains("전체 구조") ||
lower.Contains("아키텍처") || lower.Contains("전체 점검"))
@@ -52,14 +249,47 @@ public partial class AgentLoopService
: ExplorationScope.OpenEnded;
}
+ ///
+ /// 사용자가 문서를 새로 생성/작성하려는 의도인지 판별합니다.
+ /// "보고서 작성해줘", "문서 만들어줘" 등 생성 동사가 포함된 경우 true.
+ ///
+ private static bool HasDocumentCreationIntent(string lowerQuery)
+ {
+ // 생성 동사 키워드
+ var hasCreationVerb = ContainsAny(lowerQuery,
+ "작성해", "써줘", "써 줘", "만들어", "생성해", "작성 해",
+ "만들어줘", "만들어 줘", "생성해줘", "생성해 줘",
+ "write", "create", "draft", "generate", "compose",
+ "작성하", "작성을", "생성하", "생성을",
+ "리포트 써", "보고서 써", "문서 써",
+ "작성 부탁", "만들어 부탁");
+
+ if (!hasCreationVerb)
+ return false;
+
+ // 생성 대상이 문서/보고서/자료 등인지 확인
+ return ContainsAny(lowerQuery,
+ "보고서", "문서", "제안서", "리포트", "분석서", "기획서",
+ "report", "document", "proposal", "analysis",
+ "요약서", "발표자료", "ppt", "pptx", "docx", "xlsx", "excel", "word",
+ "표", "차트", "스프레드시트", "프레젠테이션",
+ "정리해", "정리 해");
+ }
+
private static void InjectExplorationScopeGuidance(List messages, ExplorationScope scope)
{
var guidance = scope switch
{
+ ExplorationScope.DirectCreation =>
+ "Exploration scope = direct-creation. The user wants to CREATE a new document/report/file. " +
+ "Do NOT search for existing files with glob/grep/folder_map — skip exploration entirely. " +
+ "Call document_plan first to outline the document structure, then immediately call the appropriate creation tool " +
+ "(docx_create, html_create, excel_create, markdown_create, etc.) to produce the actual file. " +
+ "The output MUST be a real file on disk, not a text response.",
ExplorationScope.Localized =>
- "Exploration scope = localized. Start with grep/glob and targeted file reads. Avoid folder_map unless structure is unclear.",
+ "Exploration scope = localized. Start with lsp_code_intel when the request is about definitions/references/implementations/call hierarchy; otherwise use targeted file_read or grep/glob. Avoid folder_map unless the user explicitly asks for folder structure or file listing.",
ExplorationScope.TopicBased =>
- "Exploration scope = topic-based. Identify candidate files by topic keywords first, then read only a small targeted set.",
+ "Exploration scope = topic-based. Identify candidate files by topic keywords first with glob/grep, then read only a small targeted set.",
ExplorationScope.RepoWide =>
"Exploration scope = repo-wide. Broad structure inspection is allowed when needed.",
_ =>
@@ -93,6 +323,15 @@ public partial class AgentLoopService
string toolName,
string argsJson)
{
+ // 문서 생성 모드: 탐색 도구가 1회라도 호출되면 즉시 교정
+ if (state.Scope == ExplorationScope.DirectCreation)
+ {
+ return string.Equals(toolName, "folder_map", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(toolName, "glob", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(toolName, "grep", StringComparison.OrdinalIgnoreCase)
+ || state.TotalFilesRead >= 1;
+ }
+
if (state.Scope is ExplorationScope.RepoWide or ExplorationScope.OpenEnded)
return false;
diff --git a/src/AxCopilot/Services/Agent/AgentLoopService.cs b/src/AxCopilot/Services/Agent/AgentLoopService.cs
index cc1ea26..31d4f1d 100644
--- a/src/AxCopilot/Services/Agent/AgentLoopService.cs
+++ b/src/AxCopilot/Services/Agent/AgentLoopService.cs
@@ -255,6 +255,7 @@ public partial class AgentLoopService
"",
explorationState.Scope switch
{
+ ExplorationScope.DirectCreation => "문서 생성 모드 · 바로 문서를 만드는 중",
ExplorationScope.Localized => "국소 범위 탐색 · 관련 파일만 찾는 중",
ExplorationScope.TopicBased => "주제 범위 탐색 · 관련 후보만 추리는 중",
ExplorationScope.RepoWide => "저장소 범위 탐색 · 구조를 확인하는 중",
@@ -2832,7 +2833,7 @@ public partial class AgentLoopService
? testReviewLine +
"5. grep 또는 glob으로 공용 API, 호출부, 의존성 등록, 테스트 영향을 모두 다시 확인합니다.\n" +
"6. build_run으로 build와 test를 모두 실행해 통과 여부를 확인합니다.\n" +
- "7. 필요하면 spawn_agent로 호출부 분석이나 관련 테스트 탐색을 병렬 조사하게 하고, wait_agents로 결과를 통합합니다.\n" +
+ "7. 병렬 조사가 실제로 도움이 될 때만 spawn_agent로 호출부 분석이나 관련 테스트 탐색을 위임하고, wait_agents로 결과를 통합합니다.\n" +
"8. 문제가 발견되면 즉시 수정하고 다시 검증합니다.\n" +
"중요: 이 변경은 영향 범위가 넓을 가능성이 큽니다. build/test와 참조 검토가 모두 끝나기 전에는 마무리하지 마세요."
: testReviewLine +
@@ -2983,7 +2984,7 @@ public partial class AgentLoopService
var failureHint = BuildFailureTypeRecoveryHint(ClassifyFailureRecoveryKind(toolName, toolOutput), toolName);
var highImpactLine = highImpactChange
? "4. 공용 API, 인터페이스, DI 등록, 모델 계약, 호출부 전파 영향까지 반드시 확인합니다.\n" +
- "5. 필요하면 spawn_agent로 호출부/관련 테스트 조사를 병렬 실행해 근거를 보강합니다.\n" +
+ "5. 병렬 조사가 실제로 도움이 될 때만 spawn_agent로 호출부/관련 테스트 조사를 실행해 근거를 보강합니다.\n" +
"6. 관련 테스트가 없다면 테스트 부재 사실과 확인 근거를 남깁니다.\n" +
"7. 근본 원인을 수정한 뒤 build/test를 다시 실행합니다.\n" +
"중요: 이 변경은 영향 범위가 넓을 수 있으므로 build와 test 둘 다 확인하기 전에는 종료하지 마세요."
@@ -3859,7 +3860,9 @@ public partial class AgentLoopService
$"- 실패 도구: {unknownToolName}\n" +
aliasHint +
$"- 사용 가능한 도구 예시: {string.Join(", ", suggestions)}\n" +
- "- 도구가 애매하면 먼저 tool_search를 호출해 정확한 이름을 찾으세요.\n" +
+ (string.IsNullOrEmpty(aliasHint)
+ ? "- 위 후보만으로도 충분히 선택 가능하면 바로 그 도구를 호출하세요. 그래도 도구가 애매할 때만 tool_search를 사용하세요.\n"
+ : "- 자동 매핑 후보가 맞으면 바로 그 도구를 호출하세요. 정말 모호할 때만 tool_search를 사용하세요.\n") +
"위 목록에서 실제 존재하는 도구 하나를 골라 다시 호출하세요. 같은 미등록 도구를 반복 호출하지 마세요.";
}
@@ -3879,7 +3882,7 @@ public partial class AgentLoopService
$"- 요청 도구: {requestedToolName}\n" +
policyLine +
$"- 지금 사용 가능한 도구 예시: {activePreview}\n" +
- "- 도구 선택이 모호하면 tool_search로 허용 가능한 대체 도구를 먼저 찾으세요.\n" +
+ "- 위 허용 도구 예시로 바로 대체할 수 있으면 즉시 호출하세요. 그래도 도구 선택이 모호할 때만 tool_search를 사용하세요.\n" +
"허용 목록에서 대체 도구를 선택해 다시 호출하세요. 동일한 비허용 도구 재호출은 금지합니다.";
}
@@ -3893,7 +3896,7 @@ public partial class AgentLoopService
$"- 반복 횟수: {repeatedUnknownToolCount}\n" +
$"- 실패 도구: {unknownToolName}\n" +
$"- 현재 사용 가능한 도구 예시: {preview}\n" +
- "- 다음 실행에서는 tool_search로 도구명을 확인한 뒤 위 목록의 실제 도구 이름으로 호출하세요.";
+ "- 다음 실행에서는 먼저 위 목록의 실제 도구 이름을 직접 선택하고, 그래도 애매할 때만 tool_search를 사용하세요.";
}
private static string BuildDisallowedToolLoopAbortResponse(
@@ -3906,7 +3909,7 @@ public partial class AgentLoopService
$"- 반복 횟수: {repeatedCount}\n" +
$"- 비허용 도구: {toolName}\n" +
$"- 현재 사용 가능한 도구 예시: {preview}\n" +
- "- 다음 실행에서는 tool_search로 허용 도구를 확인하고 계획을 수정하세요.";
+ "- 다음 실행에서는 허용 도구 예시에서 직접 고를 수 있으면 바로 바꾸고, 그래도 애매할 때만 tool_search를 사용하세요.";
}
private static readonly Dictionary ToolAliasMap = new(StringComparer.OrdinalIgnoreCase)
diff --git a/src/AxCopilot/Services/Agent/AgentLoopTransitions.Execution.cs b/src/AxCopilot/Services/Agent/AgentLoopTransitions.Execution.cs
index 0079510..bd28585 100644
--- a/src/AxCopilot/Services/Agent/AgentLoopTransitions.Execution.cs
+++ b/src/AxCopilot/Services/Agent/AgentLoopTransitions.Execution.cs
@@ -300,7 +300,7 @@ public partial class AgentLoopService
};
var verificationTools = new HashSet(StringComparer.OrdinalIgnoreCase)
{
- "file_read", "document_read", "document_review"
+ "file_read", "document_read"
};
var sawVerification = false;
@@ -421,7 +421,7 @@ public partial class AgentLoopService
}
private bool TryHandleRepeatedFailureGuardTransition(
- LlmService.ContentBlock call,
+ ContentBlock call,
string toolCallSignature,
List messages,
string? lastFailedToolSignature,
@@ -463,7 +463,7 @@ public partial class AgentLoopService
}
private bool TryHandleNoProgressReadOnlyLoopTransition(
- LlmService.ContentBlock call,
+ ContentBlock call,
string toolCallSignature,
int repeatedSameSignatureCount,
List messages,
@@ -911,22 +911,22 @@ public partial class AgentLoopService
}
}
- private async Task TryPrefetchReadOnlyToolAsync(
- LlmService.ContentBlock block,
+ private async Task TryPrefetchReadOnlyToolAsync(
+ ContentBlock block,
IReadOnlyCollection tools,
AgentContext context,
CancellationToken ct)
=> await _toolExecutionCoordinator.TryPrefetchReadOnlyToolAsync(block, tools, context, ct);
- private async Task> SendWithToolsWithRecoveryAsync(
+ private async Task> SendWithToolsWithRecoveryAsync(
List messages,
IReadOnlyCollection tools,
CancellationToken ct,
string phaseLabel,
RunState? runState = null,
bool forceToolCall = false,
- Func>? prefetchToolCallAsync = null,
- Func? onStreamEventAsync = null)
+ Func>? prefetchToolCallAsync = null,
+ Func? onStreamEventAsync = null)
=> await _toolExecutionCoordinator.SendWithToolsWithRecoveryAsync(
messages,
tools,
@@ -938,7 +938,7 @@ public partial class AgentLoopService
onStreamEventAsync);
private void ApplyToolPostExecutionBookkeeping(
- LlmService.ContentBlock call,
+ ContentBlock call,
ToolResult result,
TokenUsage? tokenUsage,
Models.LlmSettings llm,
@@ -986,7 +986,7 @@ public partial class AgentLoopService
}
private bool TryHandleToolFailureTransition(
- LlmService.ContentBlock call,
+ ContentBlock call,
ToolResult result,
AgentContext context,
TaskTypePolicy taskPolicy,
@@ -1120,7 +1120,7 @@ public partial class AgentLoopService
}
private async Task<(bool ShouldContinue, string? TerminalResponse)> TryHandleUserDecisionTransitionsAsync(
- LlmService.ContentBlock call,
+ ContentBlock call,
AgentContext context,
List messages)
{
@@ -1193,4 +1193,3 @@ public partial class AgentLoopService
}
-
diff --git a/src/AxCopilot/Services/Agent/LspTool.cs b/src/AxCopilot/Services/Agent/LspTool.cs
index 75add39..ed3005b 100644
--- a/src/AxCopilot/Services/Agent/LspTool.cs
+++ b/src/AxCopilot/Services/Agent/LspTool.cs
@@ -5,18 +5,24 @@ namespace AxCopilot.Services.Agent;
///
/// LSP 기반 코드 인텔리전스 도구.
-/// 정의 이동(goto_definition), 참조 검색(find_references), 심볼 목록(symbols) 3가지 액션을 제공합니다.
+/// 정의 이동, 참조 검색, hover, 구현 위치, 심볼 검색, 호출 계층 등 구조적 코드 탐색을 제공합니다.
///
public class LspTool : IAgentTool, IDisposable
{
public string Name => "lsp_code_intel";
public string Description =>
- "코드 인텔리전스 도구. 정의 이동, 참조 검색, 심볼 목록을 제공합니다.\n" +
- "- action=\"goto_definition\": 심볼의 정의 위치를 찾습니다 (파일, 라인, 컬럼)\n" +
+ "코드 인텔리전스 도구. 정의, 참조, hover, 구현 위치, 문서/워크스페이스 심볼, 호출 계층을 제공합니다.\n" +
+ "- action=\"goto_definition\": 심볼의 정의 위치를 찾습니다\n" +
"- action=\"find_references\": 심볼이 사용된 모든 위치를 찾습니다\n" +
- "- action=\"symbols\": 파일 내 모든 심볼(클래스, 메서드, 필드 등)을 나열합니다\n" +
- "file_path, line, character 파라미터가 필요합니다 (line과 character는 0-based).";
+ "- action=\"hover\": 심볼의 타입/문서 정보를 가져옵니다\n" +
+ "- action=\"goto_implementation\": 인터페이스/추상 멤버의 구현 위치를 찾습니다\n" +
+ "- action=\"symbols\": 파일 내 모든 심볼을 나열합니다\n" +
+ "- action=\"workspace_symbols\": 워크스페이스 전체 심볼을 검색합니다 (query 권장)\n" +
+ "- action=\"prepare_call_hierarchy\": 현재 위치의 호출 계층 기준 심볼을 확인합니다\n" +
+ "- action=\"incoming_calls\": 현재 심볼을 호출하는 상위 호출자를 찾습니다\n" +
+ "- action=\"outgoing_calls\": 현재 심볼이 호출하는 하위 호출 대상을 찾습니다\n" +
+ "line/character는 기본적으로 1-based 입력을 기대하며, 0-based 값도 호환 처리합니다.";
public ToolParameterSchema Parameters => new()
{
@@ -25,23 +31,39 @@ public class LspTool : IAgentTool, IDisposable
["action"] = new ToolProperty
{
Type = "string",
- Description = "수행할 작업: goto_definition | find_references | symbols",
- Enum = new() { "goto_definition", "find_references", "symbols" }
+ Description = "수행할 작업",
+ Enum = new()
+ {
+ "goto_definition",
+ "find_references",
+ "hover",
+ "goto_implementation",
+ "symbols",
+ "workspace_symbols",
+ "prepare_call_hierarchy",
+ "incoming_calls",
+ "outgoing_calls"
+ }
},
["file_path"] = new ToolProperty
{
Type = "string",
- Description = "대상 파일 경로 (절대 또는 작업 폴더 기준 상대 경로)"
+ Description = "대상 파일 경로 (절대 또는 작업 폴더 기준 상대 경로). workspace_symbols에서는 작업 폴더 기준 힌트로만 사용 가능."
},
["line"] = new ToolProperty
{
Type = "integer",
- Description = "대상 라인 번호 (0-based). symbols 액션에서는 불필요."
+ Description = "대상 라인 번호. 기본 1-based 입력을 기대하며 내부에서 0-based로 변환합니다. symbols/workspace_symbols에서는 불필요."
},
["character"] = new ToolProperty
{
Type = "integer",
- Description = "라인 내 문자 위치 (0-based). symbols 액션에서는 불필요."
+ Description = "라인 내 문자 위치. 기본 1-based 입력을 기대하며 내부에서 0-based로 변환합니다. symbols/workspace_symbols에서는 불필요."
+ },
+ ["query"] = new ToolProperty
+ {
+ Type = "string",
+ Description = "workspace_symbols에서 사용할 심볼 검색어. 비어 있으면 file_path의 파일명/심볼 힌트를 사용합니다."
},
},
Required = new() { "action", "file_path" }
@@ -59,8 +81,9 @@ public class LspTool : IAgentTool, IDisposable
var action = args.SafeTryGetProperty("action", out var a) ? a.SafeGetString() ?? "" : "";
var filePath = args.SafeTryGetProperty("file_path", out var f) ? f.SafeGetString() ?? "" : "";
- var line = args.SafeTryGetProperty("line", out var l) ? l.GetInt32() : 0;
- var character = args.SafeTryGetProperty("character", out var ch) ? ch.GetInt32() : 0;
+ var line = args.SafeTryGetProperty("line", out var l) ? NormalizePosition(l.SafeGetInt32()) : 0;
+ var character = args.SafeTryGetProperty("character", out var ch) ? NormalizePosition(ch.SafeGetInt32()) : 0;
+ var query = args.SafeTryGetProperty("query", out var q) ? q.SafeGetString() ?? "" : "";
if (string.IsNullOrEmpty(filePath))
return ToolResult.Fail("file_path가 필요합니다.");
@@ -88,8 +111,14 @@ public class LspTool : IAgentTool, IDisposable
{
"goto_definition" => await GotoDefinitionAsync(client, filePath, line, character, ct),
"find_references" => await FindReferencesAsync(client, filePath, line, character, ct),
+ "hover" => await HoverAsync(client, filePath, line, character, ct),
+ "goto_implementation" => await GotoImplementationAsync(client, filePath, line, character, ct),
"symbols" => await GetSymbolsAsync(client, filePath, ct),
- _ => ToolResult.Fail($"알 수 없는 액션: {action}. goto_definition | find_references | symbols 중 선택하세요.")
+ "workspace_symbols" => await GetWorkspaceSymbolsAsync(client, filePath, query, ct),
+ "prepare_call_hierarchy" => await PrepareCallHierarchyAsync(client, filePath, line, character, ct),
+ "incoming_calls" => await GetIncomingCallsAsync(client, filePath, line, character, ct),
+ "outgoing_calls" => await GetOutgoingCallsAsync(client, filePath, line, character, ct),
+ _ => ToolResult.Fail("알 수 없는 action입니다. goto_definition | find_references | hover | goto_implementation | symbols | workspace_symbols | prepare_call_hierarchy | incoming_calls | outgoing_calls 중 선택하세요.")
};
}
catch (Exception ex)
@@ -118,13 +147,44 @@ public class LspTool : IAgentTool, IDisposable
return ToolResult.Ok("참조를 찾을 수 없습니다.");
var sb = new System.Text.StringBuilder();
- sb.AppendLine($"총 {locations.Count}개 참조:");
+ sb.AppendLine($"총 {locations.Count}개 참조");
+ sb.AppendLine($"파일 수: {locations.Select(l => l.FilePath).Distinct(StringComparer.OrdinalIgnoreCase).Count()}");
+ sb.AppendLine($"첫 참조: {locations[0]}");
+ sb.AppendLine();
foreach (var loc in locations.Take(30))
sb.AppendLine($" {loc}");
if (locations.Count > 30)
sb.AppendLine($" ... 외 {locations.Count - 30}개");
- return ToolResult.Ok(sb.ToString());
+ return ToolResult.Ok(sb.ToString(), locations[0].FilePath);
+ }
+
+ private async Task HoverAsync(LspClientService client, string filePath, int line, int character, CancellationToken ct)
+ {
+ var hover = await client.HoverAsync(filePath, line, character, ct);
+ if (string.IsNullOrWhiteSpace(hover))
+ return ToolResult.Ok("hover 정보를 찾을 수 없습니다.");
+
+ return ToolResult.Ok($"Hover 정보\n위치: {Path.GetFileName(filePath)}:{line + 1}:{character + 1}\n\n{hover}", filePath);
+ }
+
+ private async Task GotoImplementationAsync(LspClientService client, string filePath, int line, int character, CancellationToken ct)
+ {
+ var locations = await client.GotoImplementationAsync(filePath, line, character, ct);
+ if (locations.Count == 0)
+ return ToolResult.Ok("구현 위치를 찾을 수 없습니다.");
+
+ var sb = new System.Text.StringBuilder();
+ sb.AppendLine($"총 {locations.Count}개 구현 위치");
+ sb.AppendLine($"파일 수: {locations.Select(l => l.FilePath).Distinct(StringComparer.OrdinalIgnoreCase).Count()}");
+ sb.AppendLine($"첫 구현: {locations[0]}");
+ sb.AppendLine();
+ foreach (var loc in locations.Take(20))
+ sb.AppendLine($" {loc}");
+ if (locations.Count > 20)
+ sb.AppendLine($" ... 외 {locations.Count - 20}개");
+
+ return ToolResult.Ok(sb.ToString(), locations[0].FilePath);
}
private async Task GetSymbolsAsync(LspClientService client, string filePath, CancellationToken ct)
@@ -134,13 +194,92 @@ public class LspTool : IAgentTool, IDisposable
return ToolResult.Ok("심볼을 찾을 수 없습니다.");
var sb = new System.Text.StringBuilder();
- sb.AppendLine($"총 {symbols.Count}개 심볼:");
+ sb.AppendLine($"총 {symbols.Count}개 심볼");
+ sb.AppendLine($"파일: {Path.GetFileName(filePath)}");
+ sb.AppendLine();
foreach (var sym in symbols)
sb.AppendLine($" {sym}");
return ToolResult.Ok(sb.ToString());
}
+ private async Task GetWorkspaceSymbolsAsync(LspClientService client, string filePath, string query, CancellationToken ct)
+ {
+ var fallbackQuery = !string.IsNullOrWhiteSpace(query)
+ ? query
+ : Path.GetFileNameWithoutExtension(filePath);
+ var symbols = await client.SearchWorkspaceSymbolsAsync(fallbackQuery, ct);
+ if (symbols.Count == 0)
+ return ToolResult.Ok($"워크스페이스 심볼을 찾을 수 없습니다. query=\"{fallbackQuery}\"");
+
+ var sb = new System.Text.StringBuilder();
+ sb.AppendLine($"query=\"{fallbackQuery}\" 결과 {symbols.Count}개");
+ sb.AppendLine($"파일 수: {symbols.Select(s => s.Location?.FilePath).Where(p => !string.IsNullOrWhiteSpace(p)).Distinct(StringComparer.OrdinalIgnoreCase).Count()}");
+ if (symbols.FirstOrDefault() is { } firstSymbol)
+ sb.AppendLine($"첫 결과: {firstSymbol}");
+ sb.AppendLine();
+ foreach (var sym in symbols.Take(30))
+ sb.AppendLine($" {sym}");
+ if (symbols.Count > 30)
+ sb.AppendLine($" ... 외 {symbols.Count - 30}개");
+
+ return ToolResult.Ok(sb.ToString(), symbols.FirstOrDefault(s => s.Location != null)?.Location?.FilePath);
+ }
+
+ private async Task PrepareCallHierarchyAsync(LspClientService client, string filePath, int line, int character, CancellationToken ct)
+ {
+ var items = await client.PrepareCallHierarchyAsync(filePath, line, character, ct);
+ if (items.Count == 0)
+ return ToolResult.Ok("호출 계층 기준 심볼을 찾을 수 없습니다.");
+
+ var sb = new System.Text.StringBuilder();
+ sb.AppendLine($"호출 계층 기준 {items.Count}개");
+ sb.AppendLine($"대표 심볼: {items[0]}");
+ sb.AppendLine();
+ foreach (var item in items.Take(10))
+ sb.AppendLine($" {item}");
+ if (items.Count > 10)
+ sb.AppendLine($" ... 외 {items.Count - 10}개");
+
+ return ToolResult.Ok(sb.ToString(), items[0].Location.FilePath);
+ }
+
+ private async Task GetIncomingCallsAsync(LspClientService client, string filePath, int line, int character, CancellationToken ct)
+ {
+ var calls = await client.GetIncomingCallsAsync(filePath, line, character, ct);
+ if (calls.Count == 0)
+ return ToolResult.Ok("incoming call을 찾을 수 없습니다.");
+
+ var sb = new System.Text.StringBuilder();
+ sb.AppendLine($"상위 호출자 {calls.Count}개");
+ sb.AppendLine($"대표 호출자: {calls[0]}");
+ sb.AppendLine();
+ foreach (var call in calls.Take(20))
+ sb.AppendLine($" {call}");
+ if (calls.Count > 20)
+ sb.AppendLine($" ... 외 {calls.Count - 20}개");
+
+ return ToolResult.Ok(sb.ToString(), calls[0].Location.FilePath);
+ }
+
+ private async Task GetOutgoingCallsAsync(LspClientService client, string filePath, int line, int character, CancellationToken ct)
+ {
+ var calls = await client.GetOutgoingCallsAsync(filePath, line, character, ct);
+ if (calls.Count == 0)
+ return ToolResult.Ok("outgoing call을 찾을 수 없습니다.");
+
+ var sb = new System.Text.StringBuilder();
+ sb.AppendLine($"하위 호출 대상 {calls.Count}개");
+ sb.AppendLine($"첫 호출 대상: {calls[0]}");
+ sb.AppendLine();
+ foreach (var call in calls.Take(20))
+ sb.AppendLine($" {call}");
+ if (calls.Count > 20)
+ sb.AppendLine($" ... 외 {calls.Count - 20}개");
+
+ return ToolResult.Ok(sb.ToString(), calls[0].Location.FilePath);
+ }
+
private async Task GetOrCreateClientAsync(string language, string workFolder, CancellationToken ct)
{
if (_clients.TryGetValue(language, out var existing) && existing.IsConnected)
@@ -173,6 +312,14 @@ public class LspTool : IAgentTool, IDisposable
};
}
+ private static int NormalizePosition(int value)
+ {
+ if (value <= 0)
+ return 0;
+
+ return value - 1;
+ }
+
public void Dispose()
{
foreach (var client in _clients.Values)
diff --git a/src/AxCopilot/Services/LlmService.ToolUse.cs b/src/AxCopilot/Services/LlmService.ToolUse.cs
index 17b603f..fc73795 100644
--- a/src/AxCopilot/Services/LlmService.ToolUse.cs
+++ b/src/AxCopilot/Services/LlmService.ToolUse.cs
@@ -870,59 +870,28 @@ public partial class LlmService
{
if (m.Role == "system") continue;
- // tool_result → OpenAI role:"tool" 형식 (watsonx /text/chat 지원)
+ // IBM/Qwen 배포형은 과거 tool_calls/tool 메시지 이력을 엄격하게 검사하는 경우가 있어
+ // 이전 tool-use 대화는 평탄한 transcript로 재구성한다.
if (m.Role == "user" && m.Content.StartsWith("{\"type\":\"tool_result\""))
{
try
{
using var doc = JsonDocument.Parse(m.Content);
var root = doc.RootElement;
- msgs.Add(new
- {
- role = "tool",
- tool_call_id = root.GetProperty("tool_use_id").SafeGetString(),
- content = root.GetProperty("content").SafeGetString(),
- });
+ msgs.Add(new { role = "user", content = BuildIbmToolResultTranscript(root) });
continue;
}
catch { }
}
- // _tool_use_blocks → OpenAI tool_calls 형식
+ // _tool_use_blocks도 assistant + tool_calls로 다시 보내지 않고 plain assistant transcript로 평탄화
if (m.Role == "assistant" && m.Content.StartsWith("{\"_tool_use_blocks\""))
{
try
{
using var doc = JsonDocument.Parse(m.Content);
var blocksArr = doc.RootElement.GetProperty("_tool_use_blocks");
- var textContent = "";
- var toolCallsList = new List