Skip to content
This repository was archived by the owner on Oct 26, 2026. It is now read-only.

Commit b46c58f

Browse files
fix(types,plugin-chatbot): the authoring tool-invocation state union sheds the three runtime-only approval states (objectui#10018) (#10308)
Fixes #10018 Clause-②: no This executes the residual clause of the objectui#8426 ruling (director seat, decision batch #86, maintainer 「继续决策」), verbatim: 「the authoring `state` union sheds the three runtime-only approval states, so a schema-authored invocation cannot claim `approval-requested` without an envelope」. The shape follows the seat's round-2 answers on the card (comment `5817891136`): the fence against objectui#10273 is cleared for the chat-invocation hunks, the file surface is widened, `onSend` uses design B, and the envelope's enforcement is left to its own card. ## What changed - **`@object-ui/types`.** `ChatToolInvocation.state` and its Zod mirror `ChatToolInvocationSchema` drop `approval-requested`, `approval-responded` and `output-denied`. An authored claim of one is refused, with or without an `approval` envelope. The mirror refuses it as one `invalid_value` at `state`. - **The `onSend` slot (design B, zero new exports).** `ChatbotSchema.onSend` hands a host the thread the runtime holds, and in API mode that thread carries the three states. Its `messages` is therefore typed as the authoring message widened by exactly those states. Two NON-exported aliases in `complex.ts` do this: `ChatToolRuntimeOnlyState` and `ChatMessageHandedBack`. The built `complex.d.ts` emits both without `export`, and no barrel changes. - **The seam, derived rather than copied (`chatMessageAdapter.ts`).** `SeamToolInvocation.state` is the union of the authoring state and `ChatbotEnhanced`'s runtime state. `toRuntimeToolState` takes that union. The accepted set of values is unchanged, so existing callers still compile. - **`useObjectChat.ts`.** `initialMessages` is now the seam's input shape, because app-shell hands it runtime values restored from server history. The parts builder's un-backed-approval branch and its tests are KEPT. A runtime producer still constructs an envelope-less `approval-requested`: app-shell's `mergeToolResultsInto` promotes it from a pending-action result, and `hydratedMessagesToChatMessages` hands it to the builder. Its docblock now says this instead of predicting its own removal. - **One vocabulary across two packages.** `chat-message-contract.test.ts` derives the types-side alias through `ChatbotSchema['onSend']` and pins it EQUAL to `ChatbotEnhanced`'s runtime-only states. It also pins the set to the SDK's approval triple, pins the authoring union to hold none of them, and pins `ObjectChatMessage` as assignable to the slot. The old `_StillAnAuthoredMessage` pin asserted the subtype relation this change removes, so it is inverted to `_NoLongerAnAuthoredMessage`. - **Docblocks and published docs made true.** The envelope sentence 「Optional because the other seven states never carry one」 is replaced by what holds after the shed: no authorable state requires the envelope; the SDK admits an approved envelope on `output-available` and `output-error` only; the three states that require one are not authorable. The subtype promise is corrected in `ChatbotSchema.onSend`, `ObjectChatMessage`, `UseObjectChatOptions.onSend`, `packages/plugin-chatbot/src/renderer.tsx`'s header comment (comment only, `3ce44937`), the plugin-chatbot README and `content/docs/plugins/plugin-chatbot.mdx`. - **Fixture triage (`chat-tool-approval-envelope-8442.test.ts`).** `invocation()` now defaults to `output-available`. The id-judgment case asserts its refusal AT `approval.id`: before this change it would have kept passing on the state's refusal alone, a phantom check. The old 「is OPTIONAL」 case becomes the refusal pin for the three states, envelope or not. ## Changeset: `minor` with a BREAKING banner (版本号策略; `major` is banned) - **Break 1.** Authored approval states are refused, at TS and at parse, in `ChatbotSchema.messages` and anywhere else `ChatToolInvocation` is authored. - **Break 2.** An `onSend` handler typed against the authoring `ChatMessage[]` stops type-checking, because the messages it receives are no longer a subtype of that shape. - **Declared, not newly accepted.** `UseObjectChatOptions.initialMessages` now takes `SeamChatMessage`, so nine optional runtime-only keys are declared on that option: `buildProgress`, `blueprintProgress`, `charts`; `pendingActionId`, `draftReview`, `proposedPlan`, `proposedChanges`, `builderHandoff`, `replayOutcome`. The runtime is unchanged. Local mode keeps the six tool-invocation keys (`normalizeMessages` forwards `toolInvocations` whole) and drops the three message-level ones. API mode's `aiInitialMessages` carries none of the nine into SDK parts. - **Sibling pending changesets made true (`04ed6186`, prose only, frontmatter byte-identical).** This change makes two other cards' pending entries false in the same release, so they are corrected here (the objectui#10019 precedent). In `.changeset/8442-chat-tool-approval-envelope.md`, the 「ten declared `state` values」 sentence and the closing no-narrowing paragraph are corrected. In `.changeset/6169-chatbot-authoring-face-type.md`, the `onSend` bullet is corrected. - **Not measured.** Out-of-repo authors and hosts. In-repo, the type-check of types, plugin-chatbot and app-shell below is the reading. ## Evidence All readings are from the committed state. Code head: `3aa259d8`, which merges `origin/main` at `8b1f0661`. The later commits change no executable byte: - **Round 3** (`3ce44937`, comments and changeset prose): plugin-chatbot type-check exits 0. `check-changeset-presence`, `check:changeset-claims`, `check:control-bytes` and `check:new-line-citations` exit 0. - **Round 4** (`04ed6186`, the two sibling changesets, prose only): the same gates plus `check:pending-changeset-literals` exit 0, and `check-changeset-overwrite` reads declared = declares for both. - **Round 5** (`340efe60`, one changeset sentence): the Not-breaking paragraph no longer claims the hook forwarded the nine keys; it states what each builder does. The same gates exit 0. The contract review of `3aa259d8..04ed618` found one false sentence, and round 5 fixes it. **Pins, red first on base `5ea623ea`, before any source edit:** - `tsc -p tsconfig.test.json` on types exits 2 with exactly 7x TS2578 (unused `@ts-expect-error`). They sit on the six invocation literals (each state, bare and enveloped) and the authored-message literal. No control line errs. - `vitest` on the new file reads `Tests 9 failed | 4 passed (13)`: the 9 refusals fail and the 4 controls pass. **After the fix, at `3aa259d8`:** - `type-check` (main + test programs, plus examples for types) exits 0 for `@object-ui/types`, `@object-ui/plugin-chatbot` and `@object-ui/app-shell`. The new file's membership in the types test program is shown by the red run above. - From the repo root, `vitest run packages/types/ packages/plugin-chatbot/` reads `Test Files 263 passed (263)`, `Tests 5468 passed (5468)`. - A verbose run of the touched files plus the app-shell hydration and approval-rehydration suites reads `7 passed (7)`, `72 passed (72)`, and lists each new case by name. **Ablation 1: `approval-requested` re-added to the authoring union and its mirror.** - Setup: from the committed fix, through `ablation-replace.mjs` with anchors hit x1; blobs `c7c58748` to `ff55f890` and `a1f3bd36` to `39bdaf7d`. types was rebuilt, and the dist markers read 1 (`complex.d.ts` union member, `zod/complex.zod.js`). - Prediction: the three `approval-requested` refusal pins red, the other states' pins and every control green, `_IsTheApprovalTriple` red. - Measured: types test program exits 2 with exactly 3x TS2578, all on the `approval-requested` literals. vitest reads `4 failed | 15 passed (19)`: the three `approval-requested` refusals plus the 8442 refusal case, with all controls passing. plugin-chatbot main exits 0, and its test program exits 2 with 1 error, `_IsTheApprovalTriple`. - Restore: both blobs equal HEAD, `git diff HEAD` is empty, types was rebuilt, and the dist markers read 0. **Ablation 2: `output-denied` removed from the non-exported alias.** This proves the Equal pin can fail. - Measured: `_OneVocabulary`, `_IsWhatOnSendHandsBack` and `_HandedBackIsAuthoringPlusTriple` go red, and so do renderer.tsx's three `onSend: schema.onSend` forwards (main program TS2322). - Restore: blob equals HEAD, types was rebuilt, and the dist marker reads 0. **Gates at `3aa259d8`, each exit 0:** - `check:control-bytes` and `check:new-line-citations`. - `node scripts/check-changeset-presence.mjs` and `changeset:check` (fixed group, no-major). - `check:changeset-claims` and `check:pending-changeset-literals`. - Docs: `check:doc-types`, `check:doc-snippets` (672 of 672 blocks judged), `check:doc-examples`, `check:doc-fences`, `check:doc-example-ids`, `check:doc-example-readers`, `docs:check-links` and `check:readme-exports`. The first runs of doc-snippets, doc-examples and readme-exports read PRECONDITION NOT MET (unbuilt packages); they are green after the scoped build each gate prints. - `check:handler-key-reads`, `check:component-surface-parity` and `check:test-path-roots`. - `check-governed-queue-guard --test` reads NOT GOVERNED. - `eslint --no-inline-config --format json` on the 7 changed TS files: 7 files, 0 errors, 20 warnings, 0 of them on an added line. The 20 are pre-existing `no-explicit-any` and `react-hooks` warnings. **NOT MEASURED:** - Repo-wide `pnpm lint`, the full `pnpm test`, and CI convergence (left to the seat per standing rules). - Type-check of `apps/console`, `apps/site`, `examples/console-starter` and `examples/schema-catalog`. - Out-of-repo consumers. ## Overlap with objectui#10273 (per the seat's fence answer A) This PR and objectui#10273 both edit `packages/types/src/complex.ts` and `zod/complex.zod.ts`, and the hunks do not overlap: - **This PR:** `ChatToolInvocation`, the two new non-exported aliases after it and `ChatbotSchema.onSend` in `complex.ts`; `ChatToolInvocationSchema` in `complex.zod.ts`. It does not edit `zod-mirror-parity.test.ts`, because that pin's type equality moves with both sides. - **objectui#10273:** the import block and the Dashboard/Page twins in `complex.ts`; `GlobalFilterSchema` in `complex.zod.ts`; the parity census EXCLUSIONS. Whichever lands second merges `origin/main`; the union of both is the resolution. ## Acceptance notes - The authoring `approval` envelope now has no authorable state whose builder arm reads it. The seat is filing its enforcement as its own card; this PR only makes its docblock true. - `renderer.tsx`'s header comment made the same subtype promise. It is retracted in `3ce44937` (comment only). The seat widened the claim surface to that file. - The warning text `warnApprovalStateWithoutEnvelope` prints ("not representable as authored") was left unchanged to avoid churning the tests that pin it. The docblock above it is what was rewritten. The seat's session is `https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC`. This PR was implemented by a dispatched `os-dev` run in that session (seat `domain:ui#1`, claim `5817363473`). --- _Generated by [Claude Code](https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 92e6942 commit b46c58f

13 files changed

Lines changed: 456 additions & 93 deletions
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
'@object-ui/types': minor
3+
'@object-ui/plugin-chatbot': minor
4+
---
5+
6+
BREAKING (`@object-ui/types`, `@object-ui/plugin-chatbot`): the authoring `ChatToolInvocation.state` union sheds the AI SDK's three runtime-only approval states — `approval-requested`, `approval-responded` and `output-denied` — so a schema-authored tool invocation can no longer claim an approval the runtime has nothing to back (objectui#10018, the residual clause of the objectui#8426 ruling, decision batch #86).
7+
8+
(The bump is `minor` by this repo's release model — objectui's major is pinned to the `@objectstack` family major, and its own breaking changes ship as `minor` with the breaking semantics stated here.)
9+
10+
**Break 1 — authored approval states are refused.** `ChatToolInvocation.state` (and so `ChatMessage.toolInvocations[].state` and `ChatbotSchema.messages`) no longer admits the three approval states, and the Zod mirror `ChatToolInvocationSchema` refuses them as an `invalid_value` at `state`. The refusal holds with or without an `approval` envelope: the envelope does not make a runtime-only state authorable. A chat runtime still produces these states — from the SDK's approval envelope, or promoted from an ObjectStack pending-action result — and still renders them; only the authoring face stops declaring them. Migration: author the state the tool call is actually in (`input-streaming`, `input-available`, `output-available`, `output-error`, or a legacy `partial-call` / `call` / `result`), and leave approval states to the runtime.
11+
12+
**Break 2 — `onSend` handlers typed against the authoring `ChatMessage[]`.** The messages a chat runtime hands back can carry those three states, so they are no longer a subtype of the authoring `ChatMessage`. `ChatbotSchema.onSend`'s `messages` is now typed as the authoring message widened by exactly those states, and `useObjectChat`'s `ObjectChatMessage` is no longer assignable to the authoring `ChatMessage`. A handler that declares its parameter as the authoring `ChatMessage[]` stops type-checking. Migration: let the parameter be inferred from the slot, or declare it as `ObjectChatMessage[]` from `@object-ui/plugin-chatbot` when passing `onSend` to `useObjectChat`.
13+
14+
Not breaking: `UseObjectChatOptions.initialMessages`, `SeamToolInvocation` and `toRuntimeToolState` now take the seam's state vocabulary (authoring plus runtime), which is the same set of values they accepted before, so every existing caller still compiles. `UseObjectChatOptions.initialMessages` also moves from the authoring `ChatMessage` to the exported `SeamChatMessage`, so nine optional runtime-only keys are now declared on that option: `buildProgress`, `blueprintProgress` and `charts` on a message, and `pendingActionId`, `draftReview`, `proposedPlan`, `proposedChanges`, `builderHandoff` and `replayOutcome` on each tool invocation. Nothing changes at runtime, because the hook's two builders behave exactly as before: local mode's `normalizeMessages` forwards `toolInvocations` whole (so the six tool-invocation keys survive) but copies only the base message fields (so `buildProgress`, `blueprintProgress` and `charts` are dropped), and API mode's `aiInitialMessages` rebuilds each message as SDK parts that carry none of the nine. Each key was already declared on `SeamChatMessage`; what changes is that a host passing one in an object literal is no longer told it is an excess property. No new name is exported: the three runtime-only states are named only by a non-exported alias in `@object-ui/types`, pinned equal to `ChatbotEnhanced`'s runtime union by `chat-message-contract.test.ts`. The effect on out-of-repo authors and hosts was not measured.

‎.changeset/6169-chatbot-authoring-face-type.md‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,10 @@ to anything outside that one file:
1818
- `autoResponse`, `autoResponseText`, `autoResponseDelay` — the local
1919
auto-response (demo/playground) fields, already live via a real consumer
2020
(`packages/app-shell/src/console/ai/AiChatPage.tsx`).
21-
- `onSend?: (content: string, messages: ChatMessage[]) => void` — the
22-
send-callback, now typed against the published `ChatMessage` shape rather
23-
than the plugin's internal runtime message type.
21+
- `onSend` — the send-callback, now typed on the published authoring face
22+
rather than against the plugin's internal runtime message type. Its
23+
`messages` element is the authoring `ChatMessage` widened by the three
24+
runtime-only approval states a chat runtime hands back (objectui#10018).
2425

2526
Each was read-site-censused before being declared (renderer.tsx and/or
2627
`useObjectChat.ts` reads every one); none were dead, so none took the

‎.changeset/8442-chat-tool-approval-envelope.md‎

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@
88
hydration mapper stops dropping it — together with `pendingActionId`
99
(objectui#8442).
1010

11-
Three of the ten declared `state` values — `approval-requested`,
12-
`approval-responded` and `output-denied` — are states the AI SDK's own tool-part
13-
union cannot express WITHOUT an `{ id, approved?, reason?, isAutomatic?,
14-
signature? }` envelope. `hydratedMessagesToChatMessages` built each invocation
11+
Three AI SDK `state` values — `approval-requested`, `approval-responded` and
12+
`output-denied` — are states the SDK's own tool-part union cannot express
13+
WITHOUT an `{ id, approved?, reason?, isAutomatic?, signature? }` envelope.
14+
`hydratedMessagesToChatMessages` built each invocation
1515
from six fields and neither the envelope nor `pendingActionId` was one of them,
1616
so a rehydrated pending approval arrived carrying a state that says "a human must
1717
decide" and nothing a decision could be made with: `useHitlInChat` keys its index
@@ -48,7 +48,12 @@ regardless of what this file says. Declaring `patch` here therefore buys no
4848
smaller release and no preserved signal; it only makes the changelog line
4949
under-describe what shipped. ⇒ the accurate declaration is the cheap one.
5050

51-
⛔ Nothing is narrowed here. The envelope stays optional on purpose: an
52-
invocation may still declare an approval state and carry no envelope, and the
53-
pin that says so is deliberate, so that a later tidy-up cannot ship
54-
objectui#8426's break under this card's name.
51+
⛔ Nothing is narrowed here. The envelope stays optional on purpose, and this
52+
card does not pair it with the approval states. That narrowing — the residual
53+
clause of the objectui#8426 ruling — ships under its own name and its own
54+
BREAKING banner: objectui#10018 sheds the three approval states from the
55+
authoring `state` union, so an AUTHORED invocation cannot declare one at all,
56+
with or without an envelope. On the runtime `ChatbotEnhanced.ChatToolInvocation`
57+
an approval state may still arrive with no envelope — an ObjectStack
58+
pending-action approval is carried by `pendingActionId` instead — and the
59+
envelope stays optional there too.

‎content/docs/plugins/plugin-chatbot.mdx‎

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -714,11 +714,15 @@ tool invocation — the approval card, the "Review N changes" affordance, the pl
714714
card, the build panel, the inline charts), with `timestamp` narrowed to `string`
715715
because both modes absorb an authored `Date` before emitting.
716716

717-
It is a subtype of the authoring `ChatMessage`, so an `onSend` callback that
718-
already declares `ChatMessage[]` keeps type-checking; naming `ObjectChatMessage`
719-
is what lets it read those keys. Rebuilding a message field-by-field from the
720-
authoring type drops every one of them — silently, and with the compiler's
721-
agreement (objectui#4424).
717+
In API mode its tool invocations can also carry the AI SDK's three approval
718+
states — `approval-requested`, `approval-responded` and `output-denied`. Those
719+
are runtime-only: the authoring `ChatToolInvocation` refuses them, with or
720+
without an `approval` envelope (objectui#10018). So `ObjectChatMessage` is
721+
**not** a subtype of the authoring `ChatMessage`, and an `onSend` callback that
722+
declares its parameter as `ChatMessage[]` does not type-check; declare
723+
`ObjectChatMessage[]`, which is also what lets it read those keys. Rebuilding a
724+
message field-by-field from the authoring type drops every one of them —
725+
silently, and with the compiler's agreement (objectui#4424).
722726

723727
## Related Documentation
724728

‎packages/plugin-chatbot/README.md‎

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -130,15 +130,20 @@ modes (objectui#4424):
130130
card, the "Review N changes" affordance, the plan card, the build panel and
131131
the inline charts. The authoring contract declares none of them, so rebuilding
132132
a message field-by-field from it deletes all of them, and the compiler agrees.
133+
API mode also produces the AI SDK's three approval states
134+
(`'approval-requested'` / `'approval-responded'` / `'output-denied'`), which
135+
the authoring contract refuses — they are runtime-only (objectui#10018).
133136
- **Not this package's runtime `ChatMessage` either.** In local mode an authored
134137
`'tool'` role and the legacy `'partial-call'` / `'call'` / `'result'` tool
135138
states pass through untouched; they are folded only at the render seam
136139
(`toRuntimeMessages`, `chatMessageAdapter.ts`).
137140

138-
`ObjectChatMessage` is a subtype of the authoring `ChatMessage`, so anything
139-
already typed against that keeps compiling — naming `ObjectChatMessage` is what
140-
lets you *read* the keys above. `timestamp` is always a `string` here: both
141-
modes absorb an authored `Date` before emitting.
141+
`ObjectChatMessage` is **not** a subtype of the authoring `ChatMessage`
142+
(objectui#10018): its tool invocations can carry those three runtime-only
143+
approval states. So an `onSend` callback that declares its parameter as the
144+
authoring `ChatMessage[]` does not type-check — declare `ObjectChatMessage[]`,
145+
which is also what lets you *read* the keys above. `timestamp` is always a
146+
`string` here: both modes absorb an authored `Date` before emitting.
142147

143148
## Schema-Driven Usage
144149

@@ -332,7 +337,7 @@ function MyAuthoredChat({ messages }: { messages: AuthoredChatMessage[] }) {
332337
|---|---|---|---|
333338
| `role` | `'user' \| 'assistant' \| 'system' \| 'tool'` | `'user' \| 'assistant' \| 'system'` | a `'tool'` message **is an assistant message**: it renders as an assistant bubble with its content shown. `'system'` keeps its own role (`<Chatbot>` renders it as a centred pill). |
334339
| `timestamp` | `string \| Date` | `string` | a `Date` becomes its ISO 8601 string. The runtime renders the timestamp straight into a React child, where an object throws. |
335-
| `toolInvocations[].state` | AI SDK v6 states **+ legacy** `'partial-call' \| 'call' \| 'result'` | v6 states only | the legacy spellings map to `'input-streaming'` / `'input-available'` / `'output-available'` — the mapping the authoring type's own docs declare. |
340+
| `toolInvocations[].state` | AI SDK v6 states **except** the three approval states, **+ legacy** `'partial-call' \| 'call' \| 'result'` | v6 states only | the legacy spellings map to `'input-streaming'` / `'input-available'` / `'output-available'` — the mapping the authoring type's own docs declare. `'approval-requested'` / `'approval-responded'` / `'output-denied'` are runtime-only and not authorable (objectui#10018); the adapter's input still admits them, because API-mode values carry them. |
336341
| everything else | — | — | passed through untouched, including keys the runtime contract does not declare. |
337342

338343
The three registered SDUI renderers (`chatbot`, `chatbot-enhanced`,

‎packages/plugin-chatbot/src/__tests__/chat-message-contract.test.ts‎

Lines changed: 70 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ import type { ChatMessage as BarrelChatMessage, ChatbotEnhancedMessage } from '.
4141
/** The shape `<ChatbotEnhanced>` renders and the mappers produce. */
4242
import type { ChatMessage as EnhancedChatMessage } from '../ChatbotEnhanced';
4343
/** The OTHER side of the seam: the JSON/SDUI authoring contract. */
44-
import type { ChatMessage as AuthoredChatMessage } from '@object-ui/types';
44+
import type { ChatMessage as AuthoredChatMessage, ChatbotSchema } from '@object-ui/types';
4545
import type {
4646
authoredToRuntimeMessage,
4747
toRuntimeMessages,
@@ -65,6 +65,13 @@ type Equal<A, B> = (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ?
6565
type HasIndexSignature<T> = string extends keyof T ? true : false;
6666
type Has<T, K extends string> = K extends keyof T ? true : false;
6767

68+
/**
69+
* One element of the `messages` a chat runtime hands a schema's `onSend` — read
70+
* off the published slot, because `@object-ui/types` deliberately exports no
71+
* name for it (objectui#10018).
72+
*/
73+
type HandedBackMessage = Parameters<NonNullable<ChatbotSchema['onSend']>>[1][number];
74+
6875
/**
6976
* The retired shape, transcribed verbatim from the declaration this change
7077
* deleted from `src/index.tsx`. It exists here ONLY so the negative pin below
@@ -285,13 +292,22 @@ describe("useObjectChat's declared message type is honest about both modes", ()
285292
type _HasProposedChanges = Assert<Has<HookToolInvocation, 'proposedChanges'>>;
286293
type _HasBuilderHandoff = Assert<Has<HookToolInvocation, 'builderHandoff'>>;
287294

288-
// 5. The compatibility statement, and the reason this is a MINOR and not a
289-
// break: the honest type is a SUBTYPE of the authoring one it replaces.
290-
// Every consumer that correctly accepted `@object-ui/types`' ChatMessage
291-
// still accepts these values — including a host `onSend` typed against
292-
// the authoring contract, which type-checks by contravariance.
293-
type _StillAnAuthoredMessage = Assert<
294-
ObjectChatMessage extends AuthoredChatMessage ? true : false
295+
// 5. The compatibility statement, re-derived (objectui#10018). The honest
296+
// type WAS a subtype of the authoring one until the authoring `state`
297+
// union shed the three runtime-only approval states that API mode
298+
// produces. The values did not change; the authoring contract did. It
299+
// is NOT a subtype now, and that is the break the changeset names: a
300+
// host `onSend` typed against the authoring `ChatMessage[]` stops
301+
// type-checking, because it is handed states that contract refuses.
302+
type _NoLongerAnAuthoredMessage = Assert<
303+
Equal<ObjectChatMessage extends AuthoredChatMessage ? true : false, false>
304+
>;
305+
// What it IS assignable to: the element type `ChatbotSchema.onSend`
306+
// hands a host — the authoring shape widened by exactly those three
307+
// states. This is what lets the three renderers forward `schema.onSend`
308+
// into the hook without a cast.
309+
type _IsWhatOnSendHandsBack = Assert<
310+
ObjectChatMessage extends HandedBackMessage ? true : false
295311
>;
296312

297313
// 6. And the seam is still NECESSARY — this is not option 2 in disguise
@@ -314,6 +330,52 @@ describe("useObjectChat's declared message type is honest about both modes", ()
314330
});
315331
});
316332

333+
describe('the runtime-only approval states are ONE vocabulary across the two packages', () => {
334+
it('is pinned at compile time', () => {
335+
// objectui#10018. `@object-ui/types` sheds the AI SDK's three approval
336+
// states from the authoring `state` union and names them only in a
337+
// NON-exported alias, which types `ChatbotSchema.onSend`'s messages. This
338+
// package spells them on `ChatbotEnhanced.ChatToolInvocation['state']`.
339+
// Two spellings of one set, so they are pinned EQUAL here — derived
340+
// through the published slot, since the alias has no name to import.
341+
type AuthoredState = NonNullable<
342+
NonNullable<AuthoredChatMessage['toolInvocations']>[number]['state']
343+
>;
344+
type HandedBackState = NonNullable<
345+
NonNullable<HandedBackMessage['toolInvocations']>[number]['state']
346+
>;
347+
type RuntimeState = NonNullable<
348+
NonNullable<EnhancedChatMessage['toolInvocations']>[number]['state']
349+
>;
350+
type TypesRuntimeOnly = Exclude<HandedBackState, AuthoredState>;
351+
type ChatbotRuntimeOnly = Exclude<RuntimeState, AuthoredState>;
352+
353+
// Probe hygiene: an `any` answers every `Equal`, and two EMPTY sets are
354+
// equal too — so neither side may be `any` or `never`.
355+
type _HandedBackNotAny = Assert<Equal<IsAny<HandedBackMessage>, false>>;
356+
type _TypesSideNotEmpty = Assert<
357+
Equal<[TypesRuntimeOnly] extends [never] ? true : false, false>
358+
>;
359+
360+
// 1. The two spellings are ONE set — neither package can move alone.
361+
type _OneVocabulary = Assert<Equal<TypesRuntimeOnly, ChatbotRuntimeOnly>>;
362+
// 2. …and it is exactly the SDK's approval triple, named so a drift says
363+
// WHICH state moved rather than "types differ".
364+
type _IsTheApprovalTriple = Assert<
365+
Equal<ChatbotRuntimeOnly, 'approval-requested' | 'approval-responded' | 'output-denied'>
366+
>;
367+
// 3. The ruling: the authoring union holds none of them.
368+
type _AuthoringShedsThem = Assert<Equal<Extract<AuthoredState, ChatbotRuntimeOnly>, never>>;
369+
// 4. The slot widens the authoring vocabulary by those states and nothing
370+
// else: every authoring state is still handed back.
371+
type _HandedBackIsAuthoringPlusTriple = Assert<
372+
Equal<HandedBackState, AuthoredState | ChatbotRuntimeOnly>
373+
>;
374+
375+
expect(true).toBe(true);
376+
});
377+
});
378+
317379
describe("the seam's input contract names the keys it passes through", () => {
318380
it('is pinned at compile time', () => {
319381
// objectui#4424. PR #4416's adapter preserved the runtime-only keys with a

0 commit comments

Comments
 (0)