fix(agent): retry on HTTP 5xx server errors instead of dropping the task

When the LLM API returns HTTP 5xx errors (500, 502, 503, 504, etc.),
the agent loop now retries with exponential backoff instead of dropping
the task immediately. This fixes issue #629 where long-running tasks
would hang without retry when the server returned HTTP 500.

The retry logic now handles three error categories:
- timeout errors: retry with backoff
- context/limit errors: retry with context compression
- server errors (HTTP 5xx): retry with backoff (NEW)
This commit is contained in:
merlinmiao 2026-04-05 10:20:25 +08:00
parent 15a70ac45c
commit 4d34fd47d8

View file

@ -2070,6 +2070,37 @@ turnLoop:
strings.Contains(errMsg, "prompt is too long") || strings.Contains(errMsg, "prompt is too long") ||
strings.Contains(errMsg, "request too large")) strings.Contains(errMsg, "request too large"))
isServerError := !isTimeoutError && strings.Contains(errMsg, "status: 5")
if isServerError && retry < maxRetries {
backoff := time.Duration(retry+1) * 5 * time.Second
al.emitEvent(
EventKindLLMRetry,
ts.eventMeta("runTurn", "turn.llm.retry"),
LLMRetryPayload{
Attempt: retry + 1,
MaxRetries: maxRetries,
Reason: "server_error",
Error: err.Error(),
Backoff: backoff,
},
)
logger.WarnCF("agent", "Server error, retrying after backoff", map[string]any{
"error": err.Error(),
"retry": retry,
"backoff": backoff.String(),
})
if sleepErr := sleepWithContext(turnCtx, backoff); sleepErr != nil {
if ts.hardAbortRequested() {
turnStatus = TurnEndStatusAborted
return al.abortTurn(ts)
}
err = sleepErr
break
}
continue
}
if isTimeoutError && retry < maxRetries { if isTimeoutError && retry < maxRetries {
backoff := time.Duration(retry+1) * 5 * time.Second backoff := time.Duration(retry+1) * 5 * time.Second
al.emitEvent( al.emitEvent(