@ably/ai-transport 0.1.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 +93 -111
- package/dist/ably-ai-transport.js +2401 -1387
- 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 +44 -0
- 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 +24 -24
- package/dist/core/codec/define-codec.d.ts +100 -0
- package/dist/core/codec/encoder.d.ts +10 -12
- 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 -2
- 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/lifecycle-tracker.d.ts +10 -9
- 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 +470 -119
- package/dist/core/codec/well-known-inputs.d.ts +52 -0
- package/dist/core/transport/agent-session.d.ts +10 -0
- package/dist/core/transport/agent-view.d.ts +296 -0
- package/dist/core/transport/client-session.d.ts +13 -0
- package/dist/core/transport/decode-fold.d.ts +55 -0
- package/dist/core/transport/headers.d.ts +121 -14
- 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-history-pages.d.ts +71 -0
- package/dist/core/transport/load-history.d.ts +44 -0
- package/dist/core/transport/pipe-stream.d.ts +9 -9
- package/dist/core/transport/run-manager.d.ts +76 -0
- package/dist/core/transport/session-support.d.ts +55 -0
- package/dist/core/transport/tree.d.ts +523 -109
- package/dist/core/transport/types/agent.d.ts +375 -0
- package/dist/core/transport/types/client.d.ts +201 -0
- package/dist/core/transport/types/shared.d.ts +24 -0
- package/dist/core/transport/types/tree.d.ts +357 -0
- package/dist/core/transport/types/view.d.ts +249 -0
- package/dist/core/transport/types.d.ts +13 -553
- package/dist/core/transport/view.d.ts +390 -84
- package/dist/core/transport/wire-log.d.ts +102 -0
- package/dist/errors.d.ts +27 -10
- package/dist/index.d.ts +8 -9
- package/dist/logger.d.ts +12 -0
- package/dist/react/ably-ai-transport-react.js +1365 -1010
- 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 +37 -0
- package/dist/react/contexts/client-session-provider.d.ts +56 -0
- package/dist/react/create-session-hooks.d.ts +116 -0
- package/dist/react/index.d.ts +13 -12
- package/dist/react/internal/skipped-session.d.ts +8 -0
- 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 +81 -50
- package/dist/utils.d.ts +48 -71
- package/dist/vercel/ably-ai-transport-vercel.js +3257 -2499
- 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 +50 -0
- 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 +7 -20
- 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 +62 -0
- package/dist/vercel/codec/tool-transitions.d.ts +2 -8
- package/dist/vercel/codec/wire-data.d.ts +34 -0
- package/dist/vercel/index.d.ts +5 -5
- package/dist/vercel/react/ably-ai-transport-vercel-react.js +2859 -9705
- 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 -45
- package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs.map +1 -1
- package/dist/vercel/react/contexts/chat-transport-context.d.ts +9 -7
- 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 +84 -0
- package/dist/vercel/tool-part.d.ts +21 -0
- package/dist/vercel/transport/chat-transport.d.ts +41 -24
- package/dist/vercel/transport/index.d.ts +24 -20
- package/dist/vercel/transport/run-output-stream.d.ts +54 -0
- package/dist/version.d.ts +2 -0
- package/package.json +31 -24
- package/src/constants.ts +124 -51
- package/src/core/agent.ts +92 -0
- package/src/core/channel-options.ts +89 -0
- package/src/core/codec/codec-event.ts +27 -0
- package/src/core/codec/decoder.ts +202 -105
- package/src/core/codec/define-codec.ts +432 -0
- package/src/core/codec/encoder.ts +114 -107
- package/src/core/codec/field-bag.ts +142 -0
- package/src/core/codec/fields.ts +193 -0
- package/src/core/codec/index.ts +56 -6
- 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/lifecycle-tracker.ts +10 -9
- 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 +505 -126
- package/src/core/codec/well-known-inputs.ts +96 -0
- package/src/core/transport/agent-session.ts +1085 -0
- package/src/core/transport/agent-view.ts +738 -0
- package/src/core/transport/client-session.ts +780 -0
- package/src/core/transport/decode-fold.ts +101 -0
- package/src/core/transport/headers.ts +234 -22
- package/src/core/transport/index.ts +27 -27
- package/src/core/transport/internal/bounded-map.ts +27 -0
- package/src/core/transport/invocation.ts +98 -0
- package/src/core/transport/load-history-pages.ts +220 -0
- package/src/core/transport/load-history.ts +271 -0
- package/src/core/transport/pipe-stream.ts +63 -39
- package/src/core/transport/run-manager.ts +243 -0
- package/src/core/transport/session-support.ts +96 -0
- package/src/core/transport/tree.ts +1293 -308
- package/src/core/transport/types/agent.ts +434 -0
- package/src/core/transport/types/client.ts +247 -0
- package/src/core/transport/types/shared.ts +27 -0
- package/src/core/transport/types/tree.ts +393 -0
- package/src/core/transport/types/view.ts +288 -0
- package/src/core/transport/types.ts +13 -706
- package/src/core/transport/view.ts +1229 -450
- package/src/core/transport/wire-log.ts +189 -0
- package/src/errors.ts +29 -9
- package/src/event-emitter.ts +3 -2
- package/src/index.ts +86 -42
- package/src/logger.ts +14 -1
- package/src/react/contexts/client-session-context.ts +41 -0
- package/src/react/contexts/client-session-provider.tsx +222 -0
- package/src/react/create-session-hooks.ts +141 -0
- package/src/react/index.ts +24 -13
- package/src/react/internal/skipped-session.ts +62 -0
- 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 +178 -0
- package/src/react/use-create-view.ts +33 -29
- package/src/react/use-tree.ts +61 -30
- package/src/react/use-view.ts +138 -96
- package/src/utils.ts +83 -131
- package/src/vercel/codec/decode-lifecycle.ts +70 -0
- package/src/vercel/codec/events.ts +85 -0
- 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 +28 -21
- 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 +191 -0
- package/src/vercel/codec/tool-transitions.ts +3 -14
- package/src/vercel/codec/wire-data.ts +64 -0
- package/src/vercel/index.ts +7 -19
- package/src/vercel/react/contexts/chat-transport-context.ts +8 -7
- 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 +44 -66
- package/src/vercel/react/use-message-sync.ts +75 -39
- package/src/vercel/run-end-reason.ts +157 -0
- package/src/vercel/tool-part.ts +25 -0
- package/src/vercel/transport/chat-transport.ts +380 -98
- package/src/vercel/transport/index.ts +38 -37
- package/src/vercel/transport/run-output-stream.ts +169 -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/codec/decoder.d.ts +0 -22
- package/dist/vercel/codec/encoder.d.ts +0 -41
- 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/codec/decoder.ts +0 -618
- package/src/vercel/codec/encoder.ts +0 -410
- 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
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side run state management and lifecycle event publishing.
|
|
3
|
+
*
|
|
4
|
+
* Owns the authoritative run lifecycle. Tracks active runs with their
|
|
5
|
+
* AbortControllers and clientIds. Publishes run-start, run-resume, run-suspend, and
|
|
6
|
+
* run-end events on the Ably channel so all clients can react to run
|
|
7
|
+
* state changes.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type * as Ably from 'ably';
|
|
11
|
+
|
|
12
|
+
import { EVENT_RUN_END, EVENT_RUN_RESUME, EVENT_RUN_START, EVENT_RUN_SUSPEND } from '../../constants.js';
|
|
13
|
+
import type { Logger } from '../../logger.js';
|
|
14
|
+
import { buildLifecycleHeaders } from './headers.js';
|
|
15
|
+
import type { RunEndReason } from './types.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Per-invocation metadata carried on a run's opening lifecycle event. A
|
|
19
|
+
* continuation (re-entering an existing run) sets `continuation` and omits the
|
|
20
|
+
* structural `parent` / `forkOf` / `regenerates` fields.
|
|
21
|
+
*/
|
|
22
|
+
interface StartRunMetadata {
|
|
23
|
+
/** Structural parent codec-message-id (fresh run-start only). */
|
|
24
|
+
parent?: string;
|
|
25
|
+
/** Forked user-prompt codec-message-id for an edit (fresh run-start only). */
|
|
26
|
+
forkOf?: string;
|
|
27
|
+
/** Regenerated assistant codec-message-id (fresh run-start only). */
|
|
28
|
+
regenerates?: string;
|
|
29
|
+
/** Agent-minted invocation id, carried on the lifecycle event. */
|
|
30
|
+
invocationId?: string;
|
|
31
|
+
/** ClientId of the triggering input event. */
|
|
32
|
+
inputClientId?: string;
|
|
33
|
+
/** Codec-message-id of the triggering input event. */
|
|
34
|
+
inputCodecMessageId?: string;
|
|
35
|
+
/** When true, publish `ai-run-resume` (re-entry) instead of `ai-run-start`. */
|
|
36
|
+
continuation?: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// Interface
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
/** Manages active runs and publishes run lifecycle events on the channel. */
|
|
44
|
+
export interface RunManager {
|
|
45
|
+
/**
|
|
46
|
+
* Register a run and publish its opening lifecycle event. Publishes
|
|
47
|
+
* `ai-run-start` for a fresh run, or `ai-run-resume` when `metadata.continuation`
|
|
48
|
+
* is set (a subsequent invocation re-entering an existing run). A resume omits
|
|
49
|
+
* the structural `parent` / `forkOf` / `regenerates` headers — the original
|
|
50
|
+
* run-start owns the run's structure.
|
|
51
|
+
*/
|
|
52
|
+
startRun(runId: string, clientId?: string, controller?: AbortController, metadata?: StartRunMetadata): Promise<void>;
|
|
53
|
+
/**
|
|
54
|
+
* Suspend a run. Publishes run-suspend on the channel and drops the run's
|
|
55
|
+
* active-run entry — the agent process terminates on suspend, so there is no
|
|
56
|
+
* live AbortController to retain. A cancel arriving during suspension is a
|
|
57
|
+
* no-op; the resuming invocation re-registers the run via {@link startRun}.
|
|
58
|
+
* Carries the same per-invocation attribution as {@link endRun}
|
|
59
|
+
* (`inputClientId`, `inputCodecMessageId`), since a suspend is the terminal
|
|
60
|
+
* event of the suspending invocation just as run-end is of an ending one.
|
|
61
|
+
*/
|
|
62
|
+
suspendRun(runId: string, invocationId?: string, inputClientId?: string, inputCodecMessageId?: string): Promise<void>;
|
|
63
|
+
/**
|
|
64
|
+
* End a run. Publishes run-end on the channel (stamping `reason` as the
|
|
65
|
+
* run-reason header) and drops the run's active-run entry. Carries the same
|
|
66
|
+
* per-invocation attribution as {@link suspendRun} (`invocationId`,
|
|
67
|
+
* `inputClientId`, `inputCodecMessageId`), since run-end is the terminal event
|
|
68
|
+
* of the ending invocation. When `reason` is `'error'` and an `error` is
|
|
69
|
+
* supplied, its `code` and `message` are additionally stamped as the
|
|
70
|
+
* `error-code` / `error-message` headers — a codec-agnostic baseline failure
|
|
71
|
+
* detail for consumers; omitting `error` publishes a bare `reason: 'error'`.
|
|
72
|
+
*/
|
|
73
|
+
endRun(
|
|
74
|
+
runId: string,
|
|
75
|
+
reason: RunEndReason,
|
|
76
|
+
invocationId?: string,
|
|
77
|
+
inputClientId?: string,
|
|
78
|
+
inputCodecMessageId?: string,
|
|
79
|
+
error?: Ably.ErrorInfo,
|
|
80
|
+
): Promise<void>;
|
|
81
|
+
/** Get the clientId that owns a run. */
|
|
82
|
+
getClientId(runId: string): string | undefined;
|
|
83
|
+
/** Cancel all active runs and clear state. */
|
|
84
|
+
close(): void;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
// Internal state
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
interface ActiveRunEntry {
|
|
92
|
+
controller: AbortController;
|
|
93
|
+
clientId: string;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
// Implementation
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
class DefaultRunManager implements RunManager {
|
|
101
|
+
private readonly _channel: Ably.RealtimeChannel;
|
|
102
|
+
private readonly _logger: Logger | undefined;
|
|
103
|
+
private readonly _activeRuns = new Map<string, ActiveRunEntry>();
|
|
104
|
+
|
|
105
|
+
constructor(channel: Ably.RealtimeChannel, logger?: Logger) {
|
|
106
|
+
this._channel = channel;
|
|
107
|
+
this._logger = logger?.withContext({ component: 'RunManager' });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async startRun(
|
|
111
|
+
runId: string,
|
|
112
|
+
clientId?: string,
|
|
113
|
+
externalController?: AbortController,
|
|
114
|
+
metadata?: StartRunMetadata,
|
|
115
|
+
): Promise<void> {
|
|
116
|
+
this._logger?.trace('DefaultRunManager.startRun();', { runId, clientId });
|
|
117
|
+
|
|
118
|
+
const controller = externalController ?? new AbortController();
|
|
119
|
+
const resolvedClientId = clientId ?? '';
|
|
120
|
+
this._activeRuns.set(runId, { controller, clientId: resolvedClientId });
|
|
121
|
+
|
|
122
|
+
// A continuation re-enters an already-started run: publish `ai-run-resume`
|
|
123
|
+
// rather than `ai-run-start`. Resume is a pure re-entry signal — the
|
|
124
|
+
// original run-start already established the run's structure, so the
|
|
125
|
+
// parent / forkOf / regenerates metadata is NOT re-stamped here (doing so
|
|
126
|
+
// would point the run at content within itself). The agent learned this is
|
|
127
|
+
// a continuation from the run-id on the triggering input; the re-entry is
|
|
128
|
+
// conveyed to clients by the event name, not a header echo. The
|
|
129
|
+
// invocation-id / input attribution headers are carried on both.
|
|
130
|
+
const continuation = metadata?.continuation === true;
|
|
131
|
+
|
|
132
|
+
const headers = buildLifecycleHeaders({
|
|
133
|
+
runId,
|
|
134
|
+
runClientId: resolvedClientId,
|
|
135
|
+
parent: continuation ? undefined : metadata?.parent,
|
|
136
|
+
forkOf: continuation ? undefined : metadata?.forkOf,
|
|
137
|
+
regenerates: continuation ? undefined : metadata?.regenerates,
|
|
138
|
+
invocationId: metadata?.invocationId,
|
|
139
|
+
inputClientId: metadata?.inputClientId,
|
|
140
|
+
inputCodecMessageId: metadata?.inputCodecMessageId,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
await this._channel.publish({
|
|
144
|
+
name: continuation ? EVENT_RUN_RESUME : EVENT_RUN_START,
|
|
145
|
+
extras: { ai: { transport: headers } },
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
this._logger?.debug('DefaultRunManager.startRun(); run started', { runId });
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async suspendRun(
|
|
152
|
+
runId: string,
|
|
153
|
+
invocationId?: string,
|
|
154
|
+
inputClientId?: string,
|
|
155
|
+
inputCodecMessageId?: string,
|
|
156
|
+
): Promise<void> {
|
|
157
|
+
this._logger?.trace('DefaultRunManager.suspendRun();', { runId });
|
|
158
|
+
await this._publishTerminal(EVENT_RUN_SUSPEND, runId, { invocationId, inputClientId, inputCodecMessageId });
|
|
159
|
+
this._logger?.debug('DefaultRunManager.suspendRun(); run suspended', { runId });
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async endRun(
|
|
163
|
+
runId: string,
|
|
164
|
+
reason: RunEndReason,
|
|
165
|
+
invocationId?: string,
|
|
166
|
+
inputClientId?: string,
|
|
167
|
+
inputCodecMessageId?: string,
|
|
168
|
+
error?: Ably.ErrorInfo,
|
|
169
|
+
): Promise<void> {
|
|
170
|
+
this._logger?.trace('DefaultRunManager.endRun();', { runId, reason });
|
|
171
|
+
// Stamp error detail only for a terminal error the agent chose to surface
|
|
172
|
+
// (AIT-ST6b4: explicit, never automatic). error-code / error-message are
|
|
173
|
+
// generic transport headers, so any codec or consumer can read them.
|
|
174
|
+
const errorAttribution = reason === 'error' && error ? { errorCode: error.code, errorMessage: error.message } : {};
|
|
175
|
+
await this._publishTerminal(EVENT_RUN_END, runId, {
|
|
176
|
+
reason,
|
|
177
|
+
invocationId,
|
|
178
|
+
inputClientId,
|
|
179
|
+
inputCodecMessageId,
|
|
180
|
+
...errorAttribution,
|
|
181
|
+
});
|
|
182
|
+
this._logger?.debug('DefaultRunManager.endRun(); run ended', { runId, reason });
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Publish a run's terminal lifecycle event (run-suspend or run-end) and drop
|
|
187
|
+
* its active-run entry. Both events are the suspending/ending invocation's
|
|
188
|
+
* terminal signal, carrying the same per-invocation correlation; they differ
|
|
189
|
+
* only by event name and the run-reason header (run-end). Publishes BEFORE
|
|
190
|
+
* dropping local state so a publish failure leaves the run in the active set.
|
|
191
|
+
* @param eventName - The lifecycle event to publish (run-suspend or run-end).
|
|
192
|
+
* @param runId - The run being suspended or ended.
|
|
193
|
+
* @param attribution - Per-invocation correlation and the terminal reason.
|
|
194
|
+
* @param attribution.reason - Terminal reason; set for run-end, omitted for run-suspend.
|
|
195
|
+
* @param attribution.invocationId - The invocation's id.
|
|
196
|
+
* @param attribution.inputClientId - ClientId of the triggering input event.
|
|
197
|
+
* @param attribution.inputCodecMessageId - Codec-message-id of the triggering input event.
|
|
198
|
+
* @param attribution.errorCode - Numeric error code; set for run-end only when a terminal error is surfaced.
|
|
199
|
+
* @param attribution.errorMessage - Error message; paired with errorCode.
|
|
200
|
+
*/
|
|
201
|
+
private async _publishTerminal(
|
|
202
|
+
eventName: string,
|
|
203
|
+
runId: string,
|
|
204
|
+
attribution: {
|
|
205
|
+
reason?: RunEndReason;
|
|
206
|
+
invocationId?: string;
|
|
207
|
+
inputClientId?: string;
|
|
208
|
+
inputCodecMessageId?: string;
|
|
209
|
+
errorCode?: number;
|
|
210
|
+
errorMessage?: string;
|
|
211
|
+
},
|
|
212
|
+
): Promise<void> {
|
|
213
|
+
const resolvedClientId = this._activeRuns.get(runId)?.clientId ?? '';
|
|
214
|
+
const headers = buildLifecycleHeaders({ runId, runClientId: resolvedClientId, ...attribution });
|
|
215
|
+
await this._channel.publish({ name: eventName, extras: { ai: { transport: headers } } });
|
|
216
|
+
this._activeRuns.delete(runId);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
getClientId(runId: string): string | undefined {
|
|
220
|
+
return this._activeRuns.get(runId)?.clientId;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
close(): void {
|
|
224
|
+
this._logger?.trace('DefaultRunManager.close();', { activeRuns: this._activeRuns.size });
|
|
225
|
+
for (const state of this._activeRuns.values()) {
|
|
226
|
+
state.controller.abort();
|
|
227
|
+
}
|
|
228
|
+
this._activeRuns.clear();
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ---------------------------------------------------------------------------
|
|
233
|
+
// Factory
|
|
234
|
+
// ---------------------------------------------------------------------------
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Create a run manager bound to the given channel.
|
|
238
|
+
* @param channel - The Ably channel to publish lifecycle events on.
|
|
239
|
+
* @param logger - Optional logger for diagnostic output.
|
|
240
|
+
* @returns A new {@link RunManager} instance.
|
|
241
|
+
*/
|
|
242
|
+
export const createRunManager = (channel: Ably.RealtimeChannel, logger?: Logger): RunManager =>
|
|
243
|
+
new DefaultRunManager(channel, logger);
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared lifecycle plumbing for the client and agent sessions.
|
|
3
|
+
*
|
|
4
|
+
* Both `DefaultClientSession` and `DefaultAgentSession` gate their writes on
|
|
5
|
+
* `connect()` having run, detach their channel best-effort on close, and react
|
|
6
|
+
* to channel continuity loss with the same detection rule and error shape.
|
|
7
|
+
* These helpers own that common machinery so the two sessions cannot drift on
|
|
8
|
+
* the connection guard, the detach-swallow behaviour, or — most importantly —
|
|
9
|
+
* the continuity-loss predicate, which encodes channel protocol semantics
|
|
10
|
+
* (Spec AIT-CT19 / AIT-ST12). Each session keeps its own divergent reaction to
|
|
11
|
+
* continuity loss (the client emits; the agent aborts runs and swaps its Tree).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import * as Ably from 'ably';
|
|
15
|
+
|
|
16
|
+
import { ErrorCode } from '../../errors.js';
|
|
17
|
+
import type { Logger } from '../../logger.js';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Resolve a session's connect guard: return the in-flight/settled connect
|
|
21
|
+
* promise, or reject with `InvalidArgument` when `connect()` has not been
|
|
22
|
+
* called. Callers `await` the result before any write.
|
|
23
|
+
* @param connectPromise - The session's connect promise, or `undefined` when not yet connected.
|
|
24
|
+
* @param method - The method name being guarded, for the error message.
|
|
25
|
+
* @returns The connect promise.
|
|
26
|
+
* @throws {Ably.ErrorInfo} `InvalidArgument` when `connectPromise` is `undefined`.
|
|
27
|
+
*/
|
|
28
|
+
export const requireConnected = async (connectPromise: Promise<void> | undefined, method: string): Promise<void> => {
|
|
29
|
+
if (!connectPromise) {
|
|
30
|
+
throw new Ably.ErrorInfo(
|
|
31
|
+
`unable to ${method}; connect() must be called before ${method}()`,
|
|
32
|
+
ErrorCode.InvalidArgument,
|
|
33
|
+
400,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
return connectPromise;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Detach the session's channel on close, best-effort. `connect()` subscribes
|
|
41
|
+
* (which implicitly attaches), so a detach is only attempted when `connect()`
|
|
42
|
+
* ran. A detach failure (e.g. the channel is already FAILED) must not throw out
|
|
43
|
+
* of `close()`, so it is swallowed and logged at debug.
|
|
44
|
+
* @param channel - The session's channel.
|
|
45
|
+
* @param connectPromise - The session's connect promise; detach is skipped when `undefined`.
|
|
46
|
+
* @param logger - Logger for the swallowed-failure debug line, or `undefined`.
|
|
47
|
+
* @param component - The owning class name, used as the log message prefix.
|
|
48
|
+
*/
|
|
49
|
+
export const bestEffortDetach = async (
|
|
50
|
+
channel: Ably.RealtimeChannel,
|
|
51
|
+
connectPromise: Promise<void> | undefined,
|
|
52
|
+
logger: Logger | undefined,
|
|
53
|
+
component: string,
|
|
54
|
+
): Promise<void> => {
|
|
55
|
+
if (connectPromise === undefined) return;
|
|
56
|
+
try {
|
|
57
|
+
await channel.detach();
|
|
58
|
+
} catch (error) {
|
|
59
|
+
logger?.debug(`${component}.close(); channel detach failed`, { error });
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Whether a channel state change breaks message continuity:
|
|
65
|
+
* - FAILED, SUSPENDED, DETACHED — no more messages expected (or a gap)
|
|
66
|
+
* - ATTACHED with `resumed: false` (an UPDATE) — messages were lost
|
|
67
|
+
*
|
|
68
|
+
* The initial attach (ATTACHED with no prior attach) is the caller's concern
|
|
69
|
+
* and is not handled here.
|
|
70
|
+
* @param stateChange - The channel state change to classify.
|
|
71
|
+
* @returns True when continuity was lost.
|
|
72
|
+
*/
|
|
73
|
+
export const isContinuityLost = (stateChange: Ably.ChannelStateChange): boolean => {
|
|
74
|
+
const { current, resumed } = stateChange;
|
|
75
|
+
return (
|
|
76
|
+
current === 'failed' || current === 'suspended' || current === 'detached' || (current === 'attached' && !resumed)
|
|
77
|
+
);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Build the `ChannelContinuityLost` error for a continuity-breaking state
|
|
82
|
+
* change, attaching the state change's `reason` as `cause`.
|
|
83
|
+
* @param stateChange - The continuity-breaking state change.
|
|
84
|
+
* @param verb - The operation that can no longer proceed, for the
|
|
85
|
+
* `unable to <verb>; ...` message (e.g. "deliver events", "continue").
|
|
86
|
+
* @returns The continuity-loss error.
|
|
87
|
+
*/
|
|
88
|
+
export const continuityLostError = (stateChange: Ably.ChannelStateChange, verb: string): Ably.ErrorInfo => {
|
|
89
|
+
const { current } = stateChange;
|
|
90
|
+
return new Ably.ErrorInfo(
|
|
91
|
+
`unable to ${verb}; channel continuity lost (${current}${current === 'attached' ? ', resumed: false' : ''})`,
|
|
92
|
+
ErrorCode.ChannelContinuityLost,
|
|
93
|
+
500,
|
|
94
|
+
stateChange.reason,
|
|
95
|
+
);
|
|
96
|
+
};
|