You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Fixes an issue where `sendMessages()` returned an empty `ReadableStream`
to `useChat`. The stream returned here is used to power important
features of useChat, such as:
- status changes (submitted -> streaming -> ready)
- turn lifecycle hooks (e.g. `onToolCall`, `onData`, `onFinish`)
- evaluating whether `sendAutomaticallyWhen` should send
With an empty stream, none of these features worked. This transport aims
to, as much as possible, be a drop-in replacement for the default
transport so we need to make these features work.
This commit returns the real stream from `turn.stream` to `useChat`,
which allows these features to work as expected. As a result of this
change, both `useChat` and `useMessageSync` now accumulate messages in
parallel but both produce identical messages from the same underlying
Ably events.
When the AI SDK doesn't set messageId on the start chunk (currently this
is logically equivalent to not being in 'persistence mode') useChat and
the transport will assign different IDs. This commit adds `messageId` to
`EncoderOptions` so that the server transport can pass its generated ID
to the vercel encoder which uses it as a fallback, which ensures that
both sides converge on the same ID.
Copy file name to clipboardExpand all lines: docs/internals/chat-transport.md
+6-4Lines changed: 6 additions & 4 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -9,7 +9,7 @@ Vercel's `useChat` manages message state internally. When the user submits a mes
9
9
1. Determine which messages are new vs history
10
10
2. Compute fork metadata for regeneration
11
11
3. Delegate to the core transport's `send()`
12
-
4. Return a stream that signals completion without duplicating state
12
+
4. Return the turn stream so `useChat` can drive status and callbacks
13
13
14
14
## sendMessages
15
15
@@ -28,11 +28,13 @@ The `prepareSendMessagesRequest` hook (optional) lets the server app customize t
28
28
29
29
Without the hook, the adapter builds a default body with `history` (including per-message Ably headers), `id`, `trigger`, and fork metadata fields.
30
30
31
-
### Empty stream return
31
+
### Real stream return
32
32
33
-
The adapter returns an **empty stream** that closes when the turn ends - not the real event stream. This is intentional: `useChat` consumes the returned stream to accumulate the assistant message, but `useMessageSync` (the companion React hook) already pushes the transport's authoritative message state into `useChat` via `setMessages`. Returning the real event stream would cause `useChat` to accumulate a duplicate assistant message.
33
+
The adapter returns the real turn stream from `sendMessages()`. `useChat` consumes this stream to drive status transitions (`submitted` -> `streaming` -> `ready`), fire callbacks (`onToolCall`, `onData`, `onFinish`), and evaluate `sendAutomaticallyWhen`.
34
34
35
-
The empty stream is created via a `TransformStream` whose writable side closes when the turn's real stream finishes.
35
+
Both `useChat` and `useMessageSync` accumulate messages in parallel: `useChat` builds from the stream, while `useMessageSync` pushes from the transport's message store via `setMessages` (a full replacement). The transport's version is always authoritative - both accumulators produce identical messages from the same chunks, and `setMessages` overwrites `useChat`'s state on every transport event.
36
+
37
+
The server encoder ensures `messageId` alignment by stamping the transport-assigned `x-ably-msg-id` as a fallback domain `messageId` on the `start` chunk. This ensures both accumulators assign the same message ID.
0 commit comments