Skip to content

fix(chatbot): deprecate the inert maxToolRoundtrips key (stage 1 of 2) - #5918

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-5605-max-tool-roundtrips
Aug 24, 2026
Merged

fix(chatbot): deprecate the inert maxToolRoundtrips key (stage 1 of 2)#5918
os-zhuang merged 2 commits into
mainfrom
claude/issue-5605-max-tool-roundtrips

Conversation

@claude

@claude claude Bot commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Part of #5605

Part of, not a closing keyword, and that is the finding: the card is an ADR-0049 enforce-or-remove call whose arm was to be chosen by measurement, and the measurement lands on the fallback arm the maintainer's 2026-08-22 ruling attached to Option A. Retirement is two-stage (maintainer ruling, 2026-08-22 item 13), so this PR ships stage 1 only and the card stays open for the deletion.

The measurement: the cap cannot be honoured from here

The ruling was "enforce, unless the AI SDK offers no corresponding round-trip-cap knob (or the semantics do not line up)". Read out of the installed packages, not their docs:

1. useChat has no numeric cap. @ai-sdk/react@4.0.68 declares UseChatOptions as ({ chat } | ChatInit) & { throttle, experimental_throttle, resume }. Its ChatInit (from ai@7.0.65) declares, in full: id, messageMetadataSchema, dataPartSchemas, messages, generateId, transport, onError, onToolCall, onFinish, onData, and one loop control —

sendAutomaticallyWhen?: (options: { messages: UI_MESSAGE[] }) => boolean | PromiseLike<boolean>;

A boolean predicate, not a count. maxToolRoundtrips and maxSteps have zero occurrences in either installed .d.ts.

2. That is deliberate upstream history, not an oversight. @ai-sdk/react's own CHANGELOG records the removal as a major:

## 1.0.0### Major Changese117b54: chore (ui): remove deprecated useChat roundtrip options

The installed version is four majors past that. The numeric successor migrated server-side: 332167b: chore (ai): move maxSteps into UseChatOptions64f6d64/d306260: feat (ai): replace maxSteps with continueUntil9315076: chore (ai): rename continueUntil to stopWhen. Rename maxSteps stop condition to isStepCount. In the installed ai package stopWhen is declared at exactly three sites — generateText, streamText, ToolLoopAgentSettings — all server-side call functions.

3. There is no client loop here to cap. useObjectChat never passes sendAutomaticallyWhen, so the client performs no automatic tool round-trips at all.

4. And no server loop we own. ObjectUI is backend-agnostic — api is whatever endpoint the author names. Putting the number in the request body would move the same dead key one hop further out, onto a wire contract no backend reads, while widening a public request shape. The platform's real cap is a different key with a different default: @objectstack/spec declares ChatWithToolsOptions.maxIterations (default 10) and agent planning.maxIterations (min(1).max(100).default(10), aliases maxSteps/maxLoops/iterations/maxTurnsmaxToolRoundtrips is not among them). The docs promised a default of 5.

So the semantics do not line up in either direction, and the fallback condition is met.

What stage 1 does

Nothing an author already wrote breaks. The key still parses, keeps its declared shape, and the renderer still threads it. What changes is that it stops lying:

  • @object-ui/types@deprecated on the interface field (the false @default 5 is gone) and a DEPRECATED (inert, slated for removal) zod description.
  • @object-ui/plugin-chatbot — authoring the key logs a one-time notice naming the knob that does work (planning.maxIterations). The @typescript-eslint/no-unused-vars warning the issue quoted resolves as a consequence of the value finally being read, not by deleting the destructure.
  • Docs — both locations marked deprecated. Declared as a bounded in-place fix: the issue names content/docs/plugins/plugin-chatbot.mdx:111/:155 as part of the same defect, and the ruling's remove arm names "both docs locations" explicitly. Leaving the docs promising a working cap would have left stage 1 half-done.

The runtime notice is load-bearing rather than decorative: BaseSchema is .passthrough(), so a JSON/AI metadata author gets no schema signal — @deprecated reaches TypeScript consumers only.

Verification — all at be40354d4

Targeted, path-filtered, heavy legs through the shared verify lock.

Leg Verdict line
vitest run packages/plugin-chatbot Test Files 26 passed (26) · Tests 354 passed (354)
vitest run packages/types Test Files 50 passed (50) · Tests 559 passed (559)
type-check (both packages) packages/types type-check: Done · packages/plugin-chatbot type-check: Done (script names echoed, so not a zero-match no-op)
check-changeset-presence ✅ 4 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)
check-changeset-no-major ✅ No changeset declares a major bump.
check-control-bytes ✅ check-control-bytes: OK (scanned 4918 tracked text file(s); skipped 85 binary).
check-phantom-dependencies ✅ Every in-scope import is declared by the package that publishes it.
check-doc-component-types ✅ Every documented component type is registered.
check-doc-links Links are valid across 13 scan roots.
check-spec-symbol-derivation ✅ spec symbol derivation: 1300 files scanned against 4959 spec export names
check-lint-coverage ✅ lint coverage: 46/46 packages linted, 0 with outstanding errors (0 total).
check-type-check-coverage ✅ type-check coverage: 45/46 via type-check … ✅ test type-check coverage: 41/41

Not run locally — check-doc-snippet-types. It refused to run on an unbuilt-workspace precondition unrelated to this diff: @object-ui/plugin-view declares types at packages/plugin-view/dist/index.d.ts and it is not on disk, 10 such packages, 0 of them touched here, and the edited block is a ```plaintext fence this gate does not typecheck. CI builds first.

Reverse verification

Each leg confirmed on disk by grepping injected and removed text (never an editor's exit code), each with a restore trap, direction predicted before running, tree restored after.

Ablation Predicted Observed
A — delete the warn call the two "warns" tests RED, silence control GREEN `2 failed
B — remove the authored-guard (!== undefined) only the silence CONTROL RED `1 failed
C — put the value on the wire "absent from the chat POST" RED `1 failed
D — remove the zod declaration (stage-2 sim) declaration pin RED `1 failed

Leg C is the one that matters most: "the value is absent from the request body" is precisely the assertion shape that passes when the harness is dead, so it was made to fail on demand. Its control asserts conversationId and model do arrive in that same POST.

Leg D first ran GREEN against a RED prediction — the green-direction failure. Cause was not dist/ staleness (packages/types/dist does not exist, and the root vitest config aliases @object-ui/typespackages/types/src, so this leg reads source): the original assertion was a phantom. It asserted an authored key survives safeParse, assuming z.object strips undeclared keys — but BaseSchema is .passthrough() (packages/types/src/zod/base.zod.ts:197), so it passes for any key, declared or not. The pin was rewritten onto ChatbotSchema.shape membership, which is the only observable separating "declared" from "gone", and re-ablated to RED. The parse case is kept, explicitly labelled non-discriminating, as a regression floor.

Legs that do not discriminate, named rather than implied: the docs edit and the interface @deprecated JSDoc are observed by no test — JSDoc is type-level only, and no gate reads the prop table's prose.

Declared lint narrowing

Repo-scale turbo run lint is CI's run. Narrowed to the two touched packages, with the three pieces of evidence:

  1. Population from eslint's own config, not a guess: eslint . run inside each package, resolving the root flat eslint.config.js itself.
  2. Counts from --format json: 58 files (plugin-chatbot) + 111 (types) = 169, 0 errors in both.
  3. Invariance for untouched files: eslint.config.js (251 lines) sets no projectService, no parserOptions.project, no recommendedTypeCheckedtype-aware linting is not enabled, so every rule is single-file and this diff cannot move the verdict on a file it did not touch.

Corroborated independently by check-lint-coverage: 46/46 packages linted, 0 with outstanding errors.

The measured consequence: useObjectChat.ts went from 12 warnings (including 363:5 'maxToolRoundtrips' is assigned a value but never used) to 11 — the remaining 11 are pre-existing react-hooks/refs and no-explicit-any, untouched here. The new test file lints clean.

Follow-up

Stage 2 — deleting the key from the interface, the zod schema, the three renderer call sites and the docs — is filed as #5919, gated on this deprecation shipping in a release. It is deliberately not done here.

That card carries one measured warning for whoever takes it: because BaseSchema is .passthrough(), deleting the declaration will not make an authoring document fail — it keeps parsing green, silently.


Generated by Claude Code


Generated by Claude Code

claude added 2 commits August 23, 2026 20:05
`maxToolRoundtrips` is declared authorable in `@object-ui/types` (interface +
zod), threaded through the chatbot renderer at three call sites, accepted by
`useObjectChat`, given a default — and then dropped on the floor. ADR-0049 says
enforce-or-remove; measuring the installed runtime says it cannot be enforced
from here.

`@ai-sdk/react`'s `useChat` takes `ChatInit` + `{throttle, resume}`, and
`ChatInit` declares exactly one loop control — the boolean predicate
`sendAutomaticallyWhen` — and no numeric cap under any spelling. The numeric
knob was removed from `useChat` in a major; its successor `maxSteps` became
`stopWhen`/`stepCountIs`, declared only on `generateText`, `streamText` and
`ToolLoopAgentSettings` — all server-side. This hook never passes
`sendAutomaticallyWhen`, so there is no client loop to cap; and ObjectUI is
backend-agnostic, so there is no server loop we own either.

Stage 1 of a two-stage retirement: the key still parses and keeps its declared
shape, but it is marked `@deprecated` in the interface, the zod description and
the docs, and authoring it now logs a one-time notice pointing at the knob that
does work (`planning.maxIterations` on the agent). The unused-var lint warning
resolves as a consequence of the value finally being read — not by silencing it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EuPCi56cnGyykygi3z9w4m
The first version asserted that an authored `maxToolRoundtrips` survives
`ChatbotSchema.safeParse`, on the assumption that `z.object` strips undeclared
keys. It does not here: `BaseSchema` is `.passthrough()`
(packages/types/src/zod/base.zod.ts:197), so the key round-trips whether or not
it is declared — the assertion could not fail. Measured by ablating the zod
declaration and watching the parse-based assertion stay green.

Membership in `ChatbotSchema.shape` is the observable that actually separates
"declared authorable" from "gone", so the pin now reads the shape (and the
description's deprecation marker). Re-ablated: the rewritten assertion fails
with `expected [ Array(37) ] to include 'maxToolRoundtrips'`.

The parse case is kept as an explicitly labelled regression floor. Its
non-discrimination is itself worth recording for stage 2: deleting the
declaration will not make an authoring document fail, it will keep passing
silently — which is why the runtime notice is the part that reaches an author.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EuPCi56cnGyykygi3z9w4m
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 52 chunks) 3230.7 KB 3990.2 KB
Main entry chunk (gzip) 153.6 KB 350 KB
Entry file index-Ck-n5Ytx.js
Status PASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 10.13KB 3.77KB
app-shell (runtime-config.js) 13.57KB 4.78KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 5.13KB 2.35KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 504.18KB 114.10KB
core (index.js) 4.92KB 1.97KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 164.55KB 45.67KB
fields (index.js) 238.40KB 59.89KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (i18n.js) 4.28KB 1.75KB
i18n (index.js) 3.44KB 1.39KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 23.13KB 7.63KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 33.40KB 8.71KB
i18n (useSafeTranslation.js) 7.77KB 3.13KB
layout (index.js) 38.95KB 10.97KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.55KB 0.62KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useResponsiveConfig.js) 1.37KB 0.63KB
mobile (useSpecGesture.js) 4.32KB 1.64KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 9.53KB 3.38KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 4.64KB 1.50KB
permissions (evaluator.js) 5.12KB 1.74KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 1.93KB 0.88KB
plugin-ai (index.js) 15.75KB 3.80KB
plugin-calendar (index.js) 46.62KB 12.83KB
plugin-charts (index.js) 64.66KB 18.32KB
plugin-chatbot (index.js) 187.90KB 44.61KB
plugin-dashboard (index.js) 133.32KB 34.42KB
plugin-designer (index.js) 212.30KB 42.80KB
plugin-detail (index.js) 244.01KB 61.72KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 125.63KB 30.64KB
plugin-gantt (index.js) 164.15KB 39.88KB
plugin-grid (index.js) 200.79KB 54.26KB
plugin-kanban (index.js) 52.93KB 14.60KB
plugin-list (index.js) 111.86KB 27.22KB
plugin-map (index.js) 20.06KB 6.62KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 43.49KB 11.93KB
plugin-timeline (index.js) 26.49KB 7.59KB
plugin-tree (index.js) 8.50KB 2.88KB
plugin-view (index.js) 84.57KB 20.74KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 3.77KB 1.33KB
react (SchemaRenderer.js) 52.40KB 17.45KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.35KB 0.70KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 5.41KB 2.34KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 4.93KB 2.24KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 12.13KB 3.65KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 7.54KB 2.63KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 0.20KB 0.18KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 0.20KB 0.18KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.87KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (index.js) 3.88KB 1.85KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 2.59KB 1.31KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 3.40KB 1.71KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-zhuang
os-zhuang marked this pull request as ready for review August 24, 2026 00:51
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 24, 2026
Merged via the queue into main with commit 0fce2ef Aug 24, 2026
23 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-5605-max-tool-roundtrips branch August 24, 2026 00:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants