mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-19 22:15:02 +00:00
fix(agent): prevent "..." fallback when iteration budget exhausted
Add early detection and graceful degradation when the model exhausts its iteration budget without producing a text response: - Inject 75% budget nudge telling model to start summarizing - Strip all tool schemas on final iteration to force text-only response - Adaptive web_fetch maxChars (60K→20K→10K) via iteration progress context - Bump DefaultMaxIterations 20→30 for web-research workloads
This commit is contained in:
1 parent
84650c5c14
commit
dba3f4e4e6
4 files changed
+59
-3
No files matched your search
+27
-2
@@ -584,6 +584,21 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (*RunResult, error)
|
|||||||
messages = append(messages, providers.Message{Role: "user", Content: nudge})
|
messages = append(messages, providers.Message{Role: "user", Content: nudge})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Iteration budget nudge: when model has used 75% of iterations without
|
||||||
|
// producing any text response, warn it to start summarizing.
|
||||||
|
if maxIter > 0 && iteration > 1 && iteration == maxIter*3/4 && finalContent == "" {
|
||||||
|
messages = append(messages, providers.Message{
|
||||||
|
Role: "user",
|
||||||
|
Content: "[System] You have used 75% of your iteration budget without providing a text response. Start summarizing your findings and respond to the user within the next few iterations.",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inject iteration progress into context so tools can adapt (e.g. web_fetch reduces maxChars).
|
||||||
|
iterCtx := tools.WithIterationProgress(ctx, tools.IterationProgress{
|
||||||
|
Current: iteration,
|
||||||
|
Max: maxIter,
|
||||||
|
})
|
||||||
|
|
||||||
// Emit activity event: thinking phase
|
// Emit activity event: thinking phase
|
||||||
emitRun(AgentEvent{
|
emitRun(AgentEvent{
|
||||||
Type: protocol.AgentEventActivity,
|
Type: protocol.AgentEventActivity,
|
||||||
@@ -645,6 +660,16 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (*RunResult, error)
|
|||||||
toolDefs = filtered
|
toolDefs = filtered
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Final iteration: strip all tools to force a text-only response.
|
||||||
|
// Without this the model may keep requesting tools and exit with "...".
|
||||||
|
if iteration == maxIter {
|
||||||
|
toolDefs = nil
|
||||||
|
messages = append(messages, providers.Message{
|
||||||
|
Role: "user",
|
||||||
|
Content: "[System] Final iteration reached. Summarize all findings and respond to the user now. No more tool calls allowed.",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Use per-request model override if set (e.g. heartbeat uses cheaper model).
|
// Use per-request model override if set (e.g. heartbeat uses cheaper model).
|
||||||
model := l.model
|
model := l.model
|
||||||
if req.ModelOverride != "" {
|
if req.ModelOverride != "" {
|
||||||
@@ -924,7 +949,7 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (*RunResult, error)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if result == nil {
|
if result == nil {
|
||||||
result = l.tools.ExecuteWithContext(ctx, registryName, tc.Arguments, req.Channel, req.ChatID, req.PeerKind, req.SessionKey, nil)
|
result = l.tools.ExecuteWithContext(iterCtx, registryName, tc.Arguments, req.Channel, req.ChatID, req.PeerKind, req.SessionKey, nil)
|
||||||
}
|
}
|
||||||
stopSlowTimer()
|
stopSlowTimer()
|
||||||
|
|
||||||
@@ -1100,7 +1125,7 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (*RunResult, error)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if result == nil {
|
if result == nil {
|
||||||
result = l.tools.ExecuteWithContext(ctx, registryName, tc.Arguments, req.Channel, req.ChatID, req.PeerKind, req.SessionKey, nil)
|
result = l.tools.ExecuteWithContext(iterCtx, registryName, tc.Arguments, req.Channel, req.ChatID, req.PeerKind, req.SessionKey, nil)
|
||||||
}
|
}
|
||||||
stopSlowTimer()
|
stopSlowTimer()
|
||||||
l.emitToolSpanEnd(ctx, spanID, spanStart, result)
|
l.emitToolSpanEnd(ctx, spanID, spanStart, result)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ const (
|
|||||||
DefaultContextWindow = 200000
|
DefaultContextWindow = 200000
|
||||||
DefaultMaxTokens = 8192
|
DefaultMaxTokens = 8192
|
||||||
DefaultMaxMessageChars = 32000
|
DefaultMaxMessageChars = 32000
|
||||||
DefaultMaxIterations = 20
|
DefaultMaxIterations = 30
|
||||||
DefaultTemperature = 0.7
|
DefaultTemperature = 0.7
|
||||||
DefaultHistoryShare = 0.75
|
DefaultHistoryShare = 0.75
|
||||||
)
|
)
|
||||||
@@ -412,6 +412,26 @@ func RunMediaNamesFromCtx(ctx context.Context) map[string]string {
|
|||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Iteration progress (loop → tools) ---
|
||||||
|
|
||||||
|
const ctxIterProgress toolContextKey = "tool_iter_progress"
|
||||||
|
|
||||||
|
// IterationProgress carries the agent loop's current iteration state
|
||||||
|
// so tools can adapt behaviour (e.g. reduce output size) as the budget shrinks.
|
||||||
|
type IterationProgress struct {
|
||||||
|
Current int
|
||||||
|
Max int
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithIterationProgress(ctx context.Context, p IterationProgress) context.Context {
|
||||||
|
return context.WithValue(ctx, ctxIterProgress, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func IterationProgressFromCtx(ctx context.Context) (IterationProgress, bool) {
|
||||||
|
v, ok := ctx.Value(ctxIterProgress).(IterationProgress)
|
||||||
|
return v, ok
|
||||||
|
}
|
||||||
|
|
||||||
// --- Per-agent sandbox config override ---
|
// --- Per-agent sandbox config override ---
|
||||||
|
|
||||||
const ctxSandboxCfg toolContextKey = "tool_sandbox_config"
|
const ctxSandboxCfg toolContextKey = "tool_sandbox_config"
|
||||||
|
|||||||
@@ -192,6 +192,17 @@ func (t *WebFetchTool) Execute(ctx context.Context, args map[string]any) *Result
|
|||||||
maxChars = int(mc)
|
maxChars = int(mc)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Adaptive maxChars: reduce as iterations progress to prevent context bloat.
|
||||||
|
if prog, ok := IterationProgressFromCtx(ctx); ok && prog.Max > 0 {
|
||||||
|
ratio := float64(prog.Current) / float64(prog.Max)
|
||||||
|
switch {
|
||||||
|
case ratio >= 0.75:
|
||||||
|
maxChars = min(maxChars, 10000)
|
||||||
|
case ratio >= 0.50:
|
||||||
|
maxChars = min(maxChars, 20000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Check cache (scoped per channel to prevent cross-channel cache poisoning)
|
// Check cache (scoped per channel to prevent cross-channel cache poisoning)
|
||||||
channel := ToolChannelFromCtx(ctx)
|
channel := ToolChannelFromCtx(ctx)
|
||||||
cacheKey := fmt.Sprintf("fetch:%s:%s:%s:%d", channel, rawURL, extractMode, maxChars)
|
cacheKey := fmt.Sprintf("fetch:%s:%s:%s:%d", channel, rawURL, extractMode, maxChars)
|
||||||
|
|||||||
Reference in new issue
Block a user