@ably/ai-transport 0.2.0 → 0.3.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 +10 -19
- package/dist/ably-ai-transport.js +1790 -1091
- 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 +2 -2
- package/dist/core/agent.d.ts +20 -5
- package/dist/core/channel-options.d.ts +57 -0
- package/dist/core/codec/codec-event.d.ts +9 -0
- package/dist/core/codec/decoder.d.ts +4 -1
- package/dist/core/codec/define-codec.d.ts +100 -0
- package/dist/core/codec/encoder.d.ts +2 -7
- package/dist/core/codec/field-bag.d.ts +85 -0
- package/dist/core/codec/fields.d.ts +141 -0
- package/dist/core/codec/index.d.ts +8 -1
- package/dist/core/codec/input-descriptor-decoder.d.ts +19 -0
- package/dist/core/codec/input-descriptor-encoder.d.ts +22 -0
- package/dist/core/codec/input-descriptors.d.ts +281 -0
- package/dist/core/codec/output-descriptor-decoder.d.ts +29 -0
- package/dist/core/codec/output-descriptor-encoder.d.ts +31 -0
- package/dist/core/codec/output-descriptors.d.ts +237 -0
- package/dist/core/codec/types.d.ts +95 -36
- package/dist/core/codec/well-known-inputs.d.ts +52 -0
- package/dist/core/transport/agent-view.d.ts +296 -0
- package/dist/core/transport/decode-fold.d.ts +40 -32
- package/dist/core/transport/headers.d.ts +30 -1
- package/dist/core/transport/index.d.ts +1 -1
- package/dist/core/transport/invocation.d.ts +1 -1
- package/dist/core/transport/load-history-pages.d.ts +71 -0
- package/dist/core/transport/load-history.d.ts +21 -16
- package/dist/core/transport/run-manager.d.ts +9 -11
- package/dist/core/transport/session-support.d.ts +55 -0
- package/dist/core/transport/tree.d.ts +165 -15
- package/dist/core/transport/types/agent.d.ts +120 -98
- package/dist/core/transport/types/client.d.ts +45 -12
- package/dist/core/transport/types/tree.d.ts +52 -10
- package/dist/core/transport/types/view.d.ts +55 -28
- package/dist/core/transport/view.d.ts +176 -58
- package/dist/core/transport/wire-log.d.ts +102 -0
- package/dist/errors.d.ts +10 -4
- package/dist/index.d.ts +6 -5
- package/dist/react/ably-ai-transport-react.js +784 -415
- 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 +2 -1
- package/dist/react/contexts/client-session-provider.d.ts +3 -0
- package/dist/react/index.d.ts +2 -1
- package/dist/react/internal/skipped-session.d.ts +8 -0
- package/dist/react/use-view.d.ts +3 -3
- package/dist/utils.d.ts +22 -54
- package/dist/vercel/ably-ai-transport-vercel.js +2297 -2026
- 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/decode-lifecycle.d.ts +9 -0
- package/dist/vercel/codec/events.d.ts +1 -2
- package/dist/vercel/codec/fields.d.ts +44 -0
- package/dist/vercel/codec/fold-content.d.ts +16 -0
- package/dist/vercel/codec/fold-data.d.ts +16 -0
- package/dist/vercel/codec/fold-input.d.ts +67 -0
- package/dist/vercel/codec/fold-lifecycle.d.ts +16 -0
- package/dist/vercel/codec/fold-text.d.ts +16 -0
- package/dist/vercel/codec/fold-tool-input.d.ts +17 -0
- package/dist/vercel/codec/fold-tool-output.d.ts +16 -0
- package/dist/vercel/codec/index.d.ts +5 -30
- package/dist/vercel/codec/inputs.d.ts +11 -0
- package/dist/vercel/codec/outputs.d.ts +11 -0
- package/dist/vercel/codec/reducer-state.d.ts +121 -0
- package/dist/vercel/codec/reducer.d.ts +20 -102
- package/dist/vercel/codec/tool-transitions.d.ts +0 -6
- package/dist/vercel/codec/wire-data.d.ts +34 -0
- package/dist/vercel/index.d.ts +1 -0
- package/dist/vercel/react/ably-ai-transport-vercel-react.js +2013 -9500
- package/dist/vercel/react/ably-ai-transport-vercel-react.js.map +1 -1
- package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs +1 -70
- package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs.map +1 -1
- package/dist/vercel/react/contexts/chat-transport-context.d.ts +2 -1
- package/dist/vercel/run-end-reason.d.ts +66 -11
- package/dist/vercel/tool-part.d.ts +21 -0
- package/dist/vercel/transport/chat-transport.d.ts +0 -2
- package/dist/vercel/transport/index.d.ts +1 -1
- package/dist/vercel/transport/run-output-stream.d.ts +6 -8
- package/dist/version.d.ts +1 -1
- package/package.json +2 -2
- package/src/constants.ts +2 -2
- package/src/core/agent.ts +43 -19
- package/src/core/channel-options.ts +89 -0
- package/src/core/codec/codec-event.ts +27 -0
- package/src/core/codec/decoder.ts +145 -21
- package/src/core/codec/define-codec.ts +432 -0
- package/src/core/codec/encoder.ts +13 -54
- package/src/core/codec/field-bag.ts +142 -0
- package/src/core/codec/fields.ts +193 -0
- package/src/core/codec/index.ts +43 -0
- package/src/core/codec/input-descriptor-decoder.ts +97 -0
- package/src/core/codec/input-descriptor-encoder.ts +150 -0
- package/src/core/codec/input-descriptors.ts +373 -0
- package/src/core/codec/output-descriptor-decoder.ts +139 -0
- package/src/core/codec/output-descriptor-encoder.ts +101 -0
- package/src/core/codec/output-descriptors.ts +307 -0
- package/src/core/codec/types.ts +99 -36
- package/src/core/codec/well-known-inputs.ts +96 -0
- package/src/core/transport/agent-session.ts +330 -589
- package/src/core/transport/agent-view.ts +738 -0
- package/src/core/transport/client-session.ts +74 -69
- package/src/core/transport/decode-fold.ts +57 -47
- package/src/core/transport/headers.ts +57 -4
- package/src/core/transport/index.ts +2 -1
- package/src/core/transport/invocation.ts +1 -1
- package/src/core/transport/load-history-pages.ts +220 -0
- package/src/core/transport/load-history.ts +63 -61
- package/src/core/transport/pipe-stream.ts +10 -1
- package/src/core/transport/run-manager.ts +25 -31
- package/src/core/transport/session-support.ts +96 -0
- package/src/core/transport/tree.ts +414 -47
- package/src/core/transport/types/agent.ts +129 -102
- package/src/core/transport/types/client.ts +49 -13
- package/src/core/transport/types/tree.ts +61 -12
- package/src/core/transport/types/view.ts +57 -28
- package/src/core/transport/view.ts +520 -172
- package/src/core/transport/wire-log.ts +189 -0
- package/src/errors.ts +10 -3
- package/src/index.ts +44 -11
- package/src/react/contexts/client-session-context.ts +1 -1
- package/src/react/contexts/client-session-provider.tsx +38 -2
- package/src/react/index.ts +2 -1
- package/src/react/internal/skipped-session.ts +62 -0
- package/src/react/use-client-session.ts +7 -30
- package/src/react/use-view.ts +3 -3
- package/src/utils.ts +31 -97
- package/src/vercel/codec/decode-lifecycle.ts +70 -0
- package/src/vercel/codec/events.ts +1 -3
- package/src/vercel/codec/fields.ts +58 -0
- package/src/vercel/codec/fold-content.ts +54 -0
- package/src/vercel/codec/fold-data.ts +46 -0
- package/src/vercel/codec/fold-input.ts +255 -0
- package/src/vercel/codec/fold-lifecycle.ts +85 -0
- package/src/vercel/codec/fold-text.ts +55 -0
- package/src/vercel/codec/fold-tool-input.ts +86 -0
- package/src/vercel/codec/fold-tool-output.ts +79 -0
- package/src/vercel/codec/index.ts +23 -63
- package/src/vercel/codec/inputs.ts +116 -0
- package/src/vercel/codec/outputs.ts +207 -0
- package/src/vercel/codec/reducer-state.ts +169 -0
- package/src/vercel/codec/reducer.ts +52 -838
- package/src/vercel/codec/tool-transitions.ts +1 -12
- package/src/vercel/codec/wire-data.ts +64 -0
- package/src/vercel/index.ts +1 -0
- package/src/vercel/react/contexts/chat-transport-context.ts +1 -1
- package/src/vercel/react/use-chat-transport.ts +8 -28
- package/src/vercel/react/use-message-sync.ts +5 -10
- package/src/vercel/run-end-reason.ts +95 -16
- package/src/vercel/tool-part.ts +25 -0
- package/src/vercel/transport/chat-transport.ts +10 -22
- package/src/vercel/transport/index.ts +1 -1
- package/src/vercel/transport/run-output-stream.ts +7 -8
- package/src/version.ts +1 -1
- package/dist/core/transport/branch-chain.d.ts +0 -43
- package/dist/core/transport/load-conversation.d.ts +0 -128
- package/dist/vercel/codec/decoder.d.ts +0 -9
- package/dist/vercel/codec/encoder.d.ts +0 -11
- package/src/core/transport/branch-chain.ts +0 -58
- package/src/core/transport/load-conversation.ts +0 -355
- package/src/vercel/codec/decoder.ts +0 -696
- package/src/vercel/codec/encoder.ts +0 -548
|
@@ -17,13 +17,13 @@
|
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
import * as Ably from 'ably';
|
|
20
|
+
// Also augments RealtimeChannel with `.object` (ably/liveobjects side-effect).
|
|
21
|
+
import type * as AblyObjects from 'ably/liveobjects';
|
|
20
22
|
|
|
21
23
|
import {
|
|
22
24
|
EVENT_CANCEL,
|
|
23
25
|
EVENT_RUN_END,
|
|
24
26
|
HEADER_CODEC_MESSAGE_ID,
|
|
25
|
-
HEADER_ERROR_CODE,
|
|
26
|
-
HEADER_ERROR_MESSAGE,
|
|
27
27
|
HEADER_EVENT_ID,
|
|
28
28
|
HEADER_INPUT_CODEC_MESSAGE_ID,
|
|
29
29
|
HEADER_INVOCATION_ID,
|
|
@@ -36,12 +36,14 @@ import { ErrorCode } from '../../errors.js';
|
|
|
36
36
|
import { EventEmitter } from '../../event-emitter.js';
|
|
37
37
|
import type { Logger } from '../../logger.js';
|
|
38
38
|
import { LogLevel, makeLogger } from '../../logger.js';
|
|
39
|
-
import { getTransportHeaders } from '../../utils.js';
|
|
39
|
+
import { errorCause, errorMessage, getTransportHeaders } from '../../utils.js';
|
|
40
40
|
import { registerAgent } from '../agent.js';
|
|
41
|
-
import
|
|
42
|
-
import {
|
|
43
|
-
import {
|
|
41
|
+
import { resolveChannelModes } from '../channel-options.js';
|
|
42
|
+
import type { Codec, CodecInputEvent, CodecOutputEvent, Encoder } from '../codec/types.js';
|
|
43
|
+
import { createWireApplier, type WireApplier } from './decode-fold.js';
|
|
44
|
+
import { buildRunEndError, buildTransportHeaders } from './headers.js';
|
|
44
45
|
import { Invocation } from './invocation.js';
|
|
46
|
+
import { bestEffortDetach, continuityLostError, isContinuityLost, requireConnected } from './session-support.js';
|
|
45
47
|
import type { DefaultTree } from './tree.js';
|
|
46
48
|
import { createTree } from './tree.js';
|
|
47
49
|
import type { ActiveRun, ClientSession, ClientSessionOptions, RunEndReason, SendOptions, Tree, View } from './types.js';
|
|
@@ -83,8 +85,8 @@ class DefaultClientSession<
|
|
|
83
85
|
TMessage,
|
|
84
86
|
> implements ClientSession<TInput, TOutput, TProjection, TMessage> {
|
|
85
87
|
private readonly _channel: Ably.RealtimeChannel;
|
|
86
|
-
private readonly
|
|
87
|
-
private readonly
|
|
88
|
+
private readonly _client: Ably.Realtime;
|
|
89
|
+
private readonly _codec: Codec<TInput, TOutput, TProjection, TMessage>;
|
|
88
90
|
private readonly _logger: Logger;
|
|
89
91
|
|
|
90
92
|
// Typed event emitter — the session emits only 'error'; all data events live on Tree/View
|
|
@@ -94,7 +96,13 @@ class DefaultClientSession<
|
|
|
94
96
|
private readonly _tree: DefaultTree<TInput, TOutput, TProjection>;
|
|
95
97
|
private readonly _view: DefaultView<TInput, TOutput, TProjection, TMessage>;
|
|
96
98
|
private readonly _views = new Set<DefaultView<TInput, TOutput, TProjection, TMessage>>();
|
|
97
|
-
|
|
99
|
+
/**
|
|
100
|
+
* The Tree's single decode-and-apply engine, binding the session's one
|
|
101
|
+
* decoder instance. Shared by the live decode loop and every View's history
|
|
102
|
+
* replay so an attach-boundary in-flight stream is continued (not
|
|
103
|
+
* re-started) by hydration, and re-delivered content decodes to nothing.
|
|
104
|
+
*/
|
|
105
|
+
private readonly _applier: WireApplier;
|
|
98
106
|
/**
|
|
99
107
|
* Shared encoder for the lifetime of the session. The client only ever
|
|
100
108
|
* uses `publishInput` (input wire), so the encoder's stream tracker map
|
|
@@ -137,10 +145,13 @@ class DefaultClientSession<
|
|
|
137
145
|
// Spec: AIT-CT1a, AIT-CT1a2 — register this SDK on both the connection
|
|
138
146
|
// (options.agents) and channel-attach (params.agent) paths. Idempotent
|
|
139
147
|
// across sessions sharing one client.
|
|
140
|
-
const channelOptions = registerAgent(options.client, options.codec);
|
|
148
|
+
const channelOptions: Ably.ChannelOptions = registerAgent(options.client, options.codec);
|
|
149
|
+
// Spec: AIT-CT23 — request object modes etc. when channelModes opts in.
|
|
150
|
+
const modes = resolveChannelModes(options.channelModes);
|
|
151
|
+
if (modes) channelOptions.modes = modes;
|
|
141
152
|
this._channel = options.client.channels.get(options.channelName, channelOptions);
|
|
153
|
+
this._client = options.client;
|
|
142
154
|
this._codec = options.codec;
|
|
143
|
-
this._clientId = options.clientId;
|
|
144
155
|
this._logger = (options.logger ?? makeLogger({ logLevel: LogLevel.Silent })).withContext({
|
|
145
156
|
component: 'ClientSession',
|
|
146
157
|
});
|
|
@@ -150,19 +161,17 @@ class DefaultClientSession<
|
|
|
150
161
|
|
|
151
162
|
// Compose sub-components
|
|
152
163
|
this._tree = createTree<TInput, TOutput, TProjection>(this._codec, this._logger);
|
|
164
|
+
this._applier = createWireApplier(this._tree, this._codec.createDecoder());
|
|
153
165
|
this._view = createView<TInput, TOutput, TProjection, TMessage>({
|
|
154
166
|
tree: this._tree,
|
|
155
167
|
channel: this._channel,
|
|
156
168
|
codec: this._codec,
|
|
169
|
+
applier: this._applier,
|
|
157
170
|
sendDelegate: this._internalSend.bind(this),
|
|
158
171
|
logger: this._logger,
|
|
159
172
|
onClose: () => this._views.delete(this._view),
|
|
160
173
|
});
|
|
161
|
-
this.
|
|
162
|
-
this._encoder = this._codec.createEncoder(
|
|
163
|
-
this._channel,
|
|
164
|
-
this._clientId === undefined ? undefined : { clientId: this._clientId },
|
|
165
|
-
);
|
|
174
|
+
this._encoder = this._codec.createEncoder(this._channel);
|
|
166
175
|
|
|
167
176
|
this._views.add(this._view);
|
|
168
177
|
|
|
@@ -204,6 +213,20 @@ class DefaultClientSession<
|
|
|
204
213
|
this._channel.on(this._onChannelStateChange);
|
|
205
214
|
}
|
|
206
215
|
|
|
216
|
+
// ---------------------------------------------------------------------------
|
|
217
|
+
// Public accessors
|
|
218
|
+
// ---------------------------------------------------------------------------
|
|
219
|
+
|
|
220
|
+
// Spec: AIT-CT21
|
|
221
|
+
get presence(): Ably.RealtimePresence {
|
|
222
|
+
return this._channel.presence;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Spec: AIT-CT22
|
|
226
|
+
get object(): AblyObjects.RealtimeObject {
|
|
227
|
+
return this._channel.object;
|
|
228
|
+
}
|
|
229
|
+
|
|
207
230
|
// ---------------------------------------------------------------------------
|
|
208
231
|
// Public connection API
|
|
209
232
|
// ---------------------------------------------------------------------------
|
|
@@ -224,10 +247,10 @@ class DefaultClientSession<
|
|
|
224
247
|
},
|
|
225
248
|
(error: unknown) => {
|
|
226
249
|
const errInfo = new Ably.ErrorInfo(
|
|
227
|
-
`unable to subscribe to channel; ${
|
|
250
|
+
`unable to subscribe to channel; ${errorMessage(error)}`,
|
|
228
251
|
ErrorCode.SessionSubscriptionError,
|
|
229
252
|
500,
|
|
230
|
-
error
|
|
253
|
+
errorCause(error),
|
|
231
254
|
);
|
|
232
255
|
this._logger.error('DefaultClientSession.connect(); subscribe failed');
|
|
233
256
|
this._emitter.emit('error', errInfo);
|
|
@@ -237,15 +260,24 @@ class DefaultClientSession<
|
|
|
237
260
|
return this._connectPromise;
|
|
238
261
|
}
|
|
239
262
|
|
|
263
|
+
/**
|
|
264
|
+
* The session's identity, read from the Ably client's `auth.clientId`. Read
|
|
265
|
+
* lazily (never cached at construction): under token auth the client only
|
|
266
|
+
* learns its clientId once the connection reaches CONNECTED, which is
|
|
267
|
+
* guaranteed by the time any write runs — every write awaits `connect()`,
|
|
268
|
+
* and the channel cannot attach before the connection is CONNECTED. A
|
|
269
|
+
* connection with no concrete identity (anonymous, or a wildcard `*` token)
|
|
270
|
+
* resolves to `undefined`, so no run/input client id is stamped.
|
|
271
|
+
* @returns The client's concrete identity, or `undefined` if it has none.
|
|
272
|
+
*/
|
|
273
|
+
// Spec: AIT-CT1b
|
|
274
|
+
private _resolveClientId(): string | undefined {
|
|
275
|
+
const clientId = this._client.auth.clientId;
|
|
276
|
+
return clientId && clientId !== '*' ? clientId : undefined;
|
|
277
|
+
}
|
|
278
|
+
|
|
240
279
|
private async _requireConnected(method: string): Promise<void> {
|
|
241
|
-
|
|
242
|
-
throw new Ably.ErrorInfo(
|
|
243
|
-
`unable to ${method}; connect() must be called before ${method}()`,
|
|
244
|
-
ErrorCode.InvalidArgument,
|
|
245
|
-
400,
|
|
246
|
-
);
|
|
247
|
-
}
|
|
248
|
-
return this._connectPromise;
|
|
280
|
+
return requireConnected(this._connectPromise, method);
|
|
249
281
|
}
|
|
250
282
|
|
|
251
283
|
// ---------------------------------------------------------------------------
|
|
@@ -267,24 +299,21 @@ class DefaultClientSession<
|
|
|
267
299
|
// CAST: agent always writes a valid RunEndReason; default to 'complete' for robustness
|
|
268
300
|
const reason = (headers[HEADER_RUN_REASON] ?? 'complete') as RunEndReason;
|
|
269
301
|
if (reason === 'error') {
|
|
270
|
-
const
|
|
271
|
-
const parsedCode = codeRaw === undefined ? Number.NaN : Number(codeRaw);
|
|
272
|
-
const code = Number.isFinite(parsedCode) ? parsedCode : ErrorCode.SessionSubscriptionError;
|
|
273
|
-
const message = headers[HEADER_ERROR_MESSAGE] ?? 'agent reported an error';
|
|
274
|
-
const statusCode = code >= 10000 && code < 60000 ? Math.floor(code / 100) : 500;
|
|
275
|
-
const errInfo = new Ably.ErrorInfo(message, code, statusCode);
|
|
302
|
+
const errInfo = buildRunEndError(headers);
|
|
276
303
|
this._logger.error('ClientSession._handleMessage(); agent error received', {
|
|
277
304
|
runId: headers[HEADER_RUN_ID],
|
|
278
305
|
invocationId: headers[HEADER_INVOCATION_ID],
|
|
279
|
-
code,
|
|
306
|
+
code: errInfo.code,
|
|
280
307
|
});
|
|
281
308
|
this._emitter.emit('error', errInfo);
|
|
282
309
|
}
|
|
283
310
|
}
|
|
284
311
|
|
|
285
|
-
// Reconstruct the tree via the
|
|
286
|
-
// the
|
|
287
|
-
|
|
312
|
+
// Reconstruct the tree via the Tree's single decode-and-apply engine —
|
|
313
|
+
// the same applier (and decoder instance) the Views' history replay
|
|
314
|
+
// uses, so the live loop can't drift from it and an attach-boundary
|
|
315
|
+
// stream isn't double-decoded.
|
|
316
|
+
const event = this._applier.apply(ablyMessage);
|
|
288
317
|
|
|
289
318
|
// Live-only: resolve the pending `runId` promise on a fresh run-start or
|
|
290
319
|
// a continuation run-resume. Key by the echoed `input-codec-message-id`
|
|
@@ -309,14 +338,13 @@ class DefaultClientSession<
|
|
|
309
338
|
// 'update' events the apply triggers.
|
|
310
339
|
this._tree.emitAblyMessage(ablyMessage);
|
|
311
340
|
} catch (error) {
|
|
312
|
-
const cause = error instanceof Ably.ErrorInfo ? error : undefined;
|
|
313
341
|
this._emitter.emit(
|
|
314
342
|
'error',
|
|
315
343
|
new Ably.ErrorInfo(
|
|
316
|
-
`unable to process channel message; ${
|
|
344
|
+
`unable to process channel message; ${errorMessage(error)}`,
|
|
317
345
|
ErrorCode.SessionSubscriptionError,
|
|
318
346
|
500,
|
|
319
|
-
|
|
347
|
+
errorCause(error),
|
|
320
348
|
),
|
|
321
349
|
);
|
|
322
350
|
}
|
|
@@ -338,13 +366,7 @@ class DefaultClientSession<
|
|
|
338
366
|
return;
|
|
339
367
|
}
|
|
340
368
|
|
|
341
|
-
|
|
342
|
-
// - FAILED, SUSPENDED, DETACHED: no more messages expected (or gap)
|
|
343
|
-
// - ATTACHED with resumed: false (UPDATE): messages were lost
|
|
344
|
-
const continuityLost =
|
|
345
|
-
current === 'failed' || current === 'suspended' || current === 'detached' || (current === 'attached' && !resumed);
|
|
346
|
-
|
|
347
|
-
if (!continuityLost) return;
|
|
369
|
+
if (!isContinuityLost(stateChange)) return;
|
|
348
370
|
|
|
349
371
|
this._logger.error('ClientSession._handleChannelStateChange(); channel continuity lost', {
|
|
350
372
|
current,
|
|
@@ -352,12 +374,7 @@ class DefaultClientSession<
|
|
|
352
374
|
previous: stateChange.previous,
|
|
353
375
|
});
|
|
354
376
|
|
|
355
|
-
const err =
|
|
356
|
-
`unable to deliver events; channel continuity lost (${current}${current === 'attached' ? ', resumed: false' : ''})`,
|
|
357
|
-
ErrorCode.ChannelContinuityLost,
|
|
358
|
-
500,
|
|
359
|
-
stateChange.reason,
|
|
360
|
-
);
|
|
377
|
+
const err = continuityLostError(stateChange, 'deliver events');
|
|
361
378
|
|
|
362
379
|
// Surface the loss via the session `error` event. Consumers that expose a
|
|
363
380
|
// per-run stream (e.g. the Vercel ChatTransport) error their stream off
|
|
@@ -406,6 +423,7 @@ class DefaultClientSession<
|
|
|
406
423
|
tree: this._tree,
|
|
407
424
|
channel: this._channel,
|
|
408
425
|
codec: this._codec,
|
|
426
|
+
applier: this._applier,
|
|
409
427
|
sendDelegate: this._internalSend.bind(this),
|
|
410
428
|
logger: this._logger,
|
|
411
429
|
onClose: () => this._views.delete(view),
|
|
@@ -506,7 +524,7 @@ class DefaultClientSession<
|
|
|
506
524
|
role: 'user',
|
|
507
525
|
runId,
|
|
508
526
|
codecMessageId,
|
|
509
|
-
runClientId: this.
|
|
527
|
+
runClientId: this._resolveClientId(),
|
|
510
528
|
...(parent !== undefined && { parent }),
|
|
511
529
|
...(forkOf !== undefined && { forkOf }),
|
|
512
530
|
...(regenerates !== undefined && { regenerates }),
|
|
@@ -574,16 +592,15 @@ class DefaultClientSession<
|
|
|
574
592
|
await this._encoder.publishInput(item.input, {
|
|
575
593
|
extras: { headers: item.headers },
|
|
576
594
|
messageId: item.codecMessageId,
|
|
577
|
-
...(this._clientId !== undefined && { clientId: this._clientId }),
|
|
578
595
|
});
|
|
579
596
|
}
|
|
580
597
|
} catch (error) {
|
|
581
|
-
const cause = error
|
|
598
|
+
const cause = errorCause(error);
|
|
582
599
|
const isPermission = cause?.statusCode === 401 || cause?.statusCode === 403;
|
|
583
600
|
const err = new Ably.ErrorInfo(
|
|
584
601
|
isPermission
|
|
585
602
|
? `unable to publish events; missing publish capability on the channel`
|
|
586
|
-
: `unable to publish events; ${
|
|
603
|
+
: `unable to publish events; ${errorMessage(error)}`,
|
|
587
604
|
isPermission ? ErrorCode.InsufficientCapability : ErrorCode.SessionSendFailed,
|
|
588
605
|
isPermission ? 401 : 500,
|
|
589
606
|
cause,
|
|
@@ -735,19 +752,7 @@ class DefaultClientSession<
|
|
|
735
752
|
// Swallow: encoder close is best-effort during teardown
|
|
736
753
|
}
|
|
737
754
|
|
|
738
|
-
|
|
739
|
-
// implicitly attaches), so we only detach when connect() ran. Best-effort:
|
|
740
|
-
// a detach failure (e.g. the channel is already FAILED) must not throw out
|
|
741
|
-
// of close().
|
|
742
|
-
if (this._connectPromise) {
|
|
743
|
-
try {
|
|
744
|
-
await this._channel.detach();
|
|
745
|
-
} catch (error) {
|
|
746
|
-
// Swallowed (see above): a detach failure must not throw out of
|
|
747
|
-
// close(). Logged at debug for observability.
|
|
748
|
-
this._logger.debug('ClientSession.close(); channel detach failed', { error });
|
|
749
|
-
}
|
|
750
|
-
}
|
|
755
|
+
await bestEffortDetach(this._channel, this._connectPromise, this._logger, 'ClientSession');
|
|
751
756
|
}
|
|
752
757
|
}
|
|
753
758
|
|
|
@@ -5,87 +5,97 @@
|
|
|
5
5
|
* the conversation Tree from the same raw Ably wire log. This module is the one
|
|
6
6
|
* place that classifies a wire message (run-lifecycle vs codec-decoded), parses
|
|
7
7
|
* or decodes it, and applies it to the Tree — so the two paths can never drift.
|
|
8
|
+
*
|
|
9
|
+
* The engine is exposed as a {@link WireApplier} binding one Tree to one
|
|
10
|
+
* decoder. A Tree has exactly one applier (the session constructs it and hands
|
|
11
|
+
* it to every View), so every route a wire message can arrive by — the live
|
|
12
|
+
* subscription, View history pagination, the agent's hydration walks — feeds
|
|
13
|
+
* the same decoder. The decoder's version-guarded stream trackers then make
|
|
14
|
+
* re-delivery across routes (an attach-boundary in-flight stream, a replayed
|
|
15
|
+
* history page) decode to nothing instead of double-folding. The delivery's
|
|
16
|
+
* `version.serial` is also threaded into the Tree, whose per-entry
|
|
17
|
+
* `decodedThrough` high-water-mark drops whole-wire replays that no decoder
|
|
18
|
+
* state can see (stateless discrete re-decodes).
|
|
8
19
|
*/
|
|
9
20
|
|
|
10
21
|
import type * as Ably from 'ably';
|
|
11
22
|
|
|
12
23
|
import { HEADER_RUN_ID } from '../../constants.js';
|
|
13
24
|
import { getTransportHeaders } from '../../utils.js';
|
|
14
|
-
import type {
|
|
25
|
+
import type { CodecInputEvent, CodecOutputEvent, Decoder } from '../codec/types.js';
|
|
15
26
|
import { isRunLifecycleName, parseRunLifecycle } from './headers.js';
|
|
16
27
|
import type { TreeInternal } from './tree.js';
|
|
17
28
|
import type { RunLifecycleEvent } from './types.js';
|
|
18
29
|
|
|
19
30
|
/**
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
* The decode-and-apply engine for one Tree: a single codec decoder bound to a
|
|
32
|
+
* single Tree, shared by every route that feeds the Tree wire messages.
|
|
33
|
+
*/
|
|
34
|
+
export interface WireApplier {
|
|
35
|
+
/**
|
|
36
|
+
* Apply one inbound wire message to the bound tree.
|
|
37
|
+
*
|
|
38
|
+
* Run-lifecycle messages are turned into a {@link RunLifecycleEvent} via
|
|
39
|
+
* {@link parseRunLifecycle} and applied with `applyRunLifecycle`; everything
|
|
40
|
+
* else is decoded with the bound decoder and applied with `applyMessage`,
|
|
41
|
+
* skipping wire-only carriers that decode to no events and carry no run-id
|
|
42
|
+
* (the eventual reply run is created later by its run-start).
|
|
43
|
+
*
|
|
44
|
+
* Does NOT emit the tree's `ably-message` event — the caller owns that,
|
|
45
|
+
* because the live loop emits per message while history replay emits in a
|
|
46
|
+
* batch once the whole page is applied. Returns the parsed lifecycle event
|
|
47
|
+
* so a live caller can run its own side-effects (resolving a pending
|
|
48
|
+
* run-start, surfacing an agent error); returns `undefined` for a
|
|
49
|
+
* codec-decoded message or a lifecycle message that carried no run-id.
|
|
50
|
+
* @param rawMsg - The inbound Ably wire message.
|
|
51
|
+
* @returns The parsed run-lifecycle event, or `undefined`.
|
|
52
|
+
*/
|
|
53
|
+
apply(rawMsg: Ably.InboundMessage): RunLifecycleEvent | undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Classify, decode, and apply one inbound wire message to the tree. See
|
|
58
|
+
* {@link WireApplier.apply} for the contract.
|
|
34
59
|
* @param tree - The tree to apply the message to.
|
|
35
60
|
* @param decoder - The codec decoder used for non-lifecycle messages.
|
|
36
61
|
* @param rawMsg - The inbound Ably wire message.
|
|
37
62
|
* @returns The parsed run-lifecycle event, or `undefined`.
|
|
38
63
|
*/
|
|
39
|
-
|
|
64
|
+
const applyWireMessage = <TInput extends CodecInputEvent, TOutput extends CodecOutputEvent, TProjection>(
|
|
40
65
|
tree: TreeInternal<TInput, TOutput, TProjection>,
|
|
41
66
|
decoder: Decoder<TInput, TOutput>,
|
|
42
67
|
rawMsg: Ably.InboundMessage,
|
|
43
68
|
): RunLifecycleEvent | undefined => {
|
|
44
69
|
const headers = getTransportHeaders(rawMsg);
|
|
45
70
|
const serial = rawMsg.serial;
|
|
71
|
+
// Top-level timestamp — the message's create time on every delivery (an
|
|
72
|
+
// append's own receive time lives in `version.timestamp`). The retention
|
|
73
|
+
// clock is sound on this timeline because run-end, a fresh create published
|
|
74
|
+
// after every wire of its run, bounds the node's last activity.
|
|
75
|
+
const timestamp = rawMsg.timestamp;
|
|
46
76
|
|
|
47
77
|
if (isRunLifecycleName(rawMsg.name)) {
|
|
48
|
-
const event = parseRunLifecycle(rawMsg.name, headers, serial);
|
|
78
|
+
const event = parseRunLifecycle(rawMsg.name, headers, serial, timestamp);
|
|
49
79
|
if (event) tree.applyRunLifecycle(event);
|
|
50
80
|
return event;
|
|
51
81
|
}
|
|
52
82
|
|
|
53
83
|
const { inputs, outputs } = decoder.decode(rawMsg);
|
|
54
84
|
if (inputs.length > 0 || outputs.length > 0 || headers[HEADER_RUN_ID]) {
|
|
55
|
-
tree.applyMessage({ inputs, outputs }, headers, serial);
|
|
85
|
+
tree.applyMessage({ inputs, outputs }, headers, serial, timestamp, rawMsg.version.serial);
|
|
56
86
|
}
|
|
57
87
|
return undefined;
|
|
58
88
|
};
|
|
59
89
|
|
|
60
90
|
/**
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
* state can span the messages of a run, and chooses the reducer routing key.
|
|
66
|
-
* @param codec - The codec whose inherited Reducer `fold` method folds each decoded event into the projection.
|
|
67
|
-
* @param decoder - The caller-owned codec decoder (reused across a run's wires).
|
|
68
|
-
* @param projection - The projection to fold the message's events into.
|
|
69
|
-
* @param rawMsg - The wire message to decode and fold.
|
|
70
|
-
* @param messageId - The reducer routing key (codec-message-id) for this message.
|
|
71
|
-
* @returns The projection after folding all of the message's decoded events.
|
|
91
|
+
* Bind a Tree and a decoder into the Tree's single {@link WireApplier}.
|
|
92
|
+
* @param tree - The tree the applier feeds.
|
|
93
|
+
* @param decoder - The codec decoder shared by every route into the tree.
|
|
94
|
+
* @returns The applier.
|
|
72
95
|
*/
|
|
73
|
-
export const
|
|
74
|
-
TInput
|
|
75
|
-
TOutput extends CodecOutputEvent,
|
|
76
|
-
TProjection,
|
|
77
|
-
TMessage,
|
|
78
|
-
>(
|
|
79
|
-
codec: Codec<TInput, TOutput, TProjection, TMessage>,
|
|
96
|
+
export const createWireApplier = <TInput extends CodecInputEvent, TOutput extends CodecOutputEvent, TProjection>(
|
|
97
|
+
tree: TreeInternal<TInput, TOutput, TProjection>,
|
|
80
98
|
decoder: Decoder<TInput, TOutput>,
|
|
81
|
-
|
|
82
|
-
rawMsg: Ably.InboundMessage,
|
|
83
|
-
|
|
84
|
-
): TProjection => {
|
|
85
|
-
const { inputs, outputs } = decoder.decode(rawMsg);
|
|
86
|
-
let next = projection;
|
|
87
|
-
for (const event of [...inputs, ...outputs]) {
|
|
88
|
-
next = codec.fold(next, event, { serial: rawMsg.serial ?? '', messageId });
|
|
89
|
-
}
|
|
90
|
-
return next;
|
|
91
|
-
};
|
|
99
|
+
): WireApplier => ({
|
|
100
|
+
apply: (rawMsg: Ably.InboundMessage): RunLifecycleEvent | undefined => applyWireMessage(tree, decoder, rawMsg),
|
|
101
|
+
});
|
|
@@ -2,16 +2,20 @@
|
|
|
2
2
|
* Transport header builder.
|
|
3
3
|
*
|
|
4
4
|
* Single source of truth for which transport headers every transport
|
|
5
|
-
* message carries. Used by the agent session (pipe
|
|
5
|
+
* message carries. Used by the agent session (pipe) and by
|
|
6
6
|
* the client session (optimistic message stamping).
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import * as Ably from 'ably';
|
|
10
|
+
|
|
9
11
|
import {
|
|
10
12
|
EVENT_RUN_END,
|
|
11
13
|
EVENT_RUN_RESUME,
|
|
12
14
|
EVENT_RUN_START,
|
|
13
15
|
EVENT_RUN_SUSPEND,
|
|
14
16
|
HEADER_CODEC_MESSAGE_ID,
|
|
17
|
+
HEADER_ERROR_CODE,
|
|
18
|
+
HEADER_ERROR_MESSAGE,
|
|
15
19
|
HEADER_EVENT_ID,
|
|
16
20
|
HEADER_FORK_OF,
|
|
17
21
|
HEADER_INPUT_CLIENT_ID,
|
|
@@ -24,6 +28,7 @@ import {
|
|
|
24
28
|
HEADER_RUN_ID,
|
|
25
29
|
HEADER_RUN_REASON,
|
|
26
30
|
} from '../../constants.js';
|
|
31
|
+
import { ErrorCode } from '../../errors.js';
|
|
27
32
|
import type { RunEndReason, RunLifecycleEvent } from './types.js';
|
|
28
33
|
|
|
29
34
|
/**
|
|
@@ -105,6 +110,11 @@ export const buildTransportHeaders = (opts: {
|
|
|
105
110
|
* @param opts.inputClientId - ClientId of the triggering input event.
|
|
106
111
|
* @param opts.inputCodecMessageId - Codec-message-id of the triggering input event.
|
|
107
112
|
* @param opts.reason - Terminal reason; stamped on run-end only.
|
|
113
|
+
* @param opts.errorCode - Numeric error code stamped as `error-code` on
|
|
114
|
+
* run-end. Set only when the run ended in error and the agent supplied an
|
|
115
|
+
* error to surface; gives codec-agnostic consumers a baseline failure detail.
|
|
116
|
+
* @param opts.errorMessage - Error message stamped as `error-message` on
|
|
117
|
+
* run-end. Paired with `errorCode`; set under the same condition.
|
|
108
118
|
* @returns A headers record with the lifecycle headers set.
|
|
109
119
|
*/
|
|
110
120
|
export const buildLifecycleHeaders = (opts: {
|
|
@@ -117,6 +127,8 @@ export const buildLifecycleHeaders = (opts: {
|
|
|
117
127
|
inputClientId?: string;
|
|
118
128
|
inputCodecMessageId?: string;
|
|
119
129
|
reason?: RunEndReason;
|
|
130
|
+
errorCode?: number;
|
|
131
|
+
errorMessage?: string;
|
|
120
132
|
}): Record<string, string> => {
|
|
121
133
|
const h: Record<string, string> = {
|
|
122
134
|
[HEADER_RUN_ID]: opts.runId,
|
|
@@ -129,6 +141,8 @@ export const buildLifecycleHeaders = (opts: {
|
|
|
129
141
|
if (opts.invocationId !== undefined) h[HEADER_INVOCATION_ID] = opts.invocationId;
|
|
130
142
|
if (opts.inputClientId !== undefined) h[HEADER_INPUT_CLIENT_ID] = opts.inputClientId;
|
|
131
143
|
if (opts.inputCodecMessageId !== undefined) h[HEADER_INPUT_CODEC_MESSAGE_ID] = opts.inputCodecMessageId;
|
|
144
|
+
if (opts.errorCode !== undefined) h[HEADER_ERROR_CODE] = String(opts.errorCode);
|
|
145
|
+
if (opts.errorMessage !== undefined) h[HEADER_ERROR_MESSAGE] = opts.errorMessage;
|
|
132
146
|
return h;
|
|
133
147
|
};
|
|
134
148
|
|
|
@@ -151,6 +165,26 @@ type RunLifecycleName =
|
|
|
151
165
|
export const isRunLifecycleName = (name: string | undefined): name is RunLifecycleName =>
|
|
152
166
|
name === EVENT_RUN_START || name === EVENT_RUN_SUSPEND || name === EVENT_RUN_RESUME || name === EVENT_RUN_END;
|
|
153
167
|
|
|
168
|
+
/**
|
|
169
|
+
* Reconstruct the terminal `Ably.ErrorInfo` for a run that ended in error, from
|
|
170
|
+
* its run-end transport headers. Reads the `error-code` / `error-message`
|
|
171
|
+
* headers the agent stamps (see {@link buildLifecycleHeaders}); falls back to a
|
|
172
|
+
* generic code/message when a run ended in error without detail. Single source
|
|
173
|
+
* of truth for the header→ErrorInfo derivation, shared by the client session's
|
|
174
|
+
* `on('error')` emit and the Tree's `RunInfo.error`.
|
|
175
|
+
* @param headers - Transport headers from the inbound run-end message.
|
|
176
|
+
* @returns The reconstructed terminal error.
|
|
177
|
+
*/
|
|
178
|
+
export const buildRunEndError = (headers: Record<string, string>): Ably.ErrorInfo => {
|
|
179
|
+
const codeRaw = headers[HEADER_ERROR_CODE];
|
|
180
|
+
const parsedCode = codeRaw === undefined ? Number.NaN : Number(codeRaw);
|
|
181
|
+
const code = Number.isFinite(parsedCode) ? parsedCode : ErrorCode.SessionSubscriptionError;
|
|
182
|
+
const message = headers[HEADER_ERROR_MESSAGE] ?? 'agent reported an error';
|
|
183
|
+
// 5-digit codes encode their HTTP status in the leading 3 digits; otherwise 500.
|
|
184
|
+
const statusCode = code >= 10000 && code < 60000 ? Math.floor(code / 100) : 500;
|
|
185
|
+
return new Ably.ErrorInfo(message, code, statusCode);
|
|
186
|
+
};
|
|
187
|
+
|
|
154
188
|
/**
|
|
155
189
|
* Parse an inbound run-lifecycle Ably message into a {@link RunLifecycleEvent}.
|
|
156
190
|
*
|
|
@@ -162,6 +196,9 @@ export const isRunLifecycleName = (name: string | undefined): name is RunLifecyc
|
|
|
162
196
|
* @param headers - Transport headers from the inbound Ably message.
|
|
163
197
|
* @param serial - Ably channel serial of the message, or `undefined` for an
|
|
164
198
|
* optimistic local event. Stamped onto the returned event.
|
|
199
|
+
* @param timestamp - Ably server timestamp (epoch ms) of the message, or
|
|
200
|
+
* `undefined` for an optimistic local event. Stamped onto the returned
|
|
201
|
+
* event; drives the Tree's event-log retention clock.
|
|
165
202
|
* @returns The lifecycle event, or `undefined` when `name` is not a
|
|
166
203
|
* run-lifecycle event name or the message carries no `run-id`.
|
|
167
204
|
*/
|
|
@@ -169,11 +206,13 @@ export const parseRunLifecycle = (
|
|
|
169
206
|
name: string,
|
|
170
207
|
headers: Record<string, string>,
|
|
171
208
|
serial: string | undefined,
|
|
209
|
+
timestamp: number | undefined,
|
|
172
210
|
): RunLifecycleEvent | undefined => {
|
|
173
211
|
const runId = headers[HEADER_RUN_ID];
|
|
174
212
|
if (!runId) return undefined;
|
|
175
213
|
|
|
176
214
|
const clientId = headers[HEADER_RUN_CLIENT_ID] ?? '';
|
|
215
|
+
const stamped = timestamp === undefined ? {} : { timestamp };
|
|
177
216
|
|
|
178
217
|
if (name === EVENT_RUN_START) {
|
|
179
218
|
const parent = headers[HEADER_PARENT];
|
|
@@ -185,6 +224,7 @@ export const parseRunLifecycle = (
|
|
|
185
224
|
clientId,
|
|
186
225
|
serial,
|
|
187
226
|
invocationId: headers[HEADER_INVOCATION_ID] ?? '',
|
|
227
|
+
...stamped,
|
|
188
228
|
...(parent !== undefined && { parent }),
|
|
189
229
|
...(forkOf !== undefined && { forkOf }),
|
|
190
230
|
...(regenerates !== undefined && { regenerates }),
|
|
@@ -192,17 +232,30 @@ export const parseRunLifecycle = (
|
|
|
192
232
|
}
|
|
193
233
|
|
|
194
234
|
if (name === EVENT_RUN_SUSPEND) {
|
|
195
|
-
return { type: 'suspend', runId, clientId, serial, invocationId: headers[HEADER_INVOCATION_ID] ?? '' };
|
|
235
|
+
return { type: 'suspend', runId, clientId, serial, invocationId: headers[HEADER_INVOCATION_ID] ?? '', ...stamped };
|
|
196
236
|
}
|
|
197
237
|
|
|
198
238
|
if (name === EVENT_RUN_RESUME) {
|
|
199
|
-
return { type: 'resume', runId, clientId, serial, invocationId: headers[HEADER_INVOCATION_ID] ?? '' };
|
|
239
|
+
return { type: 'resume', runId, clientId, serial, invocationId: headers[HEADER_INVOCATION_ID] ?? '', ...stamped };
|
|
200
240
|
}
|
|
201
241
|
|
|
202
242
|
if (name === EVENT_RUN_END) {
|
|
203
243
|
// CAST: agent always writes a valid RunEndReason; default to 'complete' for robustness.
|
|
204
244
|
const reason = (headers[HEADER_RUN_REASON] ?? 'complete') as RunEndReason;
|
|
205
|
-
|
|
245
|
+
const invocationId = headers[HEADER_INVOCATION_ID] ?? '';
|
|
246
|
+
if (reason === 'error') {
|
|
247
|
+
return {
|
|
248
|
+
type: 'end',
|
|
249
|
+
runId,
|
|
250
|
+
clientId,
|
|
251
|
+
serial,
|
|
252
|
+
invocationId,
|
|
253
|
+
reason,
|
|
254
|
+
...stamped,
|
|
255
|
+
error: buildRunEndError(headers),
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
return { type: 'end', runId, clientId, serial, invocationId, reason, ...stamped };
|
|
206
259
|
}
|
|
207
260
|
|
|
208
261
|
return undefined;
|
|
@@ -8,17 +8,18 @@ export type {
|
|
|
8
8
|
ClientSession,
|
|
9
9
|
ClientSessionOptions,
|
|
10
10
|
ConversationNode,
|
|
11
|
-
EventsNode,
|
|
12
11
|
InputNode,
|
|
13
12
|
LoadConversationOptions,
|
|
14
13
|
MessageNode,
|
|
15
14
|
OutputEvent,
|
|
16
15
|
PipeOptions,
|
|
17
16
|
Run,
|
|
17
|
+
RunEndParams,
|
|
18
18
|
RunEndReason,
|
|
19
19
|
RunInfo,
|
|
20
20
|
RunLifecycleEvent,
|
|
21
21
|
RunNode,
|
|
22
|
+
RunNodeState,
|
|
22
23
|
RunRuntime,
|
|
23
24
|
RunView,
|
|
24
25
|
SendOptions,
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* const invocation = Invocation.fromJSON(data);
|
|
12
12
|
* const run = session.createRun(invocation, { signal: req.signal });
|
|
13
13
|
* await run.start();
|
|
14
|
-
* await run.
|
|
14
|
+
* await run.loadConversation(); // walk channel history into the session tree
|
|
15
15
|
* const messages = run.messages;
|
|
16
16
|
* ```
|
|
17
17
|
*
|