feat: add 3-tier context overflow recovery with tool result truncation
Replace single-strategy context overflow handling with a tiered cascade: - Tier 1: Truncate oversized tool results (>8000 chars) with newline-aware boundary detection. Cheapest recovery — no messages dropped. - Tier 2: Drop oldest 50% of messages (existing forceCompression). - Tier 3: Both truncation + compression for maximum space reclamation. Each retry escalates to the next tier, giving the LLM progressively more aggressive context reduction before giving up.
This commit is contained in:
parent
94675e024a
commit
d83280739c
1 changed files with 64 additions and 5 deletions
|
|
@ -671,9 +671,11 @@ func (al *AgentLoop) runLLMIteration(ctx context.Context, agent *AgentInstance,
|
|||
strings.Contains(errMsg, "length")
|
||||
|
||||
if isContextError && retry < maxRetries {
|
||||
logger.WarnCF("agent", "Context window error detected, attempting compression", map[string]interface{}{
|
||||
logger.WarnCF("agent", "Context overflow recovery",
|
||||
map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"retry": retry,
|
||||
"tier": retry + 1,
|
||||
})
|
||||
|
||||
if retry == 0 && !constants.IsInternalChannel(opts.Channel) {
|
||||
|
|
@ -684,7 +686,19 @@ func (al *AgentLoop) runLLMIteration(ctx context.Context, agent *AgentInstance,
|
|||
})
|
||||
}
|
||||
|
||||
// 3-tier context overflow recovery:
|
||||
// Tier 1: Truncate oversized tool results (cheapest, no LLM call)
|
||||
// Tier 2: Drop oldest 50% of messages
|
||||
// Tier 3: Both truncation + compression (most aggressive)
|
||||
switch retry {
|
||||
case 0:
|
||||
al.truncateToolResults(agent, opts.SessionKey)
|
||||
case 1:
|
||||
al.forceCompression(agent, opts.SessionKey)
|
||||
default:
|
||||
al.truncateToolResults(agent, opts.SessionKey)
|
||||
al.forceCompression(agent, opts.SessionKey)
|
||||
}
|
||||
newHistory := agent.Sessions.GetHistory(opts.SessionKey)
|
||||
newSummary := agent.Sessions.GetSummary(opts.SessionKey)
|
||||
messages = agent.ContextBuilder.BuildMessages(
|
||||
|
|
@ -895,6 +909,51 @@ func (al *AgentLoop) maybeSummarize(agent *AgentInstance, sessionKey, channel, c
|
|||
}
|
||||
}
|
||||
|
||||
// maxToolResultChars is the maximum allowed length for a single tool result message.
|
||||
// Results exceeding this are truncated during context overflow recovery (Tier 1).
|
||||
// 8000 chars ≈ ~2000 tokens — large enough for useful output, small enough to reclaim space.
|
||||
const maxToolResultChars = 8000
|
||||
|
||||
// truncateToolResults walks the message history and truncates oversized tool
|
||||
// result messages in-place. This is the cheapest recovery tier — no messages
|
||||
// are dropped, only oversized Content fields are shortened.
|
||||
func (al *AgentLoop) truncateToolResults(agent *AgentInstance, sessionKey string) {
|
||||
history := agent.Sessions.GetHistory(sessionKey)
|
||||
truncated := 0
|
||||
|
||||
for i := range history {
|
||||
if history[i].Role != "tool" {
|
||||
continue
|
||||
}
|
||||
if utf8.RuneCountInString(history[i].Content) <= maxToolResultChars {
|
||||
continue
|
||||
}
|
||||
|
||||
// Truncate at a newline boundary if possible, otherwise at the limit.
|
||||
runes := []rune(history[i].Content)
|
||||
cutPoint := maxToolResultChars
|
||||
// Search backward from cut point for a newline (up to 200 chars back).
|
||||
for j := cutPoint; j > cutPoint-200 && j > 0; j-- {
|
||||
if runes[j] == '\n' {
|
||||
cutPoint = j
|
||||
break
|
||||
}
|
||||
}
|
||||
history[i].Content = string(runes[:cutPoint]) +
|
||||
"\n\n[... content truncated — original too large for context window]"
|
||||
truncated++
|
||||
}
|
||||
|
||||
if truncated > 0 {
|
||||
agent.Sessions.SetHistory(sessionKey, history)
|
||||
logger.WarnCF("agent", "Tool results truncated", map[string]interface{}{
|
||||
"session_key": sessionKey,
|
||||
"truncated": truncated,
|
||||
"max_chars": maxToolResultChars,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// forceCompression aggressively reduces context when the limit is hit.
|
||||
// It drops the oldest 50% of messages (keeping system prompt and last user message).
|
||||
func (al *AgentLoop) forceCompression(agent *AgentInstance, sessionKey string) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue