@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.
Files changed (221) hide show
  1. package/README.md +93 -111
  2. package/dist/ably-ai-transport.js +2401 -1387
  3. package/dist/ably-ai-transport.js.map +1 -1
  4. package/dist/ably-ai-transport.umd.cjs +1 -1
  5. package/dist/ably-ai-transport.umd.cjs.map +1 -1
  6. package/dist/constants.d.ts +116 -42
  7. package/dist/core/agent.d.ts +44 -0
  8. package/dist/core/channel-options.d.ts +57 -0
  9. package/dist/core/codec/codec-event.d.ts +9 -0
  10. package/dist/core/codec/decoder.d.ts +24 -24
  11. package/dist/core/codec/define-codec.d.ts +100 -0
  12. package/dist/core/codec/encoder.d.ts +10 -12
  13. package/dist/core/codec/field-bag.d.ts +85 -0
  14. package/dist/core/codec/fields.d.ts +141 -0
  15. package/dist/core/codec/index.d.ts +8 -2
  16. package/dist/core/codec/input-descriptor-decoder.d.ts +19 -0
  17. package/dist/core/codec/input-descriptor-encoder.d.ts +22 -0
  18. package/dist/core/codec/input-descriptors.d.ts +281 -0
  19. package/dist/core/codec/lifecycle-tracker.d.ts +10 -9
  20. package/dist/core/codec/output-descriptor-decoder.d.ts +29 -0
  21. package/dist/core/codec/output-descriptor-encoder.d.ts +31 -0
  22. package/dist/core/codec/output-descriptors.d.ts +237 -0
  23. package/dist/core/codec/types.d.ts +470 -119
  24. package/dist/core/codec/well-known-inputs.d.ts +52 -0
  25. package/dist/core/transport/agent-session.d.ts +10 -0
  26. package/dist/core/transport/agent-view.d.ts +296 -0
  27. package/dist/core/transport/client-session.d.ts +13 -0
  28. package/dist/core/transport/decode-fold.d.ts +55 -0
  29. package/dist/core/transport/headers.d.ts +121 -14
  30. package/dist/core/transport/index.d.ts +5 -6
  31. package/dist/core/transport/internal/bounded-map.d.ts +20 -0
  32. package/dist/core/transport/invocation.d.ts +74 -0
  33. package/dist/core/transport/load-history-pages.d.ts +71 -0
  34. package/dist/core/transport/load-history.d.ts +44 -0
  35. package/dist/core/transport/pipe-stream.d.ts +9 -9
  36. package/dist/core/transport/run-manager.d.ts +76 -0
  37. package/dist/core/transport/session-support.d.ts +55 -0
  38. package/dist/core/transport/tree.d.ts +523 -109
  39. package/dist/core/transport/types/agent.d.ts +375 -0
  40. package/dist/core/transport/types/client.d.ts +201 -0
  41. package/dist/core/transport/types/shared.d.ts +24 -0
  42. package/dist/core/transport/types/tree.d.ts +357 -0
  43. package/dist/core/transport/types/view.d.ts +249 -0
  44. package/dist/core/transport/types.d.ts +13 -553
  45. package/dist/core/transport/view.d.ts +390 -84
  46. package/dist/core/transport/wire-log.d.ts +102 -0
  47. package/dist/errors.d.ts +27 -10
  48. package/dist/index.d.ts +8 -9
  49. package/dist/logger.d.ts +12 -0
  50. package/dist/react/ably-ai-transport-react.js +1365 -1010
  51. package/dist/react/ably-ai-transport-react.js.map +1 -1
  52. package/dist/react/ably-ai-transport-react.umd.cjs +1 -1
  53. package/dist/react/ably-ai-transport-react.umd.cjs.map +1 -1
  54. package/dist/react/contexts/client-session-context.d.ts +37 -0
  55. package/dist/react/contexts/client-session-provider.d.ts +56 -0
  56. package/dist/react/create-session-hooks.d.ts +116 -0
  57. package/dist/react/index.d.ts +13 -12
  58. package/dist/react/internal/skipped-session.d.ts +8 -0
  59. package/dist/react/internal/use-resolved-session.d.ts +36 -0
  60. package/dist/react/use-ably-messages.d.ts +17 -14
  61. package/dist/react/use-client-session.d.ts +81 -0
  62. package/dist/react/use-create-view.d.ts +14 -13
  63. package/dist/react/use-tree.d.ts +30 -15
  64. package/dist/react/use-view.d.ts +81 -50
  65. package/dist/utils.d.ts +48 -71
  66. package/dist/vercel/ably-ai-transport-vercel.js +3257 -2499
  67. package/dist/vercel/ably-ai-transport-vercel.js.map +1 -1
  68. package/dist/vercel/ably-ai-transport-vercel.umd.cjs +1 -1
  69. package/dist/vercel/ably-ai-transport-vercel.umd.cjs.map +1 -1
  70. package/dist/vercel/codec/decode-lifecycle.d.ts +9 -0
  71. package/dist/vercel/codec/events.d.ts +50 -0
  72. package/dist/vercel/codec/fields.d.ts +44 -0
  73. package/dist/vercel/codec/fold-content.d.ts +16 -0
  74. package/dist/vercel/codec/fold-data.d.ts +16 -0
  75. package/dist/vercel/codec/fold-input.d.ts +67 -0
  76. package/dist/vercel/codec/fold-lifecycle.d.ts +16 -0
  77. package/dist/vercel/codec/fold-text.d.ts +16 -0
  78. package/dist/vercel/codec/fold-tool-input.d.ts +17 -0
  79. package/dist/vercel/codec/fold-tool-output.d.ts +16 -0
  80. package/dist/vercel/codec/index.d.ts +7 -20
  81. package/dist/vercel/codec/inputs.d.ts +11 -0
  82. package/dist/vercel/codec/outputs.d.ts +11 -0
  83. package/dist/vercel/codec/reducer-state.d.ts +121 -0
  84. package/dist/vercel/codec/reducer.d.ts +62 -0
  85. package/dist/vercel/codec/tool-transitions.d.ts +2 -8
  86. package/dist/vercel/codec/wire-data.d.ts +34 -0
  87. package/dist/vercel/index.d.ts +5 -5
  88. package/dist/vercel/react/ably-ai-transport-vercel-react.js +2859 -9705
  89. package/dist/vercel/react/ably-ai-transport-vercel-react.js.map +1 -1
  90. package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs +1 -45
  91. package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs.map +1 -1
  92. package/dist/vercel/react/contexts/chat-transport-context.d.ts +9 -7
  93. package/dist/vercel/react/contexts/chat-transport-provider.d.ts +53 -41
  94. package/dist/vercel/react/index.d.ts +1 -2
  95. package/dist/vercel/react/use-chat-transport.d.ts +30 -26
  96. package/dist/vercel/react/use-message-sync.d.ts +17 -30
  97. package/dist/vercel/run-end-reason.d.ts +84 -0
  98. package/dist/vercel/tool-part.d.ts +21 -0
  99. package/dist/vercel/transport/chat-transport.d.ts +41 -24
  100. package/dist/vercel/transport/index.d.ts +24 -20
  101. package/dist/vercel/transport/run-output-stream.d.ts +54 -0
  102. package/dist/version.d.ts +2 -0
  103. package/package.json +31 -24
  104. package/src/constants.ts +124 -51
  105. package/src/core/agent.ts +92 -0
  106. package/src/core/channel-options.ts +89 -0
  107. package/src/core/codec/codec-event.ts +27 -0
  108. package/src/core/codec/decoder.ts +202 -105
  109. package/src/core/codec/define-codec.ts +432 -0
  110. package/src/core/codec/encoder.ts +114 -107
  111. package/src/core/codec/field-bag.ts +142 -0
  112. package/src/core/codec/fields.ts +193 -0
  113. package/src/core/codec/index.ts +56 -6
  114. package/src/core/codec/input-descriptor-decoder.ts +97 -0
  115. package/src/core/codec/input-descriptor-encoder.ts +150 -0
  116. package/src/core/codec/input-descriptors.ts +373 -0
  117. package/src/core/codec/lifecycle-tracker.ts +10 -9
  118. package/src/core/codec/output-descriptor-decoder.ts +139 -0
  119. package/src/core/codec/output-descriptor-encoder.ts +101 -0
  120. package/src/core/codec/output-descriptors.ts +307 -0
  121. package/src/core/codec/types.ts +505 -126
  122. package/src/core/codec/well-known-inputs.ts +96 -0
  123. package/src/core/transport/agent-session.ts +1085 -0
  124. package/src/core/transport/agent-view.ts +738 -0
  125. package/src/core/transport/client-session.ts +780 -0
  126. package/src/core/transport/decode-fold.ts +101 -0
  127. package/src/core/transport/headers.ts +234 -22
  128. package/src/core/transport/index.ts +27 -27
  129. package/src/core/transport/internal/bounded-map.ts +27 -0
  130. package/src/core/transport/invocation.ts +98 -0
  131. package/src/core/transport/load-history-pages.ts +220 -0
  132. package/src/core/transport/load-history.ts +271 -0
  133. package/src/core/transport/pipe-stream.ts +63 -39
  134. package/src/core/transport/run-manager.ts +243 -0
  135. package/src/core/transport/session-support.ts +96 -0
  136. package/src/core/transport/tree.ts +1293 -308
  137. package/src/core/transport/types/agent.ts +434 -0
  138. package/src/core/transport/types/client.ts +247 -0
  139. package/src/core/transport/types/shared.ts +27 -0
  140. package/src/core/transport/types/tree.ts +393 -0
  141. package/src/core/transport/types/view.ts +288 -0
  142. package/src/core/transport/types.ts +13 -706
  143. package/src/core/transport/view.ts +1229 -450
  144. package/src/core/transport/wire-log.ts +189 -0
  145. package/src/errors.ts +29 -9
  146. package/src/event-emitter.ts +3 -2
  147. package/src/index.ts +86 -42
  148. package/src/logger.ts +14 -1
  149. package/src/react/contexts/client-session-context.ts +41 -0
  150. package/src/react/contexts/client-session-provider.tsx +222 -0
  151. package/src/react/create-session-hooks.ts +141 -0
  152. package/src/react/index.ts +24 -13
  153. package/src/react/internal/skipped-session.ts +62 -0
  154. package/src/react/internal/use-resolved-session.ts +63 -0
  155. package/src/react/use-ably-messages.ts +32 -22
  156. package/src/react/use-client-session.ts +178 -0
  157. package/src/react/use-create-view.ts +33 -29
  158. package/src/react/use-tree.ts +61 -30
  159. package/src/react/use-view.ts +138 -96
  160. package/src/utils.ts +83 -131
  161. package/src/vercel/codec/decode-lifecycle.ts +70 -0
  162. package/src/vercel/codec/events.ts +85 -0
  163. package/src/vercel/codec/fields.ts +58 -0
  164. package/src/vercel/codec/fold-content.ts +54 -0
  165. package/src/vercel/codec/fold-data.ts +46 -0
  166. package/src/vercel/codec/fold-input.ts +255 -0
  167. package/src/vercel/codec/fold-lifecycle.ts +85 -0
  168. package/src/vercel/codec/fold-text.ts +55 -0
  169. package/src/vercel/codec/fold-tool-input.ts +86 -0
  170. package/src/vercel/codec/fold-tool-output.ts +79 -0
  171. package/src/vercel/codec/index.ts +28 -21
  172. package/src/vercel/codec/inputs.ts +116 -0
  173. package/src/vercel/codec/outputs.ts +207 -0
  174. package/src/vercel/codec/reducer-state.ts +169 -0
  175. package/src/vercel/codec/reducer.ts +191 -0
  176. package/src/vercel/codec/tool-transitions.ts +3 -14
  177. package/src/vercel/codec/wire-data.ts +64 -0
  178. package/src/vercel/index.ts +7 -19
  179. package/src/vercel/react/contexts/chat-transport-context.ts +8 -7
  180. package/src/vercel/react/contexts/chat-transport-provider.tsx +87 -59
  181. package/src/vercel/react/index.ts +3 -5
  182. package/src/vercel/react/use-chat-transport.ts +44 -66
  183. package/src/vercel/react/use-message-sync.ts +75 -39
  184. package/src/vercel/run-end-reason.ts +157 -0
  185. package/src/vercel/tool-part.ts +25 -0
  186. package/src/vercel/transport/chat-transport.ts +380 -98
  187. package/src/vercel/transport/index.ts +38 -37
  188. package/src/vercel/transport/run-output-stream.ts +169 -0
  189. package/src/version.ts +2 -0
  190. package/dist/core/transport/client-transport.d.ts +0 -10
  191. package/dist/core/transport/decode-history.d.ts +0 -43
  192. package/dist/core/transport/server-transport.d.ts +0 -7
  193. package/dist/core/transport/stream-router.d.ts +0 -29
  194. package/dist/core/transport/turn-manager.d.ts +0 -37
  195. package/dist/react/contexts/transport-context.d.ts +0 -31
  196. package/dist/react/contexts/transport-provider.d.ts +0 -49
  197. package/dist/react/create-transport-hooks.d.ts +0 -124
  198. package/dist/react/use-active-turns.d.ts +0 -12
  199. package/dist/react/use-client-transport.d.ts +0 -80
  200. package/dist/vercel/codec/accumulator.d.ts +0 -21
  201. package/dist/vercel/codec/decoder.d.ts +0 -22
  202. package/dist/vercel/codec/encoder.d.ts +0 -41
  203. package/dist/vercel/react/use-staged-add-tool-approval-response.d.ts +0 -30
  204. package/dist/vercel/tool-approvals.d.ts +0 -124
  205. package/dist/vercel/tool-events.d.ts +0 -26
  206. package/src/core/transport/client-transport.ts +0 -977
  207. package/src/core/transport/decode-history.ts +0 -485
  208. package/src/core/transport/server-transport.ts +0 -612
  209. package/src/core/transport/stream-router.ts +0 -136
  210. package/src/core/transport/turn-manager.ts +0 -165
  211. package/src/react/contexts/transport-context.ts +0 -37
  212. package/src/react/contexts/transport-provider.tsx +0 -164
  213. package/src/react/create-transport-hooks.ts +0 -144
  214. package/src/react/use-active-turns.ts +0 -72
  215. package/src/react/use-client-transport.ts +0 -197
  216. package/src/vercel/codec/accumulator.ts +0 -588
  217. package/src/vercel/codec/decoder.ts +0 -618
  218. package/src/vercel/codec/encoder.ts +0 -410
  219. package/src/vercel/react/use-staged-add-tool-approval-response.ts +0 -87
  220. package/src/vercel/tool-approvals.ts +0 -380
  221. 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
+ };