코드 탭 컨텍스트 영속화와 Auto budget 복구 적용

- AxAgentExecutionEngine에서 시스템 프롬프트 중복을 제거하고 structured tool_use/tool_result 전사본을 conversation.Messages로 동기화해 다음 턴과 저장 이력에서도 코드 작업 컨텍스트가 유지되도록 수정
- AgentQueryContextBuilder와 ContextCondenser에 post-compact tool snippet 복원, recent window 확대, tool result 보존 강화 로직을 추가해 장기 코드 실행 중 빌드/파일 근거 손실을 줄임
- MaxContextTokens=0 Auto 모드를 AppSettings, SettingsService 마이그레이션, 설정 UI, 오버레이 UI, 컨텍스트 사용량 표시, LLM 요청 본문에 연결하고 Auto 모드에서는 provider output cap 강제 주입을 제거
- 관련 회귀 테스트와 문서 README/DEVELOPMENT/CODE_CONTEXT_RELIABILITY_PLAN을 갱신하고 깨진 진단 문자열 기대값을 영어 기준으로 정리

검증:
- dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_context_reliability_followup\\ -p:IntermediateOutputPath=obj\\verify_context_reliability_followup\\
- dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "AxAgentExecutionEngineTests|AgentQueryContextBuilderTests|ContextCondenserTests|SettingsServiceTests|AgentLoopDiagnosticsFormatterTests" -p:OutputPath=bin\\verify_context_reliability_followup_tests\\ -p:IntermediateOutputPath=obj\\verify_context_reliability_followup_tests\\
- dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "AgentLoopQueryAssemblyServiceTests|AgentLoopPreLlmStageServiceTests|AgentLoopLlmRequestPreparationServiceTests|AgentMessageInvariantHelperTests|CodeTaskWorkingSetServiceTests|AgentLoopE2ETests" -p:OutputPath=bin\\verify_context_reliability_followup_tests2\\ -p:IntermediateOutputPath=obj\\verify_context_reliability_followup_tests2\\
- dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_context_reliability_final\\ -p:IntermediateOutputPath=obj\\verify_context_reliability_final\\
This commit is contained in:
2026-04-16 07:45:36 +09:00
parent c6e5abfa50
commit e07b6dbed0
23 changed files with 851 additions and 268 deletions

View File

@@ -301,14 +301,25 @@ public partial class LlmService : ILlmService
return llm.Model;
}
private int ResolveOpenAiCompatibleMaxTokens()
private int? ResolveConfiguredMaxOutputTokens()
{
var llm = _settings.Settings.Llm;
var requested = Math.Clamp(llm.MaxContextTokens, 1, 1_000_000);
return llm.MaxContextTokens > 0
? Math.Clamp(llm.MaxContextTokens, 1, 1_000_000)
: null;
}
private int? ResolveOpenAiCompatibleMaxTokens()
{
var llm = _settings.Settings.Llm;
var requested = ResolveConfiguredMaxOutputTokens();
if (!requested.HasValue)
return null;
var service = NormalizeServiceName(llm.Service);
if (service == "vllm")
return Math.Min(requested, 8192);
return Math.Min(requested.Value, 8192);
return requested;
}
@@ -469,16 +480,19 @@ public partial class LlmService : ILlmService
var temperature = ResolveTemperature();
var maxTokens = ResolveOpenAiCompatibleMaxTokens();
IbmDiagDebug($"[IBM진단] BuildIbmDeploymentBody 완료: finalMessages={msgs.Count}건, temp={temperature}, maxTokens={maxTokens}");
return new
var parameters = new Dictionary<string, object?>
{
messages = msgs,
parameters = new
{
temperature,
max_new_tokens = maxTokens
},
// Qwen3.5 thinking 모드 비활성화: 활성화되면 content=null, reasoning_content에만 출력됨
chat_template_kwargs = new { enable_thinking = false },
["temperature"] = temperature,
};
if (maxTokens.HasValue)
parameters["max_new_tokens"] = maxTokens.Value;
return new Dictionary<string, object?>
{
["messages"] = msgs,
["parameters"] = parameters,
// Qwen3.5 thinking mode is disabled to keep content in the normal response field.
["chat_template_kwargs"] = new { enable_thinking = false },
};
}
@@ -1155,7 +1169,6 @@ public partial class LlmService : ILlmService
private object BuildOpenAiBody(List<ChatMessage> messages, bool stream)
{
var llm = _settings.Settings.Llm;
var msgs = BuildMessageList(messages, openAiVision: true);
var body = new Dictionary<string, object?>
{
@@ -1163,8 +1176,10 @@ public partial class LlmService : ILlmService
["messages"] = msgs,
["stream"] = stream,
["temperature"] = ResolveTemperature(),
["max_tokens"] = ResolveOpenAiCompatibleMaxTokens()
};
var maxTokens = ResolveOpenAiCompatibleMaxTokens();
if (maxTokens.HasValue)
body["max_tokens"] = maxTokens.Value;
// 스트리밍 시 마지막 청크에 토큰 사용량을 포함하도록 요청 (vLLM/OpenAI 호환)
if (stream)
body["stream_options"] = new { include_usage = true };
@@ -1298,19 +1313,23 @@ public partial class LlmService : ILlmService
});
}
if (systemInstruction != null)
return new
{
systemInstruction,
contents,
generationConfig = new { temperature = ResolveTemperature(), maxOutputTokens = llm.MaxContextTokens }
};
return new
var generationConfig = new Dictionary<string, object?>
{
contents,
generationConfig = new { temperature = ResolveTemperature(), maxOutputTokens = llm.MaxContextTokens }
["temperature"] = ResolveTemperature(),
};
var maxOutputTokens = ResolveConfiguredMaxOutputTokens();
if (maxOutputTokens.HasValue)
generationConfig["maxOutputTokens"] = maxOutputTokens.Value;
var body = new Dictionary<string, object?>
{
["contents"] = contents,
["generationConfig"] = generationConfig,
};
if (systemInstruction != null)
body["systemInstruction"] = systemInstruction;
return body;
}
// ═══════════════════════════════════════════════════════════════════════
@@ -1422,7 +1441,6 @@ public partial class LlmService : ILlmService
private object BuildSigmoidBody(List<ChatMessage> messages, bool stream)
{
var llm = _settings.Settings.Llm;
var msgs = new List<object>();
foreach (var m in messages)
@@ -1444,27 +1462,32 @@ public partial class LlmService : ILlmService
}
var activeModel = ResolveModel();
var maxTokens = ResolveConfiguredMaxOutputTokens();
if (!string.IsNullOrEmpty(_systemPrompt))
{
return new
var body = new Dictionary<string, object?>
{
model = activeModel,
max_tokens = llm.MaxContextTokens,
temperature = ResolveTemperature(),
system = _systemPrompt,
messages = msgs,
stream
["model"] = activeModel,
["temperature"] = ResolveTemperature(),
["system"] = _systemPrompt,
["messages"] = msgs,
["stream"] = stream,
};
if (maxTokens.HasValue)
body["max_tokens"] = maxTokens.Value;
return body;
}
return new
var fallbackBody = new Dictionary<string, object?>
{
model = activeModel,
max_tokens = llm.MaxContextTokens,
temperature = ResolveTemperature(),
messages = msgs,
stream
["model"] = activeModel,
["temperature"] = ResolveTemperature(),
["messages"] = msgs,
["stream"] = stream,
};
if (maxTokens.HasValue)
fallbackBody["max_tokens"] = maxTokens.Value;
return fallbackBody;
}
// ─── 공용 헬퍼 ─────────────────────────────────────────────────────────