@ably/ai-transport 0.0.1 → 0.1.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 +54 -47
- package/dist/ably-ai-transport.js +1006 -539
- 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 +4 -0
- package/dist/core/codec/types.d.ts +19 -2
- package/dist/core/transport/decode-history.d.ts +8 -6
- package/dist/core/transport/headers.d.ts +4 -2
- package/dist/core/transport/index.d.ts +4 -1
- package/dist/core/transport/pipe-stream.d.ts +3 -2
- package/dist/core/transport/stream-router.d.ts +11 -1
- package/dist/core/transport/tree.d.ts +171 -0
- package/dist/core/transport/turn-manager.d.ts +4 -1
- package/dist/core/transport/types.d.ts +270 -119
- package/dist/core/transport/view.d.ts +166 -0
- package/dist/errors.d.ts +19 -2
- package/dist/index.d.ts +3 -1
- package/dist/react/ably-ai-transport-react.js +1019 -486
- 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/transport-context.d.ts +31 -0
- package/dist/react/contexts/transport-provider.d.ts +49 -0
- package/dist/react/create-transport-hooks.d.ts +124 -0
- package/dist/react/index.d.ts +14 -8
- package/dist/react/use-ably-messages.d.ts +14 -8
- package/dist/react/use-active-turns.d.ts +7 -3
- package/dist/react/use-client-transport.d.ts +78 -5
- package/dist/react/use-create-view.d.ts +22 -0
- package/dist/react/use-tree.d.ts +20 -0
- package/dist/react/use-view.d.ts +79 -0
- package/dist/vercel/ably-ai-transport-vercel.js +1478 -842
- 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/tool-transitions.d.ts +50 -0
- package/dist/vercel/index.d.ts +3 -0
- package/dist/vercel/react/ably-ai-transport-vercel-react.js +9099 -852
- package/dist/vercel/react/ably-ai-transport-vercel-react.js.map +1 -1
- package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs +45 -1
- package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs.map +1 -1
- package/dist/vercel/react/contexts/chat-transport-context.d.ts +32 -0
- package/dist/vercel/react/contexts/chat-transport-provider.d.ts +84 -0
- package/dist/vercel/react/index.d.ts +5 -0
- package/dist/vercel/react/use-chat-transport.d.ts +61 -20
- package/dist/vercel/react/use-message-sync.d.ts +41 -9
- package/dist/vercel/react/use-staged-add-tool-approval-response.d.ts +30 -0
- package/dist/vercel/tool-approvals.d.ts +124 -0
- package/dist/vercel/tool-events.d.ts +26 -0
- package/dist/vercel/transport/chat-transport.d.ts +33 -11
- package/dist/vercel/transport/index.d.ts +5 -2
- package/package.json +23 -17
- package/src/constants.ts +6 -0
- package/src/core/codec/encoder.ts +10 -1
- package/src/core/codec/types.ts +19 -3
- package/src/core/transport/client-transport.ts +382 -364
- package/src/core/transport/decode-history.ts +229 -81
- package/src/core/transport/headers.ts +6 -2
- package/src/core/transport/index.ts +13 -5
- package/src/core/transport/pipe-stream.ts +8 -5
- package/src/core/transport/server-transport.ts +212 -58
- package/src/core/transport/stream-router.ts +21 -3
- package/src/core/transport/{conversation-tree.ts → tree.ts} +192 -77
- package/src/core/transport/turn-manager.ts +28 -10
- package/src/core/transport/types.ts +318 -139
- package/src/core/transport/view.ts +840 -0
- package/src/errors.ts +21 -1
- package/src/index.ts +10 -5
- package/src/react/contexts/transport-context.ts +37 -0
- package/src/react/contexts/transport-provider.tsx +164 -0
- package/src/react/create-transport-hooks.ts +144 -0
- package/src/react/index.ts +15 -8
- package/src/react/use-ably-messages.ts +34 -16
- package/src/react/use-active-turns.ts +28 -17
- package/src/react/use-client-transport.ts +184 -24
- package/src/react/use-create-view.ts +68 -0
- package/src/react/use-tree.ts +53 -0
- package/src/react/use-view.ts +233 -0
- package/src/react/vite.config.ts +4 -1
- package/src/vercel/codec/accumulator.ts +64 -79
- package/src/vercel/codec/decoder.ts +11 -8
- package/src/vercel/codec/encoder.ts +68 -54
- package/src/vercel/codec/index.ts +0 -2
- package/src/vercel/codec/tool-transitions.ts +122 -0
- package/src/vercel/index.ts +17 -0
- package/src/vercel/react/contexts/chat-transport-context.ts +40 -0
- package/src/vercel/react/contexts/chat-transport-provider.tsx +122 -0
- package/src/vercel/react/index.ts +14 -0
- package/src/vercel/react/use-chat-transport.ts +164 -42
- package/src/vercel/react/use-message-sync.ts +77 -19
- package/src/vercel/react/use-staged-add-tool-approval-response.ts +87 -0
- package/src/vercel/react/vite.config.ts +4 -2
- package/src/vercel/tool-approvals.ts +380 -0
- package/src/vercel/tool-events.ts +53 -0
- package/src/vercel/transport/chat-transport.ts +225 -79
- package/src/vercel/transport/index.ts +14 -3
- package/dist/core/transport/conversation-tree.d.ts +0 -9
- package/dist/react/use-conversation-tree.d.ts +0 -20
- package/dist/react/use-edit.d.ts +0 -7
- package/dist/react/use-history.d.ts +0 -19
- package/dist/react/use-messages.d.ts +0 -7
- package/dist/react/use-regenerate.d.ts +0 -7
- package/dist/react/use-send.d.ts +0 -7
- package/src/react/use-conversation-tree.ts +0 -71
- package/src/react/use-edit.ts +0 -24
- package/src/react/use-history.ts +0 -111
- package/src/react/use-messages.ts +0 -32
- package/src/react/use-regenerate.ts +0 -24
- package/src/react/use-send.ts +0 -25
|
@@ -18,7 +18,6 @@ import {
|
|
|
18
18
|
HEADER_CANCEL_CLIENT_ID,
|
|
19
19
|
HEADER_CANCEL_OWN,
|
|
20
20
|
HEADER_CANCEL_TURN_ID,
|
|
21
|
-
HEADER_MSG_ID,
|
|
22
21
|
} from '../../constants.js';
|
|
23
22
|
import { ErrorCode } from '../../errors.js';
|
|
24
23
|
import type { Logger } from '../../logger.js';
|
|
@@ -32,7 +31,8 @@ import type {
|
|
|
32
31
|
AddMessagesResult,
|
|
33
32
|
CancelFilter,
|
|
34
33
|
CancelRequest,
|
|
35
|
-
|
|
34
|
+
EventsNode,
|
|
35
|
+
MessageNode,
|
|
36
36
|
NewTurnOptions,
|
|
37
37
|
ServerTransport,
|
|
38
38
|
ServerTransportOptions,
|
|
@@ -50,10 +50,27 @@ interface RegisteredTurn {
|
|
|
50
50
|
turnId: string;
|
|
51
51
|
clientId: string;
|
|
52
52
|
controller: AbortController;
|
|
53
|
+
/** Composite signal that fires when either the internal controller or the external signal aborts. */
|
|
54
|
+
signal: AbortSignal;
|
|
53
55
|
onCancel?: (request: CancelRequest) => Promise<boolean>;
|
|
54
56
|
onError?: (error: Ably.ErrorInfo) => void;
|
|
55
57
|
}
|
|
56
58
|
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
// Internal state machines
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
enum ServerTransportState {
|
|
64
|
+
READY = 'ready',
|
|
65
|
+
CLOSED = 'closed',
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
enum TurnState {
|
|
69
|
+
INITIALIZED = 'initialized',
|
|
70
|
+
STARTED = 'started',
|
|
71
|
+
ENDED = 'ended',
|
|
72
|
+
}
|
|
73
|
+
|
|
57
74
|
// ---------------------------------------------------------------------------
|
|
58
75
|
// Implementation
|
|
59
76
|
// ---------------------------------------------------------------------------
|
|
@@ -69,6 +86,10 @@ class DefaultServerTransport<TEvent, TMessage> implements ServerTransport<TEvent
|
|
|
69
86
|
private readonly _channelListener: (msg: Ably.InboundMessage) => void;
|
|
70
87
|
private readonly _attachPromise: Promise<void>;
|
|
71
88
|
|
|
89
|
+
private _state = ServerTransportState.READY;
|
|
90
|
+
private _hasAttachedOnce: boolean;
|
|
91
|
+
private readonly _onChannelStateChange: Ably.channelEventCallback;
|
|
92
|
+
|
|
72
93
|
constructor(options: ServerTransportOptions<TEvent, TMessage>) {
|
|
73
94
|
this._channel = options.channel;
|
|
74
95
|
this._codec = options.codec;
|
|
@@ -98,6 +119,19 @@ class DefaultServerTransport<TEvent, TMessage> implements ServerTransport<TEvent
|
|
|
98
119
|
},
|
|
99
120
|
);
|
|
100
121
|
|
|
122
|
+
// Spec: AIT-ST12, AIT-ST12a
|
|
123
|
+
// Listen for channel state changes that break message continuity. The
|
|
124
|
+
// server only consumes cancel messages from the channel, so losing one
|
|
125
|
+
// is survivable — but the developer needs to know so they can decide
|
|
126
|
+
// whether to abort in-flight work. _hasAttachedOnce is seeded from the
|
|
127
|
+
// channel's current state so pre-attached channels are handled correctly;
|
|
128
|
+
// it distinguishes the initial attach from a genuine discontinuity.
|
|
129
|
+
this._hasAttachedOnce = this._channel.state === 'attached';
|
|
130
|
+
this._onChannelStateChange = (stateChange: Ably.ChannelStateChange) => {
|
|
131
|
+
this._handleChannelStateChange(stateChange);
|
|
132
|
+
};
|
|
133
|
+
this._channel.on(this._onChannelStateChange);
|
|
134
|
+
|
|
101
135
|
this._logger?.debug('DefaultServerTransport(); transport created');
|
|
102
136
|
}
|
|
103
137
|
|
|
@@ -113,8 +147,11 @@ class DefaultServerTransport<TEvent, TMessage> implements ServerTransport<TEvent
|
|
|
113
147
|
|
|
114
148
|
// Spec: AIT-ST11
|
|
115
149
|
close(): void {
|
|
150
|
+
if (this._state === ServerTransportState.CLOSED) return;
|
|
151
|
+
this._state = ServerTransportState.CLOSED;
|
|
116
152
|
this._logger?.trace('DefaultServerTransport.close();');
|
|
117
153
|
this._channel.unsubscribe(EVENT_CANCEL, this._channelListener);
|
|
154
|
+
this._channel.off(this._onChannelStateChange);
|
|
118
155
|
for (const reg of this._registeredTurns.values()) {
|
|
119
156
|
reg.controller.abort();
|
|
120
157
|
}
|
|
@@ -143,10 +180,11 @@ class DefaultServerTransport<TEvent, TMessage> implements ServerTransport<TEvent
|
|
|
143
180
|
return [];
|
|
144
181
|
}
|
|
145
182
|
|
|
146
|
-
// Spec: AIT-ST8, AIT-ST9
|
|
183
|
+
// Spec: AIT-ST8, AIT-ST8a, AIT-ST8b, AIT-ST8c, AIT-ST8d, AIT-ST9, AIT-ST9a
|
|
147
184
|
private async _handleCancelMessage(msg: Ably.InboundMessage): Promise<void> {
|
|
148
185
|
const headers = getHeaders(msg);
|
|
149
186
|
|
|
187
|
+
// Spec: AIT-ST8a, AIT-ST8b, AIT-ST8c, AIT-ST8d
|
|
150
188
|
const filter: CancelFilter = {};
|
|
151
189
|
if (headers[HEADER_CANCEL_TURN_ID]) {
|
|
152
190
|
filter.turnId = headers[HEADER_CANCEL_TURN_ID];
|
|
@@ -203,6 +241,50 @@ class DefaultServerTransport<TEvent, TMessage> implements ServerTransport<TEvent
|
|
|
203
241
|
}
|
|
204
242
|
}
|
|
205
243
|
|
|
244
|
+
// -------------------------------------------------------------------------
|
|
245
|
+
// Channel state change handler
|
|
246
|
+
// -------------------------------------------------------------------------
|
|
247
|
+
|
|
248
|
+
// Spec: AIT-ST12, AIT-ST12a
|
|
249
|
+
private _handleChannelStateChange(stateChange: Ably.ChannelStateChange): void {
|
|
250
|
+
if (this._state === ServerTransportState.CLOSED) return;
|
|
251
|
+
|
|
252
|
+
const { current, resumed } = stateChange;
|
|
253
|
+
|
|
254
|
+
// Track the initial attach so we don't treat it as a discontinuity
|
|
255
|
+
if (current === 'attached' && !this._hasAttachedOnce) {
|
|
256
|
+
this._hasAttachedOnce = true;
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Continuity-breaking states:
|
|
261
|
+
// - FAILED, SUSPENDED, DETACHED: no more messages expected (or gap)
|
|
262
|
+
// - ATTACHED with resumed: false (UPDATE): messages were lost
|
|
263
|
+
const continuityLost =
|
|
264
|
+
current === 'failed' || current === 'suspended' || current === 'detached' || (current === 'attached' && !resumed);
|
|
265
|
+
|
|
266
|
+
if (!continuityLost) return;
|
|
267
|
+
|
|
268
|
+
this._logger?.error('DefaultServerTransport._handleChannelStateChange(); channel continuity lost', {
|
|
269
|
+
current,
|
|
270
|
+
resumed,
|
|
271
|
+
previous: stateChange.previous,
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
const err = new Ably.ErrorInfo(
|
|
275
|
+
`unable to deliver cancel messages; channel continuity lost (${current}${current === 'attached' ? ', resumed: false' : ''})`,
|
|
276
|
+
ErrorCode.ChannelContinuityLost,
|
|
277
|
+
500,
|
|
278
|
+
stateChange.reason,
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
// Transport-level notification only: continuity loss is not scoped to any
|
|
282
|
+
// turn. Per-turn onError handlers are reserved for errors from that turn's
|
|
283
|
+
// own operations (publish failures, encoder errors). Developers that need
|
|
284
|
+
// per-turn reaction can iterate active turns from the transport handler.
|
|
285
|
+
this._onError?.(err);
|
|
286
|
+
}
|
|
287
|
+
|
|
206
288
|
// -------------------------------------------------------------------------
|
|
207
289
|
// Channel subscription handler
|
|
208
290
|
// -------------------------------------------------------------------------
|
|
@@ -248,17 +330,23 @@ class DefaultServerTransport<TEvent, TMessage> implements ServerTransport<TEvent
|
|
|
248
330
|
onError: turnOnError,
|
|
249
331
|
parent: turnParent,
|
|
250
332
|
forkOf: turnForkOf,
|
|
333
|
+
signal: externalSignal,
|
|
251
334
|
} = turnOpts;
|
|
252
335
|
|
|
253
336
|
const controller = new AbortController();
|
|
254
|
-
let
|
|
255
|
-
|
|
337
|
+
let state = TurnState.INITIALIZED;
|
|
338
|
+
|
|
339
|
+
// Compose the internal controller signal with the external signal (e.g.
|
|
340
|
+
// req.signal) so platform-level cancellation (request cancellation, function
|
|
341
|
+
// timeout) aborts the turn through the same path as Ably cancel messages.
|
|
342
|
+
const signal = externalSignal ? AbortSignal.any([controller.signal, externalSignal]) : controller.signal;
|
|
256
343
|
|
|
257
|
-
//
|
|
344
|
+
// Spec: AIT-ST3a — register immediately so early cancels can fire the abort signal.
|
|
258
345
|
const registration: RegisteredTurn = {
|
|
259
346
|
turnId,
|
|
260
347
|
clientId: turnClientId ?? '',
|
|
261
348
|
controller,
|
|
349
|
+
signal,
|
|
262
350
|
onCancel,
|
|
263
351
|
onError: turnOnError,
|
|
264
352
|
};
|
|
@@ -278,25 +366,29 @@ class DefaultServerTransport<TEvent, TMessage> implements ServerTransport<TEvent
|
|
|
278
366
|
return turnId;
|
|
279
367
|
},
|
|
280
368
|
get abortSignal() {
|
|
281
|
-
return
|
|
369
|
+
return signal;
|
|
282
370
|
},
|
|
283
371
|
|
|
284
|
-
// Spec: AIT-ST4
|
|
372
|
+
// Spec: AIT-ST4, AIT-ST4a, AIT-ST4b
|
|
285
373
|
start: async (): Promise<void> => {
|
|
286
374
|
logger?.trace('Turn.start();', { turnId });
|
|
287
375
|
|
|
288
|
-
|
|
376
|
+
// Spec: AIT-ST4a
|
|
377
|
+
if (signal.aborted) {
|
|
289
378
|
throw new Ably.ErrorInfo(
|
|
290
379
|
`unable to start turn; turn ${turnId} was cancelled before start()`,
|
|
291
380
|
ErrorCode.InvalidArgument,
|
|
292
381
|
400,
|
|
293
382
|
);
|
|
294
383
|
}
|
|
295
|
-
if (
|
|
296
|
-
|
|
384
|
+
if (state !== TurnState.INITIALIZED) return;
|
|
385
|
+
state = TurnState.STARTED;
|
|
297
386
|
|
|
298
387
|
try {
|
|
299
|
-
await turnManager.startTurn(turnId, turnClientId, controller
|
|
388
|
+
await turnManager.startTurn(turnId, turnClientId, controller, {
|
|
389
|
+
parent: turnParent,
|
|
390
|
+
forkOf: turnForkOf,
|
|
391
|
+
});
|
|
300
392
|
} catch (error) {
|
|
301
393
|
const errInfo = new Ably.ErrorInfo(
|
|
302
394
|
`unable to publish turn-start for turn ${turnId}; ${error instanceof Error ? error.message : String(error)}`,
|
|
@@ -305,21 +397,17 @@ class DefaultServerTransport<TEvent, TMessage> implements ServerTransport<TEvent
|
|
|
305
397
|
error instanceof Ably.ErrorInfo ? error : undefined,
|
|
306
398
|
);
|
|
307
399
|
logger?.error('Turn.start(); failed to publish turn-start', { turnId });
|
|
308
|
-
turnOnError?.(errInfo);
|
|
309
400
|
throw errInfo;
|
|
310
401
|
}
|
|
311
402
|
|
|
312
403
|
logger?.debug('Turn.start(); turn started', { turnId });
|
|
313
404
|
},
|
|
314
405
|
|
|
315
|
-
// Spec: AIT-ST5
|
|
316
|
-
addMessages: async (
|
|
317
|
-
|
|
318
|
-
opts?: AddMessageOptions,
|
|
319
|
-
): Promise<AddMessagesResult> => {
|
|
320
|
-
logger?.trace('Turn.addMessages();', { turnId, count: inputs.length });
|
|
406
|
+
// Spec: AIT-ST5, AIT-ST5a, AIT-ST5b, AIT-ST5c
|
|
407
|
+
addMessages: async (nodes: MessageNode<TMessage>[], opts?: AddMessageOptions): Promise<AddMessagesResult> => {
|
|
408
|
+
logger?.trace('Turn.addMessages();', { turnId, count: nodes.length });
|
|
321
409
|
|
|
322
|
-
if (
|
|
410
|
+
if (state === TurnState.INITIALIZED) {
|
|
323
411
|
throw new Ably.ErrorInfo(
|
|
324
412
|
`unable to add messages; start() must be called before addMessages() (turn ${turnId})`,
|
|
325
413
|
ErrorCode.InvalidArgument,
|
|
@@ -330,48 +418,104 @@ class DefaultServerTransport<TEvent, TMessage> implements ServerTransport<TEvent
|
|
|
330
418
|
|
|
331
419
|
const msgIds: string[] = [];
|
|
332
420
|
|
|
333
|
-
|
|
334
|
-
const
|
|
421
|
+
try {
|
|
422
|
+
for (const node of nodes) {
|
|
423
|
+
// Build transport headers from the node's typed fields, then merge
|
|
424
|
+
// any extra headers from the node (e.g. domain-specific headers).
|
|
425
|
+
const headers = mergeHeaders(
|
|
426
|
+
buildTransportHeaders({
|
|
427
|
+
role: 'user',
|
|
428
|
+
turnId,
|
|
429
|
+
msgId: node.msgId,
|
|
430
|
+
turnClientId: opts?.clientId,
|
|
431
|
+
parent: node.parentId ?? turnParent,
|
|
432
|
+
forkOf: node.forkOf ?? turnForkOf,
|
|
433
|
+
}),
|
|
434
|
+
node.headers,
|
|
435
|
+
);
|
|
436
|
+
|
|
437
|
+
const encoder = codec.createEncoder(channel, {
|
|
438
|
+
extras: { headers },
|
|
439
|
+
onMessage,
|
|
440
|
+
});
|
|
335
441
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
parent: opts?.parent === undefined ? (turnParent ?? undefined) : (opts.parent ?? undefined),
|
|
347
|
-
forkOf: opts?.forkOf ?? turnForkOf,
|
|
348
|
-
}),
|
|
349
|
-
input.headers,
|
|
442
|
+
await encoder.writeMessages([node.message], opts?.clientId ? { clientId: opts.clientId } : undefined);
|
|
443
|
+
|
|
444
|
+
msgIds.push(node.msgId);
|
|
445
|
+
}
|
|
446
|
+
} catch (error) {
|
|
447
|
+
const errInfo = new Ably.ErrorInfo(
|
|
448
|
+
`unable to publish messages for turn ${turnId}; ${error instanceof Error ? error.message : String(error)}`,
|
|
449
|
+
ErrorCode.TurnLifecycleError,
|
|
450
|
+
500,
|
|
451
|
+
error instanceof Ably.ErrorInfo ? error : undefined,
|
|
350
452
|
);
|
|
453
|
+
logger?.error('Turn.addMessages(); publish failed', { turnId });
|
|
454
|
+
throw errInfo;
|
|
455
|
+
}
|
|
351
456
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
});
|
|
457
|
+
logger?.debug('Turn.addMessages(); messages published', { turnId, count: nodes.length });
|
|
458
|
+
return { msgIds };
|
|
459
|
+
},
|
|
356
460
|
|
|
357
|
-
|
|
461
|
+
// Spec: AIT-ST5c
|
|
462
|
+
addEvents: async (nodes: EventsNode<TEvent>[]): Promise<void> => {
|
|
463
|
+
logger?.trace('Turn.addEvents();', { turnId, count: nodes.length });
|
|
358
464
|
|
|
359
|
-
|
|
360
|
-
|
|
465
|
+
if (state === TurnState.INITIALIZED) {
|
|
466
|
+
throw new Ably.ErrorInfo(
|
|
467
|
+
`unable to add events; start() must be called before addEvents() (turn ${turnId})`,
|
|
468
|
+
ErrorCode.InvalidArgument,
|
|
469
|
+
400,
|
|
470
|
+
);
|
|
361
471
|
}
|
|
472
|
+
await attachPromise;
|
|
362
473
|
|
|
363
|
-
|
|
364
|
-
|
|
474
|
+
const turnOwnerClientId = turnManager.getClientId(turnId);
|
|
475
|
+
|
|
476
|
+
try {
|
|
477
|
+
for (const node of nodes) {
|
|
478
|
+
const headers = buildTransportHeaders({
|
|
479
|
+
role: 'assistant',
|
|
480
|
+
turnId,
|
|
481
|
+
msgId: node.msgId,
|
|
482
|
+
turnClientId: turnOwnerClientId,
|
|
483
|
+
amend: node.msgId,
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
const encoder = codec.createEncoder(channel, {
|
|
487
|
+
extras: { headers },
|
|
488
|
+
onMessage,
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
for (const event of node.events) {
|
|
492
|
+
await encoder.writeEvent(event);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
await encoder.close();
|
|
496
|
+
}
|
|
497
|
+
} catch (error) {
|
|
498
|
+
const errInfo = new Ably.ErrorInfo(
|
|
499
|
+
`unable to publish events for turn ${turnId}; ${error instanceof Error ? error.message : String(error)}`,
|
|
500
|
+
ErrorCode.TurnLifecycleError,
|
|
501
|
+
500,
|
|
502
|
+
error instanceof Ably.ErrorInfo ? error : undefined,
|
|
503
|
+
);
|
|
504
|
+
logger?.error('Turn.addEvents(); publish failed', { turnId });
|
|
505
|
+
throw errInfo;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
logger?.debug('Turn.addEvents(); events published', { turnId, count: nodes.length });
|
|
365
509
|
},
|
|
366
510
|
|
|
367
|
-
// Spec: AIT-ST6
|
|
511
|
+
// Spec: AIT-ST6, AIT-ST6a, AIT-ST6b, AIT-ST6b1, AIT-ST6b2, AIT-ST6b3, AIT-ST6b4, AIT-ST6c
|
|
368
512
|
streamResponse: async (
|
|
369
513
|
stream: ReadableStream<TEvent>,
|
|
370
|
-
streamOpts?: StreamResponseOptions
|
|
514
|
+
streamOpts?: StreamResponseOptions<TEvent>,
|
|
371
515
|
): Promise<StreamResult> => {
|
|
372
516
|
logger?.trace('Turn.streamResponse();', { turnId });
|
|
373
517
|
|
|
374
|
-
if (
|
|
518
|
+
if (state === TurnState.INITIALIZED) {
|
|
375
519
|
throw new Ably.ErrorInfo(
|
|
376
520
|
`unable to stream response; start() must be called before streamResponse() (turn ${turnId})`,
|
|
377
521
|
ErrorCode.InvalidArgument,
|
|
@@ -380,17 +524,16 @@ class DefaultServerTransport<TEvent, TMessage> implements ServerTransport<TEvent
|
|
|
380
524
|
}
|
|
381
525
|
await attachPromise;
|
|
382
526
|
|
|
383
|
-
const signal = turnManager.getSignal(turnId);
|
|
384
527
|
const turnOwnerClientId = turnManager.getClientId(turnId);
|
|
385
528
|
|
|
386
529
|
// Per-operation parent overrides the turn-level default.
|
|
387
|
-
const assistantParent =
|
|
388
|
-
streamOpts?.parent === undefined ? (turnParent ?? undefined) : (streamOpts.parent ?? undefined);
|
|
530
|
+
const assistantParent = streamOpts?.parent === undefined ? turnParent : streamOpts.parent;
|
|
389
531
|
|
|
532
|
+
const msgId = crypto.randomUUID();
|
|
390
533
|
const defaultHeaders = buildTransportHeaders({
|
|
391
534
|
role: 'assistant',
|
|
392
535
|
turnId,
|
|
393
|
-
msgId
|
|
536
|
+
msgId,
|
|
394
537
|
turnClientId: turnOwnerClientId,
|
|
395
538
|
parent: assistantParent,
|
|
396
539
|
forkOf: streamOpts?.forkOf ?? turnForkOf,
|
|
@@ -398,27 +541,39 @@ class DefaultServerTransport<TEvent, TMessage> implements ServerTransport<TEvent
|
|
|
398
541
|
const encoder = codec.createEncoder(channel, {
|
|
399
542
|
extras: { headers: defaultHeaders },
|
|
400
543
|
onMessage,
|
|
544
|
+
messageId: msgId,
|
|
401
545
|
});
|
|
402
546
|
|
|
403
|
-
const result = await pipeStream(stream, encoder, signal, onAbort, logger);
|
|
547
|
+
const result = await pipeStream(stream, encoder, signal, onAbort, streamOpts?.resolveWriteOptions, logger);
|
|
548
|
+
|
|
549
|
+
if (result.error) {
|
|
550
|
+
const errInfo = new Ably.ErrorInfo(
|
|
551
|
+
`unable to stream response for turn ${turnId}; ${result.error.message}`,
|
|
552
|
+
ErrorCode.StreamError,
|
|
553
|
+
500,
|
|
554
|
+
result.error instanceof Ably.ErrorInfo ? result.error : undefined,
|
|
555
|
+
);
|
|
556
|
+
logger?.error('Turn.streamResponse(); stream error', { turnId });
|
|
557
|
+
turnOnError?.(errInfo);
|
|
558
|
+
}
|
|
404
559
|
|
|
405
560
|
logger?.debug('Turn.streamResponse(); stream finished', { turnId, reason: result.reason });
|
|
406
561
|
return result;
|
|
407
562
|
},
|
|
408
563
|
|
|
409
|
-
// Spec: AIT-ST7
|
|
564
|
+
// Spec: AIT-ST7, AIT-ST7a, AIT-ST7b
|
|
410
565
|
end: async (reason: TurnEndReason): Promise<void> => {
|
|
411
566
|
logger?.trace('Turn.end();', { turnId, reason });
|
|
412
567
|
|
|
413
|
-
if (
|
|
568
|
+
if (state === TurnState.INITIALIZED) {
|
|
414
569
|
throw new Ably.ErrorInfo(
|
|
415
570
|
`unable to end turn; start() must be called before end() (turn ${turnId})`,
|
|
416
571
|
ErrorCode.InvalidArgument,
|
|
417
572
|
400,
|
|
418
573
|
);
|
|
419
574
|
}
|
|
420
|
-
if (
|
|
421
|
-
|
|
575
|
+
if (state === TurnState.ENDED) return;
|
|
576
|
+
state = TurnState.ENDED;
|
|
422
577
|
|
|
423
578
|
try {
|
|
424
579
|
await turnManager.endTurn(turnId, reason);
|
|
@@ -430,7 +585,6 @@ class DefaultServerTransport<TEvent, TMessage> implements ServerTransport<TEvent
|
|
|
430
585
|
error instanceof Ably.ErrorInfo ? error : undefined,
|
|
431
586
|
);
|
|
432
587
|
logger?.error('Turn.end(); failed to publish turn-end', { turnId });
|
|
433
|
-
turnOnError?.(errInfo);
|
|
434
588
|
throw errInfo;
|
|
435
589
|
} finally {
|
|
436
590
|
registeredTurns.delete(turnId);
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* Client-side stream routing.
|
|
3
3
|
*
|
|
4
4
|
* Maintains a map of turnId to ReadableStreamController. Routes decoded events
|
|
5
|
-
* to the correct stream. Closes streams on terminal events
|
|
5
|
+
* to the correct stream. Closes streams on terminal events, explicit close, or
|
|
6
|
+
* error.
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
import * as Ably from 'ably';
|
|
@@ -19,8 +20,10 @@ import type { TurnEntry } from './types.js';
|
|
|
19
20
|
export interface StreamRouter<TEvent> {
|
|
20
21
|
/** Register a new stream for a turnId. Returns the ReadableStream the consumer reads from. */
|
|
21
22
|
createStream(turnId: string): ReadableStream<TEvent>;
|
|
22
|
-
/** Close the stream for a turnId. Returns true if a stream
|
|
23
|
+
/** Close the stream for a turnId. Returns true if a stream existed. */
|
|
23
24
|
closeStream(turnId: string): boolean;
|
|
25
|
+
/** Error the stream for a turnId. The consumer's reader will reject with the given error. Returns true if a stream existed. */
|
|
26
|
+
errorStream(turnId: string, error: Ably.ErrorInfo): boolean;
|
|
24
27
|
/** Enqueue an event to the correct stream. Returns true if routed successfully. */
|
|
25
28
|
route(turnId: string, event: TEvent): boolean;
|
|
26
29
|
/** Whether a specific turnId has an active stream. */
|
|
@@ -73,7 +76,22 @@ class DefaultStreamRouter<TEvent> implements StreamRouter<TEvent> {
|
|
|
73
76
|
try {
|
|
74
77
|
turn.controller.close();
|
|
75
78
|
} catch {
|
|
76
|
-
/*
|
|
79
|
+
/* consumer cancelled the stream */
|
|
80
|
+
}
|
|
81
|
+
this._turns.delete(turnId);
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Spec: AIT-CT14c
|
|
86
|
+
errorStream(turnId: string, error: Ably.ErrorInfo): boolean {
|
|
87
|
+
const turn = this._turns.get(turnId);
|
|
88
|
+
if (!turn) return false;
|
|
89
|
+
|
|
90
|
+
this._logger.debug('StreamRouter.errorStream(); erroring stream', { turnId });
|
|
91
|
+
try {
|
|
92
|
+
turn.controller.error(error);
|
|
93
|
+
} catch {
|
|
94
|
+
/* consumer cancelled the stream */
|
|
77
95
|
}
|
|
78
96
|
this._turns.delete(turnId);
|
|
79
97
|
return true;
|