fix(context): show both summarize and compress thresholds in /context

The /context command previously showed only the hard budget compression
threshold (contextWindow - maxTokens), which confused users who expected
to see the soft summarization trigger from summarize_token_percent.

This commit adds SummarizeAtTokens alongside the existing CompressAtTokens
so that both thresholds are visible:

- Compress at: contextWindow - maxTokens (hard budget, triggers proactive
  compression when exceeded)
- Summarize at: contextWindow * summarizeTokenPercent / 100 (soft trigger,
  matches maybeSummarize's threshold)

The fix updates the /context command output, the Web UI popover, and the
pico channel WebSocket payload.

Fixes #2968
This commit is contained in:
程智超0668000959 2026-06-02 15:43:24 +08:00
parent 709c8b2b52
commit 5f826f4448
9 changed files with 57 additions and 24 deletions

View file

@ -350,11 +350,12 @@ func (al *AgentLoop) buildCommandsRuntime(
} }
history := agent.Sessions.GetHistory(opts.SessionKey) history := agent.Sessions.GetHistory(opts.SessionKey)
return &commands.ContextStats{ return &commands.ContextStats{
UsedTokens: usage.UsedTokens, UsedTokens: usage.UsedTokens,
TotalTokens: usage.TotalTokens, TotalTokens: usage.TotalTokens,
CompressAtTokens: usage.CompressAtTokens, CompressAtTokens: usage.CompressAtTokens,
UsedPercent: usage.UsedPercent, SummarizeAtTokens: usage.SummarizeAtTokens,
MessageCount: len(history), UsedPercent: usage.UsedPercent,
MessageCount: len(history),
} }
} }
} }

View file

@ -61,6 +61,13 @@ func computeContextUsage(agent *AgentInstance, sessionKey string) *bus.ContextUs
// proactive trigger (msgTokens + toolTokens + maxTokens > contextWindow). // proactive trigger (msgTokens + toolTokens + maxTokens > contextWindow).
compressAt := effectiveWindow compressAt := effectiveWindow
// summarizeAt = soft summarization trigger: matches maybeSummarize's
// threshold (contextWindow * SummarizeTokenPercent / 100).
summarizeAt := contextWindow * agent.SummarizeTokenPercent / 100
if summarizeAt <= 0 {
summarizeAt = compressAt
}
usedPercent := 0 usedPercent := 0
if compressAt > 0 { if compressAt > 0 {
usedPercent = usedTokens * 100 / compressAt usedPercent = usedTokens * 100 / compressAt
@ -70,9 +77,10 @@ func computeContextUsage(agent *AgentInstance, sessionKey string) *bus.ContextUs
} }
return &bus.ContextUsage{ return &bus.ContextUsage{
UsedTokens: usedTokens, UsedTokens: usedTokens,
TotalTokens: contextWindow, TotalTokens: contextWindow,
CompressAtTokens: compressAt, CompressAtTokens: compressAt,
UsedPercent: usedPercent, SummarizeAtTokens: summarizeAt,
UsedPercent: usedPercent,
} }
} }

View file

@ -64,10 +64,11 @@ type OutboundScope struct {
// ContextUsage describes how much of the model's context window the current // ContextUsage describes how much of the model's context window the current
// session consumes, and how far it is from triggering compression. // session consumes, and how far it is from triggering compression.
type ContextUsage struct { type ContextUsage struct {
UsedTokens int `json:"used_tokens"` UsedTokens int `json:"used_tokens"`
TotalTokens int `json:"total_tokens"` // model context window TotalTokens int `json:"total_tokens"` // model context window
CompressAtTokens int `json:"compress_at_tokens"` // threshold that triggers compression CompressAtTokens int `json:"compress_at_tokens"` // hard budget compression threshold (contextWindow - maxTokens)
UsedPercent int `json:"used_percent"` // 0-100 SummarizeAtTokens int `json:"summarize_at_tokens"` // soft summarization trigger (contextWindow * summarizeTokenPercent / 100)
UsedPercent int `json:"used_percent"` // 0-100, relative to compressAt
} }
type OutboundMessage struct { type OutboundMessage struct {

View file

@ -1394,10 +1394,11 @@ func setContextUsagePayload(payload map[string]any, u *bus.ContextUsage) {
return return
} }
payload["context_usage"] = map[string]any{ payload["context_usage"] = map[string]any{
"used_tokens": u.UsedTokens, "used_tokens": u.UsedTokens,
"total_tokens": u.TotalTokens, "total_tokens": u.TotalTokens,
"compress_at_tokens": u.CompressAtTokens, "compress_at_tokens": u.CompressAtTokens,
"used_percent": u.UsedPercent, "summarize_at_tokens": u.SummarizeAtTokens,
"used_percent": u.UsedPercent,
} }
} }

View file

@ -29,14 +29,16 @@ func formatContextStats(s *ContextStats) string {
remaining = 0 remaining = 0
} }
usedWindowPercent := s.UsedTokens * 100 / max(s.TotalTokens, 1) usedWindowPercent := s.UsedTokens * 100 / max(s.TotalTokens, 1)
return fmt.Sprintf( msg := fmt.Sprintf(
"Context usage \nMessages: %d \nUsed: ~%d / %d tokens (%d%%) \nCompress at: %d tokens \nCompression progress: %d%% \nRemaining: ~%d tokens", "Context usage \nMessages: %d \nUsed: ~%d / %d tokens (%d%%) \nCompress at: %d tokens \nSummarize at: %d tokens \nCompression progress: %d%% \nRemaining: ~%d tokens",
s.MessageCount, s.MessageCount,
s.UsedTokens, s.UsedTokens,
s.TotalTokens, s.TotalTokens,
usedWindowPercent, usedWindowPercent,
s.CompressAtTokens, s.CompressAtTokens,
s.SummarizeAtTokens,
s.UsedPercent, s.UsedPercent,
remaining, remaining,
) )
return msg
} }

View file

@ -29,11 +29,12 @@ type MCPToolInfo struct {
// ContextStats describes current session context window usage. // ContextStats describes current session context window usage.
type ContextStats struct { type ContextStats struct {
UsedTokens int UsedTokens int
TotalTokens int // model context window TotalTokens int // model context window
CompressAtTokens int // compression threshold CompressAtTokens int // hard budget compression threshold
UsedPercent int // 0-100 SummarizeAtTokens int // soft summarization trigger
MessageCount int UsedPercent int // 0-100
MessageCount int
} }
// StopResult describes the outcome of a stop request for the current session. // StopResult describes the outcome of a stop request for the current session.

View file

@ -145,6 +145,23 @@ export function ContextUsageRing({
/> />
</div> </div>
<div className="mt-2 space-y-0.5">
<div className="flex items-center justify-between text-[10px]">
<span className="text-muted-foreground">Compress at</span>
<span className="tabular-nums">
{formatTokens(usage.compress_at_tokens)}
</span>
</div>
{usage.summarize_at_tokens != null && usage.summarize_at_tokens > 0 && (
<div className="flex items-center justify-between text-[10px]">
<span className="text-muted-foreground">Summarize at</span>
<span className="tabular-nums">
{formatTokens(usage.summarize_at_tokens)}
</span>
</div>
)}
</div>
<button <button
type="button" type="button"
onClick={handleDetail} onClick={handleDetail}

View file

@ -79,6 +79,7 @@ function parseContextUsage(
used_tokens: used, used_tokens: used,
total_tokens: total, total_tokens: total,
compress_at_tokens: Number(obj.compress_at_tokens) || 0, compress_at_tokens: Number(obj.compress_at_tokens) || 0,
summarize_at_tokens: obj.summarize_at_tokens != null ? Number(obj.summarize_at_tokens) : undefined,
used_percent: Number(obj.used_percent) || 0, used_percent: Number(obj.used_percent) || 0,
} }
} }

View file

@ -53,6 +53,7 @@ export interface ContextUsage {
used_tokens: number used_tokens: number
total_tokens: number total_tokens: number
compress_at_tokens: number compress_at_tokens: number
summarize_at_tokens?: number
used_percent: number used_percent: number
} }