Skip to content

feat(agents): enable conservative retries by default for transient errors#449

Open
MarioCadenas wants to merge 2 commits into
mainfrom
feat/agent-default-retries
Open

feat(agents): enable conservative retries by default for transient errors#449
MarioCadenas wants to merge 2 commits into
mainfrom
feat/agent-default-retries

Conversation

@MarioCadenas

Copy link
Copy Markdown
Collaborator

What

Agents previously disabled retries entirely — agentStreamDefaults set retry: { enabled: false }. A single transient serving error (5xx, 429, connection reset) would fail the whole turn.

This enables a conservative default that reuses the existing RetryInterceptor (exponential backoff with full jitter, only retries transient/retryable errors):

retry: { enabled: true, attempts: 2, initialDelay: 500, maxDelay: 4_000 }
  • Old default: retries off — any transient blip failed the turn.
  • New default: at most one extra attempt, 500ms→4s jittered backoff. Non-retryable errors (4xx, AppKitError with isRetryable=false) are still never retried — see isRetryableError in interceptors/retry.ts.

No new retry logic was added; only the default config value changed.

Streaming / non-idempotency safety (the scope applied)

The agents plugin consumes retry in exactly one place: the streaming /chat path via executeStream (agents.ts:1162). This change is streaming-replay-safe by construction, not by configuration:

  • In executeStream, the interceptor chain wraps wrappedFn = async () => fn(signal), where fn is the async generator function for the turn.
  • Calling a generator function returns the generator object synchronously without running its body. So wrappedFn resolves immediately and the RetryInterceptor sees success on attempt 1.
  • Token emission and tool dispatch happen later, during yield* iteration — which runs outside the interceptor chain.
  • Therefore a transient error thrown after the first streamed event surfaces during iteration and is never seen by the retry loop. There is no path by which retry can re-emit tokens or re-run a tool side-effect (no double-charge / duplicate writes).
  • The only thing retried is a transient failure during generator setup, before any output — which is safe and idempotent.

The non-streaming /invocations and /responses path (_runAgentNonStreaming) does not route through execute()/executeStream() at all, so this default does not affect it.

Tests

Added packages/appkit/src/plugins/agents/tests/defaults.test.ts:

  • asserts the conservative default values (enabled, attempts === 2, bounded backoff caps).
  • proves no mid-stream replay: drives the real RetryInterceptor exactly as executeStream does, throws a 5xx after the first yielded token, and asserts the generator body ran once and the tool side-effect fired once (no replay).
  • proves a transient error during generator setup (before output) is retried.

Verification

  • pnpm --filter=@databricks/appkit typecheck — clean.
  • agents suite: 120 passed; retry.test.ts: 19 passed; new defaults.test.ts: 3 passed.
  • biome check on changed files — no fixes/errors.

This pull request and its description were written by Isaac.

…ent errors

Agent stream defaults previously set retry { enabled: false }. Enable a
conservative default (attempts: 2, 500ms..4s backoff) so a transient serving
error (5xx / 429 / connection reset) doesn't fail the whole turn. Non-retryable
errors (4xx, AppKitError isRetryable=false) are still never retried.

Streaming safety: in executeStream the RetryInterceptor wraps only the
synchronous creation of the adapter async generator; token emission and tool
dispatch run during yield* iteration, outside the interceptor chain. A transient
error thrown after the first streamed event therefore cannot be retried, so
retries can never re-emit tokens or re-run a tool side-effect. Only a failure
during generator setup (before any output) is retried. Added defaults.test.ts
asserting the conservative values and proving no mid-stream replay.

Co-authored-by: Isaac
Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
@MarioCadenas MarioCadenas requested a review from a team as a code owner June 13, 2026 19:26
@MarioCadenas MarioCadenas requested a review from pkosiec June 13, 2026 19:26
Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

📦 Bundle size report

Compared against bundle-size-baseline.json (main).

@databricks/appkit

npm tarball (packed): 659 KB (+460 B) — gzipped download (dist + bin; excludes release-only docs/NOTICE).

dist raw gzip
JS (runtime) 686 KB (+60 B) 240 KB (+40 B)
Type declarations 268 KB 91 KB
Source maps 1.3 MB (+973 B) 446 KB (+519 B)
Other 11 KB 3.7 KB
Total 2.3 MB (+1.0 KB) 780 KB (+559 B)
Per-entry composition (own code — deps external (as shipped))
Entry Initial (gz) Lazy (gz) Total (gz) node_modules (min) Own code (min)
. 74 KB 2.5 KB 76 KB external 243 KB
./beta 39 KB (+17 B) 231 B 39 KB (+17 B) external 117 KB (+41 B)
./type-generator 18 KB 0 B 18 KB external 53 KB

Chunks:

Entry Chunk Load Size (gz)
. index.js initial 70 KB
. utils.js initial 4.0 KB
. remote-tunnel-manager.js lazy 2.5 KB
./beta beta.js initial 30 KB
./beta databricks.js initial 5.7 KB
./beta service-context.js initial 3.1 KB
./beta client-options.js initial 220 B
./beta databricks.js lazy 128 B
./beta index.js lazy 103 B
./type-generator index.js initial 18 KB

@databricks/appkit-ui

npm tarball (packed): 298 KB — gzipped download (dist + bin; excludes release-only docs/NOTICE).

dist raw gzip
JS (runtime) 354 KB 117 KB
Type declarations 204 KB 73 KB
Source maps 676 KB 220 KB
CSS 16 KB 3.3 KB
Total 1.2 MB 413 KB
Per-entry composition (consumer bundle — deps bundled, peerDeps external)
Entry Initial (gz) Lazy (gz) Total (gz) node_modules (min) Own code (min)
./js 4.2 KB 49 KB 54 KB 208 KB 11 KB
./js/beta 20 B 0 B 20 B 0 B 0 B
./react 429 KB 49 KB 477 KB 1.3 MB 166 KB
./react/beta 20 B 0 B 20 B 0 B 0 B

Chunks:

Entry Chunk Load Size (gz)
./js index.js initial 4.1 KB
./js chunk initial 120 B
./js apache-arrow lazy 49 KB
./js/beta beta.js initial 20 B
./react index.js initial 427 KB
./react tslib initial 2.1 KB
./react apache-arrow lazy 49 KB
./react/beta beta.js initial 20 B

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant