mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-07-20 06:19:24 +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:
+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})
|
||||
}
|
||||
|
||||
// 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
|
||||
emitRun(AgentEvent{
|
||||
Type: protocol.AgentEventActivity,
|
||||
@@ -645,6 +660,16 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (*RunResult, error)
|
||||
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).
|
||||
model := l.model
|
||||
if req.ModelOverride != "" {
|
||||
@@ -924,7 +949,7 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (*RunResult, error)
|
||||
}
|
||||
}
|
||||
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()
|
||||
|
||||
@@ -1100,7 +1125,7 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (*RunResult, error)
|
||||
}
|
||||
}
|
||||
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()
|
||||
l.emitToolSpanEnd(ctx, spanID, spanStart, result)
|
||||
|
||||
@@ -7,7 +7,7 @@ const (
|
||||
DefaultContextWindow = 200000
|
||||
DefaultMaxTokens = 8192
|
||||
DefaultMaxMessageChars = 32000
|
||||
DefaultMaxIterations = 20
|
||||
DefaultMaxIterations = 30
|
||||
DefaultTemperature = 0.7
|
||||
DefaultHistoryShare = 0.75
|
||||
)
|
||||
|
||||
@@ -412,6 +412,26 @@ func RunMediaNamesFromCtx(ctx context.Context) map[string]string {
|
||||
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 ---
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
channel := ToolChannelFromCtx(ctx)
|
||||
cacheKey := fmt.Sprintf("fetch:%s:%s:%s:%d", channel, rawURL, extractMode, maxChars)
|
||||
|
||||
Reference in New Issue
Block a user