claude-code 기준 provider 호환성과 compact 후속 흐름을 보강한다
- OpenAI 호환 tool_choice 400 오류에 대한 일반 fallback을 추가하고 Qwen·LLaMA·DeepSeek 계열 vLLM의 도구 호출 프로파일을 더 보수적으로 조정 - compact 이후 branch context와 최근 tool state를 query view에 재주입하고 UI 표현 수준에 맞춰 compact 카드/컨텍스트 사용 팝업/최종 보고 밀도를 세분화 - README와 DEVELOPMENT 문서 이력을 2026-04-12 23:45 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:
@@ -7,6 +7,12 @@ Windows 전용 시맨틱 런처 & 워크스페이스 매니저
|
||||
개발 참고: Claw Code 동등성 작업 추적 문서
|
||||
`docs/claw-code-parity-plan.md`
|
||||
|
||||
- 업데이트: 2026-04-12 23:45 (KST)
|
||||
- `claude-code` 기준으로 남아 있던 provider/compact/UI 후속 차이를 더 줄였습니다. OpenAI 호환 도구 호출은 이제 모델 계열별 호환 프로파일을 적용해 Qwen·LLaMA·DeepSeek 같은 취약한 vLLM 계열에서 최근 structured tool history 범위를 더 좁히고, 병렬 tool call과 reasoning_effort도 더 보수적으로 전송합니다.
|
||||
- tool forcing fallback도 보강했습니다. IBM 배포형뿐 아니라 일반 OpenAI 호환 경로에서도 `tool_choice`가 400으로 거부되면, plain text 지시를 덧붙인 대체 요청으로 한 번 더 재시도해 `채팅은 되지만 Cowork/Code만 막히는` 조합을 줄였습니다.
|
||||
- compact 이후 query view에는 복원된 branch context와 최근 tool state까지 함께 다시 주입합니다. UI는 표현 수준(`rich/balanced/simple`)에 맞춰 compact 카드와 컨텍스트 사용 팝업 밀도를 다르게 보여주고, 최종 보고 프롬프트도 같은 수준에 맞춰 더 짧거나 더 구조적으로 정리되도록 맞췄습니다.
|
||||
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\` 경고 0 / 오류 0
|
||||
|
||||
- 업데이트: 2026-04-10 14:30 (KST)
|
||||
- **UI 프리징 근본 수정**: 스트리밍 중 렌더링 쓰로틀(1.5초 최소 간격), 이중 RenderMessages 제거, 타이머 Stop→Start 무한 루프 차단, 불필요 타이머 4개 일시 정지, 타이머 간격 2-10배 증가(350ms→5s, 500ms→2s 등). 에이전트 이벤트 디스패처 우선순위를 Normal→Background로 하향.
|
||||
- **모델 프로파일 도구 사용 버그 수정**: Ollama 모델에 `tool_choice: "required"` 미전달 버그 수정 — `BuildOpenAiToolBody`에서 Ollama 조기 리턴 전에 `tool_choice` 주입. `FindRegisteredModel`의 대소문자 민감 비교를 `OrdinalIgnoreCase`로 변경하여 프로파일 매칭 실패 방지.
|
||||
|
||||
@@ -734,3 +734,12 @@ owKindCounts를 함께 남겨 %APPDATA%\\AxCopilot\\perf 기준으로 transcript
|
||||
- compact 뒤 첫 LLM 호출이 복원된 맥락의 종류를 더 안정적으로 전달받습니다.
|
||||
- transcript와 usage UI가 `claw-code`처럼 더 얇고 조용한 운영 메타 표현을 유지합니다.
|
||||
|
||||
## claude-code식 provider/compact/UI 후속 정렬
|
||||
|
||||
- 업데이트: 2026-04-12 23:45 (KST)
|
||||
- `LlmService.ToolUse`에 OpenAI 호환 provider별 도구 호출 호환 프로파일을 추가했습니다. Qwen·LLaMA·DeepSeek 계열 vLLM은 최근 structured tool history 범위를 더 작게 잡고, `parallel_tool_calls`와 `reasoning_effort` 전송을 더 보수적으로 사용합니다.
|
||||
- OpenAI 호환 도구 호출 재시도도 확장했습니다. IBM 배포형만이 아니라 일반 OpenAI 호환 경로에서도 `tool_choice`가 400으로 거부되면 `tool_choice` 없이 plain-text tool-only 지시를 덧붙인 fallback body로 한 번 더 재시도합니다.
|
||||
- `AgentQueryContextBuilder`의 post-compact context에는 compact summary, file/image refs 외에 `branch_context`와 최근 tool state를 함께 싣도록 보강했습니다. compact 직후 첫 턴에서 요약/분기/도구 맥락이 더 자연스럽게 이어지도록 맞춘 변경입니다.
|
||||
- `AgentLoopService`의 최종 보고 프롬프트는 `AgentUiExpressionLevel`을 반영합니다. `simple`은 매우 짧은 종료 보고, `balanced`는 기본 요약, `rich`는 review/high-impact 중심의 구조화 보고로 밀도를 달리합니다.
|
||||
- `ChatWindow.TimelinePresentation`, `ChatWindow.ContextUsagePresentation`도 표현 수준에 맞춰 compact 메타 노출량을 다르게 조정했습니다. `simple`은 짧은 한 줄 위주, `rich`는 필요한 경우 compact preview를 덧붙여 보여줍니다.
|
||||
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\` 경고 0 / 오류 0
|
||||
|
||||
@@ -3517,8 +3517,30 @@ public partial class AgentLoopService
|
||||
"찾을 수 없");
|
||||
}
|
||||
|
||||
private static string GetAgentUiExpressionLevel()
|
||||
{
|
||||
var app = System.Windows.Application.Current as App;
|
||||
var raw = app?.SettingsService?.Settings?.Llm?.AgentUiExpressionLevel;
|
||||
return (raw ?? "balanced").Trim().ToLowerInvariant() switch
|
||||
{
|
||||
"rich" => "rich",
|
||||
"simple" => "simple",
|
||||
_ => "balanced",
|
||||
};
|
||||
}
|
||||
|
||||
private static string BuildFinalReportQualityPrompt(TaskTypePolicy taskPolicy, bool highImpact)
|
||||
{
|
||||
var expressionLevel = GetAgentUiExpressionLevel();
|
||||
|
||||
if (expressionLevel == "simple" && !taskPolicy.IsReviewTask && !highImpact)
|
||||
{
|
||||
return "[System:FinalReportQuality] 최종 답변을 매우 짧게 정리하세요.\n" +
|
||||
"1. 변경 또는 산출물 한 줄\n" +
|
||||
"2. 확인한 근거 한 줄\n" +
|
||||
"실제 미해결 문제가 있을 때만 마지막 한 줄을 추가하세요.";
|
||||
}
|
||||
|
||||
if (!taskPolicy.IsReviewTask && !highImpact)
|
||||
{
|
||||
return "[System:FinalReportQuality] 최종 답변을 짧고 명확하게 정리하세요.\n" +
|
||||
@@ -3531,6 +3553,19 @@ public partial class AgentLoopService
|
||||
var taskLine = taskPolicy.FinalReportTaskLine;
|
||||
var riskLine = "남은 리스크나 추가 확인 필요 사항이 실제로 남아 있을 때만 짧게 적으세요.\n";
|
||||
|
||||
if (expressionLevel == "rich")
|
||||
{
|
||||
return "[System:FinalReportQuality] 최종 답변을 구조적으로 정리하세요.\n" +
|
||||
"1. 무엇을 변경했는지\n" +
|
||||
"2. 어떤 파일/호출부/자료를 확인했는지\n" +
|
||||
"3. 어떤 build/test/검증 근거가 있는지\n" +
|
||||
"4. 실제 파일 경로 또는 파일명 1~3개를 명시하세요\n" +
|
||||
"5. review 작업이면 이슈별 상태를 구분하세요\n" +
|
||||
taskLine +
|
||||
riskLine +
|
||||
"후속 권유는 실제 미해결 위험이 남아 있을 때만 포함하세요.";
|
||||
}
|
||||
|
||||
return "[System:FinalReportQuality] 최종 답변을 더 구조적으로 정리하세요.\n" +
|
||||
"1. 무엇을 변경했는지\n" +
|
||||
"2. 어떤 파일/호출부를 확인했는지\n" +
|
||||
|
||||
@@ -157,21 +157,39 @@ public static class AgentQueryContextBuilder
|
||||
string.Equals(m.MetaKind, "microcompact_boundary", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(m.MetaKind, "session_memory_compaction", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(m.MetaKind, "collapsed_boundary", StringComparison.OrdinalIgnoreCase));
|
||||
var branchContextCount = messages.Count(m =>
|
||||
string.Equals(m.MetaKind, "branch_context", StringComparison.OrdinalIgnoreCase));
|
||||
var structuredToolHistoryCount = messages.Count(m =>
|
||||
{
|
||||
var content = m.Content ?? "";
|
||||
return content.StartsWith("{\"_tool_use_blocks\"", StringComparison.Ordinal)
|
||||
|| content.StartsWith("{\"type\":\"tool_result\"", StringComparison.Ordinal);
|
||||
});
|
||||
var recentToolNames = messages
|
||||
.Where(m => m.Role == "user")
|
||||
.Select(m => TryExtractToolResultToolName(m, out var toolName) ? toolName : "")
|
||||
.Where(toolName => !string.IsNullOrWhiteSpace(toolName))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.Take(4)
|
||||
.ToList();
|
||||
|
||||
if (attachedFiles.Count == 0 && imageCount == 0 && compactSummaryCount == 0 && structuredToolHistoryCount == 0)
|
||||
if (attachedFiles.Count == 0
|
||||
&& imageCount == 0
|
||||
&& compactSummaryCount == 0
|
||||
&& structuredToolHistoryCount == 0
|
||||
&& branchContextCount == 0
|
||||
&& recentToolNames.Count == 0)
|
||||
return;
|
||||
|
||||
var lines = new List<string> { "[post-compact context]" };
|
||||
if (compactSummaryCount > 0)
|
||||
lines.Add($"restored compact summaries: {compactSummaryCount}");
|
||||
if (branchContextCount > 0)
|
||||
lines.Add($"restored branch context: {branchContextCount}");
|
||||
if (structuredToolHistoryCount > 0)
|
||||
lines.Add($"restored tool history blocks: {structuredToolHistoryCount}");
|
||||
if (recentToolNames.Count > 0)
|
||||
lines.Add("restored recent tools: " + string.Join(", ", recentToolNames));
|
||||
if (attachedFiles.Count > 0)
|
||||
lines.Add("restored file refs: " + string.Join(", ", attachedFiles));
|
||||
if (imageCount > 0)
|
||||
@@ -190,4 +208,28 @@ public static class AgentQueryContextBuilder
|
||||
AttachedFiles = attachedFiles.Count > 0 ? attachedFiles : null,
|
||||
});
|
||||
}
|
||||
|
||||
private static bool TryExtractToolResultToolName(ChatMessage message, out string toolName)
|
||||
{
|
||||
toolName = "";
|
||||
if (!string.Equals(message.Role, "user", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.IsNullOrWhiteSpace(message.Content)
|
||||
|| !message.Content.StartsWith("{\"type\":\"tool_result\"", StringComparison.Ordinal))
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
using var doc = System.Text.Json.JsonDocument.Parse(message.Content);
|
||||
if (doc.RootElement.TryGetProperty("tool_name", out var toolNameEl))
|
||||
{
|
||||
toolName = toolNameEl.GetString() ?? "";
|
||||
return !string.IsNullOrWhiteSpace(toolName);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -545,10 +545,14 @@ public partial class LlmService
|
||||
var detail = ExtractErrorDetail(errBody);
|
||||
LogService.Warn($"[ToolUse] {activeService} API 오류 ({resp.StatusCode}): {errBody}");
|
||||
|
||||
if (isIbmDeployment && forceToolCall && (int)resp.StatusCode == 400)
|
||||
if (forceToolCall && (int)resp.StatusCode == 400)
|
||||
{
|
||||
LogService.Warn("[ToolUse] IBM 배포형 경로에서 tool_choice가 거부되어 대체 강제 전략으로 재시도합니다.");
|
||||
var fallbackBody = BuildIbmToolBody(messages, tools, forceToolCall: true, useToolChoice: false);
|
||||
LogService.Warn(isIbmDeployment
|
||||
? "[ToolUse] IBM 배포형 경로에서 tool_choice가 거부되어 대체 강제 전략으로 재시도합니다."
|
||||
: "[ToolUse] OpenAI 호환 경로에서 tool_choice가 거부되어 텍스트 지시 기반 강제 전략으로 재시도합니다.");
|
||||
var fallbackBody = isIbmDeployment
|
||||
? BuildIbmToolBody(messages, tools, forceToolCall: true, useToolChoice: false)
|
||||
: BuildOpenAiToolBody(messages, tools, forceToolCall: true, useToolChoice: false);
|
||||
var fallbackJson = JsonSerializer.Serialize(fallbackBody);
|
||||
WorkflowLogService.LogLlmRawRequestFromContext(url, fallbackJson);
|
||||
using var retryReq = new HttpRequestMessage(HttpMethod.Post, url)
|
||||
@@ -558,7 +562,7 @@ public partial class LlmService
|
||||
await ApplyAuthHeaderAsync(retryReq, ct);
|
||||
using var retryResp = await SendWithTlsAsync(retryReq, allowInsecureTls, ct, HttpCompletionOption.ResponseHeadersRead);
|
||||
if (retryResp.IsSuccessStatusCode)
|
||||
return await ReadOpenAiToolBlocksFromStreamAsync(retryResp, true, prefetchToolCallAsync, ct);
|
||||
return await ReadOpenAiToolBlocksFromStreamAsync(retryResp, isIbmDeployment, prefetchToolCallAsync, ct);
|
||||
}
|
||||
|
||||
// 400 BadRequest → 도구 없이 일반 응답으로 폴백 시도
|
||||
@@ -686,11 +690,59 @@ public partial class LlmService
|
||||
}
|
||||
}
|
||||
|
||||
private object BuildOpenAiToolBody(List<ChatMessage> messages, IReadOnlyCollection<IAgentTool> tools, bool forceToolCall = false)
|
||||
private sealed record OpenAiToolCompatibilityProfile(
|
||||
int StructuredHistoryRecentWindow,
|
||||
bool AllowParallelToolCalls,
|
||||
bool IncludeReasoningEffort,
|
||||
bool AddToolOnlyDirectiveOnFallback);
|
||||
|
||||
private OpenAiToolCompatibilityProfile GetOpenAiToolCompatibilityProfile(string service, string model)
|
||||
{
|
||||
var normalizedService = NormalizeServiceName(service);
|
||||
var normalizedModel = (model ?? "").Trim().ToLowerInvariant();
|
||||
var isFragileVllmFamily =
|
||||
normalizedService == "vllm" &&
|
||||
(normalizedModel.Contains("qwen", StringComparison.OrdinalIgnoreCase)
|
||||
|| normalizedModel.Contains("llama", StringComparison.OrdinalIgnoreCase)
|
||||
|| normalizedModel.Contains("deepseek", StringComparison.OrdinalIgnoreCase)
|
||||
|| normalizedModel.Contains("mistral", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (isFragileVllmFamily)
|
||||
{
|
||||
return new OpenAiToolCompatibilityProfile(
|
||||
StructuredHistoryRecentWindow: 4,
|
||||
AllowParallelToolCalls: false,
|
||||
IncludeReasoningEffort: false,
|
||||
AddToolOnlyDirectiveOnFallback: true);
|
||||
}
|
||||
|
||||
return new OpenAiToolCompatibilityProfile(
|
||||
StructuredHistoryRecentWindow: 8,
|
||||
AllowParallelToolCalls: true,
|
||||
IncludeReasoningEffort: true,
|
||||
AddToolOnlyDirectiveOnFallback: true);
|
||||
}
|
||||
|
||||
private static string BuildOpenAiToolOnlyDirective(IReadOnlyCollection<IAgentTool> tools)
|
||||
{
|
||||
var toolNames = string.Join(", ", tools.Select(t => t.Name).Take(12));
|
||||
return "[TOOL_ONLY] 텍스트로 설명하지 말고 지금 바로 도구를 호출하세요. " +
|
||||
$"사용 가능한 도구: {toolNames}. " +
|
||||
"plain text 대신 function/tool call을 사용하세요.";
|
||||
}
|
||||
|
||||
private object BuildOpenAiToolBody(
|
||||
List<ChatMessage> messages,
|
||||
IReadOnlyCollection<IAgentTool> tools,
|
||||
bool forceToolCall = false,
|
||||
bool useToolChoice = true)
|
||||
{
|
||||
var llm = _settings.Settings.Llm;
|
||||
var activeService = ResolveService();
|
||||
var activeModel = ResolveModel();
|
||||
var compatibilityProfile = GetOpenAiToolCompatibilityProfile(activeService, activeModel);
|
||||
var msgs = new List<object>();
|
||||
var structuredHistoryStart = GetStructuredToolHistoryStartIndex(messages);
|
||||
var structuredHistoryStart = GetStructuredToolHistoryStartIndex(messages, compatibilityProfile.StructuredHistoryRecentWindow);
|
||||
|
||||
for (var messageIndex = 0; messageIndex < messages.Count; messageIndex++)
|
||||
{
|
||||
@@ -790,6 +842,15 @@ public partial class LlmService
|
||||
}
|
||||
}
|
||||
|
||||
if (forceToolCall && !useToolChoice && compatibilityProfile.AddToolOnlyDirectiveOnFallback)
|
||||
{
|
||||
msgs.Add(new
|
||||
{
|
||||
role = "user",
|
||||
content = BuildOpenAiToolOnlyDirective(tools),
|
||||
});
|
||||
}
|
||||
|
||||
// ── tool_calls ↔ tool 메시지 쌍 검증 ──
|
||||
// 컨텍스트 압축 후 tool_calls assistant 메시지는 남아있는데
|
||||
// 대응하는 tool result 메시지가 누락되면 vLLM이 400 에러를 반환함.
|
||||
@@ -822,8 +883,6 @@ public partial class LlmService
|
||||
};
|
||||
}).ToArray();
|
||||
|
||||
var activeService = ResolveService();
|
||||
var activeModel = ResolveModel();
|
||||
var executionPolicy = GetActiveExecutionPolicy();
|
||||
var isOllama = activeService.Equals("ollama", StringComparison.OrdinalIgnoreCase);
|
||||
if (isOllama)
|
||||
@@ -839,7 +898,7 @@ public partial class LlmService
|
||||
["options"] = new { temperature = ResolveToolTemperature() }
|
||||
};
|
||||
// Ollama에도 tool_choice 전달 — 프로파일 ForceInitialToolCall 적용
|
||||
if (forceToolCall)
|
||||
if (forceToolCall && useToolChoice)
|
||||
ollamaBody["tool_choice"] = "required";
|
||||
return ollamaBody;
|
||||
}
|
||||
@@ -852,23 +911,22 @@ public partial class LlmService
|
||||
["stream"] = true,
|
||||
["temperature"] = ResolveToolTemperature(),
|
||||
["max_tokens"] = ResolveOpenAiCompatibleMaxTokens(),
|
||||
["parallel_tool_calls"] = executionPolicy.EnableParallelReadBatch,
|
||||
["parallel_tool_calls"] = executionPolicy.EnableParallelReadBatch && compatibilityProfile.AllowParallelToolCalls,
|
||||
};
|
||||
// 스트리밍 시 마지막 청크에 토큰 사용량 포함 요청 (vLLM/OpenAI 호환)
|
||||
body["stream_options"] = new { include_usage = true };
|
||||
// tool_choice: "required" — 모델이 반드시 도구를 호출하도록 강제
|
||||
// 아직 한 번도 도구를 호출하지 않은 첫 번째 요청에서만 사용 (chatty 모델 대응)
|
||||
if (forceToolCall)
|
||||
if (forceToolCall && useToolChoice)
|
||||
body["tool_choice"] = "required";
|
||||
var effort = ResolveReasoningEffort();
|
||||
if (!string.IsNullOrWhiteSpace(effort))
|
||||
if (compatibilityProfile.IncludeReasoningEffort && !string.IsNullOrWhiteSpace(effort))
|
||||
body["reasoning_effort"] = effort;
|
||||
return body;
|
||||
}
|
||||
|
||||
private static int GetStructuredToolHistoryStartIndex(IReadOnlyList<ChatMessage> messages)
|
||||
private static int GetStructuredToolHistoryStartIndex(IReadOnlyList<ChatMessage> messages, int protectedRecentNonSystemMessages)
|
||||
{
|
||||
const int protectedRecentNonSystemMessages = 8;
|
||||
var nonSystemMessages = messages
|
||||
.Select((message, index) => new { message, index })
|
||||
.Where(x => !string.Equals(x.message.Role, "system", StringComparison.OrdinalIgnoreCase))
|
||||
@@ -1229,9 +1287,14 @@ public partial class LlmService
|
||||
{
|
||||
var errBody = await resp.Content.ReadAsStringAsync(ct);
|
||||
var detail = ExtractErrorDetail(errBody);
|
||||
if (isIbmDeployment && forceToolCall && (int)resp.StatusCode == 400)
|
||||
if (forceToolCall && (int)resp.StatusCode == 400)
|
||||
{
|
||||
var fallbackBody = BuildIbmToolBody(messages, tools, forceToolCall: true, useToolChoice: false);
|
||||
LogService.Warn(isIbmDeployment
|
||||
? "[ToolUse] IBM 배포형 스트리밍 경로에서 tool_choice가 거부되어 대체 강제 전략으로 재시도합니다."
|
||||
: "[ToolUse] OpenAI 호환 스트리밍 경로에서 tool_choice가 거부되어 텍스트 지시 기반 강제 전략으로 재시도합니다.");
|
||||
var fallbackBody = isIbmDeployment
|
||||
? BuildIbmToolBody(messages, tools, forceToolCall: true, useToolChoice: false)
|
||||
: BuildOpenAiToolBody(messages, tools, forceToolCall: true, useToolChoice: false);
|
||||
var fallbackJson = JsonSerializer.Serialize(fallbackBody);
|
||||
WorkflowLogService.LogLlmRawRequestFromContext(url, fallbackJson);
|
||||
using var retryReq = new HttpRequestMessage(HttpMethod.Post, url)
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace AxCopilot.Views;
|
||||
|
||||
public partial class ChatWindow
|
||||
{
|
||||
// 토큰 추정 캐시: 메시지 수나 대화 ID가 달라질 때만 재계산
|
||||
// 메시지 개수와 대화 ID가 바뀔 때만 재계산한다.
|
||||
private int _cachedMessageTokens;
|
||||
private int _cachedMessageCountForTokens = -1;
|
||||
private string? _cachedConvIdForTokens;
|
||||
@@ -28,6 +28,7 @@ public partial class ChatWindow
|
||||
}
|
||||
|
||||
var llm = _settings.Settings.Llm;
|
||||
var expressionLevel = GetAgentUiExpressionLevel();
|
||||
var maxContextTokens = Math.Clamp(llm.MaxContextTokens, 1024, 1_000_000);
|
||||
var triggerPercent = Math.Clamp(llm.ContextCompactTriggerPercent, 10, 95);
|
||||
var triggerRatio = triggerPercent / 100.0;
|
||||
@@ -45,6 +46,7 @@ public partial class ChatWindow
|
||||
_cachedConvIdForTokens = convId;
|
||||
_cachedMessageCountForTokens = msgCount;
|
||||
}
|
||||
|
||||
messageTokens = _cachedMessageTokens;
|
||||
}
|
||||
|
||||
@@ -52,7 +54,7 @@ public partial class ChatWindow
|
||||
var draftTokens = string.IsNullOrWhiteSpace(draftText) ? 0 : Services.TokenEstimator.Estimate(draftText) + 4;
|
||||
|
||||
var hasAnyMessages = messageTokens > 0 || _isStreaming;
|
||||
int baseOverhead = 0;
|
||||
var baseOverhead = 0;
|
||||
if (hasAnyMessages)
|
||||
{
|
||||
var sysPromptLen = _llm?.SystemPrompt?.Length ?? 0;
|
||||
@@ -118,11 +120,21 @@ public partial class ChatWindow
|
||||
if (TokenUsagePopupUsage != null)
|
||||
TokenUsagePopupUsage.Text = $"{Services.TokenEstimator.Format(currentTokens)}/{Services.TokenEstimator.Format(maxContextTokens)}";
|
||||
if (TokenUsagePopupDetail != null)
|
||||
TokenUsagePopupDetail.Text = detailText;
|
||||
TokenUsagePopupDetail.Text = expressionLevel == "simple"
|
||||
? detailText
|
||||
: $"{detailText} · 임계 {triggerPercent}%";
|
||||
if (TokenUsagePopupCompact != null)
|
||||
{
|
||||
TokenUsagePopupCompact.Text = _sessionCompactionCount > 0
|
||||
? $"누적 압축 {_sessionCompactionCount}회 · 절감 {FormatTokenCount(_sessionCompactionSavedTokens)}"
|
||||
: "AX Agent가 컨텍스트를 자동 관리합니다";
|
||||
? expressionLevel switch
|
||||
{
|
||||
"simple" => $"압축 {_sessionCompactionCount}회 · {FormatTokenCount(_sessionCompactionSavedTokens)} 절감",
|
||||
_ => $"누적 압축 {_sessionCompactionCount}회 · 절감 {FormatTokenCount(_sessionCompactionSavedTokens)}"
|
||||
}
|
||||
: expressionLevel == "rich"
|
||||
? "AX Agent가 컨텍스트를 자동 관리합니다"
|
||||
: "컨텍스트 자동 관리";
|
||||
}
|
||||
|
||||
TokenUsageCard.ToolTip = null;
|
||||
|
||||
|
||||
@@ -350,6 +350,7 @@ public partial class ChatWindow
|
||||
|
||||
private Border CreateCompactionMetaCard(ChatMessage message, Brush primaryText, Brush secondaryText, Brush hintBg, Brush borderBrush, Brush accentBrush)
|
||||
{
|
||||
var expressionLevel = GetAgentUiExpressionLevel();
|
||||
var icon = "\uE9CE";
|
||||
var title = message.MetaKind switch
|
||||
{
|
||||
@@ -399,20 +400,40 @@ public partial class ChatWindow
|
||||
};
|
||||
|
||||
var detailBits = new List<string>();
|
||||
if (message.AttachedFiles?.Count > 0)
|
||||
if (message.AttachedFiles?.Count > 0 && expressionLevel != "simple")
|
||||
detailBits.Add($"파일 {message.AttachedFiles.Count}개");
|
||||
var compactDetail = detailBits.Count > 0
|
||||
? $"{summary} · {string.Join(", ", detailBits)}"
|
||||
: summary;
|
||||
var compactDetail = expressionLevel switch
|
||||
{
|
||||
"simple" => summary,
|
||||
_ when detailBits.Count > 0 => $"{summary} · {string.Join(", ", detailBits)}",
|
||||
_ => summary
|
||||
};
|
||||
|
||||
stack.Children.Add(new TextBlock
|
||||
{
|
||||
Text = compactDetail,
|
||||
FontSize = 10.5,
|
||||
FontSize = expressionLevel == "simple" ? 10 : 10.5,
|
||||
Foreground = secondaryText,
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
});
|
||||
|
||||
if (expressionLevel == "rich" && !string.IsNullOrWhiteSpace(message.Content))
|
||||
{
|
||||
var preview = message.Content.Trim();
|
||||
if (preview.Length > 120)
|
||||
preview = preview[..120] + "...";
|
||||
|
||||
stack.Children.Add(new TextBlock
|
||||
{
|
||||
Text = preview,
|
||||
FontSize = 10,
|
||||
Foreground = secondaryText,
|
||||
Opacity = 0.88,
|
||||
Margin = new Thickness(0, 6, 0, 0),
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
});
|
||||
}
|
||||
|
||||
wrapper.Child = stack;
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user