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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user