@ably/ai-transport 0.0.1 → 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 (167) hide show
  1. package/README.md +114 -116
  2. package/dist/ably-ai-transport.js +1743 -961
  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 +117 -39
  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 +410 -101
  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 +97 -17
  18. package/dist/core/transport/index.d.ts +5 -3
  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 -8
  24. package/dist/core/transport/run-manager.d.ts +78 -0
  25. package/dist/core/transport/tree.d.ts +435 -0
  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 -402
  32. package/dist/core/transport/view.d.ts +354 -0
  33. package/dist/errors.d.ts +37 -9
  34. package/dist/index.d.ts +6 -6
  35. package/dist/logger.d.ts +12 -0
  36. package/dist/react/ably-ai-transport-react.js +1164 -645
  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 +16 -10
  44. package/dist/react/internal/use-resolved-session.d.ts +36 -0
  45. package/dist/react/use-ably-messages.d.ts +20 -11
  46. package/dist/react/use-client-session.d.ts +81 -0
  47. package/dist/react/use-create-view.d.ts +23 -0
  48. package/dist/react/use-tree.d.ts +35 -0
  49. package/dist/react/use-view.d.ts +110 -0
  50. package/dist/utils.d.ts +32 -23
  51. package/dist/vercel/ably-ai-transport-vercel.js +2748 -1625
  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 +50 -0
  61. package/dist/vercel/index.d.ts +4 -2
  62. package/dist/vercel/react/ably-ai-transport-vercel-react.js +10298 -1410
  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 +70 -1
  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 +33 -0
  67. package/dist/vercel/react/contexts/chat-transport-provider.d.ts +96 -0
  68. package/dist/vercel/react/index.d.ts +4 -0
  69. package/dist/vercel/react/use-chat-transport.d.ts +66 -21
  70. package/dist/vercel/react/use-message-sync.d.ts +31 -12
  71. package/dist/vercel/run-end-reason.d.ts +29 -0
  72. package/dist/vercel/transport/chat-transport.d.ts +71 -30
  73. package/dist/vercel/transport/index.d.ts +25 -18
  74. package/dist/vercel/transport/run-output-stream.d.ts +56 -0
  75. package/dist/version.d.ts +2 -0
  76. package/package.json +47 -34
  77. package/src/constants.ts +126 -47
  78. package/src/core/agent.ts +68 -0
  79. package/src/core/codec/decoder.ts +71 -98
  80. package/src/core/codec/encoder.ts +115 -58
  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 +438 -106
  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 +182 -19
  89. package/src/core/transport/index.ts +29 -22
  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 +58 -40
  95. package/src/core/transport/run-manager.ts +249 -0
  96. package/src/core/transport/tree.ts +1167 -0
  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 -527
  103. package/src/core/transport/view.ts +1271 -0
  104. package/src/errors.ts +42 -9
  105. package/src/event-emitter.ts +3 -2
  106. package/src/index.ts +55 -39
  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 +27 -10
  112. package/src/react/internal/use-resolved-session.ts +63 -0
  113. package/src/react/use-ably-messages.ts +47 -19
  114. package/src/react/use-client-session.ts +201 -0
  115. package/src/react/use-create-view.ts +72 -0
  116. package/src/react/use-tree.ts +84 -0
  117. package/src/react/use-view.ts +275 -0
  118. package/src/react/vite.config.ts +4 -1
  119. package/src/utils.ts +63 -45
  120. package/src/vercel/codec/decoder.ts +336 -255
  121. package/src/vercel/codec/encoder.ts +348 -196
  122. package/src/vercel/codec/events.ts +87 -0
  123. package/src/vercel/codec/index.ts +59 -14
  124. package/src/vercel/codec/reducer.ts +977 -0
  125. package/src/vercel/codec/tool-transitions.ts +122 -0
  126. package/src/vercel/index.ts +7 -3
  127. package/src/vercel/react/contexts/chat-transport-context.ts +41 -0
  128. package/src/vercel/react/contexts/chat-transport-provider.tsx +150 -0
  129. package/src/vercel/react/index.ts +13 -1
  130. package/src/vercel/react/use-chat-transport.ts +162 -42
  131. package/src/vercel/react/use-message-sync.ts +121 -22
  132. package/src/vercel/react/vite.config.ts +4 -2
  133. package/src/vercel/run-end-reason.ts +78 -0
  134. package/src/vercel/transport/chat-transport.ts +553 -113
  135. package/src/vercel/transport/index.ts +40 -28
  136. package/src/vercel/transport/run-output-stream.ts +170 -0
  137. package/src/version.ts +2 -0
  138. package/dist/core/transport/client-transport.d.ts +0 -10
  139. package/dist/core/transport/conversation-tree.d.ts +0 -9
  140. package/dist/core/transport/decode-history.d.ts +0 -41
  141. package/dist/core/transport/server-transport.d.ts +0 -7
  142. package/dist/core/transport/stream-router.d.ts +0 -19
  143. package/dist/core/transport/turn-manager.d.ts +0 -34
  144. package/dist/react/use-active-turns.d.ts +0 -8
  145. package/dist/react/use-client-transport.d.ts +0 -7
  146. package/dist/react/use-conversation-tree.d.ts +0 -20
  147. package/dist/react/use-edit.d.ts +0 -7
  148. package/dist/react/use-history.d.ts +0 -19
  149. package/dist/react/use-messages.d.ts +0 -7
  150. package/dist/react/use-regenerate.d.ts +0 -7
  151. package/dist/react/use-send.d.ts +0 -7
  152. package/dist/vercel/codec/accumulator.d.ts +0 -21
  153. package/src/core/transport/client-transport.ts +0 -959
  154. package/src/core/transport/conversation-tree.ts +0 -434
  155. package/src/core/transport/decode-history.ts +0 -337
  156. package/src/core/transport/server-transport.ts +0 -458
  157. package/src/core/transport/stream-router.ts +0 -118
  158. package/src/core/transport/turn-manager.ts +0 -147
  159. package/src/react/use-active-turns.ts +0 -61
  160. package/src/react/use-client-transport.ts +0 -37
  161. package/src/react/use-conversation-tree.ts +0 -71
  162. package/src/react/use-edit.ts +0 -24
  163. package/src/react/use-history.ts +0 -111
  164. package/src/react/use-messages.ts +0 -32
  165. package/src/react/use-regenerate.ts +0 -24
  166. package/src/react/use-send.ts +0 -25
  167. package/src/vercel/codec/accumulator.ts +0 -603
@@ -1,34 +1,133 @@
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's 'message' event and replaces messages state
5
- * with the transport's authoritative message list. Events fire immediately
6
- * on every store update (including during active streaming), so this hook
7
- * keeps React state in sync in real time.
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.
8
7
  *
9
- * Returns the unsubscribe function in the useEffect cleanup so handlers
10
- * 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.
11
14
  */
12
15
 
13
16
  import type * as AI from 'ai';
14
- import { useEffect } from 'react';
17
+ import { useEffect, useState } from 'react';
15
18
 
16
- import type { ClientTransport } from '../../core/transport/types.js';
19
+ import { useChatTransport } from './use-chat-transport.js';
20
+
21
+ /** Options for {@link useMessageSync}. */
22
+ export interface UseMessageSyncOptions {
23
+ /**
24
+ * The `setMessages` updater function from `useChat()`. Called with an
25
+ * updater that returns the next overlay.
26
+ */
27
+ setMessages: (updater: (prev: AI.UIMessage[]) => AI.UIMessage[]) => void;
28
+ /**
29
+ * Channel name of the {@link ChatTransportProvider} to observe.
30
+ * Omit to use the nearest provider.
31
+ */
32
+ channelName?: string;
33
+ /** When `true`, skip all subscriptions. */
34
+ skip?: boolean;
35
+ }
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
+ // ---------------------------------------------------------------------------
17
87
 
18
88
  /**
19
- * Wire transport message updates into useChat's `setMessages` updater.
20
- * @param transport - The client transport to observe, or null/undefined if not yet available.
21
- * @param setMessages - The `setMessages` updater function from useChat.
89
+ * Subscribe to view updates and sync them into `useChat()`'s overlay.
90
+ * @param options - Hook options.
91
+ * @param options.setMessages - The `setMessages` function from `useChat()`.
92
+ * @param options.channelName - Channel name of the provider to observe; defaults to the nearest.
93
+ * @param options.skip - When `true`, skip all subscriptions.
22
94
  */
23
- export const useMessageSync = (
24
- transport: ClientTransport<unknown, AI.UIMessage> | null | undefined,
25
- setMessages: (updater: (prev: AI.UIMessage[]) => AI.UIMessage[]) => void,
26
- ): void => {
95
+ export const useMessageSync = ({ setMessages, channelName, skip }: UseMessageSyncOptions): void => {
96
+ const { session, chatTransport, chatTransportError } = useChatTransport({ channelName, skip });
97
+
98
+ const resolved = !skip && !chatTransportError;
99
+ const view = resolved ? session.view : undefined;
100
+ const resolvedChatTransport = resolved ? chatTransport : undefined;
101
+
102
+ const [gated, setGated] = useState(false);
103
+
104
+ // Subscribe to the ChatTransport's streaming state. Reset on transport
105
+ // change so a stale `true` doesn't permanently suppress syncs.
27
106
  useEffect(() => {
28
- if (!transport) return;
29
- const unsubscribe = transport.on('message', () => {
30
- setMessages(() => transport.getMessages());
31
- });
32
- return unsubscribe;
33
- }, [transport, setMessages]);
107
+ if (!resolvedChatTransport) {
108
+ setGated(false);
109
+ return;
110
+ }
111
+ setGated(resolvedChatTransport.streaming);
112
+ return resolvedChatTransport.onStreamingChange(setGated);
113
+ }, [resolvedChatTransport]);
114
+
115
+ // Subscribe to view updates and sync, unless gated.
116
+ useEffect(() => {
117
+ if (!view || gated) return;
118
+
119
+ const sync = (): void => {
120
+ setMessages((overlay) =>
121
+ mergeMessages(
122
+ view.getMessages().map((m) => m.message),
123
+ overlay,
124
+ ),
125
+ );
126
+ };
127
+
128
+ // Sync immediately to cover gate-open and initial mount.
129
+ sync();
130
+
131
+ return view.on('update', sync);
132
+ }, [view, setMessages, gated]);
34
133
  };
@@ -19,12 +19,14 @@ export default defineConfig({
19
19
  formats: ['es', 'umd'],
20
20
  },
21
21
  rollupOptions: {
22
- external: ['ably', 'ai', 'react'],
22
+ external: ['ably', 'ably/react', 'react', 'react/jsx-runtime', 'react/jsx-dev-runtime'],
23
23
  output: {
24
24
  globals: {
25
25
  ably: 'Ably',
26
- ai: 'AI',
26
+ 'ably/react': 'AblyReact',
27
27
  react: 'React',
28
+ 'react/jsx-runtime': 'ReactJsxRuntime',
29
+ 'react/jsx-dev-runtime': 'ReactJsxDevRuntime',
28
30
  },
29
31
  },
30
32
  },
@@ -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
+ };