preview 상태 고정과 no-LSP 언어 fallback 마감

목적:
- 긴 세션, compact, query view 생성 시점에도 tool_result preview 축약 상태를 더 안정적으로 유지합니다.
- 격리 환경에서 로컬 LSP 서버가 없더라도 코드 탭이 언어별 정적 분석 힌트를 계속 제공하도록 마감합니다.
- 설정/프롬프트/로드맵 문서까지 현재 구현 상태와 일치시키고 남은 고도화 범위를 정리합니다.

핵심 수정:
- AgentQueryContextBuilder와 ContextCondenser가 query/compact 진입 전에 누락된 tool_result preview를 먼저 복원하도록 정리했습니다.
- AgentToolResultBudget는 sourceMessages가 없는 호출에서도 현재 window의 tool_use_id preview를 재사용하도록 보강했습니다.
- CodeLanguageCatalog에 언어별 manifest/build/test/lint fallback 힌트를 추가하고, LspTool은 LSP 서버 미가동 시 정적 fallback 안내를 반환하도록 변경했습니다.
- SettingsViewModel, SettingsWindow, ChatWindow.SystemPromptBuilder에 Fallback 분석 설명과 LSP 미사용 시 대체 분석 지침을 반영했습니다.
- AgentQueryContextBuilderTests를 새로 추가하고 AgentToolResultBudgetTests, CodeLanguageCatalogTests를 확장했습니다.
- README.md, docs/DEVELOPMENT.md, docs/AGENT_ROADMAP.md, docs/NEXT_ROADMAP.md를 2026-04-15 08:32 (KST) 기준으로 갱신했습니다.

검증:
- dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_loop_lang_finish\\ -p:IntermediateOutputPath=obj\\verify_loop_lang_finish\\
- dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "AgentToolResultBudgetTests|AgentQueryContextBuilderTests|CodeLanguageCatalogTests|ContextCondenserTests" -p:OutputPath=bin\\verify_loop_lang_finish_tests\\ -p:IntermediateOutputPath=obj\\verify_loop_lang_finish_tests\\ (통과 20)
This commit is contained in:
2026-04-15 08:34:24 +09:00
parent 7c138f8ed9
commit 6b3e5e6797
15 changed files with 245 additions and 3 deletions

View File

@@ -27,6 +27,9 @@ public static class AgentQueryContextBuilder
public static AgentQueryContextWindowResult Build(IReadOnlyList<ChatMessage> sourceMessages)
{
if (sourceMessages is IList<ChatMessage> mutableSourceMessages)
AgentMessageInvariantHelper.PopulateMissingToolResultPreviews(mutableSourceMessages);
if (sourceMessages.Count == 0)
{
return new AgentQueryContextWindowResult

View File

@@ -27,10 +27,12 @@ public static class AgentToolResultBudget
IReadOnlyList<ChatMessage>? sourceMessages = null)
{
var result = new AgentToolResultBudgetResult();
AgentMessageInvariantHelper.PopulateMissingToolResultPreviews(messages);
var sourceById = sourceMessages?
.Where(message => !string.IsNullOrWhiteSpace(message.MsgId))
.ToDictionary(message => message.MsgId, StringComparer.OrdinalIgnoreCase);
var sourcePreviewByToolResultId = AgentMessageInvariantHelper.BuildToolResultPreviewMap(sourceMessages);
var previewSourceMessages = sourceMessages ?? messages;
var sourcePreviewByToolResultId = AgentMessageInvariantHelper.BuildToolResultPreviewMap(previewSourceMessages);
var nonSystemIndexes = messages
.Select((message, index) => new { message, index })
.Where(x => !string.Equals(x.message.Role, "system", StringComparison.OrdinalIgnoreCase))

View File

@@ -117,6 +117,8 @@ public static class ContextCondenser
if (messages.Count < 6) return result;
if (!force && !proactiveEnabled) return result;
AgentMessageInvariantHelper.PopulateMissingToolResultPreviews(messages);
// 현재 모델의 입력 토큰 한도
var settings = llm.GetCurrentModelInfo();
// 사용자가 설정한 컨텍스트 크기를 우선 사용. 미설정 시 모델별 기본값 적용.

View File

@@ -23,6 +23,7 @@ public class LspTool : IAgentTool, IDisposable
"- action=\"incoming_calls\": 현재 심볼을 호출하는 상위 호출자를 찾습니다\n" +
"- action=\"outgoing_calls\": 현재 심볼이 호출하는 하위 호출 대상을 찾습니다\n" +
$"지원 언어 서버: {Services.CodeLanguageCatalog.BuildLspSupportDescription()}\n" +
$"정적 fallback: {Services.CodeLanguageCatalog.BuildFallbackSupportDescription()}\n" +
"line/character는 기본적으로 1-based 입력을 기대하며, 0-based 값도 호환 처리합니다.";
public ToolParameterSchema Parameters => new()
@@ -99,12 +100,16 @@ public class LspTool : IAgentTool, IDisposable
// 언어 감지
var language = DetectLanguage(filePath);
if (language == null)
return ToolResult.Fail($"지원하지 않는 파일 형식: {Path.GetExtension(filePath)}");
return ToolResult.Ok(
$"지원하지 않는 LSP 파일 형식: {Path.GetExtension(filePath)}\n\n" +
Services.CodeLanguageCatalog.BuildFallbackSummary(filePath));
// LSP 클라이언트 시작 (캐시)
var client = await GetOrCreateClientAsync(language, context.WorkFolder, ct);
if (client == null || !client.IsConnected)
return ToolResult.Fail($"{language} 언어 서버를 시작할 수 없습니다. 해당 언어 서버가 설치되어 있는지 확인하세요.");
return ToolResult.Ok(
$"{language} 언어 서버를 시작할 수 없습니다. 로컬 LSP 서버가 없거나 연결되지 않았습니다.\n\n" +
Services.CodeLanguageCatalog.BuildFallbackSummary(filePath));
try
{

View File

@@ -29,6 +29,73 @@ public sealed record CodeLanguageCapability(
/// </summary>
public static class CodeLanguageCatalog
{
private static readonly IReadOnlyDictionary<string, string[]> s_manifestHints =
new Dictionary<string, string[]>(StringComparer.OrdinalIgnoreCase)
{
["csharp"] = ["*.sln", "*.csproj", "Directory.Build.props", "Directory.Build.targets"],
["python"] = ["pyproject.toml", "requirements.txt", "setup.py", "environment.yml", "Pipfile"],
["java"] = ["pom.xml", "build.gradle", "build.gradle.kts", "settings.gradle", "settings.gradle.kts"],
["cpp"] = ["CMakeLists.txt", "*.vcxproj", "compile_commands.json", "Makefile"],
["typescript"] = ["package.json", "tsconfig.json", "pnpm-lock.yaml", "yarn.lock", "package-lock.json"],
["javascript"] = ["package.json", "vite.config.*", "nuxt.config.*", "pnpm-lock.yaml", "yarn.lock", "package-lock.json"],
["go"] = ["go.mod", "go.sum"],
["rust"] = ["Cargo.toml", "Cargo.lock"],
["php"] = ["composer.json", "composer.lock"],
["ruby"] = ["Gemfile", "Gemfile.lock", "*.gemspec"],
["kotlin"] = ["build.gradle.kts", "settings.gradle.kts", "gradle.properties"],
["swift"] = ["Package.swift", "*.xcodeproj", "*.xcworkspace"],
};
private static readonly IReadOnlyDictionary<string, string[]> s_buildHints =
new Dictionary<string, string[]>(StringComparer.OrdinalIgnoreCase)
{
["csharp"] = ["dotnet build", "dotnet msbuild"],
["python"] = ["python -m py_compile", "python -m build"],
["java"] = ["mvn compile", "gradle build"],
["cpp"] = ["cmake --build .", "msbuild", "make"],
["typescript"] = ["npm run build", "pnpm build", "tsc -p ."],
["javascript"] = ["npm run build", "pnpm build", "vite build"],
["go"] = ["go build ./..."],
["rust"] = ["cargo build"],
["php"] = ["composer install --dry-run", "php -l"],
["ruby"] = ["bundle exec rake", "ruby -c"],
["kotlin"] = ["gradle build", "./gradlew build"],
["swift"] = ["swift build", "xcodebuild build"],
};
private static readonly IReadOnlyDictionary<string, string[]> s_testHints =
new Dictionary<string, string[]>(StringComparer.OrdinalIgnoreCase)
{
["csharp"] = ["dotnet test"],
["python"] = ["pytest", "python -m unittest"],
["java"] = ["mvn test", "gradle test"],
["cpp"] = ["ctest", "cmake --build . --target test"],
["typescript"] = ["npm test", "pnpm test", "vitest", "jest"],
["javascript"] = ["npm test", "pnpm test", "vitest", "jest"],
["go"] = ["go test ./..."],
["rust"] = ["cargo test"],
["php"] = ["phpunit", "composer test"],
["ruby"] = ["bundle exec rspec", "bundle exec rake test"],
["kotlin"] = ["gradle test", "./gradlew test"],
["swift"] = ["swift test", "xcodebuild test"],
};
private static readonly IReadOnlyDictionary<string, string[]> s_lintHints =
new Dictionary<string, string[]>(StringComparer.OrdinalIgnoreCase)
{
["csharp"] = ["dotnet format", "dotnet build /warnaserror"],
["python"] = ["ruff check", "black --check", "flake8"],
["cpp"] = ["clang-format --dry-run", "clang-tidy"],
["typescript"] = ["npm run lint", "pnpm lint", "eslint ."],
["javascript"] = ["npm run lint", "pnpm lint", "eslint ."],
["go"] = ["gofmt -w", "golangci-lint run"],
["rust"] = ["cargo fmt --check", "cargo clippy"],
["php"] = ["php -l", "phpcs"],
["ruby"] = ["rubocop", "standardrb"],
["kotlin"] = ["./gradlew ktlintCheck", "./gradlew detekt"],
["swift"] = ["swiftformat --lint", "swiftlint"],
};
private static readonly ReadOnlyCollection<CodeLanguageCapability> s_all =
new(new List<CodeLanguageCapability>
{
@@ -313,4 +380,41 @@ public static class CodeLanguageCatalog
sb.Append(BuildLspSupportDescription());
return sb.ToString();
}
public static string BuildFallbackSupportDescription()
=> "LSP 서버가 없거나 연결되지 않아도 확장자, 매니페스트, build/test/lint 힌트 기반의 정적 분석을 계속 제공합니다.";
public static string BuildFallbackSummary(string? filePathOrExtension)
{
CodeLanguageCapability? capability = null;
if (!string.IsNullOrWhiteSpace(filePathOrExtension))
{
capability = filePathOrExtension.StartsWith('.')
? FindByExtension(filePathOrExtension)
: FindByExtension(Path.GetExtension(filePathOrExtension));
}
if (capability == null)
return "정적 fallback: 확장자와 프로젝트 매니페스트를 먼저 확인하고 관련 build/test 명령 힌트를 따라 수동 검증하세요.";
var sb = new StringBuilder();
sb.AppendLine($"정적 fallback 분석: {capability.DisplayName}");
if (s_manifestHints.TryGetValue(capability.Key, out var manifests) && manifests.Length > 0)
sb.AppendLine("주요 매니페스트: " + string.Join(", ", manifests));
if (s_buildHints.TryGetValue(capability.Key, out var buildHints) && buildHints.Length > 0)
sb.AppendLine("권장 build 힌트: " + string.Join(" | ", buildHints));
if (s_testHints.TryGetValue(capability.Key, out var testHints) && testHints.Length > 0)
sb.AppendLine("권장 test 힌트: " + string.Join(" | ", testHints));
if (s_lintHints.TryGetValue(capability.Key, out var lintHints) && lintHints.Length > 0)
sb.AppendLine("권장 lint/format 힌트: " + string.Join(" | ", lintHints));
var primaryGuidance = capability.Guidance.FirstOrDefault();
if (!string.IsNullOrWhiteSpace(primaryGuidance))
sb.Append("분석 포인트: ").Append(primaryGuidance);
return sb.ToString().TrimEnd();
}
}

View File

@@ -141,6 +141,7 @@ public class SettingsViewModel : INotifyPropertyChanged
public string CodeQuickSelectLanguagesText => CodeLanguageCatalog.BuildQuickSelectSupportDescription();
public string CodeLspSupportedLanguagesText => CodeLanguageCatalog.BuildLspSupportDescription();
public string CodeStaticSupportedLanguagesText => CodeLanguageCatalog.BuildStaticSupportDescription();
public string CodeFallbackSupportText => CodeLanguageCatalog.BuildFallbackSupportDescription();
public string CodeTabSupportSummaryText => CodeLanguageCatalog.BuildSupportSummaryDescription();
// ─── 작업 복사본 ───────────────────────────────────────────────────────

View File

@@ -289,6 +289,7 @@ public partial class ChatWindow
sb.AppendLine("\n## Language Guidelines");
foreach (var guidance in CodeLanguageCatalog.GetGuidanceLines(_selectedLanguage == "auto" ? null : _selectedLanguage))
sb.AppendLine(guidance);
sb.AppendLine("- Fallback: If LSP is unavailable, continue with extension-based detection, manifest files, and likely build/test/lint commands instead of stopping.");
// 코드 품질 + 안전 수칙
sb.AppendLine("\n## Code Quality & Safety");

View File

@@ -5469,6 +5469,7 @@
<TextBlock Style="{StaticResource RowHint}" Margin="0,4,0,0" Text="{Binding CodeQuickSelectLanguagesText, StringFormat=빠른 선택 언어: {0}}"/>
<TextBlock Style="{StaticResource RowHint}" Margin="0,4,0,0" Text="{Binding CodeLspSupportedLanguagesText, StringFormat=지원 언어(LSP): {0}}"/>
<TextBlock Style="{StaticResource RowHint}" Margin="0,2,0,0" Text="{Binding CodeStaticSupportedLanguagesText, StringFormat=코드 탭 기본 지원: {0}}"/>
<TextBlock Style="{StaticResource RowHint}" Margin="0,2,0,0" Text="{Binding CodeFallbackSupportText, StringFormat=Fallback 분석: {0}}"/>
<TextBlock Style="{StaticResource RowHint}" Margin="0,2,0,0" Text="격리 환경에서는 내장 분석과 로컬에 이미 설치된 언어 서버만 활용합니다"/>
</StackPanel>
<CheckBox Style="{StaticResource ToggleSwitch}" HorizontalAlignment="Right" VerticalAlignment="Center"