AX Agent 코워크·코드 후속 호환 과제 정리 및 IBM Qwen 도구호출 경로 보강

이번 커밋은 claude-code 기준 후속 과제를 이어서 반영해 Cowork/Code의 도구 선택 강도와 IBM 배포형 vLLM(Qwen) 호환 경로를 정리했다.

핵심 변경 사항:

- IBM 전용 tool body에서 과거 assistant tool_calls 및 role=tool 이력을 OpenAI 형식으로 재전송하지 않고 평탄한 transcript로 직렬화하도록 변경

- Cowork 프롬프트에서 document_review 및 format_convert를 기본 단계처럼 강제하지 않고 file_read/document_read 중심의 가벼운 검증 흐름으로 완화

- unknown/disallowed tool recovery에서 tool_search를 항상 강제하지 않고 alias 후보나 활성 도구 예시로 바로 선택 가능하면 직접 사용하도록 조정

- Code 탐색에서 정의/참조/구현/호출관계 의도는 lsp_code_intel을 더 우선하도록 보강하고 LSP 결과 요약 품질 개선

- 문서 검증 근거는 file_read/document_read면 충분하도록 단순화

문서 반영:

- README.md, docs/DEVELOPMENT.md에 2026-04-09 22:48 (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:
2026-04-09 22:50:27 +09:00
parent 3c6d2f1ce4
commit 1fab215344
8 changed files with 967 additions and 72 deletions

View File

@@ -11,6 +11,8 @@ public partial class AgentLoopService
TopicBased,
RepoWide,
OpenEnded,
/// <summary>문서 생성 요청 — 탐색 단계를 건너뛰고 바로 document_plan/생성 도구 사용.</summary>
DirectCreation,
}
private sealed class ExplorationTrackingState
@@ -24,6 +26,196 @@ public partial class AgentLoopService
public bool CorrectiveHintInjected { get; set; }
}
private static IReadOnlyCollection<IAgentTool> FilterExplorationToolsForCurrentIteration(
IReadOnlyCollection<IAgentTool> tools,
ExplorationTrackingState state,
string userQuery,
string? activeTab,
int totalToolCalls)
{
if (tools.Count == 0)
return tools;
// 문서 생성 모드: 생성 도구를 최우선, 탐색 도구를 뒤로 배치
if (state.Scope == ExplorationScope.DirectCreation)
{
var creationFirst = new List<IAgentTool>(tools.Count);
creationFirst.AddRange(tools.Where(IsDocumentCreationTool));
creationFirst.AddRange(tools.Where(t => !IsDocumentCreationTool(t)
&& !string.Equals(t.Name, "folder_map", StringComparison.OrdinalIgnoreCase)
&& !string.Equals(t.Name, "glob", StringComparison.OrdinalIgnoreCase)
&& !string.Equals(t.Name, "grep", StringComparison.OrdinalIgnoreCase)));
// 탐색 도구는 마지막에 (필요 시에만 사용)
creationFirst.AddRange(tools.Where(t =>
string.Equals(t.Name, "glob", StringComparison.OrdinalIgnoreCase)
|| string.Equals(t.Name, "grep", StringComparison.OrdinalIgnoreCase)));
return creationFirst
.DistinctBy(t => t.Name, StringComparer.OrdinalIgnoreCase)
.ToList()
.AsReadOnly();
}
var allowFolderMap = ShouldAllowFolderMapForCurrentIteration(state, userQuery, activeTab, totalToolCalls);
var ordered = new List<IAgentTool>(tools.Count);
ordered.AddRange(tools.Where(IsSelectiveDiscoveryTool));
ordered.AddRange(tools.Where(t => !IsSelectiveDiscoveryTool(t) &&
(allowFolderMap || !string.Equals(t.Name, "folder_map", StringComparison.OrdinalIgnoreCase))));
if (allowFolderMap)
{
ordered.AddRange(tools.Where(t => string.Equals(t.Name, "folder_map", StringComparison.OrdinalIgnoreCase)));
}
return ordered
.DistinctBy(t => t.Name, StringComparer.OrdinalIgnoreCase)
.ToList()
.AsReadOnly();
}
private static bool IsDocumentCreationTool(IAgentTool tool)
{
return tool.Name is "document_plan" or "docx_create" or "html_create" or "excel_create"
or "markdown_create" or "csv_create" or "pptx_create" or "chart_create"
or "document_assemble" or "file_write";
}
private static bool ShouldAllowFolderMapForCurrentIteration(
ExplorationTrackingState state,
string userQuery,
string? activeTab,
int totalToolCalls)
{
// 문서 생성 모드에서는 folder_map 차단 — 탐색 없이 바로 생성
if (state.Scope == ExplorationScope.DirectCreation)
return false;
if (state.Scope is ExplorationScope.RepoWide or ExplorationScope.OpenEnded)
return true;
if (HasExplicitFolderIntent(userQuery))
return true;
if (string.Equals(activeTab, "Cowork", StringComparison.OrdinalIgnoreCase)
&& IsExistingMaterialReferenceRequest(userQuery))
return true;
return totalToolCalls >= 2 && state.TotalFilesRead == 0 && state.FolderMapCalls == 0;
}
private static bool HasExplicitFolderIntent(string userQuery)
{
if (string.IsNullOrWhiteSpace(userQuery))
return false;
return ContainsAny(
userQuery,
"folder",
"directory",
"tree",
"structure",
"list files",
"workspace layout",
"work folder",
"project structure",
"폴더",
"디렉터리",
"폴더 구조",
"디렉터리 구조",
"파일 목록",
"작업 폴더",
"폴더 안",
"구조를 보여",
"구조 확인");
}
private static bool IsExistingMaterialReferenceRequest(string userQuery)
{
if (string.IsNullOrWhiteSpace(userQuery))
return false;
return ContainsAny(
userQuery,
"existing file",
"existing files",
"existing document",
"existing documents",
"reference files",
"reference docs",
"existing materials",
"기존 파일",
"기존 문서",
"기존 자료",
"참고 파일",
"참고 문서",
"폴더 내 자료",
"안의 자료",
"작업 폴더 파일");
}
private static bool IsSelectiveDiscoveryTool(IAgentTool tool)
{
return tool.Name is "glob" or "grep" or "file_read" or "document_read" or "multi_read" or "lsp_code_intel";
}
private static bool HasSemanticNavigationIntent(string userQuery)
{
if (string.IsNullOrWhiteSpace(userQuery))
return false;
return ContainsAny(
userQuery,
"definition",
"reference",
"references",
"implementation",
"implementations",
"caller",
"callers",
"callee",
"call hierarchy",
"symbol",
"symbols",
"interface",
"override",
"정의",
"참조",
"구현",
"호출부",
"호출 관계",
"심볼",
"인터페이스",
"오버라이드");
}
private static string BuildPreferredInitialToolSequence(
ExplorationTrackingState state,
TaskTypePolicy taskPolicy,
string? activeTab,
string userQuery)
{
// 문서 생성 의도가 감지되면 탐색 없이 바로 문서 계획/생성 도구 사용
if (state.Scope == ExplorationScope.DirectCreation)
return "document_plan -> docx_create/html_create/excel_create -> self-review";
if (HasExplicitFolderIntent(userQuery))
return "folder_map -> glob/grep -> targeted read";
if (string.Equals(taskPolicy.TaskType, "docs", StringComparison.OrdinalIgnoreCase))
return "glob/grep -> document_read/file_read -> drafting tool";
if (string.Equals(activeTab, "Code", StringComparison.OrdinalIgnoreCase)
&& HasSemanticNavigationIntent(userQuery))
return "lsp_code_intel -> targeted file_read -> file_edit/file_write -> build_run/test_loop as needed";
if (string.Equals(activeTab, "Code", StringComparison.OrdinalIgnoreCase))
return "targeted file_read or grep/glob/lsp -> file_edit/file_write -> build_run/test_loop as needed";
if (state.Scope == ExplorationScope.TopicBased)
return "glob/grep -> targeted read -> next action";
return "glob/grep -> file_read/document_read -> next action";
}
private static ExplorationScope ClassifyExplorationScope(string userQuery, string? activeTab)
{
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;
}
/// <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 +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;

View File

@@ -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<string, string> ToolAliasMap = new(StringComparer.OrdinalIgnoreCase)

View File

@@ -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;
@@ -421,7 +421,7 @@ public partial class AgentLoopService
}
private bool TryHandleRepeatedFailureGuardTransition(
LlmService.ContentBlock call,
ContentBlock call,
string toolCallSignature,
List<ChatMessage> messages,
string? lastFailedToolSignature,
@@ -463,7 +463,7 @@ public partial class AgentLoopService
}
private bool TryHandleNoProgressReadOnlyLoopTransition(
LlmService.ContentBlock call,
ContentBlock call,
string toolCallSignature,
int repeatedSameSignatureCount,
List<ChatMessage> messages,
@@ -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,
@@ -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)
{
@@ -1193,4 +1193,3 @@ public partial class AgentLoopService
}

View File

@@ -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)

View File

@@ -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<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 { }
@@ -999,6 +968,60 @@ public partial class LlmService
};
}
private static string BuildIbmAssistantTranscript(JsonElement blocksArr)
{
var textSegments = new List<string>();
var toolSegments = new List<string>();
foreach (var block in blocksArr.EnumerateArray())
{
var blockType = block.GetProperty("type").SafeGetString();
if (string.Equals(blockType, "text", StringComparison.OrdinalIgnoreCase))
{
var text = block.SafeTryGetProperty("text", out var textEl) ? textEl.SafeGetString() ?? "" : "";
if (!string.IsNullOrWhiteSpace(text))
textSegments.Add(text.Trim());
continue;
}
if (!string.Equals(blockType, "tool_use", StringComparison.OrdinalIgnoreCase))
continue;
var name = block.SafeTryGetProperty("name", out var nameEl) ? nameEl.SafeGetString() ?? "" : "";
var args = block.SafeTryGetProperty("input", out var inputEl) ? inputEl.GetRawText() : "{}";
if (string.IsNullOrWhiteSpace(name))
continue;
toolSegments.Add($"<tool_call>\n{{\"name\":\"{name}\",\"arguments\":{args}}}\n</tool_call>");
}
var parts = new List<string>();
if (textSegments.Count > 0)
parts.Add(string.Join("\n\n", textSegments));
if (toolSegments.Count > 0)
parts.Add(string.Join("\n", toolSegments));
return parts.Count == 0
? "<tool_call>\n{\"name\":\"unknown_tool\",\"arguments\":{}}\n</tool_call>"
: string.Join("\n\n", parts);
}
private static string BuildIbmToolResultTranscript(JsonElement root)
{
var toolCallId = root.SafeTryGetProperty("tool_use_id", out var idEl) ? idEl.SafeGetString() ?? "" : "";
var toolName = root.SafeTryGetProperty("tool_name", out var nameEl) ? nameEl.SafeGetString() ?? "" : "";
var content = root.SafeTryGetProperty("content", out var contentEl) ? contentEl.SafeGetString() ?? "" : "";
var header = string.IsNullOrWhiteSpace(toolName)
? "[Tool Result]"
: $"[Tool Result: {toolName}]";
if (!string.IsNullOrWhiteSpace(toolCallId))
header += $" (id={toolCallId})";
return string.IsNullOrWhiteSpace(content)
? $"{header}\n(no output)"
: $"{header}\n{content}";
}
private sealed class ToolCallAccumulator
{
public int Index { get; init; }

View File

@@ -0,0 +1,435 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using AxCopilot.Models;
using AxCopilot.Services;
namespace AxCopilot.Views;
public partial class ChatWindow
{
// ─── 코워크 에이전트 지원 ────────────────────────────────────────────
private string BuildCoworkSystemPrompt()
{
var workFolder = GetCurrentWorkFolder();
var llm = _settings.Settings.Llm;
var sb = new System.Text.StringBuilder();
// ══════════════════════════════════════════════════════════════════
// 도구 호출 절대 규칙을 시스템 프롬프트 맨 첫 줄에 배치 — 최대 강제력
// ══════════════════════════════════════════════════════════════════
sb.AppendLine("## [절대 규칙] 도구 우선 — USE TOOLS WHEN THEY MATERIALLY ADVANCE THE TASK");
sb.AppendLine("이 규칙들은 다른 모든 지시보다 우선합니다. 위반 시 작업이 즉시 실패합니다.");
sb.AppendLine("Treat these as operating guidance, not as a requirement to force unnecessary tool calls.");
sb.AppendLine("");
sb.AppendLine("### [규칙 1] 필요한 도구를 먼저 사용하라 — Tools First When Needed");
sb.AppendLine("모든 응답에는 반드시 하나 이상의 도구 호출이 포함되어야 합니다.");
sb.AppendLine("텍스트만 있고 도구 호출이 없는 응답은 무효이며 즉시 거부됩니다.");
sb.AppendLine("Use tools whenever they materially advance the task. Do not answer with text only when inspection, file creation, editing, or verification is still needed.");
sb.AppendLine("A text-only response is fine once the requested artifact already exists or enough evidence has been gathered.");
sb.AppendLine("출력의 첫 번째 항목은 반드시 도구 호출이어야 합니다. 텍스트로 시작하는 것은 금지입니다.");
sb.AppendLine("When a tool is clearly useful, call it promptly without a long preamble. Do not force an unnecessary tool call if the task is already complete.");
sb.AppendLine("");
sb.AppendLine("### [규칙 2] 말 먼저 하지 마라 — No Verbal Preamble");
sb.AppendLine("절대 금지 문구 — 도구 호출 전에 절대 쓰지 말 것:");
sb.AppendLine(" ✗ '알겠습니다' / '네' / '확인했습니다' / '작업을 시작하겠습니다' / '먼저 ... 하겠습니다'");
sb.AppendLine(" ✗ '다음과 같이 진행하겠습니다' / '분석해보겠습니다' / '살펴보겠습니다'");
sb.AppendLine(" ✗ 'I will ...' / 'Let me ...' / 'Sure!' / 'Of course!' / 'I'll now ...' / 'First, I need to ...'");
sb.AppendLine("올바른 행동: 즉시 도구를 호출하세요. 간단한 설명은 도구 결과 이후에만 허용됩니다.");
sb.AppendLine("CORRECT: call the tool immediately. A brief explanation may follow AFTER the tool result, never before.");
sb.AppendLine("");
sb.AppendLine("### [규칙 3] 한 번에 여러 도구를 호출하라 — Batch Multiple Tools");
sb.AppendLine("독립적인 작업들은 반드시 같은 응답에서 동시에 호출해야 합니다. 순차적으로 하지 마세요.");
sb.AppendLine("Batch independent reads only when it genuinely saves time and keeps the scope narrow.");
sb.AppendLine(" 나쁜 예(BAD): 응답1: folder_map → 응답2: document_read → 응답3: html_create (순차 처리 — 금지)");
sb.AppendLine(" 좋은 예(GOOD): 응답1: folder_map + document_read 동시 호출 → 응답2: html_create (배치 처리)");
sb.AppendLine("");
sb.AppendLine("### [규칙 4] 불확실해도 즉시 도구를 호출하라 — Act on Uncertainty");
sb.AppendLine("어떤 파일을 읽어야 할지 모를 때도 사용자에게 묻지 말고 즉시 folder_map이나 document_read를 호출하세요.");
sb.AppendLine("If the task requires existing files, start with glob or grep, then use document_read or file_read on the best candidates. If the user wants a brand-new document, skip file exploration and create the artifact directly.");
sb.AppendLine("");
sb.AppendLine("### [규칙 5] 완료까지 계속 도구를 호출하라 — Continue Until Complete");
sb.AppendLine("사용자의 요청이 완전히 완료될 때까지 도구 호출을 계속하세요. '작업을 시작했습니다'는 완료가 아닙니다.");
sb.AppendLine("'완료'의 정의: 결과 파일이 실제로 존재하고, 검증되고, 사용자에게 보고된 상태.");
sb.AppendLine("Keep working until the artifact exists or a concrete blocker is reached. Prefer the shortest path to a complete result.");
sb.AppendLine("");
// 소개 및 메타 정보
sb.AppendLine("---");
sb.AppendLine("You are AX Copilot Agent. You can read, write, and edit files using the provided tools.");
sb.AppendLine($"Today's date: {DateTime.Now:yyyy년 M월 d일} ({DateTime.Now:yyyy-MM-dd}, {DateTime.Now:dddd}).");
sb.AppendLine("Available skills: excel_create (.xlsx), docx_create (.docx), csv_create (.csv), markdown_create (.md), html_create (.html), script_create (.bat/.ps1), document_review (품질 검증), format_convert (포맷 변환).");
sb.AppendLine("\nOnly present a step-by-step plan when the user explicitly asks for a plan or when the task is too ambiguous to execute safely.");
sb.AppendLine("For ordinary Cowork requests, proceed directly with the work and focus on producing the real artifact.");
sb.AppendLine("If the user asks for a brand-new report, proposal, analysis, manual, or other document and does not explicitly ask to reference workspace files, do NOT start with glob, grep, document_read, or folder_map.");
sb.AppendLine("In that case, go straight to document_plan when helpful, then immediately call the creation tool such as docx_create, html_create, markdown_create, excel_create, or document_assemble.");
sb.AppendLine("After creating files, summarize what was created and include the actual output path.");
sb.AppendLine("Do not stop after a single step. Continue autonomously until the request is completed or a concrete blocker (permission denial, missing dependency, hard error) is encountered.");
sb.AppendLine("When adapting external references, rewrite names/structure/comments to AX Copilot style. Avoid clone-like outputs.");
sb.AppendLine("IMPORTANT: When creating documents with dates, always use today's actual date above. Never use placeholder or fictional dates.");
sb.AppendLine("IMPORTANT: For reports, proposals, analyses, and manuals with multiple sections:");
sb.AppendLine(" 1. Decide the document structure internally first.");
sb.AppendLine(" 2. Use document_plan only when it improves the actual output, not as a mandatory user-facing approval step.");
sb.AppendLine(" 3. Then immediately create the real output file with html_create, docx_create, markdown_create, or file_write.");
sb.AppendLine(" 4. Fill every section with real content. Do not stop at an outline or plan only.");
sb.AppendLine(" 5. The task is complete only after the actual document file has been created or updated.");
// 문서 품질 검증 루프
sb.AppendLine("\n## Document Quality Review");
sb.AppendLine("After creating a document, verify it in the lightest way that is sufficient:");
sb.AppendLine("1. Start with file_read or document_read to confirm the generated file exists and covers the requested sections");
sb.AppendLine("2. Use document_review only for large generated documents, conversion-heavy output, or when the user explicitly asks for a quality check");
sb.AppendLine("3. Check logical errors only when relevant: dates, missing sections, broken formatting, or placeholder text");
sb.AppendLine("4. If issues are found, fix them using file_write or file_edit, then re-verify");
sb.AppendLine("5. Report what was checked and whether corrections were needed");
// 문서 포맷 변환 지원
sb.AppendLine("\n## Format Conversion");
sb.AppendLine("When the user requests format conversion (e.g., HTML→Word, Excel→CSV, Markdown→HTML):");
sb.AppendLine("Use format conversion tools only when the user explicitly requests conversion between formats.");
sb.AppendLine("1. Use file_read or document_read to read the source file content");
sb.AppendLine("2. Create a new file in the target format using the appropriate skill (docx_create, html_create, etc.)");
sb.AppendLine("3. Preserve the content structure, formatting, and data as closely as possible");
// 사용자 지정 출력 포맷
var fmt = llm.DefaultOutputFormat;
if (!string.IsNullOrEmpty(fmt) && fmt != "auto")
{
var fmtMap = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
["xlsx"] = "Excel (.xlsx) using excel_create",
["docx"] = "Word (.docx) using docx_create",
["html"] = "HTML (.html) using html_create",
["md"] = "Markdown (.md) using markdown_create",
["csv"] = "CSV (.csv) using csv_create",
};
if (fmtMap.TryGetValue(fmt, out var fmtDesc))
sb.AppendLine($"IMPORTANT: User prefers output format: {fmtDesc}. Use this format unless the user specifies otherwise.");
}
// 디자인 무드 — HTML 문서 생성 시 mood 파라미터로 전달하도록 안내
if (!string.IsNullOrEmpty(_selectedMood) && _selectedMood != "modern")
sb.AppendLine($"When creating HTML documents with html_create, use mood=\"{_selectedMood}\" for the design template.");
else
sb.AppendLine("When creating HTML documents with html_create, you can set 'mood' parameter: modern, professional, creative, minimal, elegant, dark, colorful, corporate, magazine, dashboard.");
if (!string.IsNullOrEmpty(workFolder))
sb.AppendLine($"Current work folder: {workFolder}");
sb.AppendLine($"File permission mode: {llm.FilePermission}");
sb.Append(BuildSubAgentDelegationSection(false));
// 폴더 데이터 활용 지침 (사용자 옵션이 아닌 탭별 자동 정책)
switch (GetAutomaticFolderDataUsage())
{
case "active":
sb.AppendLine("IMPORTANT: Folder Data Usage = ACTIVE. You have 'document_read' and 'folder_map' tools available.");
sb.AppendLine("Use workspace exploration only when the user explicitly asks to reference existing materials, provides a file/path hint, or asks about folder contents.");
sb.AppendLine("For brand-new document creation requests, skip glob/grep/document_read/folder_map and proceed directly to document_plan plus the creation tool.");
sb.AppendLine("[CRITICAL] FILE SELECTION STRATEGY — DO NOT READ ALL FILES:");
sb.AppendLine(" 1. Identify candidate files by filename or topic keywords first.");
sb.AppendLine(" 2. Read ONLY files that clearly match the user's topic. Skip unrelated topics.");
sb.AppendLine(" 3. Maximum 2-3 relevant files for the first pass. Expand only when evidence shows more files are needed.");
sb.AppendLine(" 4. Do NOT read every file 'just in case'. Broad reading without evidence is forbidden.");
sb.AppendLine(" 5. If no files match the topic, proceed WITHOUT reading any workspace files.");
sb.AppendLine("VIOLATION: Reading all files in the folder is FORBIDDEN. It wastes tokens and degrades quality.");
break;
case "passive":
sb.AppendLine("Folder Data Usage = PASSIVE. You have 'document_read' and 'folder_map' tools. " +
"Only read folder documents when the user explicitly asks you to reference or use them.");
break;
default: // "none"
sb.AppendLine("Folder Data Usage = NONE. Do NOT read or reference documents in the work folder unless the user explicitly provides a file path.");
break;
}
// 프리셋 시스템 프롬프트가 있으면 추가
lock (_convLock)
{
if (_currentConversation != null && !string.IsNullOrEmpty(_currentConversation.SystemCommand))
sb.AppendLine("\n" + _currentConversation.SystemCommand);
}
// 프로젝트 문맥 파일 (AGENTS.md) 주입
sb.Append(LoadProjectContext(workFolder));
// 프로젝트 규칙 (.ax/rules/) 자동 주입
sb.Append(BuildProjectRulesSection(workFolder));
// 에이전트 메모리 주입
sb.Append(BuildMemorySection(workFolder));
// 피드백 학습 컨텍스트 주입
sb.Append(BuildFeedbackContext());
return sb.ToString();
}
private string BuildCodeSystemPrompt()
{
var workFolder = GetCurrentWorkFolder();
var llm = _settings.Settings.Llm;
var code = llm.Code;
var sb = new System.Text.StringBuilder();
// ══════════════════════════════════════════════════════════════════
// 도구 호출 절대 규칙을 시스템 프롬프트 맨 첫 줄에 배치 — 최대 강제력
// ══════════════════════════════════════════════════════════════════
sb.AppendLine("## [절대 규칙] 도구 우선 — USE TOOLS WHEN THEY MATERIALLY ADVANCE THE TASK");
sb.AppendLine("이 규칙들은 다른 모든 지시보다 우선합니다. 위반 시 작업이 즉시 실패합니다.");
sb.AppendLine("Treat these as operating guidance, not as a requirement to force unnecessary tool calls.");
sb.AppendLine("");
sb.AppendLine("### [규칙 1] 필요한 도구를 먼저 사용하라 — Tools First When Needed");
sb.AppendLine("모든 응답에는 반드시 하나 이상의 도구 호출이 포함되어야 합니다.");
sb.AppendLine("텍스트만 있고 도구 호출이 없는 응답은 무효이며 즉시 거부됩니다.");
sb.AppendLine("Use tools whenever they materially advance the task. Do not answer with text only when code inspection, editing, diff review, or verification is still needed.");
sb.AppendLine("A text-only response is fine once the requested code change is complete or enough evidence has been gathered.");
sb.AppendLine("출력의 첫 번째 항목은 반드시 도구 호출이어야 합니다. 텍스트로 시작하는 것은 금지입니다.");
sb.AppendLine("When a tool is clearly useful, call it promptly without a long preamble. Do not force an unnecessary tool call if the task is already complete.");
sb.AppendLine("");
sb.AppendLine("### [규칙 2] 말 먼저 하지 마라 — No Verbal Preamble");
sb.AppendLine("절대 금지 문구 — 도구 호출 전에 절대 쓰지 말 것:");
sb.AppendLine(" ✗ '알겠습니다' / '네' / '확인했습니다' / '분석해보겠습니다' / '살펴보겠습니다'");
sb.AppendLine(" ✗ '먼저 폴더 구조를 파악하겠습니다' — folder_map을 즉시 호출하세요, 예고하지 마세요");
sb.AppendLine(" ✗ 'I will ...' / 'Let me ...' / 'Sure!' / 'I'll start by ...' / 'First, I need to ...'");
sb.AppendLine("올바른 행동: 즉시 도구를 호출하세요. 한 문장 설명은 도구 결과 이후에만 허용됩니다.");
sb.AppendLine("CORRECT: call the tool immediately. One-sentence explanation may follow the tool result, never precede it.");
sb.AppendLine("");
sb.AppendLine("### [규칙 3] 모든 독립 작업은 병렬로 호출하라 — Parallelise Everything Possible");
sb.AppendLine("독립적인 읽기/검색 작업들은 반드시 같은 응답에서 동시에 호출해야 합니다. 절대 순차적으로 하지 마세요.");
sb.AppendLine("Batch independent reads/searches only when it genuinely saves time and keeps the scope narrow.");
sb.AppendLine(" 나쁜 예(BAD): R1: folder_map → R2: grep → R3: file_read → R4: file_edit (순차 — 금지)");
sb.AppendLine(" 좋은 예(GOOD): R1: folder_map + grep + file_read 동시 호출 → R2: file_edit + build_run");
sb.AppendLine("모든 독립적인 읽기/검색 작업은 같은 응답에 배치해야 합니다.");
sb.AppendLine("Prefer targeted grep/glob plus a small number of file reads over broad parallel sweeps.");
sb.AppendLine("");
sb.AppendLine("### [규칙 4] 불확실해도 즉시 도구를 호출하라 — Act on Uncertainty, Do Not Ask");
sb.AppendLine("어떤 파일을 읽어야 할지 모를 때: 즉시 glob이나 grep을 호출하세요.");
sb.AppendLine("의존성이 불확실할 때: 즉시 dev_env_detect를 호출하세요.");
sb.AppendLine("If unsure which file, use glob or grep to narrow the target. If unsure about dependencies or tooling, use dev_env_detect.");
sb.AppendLine("'잘 모르겠습니다, 알려주시겠어요?' — 도구 호출로 답을 구할 수 있다면 절대 사용자에게 묻지 마세요.");
sb.AppendLine("Prefer answering uncertainty with a focused tool call instead of asking avoidable clarification questions.");
sb.AppendLine("");
sb.AppendLine("### [규칙 5] 도구로 검증하라, 가정하지 마라 — Verify with Tools, Not Assumptions");
sb.AppendLine("편집 후에는 반드시 file_read로 최종 상태를 확인하세요. 편집이 성공했다고 가정하지 마세요.");
sb.AppendLine("빌드/테스트 후에는 build_run의 실제 출력을 인용하세요. '정상 작동할 것입니다'라고 말하지 마세요.");
sb.AppendLine("After editing, re-open the changed file when confirmation is helpful. Do not assume a risky edit succeeded.");
sb.AppendLine("When you run build/test, cite the actual build_run output instead of assuming success.");
sb.AppendLine("'완료'의 정의: 빌드 통과 + 테스트 통과 + 편집 파일 재확인 + 결과 보고 — 모두 도구 출력으로 증명된 상태.");
sb.AppendLine("'Done' = build passes + tests pass + edited files re-read + result reported — all proven by tool output.");
sb.AppendLine("");
// 소개 및 메타 정보
sb.AppendLine("---");
sb.AppendLine("You are AX Copilot Code Agent — a senior software engineer for enterprise development.");
sb.AppendLine($"Today's date: {DateTime.Now:yyyy년 M월 d일} ({DateTime.Now:yyyy-MM-dd}).");
sb.AppendLine("Available tools: file_read, file_write, file_edit (supports replace_all), glob, grep (supports context_lines, case_sensitive), lsp_code_intel, folder_map, process, dev_env_detect, build_run, git_tool.");
sb.AppendLine("Do not pause after partial progress. Keep executing consecutive steps until completion or a concrete blocker is reached.");
sb.AppendLine("IMPORTANT: When creating documents with dates, always use today's actual date above.");
sb.AppendLine("\n## Core Workflow");
sb.AppendLine("1. ORIENT: Pick the smallest next step that can answer the request.");
sb.AppendLine(" - If a specific file is already obvious, start with file_read directly.");
sb.AppendLine(" - If you need definitions, references, symbols, implementations, or call hierarchy, prefer lsp_code_intel before broad searching.");
sb.AppendLine(" - Otherwise use grep/glob to find the smallest relevant set of files.");
sb.AppendLine(" - Use folder_map only when the repository structure itself is unclear.");
sb.AppendLine("2. ANALYZE: Read only the files needed to understand the change.");
sb.AppendLine(" - Check impacted callers/references when changing shared code or public behavior. Use lsp_code_intel when semantic lookup will be more precise than text search.");
sb.AppendLine("3. IMPLEMENT: Apply the smallest safe edit. Use file_edit for existing files and file_write for new files.");
sb.AppendLine("4. VERIFY: Run build_run/test_loop when the change affects buildable or testable behavior, or when the user explicitly asks for verification.");
sb.AppendLine(" - Use git_tool(diff) when it helps confirm the final change set or explain what changed.");
sb.AppendLine("5. REPORT: Summarize what changed, what was verified, and any remaining risk.");
sb.AppendLine("\n## Development Environment");
sb.AppendLine("Use dev_env_detect to check installed IDEs, runtimes, and build tools before running commands.");
sb.AppendLine("IMPORTANT: Do NOT attempt to install compilers, IDEs, or build tools. Only use what is already installed.");
// 패키지 저장소 정보
sb.AppendLine("\n## Package Repositories");
if (!string.IsNullOrEmpty(code.NexusBaseUrl))
sb.AppendLine($"Enterprise Nexus: {code.NexusBaseUrl}");
sb.AppendLine($"NuGet (.NET): {code.NugetSource}");
sb.AppendLine($"PyPI/Conda (Python): {code.PypiSource}");
sb.AppendLine($"Maven (Java): {code.MavenSource}");
sb.AppendLine($"npm (JavaScript): {code.NpmSource}");
sb.AppendLine("When adding dependencies, use these repository URLs.");
// IDE 정보
if (!string.IsNullOrEmpty(code.PreferredIdePath))
sb.AppendLine($"\nPreferred IDE: {code.PreferredIdePath}");
// 사용자 선택 개발 언어
if (_selectedLanguage != "auto")
{
var langName = _selectedLanguage switch { "python" => "Python", "java" => "Java", "csharp" => "C# (.NET)", "cpp" => "C/C++", "javascript" => "JavaScript/TypeScript", _ => _selectedLanguage };
sb.AppendLine($"\nIMPORTANT: User selected language: {langName}. Prioritize this language for code analysis and generation.");
}
// 언어별 가이드라인
sb.AppendLine("\n## Language Guidelines");
sb.AppendLine("- C# (.NET): Use dotnet CLI. NuGet for packages. Follow Microsoft naming conventions.");
sb.AppendLine("- Python: Use conda/pip. Follow PEP8. Use type hints. Virtual env preferred.");
sb.AppendLine("- Java: Use Maven/Gradle. Follow Google Java Style Guide.");
sb.AppendLine("- C++: Use CMake for build. Follow C++ Core Guidelines.");
sb.AppendLine("- JavaScript/TypeScript: Use npm/yarn. Follow ESLint rules. Vue3 uses Composition API.");
// 코드 품질 + 안전 수칙
sb.AppendLine("\n## Code Quality & Safety");
sb.AppendLine("- NEVER delete or overwrite files without user confirmation.");
sb.AppendLine("- ALWAYS read a file before editing it. Don't guess contents.");
sb.AppendLine("- Prefer file_edit over file_write for existing files (shows diff).");
sb.AppendLine("- When porting/referencing external code, do not copy verbatim. Rename and re-structure to match AX Copilot conventions.");
sb.AppendLine("- Use grep to find ALL references before renaming/removing anything.");
sb.AppendLine("- After editing, re-open the changed files and nearby callers to verify the final state, not just the patch intent.");
sb.AppendLine("- Treat verification as incomplete unless you can cite build/test or direct file-read evidence.");
sb.AppendLine("- If unsure about a change's impact, ask the user first.");
sb.AppendLine("- For large refactors, do them incrementally with build verification between steps.");
sb.AppendLine("- Use git_tool action='diff' to review your changes before committing.");
sb.AppendLine("\n## Lint & Format");
sb.AppendLine("After code changes, check for available linters:");
sb.AppendLine("- Python: ruff, black, flake8, pylint");
sb.AppendLine("- JavaScript: eslint, prettier");
sb.AppendLine("- C#: dotnet format");
sb.AppendLine("- C++: clang-format");
sb.AppendLine("Run the appropriate linter via process tool if detected by dev_env_detect.");
if (!string.IsNullOrEmpty(workFolder))
sb.AppendLine($"\nCurrent work folder: {workFolder}");
sb.AppendLine($"File permission mode: {llm.FilePermission}");
sb.Append(BuildSubAgentDelegationSection(true));
// 폴더 데이터 활용
sb.AppendLine("\nFolder Data Usage = ACTIVE.");
sb.AppendLine("Prefer targeted file_read, grep/glob, and lsp_code_intel for narrow requests.");
sb.AppendLine("Use folder_map only when structure is unclear, the user explicitly asks for folder contents, or the request is repo-wide.");
sb.AppendLine("Read only files that are relevant to the current question. Avoid broad codebase sweeps without evidence.");
// 프리셋 시스템 프롬프트
lock (_convLock)
{
if (_currentConversation?.SystemCommand is { Length: > 0 } sysCmd)
sb.AppendLine("\n" + sysCmd);
}
// 프로젝트 문맥 파일 (AGENTS.md) 주입
sb.Append(LoadProjectContext(workFolder));
// 프로젝트 규칙 (.ax/rules/) 자동 주입
sb.Append(BuildProjectRulesSection(workFolder));
// 에이전트 메모리 주입
sb.Append(BuildMemorySection(workFolder));
// 피드백 학습 컨텍스트 주입
sb.Append(BuildFeedbackContext());
return sb.ToString();
}
private static string BuildSubAgentDelegationSection(bool codeMode)
{
var sb = new System.Text.StringBuilder();
sb.AppendLine("\n## Sub-Agent Delegation");
sb.AppendLine("Use spawn_agent only when a bounded side investigation can run in parallel without blocking your own next step.");
sb.AppendLine("Good delegation targets: impact analysis, reference/caller search, test-file discovery, diff review, and bug root-cause investigation.");
sb.AppendLine("Do not delegate the final editing decision, the final report, or blocking work that you must inspect immediately yourself.");
sb.AppendLine("When spawning a sub-agent, give a concrete task with the exact question, likely file/module scope, and the output shape you want.");
sb.AppendLine("Expected sub-agent result shape: conclusion, files checked, key evidence, recommended next action, risks/unknowns.");
sb.AppendLine("Use wait_agents only when you are ready to integrate the result. Keep doing useful local work while the sub-agent runs.");
if (codeMode)
{
sb.AppendLine("For code tasks, prefer delegating read-only investigations such as caller mapping, related test discovery, or build/test failure triage.");
sb.AppendLine("For high-impact code changes, consider delegation when it quickly adds evidence, but do not treat it as mandatory.");
}
else
{
sb.AppendLine("For cowork tasks, consider delegation only when fact gathering or source cross-checking clearly speeds up the work.");
}
return sb.ToString();
}
/// <summary>프로젝트 규칙 (.ax/rules/)을 시스템 프롬프트 섹션으로 포맷합니다.</summary>
private string BuildProjectRulesSection(string? workFolder)
{
if (string.IsNullOrEmpty(workFolder)) return "";
if (!_settings.Settings.Llm.EnableProjectRules) return "";
try
{
var rules = Services.Agent.ProjectRulesService.LoadRules(workFolder);
if (rules.Count == 0) return "";
// 컨텍스트별 필터링: Cowork=document, Code=always (기본)
var when = _activeTab == "Code" ? "always" : "always";
var filtered = Services.Agent.ProjectRulesService.FilterRules(rules, when);
return Services.Agent.ProjectRulesService.FormatForSystemPrompt(filtered);
}
catch
{
return "";
}
}
/// <summary>에이전트 메모리를 시스템 프롬프트 섹션으로 포맷합니다.</summary>
private string BuildMemorySection(string? workFolder)
{
if (!_settings.Settings.Llm.EnableAgentMemory) return "";
var app = System.Windows.Application.Current as App;
var memService = app?.MemoryService;
if (memService == null) return "";
// 메모리를 로드 (작업 폴더 변경 시 재로드)
memService.Load(workFolder ?? "");
var all = memService.All;
var layeredDocs = memService.InstructionDocuments;
if (all.Count == 0 && layeredDocs.Count == 0) return "";
var sb = new System.Text.StringBuilder();
sb.AppendLine("\n## 메모리 계층");
sb.AppendLine("다음 메모리는 claude-code와 비슷하게 관리형 → 사용자 → 프로젝트 → 로컬 순서로 조립됩니다.");
sb.AppendLine("현재 작업 디렉토리에 가까운 메모리가 더 높은 우선순위를 가집니다.\n");
const int maxLayeredDocs = 8;
const int maxDocChars = 1800;
foreach (var doc in layeredDocs.Take(maxLayeredDocs))
{
sb.AppendLine($"[{doc.Label}] {doc.Path}");
var text = doc.Content;
if (text.Length > maxDocChars)
text = text[..maxDocChars] + "\n...(생략)";
sb.AppendLine(text);
sb.AppendLine();
}
if (all.Count > 0)
{
sb.AppendLine("## 학습 메모리 (이전 대화에서 학습한 내용)");
sb.AppendLine("아래는 이전 대화에서 학습한 규칙과 선호도입니다. 작업 시 참고하세요.");
sb.AppendLine("새로운 규칙이나 선호도를 발견하면 memory 도구의 save 액션으로 저장하세요.");
sb.AppendLine("사용자가 이전 학습 내용과 다른 지시를 하면 memory 도구의 delete 후 새로 save 하세요.\n");
foreach (var group in all.GroupBy(e => e.Type))
{
var label = group.Key switch
{
"rule" => "프로젝트 규칙",
"preference" => "사용자 선호",
"fact" => "프로젝트 사실",
"correction" => "이전 교정",
_ => group.Key,
};
sb.AppendLine($"[{label}]");
foreach (var e in group.OrderByDescending(e => e.UseCount).Take(15))
sb.AppendLine($"- {e.Content}");
sb.AppendLine();
}
}
return sb.ToString();
}
}