@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,21 +1,16 @@
1
1
  /**
2
- * useMessageSync: wires transport message lifecycle events into useChat's setMessages.
2
+ * useMessageSync: wire view updates into useChat's setMessages.
3
3
  *
4
- * Subscribes to the transport view's 'update' event and replaces messages state
5
- * with the view's authoritative message list.
4
+ * During active own-run streams, setMessages is gated to avoid an
5
+ * ID-mismatch in useChat's write(). When the stream ends, the gate
6
+ * opens and the view is synced into useChat's overlay.
6
7
  *
7
- * When a ChatTransport is provided (resolved from the nearest ChatTransportProvider),
8
- * setMessages calls are gated during active own-turn streams. This prevents the
9
- * push/replace ID mismatch in useChat's write() function. When the stream finishes,
10
- * the gate opens and an immediate sync fires to pick up any observer messages that
11
- * arrived during the stream.
12
- *
13
- * All dependencies are resolved from the nearest ChatTransportProvider via
14
- * useChatTransport(). Pass channelName to select a specific provider; omit to use
15
- * the nearest. Pass skip: true to pause all subscriptions.
16
- *
17
- * Returns the unsubscribe function in the useEffect cleanup so handlers
18
- * are removed on unmount or when dependencies change.
8
+ * The sync is a per-message merge, not a replace: when the overlay has
9
+ * resolved a client-side tool locally (via addToolResult) but the
10
+ * tree's echo hasn't landed yet, the overlay's resolution wins.
11
+ * Without that, the gate-open sync would race the AI SDK's post-stream
12
+ * sendAutomaticallyWhen check and could clobber the resolution before
13
+ * the continuation publishes.
19
14
  */
20
15
 
21
16
  import type * as AI from 'ai';
@@ -26,47 +21,88 @@ import { useChatTransport } from './use-chat-transport.js';
26
21
  /** Options for {@link useMessageSync}. */
27
22
  export interface UseMessageSyncOptions {
28
23
  /**
29
- * The `setMessages` updater function from `useChat()`. Required.
30
- * Called with a function that replaces the previous message list with the
31
- * transport's current authoritative message list.
24
+ * The `setMessages` updater function from `useChat()`. Called with an
25
+ * updater that returns the next overlay.
32
26
  */
33
27
  setMessages: (updater: (prev: AI.UIMessage[]) => AI.UIMessage[]) => void;
34
28
  /**
35
29
  * Channel name of the {@link ChatTransportProvider} to observe.
36
- * Omit to use the nearest provider in the tree.
30
+ * Omit to use the nearest provider.
37
31
  */
38
32
  channelName?: string;
39
- /**
40
- * When `true`, skip all subscriptions and do nothing.
41
- * Use when the hook's dependencies are not yet resolved (e.g. auth pending).
42
- */
33
+ /** When `true`, skip all subscriptions. */
43
34
  skip?: boolean;
44
35
  }
45
36
 
37
+ // ---------------------------------------------------------------------------
38
+ // Tool-resolution merge
39
+ // ---------------------------------------------------------------------------
40
+ //
41
+ // The Vercel codec normalises every tool part to `dynamic-tool`, but the
42
+ // AI SDK emits `tool-${name}` for statically-declared tools. Both shapes
43
+ // share `toolCallId` + `state`; the merge matches by toolCallId and keeps
44
+ // the tree's `type` on the result so downstream consumers narrowing on
45
+ // `dynamic-tool` keep working.
46
+
47
+ type ToolPart = AI.DynamicToolUIPart | AI.ToolUIPart;
48
+
49
+ const RESOLVED_TOOL_STATES = new Set(['output-available', 'output-error', 'approval-responded', 'output-denied']);
50
+
51
+ const isToolPart = (part: AI.UIMessage['parts'][number]): part is ToolPart =>
52
+ (part.type === 'dynamic-tool' || part.type.startsWith('tool-')) && 'toolCallId' in part && 'state' in part;
53
+
54
+ const mergeAssistant = (tree: AI.UIMessage, overlay: AI.UIMessage): AI.UIMessage => {
55
+ const overlayByCallId = new Map<string, ToolPart>();
56
+ for (const part of overlay.parts) {
57
+ if (isToolPart(part)) overlayByCallId.set(part.toolCallId, part);
58
+ }
59
+ if (overlayByCallId.size === 0) return tree;
60
+
61
+ const parts = tree.parts.map((part) => {
62
+ if (!isToolPart(part)) return part;
63
+ if (RESOLVED_TOOL_STATES.has(part.state)) return part;
64
+ const overlayPart = overlayByCallId.get(part.toolCallId);
65
+ if (!overlayPart || !RESOLVED_TOOL_STATES.has(overlayPart.state)) return part;
66
+ // CAST: tool-${name} and dynamic-tool share the discriminated payload schema.
67
+ return { ...overlayPart, type: part.type } as AI.UIMessage['parts'][number];
68
+ });
69
+
70
+ const changed = parts.some((p, i) => p !== tree.parts[i]);
71
+ return changed ? { ...tree, parts } : tree;
72
+ };
73
+
74
+ const mergeMessages = (tree: AI.UIMessage[], overlay: AI.UIMessage[]): AI.UIMessage[] => {
75
+ if (overlay.length === 0) return tree;
76
+ const overlayById = new Map(overlay.map((m) => [m.id, m]));
77
+ return tree.map((treeMsg) => {
78
+ if (treeMsg.role !== 'assistant') return treeMsg;
79
+ const overlayMsg = overlayById.get(treeMsg.id);
80
+ return overlayMsg ? mergeAssistant(treeMsg, overlayMsg) : treeMsg;
81
+ });
82
+ };
83
+
84
+ // ---------------------------------------------------------------------------
85
+ // Hook
86
+ // ---------------------------------------------------------------------------
87
+
46
88
  /**
47
- * Wire transport message updates into `useChat()`'s `setMessages` updater.
48
- *
49
- * Resolves both the transport view and the streaming gate from the nearest
50
- * `ChatTransportProvider`. Pass `channelName` to target a specific provider.
51
- * Pass `skip: true` to pause all subscriptions.
89
+ * Subscribe to view updates and sync them into `useChat()`'s overlay.
52
90
  * @param options - Hook options.
53
- * @param options.setMessages - The `setMessages` function from `useChat()`. Required.
54
- * @param options.channelName - Channel name of the provider to observe; defaults to nearest.
91
+ * @param options.setMessages - The `setMessages` function from `useChat()`.
92
+ * @param options.channelName - Channel name of the provider to observe; defaults to the nearest.
55
93
  * @param options.skip - When `true`, skip all subscriptions.
56
94
  */
57
95
  export const useMessageSync = ({ setMessages, channelName, skip }: UseMessageSyncOptions): void => {
58
- const { transport, chatTransport, chatTransportError } = useChatTransport({ channelName, skip });
96
+ const { session, chatTransport, chatTransportError } = useChatTransport({ channelName, skip });
59
97
 
60
- // Only use resolved values when a provider was found and skip is false.
61
98
  const resolved = !skip && !chatTransportError;
62
- const view = resolved ? transport.view : undefined;
99
+ const view = resolved ? session.view : undefined;
63
100
  const resolvedChatTransport = resolved ? chatTransport : undefined;
64
101
 
65
102
  const [gated, setGated] = useState(false);
66
103
 
67
- // Subscribe to the ChatTransport's streaming state to gate setMessages.
68
- // Reset gated to the new instance's current state so a stale `true`
69
- // from a previous instance doesn't permanently suppress syncs.
104
+ // Subscribe to the ChatTransport's streaming state. Reset on transport
105
+ // change so a stale `true` doesn't permanently suppress syncs.
70
106
  useEffect(() => {
71
107
  if (!resolvedChatTransport) {
72
108
  setGated(false);
@@ -76,15 +112,20 @@ export const useMessageSync = ({ setMessages, channelName, skip }: UseMessageSyn
76
112
  return resolvedChatTransport.onStreamingChange(setGated);
77
113
  }, [resolvedChatTransport]);
78
114
 
79
- // Subscribe to view updates and sync messages, unless gated.
115
+ // Subscribe to view updates and sync, unless gated.
80
116
  useEffect(() => {
81
117
  if (!view || gated) return;
82
118
 
83
119
  const sync = (): void => {
84
- setMessages(() => view.flattenNodes().map((n) => n.message));
120
+ setMessages((overlay) =>
121
+ mergeMessages(
122
+ view.getMessages().map((m) => m.message),
123
+ overlay,
124
+ ),
125
+ );
85
126
  };
86
127
 
87
- // Sync immediately when the effect runs (covers gate-open and initial mount).
128
+ // Sync immediately to cover gate-open and initial mount.
88
129
  sync();
89
130
 
90
131
  return view.on('update', sync);
@@ -0,0 +1,78 @@
1
+ import type * as AI from 'ai';
2
+
3
+ import type { RunEndReason, StreamResult } from '../core/transport/types.js';
4
+
5
+ /**
6
+ * Derive the outcome for a Vercel `streamText` response that was piped through
7
+ * `Run.pipe`: either a terminal {@link RunEndReason} the caller passes to
8
+ * `Run.end`, or the sentinel `'suspend'` telling the caller to call
9
+ * `Run.suspend` instead. Preserves transport-level outcomes (`'cancelled'`,
10
+ * `'error'`) from the pipe result; when the pipe completed naturally, awaits
11
+ * Vercel's `finishReason` and returns `'suspend'` for `'tool-calls'` (the LLM
12
+ * requested tools the SDK did not auto-execute, so the run should suspend
13
+ * rather than end), or `'complete'` otherwise.
14
+ *
15
+ * Tolerates `finishReason` rejection. Vercel AI SDK v6 rejects
16
+ * `streamText().finishReason` with the abort signal's reason when the stream
17
+ * is aborted before any step completes, and rejects with
18
+ * `NoOutputGeneratedError` when the model produced nothing at all. Without
19
+ * this guard the rejection would bubble out of the route handler's `after()`
20
+ * block, skip the developer's `Run.end(...)` call, and leave the run with no
21
+ * `ai-run-end` event on the channel — so observers' UIs stay stuck on
22
+ * `streaming` indefinitely.
23
+ *
24
+ * Saves callers from interpreting Vercel domain semantics inline at the end
25
+ * of every route handler.
26
+ * @param pipeResult - The result returned by `Run.pipe`.
27
+ * @param finishReason - The `finishReason` promise from a `streamText` result.
28
+ * @returns `'suspend'` when the run should suspend awaiting tool input, or the
29
+ * {@link RunEndReason} to pass to `Run.end` otherwise.
30
+ */
31
+ export const vercelRunOutcome = async (
32
+ pipeResult: StreamResult,
33
+ finishReason: PromiseLike<AI.FinishReason>,
34
+ ): Promise<RunEndReason | 'suspend'> => {
35
+ if (pipeResult.reason !== 'complete') {
36
+ // Vercel's `result.finishReason` getter creates the underlying Promise
37
+ // eagerly, before the caller hands it to us. When `streamText` is
38
+ // aborted before any step completes, Vercel rejects that Promise with
39
+ // the abort signal's reason — typically a DOMException whose
40
+ // `.message` is a read-only getter. Returning early without ever
41
+ // attaching a handler lets Node report it as an unhandled rejection;
42
+ // Next.js' dev bundler then tries to mutate `.message` for logging
43
+ // and crashes with a confusing TypeError. Attach a silent handler so
44
+ // the rejection is observed and discarded — the transport-level
45
+ // `pipeResult.reason` is already what we return.
46
+ Promise.resolve(finishReason).catch(() => {
47
+ /* intentionally discarded; reason already known from pipeResult */
48
+ });
49
+ return pipeResult.reason;
50
+ }
51
+ try {
52
+ const finish = await finishReason;
53
+ return finish === 'tool-calls' ? 'suspend' : 'complete';
54
+ } catch (error) {
55
+ // Abort-shaped rejections are surfaced from streamText when the run was
56
+ // cancelled before any step finished — treat the run as cancelled so the
57
+ // observable lifecycle matches the cancel that triggered it. Everything
58
+ // else is a real error (e.g. NoOutputGeneratedError, network blow-ups);
59
+ // surface it as such so the developer sees the failure rather than a
60
+ // silent cancel.
61
+ return _isAbortLikeError(error) ? 'cancelled' : 'error';
62
+ }
63
+ };
64
+
65
+ /**
66
+ * Heuristic for "this error came from an AbortSignal aborting".
67
+ * Covers `DOMException` aborts (browser / Node 20+ `streamText`),
68
+ * plain `Error` objects whose `name` is `'AbortError'`, and anything
69
+ * else carrying that conventional name. Avoids importing
70
+ * `@ai-sdk/provider-utils` just for `isAbortError`.
71
+ * @param error - The error to test.
72
+ * @returns `true` if the error looks like an abort.
73
+ */
74
+ const _isAbortLikeError = (error: unknown): boolean => {
75
+ if (typeof error !== 'object' || error === null) return false;
76
+ const name = (error as { name?: unknown }).name;
77
+ return name === 'AbortError';
78
+ };