diff --git a/.changeset/fix-structured-output-stream-sse-assignability.md b/.changeset/fix-structured-output-stream-sse-assignability.md new file mode 100644 index 000000000..70fa7ad9b --- /dev/null +++ b/.changeset/fix-structured-output-stream-sse-assignability.md @@ -0,0 +1,9 @@ +--- +'@tanstack/ai': patch +--- + +fix(ai): restore `StructuredOutputStream` assignability to `AsyncIterable` so it can be passed to `toServerSentEventsResponse` + +`StructuredOutputStartEvent`, `StructuredOutputCompleteEvent`, `ApprovalRequestedEvent`, and `ToolInputAvailableEvent` declared their shape with `extends Omit`. Because `CustomEvent` is inferred from a zod `passthrough` schema, it carries a `[k: string]: unknown` index signature, and `Omit` on a type with a `string` index signature collapses every surviving property to `unknown` — including `type: 'CUSTOM'`. That broke union assignability against `AGUIEvent`/`StreamChunk`, so `toServerSentEventsResponse(stream)` failed to typecheck against streams returned by `chat({ outputSchema, stream: true })`. + +Switched to `extends CustomEvent` with refined `name`/`value` (allowed: narrower types of declared properties), which keeps `type: 'CUSTOM'` intact and preserves the existing discriminated-narrowing patterns. diff --git a/packages/typescript/ai/src/types.ts b/packages/typescript/ai/src/types.ts index 3ffe72ffe..fa8d3d005 100644 --- a/packages/typescript/ai/src/types.ts +++ b/packages/typescript/ai/src/types.ts @@ -1146,10 +1146,9 @@ export interface CustomEvent extends AGUICustomEvent { * } * ``` */ -export interface StructuredOutputCompleteEvent extends Omit< - CustomEvent, - 'name' | 'value' -> { +export interface StructuredOutputCompleteEvent< + T = unknown, +> extends CustomEvent { name: 'structured-output.complete' value: { object: T; raw: string; reasoning?: string } } @@ -1162,10 +1161,7 @@ export interface StructuredOutputCompleteEvent extends Omit< * `messageId` the deltas will be tagged with so the routing decision can be * made per-message rather than globally. */ -export interface StructuredOutputStartEvent extends Omit< - CustomEvent, - 'name' | 'value' -> { +export interface StructuredOutputStartEvent extends CustomEvent { name: 'structured-output.start' value: { messageId: string } } @@ -1177,10 +1173,7 @@ export interface StructuredOutputStartEvent extends Omit< * (the agent-loop branch of `runStreamingStructuredOutputImpl` in * `activities/chat/index.ts` forwards CUSTOM events from `TextEngine.run()`). */ -export interface ApprovalRequestedEvent extends Omit< - CustomEvent, - 'name' | 'value' -> { +export interface ApprovalRequestedEvent extends CustomEvent { name: 'approval-requested' value: { toolCallId: string @@ -1196,10 +1189,7 @@ export interface ApprovalRequestedEvent extends Omit< * will not fire for that run. Shape fixed by the agent-loop forwarding in * `runStreamingStructuredOutputImpl` in `activities/chat/index.ts`. */ -export interface ToolInputAvailableEvent extends Omit< - CustomEvent, - 'name' | 'value' -> { +export interface ToolInputAvailableEvent extends CustomEvent { name: 'tool-input-available' value: { toolCallId: string diff --git a/packages/typescript/ai/tests/chat-result-types.test.ts b/packages/typescript/ai/tests/chat-result-types.test.ts index 5dd9d1eb9..2e8ad1aeb 100644 --- a/packages/typescript/ai/tests/chat-result-types.test.ts +++ b/packages/typescript/ai/tests/chat-result-types.test.ts @@ -79,6 +79,14 @@ describe('chat() return type', () => { }) }) + describe('StructuredOutputStream assignability', () => { + it('is assignable to AsyncIterable (toServerSentEventsResponse input)', () => { + expectTypeOf>().toMatchTypeOf< + AsyncIterable + >() + }) + }) + describe('without outputSchema', () => { it('stream: true → AsyncIterable', () => { expectTypeOf>().toEqualTypeOf<