cloudflare / cloudflare/agents
Add an agents/retries subpath and standardize retry utilities
- Dominant language
- TypeScript
- Stars
- 5.6k
- Forks
- 711
- Avg merge
- 1d 20h
- Merged PRs (30d)
- 53
Description
## Problem
Retry behavior is currently split across two implementations:
- `packages/agents/src/retries.ts` — `tryN`, full-jitter backoff, Agent/task retry validation, and Durable Object/platform error classifiers
- `packages/agents/src/lifecycle/durable-object-lifecycle.ts` — a separate retry loop for `routeDurableObjectRequest()` and `getAgentByName()` startup RPCs
This duplicates attempt accounting, validation, jitter, and Durable Object error classification. It also prevents other Workers and Durable Object code from importing the generic, already-tested utilities without importing the `agents` root entry.
## Proposal
Add a public `agents/retries` subpath and consolidate retry execution behind one implementation.
Use Lambros Petrou's `durable-utils` retry implementation as the design/reference baseline:
https://github.com/lambrospetrou/durable-utils/blob/main/src/retries.ts
Its useful semantics include:
- attempts are 1-indexed;
- the maximum is expressed as total attempts, including the first;
- retry predicates receive `(error, nextAttempt)`;
- exponential backoff uses full jitter;
- `tryN` is a bounded specialization of a predicate-driven retry loop;
- Durable Object error classification remains separate from generic retry control.
Adapt this to the stronger validation and platform classification already present in Agents rather than replacing those pieces wholesale.
## Initial public surface
Expose existing, proven utilities only:
```ts
import {
tryN,
jitterBackoff,
isErrorRetryable,
isPlatformTransientError,
type RetryOptions
} from "agents/retries";
```
Additional functions from the reference implementation, such as `tryWhile`, should be exported only if the consolidation requires them and their semantics are covered by a concrete consumer and tests. Do not add speculative strategies or options.
Keep existing root exports and Agent APIs compatible:
- `Agent.retry(..., { shouldRetry })`
- schedule/queue `RetryOptions`
- `getAgentByName(..., { routingRetry })`
- `routeAgentRequest(..., { routingRetry })`
The subpath should provide reusable primitives; domain APIs may continue translating their existing option names at their boundaries.
## Semantics that must be characterized before refactoring
The two current implementations are similar but not identical. Add tests that pin each behavior before consolidation:
1. **Attempt numbering**
- attempts start at `1`;
- predicates receive the next attempt number;
- `maxAttempts` includes the initial attempt.
2. **Backoff windows**
- generic `tryN` currently calls `jitterBackoff(attempt, ...)`;
- routing currently uses `baseDelayMs * 2 ** (attempt - 1)`;
- do not silently change either timing distribution. Any intended normalization must be explicit and reviewed.
3. **Defaults**
- generic retries default to `3 / 100ms / 3000ms` where applicable;
- routing retries default to `3 / 100ms / 800ms`;
- preserve domain defaults.
4. **Classification**
- retry platform errors marked `retryable: true`;
- never retry overloaded Durable Object errors;
- preserve existing message and `cause`-chain handling where currently used;
- memory-limit resets remain poison-pill signals, not generic transients.
5. **Callbacks**
- routing `onRetry` receives object name, class name, attempt, max attempts, and delay;
- a routing `onRetry` callback failure is logged and does not cancel the real retry;
- preserve whether `attempt` means failed or next attempt, or explicitly correct it with tests/docs and an appropriate release note.
6. **Disable path**
- `routingRetry: false` performs exactly one operation.
7. **Errors and responses**
- thrown retryable infrastructure errors may retry;
- ordinary application errors do not retry under routing policy;
- returned HTTP `500` responses are values and are not retried.
## Suggested structure
```text
packages/agents/src/
retries.ts # shared implementation
lifecycle/
routing.ts # routing policy/context composed from retries
```
`agents/retries` should export the reusable primitives from `retries.ts`.
Lifecycle routing should retain only routing-specific defaults, context, and event projection.
## Acceptance criteria
- [ ] Add the `agents/retries` package export and build output.
- [ ] Add characterization tests for both existing retry loops before replacing either implementation.
- [ ] Consolidate retry execution so lifecycle routing no longer maintains a second generic retry loop.
- [ ] Preserve existing Agent, queue, schedule, workflow, MCP, and routing behavior.
- [ ] Preserve existing root exports and option shapes.
- [ ] Keep Durable Object/platform classifiers reusable and tested.
- [ ] Document attempt numbering, max-attempt semantics, jitter, and classifier behavior.
- [ ] Add a changeset.
- [ ] Run the full repository check and affected Workers tests.
## Non-goals
- New backoff algorithms or presets
- Automatic retries for lifecycle capability hooks
- Retrying user `onRequest`, WebSocket callbacks, or arbitrary application errors
- Durable/checkpointed retries
- Proxying every Durable Object RPC call with implicit retries
Contributor guide
Assessment
This issue has not been assessed yet.