loop: per-step budgets — replan gate (8 rounds) and stall detection (5 rounds)
This commit is contained in:
parent
8be8b9c42d
commit
011843a59d
|
|
@ -28,6 +28,14 @@ const (
|
|||
consecutiveErrorHardLimit = 10
|
||||
)
|
||||
|
||||
// replanGate is the max consecutive tool rounds without a PLAN: block before
|
||||
// a mandatory replan nudge is injected.
|
||||
const replanGate = 8
|
||||
|
||||
// stallThreshold is the number of consecutive rounds where LastAction doesn't
|
||||
// change before injecting a stall nudge.
|
||||
const stallThreshold = 5
|
||||
|
||||
// planReinjectInterval is the number of tool-call rounds between periodic
|
||||
// task state re-injection into the conversation.
|
||||
const planReinjectInterval = 10
|
||||
|
|
@ -65,6 +73,9 @@ type agentConfig struct {
|
|||
func run(ctx context.Context, cfg agentConfig, state state) error {
|
||||
var step int
|
||||
var consecutiveErrors int // rounds where every tool call returned an error
|
||||
var roundsWithoutPlan int // consecutive tool rounds without a PLAN: block
|
||||
var lastAction string // previous round's LastAction for stall detection
|
||||
var stallRounds int // consecutive rounds with unchanged LastAction
|
||||
// resultCache stores outputs of read-safe tool calls keyed by name+args.
|
||||
// Only tools classified as parallel-safe (immutable reads) are cached.
|
||||
// sync.Map is required because execOne may run in concurrent goroutines.
|
||||
|
|
@ -492,6 +503,39 @@ func run(ctx context.Context, cfg agentConfig, state state) error {
|
|||
}, nil)
|
||||
}
|
||||
|
||||
// Replan gate: if the model has been calling tools without emitting a
|
||||
// PLAN: block, force it to replan before continuing.
|
||||
if strings.Contains(msg.Content, "PLAN:") {
|
||||
roundsWithoutPlan = 0
|
||||
} else {
|
||||
roundsWithoutPlan++
|
||||
}
|
||||
if roundsWithoutPlan >= replanGate {
|
||||
state.update(backend.Message{
|
||||
Role: "user",
|
||||
Content: "[system: you have executed " + fmt.Sprintf("%d", replanGate) + " tool rounds without replanning. Stop and write a PLAN: block showing your current checklist before calling any more tools. Update s/$OLLIE_SESSION_ID/plan.]",
|
||||
}, nil)
|
||||
roundsWithoutPlan = 0
|
||||
}
|
||||
|
||||
// Stall detection: if LastAction hasn't changed across rounds, the
|
||||
// model is likely stuck in a loop.
|
||||
if ts := state.taskState(); ts != nil {
|
||||
if ts.LastAction == lastAction && lastAction != "" {
|
||||
stallRounds++
|
||||
} else {
|
||||
stallRounds = 0
|
||||
}
|
||||
lastAction = ts.LastAction
|
||||
if stallRounds >= stallThreshold {
|
||||
state.update(backend.Message{
|
||||
Role: "user",
|
||||
Content: "[system: stall detected — you have repeated the same action for " + fmt.Sprintf("%d", stallRounds) + " rounds. Step back, reassess your approach, and try something different. Update your plan.]",
|
||||
}, nil)
|
||||
stallRounds = 0
|
||||
}
|
||||
}
|
||||
|
||||
// Soft step-budget guardrail: when MaxSteps is set and the budget is
|
||||
// exhausted, nudge the model to wrap up and exit cleanly. This is not
|
||||
// a hard abort — the model gets one final turn without tools to emit
|
||||
|
|
|
|||
Reference in New Issue