@ably/ai-transport 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. package/README.md +91 -100
  2. package/dist/ably-ai-transport.js +1553 -1238
  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 +29 -0
  8. package/dist/core/codec/decoder.d.ts +20 -23
  9. package/dist/core/codec/encoder.d.ts +11 -8
  10. package/dist/core/codec/index.d.ts +1 -2
  11. package/dist/core/codec/lifecycle-tracker.d.ts +10 -9
  12. package/dist/core/codec/types.d.ts +407 -115
  13. package/dist/core/transport/agent-session.d.ts +10 -0
  14. package/dist/core/transport/branch-chain.d.ts +43 -0
  15. package/dist/core/transport/client-session.d.ts +13 -0
  16. package/dist/core/transport/decode-fold.d.ts +47 -0
  17. package/dist/core/transport/headers.d.ts +96 -18
  18. package/dist/core/transport/index.d.ts +5 -6
  19. package/dist/core/transport/internal/bounded-map.d.ts +20 -0
  20. package/dist/core/transport/invocation.d.ts +74 -0
  21. package/dist/core/transport/load-conversation.d.ts +128 -0
  22. package/dist/core/transport/load-history.d.ts +39 -0
  23. package/dist/core/transport/pipe-stream.d.ts +9 -9
  24. package/dist/core/transport/run-manager.d.ts +78 -0
  25. package/dist/core/transport/tree.d.ts +373 -109
  26. package/dist/core/transport/types/agent.d.ts +353 -0
  27. package/dist/core/transport/types/client.d.ts +168 -0
  28. package/dist/core/transport/types/shared.d.ts +24 -0
  29. package/dist/core/transport/types/tree.d.ts +315 -0
  30. package/dist/core/transport/types/view.d.ts +222 -0
  31. package/dist/core/transport/types.d.ts +13 -553
  32. package/dist/core/transport/view.d.ts +272 -84
  33. package/dist/errors.d.ts +21 -10
  34. package/dist/index.d.ts +6 -8
  35. package/dist/logger.d.ts +12 -0
  36. package/dist/react/ably-ai-transport-react.js +976 -990
  37. package/dist/react/ably-ai-transport-react.js.map +1 -1
  38. package/dist/react/ably-ai-transport-react.umd.cjs +1 -1
  39. package/dist/react/ably-ai-transport-react.umd.cjs.map +1 -1
  40. package/dist/react/contexts/client-session-context.d.ts +36 -0
  41. package/dist/react/contexts/client-session-provider.d.ts +53 -0
  42. package/dist/react/create-session-hooks.d.ts +116 -0
  43. package/dist/react/index.d.ts +12 -12
  44. package/dist/react/internal/use-resolved-session.d.ts +36 -0
  45. package/dist/react/use-ably-messages.d.ts +17 -14
  46. package/dist/react/use-client-session.d.ts +81 -0
  47. package/dist/react/use-create-view.d.ts +14 -13
  48. package/dist/react/use-tree.d.ts +30 -15
  49. package/dist/react/use-view.d.ts +82 -51
  50. package/dist/utils.d.ts +32 -23
  51. package/dist/vercel/ably-ai-transport-vercel.js +2573 -2086
  52. package/dist/vercel/ably-ai-transport-vercel.js.map +1 -1
  53. package/dist/vercel/ably-ai-transport-vercel.umd.cjs +1 -1
  54. package/dist/vercel/ably-ai-transport-vercel.umd.cjs.map +1 -1
  55. package/dist/vercel/codec/decoder.d.ts +5 -18
  56. package/dist/vercel/codec/encoder.d.ts +6 -36
  57. package/dist/vercel/codec/events.d.ts +51 -0
  58. package/dist/vercel/codec/index.d.ts +24 -12
  59. package/dist/vercel/codec/reducer.d.ts +144 -0
  60. package/dist/vercel/codec/tool-transitions.d.ts +2 -2
  61. package/dist/vercel/index.d.ts +4 -5
  62. package/dist/vercel/react/ably-ai-transport-vercel-react.js +3907 -3266
  63. package/dist/vercel/react/ably-ai-transport-vercel-react.js.map +1 -1
  64. package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs +33 -8
  65. package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs.map +1 -1
  66. package/dist/vercel/react/contexts/chat-transport-context.d.ts +7 -6
  67. package/dist/vercel/react/contexts/chat-transport-provider.d.ts +53 -41
  68. package/dist/vercel/react/index.d.ts +1 -2
  69. package/dist/vercel/react/use-chat-transport.d.ts +30 -26
  70. package/dist/vercel/react/use-message-sync.d.ts +17 -30
  71. package/dist/vercel/run-end-reason.d.ts +29 -0
  72. package/dist/vercel/transport/chat-transport.d.ts +43 -24
  73. package/dist/vercel/transport/index.d.ts +25 -21
  74. package/dist/vercel/transport/run-output-stream.d.ts +56 -0
  75. package/dist/version.d.ts +2 -0
  76. package/package.json +30 -23
  77. package/src/constants.ts +124 -51
  78. package/src/core/agent.ts +68 -0
  79. package/src/core/codec/decoder.ts +71 -98
  80. package/src/core/codec/encoder.ts +113 -65
  81. package/src/core/codec/index.ts +13 -6
  82. package/src/core/codec/lifecycle-tracker.ts +10 -9
  83. package/src/core/codec/types.ts +436 -120
  84. package/src/core/transport/agent-session.ts +1344 -0
  85. package/src/core/transport/branch-chain.ts +58 -0
  86. package/src/core/transport/client-session.ts +775 -0
  87. package/src/core/transport/decode-fold.ts +91 -0
  88. package/src/core/transport/headers.ts +181 -22
  89. package/src/core/transport/index.ts +25 -26
  90. package/src/core/transport/internal/bounded-map.ts +27 -0
  91. package/src/core/transport/invocation.ts +98 -0
  92. package/src/core/transport/load-conversation.ts +355 -0
  93. package/src/core/transport/load-history.ts +269 -0
  94. package/src/core/transport/pipe-stream.ts +54 -39
  95. package/src/core/transport/run-manager.ts +249 -0
  96. package/src/core/transport/tree.ts +926 -308
  97. package/src/core/transport/types/agent.ts +407 -0
  98. package/src/core/transport/types/client.ts +211 -0
  99. package/src/core/transport/types/shared.ts +27 -0
  100. package/src/core/transport/types/tree.ts +344 -0
  101. package/src/core/transport/types/view.ts +259 -0
  102. package/src/core/transport/types.ts +13 -706
  103. package/src/core/transport/view.ts +864 -433
  104. package/src/errors.ts +22 -9
  105. package/src/event-emitter.ts +3 -2
  106. package/src/index.ts +52 -41
  107. package/src/logger.ts +14 -1
  108. package/src/react/contexts/client-session-context.ts +41 -0
  109. package/src/react/contexts/client-session-provider.tsx +186 -0
  110. package/src/react/create-session-hooks.ts +141 -0
  111. package/src/react/index.ts +23 -13
  112. package/src/react/internal/use-resolved-session.ts +63 -0
  113. package/src/react/use-ably-messages.ts +32 -22
  114. package/src/react/use-client-session.ts +201 -0
  115. package/src/react/use-create-view.ts +33 -29
  116. package/src/react/use-tree.ts +61 -30
  117. package/src/react/use-view.ts +139 -97
  118. package/src/utils.ts +63 -45
  119. package/src/vercel/codec/decoder.ts +336 -258
  120. package/src/vercel/codec/encoder.ts +343 -205
  121. package/src/vercel/codec/events.ts +87 -0
  122. package/src/vercel/codec/index.ts +60 -13
  123. package/src/vercel/codec/reducer.ts +977 -0
  124. package/src/vercel/codec/tool-transitions.ts +2 -2
  125. package/src/vercel/index.ts +6 -19
  126. package/src/vercel/react/contexts/chat-transport-context.ts +7 -6
  127. package/src/vercel/react/contexts/chat-transport-provider.tsx +87 -59
  128. package/src/vercel/react/index.ts +3 -5
  129. package/src/vercel/react/use-chat-transport.ts +47 -49
  130. package/src/vercel/react/use-message-sync.ts +80 -39
  131. package/src/vercel/run-end-reason.ts +78 -0
  132. package/src/vercel/transport/chat-transport.ts +392 -98
  133. package/src/vercel/transport/index.ts +39 -38
  134. package/src/vercel/transport/run-output-stream.ts +170 -0
  135. package/src/version.ts +2 -0
  136. package/dist/core/transport/client-transport.d.ts +0 -10
  137. package/dist/core/transport/decode-history.d.ts +0 -43
  138. package/dist/core/transport/server-transport.d.ts +0 -7
  139. package/dist/core/transport/stream-router.d.ts +0 -29
  140. package/dist/core/transport/turn-manager.d.ts +0 -37
  141. package/dist/react/contexts/transport-context.d.ts +0 -31
  142. package/dist/react/contexts/transport-provider.d.ts +0 -49
  143. package/dist/react/create-transport-hooks.d.ts +0 -124
  144. package/dist/react/use-active-turns.d.ts +0 -12
  145. package/dist/react/use-client-transport.d.ts +0 -80
  146. package/dist/vercel/codec/accumulator.d.ts +0 -21
  147. package/dist/vercel/react/use-staged-add-tool-approval-response.d.ts +0 -30
  148. package/dist/vercel/tool-approvals.d.ts +0 -124
  149. package/dist/vercel/tool-events.d.ts +0 -26
  150. package/src/core/transport/client-transport.ts +0 -977
  151. package/src/core/transport/decode-history.ts +0 -485
  152. package/src/core/transport/server-transport.ts +0 -612
  153. package/src/core/transport/stream-router.ts +0 -136
  154. package/src/core/transport/turn-manager.ts +0 -165
  155. package/src/react/contexts/transport-context.ts +0 -37
  156. package/src/react/contexts/transport-provider.tsx +0 -164
  157. package/src/react/create-transport-hooks.ts +0 -144
  158. package/src/react/use-active-turns.ts +0 -72
  159. package/src/react/use-client-transport.ts +0 -197
  160. package/src/vercel/codec/accumulator.ts +0 -588
  161. package/src/vercel/react/use-staged-add-tool-approval-response.ts +0 -87
  162. package/src/vercel/tool-approvals.ts +0 -380
  163. package/src/vercel/tool-events.ts +0 -53
@@ -1,33 +1,52 @@
1
1
  /**
2
2
  * useView — reactive paginated view of the conversation.
3
3
  *
4
- * Subscribes to view updates and exposes the visible nodes, branch navigation,
5
- * write operations, pagination state, and a `loadOlder` callback. Pass `transport`
6
- * to use a transport's default view, or `view` to subscribe to a specific
7
- * {@link View} directly. When both are omitted, defaults to the nearest
8
- * {@link TransportProvider}'s transport via context.
4
+ * Subscribes to view updates and exposes the visible messages, msg-anchored
5
+ * branch navigation, write operations, pagination state, and a `loadOlder`
6
+ * callback. Pass `session` to use a session's default view, or `view` to
7
+ * subscribe to a specific {@link View} directly. When both are omitted,
8
+ * defaults to the nearest {@link ClientSessionProvider}'s session via context.
9
9
  */
10
10
 
11
11
  import * as Ably from 'ably';
12
- import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
12
+ import { useCallback, useEffect, useRef, useState } from 'react';
13
13
 
14
- import type { ActiveTurn, ClientTransport, MessageNode, SendOptions, View } from '../core/transport/types.js';
14
+ import type { CodecInputEvent, CodecMessage, CodecOutputEvent } from '../core/codec/types.js';
15
+ import type { ActiveRun, BranchSelection, RunInfo, SendOptions, View } from '../core/transport/types.js';
15
16
  import { ErrorCode } from '../errors.js';
16
- import { NearestTransportContext } from './contexts/transport-context.js';
17
+ import type { BaseSessionOption } from './internal/use-resolved-session.js';
18
+ import { useResolvedSession } from './internal/use-resolved-session.js';
17
19
 
18
- /** Options for configuring the view's initial load behavior. */
19
- export interface UseViewOptions {
20
- /** Maximum number of older messages to load per page. Defaults to 100. */
20
+ /** Options for {@link useView}. */
21
+ export interface UseViewOptions<
22
+ TInput extends CodecInputEvent,
23
+ TOutput extends CodecOutputEvent,
24
+ TProjection,
25
+ TMessage,
26
+ > extends BaseSessionOption<TInput, TOutput, TProjection, TMessage> {
27
+ /** A specific {@link View} to subscribe to directly. Takes priority over `session`. */
28
+ view?: View<TInput, TMessage> | null;
29
+ /** Maximum number of older Runs to reveal per page (the pagination unit is the Run, not the message). When provided, auto-loads the first page on mount. */
21
30
  limit?: number;
31
+ /** When `true`, skip all subscriptions and return an empty handle immediately. */
32
+ skip?: boolean;
22
33
  }
23
34
 
24
35
  /** Handle for the paginated, branch-aware conversation view. */
25
- export interface ViewHandle<TEvent, TMessage> {
26
- /** The visible domain messages along the selected branch. */
27
- messages: TMessage[];
28
- /** Visible conversation nodes along the selected branch. */
29
- nodes: MessageNode<TMessage>[];
30
- /** Whether there are older messages that can be revealed via `loadOlder`. */
36
+ export interface ViewHandle<TInput extends CodecInputEvent, TMessage> {
37
+ /**
38
+ * The visible messages along the selected branch, concatenated across all
39
+ * visible Runs, each paired with its codec-message-id (see
40
+ * {@link CodecMessage}). Read the domain object from each entry's
41
+ * `message` field.
42
+ *
43
+ * Correlate a rendered message back to the View — `runOf`,
44
+ * `branchSelection`, `selectSibling`, `regenerate`, or `edit` — via its
45
+ * `codecMessageId`, which the SDK assigns and tracks independently of any
46
+ * identity the domain `message` may carry. See {@link View.getMessages}.
47
+ */
48
+ messages: CodecMessage<TMessage>[];
49
+ /** Whether there are older Runs that can be revealed via `loadOlder`. */
31
50
  hasOlder: boolean;
32
51
  /** Whether a page load is currently in progress. */
33
52
  loading: boolean;
@@ -39,65 +58,90 @@ export interface ViewHandle<TEvent, TMessage> {
39
58
  loadError: Ably.ErrorInfo | undefined;
40
59
  /**
41
60
  * Load older messages into the view. No-op if already loading.
42
- * On failure, `error` is set; on success, `error` is cleared.
61
+ * On failure, `loadError` is set; on success, `loadError` is cleared.
43
62
  */
44
63
  loadOlder: () => Promise<void>;
45
- /** Select a sibling at a fork point by index. Triggers a view update with the new branch. */
46
- select: (msgId: string, index: number) => void;
47
- /** Index of the currently selected sibling at a fork point. */
48
- getSelectedIndex: (msgId: string) => number;
49
- /** Get all sibling messages at a fork point, ordered chronologically by serial. */
50
- getSiblings: (msgId: string) => TMessage[];
51
- /** Whether a message has sibling alternatives (i.e., show navigation arrows). */
52
- hasSiblings: (msgId: string) => boolean;
53
- /** Get a node by msgId, or undefined if not found. */
54
- getNode: (msgId: string) => MessageNode<TMessage> | undefined;
55
- /** Send one or more messages in the context of this view's selected branch. */
56
- send: (messages: TMessage | TMessage[], options?: SendOptions) => Promise<ActiveTurn<TEvent>>;
57
- /** Regenerate an assistant message, using this view's branch for history. */
58
- regenerate: (messageId: string, options?: SendOptions) => Promise<ActiveTurn<TEvent>>;
59
- /** Edit a user message, forking from this view's branch. */
60
- edit: (messageId: string, newMessages: TMessage | TMessage[], options?: SendOptions) => Promise<ActiveTurn<TEvent>>;
61
- /** Amend an existing message and start a continuation turn (e.g. tool results). */
62
- update: (msgId: string, events: TEvent[], options?: SendOptions) => Promise<ActiveTurn<TEvent>>;
64
+ /**
65
+ * Look up the {@link RunInfo} for the Run that owns `codecMessageId`.
66
+ * Returns `undefined` when the codec-message-id hasn't been observed.
67
+ * See {@link View.runOf}.
68
+ */
69
+ runOf: (codecMessageId: string) => RunInfo | undefined;
70
+ /**
71
+ * Direct lookup by runId. Returns `undefined` when the Run hasn't been
72
+ * observed. See {@link View.run}.
73
+ */
74
+ run: (runId: string) => RunInfo | undefined;
75
+ /**
76
+ * Snapshot of the visible Runs along the selected branch, in
77
+ * chronological order. Returns `[]` when the view isn't resolved.
78
+ * See {@link View.runs}.
79
+ */
80
+ runs: () => RunInfo[];
81
+ /**
82
+ * Resolve the {@link BranchSelection} bundle anchored at
83
+ * `codecMessageId`. Always returns a safe object — see
84
+ * {@link BranchSelection}. See {@link View.branchSelection}.
85
+ */
86
+ branchSelection: (codecMessageId: string) => BranchSelection<TMessage>;
87
+ /**
88
+ * Select a sibling at the branch point anchored at `codecMessageId`.
89
+ * `index` is clamped to `[0, siblings.length - 1]`. Silent no-op when
90
+ * `codecMessageId` isn't a branch anchor. See {@link View.selectSibling}.
91
+ */
92
+ selectSibling: (codecMessageId: string, index: number) => void;
93
+ /**
94
+ * Send one or more TInputs on the channel and fire a POST. See {@link View.send}.
95
+ * @throws Ably.ErrorInfo with code {@link ErrorCode.InvalidArgument} when no view is resolved (before the session is available, or when `skip` is `true`).
96
+ */
97
+ send: (events: TInput | TInput[], options?: SendOptions) => Promise<ActiveRun>;
98
+ /**
99
+ * Regenerate an assistant message, using this view's branch for history.
100
+ * @throws Ably.ErrorInfo with code {@link ErrorCode.InvalidArgument} when no view is resolved (before the session is available, or when `skip` is `true`).
101
+ */
102
+ regenerate: (messageId: string, options?: SendOptions) => Promise<ActiveRun>;
103
+ /**
104
+ * Edit a user message, forking from this view's branch.
105
+ * Rejects with an `Ably.ErrorInfo` (code {@link ErrorCode.InvalidArgument}) if no view is resolved — e.g. before the session is available, or when `skip` is `true`.
106
+ */
107
+ edit: (messageId: string, inputs: TInput | TInput[], options?: SendOptions) => Promise<ActiveRun>;
63
108
  }
64
109
 
65
110
  /**
66
- * Subscribe to a view and return the visible node list with pagination, navigation, and write operations.
111
+ * Fallback returned by `branchSelection` when the view isn't resolved.
112
+ * Same shape the view returns for an unknown codec-message-id, so callers
113
+ * can destructure uniformly.
114
+ */
115
+ const EMPTY_BRANCH_SELECTION: BranchSelection<never> = {
116
+ hasSiblings: false,
117
+ siblings: [],
118
+ index: 0,
119
+ selected: undefined,
120
+ };
121
+
122
+ /**
123
+ * Subscribe to a view and return the visible messages with pagination, navigation, and write operations.
67
124
  *
68
- * `view` takes priority over `transport`. When neither is provided, the nearest
69
- * {@link TransportProvider}'s transport is used. When `limit` is provided, auto-loads
125
+ * `view` takes priority over `session`. When neither is provided, the nearest
126
+ * {@link ClientSessionProvider}'s session is used. When `limit` is provided, auto-loads
70
127
  * the first page on mount (SWR-style).
71
128
  * @param props - Options for selecting the view source and configuring auto-load.
72
- * @param props.transport - Client transport whose default view to subscribe to; defaults to the nearest provider.
73
- * @param props.view - A specific {@link View} to subscribe to directly. Takes priority over `transport`.
74
- * @param props.limit - Max older messages per page; when provided, auto-loads on mount.
129
+ * @param props.session - Client session whose default view to subscribe to; defaults to the nearest provider.
130
+ * @param props.view - A specific {@link View} to subscribe to directly. Takes priority over `session`.
131
+ * @param props.limit - Max older Runs to reveal per page; when provided, auto-loads the first page on mount.
75
132
  * @param props.skip - When `true`, skip all subscriptions and return an empty handle.
76
- * @returns A {@link ViewHandle} with nodes, pagination state, navigation, write operations, and loadOlder.
133
+ * @returns A {@link ViewHandle} with messages, pagination state, navigation, write operations, and loadOlder.
77
134
  */
78
- export const useView = <TEvent, TMessage>({
79
- transport,
135
+ export const useView = <TInput extends CodecInputEvent, TOutput extends CodecOutputEvent, TProjection, TMessage>({
136
+ session,
80
137
  view,
81
138
  limit,
82
139
  skip,
83
- }: {
84
- /** Client transport whose default view to subscribe to; defaults to the nearest provider when omitted. */
85
- transport?: ClientTransport<TEvent, TMessage> | null;
86
- /** A specific {@link View} to subscribe to directly. Takes priority over `transport`. */
87
- view?: View<TEvent, TMessage> | null;
88
- /** When provided, auto-loads the first page on mount. Omit for manual loading. */
89
- limit?: number;
90
- /** When `true`, skip all subscriptions and return an empty handle immediately. */
91
- skip?: boolean;
92
- } = {}): ViewHandle<TEvent, TMessage> => {
93
- const nearestSlot = useContext(NearestTransportContext);
94
- // CAST: NearestTransportContext stores transport with erased generics; types fixed at call site.
95
- const resolvedTransport = skip
96
- ? undefined
97
- : (transport ?? (nearestSlot?.transport as unknown as ClientTransport<TEvent, TMessage> | undefined));
98
- const resolvedView = skip ? undefined : (view ?? resolvedTransport?.view);
99
-
100
- const [nodes, setNodes] = useState<MessageNode<TMessage>[]>(() => resolvedView?.flattenNodes() ?? []);
140
+ }: UseViewOptions<TInput, TOutput, TProjection, TMessage> = {}): ViewHandle<TInput, TMessage> => {
141
+ const resolvedSession = useResolvedSession({ session, skip });
142
+ const resolvedView = skip ? undefined : (view ?? resolvedSession?.view);
143
+
144
+ const [messages, setMessages] = useState<CodecMessage<TMessage>[]>(() => resolvedView?.getMessages() ?? []);
101
145
  const [hasOlder, setHasOlder] = useState(() => resolvedView?.hasOlder() ?? false);
102
146
  const [loading, setLoading] = useState(false);
103
147
  const [loadError, setLoadError] = useState<Ably.ErrorInfo | undefined>();
@@ -112,7 +156,7 @@ export const useView = <TEvent, TMessage>({
112
156
  // Subscribe to view updates
113
157
  useEffect(() => {
114
158
  if (!resolvedView) {
115
- setNodes([]);
159
+ setMessages([]);
116
160
  setHasOlder(false);
117
161
  setLoadError(undefined);
118
162
  return;
@@ -122,12 +166,12 @@ export const useView = <TEvent, TMessage>({
122
166
  autoLoadedRef.current = false;
123
167
 
124
168
  // Sync initial state
125
- setNodes(resolvedView.flattenNodes());
169
+ setMessages(resolvedView.getMessages());
126
170
  setHasOlder(resolvedView.hasOlder());
127
171
  setLoadError(undefined);
128
172
 
129
173
  const unsub = resolvedView.on('update', () => {
130
- setNodes(resolvedView.flattenNodes());
174
+ setMessages(resolvedView.getMessages());
131
175
  setHasOlder(resolvedView.hasOlder());
132
176
  });
133
177
  return unsub;
@@ -158,30 +202,39 @@ export const useView = <TEvent, TMessage>({
158
202
  void loadOlder();
159
203
  }, [autoLoad, resolvedView, loadOlder]);
160
204
 
161
- const messages = useMemo(() => nodes.map((n) => n.message), [nodes]);
162
-
163
- // Branch navigation callbacks
164
- const select = useCallback(
165
- (msgId: string, index: number) => {
166
- resolvedView?.select(msgId, index);
167
- },
205
+ // Run lookups
206
+ const runOf = useCallback(
207
+ (codecMessageId: string): RunInfo | undefined => resolvedView?.runOf(codecMessageId),
168
208
  [resolvedView],
169
209
  );
170
210
 
171
- const getSelectedIndex = useCallback((msgId: string) => resolvedView?.getSelectedIndex(msgId) ?? 0, [resolvedView]);
211
+ const run = useCallback((runId: string): RunInfo | undefined => resolvedView?.run(runId), [resolvedView]);
172
212
 
173
- const getSiblings = useCallback((msgId: string) => resolvedView?.getSiblings(msgId) ?? [], [resolvedView]);
213
+ const runs = useCallback((): RunInfo[] => resolvedView?.runs() ?? [], [resolvedView]);
174
214
 
175
- const hasSiblings = useCallback((msgId: string) => resolvedView?.hasSiblings(msgId) ?? false, [resolvedView]);
215
+ // Branch navigation
216
+ const branchSelection = useCallback(
217
+ (codecMessageId: string): BranchSelection<TMessage> =>
218
+ // CAST: `EMPTY_BRANCH_SELECTION` is typed `BranchSelection<never>`; `never` is
219
+ // assignable to any `TMessage`, so the empty bundle is a valid fallback for
220
+ // the not-yet-resolved view case.
221
+ resolvedView?.branchSelection(codecMessageId) ?? (EMPTY_BRANCH_SELECTION as BranchSelection<TMessage>),
222
+ [resolvedView],
223
+ );
176
224
 
177
- const getNode = useCallback((msgId: string) => resolvedView?.getNode(msgId), [resolvedView]);
225
+ const selectSibling = useCallback(
226
+ (codecMessageId: string, index: number) => {
227
+ resolvedView?.selectSibling(codecMessageId, index);
228
+ },
229
+ [resolvedView],
230
+ );
178
231
 
179
232
  // Write operation callbacks
180
233
  const send = useCallback(
181
- async (msgs: TMessage | TMessage[], opts?: SendOptions) => {
234
+ async (events: TInput | TInput[], opts?: SendOptions) => {
182
235
  if (!resolvedView)
183
236
  throw new Ably.ErrorInfo('unable to send; view is not available', ErrorCode.InvalidArgument, 400);
184
- return resolvedView.send(msgs, opts);
237
+ return resolvedView.send(events, opts);
185
238
  },
186
239
  [resolvedView],
187
240
  );
@@ -196,38 +249,27 @@ export const useView = <TEvent, TMessage>({
196
249
  );
197
250
 
198
251
  const edit = useCallback(
199
- async (messageId: string, newMessages: TMessage | TMessage[], opts?: SendOptions) => {
252
+ async (messageId: string, inputs: TInput | TInput[], opts?: SendOptions) => {
200
253
  if (!resolvedView)
201
254
  throw new Ably.ErrorInfo('unable to edit; view is not available', ErrorCode.InvalidArgument, 400);
202
- return resolvedView.edit(messageId, newMessages, opts);
203
- },
204
- [resolvedView],
205
- );
206
-
207
- const update = useCallback(
208
- async (msgId: string, events: TEvent[], opts?: SendOptions) => {
209
- if (!resolvedView)
210
- throw new Ably.ErrorInfo('unable to update; view is not available', ErrorCode.InvalidArgument, 400);
211
- return resolvedView.update(msgId, events, opts);
255
+ return resolvedView.edit(messageId, inputs, opts);
212
256
  },
213
257
  [resolvedView],
214
258
  );
215
259
 
216
260
  return {
217
261
  messages,
218
- nodes,
219
262
  hasOlder,
220
263
  loading,
221
264
  loadError,
222
265
  loadOlder,
223
- select,
224
- getSelectedIndex,
225
- getSiblings,
226
- hasSiblings,
227
- getNode,
266
+ runOf,
267
+ run,
268
+ runs,
269
+ branchSelection,
270
+ selectSibling,
228
271
  send,
229
272
  regenerate,
230
273
  edit,
231
- update,
232
274
  };
233
275
  };
package/src/utils.ts CHANGED
@@ -8,24 +8,48 @@
8
8
 
9
9
  import type * as Ably from 'ably';
10
10
 
11
- import { DOMAIN_HEADER_PREFIX } from './constants.js';
12
-
13
11
  /**
14
- * Extract extras.headers from an Ably InboundMessage.
15
- * @param message - The Ably message to extract headers from.
16
- * @returns The headers record, or an empty object if absent.
12
+ * Read one tier of the SDK's `extras.ai` namespace from an Ably message.
13
+ * `extras.ai` is the SDK's reserved corner of the message envelope, split into
14
+ * a `transport` tier (generic transport headers) and a `codec` tier (codec
15
+ * headers). The application's own `extras.headers` is deliberately left
16
+ * untouched.
17
+ * @param message - The Ably message to read from.
18
+ * @param tier - Which `extras.ai` sub-namespace to read.
19
+ * @returns The tier's headers record, or an empty object if absent.
17
20
  */
18
- export const getHeaders = (message: Ably.InboundMessage): Record<string, string> => {
21
+ const getAiTier = (message: Ably.InboundMessage, tier: 'transport' | 'codec'): Record<string, string> => {
19
22
  // CAST: Ably SDK types `extras` as `any`; runtime checks below guard access.
20
23
  const extras = message.extras as unknown;
21
24
  if (!extras || typeof extras !== 'object') return {};
22
- const headers = (extras as { headers?: unknown }).headers;
23
- if (!headers || typeof headers !== 'object') return {};
24
- // CAST: Ably wire protocol guarantees headers is Record<string, string>
25
+ const ai = (extras as { ai?: unknown }).ai;
26
+ if (!ai || typeof ai !== 'object') return {};
27
+ const sub = (ai as Record<string, unknown>)[tier];
28
+ if (!sub || typeof sub !== 'object') return {};
29
+ // CAST: Ably wire protocol guarantees the tier is Record<string, string>
25
30
  // when present, verified by the runtime guards above.
26
- return headers as Record<string, string>;
31
+ return sub as Record<string, string>;
27
32
  };
28
33
 
34
+ /**
35
+ * Extract the transport-tier headers (`extras.ai.transport`) from an Ably
36
+ * InboundMessage. These are the generic transport headers (run/stream/identity/
37
+ * branching), set and read by the transport layer.
38
+ * @param message - The Ably message to extract headers from.
39
+ * @returns The transport headers record, or an empty object if absent.
40
+ */
41
+ export const getTransportHeaders = (message: Ably.InboundMessage): Record<string, string> =>
42
+ getAiTier(message, 'transport');
43
+
44
+ /**
45
+ * Extract the codec-tier headers (`extras.ai.codec`) from an Ably
46
+ * InboundMessage. These are the codec's own headers, with no prefix — the
47
+ * tier isolates them from transport headers.
48
+ * @param message - The Ably message to extract headers from.
49
+ * @returns The codec headers record, or an empty object if absent.
50
+ */
51
+ export const getCodecHeaders = (message: Ably.InboundMessage): Record<string, string> => getAiTier(message, 'codec');
52
+
29
53
  /**
30
54
  * Parse a JSON string, returning undefined on failure.
31
55
  * @param value - The JSON string to parse.
@@ -58,18 +82,6 @@ export const setIfPresent = (headers: Record<string, string>, key: string, value
58
82
  }
59
83
  };
60
84
 
61
- /**
62
- * Set multiple headers at once, skipping entries whose values are undefined or null.
63
- * Each value is converted using the same rules as {@link setIfPresent}.
64
- * @param headers - The headers object to mutate.
65
- * @param entries - Key-value pairs to set.
66
- */
67
- export const setHeadersIfPresent = (headers: Record<string, string>, entries: Record<string, unknown>): void => {
68
- for (const [key, value] of Object.entries(entries)) {
69
- setIfPresent(headers, key, value);
70
- }
71
- };
72
-
73
85
  /**
74
86
  * Merge two header records into a new object. Later values override earlier ones.
75
87
  * Undefined inputs are treated as empty.
@@ -95,30 +107,36 @@ export const parseBool = (value: string | undefined): boolean | undefined => {
95
107
  return value === 'true';
96
108
  };
97
109
 
110
+ /** A record carrying an optional Ably `serial`, orderable by {@link compareBySerial}. */
111
+ interface HasSerial {
112
+ /** Ably serial, or undefined if the server has not yet assigned one. */
113
+ readonly serial?: string;
114
+ }
115
+
98
116
  /**
99
- * Build a domain headers record from key-value pairs. Each key is automatically
100
- * prefixed with {@link DOMAIN_HEADER_PREFIX}. Values that are undefined or null
101
- * are skipped; strings are set directly; booleans, numbers, and objects are
102
- * converted using the same rules as {@link setIfPresent}.
103
- * @param entries - Unprefixed key-value pairs (e.g. `{ toolCallId: 'tc-1' }` becomes `{ 'x-domain-toolCallId': 'tc-1' }`).
104
- * @returns A new headers record with prefixed keys.
117
+ * Comparator that orders records by their Ably `serial` ascending
118
+ * (chronological). Serials are lexicographically comparable; records whose
119
+ * serial is undefined sort last. Pass directly to `Array.prototype.sort`.
120
+ * @param a - First record to compare.
121
+ * @param b - Second record to compare.
122
+ * @returns Negative if `a` precedes `b`, positive if `a` follows `b`, 0 if equal.
105
123
  */
106
- export const domainHeaders = (entries: Record<string, unknown>): Record<string, string> => {
107
- const h: Record<string, string> = {};
108
- for (const [key, value] of Object.entries(entries)) {
109
- setIfPresent(h, DOMAIN_HEADER_PREFIX + key, value);
110
- }
111
- return h;
124
+ export const compareBySerial = (a: HasSerial, b: HasSerial): number => {
125
+ if (a.serial === undefined && b.serial === undefined) return 0;
126
+ if (a.serial === undefined) return 1;
127
+ if (b.serial === undefined) return -1;
128
+ if (a.serial < b.serial) return -1;
129
+ if (a.serial > b.serial) return 1;
130
+ return 0;
112
131
  };
113
132
 
114
133
  /**
115
- * Read a domain header value from a headers record.
116
- * @param headers - The headers record to read from.
117
- * @param key - The unprefixed domain key (e.g. `'toolCallId'` reads `'x-domain-toolCallId'`).
134
+ * Read a domain header value from a codec-tier headers record.
135
+ * @param headers - The codec headers record to read from.
136
+ * @param key - The domain key (e.g. `'toolCallId'`).
118
137
  * @returns The header value, or undefined if absent.
119
138
  */
120
- export const getDomainHeader = (headers: Record<string, string>, key: string): string | undefined =>
121
- headers[DOMAIN_HEADER_PREFIX + key];
139
+ export const getDomainHeader = (headers: Record<string, string>, key: string): string | undefined => headers[key];
122
140
 
123
141
  /**
124
142
  * Mapped type that converts properties whose type includes `undefined`
@@ -167,7 +185,7 @@ export interface DomainHeaderReader {
167
185
  str(key: string): string | undefined;
168
186
  /** Read a domain header as a string, falling back to a default if absent. */
169
187
  strOr(key: string, fallback: string): string;
170
- /** Read a domain header as a boolean ("true"/"false"), or undefined if absent. */
188
+ /** Read a domain header as a boolean: `true` only for the exact string "true", `false` for any other present value, or undefined if absent. */
171
189
  bool(key: string): boolean | undefined;
172
190
  /** Read a domain header as parsed JSON, or undefined if absent or invalid. */
173
191
  json(key: string): unknown;
@@ -206,22 +224,22 @@ export interface DomainHeaderWriter {
206
224
  }
207
225
 
208
226
  /**
209
- * Create a {@link DomainHeaderWriter} for building a domain headers record.
210
- * @returns A fluent builder that prefixes each key with the domain header prefix.
227
+ * Create a {@link DomainHeaderWriter} for building a codec-tier headers record.
228
+ * @returns A fluent builder that accumulates codec headers under their bare keys.
211
229
  */
212
230
  export const headerWriter = (): DomainHeaderWriter => {
213
231
  const h: Record<string, string> = {};
214
232
  const writer: DomainHeaderWriter = {
215
233
  str: (key: string, value: string | undefined) => {
216
- if (value !== undefined) h[DOMAIN_HEADER_PREFIX + key] = value;
234
+ if (value !== undefined) h[key] = value;
217
235
  return writer;
218
236
  },
219
237
  bool: (key: string, value: boolean | undefined) => {
220
- if (value !== undefined) h[DOMAIN_HEADER_PREFIX + key] = String(value);
238
+ if (value !== undefined) h[key] = String(value);
221
239
  return writer;
222
240
  },
223
241
  json: (key: string, value: unknown) => {
224
- if (value !== undefined && value !== null) h[DOMAIN_HEADER_PREFIX + key] = JSON.stringify(value);
242
+ if (value !== undefined && value !== null) h[key] = JSON.stringify(value);
225
243
  return writer;
226
244
  },
227
245
  build: () => h,