@ably/ai-transport 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +91 -100
- package/dist/ably-ai-transport.js +1553 -1238
- package/dist/ably-ai-transport.js.map +1 -1
- package/dist/ably-ai-transport.umd.cjs +1 -1
- package/dist/ably-ai-transport.umd.cjs.map +1 -1
- package/dist/constants.d.ts +116 -42
- package/dist/core/agent.d.ts +29 -0
- package/dist/core/codec/decoder.d.ts +20 -23
- package/dist/core/codec/encoder.d.ts +11 -8
- package/dist/core/codec/index.d.ts +1 -2
- package/dist/core/codec/lifecycle-tracker.d.ts +10 -9
- package/dist/core/codec/types.d.ts +407 -115
- package/dist/core/transport/agent-session.d.ts +10 -0
- package/dist/core/transport/branch-chain.d.ts +43 -0
- package/dist/core/transport/client-session.d.ts +13 -0
- package/dist/core/transport/decode-fold.d.ts +47 -0
- package/dist/core/transport/headers.d.ts +96 -18
- package/dist/core/transport/index.d.ts +5 -6
- package/dist/core/transport/internal/bounded-map.d.ts +20 -0
- package/dist/core/transport/invocation.d.ts +74 -0
- package/dist/core/transport/load-conversation.d.ts +128 -0
- package/dist/core/transport/load-history.d.ts +39 -0
- package/dist/core/transport/pipe-stream.d.ts +9 -9
- package/dist/core/transport/run-manager.d.ts +78 -0
- package/dist/core/transport/tree.d.ts +373 -109
- package/dist/core/transport/types/agent.d.ts +353 -0
- package/dist/core/transport/types/client.d.ts +168 -0
- package/dist/core/transport/types/shared.d.ts +24 -0
- package/dist/core/transport/types/tree.d.ts +315 -0
- package/dist/core/transport/types/view.d.ts +222 -0
- package/dist/core/transport/types.d.ts +13 -553
- package/dist/core/transport/view.d.ts +272 -84
- package/dist/errors.d.ts +21 -10
- package/dist/index.d.ts +6 -8
- package/dist/logger.d.ts +12 -0
- package/dist/react/ably-ai-transport-react.js +976 -990
- package/dist/react/ably-ai-transport-react.js.map +1 -1
- package/dist/react/ably-ai-transport-react.umd.cjs +1 -1
- package/dist/react/ably-ai-transport-react.umd.cjs.map +1 -1
- package/dist/react/contexts/client-session-context.d.ts +36 -0
- package/dist/react/contexts/client-session-provider.d.ts +53 -0
- package/dist/react/create-session-hooks.d.ts +116 -0
- package/dist/react/index.d.ts +12 -12
- package/dist/react/internal/use-resolved-session.d.ts +36 -0
- package/dist/react/use-ably-messages.d.ts +17 -14
- package/dist/react/use-client-session.d.ts +81 -0
- package/dist/react/use-create-view.d.ts +14 -13
- package/dist/react/use-tree.d.ts +30 -15
- package/dist/react/use-view.d.ts +82 -51
- package/dist/utils.d.ts +32 -23
- package/dist/vercel/ably-ai-transport-vercel.js +2573 -2086
- package/dist/vercel/ably-ai-transport-vercel.js.map +1 -1
- package/dist/vercel/ably-ai-transport-vercel.umd.cjs +1 -1
- package/dist/vercel/ably-ai-transport-vercel.umd.cjs.map +1 -1
- package/dist/vercel/codec/decoder.d.ts +5 -18
- package/dist/vercel/codec/encoder.d.ts +6 -36
- package/dist/vercel/codec/events.d.ts +51 -0
- package/dist/vercel/codec/index.d.ts +24 -12
- package/dist/vercel/codec/reducer.d.ts +144 -0
- package/dist/vercel/codec/tool-transitions.d.ts +2 -2
- package/dist/vercel/index.d.ts +4 -5
- package/dist/vercel/react/ably-ai-transport-vercel-react.js +3907 -3266
- package/dist/vercel/react/ably-ai-transport-vercel-react.js.map +1 -1
- package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs +33 -8
- package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs.map +1 -1
- package/dist/vercel/react/contexts/chat-transport-context.d.ts +7 -6
- package/dist/vercel/react/contexts/chat-transport-provider.d.ts +53 -41
- package/dist/vercel/react/index.d.ts +1 -2
- package/dist/vercel/react/use-chat-transport.d.ts +30 -26
- package/dist/vercel/react/use-message-sync.d.ts +17 -30
- package/dist/vercel/run-end-reason.d.ts +29 -0
- package/dist/vercel/transport/chat-transport.d.ts +43 -24
- package/dist/vercel/transport/index.d.ts +25 -21
- package/dist/vercel/transport/run-output-stream.d.ts +56 -0
- package/dist/version.d.ts +2 -0
- package/package.json +30 -23
- package/src/constants.ts +124 -51
- package/src/core/agent.ts +68 -0
- package/src/core/codec/decoder.ts +71 -98
- package/src/core/codec/encoder.ts +113 -65
- package/src/core/codec/index.ts +13 -6
- package/src/core/codec/lifecycle-tracker.ts +10 -9
- package/src/core/codec/types.ts +436 -120
- package/src/core/transport/agent-session.ts +1344 -0
- package/src/core/transport/branch-chain.ts +58 -0
- package/src/core/transport/client-session.ts +775 -0
- package/src/core/transport/decode-fold.ts +91 -0
- package/src/core/transport/headers.ts +181 -22
- package/src/core/transport/index.ts +25 -26
- package/src/core/transport/internal/bounded-map.ts +27 -0
- package/src/core/transport/invocation.ts +98 -0
- package/src/core/transport/load-conversation.ts +355 -0
- package/src/core/transport/load-history.ts +269 -0
- package/src/core/transport/pipe-stream.ts +54 -39
- package/src/core/transport/run-manager.ts +249 -0
- package/src/core/transport/tree.ts +926 -308
- package/src/core/transport/types/agent.ts +407 -0
- package/src/core/transport/types/client.ts +211 -0
- package/src/core/transport/types/shared.ts +27 -0
- package/src/core/transport/types/tree.ts +344 -0
- package/src/core/transport/types/view.ts +259 -0
- package/src/core/transport/types.ts +13 -706
- package/src/core/transport/view.ts +864 -433
- package/src/errors.ts +22 -9
- package/src/event-emitter.ts +3 -2
- package/src/index.ts +52 -41
- package/src/logger.ts +14 -1
- package/src/react/contexts/client-session-context.ts +41 -0
- package/src/react/contexts/client-session-provider.tsx +186 -0
- package/src/react/create-session-hooks.ts +141 -0
- package/src/react/index.ts +23 -13
- package/src/react/internal/use-resolved-session.ts +63 -0
- package/src/react/use-ably-messages.ts +32 -22
- package/src/react/use-client-session.ts +201 -0
- package/src/react/use-create-view.ts +33 -29
- package/src/react/use-tree.ts +61 -30
- package/src/react/use-view.ts +139 -97
- package/src/utils.ts +63 -45
- package/src/vercel/codec/decoder.ts +336 -258
- package/src/vercel/codec/encoder.ts +343 -205
- package/src/vercel/codec/events.ts +87 -0
- package/src/vercel/codec/index.ts +60 -13
- package/src/vercel/codec/reducer.ts +977 -0
- package/src/vercel/codec/tool-transitions.ts +2 -2
- package/src/vercel/index.ts +6 -19
- package/src/vercel/react/contexts/chat-transport-context.ts +7 -6
- package/src/vercel/react/contexts/chat-transport-provider.tsx +87 -59
- package/src/vercel/react/index.ts +3 -5
- package/src/vercel/react/use-chat-transport.ts +47 -49
- package/src/vercel/react/use-message-sync.ts +80 -39
- package/src/vercel/run-end-reason.ts +78 -0
- package/src/vercel/transport/chat-transport.ts +392 -98
- package/src/vercel/transport/index.ts +39 -38
- package/src/vercel/transport/run-output-stream.ts +170 -0
- package/src/version.ts +2 -0
- package/dist/core/transport/client-transport.d.ts +0 -10
- package/dist/core/transport/decode-history.d.ts +0 -43
- package/dist/core/transport/server-transport.d.ts +0 -7
- package/dist/core/transport/stream-router.d.ts +0 -29
- package/dist/core/transport/turn-manager.d.ts +0 -37
- package/dist/react/contexts/transport-context.d.ts +0 -31
- package/dist/react/contexts/transport-provider.d.ts +0 -49
- package/dist/react/create-transport-hooks.d.ts +0 -124
- package/dist/react/use-active-turns.d.ts +0 -12
- package/dist/react/use-client-transport.d.ts +0 -80
- package/dist/vercel/codec/accumulator.d.ts +0 -21
- package/dist/vercel/react/use-staged-add-tool-approval-response.d.ts +0 -30
- package/dist/vercel/tool-approvals.d.ts +0 -124
- package/dist/vercel/tool-events.d.ts +0 -26
- package/src/core/transport/client-transport.ts +0 -977
- package/src/core/transport/decode-history.ts +0 -485
- package/src/core/transport/server-transport.ts +0 -612
- package/src/core/transport/stream-router.ts +0 -136
- package/src/core/transport/turn-manager.ts +0 -165
- package/src/react/contexts/transport-context.ts +0 -37
- package/src/react/contexts/transport-provider.tsx +0 -164
- package/src/react/create-transport-hooks.ts +0 -144
- package/src/react/use-active-turns.ts +0 -72
- package/src/react/use-client-transport.ts +0 -197
- package/src/vercel/codec/accumulator.ts +0 -588
- package/src/vercel/react/use-staged-add-tool-approval-response.ts +0 -87
- package/src/vercel/tool-approvals.ts +0 -380
- package/src/vercel/tool-events.ts +0 -53
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Vercel chat transport: wraps a core
|
|
2
|
+
* Vercel chat transport: wraps a core ClientSession to satisfy the
|
|
3
3
|
* ChatTransport interface that useChat expects.
|
|
4
4
|
*
|
|
5
|
-
* This is a thin adapter — the real logic lives in the core
|
|
5
|
+
* This is a thin adapter — the real logic lives in the core client session.
|
|
6
6
|
* The chat transport maps Vercel's sendMessages/reconnectToStream contract
|
|
7
|
-
* to the core
|
|
7
|
+
* to the core session's send/cancel methods.
|
|
8
8
|
*
|
|
9
9
|
* useChat manages message state before calling sendMessages:
|
|
10
10
|
* - submit-message (new): appends the new user message, passes the full array
|
|
@@ -12,20 +12,33 @@
|
|
|
12
12
|
* passes the truncated array with messageId set
|
|
13
13
|
* - regenerate-message: truncates after the target, passes the truncated array
|
|
14
14
|
*
|
|
15
|
-
* The adapter uses `trigger` to determine the
|
|
16
|
-
*
|
|
15
|
+
* The adapter uses `(trigger, last-message role)` to determine the
|
|
16
|
+
* history/messages split:
|
|
17
|
+
* - submit-message + last message is a user message: that last message is new
|
|
18
|
+
* (publish to channel), rest is history. A new submit and an edit both take
|
|
19
|
+
* this path — an edit just carries a messageId.
|
|
20
|
+
* - submit-message + last message is an assistant already in the tree
|
|
21
|
+
* (continuation): no new messages, entire array is history
|
|
17
22
|
* - regenerate-message: no new messages, entire array is history
|
|
18
23
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* place the response on the
|
|
24
|
+
* For an edit (submit-message with messageId) and for forking off an
|
|
25
|
+
* unresolved tool call, the adapter computes fork metadata (forkOf/parent)
|
|
26
|
+
* from the conversation tree so the server can place the response on the
|
|
27
|
+
* correct branch. Regeneration fork metadata is NOT computed here —
|
|
28
|
+
* `View.regenerate` derives forkOf/parent from the tree itself.
|
|
22
29
|
*/
|
|
23
30
|
|
|
24
31
|
import * as Ably from 'ably';
|
|
25
32
|
import type * as AI from 'ai';
|
|
26
33
|
|
|
27
|
-
import type {
|
|
34
|
+
import type { CodecMessage } from '../../core/codec/types.js';
|
|
35
|
+
import type { ActiveRun, ClientSession, SendOptions } from '../../core/transport/types.js';
|
|
28
36
|
import { ErrorCode } from '../../errors.js';
|
|
37
|
+
import { EventEmitter } from '../../event-emitter.js';
|
|
38
|
+
import { LogLevel, makeLogger } from '../../logger.js';
|
|
39
|
+
import type { VercelInput, VercelOutput, VercelProjection } from '../codec/index.js';
|
|
40
|
+
import { UIMessageCodec } from '../codec/index.js';
|
|
41
|
+
import { createRunOutputStream } from './run-output-stream.js';
|
|
29
42
|
|
|
30
43
|
// ---------------------------------------------------------------------------
|
|
31
44
|
// ChatTransport options
|
|
@@ -49,24 +62,36 @@ export interface SendMessagesRequestContext {
|
|
|
49
62
|
messageId?: string;
|
|
50
63
|
/** Previous messages in the conversation (context for the LLM). */
|
|
51
64
|
history: AI.UIMessage[];
|
|
52
|
-
/** The new message(s) being sent (to publish to the channel). Empty for regeneration. */
|
|
65
|
+
/** The new message(s) being sent (to publish to the channel). Empty for regeneration and for continuations (an auto-submit where the last message is an already-tracked assistant). */
|
|
53
66
|
messages: AI.UIMessage[];
|
|
54
|
-
/** The
|
|
67
|
+
/** The codec-message-id of the message being forked — the edited user message, or the preceding assistant when forking off an unresolved tool call. Undefined for regeneration (View.regenerate derives it) and fresh sends. */
|
|
55
68
|
forkOf?: string;
|
|
56
|
-
/** The
|
|
69
|
+
/** The codec-message-id of the predecessor in the conversation thread. */
|
|
57
70
|
parent?: string;
|
|
58
71
|
}
|
|
59
72
|
|
|
73
|
+
/** Default agent endpoint the transport POSTs invocations to — mirrors Vercel's DefaultChatTransport. */
|
|
74
|
+
export const DEFAULT_VERCEL_API = '/api/chat';
|
|
75
|
+
|
|
60
76
|
/** Options for customizing the ChatTransport behavior. */
|
|
61
77
|
export interface ChatTransportOptions {
|
|
62
78
|
/**
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
79
|
+
* Endpoint the transport POSTs the invocation pointer to, to wake the
|
|
80
|
+
* agent. Mirrors useChat's request-driven contract. Default `/api/chat`.
|
|
81
|
+
*/
|
|
82
|
+
api?: string;
|
|
83
|
+
/** Fetch credentials mode for the invocation POST (e.g. `'include'`). */
|
|
84
|
+
credentials?: RequestCredentials;
|
|
85
|
+
/** Custom fetch implementation for the invocation POST. Defaults to `globalThis.fetch`. */
|
|
86
|
+
fetch?: typeof globalThis.fetch;
|
|
87
|
+
/**
|
|
88
|
+
* Customize the invocation POST before sending. Called by sendMessages()
|
|
89
|
+
* with the conversation context; the returned `body` is merged into the
|
|
90
|
+
* POST body (the run's invocation identifiers always take precedence) and
|
|
91
|
+
* `headers` are added to the request. Use it for auth headers or extra
|
|
92
|
+
* agent metadata.
|
|
68
93
|
* @param context - The conversation context for the current request.
|
|
69
|
-
* @returns The body and headers to
|
|
94
|
+
* @returns The body and headers to merge into the invocation POST.
|
|
70
95
|
*/
|
|
71
96
|
prepareSendMessagesRequest?: (context: SendMessagesRequestContext) => {
|
|
72
97
|
body?: Record<string, unknown>;
|
|
@@ -131,9 +156,9 @@ export interface ChatTransport {
|
|
|
131
156
|
) => Promise<ReadableStream<AI.UIMessageChunk> | null>;
|
|
132
157
|
|
|
133
158
|
/** Close the underlying transport, releasing all resources. */
|
|
134
|
-
close(
|
|
159
|
+
close(): Promise<void>;
|
|
135
160
|
|
|
136
|
-
/** Whether an own-
|
|
161
|
+
/** Whether an own-run stream is currently being consumed by useChat. */
|
|
137
162
|
readonly streaming: boolean;
|
|
138
163
|
|
|
139
164
|
/**
|
|
@@ -153,11 +178,17 @@ export interface ChatTransport {
|
|
|
153
178
|
/**
|
|
154
179
|
* Wrap a ReadableStream in a passthrough TransformStream that resolves a
|
|
155
180
|
* promise when the stream completes or errors. The returned stream passes
|
|
156
|
-
* all chunks through unchanged
|
|
181
|
+
* all chunks through unchanged, and `fail(reason)` errors the readable side
|
|
182
|
+
* useChat consumes without cancelling or otherwise disturbing the source run
|
|
183
|
+
* stream (used when the agent-invocation POST fails).
|
|
157
184
|
* @param source - The original stream to wrap.
|
|
158
|
-
* @returns The wrapped stream
|
|
185
|
+
* @returns The wrapped stream, a `done` promise that resolves when the stream
|
|
186
|
+
* closes, and a `fail` callback that errors the wrapped stream.
|
|
159
187
|
*/
|
|
160
|
-
|
|
188
|
+
|
|
189
|
+
const wrapStreamWithDone = <T>(
|
|
190
|
+
source: ReadableStream<T>,
|
|
191
|
+
): { stream: ReadableStream<T>; done: Promise<void>; fail: (reason: Ably.ErrorInfo) => void } => {
|
|
161
192
|
let resolveDone: () => void;
|
|
162
193
|
const done = new Promise<void>((resolve) => {
|
|
163
194
|
resolveDone = resolve;
|
|
@@ -169,28 +200,51 @@ const wrapStreamWithDone = <T>(source: ReadableStream<T>): { stream: ReadableStr
|
|
|
169
200
|
},
|
|
170
201
|
});
|
|
171
202
|
|
|
172
|
-
//
|
|
173
|
-
//
|
|
203
|
+
// Aborting this signal errors the destination (the readable useChat reads)
|
|
204
|
+
// with the abort reason. `preventCancel` keeps the source run stream intact
|
|
205
|
+
// so the tree/observers are unaffected — only the useChat-facing view fails.
|
|
206
|
+
const failController = new AbortController();
|
|
207
|
+
|
|
208
|
+
// Pipe in the background. If the source errors/cancels, or `fail()` aborts,
|
|
209
|
+
// resolve done so the serialization queue advances.
|
|
174
210
|
// Fire-and-forget: the pipe runs independently; errors surface through
|
|
175
211
|
// the readable side that useChat consumes.
|
|
176
|
-
source.pipeTo(passthrough.writable).catch(() => {
|
|
212
|
+
source.pipeTo(passthrough.writable, { signal: failController.signal, preventCancel: true }).catch(() => {
|
|
177
213
|
resolveDone();
|
|
178
214
|
});
|
|
179
215
|
|
|
180
|
-
return {
|
|
216
|
+
return {
|
|
217
|
+
stream: passthrough.readable,
|
|
218
|
+
done,
|
|
219
|
+
fail: (reason: Ably.ErrorInfo) => {
|
|
220
|
+
failController.abort(reason);
|
|
221
|
+
},
|
|
222
|
+
};
|
|
181
223
|
};
|
|
182
224
|
|
|
183
225
|
// ---------------------------------------------------------------------------
|
|
184
226
|
// Unresolved tool call detection
|
|
185
227
|
// ---------------------------------------------------------------------------
|
|
186
228
|
|
|
229
|
+
/**
|
|
230
|
+
* Whether a UIMessage part is a tool part — either the codec-normalised
|
|
231
|
+
* `dynamic-tool` shape or the AI SDK's statically-declared `tool-${name}`
|
|
232
|
+
* shape. Both carry `toolCallId` and `state`; the shape check at the end
|
|
233
|
+
* is defensive against a future AI SDK release introducing a non-tool
|
|
234
|
+
* variant under the `tool-` prefix (none exists today).
|
|
235
|
+
* @param part - The UIMessage part to inspect.
|
|
236
|
+
* @returns True when the part is a tool part of either representation.
|
|
237
|
+
*/
|
|
238
|
+
const _isToolPart = (part: AI.UIMessage['parts'][number]): part is AI.DynamicToolUIPart | AI.ToolUIPart =>
|
|
239
|
+
(part.type === 'dynamic-tool' || part.type.startsWith('tool-')) && 'toolCallId' in part && 'state' in part;
|
|
240
|
+
|
|
187
241
|
/**
|
|
188
242
|
* Whether an assistant message has a `dynamic-tool` part that can't resolve
|
|
189
243
|
* without further user action. Matches:
|
|
190
244
|
* - `input-streaming` / `input-available` — tool call emitted, not yet run.
|
|
191
245
|
* - `approval-requested` — waiting for the user.
|
|
192
246
|
*
|
|
193
|
-
* Excludes `approval-responded` (streamText will run the tool this
|
|
247
|
+
* Excludes `approval-responded` (streamText will run the tool this run)
|
|
194
248
|
* and all terminal `output-*` states.
|
|
195
249
|
* @param msg - The UIMessage to inspect.
|
|
196
250
|
* @returns True when a fork-on-send is warranted to avoid shipping a
|
|
@@ -200,55 +254,198 @@ const hasUnresolvedToolCall = (msg: AI.UIMessage): boolean =>
|
|
|
200
254
|
msg.role === 'assistant' &&
|
|
201
255
|
msg.parts.some(
|
|
202
256
|
(p) =>
|
|
203
|
-
p
|
|
257
|
+
_isToolPart(p) &&
|
|
204
258
|
(p.state === 'input-streaming' || p.state === 'input-available' || p.state === 'approval-requested'),
|
|
205
259
|
);
|
|
206
260
|
|
|
261
|
+
/**
|
|
262
|
+
* `dynamic-tool` part states that mean "the LLM produced a tool call and
|
|
263
|
+
* is waiting on it". Used to detect new client-side resolutions in the
|
|
264
|
+
* useChat overlay relative to the tree.
|
|
265
|
+
*/
|
|
266
|
+
const UNRESOLVED_TOOL_STATES = new Set(['input-streaming', 'input-available', 'approval-requested']);
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Walk the useChat message overlay against the session tree and synthesize
|
|
270
|
+
* the {@link VercelInput}s needed to resolve every `dynamic-tool` part the
|
|
271
|
+
* user acted on (executed a tool, approved, denied) but the tree's reduced
|
|
272
|
+
* state hasn't reflected yet.
|
|
273
|
+
*
|
|
274
|
+
* Each input carries the prior assistant's tree codec-message-id (the one
|
|
275
|
+
* holding the original `dynamic-tool` part the resolution targets) in its
|
|
276
|
+
* `codecMessageId` field, so the encoder stamps `codec-message-id`
|
|
277
|
+
* and the reducer's direct-fold path lands the resolution on that assistant
|
|
278
|
+
* in one step — no cross-message redirect-by-toolCallId fallback. Every
|
|
279
|
+
* variant rides the `ai-input` wire, matching its publisher (client → input).
|
|
280
|
+
*
|
|
281
|
+
* The resulting inputs are passed alongside the continuation `view.send`
|
|
282
|
+
* so the channel publish and the continuation POST land as ONE atomic
|
|
283
|
+
* operation — the agent's `loadProjection()` history fetch is guaranteed
|
|
284
|
+
* to see them because the channel publish happens before the POST inside
|
|
285
|
+
* `_internalSend`.
|
|
286
|
+
*
|
|
287
|
+
* Three resolutions are produced:
|
|
288
|
+
*
|
|
289
|
+
* - `approval-responded` overlay vs `approval-requested` tree →
|
|
290
|
+
* `tool-approval-response` carrying the user's decision
|
|
291
|
+
* (`approved` = `overlayPart.approval.approved`, i.e. approve or deny)
|
|
292
|
+
* - `output-available` overlay vs unresolved tree → `tool-result`
|
|
293
|
+
* - `output-error` overlay vs unresolved tree → `tool-result-error`
|
|
294
|
+
* @param codecMessages - The visible tree messages paired with their codec-message-ids.
|
|
295
|
+
* @param messages - useChat's local overlay messages.
|
|
296
|
+
* @returns The continuation inputs to publish, in tree order. Each input
|
|
297
|
+
* carries its own `codecMessageId` targeting the prior assistant it folds
|
|
298
|
+
* onto.
|
|
299
|
+
*/
|
|
300
|
+
const deriveContinuationInputs = (
|
|
301
|
+
codecMessages: CodecMessage<AI.UIMessage>[],
|
|
302
|
+
messages: AI.UIMessage[],
|
|
303
|
+
): VercelInput[] => {
|
|
304
|
+
const inputs: VercelInput[] = [];
|
|
305
|
+
for (const overlay of messages) {
|
|
306
|
+
if (overlay.role !== 'assistant') continue;
|
|
307
|
+
// Match the overlay to its tree message by domain id (both sides
|
|
308
|
+
// reconstruct the same stream id), but address the emitted inputs by
|
|
309
|
+
// the tree message's codec-message-id — the agent folds tool
|
|
310
|
+
// resolutions onto the assistant by codec-message-id, never by the
|
|
311
|
+
// domain `message.id`.
|
|
312
|
+
const treeEntry = codecMessages.find((p) => p.message.id === overlay.id);
|
|
313
|
+
if (!treeEntry) continue;
|
|
314
|
+
const { codecMessageId, message: treeMessage } = treeEntry;
|
|
315
|
+
|
|
316
|
+
for (const overlayPart of overlay.parts) {
|
|
317
|
+
if (!_isToolPart(overlayPart)) continue;
|
|
318
|
+
// The codec normalises every tool part to `dynamic-tool`, but the
|
|
319
|
+
// AI SDK's useChat overlay emits `tool-${name}` parts for statically
|
|
320
|
+
// declared tools. Match by toolCallId rather than the type prefix
|
|
321
|
+
// so the cross-representation comparison works regardless of which
|
|
322
|
+
// side the tool was declared on.
|
|
323
|
+
const treePart = treeMessage.parts.find(
|
|
324
|
+
(p: AI.UIMessage['parts'][number]): p is AI.DynamicToolUIPart | AI.ToolUIPart =>
|
|
325
|
+
_isToolPart(p) && p.toolCallId === overlayPart.toolCallId,
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
// Approval response: useChat's `addToolApprovalResponse` flipped the
|
|
329
|
+
// overlay part to `approval-responded` while the tree still sits on
|
|
330
|
+
// `approval-requested`. Publish a `tool-approval-response` TInput so the
|
|
331
|
+
// agent's projection sees the decision.
|
|
332
|
+
if (overlayPart.state === 'approval-responded' && (!treePart || treePart.state === 'approval-requested')) {
|
|
333
|
+
inputs.push(
|
|
334
|
+
UIMessageCodec.createToolApprovalResponse(codecMessageId, {
|
|
335
|
+
toolCallId: overlayPart.toolCallId,
|
|
336
|
+
approved: overlayPart.approval.approved,
|
|
337
|
+
...(overlayPart.approval.reason === undefined ? {} : { reason: overlayPart.approval.reason }),
|
|
338
|
+
}),
|
|
339
|
+
);
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Client-tool resolution: overlay has `output-available` / `output-error`
|
|
344
|
+
// while the tree's part is still unresolved. Construct a TInput
|
|
345
|
+
// variant (not a UIMessageChunk) so the encoder publishes on the
|
|
346
|
+
// `ai-input` wire — client tool results belong on `ai-input`, matching
|
|
347
|
+
// their client publisher, not on `ai-output`.
|
|
348
|
+
if (overlayPart.state !== 'output-available' && overlayPart.state !== 'output-error') continue;
|
|
349
|
+
// Tree already resolved (echo arrived back) — nothing to do.
|
|
350
|
+
if (treePart && !UNRESOLVED_TOOL_STATES.has(treePart.state)) continue;
|
|
351
|
+
|
|
352
|
+
if (overlayPart.state === 'output-available') {
|
|
353
|
+
inputs.push(
|
|
354
|
+
UIMessageCodec.createToolResult(codecMessageId, {
|
|
355
|
+
toolCallId: overlayPart.toolCallId,
|
|
356
|
+
output: overlayPart.output,
|
|
357
|
+
}),
|
|
358
|
+
);
|
|
359
|
+
} else {
|
|
360
|
+
inputs.push(
|
|
361
|
+
UIMessageCodec.createToolResultError(codecMessageId, {
|
|
362
|
+
toolCallId: overlayPart.toolCallId,
|
|
363
|
+
message: overlayPart.errorText,
|
|
364
|
+
}),
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return inputs;
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Find the codec-message-id immediately preceding the message identified by
|
|
374
|
+
* domain id `domainId` in the flat visible conversation. The target is
|
|
375
|
+
* located by its domain `message.id` (the id useChat references), but the
|
|
376
|
+
* returned value is the predecessor's codec-message-id — never a domain id.
|
|
377
|
+
* Returns undefined if the target is the first message or not found.
|
|
378
|
+
* @param codecMessages - Visible messages paired with their codec-message-ids.
|
|
379
|
+
* @param domainId - The domain id of the target message.
|
|
380
|
+
* @returns The predecessor's codec-message-id, or undefined.
|
|
381
|
+
*/
|
|
382
|
+
const findPredecessorCodecId = (codecMessages: CodecMessage<AI.UIMessage>[], domainId: string): string | undefined => {
|
|
383
|
+
const idx = codecMessages.findIndex((p) => p.message.id === domainId);
|
|
384
|
+
if (idx <= 0) return undefined;
|
|
385
|
+
return codecMessages[idx - 1]?.codecMessageId;
|
|
386
|
+
};
|
|
387
|
+
|
|
207
388
|
// ---------------------------------------------------------------------------
|
|
208
389
|
// Factory
|
|
209
390
|
// ---------------------------------------------------------------------------
|
|
210
391
|
|
|
392
|
+
/** Internal EventEmitter events map backing the transport's streaming state. */
|
|
393
|
+
interface ChatTransportEventsMap {
|
|
394
|
+
/** Fired on every streaming-state transition with the new value. */
|
|
395
|
+
streaming: boolean;
|
|
396
|
+
}
|
|
397
|
+
|
|
211
398
|
/**
|
|
212
|
-
* Create a Vercel ChatTransport from a core
|
|
399
|
+
* Create a Vercel ChatTransport from a core ClientSession.
|
|
213
400
|
*
|
|
214
401
|
* Exposes a `streaming` flag and `onStreamingChange` callback so that
|
|
215
|
-
* `useMessageSync` can gate `setMessages` calls during active own-
|
|
402
|
+
* `useMessageSync` can gate `setMessages` calls during active own-run
|
|
216
403
|
* streams, preventing the push/replace ID mismatch in useChat's `write()`.
|
|
217
404
|
*
|
|
218
405
|
* Note: concurrent `sendMessage` calls from the same user are a useChat
|
|
219
406
|
* limitation that cannot be fixed from the transport layer. The
|
|
220
407
|
* developer must respect useChat's `status` and only call `sendMessage`
|
|
221
408
|
* when status is `'ready'`.
|
|
222
|
-
* @param
|
|
409
|
+
* @param session - The core client session to wrap.
|
|
223
410
|
* @param chatOptions - Optional hooks for customizing request construction.
|
|
224
411
|
* @returns A {@link ChatTransport} compatible with Vercel's useChat hook.
|
|
225
412
|
*/
|
|
226
413
|
export const createChatTransport = (
|
|
227
|
-
|
|
414
|
+
session: ClientSession<VercelInput, VercelOutput, VercelProjection, AI.UIMessage>,
|
|
228
415
|
chatOptions?: ChatTransportOptions,
|
|
229
416
|
): ChatTransport => {
|
|
417
|
+
// -- Invocation POST config (the transport owns waking the agent) ----------
|
|
418
|
+
const api = chatOptions?.api ?? DEFAULT_VERCEL_API;
|
|
419
|
+
const fetchFn = chatOptions?.fetch ?? globalThis.fetch.bind(globalThis);
|
|
420
|
+
const credentials = chatOptions?.credentials;
|
|
421
|
+
|
|
230
422
|
// -- Streaming state -------------------------------------------------------
|
|
423
|
+
// Backed by the shared EventEmitter for listener error isolation (one bad
|
|
424
|
+
// onStreamingChange handler can't prevent others from firing or block the
|
|
425
|
+
// state transition) and uniform emitter behaviour across the SDK. The
|
|
426
|
+
// factory takes no logger, so a silent one is used — listener exceptions are
|
|
427
|
+
// swallowed by the emitter rather than surfaced.
|
|
231
428
|
let _streaming = false;
|
|
232
|
-
const
|
|
429
|
+
const emitter = new EventEmitter<ChatTransportEventsMap>(makeLogger({ logLevel: LogLevel.Silent }));
|
|
233
430
|
|
|
234
431
|
const setStreaming = (value: boolean): void => {
|
|
235
432
|
_streaming = value;
|
|
236
|
-
|
|
237
|
-
try {
|
|
238
|
-
cb(value);
|
|
239
|
-
} catch {
|
|
240
|
-
// Isolate subscriber errors so one bad handler doesn't prevent
|
|
241
|
-
// other subscribers from being notified or block the streaming
|
|
242
|
-
// state transition.
|
|
243
|
-
}
|
|
244
|
-
}
|
|
433
|
+
emitter.emit('streaming', value);
|
|
245
434
|
};
|
|
246
435
|
|
|
247
436
|
// -- sendMessages implementation -------------------------------------------
|
|
248
437
|
|
|
249
438
|
const sendMessages: ChatTransport['sendMessages'] = async (opts) => {
|
|
250
439
|
const { messages, abortSignal, trigger, messageId } = opts;
|
|
251
|
-
|
|
440
|
+
|
|
441
|
+
// The visible messages paired with their codec-message-ids. useChat
|
|
442
|
+
// references messages by their domain `message.id`; we match on that to
|
|
443
|
+
// locate a message in the tree, then route every transport operation by
|
|
444
|
+
// the message's codec-message-id (the SDK never correlates on the domain
|
|
445
|
+
// id, which may differ from the codec-message-id).
|
|
446
|
+
const codecMessages = session.view.getMessages();
|
|
447
|
+
const codecIdByDomainId = new Map(codecMessages.map((m) => [m.message.id, m.codecMessageId]));
|
|
448
|
+
const codecIdOf = (domainId: string): string | undefined => codecIdByDomainId.get(domainId);
|
|
252
449
|
|
|
253
450
|
// useChat calls sendMessages in three distinct modes. We disambiguate
|
|
254
451
|
// by (trigger, last-message role) so each mode dispatches correctly:
|
|
@@ -265,27 +462,29 @@ export const createChatTransport = (
|
|
|
265
462
|
// Continuation mode must NOT publish the assistant as a new message or
|
|
266
463
|
// treat messageId as a fork target — useChat v6's sendAutomaticallyWhen
|
|
267
464
|
// path always sets messageId to the last message id regardless.
|
|
268
|
-
//
|
|
269
|
-
// Client-side tool outputs are expected to be staged on the transport
|
|
270
|
-
// via transport.stageEvents() before this runs; the core transport
|
|
271
|
-
// flushes staged events into the POST body automatically.
|
|
272
465
|
const lastMessage = messages.at(-1);
|
|
273
|
-
const
|
|
274
|
-
const isContinuation = trigger === 'submit-message' && lastMessage?.role === 'assistant' &&
|
|
466
|
+
const lastMessageInTree = !!lastMessage && codecIdByDomainId.has(lastMessage.id);
|
|
467
|
+
const isContinuation = trigger === 'submit-message' && lastMessage?.role === 'assistant' && lastMessageInTree;
|
|
275
468
|
|
|
276
469
|
// Fork-on-unresolved-tool: user sent a new message while the preceding
|
|
277
470
|
// assistant has an unresolved tool call (approval-requested, input-*).
|
|
278
471
|
// Fork the new message off the preceding assistant so the unresolved
|
|
279
|
-
// tool call stays dormant on a sibling branch. Inference this
|
|
472
|
+
// tool call stays dormant on a sibling branch. Inference for this run runs
|
|
280
473
|
// on the clean fork — the LLM never sees the dangling tool_use.
|
|
281
474
|
//
|
|
282
475
|
// Only applies to fresh user-message submits (not continuations, not
|
|
283
476
|
// regenerates, not edits-with-messageId).
|
|
477
|
+
//
|
|
478
|
+
// `messages.at(-1)` is the fresh user-prompt being submitted right now;
|
|
479
|
+
// `messages.at(-2)` is therefore the prior assistant whose tool state
|
|
480
|
+
// we need to inspect for the unresolved-tool gate below.
|
|
284
481
|
const precedingMessage =
|
|
285
482
|
trigger === 'submit-message' && !messageId && lastMessage?.role === 'user' ? messages.at(-2) : undefined;
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
483
|
+
// The domain id of the preceding assistant when it carries an unresolved
|
|
484
|
+
// tool call and is present in the tree — the new user message forks off it.
|
|
485
|
+
const forkSourceDomainId =
|
|
486
|
+
precedingMessage && hasUnresolvedToolCall(precedingMessage) && codecIdByDomainId.has(precedingMessage.id)
|
|
487
|
+
? precedingMessage.id
|
|
289
488
|
: undefined;
|
|
290
489
|
|
|
291
490
|
// Determine the history/messages split based on mode.
|
|
@@ -309,35 +508,27 @@ export const createChatTransport = (
|
|
|
309
508
|
// When forking off an unresolved tool call, drop the unresolved
|
|
310
509
|
// assistant from history too — it belongs on the sibling branch, not
|
|
311
510
|
// the ancestor chain of the new message.
|
|
312
|
-
history =
|
|
511
|
+
history = forkSourceDomainId ? messages.slice(0, -2) : messages.slice(0, -1);
|
|
313
512
|
}
|
|
314
513
|
|
|
315
|
-
// Compute fork metadata
|
|
316
|
-
//
|
|
514
|
+
// Compute fork metadata for edit (submit-message with messageId) and
|
|
515
|
+
// fork-on-unresolved-tool. Regenerate is NOT precomputed here —
|
|
516
|
+
// `View.regenerate` derives forkOf/parent from the tree itself and
|
|
517
|
+
// overrides anything we'd set.
|
|
317
518
|
let forkOf: string | undefined;
|
|
318
519
|
let parent: string | undefined;
|
|
319
520
|
|
|
320
|
-
if (messageId && !isContinuation) {
|
|
321
|
-
//
|
|
322
|
-
//
|
|
323
|
-
//
|
|
324
|
-
forkOf = messageId;
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
forkOf = node.msgId;
|
|
328
|
-
parent = node.parentId;
|
|
329
|
-
}
|
|
330
|
-
} else if (isContinuation) {
|
|
331
|
-
// Continuation: the server's next assistant message is a child of the
|
|
332
|
-
// last assistant (no fork). Pass parent so the server places the new
|
|
333
|
-
// message correctly in the tree. isContinuation narrows lastMessageNode
|
|
334
|
-
// to defined.
|
|
335
|
-
parent = lastMessageNode.msgId;
|
|
336
|
-
} else if (forkSource) {
|
|
521
|
+
if (trigger === 'submit-message' && messageId && !isContinuation) {
|
|
522
|
+
// Edit: messageId is the domain id of the user message being replaced.
|
|
523
|
+
// forkOf = its codec-message-id, parent = the immediately-preceding
|
|
524
|
+
// codec-message-id in the flat conversation.
|
|
525
|
+
forkOf = codecIdOf(messageId);
|
|
526
|
+
parent = findPredecessorCodecId(codecMessages, messageId);
|
|
527
|
+
} else if (forkSourceDomainId) {
|
|
337
528
|
// Fork off the preceding assistant — the new user message becomes a
|
|
338
529
|
// sibling of the unresolved tool call assistant, rooted at its parent.
|
|
339
|
-
forkOf =
|
|
340
|
-
parent =
|
|
530
|
+
forkOf = codecIdOf(forkSourceDomainId);
|
|
531
|
+
parent = findPredecessorCodecId(codecMessages, forkSourceDomainId);
|
|
341
532
|
}
|
|
342
533
|
|
|
343
534
|
let sendBody: Record<string, unknown>;
|
|
@@ -356,38 +547,102 @@ export const createChatTransport = (
|
|
|
356
547
|
sendBody = prepared.body ?? {};
|
|
357
548
|
sendHeaders = prepared.headers;
|
|
358
549
|
} else {
|
|
359
|
-
|
|
360
|
-
const historyNodes = allNodes.filter((n) => historyIds.has(n.message.id));
|
|
361
|
-
sendBody = {
|
|
362
|
-
history: historyNodes,
|
|
363
|
-
chatId: opts.chatId,
|
|
364
|
-
trigger,
|
|
365
|
-
...(messageId !== undefined && { messageId }),
|
|
366
|
-
...(forkOf !== undefined && { forkOf }),
|
|
367
|
-
...(parent !== undefined && { parent }),
|
|
368
|
-
};
|
|
550
|
+
sendBody = {};
|
|
369
551
|
sendHeaders = undefined;
|
|
370
552
|
}
|
|
371
553
|
|
|
372
|
-
const sendOpts: SendOptions = {
|
|
554
|
+
const sendOpts: SendOptions = {};
|
|
373
555
|
if (forkOf !== undefined) sendOpts.forkOf = forkOf;
|
|
374
556
|
if (parent !== undefined) sendOpts.parent = parent;
|
|
557
|
+
// Continuations reuse the suspended assistant's runId so the agent's
|
|
558
|
+
// existing run resumes under a fresh invocation rather than spinning
|
|
559
|
+
// up a brand-new run. `isContinuation` implies `lastMessage` is defined.
|
|
560
|
+
if (isContinuation) {
|
|
561
|
+
// `isContinuation` implies `lastMessage` is defined (it gates on
|
|
562
|
+
// `lastMessage?.role`). Route the runId lookup by codec-message-id.
|
|
563
|
+
const codecId = codecIdOf(lastMessage.id);
|
|
564
|
+
const run = codecId === undefined ? undefined : session.view.runOf(codecId);
|
|
565
|
+
if (run) sendOpts.runId = run.runId;
|
|
566
|
+
}
|
|
375
567
|
|
|
376
|
-
//
|
|
377
|
-
//
|
|
378
|
-
//
|
|
379
|
-
|
|
568
|
+
// Dispatch by mode:
|
|
569
|
+
//
|
|
570
|
+
// - Continuation: derive tool-resolution events from useChat's overlay
|
|
571
|
+
// vs the tree and pair each with the prior assistant's tree codec-message-id —
|
|
572
|
+
// the SDK stamps the wire's `codec-message-id` to that id so the
|
|
573
|
+
// reducer's direct fold path runs (no redirect, no consume).
|
|
574
|
+
// - Regenerate: route through `view.regenerate`. The View mints a
|
|
575
|
+
// wire-only regenerate event (`ait-regenerate`) carrying
|
|
576
|
+
// `forkOf=A1` / `parent=U1` on transport headers. U1 is NOT
|
|
577
|
+
// republished — A1 and A2 group as tree siblings under U1 via the
|
|
578
|
+
// existing forkOf machinery. The LLM receives the truncated history
|
|
579
|
+
// through U1 inclusive via the body.
|
|
580
|
+
// - Fresh send / edit: publish the new user-message input(s) via
|
|
581
|
+
// `view.send`.
|
|
582
|
+
let run: ActiveRun;
|
|
583
|
+
if (isContinuation) {
|
|
584
|
+
const inputs = deriveContinuationInputs(codecMessages, messages);
|
|
585
|
+
run = await session.view.send(inputs, sendOpts);
|
|
586
|
+
} else if (trigger === 'regenerate-message') {
|
|
587
|
+
if (messageId === undefined) {
|
|
588
|
+
throw new Ably.ErrorInfo(
|
|
589
|
+
'unable to regenerate; regenerate-message trigger fired without messageId',
|
|
590
|
+
ErrorCode.InvalidArgument,
|
|
591
|
+
400,
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
// useChat passes the assistant's domain id; route by its codec-message-id.
|
|
595
|
+
const regenCodecId = codecIdOf(messageId);
|
|
596
|
+
if (regenCodecId === undefined) {
|
|
597
|
+
throw new Ably.ErrorInfo(
|
|
598
|
+
`unable to regenerate; message not visible: ${messageId}`,
|
|
599
|
+
ErrorCode.InvalidArgument,
|
|
600
|
+
400,
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
run = await session.view.regenerate(regenCodecId, sendOpts);
|
|
604
|
+
} else {
|
|
605
|
+
const inputs = newMessages.map((m) => UIMessageCodec.createUserMessage(m));
|
|
606
|
+
run = await session.view.send(inputs, sendOpts);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// Build the consumer-facing stream from the Tree's events for this run.
|
|
610
|
+
// Streaming is a useChat concern owned by the Vercel layer; the core
|
|
611
|
+
// session exposes no per-run stream. Key it on
|
|
612
|
+
// `run.inputCodecMessageId` — the triggering input's codec-message-id, which
|
|
613
|
+
// the client owns from send time and the agent echoes as
|
|
614
|
+
// `input-codec-message-id`. The agent mints the runId, supplied as
|
|
615
|
+
// `run.runId` (a promise) for the run-end safety-net.
|
|
616
|
+
const runStream = createRunOutputStream(session, run.runId, run.inputCodecMessageId);
|
|
380
617
|
|
|
381
618
|
if (abortSignal) {
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
619
|
+
const onAbort = (): void => {
|
|
620
|
+
// Best-effort cancel via the run handle (knows its own key / runId);
|
|
621
|
+
// the core resolves the runId once the agent mints it.
|
|
622
|
+
void run.cancel();
|
|
623
|
+
// Close the consumer stream immediately so useChat's reader ends
|
|
624
|
+
// without waiting for the agent's run-end round-trip.
|
|
625
|
+
runStream.close();
|
|
626
|
+
};
|
|
627
|
+
// useChat sets `status: 'submitted'` synchronously inside `makeRequest`
|
|
628
|
+
// BEFORE awaiting `transport.sendMessages`. That immediately enables
|
|
629
|
+
// the Stop button in the UI. If the user clicks Stop while
|
|
630
|
+
// `session.view.send` is still awaiting the run-start ack (which
|
|
631
|
+
// can take seconds for a real LLM), useChat aborts the signal before
|
|
632
|
+
// we ever get here. `addEventListener('abort', ...)` does not fire
|
|
633
|
+
// for an already-aborted signal, so we'd silently lose the cancel
|
|
634
|
+
// and the agent would keep streaming.
|
|
635
|
+
if (abortSignal.aborted) {
|
|
636
|
+
onAbort();
|
|
637
|
+
} else {
|
|
638
|
+
abortSignal.addEventListener('abort', onAbort, { once: true });
|
|
639
|
+
}
|
|
385
640
|
}
|
|
386
641
|
|
|
387
642
|
// Wrap the stream to detect completion. The streaming flag gates
|
|
388
643
|
// useMessageSync so that setMessages doesn't interfere with
|
|
389
644
|
// useChat's internal write() during active streams.
|
|
390
|
-
const { stream, done } = wrapStreamWithDone(
|
|
645
|
+
const { stream, done, fail } = wrapStreamWithDone(runStream.stream);
|
|
391
646
|
setStreaming(true);
|
|
392
647
|
|
|
393
648
|
// Fire-and-forget: clear the streaming flag when the stream ends.
|
|
@@ -395,6 +650,45 @@ export const createChatTransport = (
|
|
|
395
650
|
setStreaming(false);
|
|
396
651
|
});
|
|
397
652
|
|
|
653
|
+
// Wake the agent: POST the invocation pointer to the configured endpoint.
|
|
654
|
+
// useChat's transport contract is request-driven, so the transport owns
|
|
655
|
+
// this POST (the core session is HTTP-free). Fire-and-forget — `await`
|
|
656
|
+
// would delay the stream return, and the agent's response arrives over
|
|
657
|
+
// the Ably channel, not the HTTP response. The run's invocation
|
|
658
|
+
// identifiers always win over any custom body so the agent can parse it
|
|
659
|
+
// via Invocation.fromJSON. A failed POST means the agent never woke, so
|
|
660
|
+
// error the useChat-facing stream; the core run and observers are
|
|
661
|
+
// untouched.
|
|
662
|
+
const postBody = { ...sendBody, ...run.toInvocation().toJSON() };
|
|
663
|
+
fetchFn(api, {
|
|
664
|
+
method: 'POST',
|
|
665
|
+
headers: { 'Content-Type': 'application/json', ...sendHeaders },
|
|
666
|
+
body: JSON.stringify(postBody),
|
|
667
|
+
...(credentials ? { credentials } : {}),
|
|
668
|
+
})
|
|
669
|
+
.then((response) => {
|
|
670
|
+
if (!response.ok) {
|
|
671
|
+
fail(
|
|
672
|
+
new Ably.ErrorInfo(
|
|
673
|
+
`unable to send; HTTP POST to ${api} returned ${String(response.status)} ${response.statusText}`,
|
|
674
|
+
ErrorCode.SessionSendFailed,
|
|
675
|
+
response.status,
|
|
676
|
+
),
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
})
|
|
680
|
+
.catch((error: unknown) => {
|
|
681
|
+
const cause = error instanceof Ably.ErrorInfo ? error : undefined;
|
|
682
|
+
fail(
|
|
683
|
+
new Ably.ErrorInfo(
|
|
684
|
+
`unable to send; HTTP POST to ${api} failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
685
|
+
ErrorCode.SessionSendFailed,
|
|
686
|
+
500,
|
|
687
|
+
cause,
|
|
688
|
+
),
|
|
689
|
+
);
|
|
690
|
+
});
|
|
691
|
+
|
|
398
692
|
return stream;
|
|
399
693
|
};
|
|
400
694
|
|
|
@@ -408,16 +702,16 @@ export const createChatTransport = (
|
|
|
408
702
|
// eslint-disable-next-line unicorn/no-null, @typescript-eslint/promise-function-async -- null is required by the AI SDK ChatTransport contract; no await needed
|
|
409
703
|
reconnectToStream: () => Promise.resolve(null),
|
|
410
704
|
|
|
411
|
-
close: async (
|
|
705
|
+
close: async () => session.close(),
|
|
412
706
|
|
|
413
707
|
get streaming(): boolean {
|
|
414
708
|
return _streaming;
|
|
415
709
|
},
|
|
416
710
|
|
|
417
711
|
onStreamingChange: (callback: (streaming: boolean) => void): (() => void) => {
|
|
418
|
-
|
|
712
|
+
emitter.on('streaming', callback);
|
|
419
713
|
return () => {
|
|
420
|
-
|
|
714
|
+
emitter.off('streaming', callback);
|
|
421
715
|
};
|
|
422
716
|
},
|
|
423
717
|
};
|