toolsrv: typed RateLimitedError, agent loop: don't count rate-limited calls as consecutive errors

This commit is contained in:
Ollie Agent 2026-07-31 07:46:32 +02:00
parent 5c27a948bd
commit 7a69f14277
3 changed files with 30 additions and 3 deletions

View File

@ -339,7 +339,12 @@ func run(ctx context.Context, cfg agentConfig, state state) error {
emit(cfg, Event{Role: "tool", Name: tc.Name, Content: result})
return toolResult{ToolCallID: tc.ID, Name: tc.Name, Content: result, IsError: true}, true
}
result = fmt.Sprintf("error: %v", err)
var rlErr *toolsrv.RateLimitedError
if errors.As(err, &rlErr) {
result = fmt.Sprintf("error: shell is rate-limited — blocked for %v. Do not call shell() again until the block expires. Use other tools or wait.", rlErr.Remaining)
} else {
result = fmt.Sprintf("error: %v", err)
}
} else {
result = out
resultBlocks = blocks
@ -516,14 +521,24 @@ func run(ctx context.Context, cfg agentConfig, state state) error {
}
// Track consecutive rounds where every tool call errored.
// Rate-limited errors don't count — they're a temporary block,
// not a real failure. Reset consecutiveErrors so the hard abort
// doesn't fire.
allErrors := len(results) > 0
allRateLimited := allErrors
for _, r := range results {
if !r.IsError {
allErrors = false
allRateLimited = false
break
}
if !strings.HasPrefix(r.Content, "error: shell is rate-limited") {
allRateLimited = false
}
}
if allErrors {
if allRateLimited {
consecutiveErrors = 0
} else if allErrors {
consecutiveErrors++
} else {
consecutiveErrors = 0

View File

@ -20,6 +20,18 @@ const (
blockDuration = 5 * time.Minute
)
// RateLimitedError is returned by checkRateLimit when the shell is blocked
// due to too many validation failures. The agent loop uses this to detect
// the condition and inject a clear signal rather than counting it as a
// normal tool error.
type RateLimitedError struct {
Remaining time.Duration
}
func (e *RateLimitedError) Error() string {
return fmt.Sprintf("rate limited: too many validation failures, blocked for %v", e.Remaining)
}
// Server runs code in a sandboxed environment.
type Server struct {
// cwd is the working directory for sandboxed commands. If empty,

View File

@ -383,7 +383,7 @@ func (e *Server) checkRateLimit() error {
now := time.Now()
if now.Before(e.blockedUntil) {
remaining := e.blockedUntil.Sub(now).Round(time.Second)
return fmt.Errorf("rate limited: too many validation failures, blocked for %v", remaining)
return &RateLimitedError{Remaining: remaining}
}
return nil
}