Skip to content

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

Closed
MarioCadenas wants to merge 1 commit into
mainfrom
feat/agent-default-retries
Closed

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

Conversation

@MarioCadenas

@MarioCadenas MarioCadenas commented Jun 13, 2026

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.

@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
@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): 722 KB (+20 KB) — gzipped download (dist + bin; excludes release-only docs/NOTICE).

dist raw gzip
JS (runtime) 746 KB (+18 KB) 261 KB (+6.7 KB)
Type declarations 281 KB (+6.4 KB) 96 KB (+2.4 KB)
Source maps 1.5 MB (+35 KB) 487 KB (+13 KB)
Other 11 KB 3.7 KB
Total 2.5 MB (+60 KB) 848 KB (+22 KB)
Per-entry composition (own code — deps external (as shipped))
Entry Initial (gz) Lazy (gz) Total (gz) node_modules (min) Own code (min)
. 80 KB (+1.1 KB) 2.5 KB 82 KB (+1.1 KB) external 262 KB (+3.4 KB)
./beta 39 KB (+476 B) 231 B (+1 B) 40 KB (+477 B) external 118 KB (+1.3 KB)
./type-generator 19 KB 0 B 19 KB external 54 KB

Chunks:

Entry Chunk Load Size (gz)
. index.js initial 76 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.8 KB
./beta service-context.js initial 3.2 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 19 KB

@databricks/appkit-ui

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

dist raw gzip
JS (runtime) 350 KB 116 KB
Type declarations 201 KB 72 KB (+2 B)
Source maps 669 KB 218 KB (-2 B)
CSS 16 KB 3.3 KB
Total 1.2 MB 410 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.3 KB 49 KB 54 KB 208 KB 12 KB
./js/beta 20 B 0 B 20 B 0 B 0 B
./react 428 KB 49 KB 476 KB 1.3 MB 163 KB
./react/beta 20 B 0 B 20 B 0 B 0 B

Chunks:

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

@MarioCadenas
MarioCadenas force-pushed the feat/agent-default-retries branch from 7b41bb7 to 2cb5e1c Compare July 22, 2026 15:34
…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 force-pushed the feat/agent-default-retries branch from 2cb5e1c to f24d238 Compare July 22, 2026 15:43
@MarioCadenas
MarioCadenas deleted the feat/agent-default-retries branch July 22, 2026 15:59
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