Skip to content

Fix TS recursion limit in deep types#904

Merged
whoiskatrin merged 2 commits intomainfrom
opencode/issue903-20260213123934
Feb 13, 2026
Merged

Fix TS recursion limit in deep types#904
whoiskatrin merged 2 commits intomainfrom
opencode/issue903-20260213123934

Conversation

@ask-bonk
Copy link
Contributor

@ask-bonk ask-bonk bot commented Feb 13, 2026

Here's a summary of the changes:

Changes

packages/agents/src/serializable.ts

Added a tuple-based depth counter to CanSerialize and IsSerializableParam types that bails out to true after 10 levels of recursion. Three new helper types were added:

  • MaxDepth — a 10-element tuple defining the recursion limit
  • Increment<D> — prepends an element to the depth tuple
  • IsMaxDepth<D> — checks if the depth tuple has reached the limit

Both CanSerialize<T, Seen, Depth> and IsSerializableParam<T, Seen, Depth> now check IsMaxDepth<Depth> before recursing deeper. If the depth limit is reached without encountering a NonSerializable type, the type is assumed to be JSON-safe (returns true).

The existing Seen parameter for circular type detection is preserved — the depth counter addresses the orthogonal problem of types that are simply deeply nested (many levels of unions and nested objects) without being circular.

packages/agents/src/tests-d/serializable.test-d.ts

Added type-level tests with a simulated CoreMessage-like type structure (deeply nested discriminated unions with TextPart | ToolCallPart | ImagePart, metadata objects, etc.) to verify these types don't trigger TypeScript's recursion limit when used with useAgent.

Verification

  • All 35 projects typecheck successfully
  • All 464 Workers tests pass (30 test files)
  • Build succeeds cleanly

Closes #903

github run

Co-authored-by: whoiskatrin <whoiskatrin@users.noreply.github.com>
@changeset-bot
Copy link

changeset-bot bot commented Feb 13, 2026

🦋 Changeset detected

Latest commit: 04fbded

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
agents Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pkg-pr-new
Copy link

pkg-pr-new bot commented Feb 13, 2026

Open in StackBlitz

npm i https://pkg.pr.new/cloudflare/agents@904

commit: 04fbded

@whoiskatrin
Copy link
Contributor

/bonk review this PR

@ask-bonk
Copy link
Contributor Author

ask-bonk bot commented Feb 13, 2026

Here's my review of this PR:


PR Review: Fix TS recursion limit in deep types

Summary

This PR adds a tuple-based depth counter to CanSerialize and IsSerializableParam types in serializable.ts to prevent TypeScript's "Type instantiation is excessively deep and possibly infinite" error when using deeply nested state types (like AI SDK CoreMessage[]). Closes #903.

Verdict: Approve with minor nits

The core approach is sound and follows a well-established pattern for depth-limiting recursive TypeScript types. The implementation matches the suggestion in the issue exactly.

What's good

  1. Correct technique — Tuple-based depth counting is the idiomatic way to limit recursive conditional types in TypeScript. Increment<D> via [0, ...D] and checking D["length"] against the max is clean and efficient.

  2. Conservative default — Bailing to true (assume serializable) at the depth limit is the right call. A false positive (allowing a non-serializable type through) will fail at runtime with a clear error, while a false negative (rejecting a valid type) gives users a confusing type error with no workaround.

  3. Preserves Seen for circularity — The existing circular reference detection is orthogonal and correctly preserved alongside the new depth counter.

  4. Good test coverage — The CoreMessage-like test types are realistic and representative of the actual AI SDK types that trigger this issue. The tests cover the happy path (deeply nested serializable types work) without weakening the existing negative tests (non-serializable types are still caught).

  5. No API surface change — The Depth parameter defaults to [], so all existing usage is unaffected.

Issues to fix

  1. Stale issue URL in test comment (serializable.test-d.ts:159):
    // See: https://github.com/cloudflare/agents/issues/XXX
    This should be issues/903.

Minor observations

  1. package-lock.json churn — The lockfile diff removes "peer": true from ~12 packages (@ai-sdk/provider, vite, @babel/core, @cloudflare/kumo, etc.). This looks like a lockfile regeneration artifact from running npm install on a slightly different npm version or with different hoisting. It's harmless but adds noise to the diff. Consider regenerating from a clean npm ci && npm install to confirm these changes are intentional, or revert the lockfile to keep the diff focused.

  2. Depth limit of 10 is reasonable — For context, TypeScript's internal recursion limit is ~50 levels. With union distribution, each "level" of the type can multiply into several branches. 10 is a pragmatic middle ground — deep enough to catch NonSerializable types in realistic structures, shallow enough to avoid hitting the TS limit even with wide unions. If someone later finds 10 is too aggressive (misses a Date buried 12 levels deep), it can be bumped, but this seems very unlikely in practice.

  3. Object branch: depth increments per-property — In the mapped type { [K in keyof T]: CanSerialize<T[K], Seen | T, Increment<Depth>> }, every property at the same level gets the same Increment<Depth>. This means depth measures nesting depth, not total nodes visited, which is the correct behavior for preventing recursion blowup.

Suggested fix for issue #1

In packages/agents/src/tests-d/serializable.test-d.ts:159:

-// See: https://github.com/cloudflare/agents/issues/XXX
+// See: https://github.com/cloudflare/agents/issues/903

Shall I apply that fix?

github run

@whoiskatrin
Copy link
Contributor

/bonk apply the issue fix

Co-authored-by: whoiskatrin <whoiskatrin@users.noreply.github.com>
@ask-bonk
Copy link
Contributor Author

ask-bonk bot commented Feb 13, 2026

Fixed the stale issue URL in packages/agents/src/tests-d/serializable.test-d.ts:159 — replaced the placeholder XXX with the actual issue number 903.

github run

@whoiskatrin whoiskatrin merged commit d611b94 into main Feb 13, 2026
4 checks passed
@whoiskatrin whoiskatrin deleted the opencode/issue903-20260213123934 branch February 13, 2026 12:54
@github-actions github-actions bot mentioned this pull request Feb 13, 2026
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.

CanSerialize type hits TypeScript recursion limit with deeply nested state types

1 participant