@ably/ai-transport 0.0.1 → 0.1.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 (110) hide show
  1. package/README.md +54 -47
  2. package/dist/ably-ai-transport.js +1006 -539
  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 +4 -0
  7. package/dist/core/codec/types.d.ts +19 -2
  8. package/dist/core/transport/decode-history.d.ts +8 -6
  9. package/dist/core/transport/headers.d.ts +4 -2
  10. package/dist/core/transport/index.d.ts +4 -1
  11. package/dist/core/transport/pipe-stream.d.ts +3 -2
  12. package/dist/core/transport/stream-router.d.ts +11 -1
  13. package/dist/core/transport/tree.d.ts +171 -0
  14. package/dist/core/transport/turn-manager.d.ts +4 -1
  15. package/dist/core/transport/types.d.ts +270 -119
  16. package/dist/core/transport/view.d.ts +166 -0
  17. package/dist/errors.d.ts +19 -2
  18. package/dist/index.d.ts +3 -1
  19. package/dist/react/ably-ai-transport-react.js +1019 -486
  20. package/dist/react/ably-ai-transport-react.js.map +1 -1
  21. package/dist/react/ably-ai-transport-react.umd.cjs +1 -1
  22. package/dist/react/ably-ai-transport-react.umd.cjs.map +1 -1
  23. package/dist/react/contexts/transport-context.d.ts +31 -0
  24. package/dist/react/contexts/transport-provider.d.ts +49 -0
  25. package/dist/react/create-transport-hooks.d.ts +124 -0
  26. package/dist/react/index.d.ts +14 -8
  27. package/dist/react/use-ably-messages.d.ts +14 -8
  28. package/dist/react/use-active-turns.d.ts +7 -3
  29. package/dist/react/use-client-transport.d.ts +78 -5
  30. package/dist/react/use-create-view.d.ts +22 -0
  31. package/dist/react/use-tree.d.ts +20 -0
  32. package/dist/react/use-view.d.ts +79 -0
  33. package/dist/vercel/ably-ai-transport-vercel.js +1478 -842
  34. package/dist/vercel/ably-ai-transport-vercel.js.map +1 -1
  35. package/dist/vercel/ably-ai-transport-vercel.umd.cjs +1 -1
  36. package/dist/vercel/ably-ai-transport-vercel.umd.cjs.map +1 -1
  37. package/dist/vercel/codec/tool-transitions.d.ts +50 -0
  38. package/dist/vercel/index.d.ts +3 -0
  39. package/dist/vercel/react/ably-ai-transport-vercel-react.js +9099 -852
  40. package/dist/vercel/react/ably-ai-transport-vercel-react.js.map +1 -1
  41. package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs +45 -1
  42. package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs.map +1 -1
  43. package/dist/vercel/react/contexts/chat-transport-context.d.ts +32 -0
  44. package/dist/vercel/react/contexts/chat-transport-provider.d.ts +84 -0
  45. package/dist/vercel/react/index.d.ts +5 -0
  46. package/dist/vercel/react/use-chat-transport.d.ts +61 -20
  47. package/dist/vercel/react/use-message-sync.d.ts +41 -9
  48. package/dist/vercel/react/use-staged-add-tool-approval-response.d.ts +30 -0
  49. package/dist/vercel/tool-approvals.d.ts +124 -0
  50. package/dist/vercel/tool-events.d.ts +26 -0
  51. package/dist/vercel/transport/chat-transport.d.ts +33 -11
  52. package/dist/vercel/transport/index.d.ts +5 -2
  53. package/package.json +23 -17
  54. package/src/constants.ts +6 -0
  55. package/src/core/codec/encoder.ts +10 -1
  56. package/src/core/codec/types.ts +19 -3
  57. package/src/core/transport/client-transport.ts +382 -364
  58. package/src/core/transport/decode-history.ts +229 -81
  59. package/src/core/transport/headers.ts +6 -2
  60. package/src/core/transport/index.ts +13 -5
  61. package/src/core/transport/pipe-stream.ts +8 -5
  62. package/src/core/transport/server-transport.ts +212 -58
  63. package/src/core/transport/stream-router.ts +21 -3
  64. package/src/core/transport/{conversation-tree.ts → tree.ts} +192 -77
  65. package/src/core/transport/turn-manager.ts +28 -10
  66. package/src/core/transport/types.ts +318 -139
  67. package/src/core/transport/view.ts +840 -0
  68. package/src/errors.ts +21 -1
  69. package/src/index.ts +10 -5
  70. package/src/react/contexts/transport-context.ts +37 -0
  71. package/src/react/contexts/transport-provider.tsx +164 -0
  72. package/src/react/create-transport-hooks.ts +144 -0
  73. package/src/react/index.ts +15 -8
  74. package/src/react/use-ably-messages.ts +34 -16
  75. package/src/react/use-active-turns.ts +28 -17
  76. package/src/react/use-client-transport.ts +184 -24
  77. package/src/react/use-create-view.ts +68 -0
  78. package/src/react/use-tree.ts +53 -0
  79. package/src/react/use-view.ts +233 -0
  80. package/src/react/vite.config.ts +4 -1
  81. package/src/vercel/codec/accumulator.ts +64 -79
  82. package/src/vercel/codec/decoder.ts +11 -8
  83. package/src/vercel/codec/encoder.ts +68 -54
  84. package/src/vercel/codec/index.ts +0 -2
  85. package/src/vercel/codec/tool-transitions.ts +122 -0
  86. package/src/vercel/index.ts +17 -0
  87. package/src/vercel/react/contexts/chat-transport-context.ts +40 -0
  88. package/src/vercel/react/contexts/chat-transport-provider.tsx +122 -0
  89. package/src/vercel/react/index.ts +14 -0
  90. package/src/vercel/react/use-chat-transport.ts +164 -42
  91. package/src/vercel/react/use-message-sync.ts +77 -19
  92. package/src/vercel/react/use-staged-add-tool-approval-response.ts +87 -0
  93. package/src/vercel/react/vite.config.ts +4 -2
  94. package/src/vercel/tool-approvals.ts +380 -0
  95. package/src/vercel/tool-events.ts +53 -0
  96. package/src/vercel/transport/chat-transport.ts +225 -79
  97. package/src/vercel/transport/index.ts +14 -3
  98. package/dist/core/transport/conversation-tree.d.ts +0 -9
  99. package/dist/react/use-conversation-tree.d.ts +0 -20
  100. package/dist/react/use-edit.d.ts +0 -7
  101. package/dist/react/use-history.d.ts +0 -19
  102. package/dist/react/use-messages.d.ts +0 -7
  103. package/dist/react/use-regenerate.d.ts +0 -7
  104. package/dist/react/use-send.d.ts +0 -7
  105. package/src/react/use-conversation-tree.ts +0 -71
  106. package/src/react/use-edit.ts +0 -24
  107. package/src/react/use-history.ts +0 -111
  108. package/src/react/use-messages.ts +0 -32
  109. package/src/react/use-regenerate.ts +0 -24
  110. package/src/react/use-send.ts +0 -25
@@ -0,0 +1,122 @@
1
+ /**
2
+ * ChatTransportProvider: creates a ChatTransport from a ClientTransport and makes it
3
+ * available to descendants via ChatTransportContext.
4
+ *
5
+ * Wraps children with TransportProvider (using UIMessageCodec) so the Ably channel
6
+ * lifecycle is managed in one place. An inner component reads the ClientTransport
7
+ * from NearestTransportContext and creates the ChatTransport once on first render
8
+ * (via useRef).
9
+ *
10
+ * The ChatTransport is NOT closed on unmount — the underlying ClientTransport
11
+ * lifecycle is managed by the wrapping TransportProvider. Auto-closing would break
12
+ * React Strict Mode, and ChatTransport.close() delegates to ClientTransport.close()
13
+ * which TransportProvider already calls.
14
+ *
15
+ * Multiple ChatTransportProviders can be nested using distinct channelNames.
16
+ * Each provider merges its transport into the parent registry, so descendants
17
+ * can access all registered transports via useChatTransport({ channelName }).
18
+ */
19
+
20
+ import type * as AI from 'ai';
21
+ import { type PropsWithChildren, type ReactNode, useContext, useMemo } from 'react';
22
+
23
+ import { createTransportHooks, type TransportProviderProps } from '../../../react/index.js';
24
+ import { UIMessageCodec } from '../../codec/index.js';
25
+ import { type ChatTransportOptions, DEFAULT_VERCEL_API } from '../../transport/index.js';
26
+ import { createChatTransport } from '../../transport/index.js';
27
+ import type { ChatTransportSlot } from './chat-transport-context.js';
28
+ import { ChatTransportContext } from './chat-transport-context.js';
29
+
30
+ export const {
31
+ TransportProvider,
32
+ useAblyMessages,
33
+ useActiveTurns,
34
+ useClientTransport,
35
+ useCreateView,
36
+ useTree,
37
+ useView,
38
+ } = createTransportHooks<AI.UIMessageChunk, AI.UIMessage>();
39
+
40
+ type CoreTransportProviderProps = Omit<TransportProviderProps<AI.UIMessageChunk, AI.UIMessage>, 'codec' | 'api'> &
41
+ Partial<Pick<TransportProviderProps<AI.UIMessageChunk, AI.UIMessage>, 'api'>>;
42
+
43
+ /**
44
+ * Props for {@link ChatTransportProvider}.
45
+ *
46
+ * All {@link TransportProviderProps} for Vercel types except `codec` (baked as UIMessageCodec),
47
+ * plus `chatOptions` for customizing chat request construction.
48
+ */
49
+ export interface ChatTransportProviderProps extends CoreTransportProviderProps {
50
+ /**
51
+ * Optional hooks for customizing chat request construction (e.g. prepareSendMessagesRequest).
52
+ * Must be stable across renders — wrap in `useMemo` or define outside the component.
53
+ * A new object reference triggers ChatTransport recreation.
54
+ */
55
+ chatOptions?: ChatTransportOptions;
56
+ }
57
+
58
+ const ChatTransportProviderInner = ({
59
+ channelName,
60
+ chatOptions,
61
+ children,
62
+ }: {
63
+ channelName: string;
64
+ chatOptions?: ChatTransportOptions;
65
+ children: ReactNode;
66
+ }) => {
67
+ const { transport, transportError } = useClientTransport();
68
+ const { providers: parentProviders } = useContext(ChatTransportContext);
69
+ const chatTransport = useMemo(() => createChatTransport(transport, chatOptions), [transport, chatOptions]);
70
+ const contextValue = useMemo(() => {
71
+ const slot: ChatTransportSlot = { transport, transportError, chatTransport };
72
+ return {
73
+ nearest: slot,
74
+ providers: { ...parentProviders, [channelName]: slot },
75
+ };
76
+ }, [channelName, parentProviders, chatTransport, transport, transportError]);
77
+
78
+ return <ChatTransportContext.Provider value={contextValue}>{children}</ChatTransportContext.Provider>;
79
+ };
80
+
81
+ /**
82
+ * Provide a {@link ChatTransport} and its underlying {@link ClientTransport} to descendant components.
83
+ *
84
+ * Wraps children with Ably's `ChannelProvider` (via `TransportProvider`) using `channelName`,
85
+ * creates a {@link ClientTransport} with UIMessageCodec, wraps it in a {@link ChatTransport},
86
+ * and registers the full slot in `ChatTransportContext` under `channelName`. Descendants call
87
+ * {@link useChatTransport} with the same `channelName` to access both transports.
88
+ *
89
+ * `useClientTransport` is also available inside this provider's subtree.
90
+ *
91
+ * ```tsx
92
+ * <ChatTransportProvider channelName="ai:demo">
93
+ * <Chat />
94
+ * </ChatTransportProvider>
95
+ *
96
+ * // Inside Chat:
97
+ * const { chatTransport, transport } = useChatTransport();
98
+ * const { transport } = useClientTransport(); // also available
99
+ * ```
100
+ * @param props - Provider configuration including `channelName`, optional `chatOptions`, and all other transport options.
101
+ * @param props.chatOptions - Optional hooks for customizing chat request construction. Must be stable (memoized) — a new reference recreates the ChatTransport.
102
+ * @param props.children - Descendant components that consume the transport via hooks.
103
+ * @returns A React element wrapping children with ChannelProvider, TransportContext, and ChatTransportContext.
104
+ */
105
+ export const ChatTransportProvider = ({
106
+ chatOptions,
107
+ children,
108
+ ...transportProps
109
+ }: ChatTransportProviderProps & PropsWithChildren): ReactNode => (
110
+ <TransportProvider
111
+ {...transportProps}
112
+ api={transportProps.api ?? DEFAULT_VERCEL_API}
113
+ codec={UIMessageCodec}
114
+ >
115
+ <ChatTransportProviderInner
116
+ channelName={transportProps.channelName}
117
+ chatOptions={chatOptions}
118
+ >
119
+ {children}
120
+ </ChatTransportProviderInner>
121
+ </TransportProvider>
122
+ );
@@ -1,4 +1,18 @@
1
1
  // Vercel-specific React hooks
2
2
  export type { ChatTransport } from '../transport/chat-transport.js';
3
+ export type { ChatTransportProviderProps } from './contexts/chat-transport-provider.js';
4
+ export {
5
+ ChatTransportProvider,
6
+ TransportProvider,
7
+ useAblyMessages,
8
+ useActiveTurns,
9
+ useClientTransport,
10
+ useCreateView,
11
+ useTree,
12
+ useView,
13
+ } from './contexts/chat-transport-provider.js';
14
+ export type { ChatTransportHandle, UseChatTransportOptions } from './use-chat-transport.js';
3
15
  export { useChatTransport } from './use-chat-transport.js';
16
+ export type { UseMessageSyncOptions } from './use-message-sync.js';
4
17
  export { useMessageSync } from './use-message-sync.js';
18
+ export { useStagedAddToolApprovalResponse } from './use-staged-add-tool-approval-response.js';
@@ -1,60 +1,182 @@
1
1
  /**
2
- * useChatTransport: wraps a core ClientTransport into the ChatTransport
3
- * shape that Vercel's useChat expects.
2
+ * useChatTransport: reads a ChatTransport and its underlying ClientTransport from
3
+ * the nearest ChatTransportProvider.
4
4
  *
5
- * Accepts either an existing ClientTransport or options to create one:
6
- * - From an existing ClientTransport wraps it directly
7
- * - From options creates a ClientTransport with UIMessageCodec and wraps it
5
+ * The transport is created by ChatTransportProvider, which also wraps the subtree
6
+ * with TransportProvider and Ably's ChannelProvider. This hook is a thin context
7
+ * reader it does not create or manage any transport state.
8
8
  *
9
- * Both forms accept an optional second argument for ChatTransportOptions
10
- * (e.g. prepareSendMessagesRequest for the persistence pattern).
11
- *
12
- * The hook does NOT auto-close the transport on unmount. Channel lifecycle is
13
- * managed by the Ably provider (useChannel). Auto-closing would break React
14
- * Strict Mode. Call chatTransport.close() explicitly if needed.
9
+ * Pass `channelName` to look up a specific provider by name. Omit to use the nearest
10
+ * provider in the tree. Pass `skip: true` to defer (e.g. when auth is not yet resolved)
11
+ * — returns stub transports whose properties throw with a descriptive error.
15
12
  */
16
13
 
14
+ import * as Ably from 'ably';
17
15
  import type * as AI from 'ai';
18
- import { useRef } from 'react';
16
+ import { useContext } from 'react';
17
+
18
+ import type { ClientTransport, Tree, View } from '../../core/transport/types.js';
19
+ import { ErrorCode } from '../../errors.js';
20
+ import type { ChatTransport } from '../transport/index.js';
21
+ import { ChatTransportContext } from './contexts/chat-transport-context.js';
19
22
 
20
- import type { ClientTransport } from '../../core/transport/types.js';
21
- import type { ChatTransport, ChatTransportOptions } from '../transport/chat-transport.js';
22
- import { createChatTransport } from '../transport/chat-transport.js';
23
- import type { VercelClientTransportOptions } from '../transport/index.js';
24
- import { createClientTransport as createCoreClientTransport } from '../transport/index.js';
23
+ const SKIPPED_CLIENT_TRANSPORT: ClientTransport<AI.UIMessageChunk, AI.UIMessage> = {
24
+ get tree(): Tree<AI.UIMessage> {
25
+ throw new Ably.ErrorInfo('unable to access tree; hook is skipped', ErrorCode.InvalidArgument, 400);
26
+ },
27
+ get view(): View<AI.UIMessageChunk, AI.UIMessage> {
28
+ throw new Ably.ErrorInfo('unable to access view; hook is skipped', ErrorCode.InvalidArgument, 400);
29
+ },
30
+ createView: (): View<AI.UIMessageChunk, AI.UIMessage> => {
31
+ throw new Ably.ErrorInfo('unable to create view; hook is skipped', ErrorCode.InvalidArgument, 400);
32
+ },
33
+ cancel: () => {
34
+ throw new Ably.ErrorInfo('unable to cancel; hook is skipped', ErrorCode.InvalidArgument, 400);
35
+ },
36
+ stageEvents: () => {
37
+ throw new Ably.ErrorInfo('unable to stage events; hook is skipped', ErrorCode.InvalidArgument, 400);
38
+ },
39
+ stageMessage: () => {
40
+ throw new Ably.ErrorInfo('unable to stage message; hook is skipped', ErrorCode.InvalidArgument, 400);
41
+ },
42
+ waitForTurn: () => {
43
+ throw new Ably.ErrorInfo('unable to wait for turn; hook is skipped', ErrorCode.InvalidArgument, 400);
44
+ },
45
+ on: () => {
46
+ throw new Ably.ErrorInfo('unable to subscribe; hook is skipped', ErrorCode.InvalidArgument, 400);
47
+ },
48
+ close: () => {
49
+ throw new Ably.ErrorInfo('unable to close; hook is skipped', ErrorCode.InvalidArgument, 400);
50
+ },
51
+ };
52
+
53
+ const SKIPPED_CHAT_TRANSPORT: ChatTransport = {
54
+ sendMessages: (): never => {
55
+ throw new Ably.ErrorInfo('unable to send messages; hook is skipped', ErrorCode.InvalidArgument, 400);
56
+ },
57
+ reconnectToStream: (): never => {
58
+ throw new Ably.ErrorInfo('unable to reconnect to stream; hook is skipped', ErrorCode.InvalidArgument, 400);
59
+ },
60
+ close: (): never => {
61
+ throw new Ably.ErrorInfo('unable to close; hook is skipped', ErrorCode.InvalidArgument, 400);
62
+ },
63
+ get streaming(): never {
64
+ throw new Ably.ErrorInfo('unable to access streaming; hook is skipped', ErrorCode.InvalidArgument, 400);
65
+ },
66
+ onStreamingChange: (): never => {
67
+ throw new Ably.ErrorInfo(
68
+ 'unable to subscribe to streaming changes; hook is skipped',
69
+ ErrorCode.InvalidArgument,
70
+ 400,
71
+ );
72
+ },
73
+ };
74
+
75
+ /** Options for {@link useChatTransport}. */
76
+ export interface UseChatTransportOptions {
77
+ /** Channel name to look up; omit to use the nearest {@link ChatTransportProvider}. */
78
+ channelName?: string;
79
+ /** When `true`, return stub transports that throw on any access. */
80
+ skip?: boolean;
81
+ }
25
82
 
26
83
  /**
27
- * Type guard: distinguish an existing ClientTransport from options.
28
- * @param x - Either a transport instance or options object.
29
- * @returns True if the argument is a ClientTransport instance.
84
+ * The value returned by {@link useChatTransport}.
85
+ * Provides both the underlying {@link ClientTransport} and the {@link ChatTransport}
86
+ * adapter for Vercel's useChat hook.
30
87
  */
31
- const isClientTransport = (
32
- x: ClientTransport<AI.UIMessageChunk, AI.UIMessage> | VercelClientTransportOptions,
33
- ): x is ClientTransport<AI.UIMessageChunk, AI.UIMessage> => 'send' in x && typeof x.send === 'function';
88
+ export interface ChatTransportHandle {
89
+ /**
90
+ * The underlying client transport, also available via {@link useClientTransport}.
91
+ * A throwing stub when `skip` is `true`, when no matching {@link TransportProvider}
92
+ * was found in the tree, or when transport construction failed. Check `transportError` before use.
93
+ */
94
+ transport: ClientTransport<AI.UIMessageChunk, AI.UIMessage>;
95
+
96
+ /**
97
+ * The chat transport adapter for use with Vercel's `useChat` hook.
98
+ *
99
+ * A throwing stub when `skip` is `true`, when no matching
100
+ * {@link ChatTransportProvider} was found in the tree, or when the underlying
101
+ * {@link ClientTransport} construction failed. Check both `chatTransportError`
102
+ * and `transportError` before use.
103
+ */
104
+ chatTransport: ChatTransport;
105
+
106
+ /**
107
+ * Set when no matching {@link TransportProvider} was found, when transport
108
+ * construction failed, and `skip` is `false`.
109
+ * `undefined` when the transport resolved successfully or when `skip` is `true`.
110
+ */
111
+ transportError?: Ably.ErrorInfo | undefined;
112
+ /**
113
+ * Set when no matching {@link ChatTransportProvider} was found or when transport
114
+ * construction failed, and `skip` is `false`.
115
+ * `undefined` when the transport resolved successfully or when `skip` is `true`.
116
+ */
117
+ chatTransportError?: Ably.ErrorInfo | undefined;
118
+ }
34
119
 
35
120
  /**
36
- * Create and memoize a {@link ChatTransport} for Vercel's useChat hook.
121
+ * Access a {@link ChatTransport} and {@link ClientTransport} from the nearest {@link ChatTransportProvider}.
37
122
  *
38
- * Pass an existing `ClientTransport` to wrap it, or pass
39
- * `VercelClientTransportOptions` to create one internally with UIMessageCodec.
40
- * @param transportOrOptions - An existing ClientTransport, or options to create one.
41
- * @param chatOptions - Optional hooks for customizing request construction.
42
- * @returns A {@link ChatTransport} compatible with Vercel's useChat hook.
123
+ * When `channelName` is omitted, the innermost `ChatTransportProvider` in the tree is used.
124
+ * When `skip` is `true`, returns stub transports whose every property and method throws
125
+ * an {@link Ably.ErrorInfo} safe to hold in state before conditions are ready.
126
+ * When no provider is found, returns stubs with `chatTransportError` set instead of throwing.
127
+ * @param props - Options for selecting the transport.
128
+ * @param props.channelName - The channel name passed to the enclosing `ChatTransportProvider`. Omit to use the nearest.
129
+ * @param props.skip - When `true`, return stubs that throw on any access instead of reading from context.
130
+ * @returns The `ChatTransportHandle` containing both the chat transport adapter and the underlying client transport.
43
131
  */
44
- export const useChatTransport = (
45
- transportOrOptions: ClientTransport<AI.UIMessageChunk, AI.UIMessage> | VercelClientTransportOptions,
46
- chatOptions?: ChatTransportOptions,
47
- ): ChatTransport => {
48
- const chatTransportRef = useRef<ChatTransport | null>(null);
49
-
50
- if (chatTransportRef.current === null) {
51
- if (isClientTransport(transportOrOptions)) {
52
- chatTransportRef.current = createChatTransport(transportOrOptions, chatOptions);
53
- } else {
54
- const transport = createCoreClientTransport(transportOrOptions);
55
- chatTransportRef.current = createChatTransport(transport, chatOptions);
132
+ export const useChatTransport = ({ channelName, skip }: UseChatTransportOptions = {}): ChatTransportHandle => {
133
+ const { nearest, providers } = useContext(ChatTransportContext);
134
+
135
+ if (skip) {
136
+ return { transport: SKIPPED_CLIENT_TRANSPORT, chatTransport: SKIPPED_CHAT_TRANSPORT };
137
+ }
138
+
139
+ if (channelName !== undefined) {
140
+ const slot = providers[channelName];
141
+ if (slot) {
142
+ return { transport: slot.transport, chatTransport: slot.chatTransport, transportError: slot.transportError };
56
143
  }
144
+ return {
145
+ transport: SKIPPED_CLIENT_TRANSPORT,
146
+ chatTransport: SKIPPED_CHAT_TRANSPORT,
147
+ transportError: new Ably.ErrorInfo(
148
+ `unable to use client transport; no TransportProvider found for channelName "${channelName}"`,
149
+ ErrorCode.BadRequest,
150
+ 400,
151
+ ),
152
+ chatTransportError: new Ably.ErrorInfo(
153
+ `unable to use chat transport; no ChatTransportProvider found for channelName "${channelName}"`,
154
+ ErrorCode.BadRequest,
155
+ 400,
156
+ ),
157
+ };
158
+ }
159
+
160
+ if (nearest) {
161
+ return {
162
+ transport: nearest.transport,
163
+ chatTransport: nearest.chatTransport,
164
+ transportError: nearest.transportError,
165
+ };
57
166
  }
58
167
 
59
- return chatTransportRef.current;
168
+ return {
169
+ transport: SKIPPED_CLIENT_TRANSPORT,
170
+ chatTransport: SKIPPED_CHAT_TRANSPORT,
171
+ transportError: new Ably.ErrorInfo(
172
+ 'unable to use transport; no TransportProvider found in the tree',
173
+ ErrorCode.BadRequest,
174
+ 400,
175
+ ),
176
+ chatTransportError: new Ably.ErrorInfo(
177
+ 'unable to use chat transport; no ChatTransportProvider found in the tree',
178
+ ErrorCode.BadRequest,
179
+ 400,
180
+ ),
181
+ };
60
182
  };
@@ -1,34 +1,92 @@
1
1
  /**
2
2
  * useMessageSync: wires transport message lifecycle events 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
+ * Subscribes to the transport view's 'update' event and replaces messages state
5
+ * with the view's authoritative message list.
6
+ *
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.
8
16
  *
9
17
  * Returns the unsubscribe function in the useEffect cleanup so handlers
10
18
  * are removed on unmount or when dependencies change.
11
19
  */
12
20
 
13
21
  import type * as AI from 'ai';
14
- import { useEffect } from 'react';
22
+ import { useEffect, useState } from 'react';
23
+
24
+ import { useChatTransport } from './use-chat-transport.js';
15
25
 
16
- import type { ClientTransport } from '../../core/transport/types.js';
26
+ /** Options for {@link useMessageSync}. */
27
+ export interface UseMessageSyncOptions {
28
+ /**
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.
32
+ */
33
+ setMessages: (updater: (prev: AI.UIMessage[]) => AI.UIMessage[]) => void;
34
+ /**
35
+ * Channel name of the {@link ChatTransportProvider} to observe.
36
+ * Omit to use the nearest provider in the tree.
37
+ */
38
+ 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
+ */
43
+ skip?: boolean;
44
+ }
17
45
 
18
46
  /**
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.
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.
52
+ * @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.
55
+ * @param options.skip - When `true`, skip all subscriptions.
22
56
  */
23
- export const useMessageSync = (
24
- transport: ClientTransport<unknown, AI.UIMessage> | null | undefined,
25
- setMessages: (updater: (prev: AI.UIMessage[]) => AI.UIMessage[]) => void,
26
- ): void => {
57
+ export const useMessageSync = ({ setMessages, channelName, skip }: UseMessageSyncOptions): void => {
58
+ const { transport, chatTransport, chatTransportError } = useChatTransport({ channelName, skip });
59
+
60
+ // Only use resolved values when a provider was found and skip is false.
61
+ const resolved = !skip && !chatTransportError;
62
+ const view = resolved ? transport.view : undefined;
63
+ const resolvedChatTransport = resolved ? chatTransport : undefined;
64
+
65
+ const [gated, setGated] = useState(false);
66
+
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.
70
+ useEffect(() => {
71
+ if (!resolvedChatTransport) {
72
+ setGated(false);
73
+ return;
74
+ }
75
+ setGated(resolvedChatTransport.streaming);
76
+ return resolvedChatTransport.onStreamingChange(setGated);
77
+ }, [resolvedChatTransport]);
78
+
79
+ // Subscribe to view updates and sync messages, unless gated.
27
80
  useEffect(() => {
28
- if (!transport) return;
29
- const unsubscribe = transport.on('message', () => {
30
- setMessages(() => transport.getMessages());
31
- });
32
- return unsubscribe;
33
- }, [transport, setMessages]);
81
+ if (!view || gated) return;
82
+
83
+ const sync = (): void => {
84
+ setMessages(() => view.flattenNodes().map((n) => n.message));
85
+ };
86
+
87
+ // Sync immediately when the effect runs (covers gate-open and initial mount).
88
+ sync();
89
+
90
+ return view.on('update', sync);
91
+ }, [view, setMessages, gated]);
34
92
  };
@@ -0,0 +1,87 @@
1
+ /**
2
+ * useStagedAddToolApprovalResponse — wrap useChat's `addToolApprovalResponse`
3
+ * so the approval response is also applied to the transport tree
4
+ * synchronously at click time.
5
+ *
6
+ * Patching the tree at click time eliminates the useChat↔tree divergence
7
+ * the ChatTransport would otherwise have to reconcile via a history
8
+ * overlay, and closes the observer-turn race that could wipe the
9
+ * approval state between `addToolApprovalResponse` and
10
+ * `sendAutomaticallyWhen`'s evaluation.
11
+ *
12
+ * Use this in place of useChat's raw `addToolApprovalResponse` wherever
13
+ * you wire Approve / Deny buttons.
14
+ */
15
+
16
+ import type * as AI from 'ai';
17
+ import type { ChatAddToolApproveResponseFunction } from 'ai';
18
+ import { useCallback } from 'react';
19
+
20
+ import type { ClientTransport } from '../../core/transport/types.js';
21
+
22
+ /**
23
+ * Returns a function with the same signature as useChat's
24
+ * `addToolApprovalResponse`, but additionally applies the approval
25
+ * response to the transport tree via `stageMessage` before delegating.
26
+ *
27
+ * If the tool call identified by `opts.id` isn't found in the tree,
28
+ * the tree update is skipped and the raw function is still called —
29
+ * matches useChat's tolerant behavior for stale approval ids.
30
+ * @param transport - The client transport whose tree to patch.
31
+ * @param addToolApprovalResponse - The raw function from `useChat()`.
32
+ * @returns A drop-in replacement that patches the tree then delegates.
33
+ */
34
+ export const useStagedAddToolApprovalResponse = (
35
+ transport: ClientTransport<AI.UIMessageChunk, AI.UIMessage>,
36
+ addToolApprovalResponse: ChatAddToolApproveResponseFunction,
37
+ ): ChatAddToolApproveResponseFunction =>
38
+ useCallback<ChatAddToolApproveResponseFunction>(
39
+ (opts) => {
40
+ stageApprovalResponseOnTree(transport, opts);
41
+ return addToolApprovalResponse(opts);
42
+ },
43
+ [transport, addToolApprovalResponse],
44
+ );
45
+
46
+ /**
47
+ * Locate the assistant message whose `dynamic-tool` part carries the
48
+ * given `approval.id`, build a patched copy with the part transitioned
49
+ * to `approval-responded`, and stage the patched message on the tree.
50
+ * @param transport - The transport whose tree to patch.
51
+ * @param opts - The approval response being applied.
52
+ * @param opts.id - The approval id matching a dynamic-tool part in the tree.
53
+ * @param opts.approved - Whether the user approved or denied.
54
+ * @param opts.reason - Optional reason accompanying the response.
55
+ */
56
+ const stageApprovalResponseOnTree = (
57
+ transport: ClientTransport<AI.UIMessageChunk, AI.UIMessage>,
58
+ opts: { id: string; approved: boolean; reason?: string },
59
+ ): void => {
60
+ const nodes = transport.view.flattenNodes();
61
+ for (const node of nodes) {
62
+ const partIndex = node.message.parts.findIndex((p) => p.type === 'dynamic-tool' && p.approval?.id === opts.id);
63
+ if (partIndex === -1) continue;
64
+
65
+ // CAST: findIndex predicate above narrows this to a dynamic-tool part
66
+ // with a non-undefined approval.
67
+ const part = node.message.parts[partIndex] as AI.DynamicToolUIPart;
68
+
69
+ // Build the approval-responded variant directly rather than spreading
70
+ // `part`, which TypeScript narrows to whichever source-state variant
71
+ // the union discriminator inferred and then rejects when we change
72
+ // `state` to a variant with different approval/output constraints.
73
+ const patchedPart: AI.DynamicToolUIPart = {
74
+ type: 'dynamic-tool',
75
+ toolName: part.toolName,
76
+ toolCallId: part.toolCallId,
77
+ state: 'approval-responded',
78
+ input: part.input,
79
+ approval: { id: opts.id, approved: opts.approved, reason: opts.reason },
80
+ };
81
+
82
+ const patchedParts = [...node.message.parts];
83
+ patchedParts[partIndex] = patchedPart;
84
+ transport.stageMessage(node.msgId, { ...node.message, parts: patchedParts });
85
+ return;
86
+ }
87
+ };
@@ -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
  },