toolsrv: typed RateLimitedError, agent loop: don't count rate-limited calls as consecutive errors
This commit is contained in:
parent
5c27a948bd
commit
7a69f14277
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue