@ably/ai-transport 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (221) hide show
  1. package/README.md +93 -111
  2. package/dist/ably-ai-transport.js +2401 -1387
  3. package/dist/ably-ai-transport.js.map +1 -1
  4. package/dist/ably-ai-transport.umd.cjs +1 -1
  5. package/dist/ably-ai-transport.umd.cjs.map +1 -1
  6. package/dist/constants.d.ts +116 -42
  7. package/dist/core/agent.d.ts +44 -0
  8. package/dist/core/channel-options.d.ts +57 -0
  9. package/dist/core/codec/codec-event.d.ts +9 -0
  10. package/dist/core/codec/decoder.d.ts +24 -24
  11. package/dist/core/codec/define-codec.d.ts +100 -0
  12. package/dist/core/codec/encoder.d.ts +10 -12
  13. package/dist/core/codec/field-bag.d.ts +85 -0
  14. package/dist/core/codec/fields.d.ts +141 -0
  15. package/dist/core/codec/index.d.ts +8 -2
  16. package/dist/core/codec/input-descriptor-decoder.d.ts +19 -0
  17. package/dist/core/codec/input-descriptor-encoder.d.ts +22 -0
  18. package/dist/core/codec/input-descriptors.d.ts +281 -0
  19. package/dist/core/codec/lifecycle-tracker.d.ts +10 -9
  20. package/dist/core/codec/output-descriptor-decoder.d.ts +29 -0
  21. package/dist/core/codec/output-descriptor-encoder.d.ts +31 -0
  22. package/dist/core/codec/output-descriptors.d.ts +237 -0
  23. package/dist/core/codec/types.d.ts +470 -119
  24. package/dist/core/codec/well-known-inputs.d.ts +52 -0
  25. package/dist/core/transport/agent-session.d.ts +10 -0
  26. package/dist/core/transport/agent-view.d.ts +296 -0
  27. package/dist/core/transport/client-session.d.ts +13 -0
  28. package/dist/core/transport/decode-fold.d.ts +55 -0
  29. package/dist/core/transport/headers.d.ts +121 -14
  30. package/dist/core/transport/index.d.ts +5 -6
  31. package/dist/core/transport/internal/bounded-map.d.ts +20 -0
  32. package/dist/core/transport/invocation.d.ts +74 -0
  33. package/dist/core/transport/load-history-pages.d.ts +71 -0
  34. package/dist/core/transport/load-history.d.ts +44 -0
  35. package/dist/core/transport/pipe-stream.d.ts +9 -9
  36. package/dist/core/transport/run-manager.d.ts +76 -0
  37. package/dist/core/transport/session-support.d.ts +55 -0
  38. package/dist/core/transport/tree.d.ts +523 -109
  39. package/dist/core/transport/types/agent.d.ts +375 -0
  40. package/dist/core/transport/types/client.d.ts +201 -0
  41. package/dist/core/transport/types/shared.d.ts +24 -0
  42. package/dist/core/transport/types/tree.d.ts +357 -0
  43. package/dist/core/transport/types/view.d.ts +249 -0
  44. package/dist/core/transport/types.d.ts +13 -553
  45. package/dist/core/transport/view.d.ts +390 -84
  46. package/dist/core/transport/wire-log.d.ts +102 -0
  47. package/dist/errors.d.ts +27 -10
  48. package/dist/index.d.ts +8 -9
  49. package/dist/logger.d.ts +12 -0
  50. package/dist/react/ably-ai-transport-react.js +1365 -1010
  51. package/dist/react/ably-ai-transport-react.js.map +1 -1
  52. package/dist/react/ably-ai-transport-react.umd.cjs +1 -1
  53. package/dist/react/ably-ai-transport-react.umd.cjs.map +1 -1
  54. package/dist/react/contexts/client-session-context.d.ts +37 -0
  55. package/dist/react/contexts/client-session-provider.d.ts +56 -0
  56. package/dist/react/create-session-hooks.d.ts +116 -0
  57. package/dist/react/index.d.ts +13 -12
  58. package/dist/react/internal/skipped-session.d.ts +8 -0
  59. package/dist/react/internal/use-resolved-session.d.ts +36 -0
  60. package/dist/react/use-ably-messages.d.ts +17 -14
  61. package/dist/react/use-client-session.d.ts +81 -0
  62. package/dist/react/use-create-view.d.ts +14 -13
  63. package/dist/react/use-tree.d.ts +30 -15
  64. package/dist/react/use-view.d.ts +81 -50
  65. package/dist/utils.d.ts +48 -71
  66. package/dist/vercel/ably-ai-transport-vercel.js +3257 -2499
  67. package/dist/vercel/ably-ai-transport-vercel.js.map +1 -1
  68. package/dist/vercel/ably-ai-transport-vercel.umd.cjs +1 -1
  69. package/dist/vercel/ably-ai-transport-vercel.umd.cjs.map +1 -1
  70. package/dist/vercel/codec/decode-lifecycle.d.ts +9 -0
  71. package/dist/vercel/codec/events.d.ts +50 -0
  72. package/dist/vercel/codec/fields.d.ts +44 -0
  73. package/dist/vercel/codec/fold-content.d.ts +16 -0
  74. package/dist/vercel/codec/fold-data.d.ts +16 -0
  75. package/dist/vercel/codec/fold-input.d.ts +67 -0
  76. package/dist/vercel/codec/fold-lifecycle.d.ts +16 -0
  77. package/dist/vercel/codec/fold-text.d.ts +16 -0
  78. package/dist/vercel/codec/fold-tool-input.d.ts +17 -0
  79. package/dist/vercel/codec/fold-tool-output.d.ts +16 -0
  80. package/dist/vercel/codec/index.d.ts +7 -20
  81. package/dist/vercel/codec/inputs.d.ts +11 -0
  82. package/dist/vercel/codec/outputs.d.ts +11 -0
  83. package/dist/vercel/codec/reducer-state.d.ts +121 -0
  84. package/dist/vercel/codec/reducer.d.ts +62 -0
  85. package/dist/vercel/codec/tool-transitions.d.ts +2 -8
  86. package/dist/vercel/codec/wire-data.d.ts +34 -0
  87. package/dist/vercel/index.d.ts +5 -5
  88. package/dist/vercel/react/ably-ai-transport-vercel-react.js +2859 -9705
  89. package/dist/vercel/react/ably-ai-transport-vercel-react.js.map +1 -1
  90. package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs +1 -45
  91. package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs.map +1 -1
  92. package/dist/vercel/react/contexts/chat-transport-context.d.ts +9 -7
  93. package/dist/vercel/react/contexts/chat-transport-provider.d.ts +53 -41
  94. package/dist/vercel/react/index.d.ts +1 -2
  95. package/dist/vercel/react/use-chat-transport.d.ts +30 -26
  96. package/dist/vercel/react/use-message-sync.d.ts +17 -30
  97. package/dist/vercel/run-end-reason.d.ts +84 -0
  98. package/dist/vercel/tool-part.d.ts +21 -0
  99. package/dist/vercel/transport/chat-transport.d.ts +41 -24
  100. package/dist/vercel/transport/index.d.ts +24 -20
  101. package/dist/vercel/transport/run-output-stream.d.ts +54 -0
  102. package/dist/version.d.ts +2 -0
  103. package/package.json +31 -24
  104. package/src/constants.ts +124 -51
  105. package/src/core/agent.ts +92 -0
  106. package/src/core/channel-options.ts +89 -0
  107. package/src/core/codec/codec-event.ts +27 -0
  108. package/src/core/codec/decoder.ts +202 -105
  109. package/src/core/codec/define-codec.ts +432 -0
  110. package/src/core/codec/encoder.ts +114 -107
  111. package/src/core/codec/field-bag.ts +142 -0
  112. package/src/core/codec/fields.ts +193 -0
  113. package/src/core/codec/index.ts +56 -6
  114. package/src/core/codec/input-descriptor-decoder.ts +97 -0
  115. package/src/core/codec/input-descriptor-encoder.ts +150 -0
  116. package/src/core/codec/input-descriptors.ts +373 -0
  117. package/src/core/codec/lifecycle-tracker.ts +10 -9
  118. package/src/core/codec/output-descriptor-decoder.ts +139 -0
  119. package/src/core/codec/output-descriptor-encoder.ts +101 -0
  120. package/src/core/codec/output-descriptors.ts +307 -0
  121. package/src/core/codec/types.ts +505 -126
  122. package/src/core/codec/well-known-inputs.ts +96 -0
  123. package/src/core/transport/agent-session.ts +1085 -0
  124. package/src/core/transport/agent-view.ts +738 -0
  125. package/src/core/transport/client-session.ts +780 -0
  126. package/src/core/transport/decode-fold.ts +101 -0
  127. package/src/core/transport/headers.ts +234 -22
  128. package/src/core/transport/index.ts +27 -27
  129. package/src/core/transport/internal/bounded-map.ts +27 -0
  130. package/src/core/transport/invocation.ts +98 -0
  131. package/src/core/transport/load-history-pages.ts +220 -0
  132. package/src/core/transport/load-history.ts +271 -0
  133. package/src/core/transport/pipe-stream.ts +63 -39
  134. package/src/core/transport/run-manager.ts +243 -0
  135. package/src/core/transport/session-support.ts +96 -0
  136. package/src/core/transport/tree.ts +1293 -308
  137. package/src/core/transport/types/agent.ts +434 -0
  138. package/src/core/transport/types/client.ts +247 -0
  139. package/src/core/transport/types/shared.ts +27 -0
  140. package/src/core/transport/types/tree.ts +393 -0
  141. package/src/core/transport/types/view.ts +288 -0
  142. package/src/core/transport/types.ts +13 -706
  143. package/src/core/transport/view.ts +1229 -450
  144. package/src/core/transport/wire-log.ts +189 -0
  145. package/src/errors.ts +29 -9
  146. package/src/event-emitter.ts +3 -2
  147. package/src/index.ts +86 -42
  148. package/src/logger.ts +14 -1
  149. package/src/react/contexts/client-session-context.ts +41 -0
  150. package/src/react/contexts/client-session-provider.tsx +222 -0
  151. package/src/react/create-session-hooks.ts +141 -0
  152. package/src/react/index.ts +24 -13
  153. package/src/react/internal/skipped-session.ts +62 -0
  154. package/src/react/internal/use-resolved-session.ts +63 -0
  155. package/src/react/use-ably-messages.ts +32 -22
  156. package/src/react/use-client-session.ts +178 -0
  157. package/src/react/use-create-view.ts +33 -29
  158. package/src/react/use-tree.ts +61 -30
  159. package/src/react/use-view.ts +138 -96
  160. package/src/utils.ts +83 -131
  161. package/src/vercel/codec/decode-lifecycle.ts +70 -0
  162. package/src/vercel/codec/events.ts +85 -0
  163. package/src/vercel/codec/fields.ts +58 -0
  164. package/src/vercel/codec/fold-content.ts +54 -0
  165. package/src/vercel/codec/fold-data.ts +46 -0
  166. package/src/vercel/codec/fold-input.ts +255 -0
  167. package/src/vercel/codec/fold-lifecycle.ts +85 -0
  168. package/src/vercel/codec/fold-text.ts +55 -0
  169. package/src/vercel/codec/fold-tool-input.ts +86 -0
  170. package/src/vercel/codec/fold-tool-output.ts +79 -0
  171. package/src/vercel/codec/index.ts +28 -21
  172. package/src/vercel/codec/inputs.ts +116 -0
  173. package/src/vercel/codec/outputs.ts +207 -0
  174. package/src/vercel/codec/reducer-state.ts +169 -0
  175. package/src/vercel/codec/reducer.ts +191 -0
  176. package/src/vercel/codec/tool-transitions.ts +3 -14
  177. package/src/vercel/codec/wire-data.ts +64 -0
  178. package/src/vercel/index.ts +7 -19
  179. package/src/vercel/react/contexts/chat-transport-context.ts +8 -7
  180. package/src/vercel/react/contexts/chat-transport-provider.tsx +87 -59
  181. package/src/vercel/react/index.ts +3 -5
  182. package/src/vercel/react/use-chat-transport.ts +44 -66
  183. package/src/vercel/react/use-message-sync.ts +75 -39
  184. package/src/vercel/run-end-reason.ts +157 -0
  185. package/src/vercel/tool-part.ts +25 -0
  186. package/src/vercel/transport/chat-transport.ts +380 -98
  187. package/src/vercel/transport/index.ts +38 -37
  188. package/src/vercel/transport/run-output-stream.ts +169 -0
  189. package/src/version.ts +2 -0
  190. package/dist/core/transport/client-transport.d.ts +0 -10
  191. package/dist/core/transport/decode-history.d.ts +0 -43
  192. package/dist/core/transport/server-transport.d.ts +0 -7
  193. package/dist/core/transport/stream-router.d.ts +0 -29
  194. package/dist/core/transport/turn-manager.d.ts +0 -37
  195. package/dist/react/contexts/transport-context.d.ts +0 -31
  196. package/dist/react/contexts/transport-provider.d.ts +0 -49
  197. package/dist/react/create-transport-hooks.d.ts +0 -124
  198. package/dist/react/use-active-turns.d.ts +0 -12
  199. package/dist/react/use-client-transport.d.ts +0 -80
  200. package/dist/vercel/codec/accumulator.d.ts +0 -21
  201. package/dist/vercel/codec/decoder.d.ts +0 -22
  202. package/dist/vercel/codec/encoder.d.ts +0 -41
  203. package/dist/vercel/react/use-staged-add-tool-approval-response.d.ts +0 -30
  204. package/dist/vercel/tool-approvals.d.ts +0 -124
  205. package/dist/vercel/tool-events.d.ts +0 -26
  206. package/src/core/transport/client-transport.ts +0 -977
  207. package/src/core/transport/decode-history.ts +0 -485
  208. package/src/core/transport/server-transport.ts +0 -612
  209. package/src/core/transport/stream-router.ts +0 -136
  210. package/src/core/transport/turn-manager.ts +0 -165
  211. package/src/react/contexts/transport-context.ts +0 -37
  212. package/src/react/contexts/transport-provider.tsx +0 -164
  213. package/src/react/create-transport-hooks.ts +0 -144
  214. package/src/react/use-active-turns.ts +0 -72
  215. package/src/react/use-client-transport.ts +0 -197
  216. package/src/vercel/codec/accumulator.ts +0 -588
  217. package/src/vercel/codec/decoder.ts +0 -618
  218. package/src/vercel/codec/encoder.ts +0 -410
  219. package/src/vercel/react/use-staged-add-tool-approval-response.ts +0 -87
  220. package/src/vercel/tool-approvals.ts +0 -380
  221. package/src/vercel/tool-events.ts +0 -53
@@ -1,92 +1,100 @@
1
1
  /**
2
- * ChatTransportProvider: creates a ChatTransport from a ClientTransport and makes it
2
+ * ChatTransportProvider: creates a ChatTransport from a ClientSession and makes it
3
3
  * available to descendants via ChatTransportContext.
4
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).
5
+ * Wraps children with ClientSessionProvider (using UIMessageCodec). The
6
+ * surrounding `<AblyProvider>` supplies the Realtime client; the session
7
+ * resolves the channel from `channelName` itself. An inner component reads
8
+ * the ClientSession via useClientSession() and creates the ChatTransport
9
+ * via useMemo, keyed on the session and transport options.
9
10
  *
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.
11
+ * The ChatTransport is NOT closed on unmount — the underlying ClientSession
12
+ * lifecycle is managed by the wrapping ClientSessionProvider. Auto-closing would break
13
+ * React Strict Mode, and ChatTransport.close() delegates to ClientSession.close()
14
+ * which ClientSessionProvider already calls.
14
15
  *
15
16
  * 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 }).
17
+ * Each provider merges its session into the parent registry, so descendants
18
+ * can access all registered sessions via useChatTransport({ channelName }).
18
19
  */
19
20
 
20
21
  import type * as AI from 'ai';
21
22
  import { type PropsWithChildren, type ReactNode, useContext, useMemo } from 'react';
22
23
 
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';
24
+ import { type ClientSessionProviderProps, createSessionHooks } from '../../../react/index.js';
25
+ import { UIMessageCodec, type VercelInput, type VercelOutput, type VercelProjection } from '../../codec/index.js';
26
+ import type { ChatTransportOptions } from '../../transport/index.js';
26
27
  import { createChatTransport } from '../../transport/index.js';
27
28
  import type { ChatTransportSlot } from './chat-transport-context.js';
28
29
  import { ChatTransportContext } from './chat-transport-context.js';
29
30
 
30
- export const {
31
- TransportProvider,
32
- useAblyMessages,
33
- useActiveTurns,
34
- useClientTransport,
35
- useCreateView,
36
- useTree,
37
- useView,
38
- } = createTransportHooks<AI.UIMessageChunk, AI.UIMessage>();
31
+ export const { ClientSessionProvider, useAblyMessages, useClientSession, useCreateView, useTree, useView } =
32
+ createSessionHooks<VercelInput, VercelOutput, VercelProjection, AI.UIMessage>();
39
33
 
40
- type CoreTransportProviderProps = Omit<TransportProviderProps<AI.UIMessageChunk, AI.UIMessage>, 'codec' | 'api'> &
41
- Partial<Pick<TransportProviderProps<AI.UIMessageChunk, AI.UIMessage>, 'api'>>;
34
+ type CoreClientSessionProviderProps = Omit<
35
+ ClientSessionProviderProps<VercelInput, VercelOutput, VercelProjection, AI.UIMessage>,
36
+ 'codec'
37
+ >;
42
38
 
43
39
  /**
44
40
  * Props for {@link ChatTransportProvider}.
45
41
  *
46
- * All {@link TransportProviderProps} for Vercel types except `codec` (baked as UIMessageCodec),
47
- * plus `chatOptions` for customizing chat request construction.
42
+ * All {@link ClientSessionProviderProps} for Vercel types except `codec` (baked as UIMessageCodec),
43
+ * plus the transport-owned invocation POST options (`api` / `credentials` / `fetch`) and
44
+ * `chatOptions` for customizing chat request construction.
48
45
  */
49
- export interface ChatTransportProviderProps extends CoreTransportProviderProps {
46
+ export interface ChatTransportProviderProps extends CoreClientSessionProviderProps {
47
+ /** Endpoint the chat transport POSTs the invocation to, to wake the agent. Default `/api/chat`. */
48
+ api?: string;
49
+ /** Fetch credentials mode for the invocation POST. */
50
+ credentials?: RequestCredentials;
51
+ /** Custom fetch implementation for the invocation POST. Defaults to `globalThis.fetch`. */
52
+ fetch?: typeof globalThis.fetch;
50
53
  /**
51
- * Optional hooks for customizing chat request construction (e.g. prepareSendMessagesRequest).
54
+ * Optional transport options for customizing chat request construction (e.g. the `prepareSendMessagesRequest` hook).
52
55
  * Must be stable across renders — wrap in `useMemo` or define outside the component.
53
56
  * A new object reference triggers ChatTransport recreation.
57
+ * If this object also sets `api`/`credentials`/`fetch`, the dedicated top-level props of the same name take precedence.
54
58
  */
55
59
  chatOptions?: ChatTransportOptions;
56
60
  }
57
61
 
58
62
  const ChatTransportProviderInner = ({
59
63
  channelName,
60
- chatOptions,
64
+ chatTransportOptions,
61
65
  children,
62
66
  }: {
63
67
  channelName: string;
64
- chatOptions?: ChatTransportOptions;
68
+ chatTransportOptions: ChatTransportOptions;
65
69
  children: ReactNode;
66
70
  }) => {
67
- const { transport, transportError } = useClientTransport();
71
+ const { session, sessionError } = useClientSession();
68
72
  const { providers: parentProviders } = useContext(ChatTransportContext);
69
- const chatTransport = useMemo(() => createChatTransport(transport, chatOptions), [transport, chatOptions]);
73
+ const chatTransport = useMemo(
74
+ () => createChatTransport(session, chatTransportOptions),
75
+ [session, chatTransportOptions],
76
+ );
70
77
  const contextValue = useMemo(() => {
71
- const slot: ChatTransportSlot = { transport, transportError, chatTransport };
78
+ const slot: ChatTransportSlot = { session, sessionError, chatTransport };
72
79
  return {
73
80
  nearest: slot,
74
81
  providers: { ...parentProviders, [channelName]: slot },
75
82
  };
76
- }, [channelName, parentProviders, chatTransport, transport, transportError]);
83
+ }, [channelName, parentProviders, chatTransport, session, sessionError]);
77
84
 
78
85
  return <ChatTransportContext.Provider value={contextValue}>{children}</ChatTransportContext.Provider>;
79
86
  };
80
87
 
81
88
  /**
82
- * Provide a {@link ChatTransport} and its underlying {@link ClientTransport} to descendant components.
89
+ * Provide a {@link ChatTransport} and its underlying {@link ClientSession} to descendant components.
83
90
  *
84
- * Wraps children with Ably's `ChannelProvider` (via `TransportProvider`) using `channelName`,
85
- * creates a {@link ClientTransport} with UIMessageCodec, wraps it in a {@link ChatTransport},
91
+ * Wraps children with `ClientSessionProvider` using `channelName` (the Realtime
92
+ * client is read from the surrounding `<AblyProvider>`), creates a
93
+ * {@link ClientSession} with UIMessageCodec, wraps it in a {@link ChatTransport},
86
94
  * and registers the full slot in `ChatTransportContext` under `channelName`. Descendants call
87
- * {@link useChatTransport} with the same `channelName` to access both transports.
95
+ * {@link useChatTransport} with the same `channelName` to access both.
88
96
  *
89
- * `useClientTransport` is also available inside this provider's subtree.
97
+ * `useClientSession` is also available inside this provider's subtree.
90
98
  *
91
99
  * ```tsx
92
100
  * <ChatTransportProvider channelName="ai:demo">
@@ -94,29 +102,49 @@ const ChatTransportProviderInner = ({
94
102
  * </ChatTransportProvider>
95
103
  *
96
104
  * // Inside Chat:
97
- * const { chatTransport, transport } = useChatTransport();
98
- * const { transport } = useClientTransport(); // also available
105
+ * const { chatTransport, session } = useChatTransport();
106
+ * const { session } = useClientSession(); // also available
99
107
  * ```
100
- * @param props - Provider configuration including `channelName`, optional `chatOptions`, and all other transport options.
108
+ * @param props - Provider configuration including `channelName`, the invocation POST options (`api` / `credentials` / `fetch`), optional `chatOptions`, and all other session options.
109
+ * @param props.api - Endpoint the chat transport POSTs the invocation to. Default `/api/chat`.
110
+ * @param props.credentials - Fetch credentials mode for the invocation POST.
111
+ * @param props.fetch - Custom fetch implementation for the invocation POST.
101
112
  * @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.
113
+ * @param props.children - Descendant components that consume the chat transport via hooks.
114
+ * @returns A React element wrapping children with ClientSessionContext and ChatTransportContext.
104
115
  */
105
116
  export const ChatTransportProvider = ({
117
+ api,
118
+ credentials,
119
+ fetch,
106
120
  chatOptions,
107
121
  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}
122
+ ...sessionProps
123
+ }: ChatTransportProviderProps & PropsWithChildren): ReactNode => {
124
+ // Fold the transport-owned POST options into a single ChatTransportOptions.
125
+ // Memoized so the ChatTransport isn't recreated each render — createChatTransport
126
+ // is keyed on this object's identity.
127
+ const chatTransportOptions = useMemo<ChatTransportOptions>(
128
+ () => ({
129
+ ...chatOptions,
130
+ ...(api !== undefined && { api }),
131
+ ...(credentials !== undefined && { credentials }),
132
+ ...(fetch !== undefined && { fetch }),
133
+ }),
134
+ [api, credentials, fetch, chatOptions],
135
+ );
136
+
137
+ return (
138
+ <ClientSessionProvider
139
+ {...sessionProps}
140
+ codec={UIMessageCodec}
118
141
  >
119
- {children}
120
- </ChatTransportProviderInner>
121
- </TransportProvider>
122
- );
142
+ <ChatTransportProviderInner
143
+ channelName={sessionProps.channelName}
144
+ chatTransportOptions={chatTransportOptions}
145
+ >
146
+ {children}
147
+ </ChatTransportProviderInner>
148
+ </ClientSessionProvider>
149
+ );
150
+ };
@@ -1,12 +1,11 @@
1
- // Vercel-specific React hooks
1
+ // Vercel-specific React entry point: providers, hooks, and their types
2
2
  export type { ChatTransport } from '../transport/chat-transport.js';
3
3
  export type { ChatTransportProviderProps } from './contexts/chat-transport-provider.js';
4
4
  export {
5
5
  ChatTransportProvider,
6
- TransportProvider,
6
+ ClientSessionProvider,
7
7
  useAblyMessages,
8
- useActiveTurns,
9
- useClientTransport,
8
+ useClientSession,
10
9
  useCreateView,
11
10
  useTree,
12
11
  useView,
@@ -15,4 +14,3 @@ export type { ChatTransportHandle, UseChatTransportOptions } from './use-chat-tr
15
14
  export { useChatTransport } from './use-chat-transport.js';
16
15
  export type { UseMessageSyncOptions } from './use-message-sync.js';
17
16
  export { useMessageSync } from './use-message-sync.js';
18
- export { useStagedAddToolApprovalResponse } from './use-staged-add-tool-approval-response.js';
@@ -1,55 +1,28 @@
1
1
  /**
2
- * useChatTransport: reads a ChatTransport and its underlying ClientTransport from
2
+ * useChatTransport: reads a ChatTransport and its underlying ClientSession from
3
3
  * the nearest ChatTransportProvider.
4
4
  *
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.
5
+ * The chat transport is created by ChatTransportProvider, which wraps the subtree
6
+ * with ClientSessionProvider. The Ably Realtime client is read from the
7
+ * surrounding `<AblyProvider>`. This hook is a thin context reader it does
8
+ * not create or manage any session/transport state.
8
9
  *
9
10
  * Pass `channelName` to look up a specific provider by name. Omit to use the nearest
10
11
  * 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.
12
+ * — returns stubs whose properties throw with a descriptive error.
12
13
  */
13
14
 
14
15
  import * as Ably from 'ably';
15
16
  import type * as AI from 'ai';
16
17
  import { useContext } from 'react';
17
18
 
18
- import type { ClientTransport, Tree, View } from '../../core/transport/types.js';
19
+ import type { ClientSession } from '../../core/transport/types.js';
19
20
  import { ErrorCode } from '../../errors.js';
21
+ import { makeSkippedClientSession } from '../../react/internal/skipped-session.js';
22
+ import type { VercelInput, VercelOutput, VercelProjection } from '../codec/index.js';
20
23
  import type { ChatTransport } from '../transport/index.js';
21
24
  import { ChatTransportContext } from './contexts/chat-transport-context.js';
22
25
 
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
26
  const SKIPPED_CHAT_TRANSPORT: ChatTransport = {
54
27
  sendMessages: (): never => {
55
28
  throw new Ably.ErrorInfo('unable to send messages; hook is skipped', ErrorCode.InvalidArgument, 400);
@@ -76,76 +49,81 @@ const SKIPPED_CHAT_TRANSPORT: ChatTransport = {
76
49
  export interface UseChatTransportOptions {
77
50
  /** Channel name to look up; omit to use the nearest {@link ChatTransportProvider}. */
78
51
  channelName?: string;
79
- /** When `true`, return stub transports that throw on any access. */
52
+ /** When `true`, return stubs that throw on any access. */
80
53
  skip?: boolean;
81
54
  }
82
55
 
83
56
  /**
84
57
  * The value returned by {@link useChatTransport}.
85
- * Provides both the underlying {@link ClientTransport} and the {@link ChatTransport}
58
+ * Provides both the underlying {@link ClientSession} and the {@link ChatTransport}
86
59
  * adapter for Vercel's useChat hook.
87
60
  */
88
61
  export interface ChatTransportHandle {
89
62
  /**
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.
63
+ * The underlying client session, also available via {@link useClientSession}.
64
+ * A throwing stub when `skip` is `true`, when no matching {@link ClientSessionProvider}
65
+ * was found in the tree, or when session construction failed. Check `sessionError` before use.
93
66
  */
94
- transport: ClientTransport<AI.UIMessageChunk, AI.UIMessage>;
67
+ session: ClientSession<VercelInput, VercelOutput, VercelProjection, AI.UIMessage>;
95
68
 
96
69
  /**
97
70
  * The chat transport adapter for use with Vercel's `useChat` hook.
98
71
  *
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.
72
+ * A throwing stub when `skip` is `true` or when no matching
73
+ * {@link ChatTransportProvider} was found in the tree. When a provider is found
74
+ * but the underlying {@link ClientSession} failed to construct, this is the real
75
+ * transport and `sessionError` is set instead. Check `chatTransportError` and
76
+ * `sessionError` before use.
103
77
  */
104
78
  chatTransport: ChatTransport;
105
79
 
106
80
  /**
107
- * Set when no matching {@link TransportProvider} was found, when transport
81
+ * Set when no matching {@link ClientSessionProvider} was found, when session
108
82
  * construction failed, and `skip` is `false`.
109
- * `undefined` when the transport resolved successfully or when `skip` is `true`.
83
+ * `undefined` when the session resolved successfully or when `skip` is `true`.
110
84
  */
111
- transportError?: Ably.ErrorInfo | undefined;
85
+ sessionError?: Ably.ErrorInfo | undefined;
112
86
  /**
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`.
87
+ * Set only when no matching {@link ChatTransportProvider} was found and `skip` is
88
+ * `false`.
89
+ * `undefined` when the chat transport resolved successfully (even if session
90
+ * construction failed — see `sessionError`) or when `skip` is `true`.
116
91
  */
117
92
  chatTransportError?: Ably.ErrorInfo | undefined;
118
93
  }
119
94
 
120
95
  /**
121
- * Access a {@link ChatTransport} and {@link ClientTransport} from the nearest {@link ChatTransportProvider}.
96
+ * Access a {@link ChatTransport} and {@link ClientSession} from the nearest {@link ChatTransportProvider}.
122
97
  *
123
98
  * 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
99
+ * When `skip` is `true`, returns stubs whose every property and method throws
125
100
  * an {@link Ably.ErrorInfo} — safe to hold in state before conditions are ready.
126
101
  * When no provider is found, returns stubs with `chatTransportError` set instead of throwing.
127
- * @param props - Options for selecting the transport.
102
+ * @param props - Options for selecting the chat transport.
128
103
  * @param props.channelName - The channel name passed to the enclosing `ChatTransportProvider`. Omit to use the nearest.
129
104
  * @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.
105
+ * @returns The `ChatTransportHandle` containing both the chat transport adapter and the underlying client session.
131
106
  */
132
107
  export const useChatTransport = ({ channelName, skip }: UseChatTransportOptions = {}): ChatTransportHandle => {
133
108
  const { nearest, providers } = useContext(ChatTransportContext);
134
109
 
135
110
  if (skip) {
136
- return { transport: SKIPPED_CLIENT_TRANSPORT, chatTransport: SKIPPED_CHAT_TRANSPORT };
111
+ return {
112
+ session: makeSkippedClientSession<VercelInput, VercelOutput, VercelProjection, AI.UIMessage>(),
113
+ chatTransport: SKIPPED_CHAT_TRANSPORT,
114
+ };
137
115
  }
138
116
 
139
117
  if (channelName !== undefined) {
140
118
  const slot = providers[channelName];
141
119
  if (slot) {
142
- return { transport: slot.transport, chatTransport: slot.chatTransport, transportError: slot.transportError };
120
+ return { session: slot.session, chatTransport: slot.chatTransport, sessionError: slot.sessionError };
143
121
  }
144
122
  return {
145
- transport: SKIPPED_CLIENT_TRANSPORT,
123
+ session: makeSkippedClientSession<VercelInput, VercelOutput, VercelProjection, AI.UIMessage>(),
146
124
  chatTransport: SKIPPED_CHAT_TRANSPORT,
147
- transportError: new Ably.ErrorInfo(
148
- `unable to use client transport; no TransportProvider found for channelName "${channelName}"`,
125
+ sessionError: new Ably.ErrorInfo(
126
+ `unable to use client session; no ClientSessionProvider found for channelName "${channelName}"`,
149
127
  ErrorCode.BadRequest,
150
128
  400,
151
129
  ),
@@ -159,17 +137,17 @@ export const useChatTransport = ({ channelName, skip }: UseChatTransportOptions
159
137
 
160
138
  if (nearest) {
161
139
  return {
162
- transport: nearest.transport,
140
+ session: nearest.session,
163
141
  chatTransport: nearest.chatTransport,
164
- transportError: nearest.transportError,
142
+ sessionError: nearest.sessionError,
165
143
  };
166
144
  }
167
145
 
168
146
  return {
169
- transport: SKIPPED_CLIENT_TRANSPORT,
147
+ session: makeSkippedClientSession<VercelInput, VercelOutput, VercelProjection, AI.UIMessage>(),
170
148
  chatTransport: SKIPPED_CHAT_TRANSPORT,
171
- transportError: new Ably.ErrorInfo(
172
- 'unable to use transport; no TransportProvider found in the tree',
149
+ sessionError: new Ably.ErrorInfo(
150
+ 'unable to use session; no ClientSessionProvider found in the tree',
173
151
  ErrorCode.BadRequest,
174
152
  400,
175
153
  ),
@@ -1,72 +1,103 @@
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';
22
17
  import { useEffect, useState } from 'react';
23
18
 
19
+ import { isToolPart, type ToolPart } from '../tool-part.js';
24
20
  import { useChatTransport } from './use-chat-transport.js';
25
21
 
26
22
  /** Options for {@link useMessageSync}. */
27
23
  export interface UseMessageSyncOptions {
28
24
  /**
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.
25
+ * The `setMessages` updater function from `useChat()`. Called with an
26
+ * updater that returns the next overlay.
32
27
  */
33
28
  setMessages: (updater: (prev: AI.UIMessage[]) => AI.UIMessage[]) => void;
34
29
  /**
35
30
  * Channel name of the {@link ChatTransportProvider} to observe.
36
- * Omit to use the nearest provider in the tree.
31
+ * Omit to use the nearest provider.
37
32
  */
38
33
  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
- */
34
+ /** When `true`, skip all subscriptions. */
43
35
  skip?: boolean;
44
36
  }
45
37
 
38
+ // ---------------------------------------------------------------------------
39
+ // Tool-resolution merge
40
+ // ---------------------------------------------------------------------------
41
+ //
42
+ // The merge matches tool parts by toolCallId (via the shared {@link isToolPart}
43
+ // guard, which accepts both the codec's `dynamic-tool` shape and the AI SDK's
44
+ // `tool-${name}` shape) and keeps the tree's `type` on the result so downstream
45
+ // consumers narrowing on `dynamic-tool` keep working.
46
+
47
+ const RESOLVED_TOOL_STATES = new Set(['output-available', 'output-error', 'approval-responded', 'output-denied']);
48
+
49
+ const mergeAssistant = (tree: AI.UIMessage, overlay: AI.UIMessage): AI.UIMessage => {
50
+ const overlayByCallId = new Map<string, ToolPart>();
51
+ for (const part of overlay.parts) {
52
+ if (isToolPart(part)) overlayByCallId.set(part.toolCallId, part);
53
+ }
54
+ if (overlayByCallId.size === 0) return tree;
55
+
56
+ const parts = tree.parts.map((part) => {
57
+ if (!isToolPart(part)) return part;
58
+ if (RESOLVED_TOOL_STATES.has(part.state)) return part;
59
+ const overlayPart = overlayByCallId.get(part.toolCallId);
60
+ if (!overlayPart || !RESOLVED_TOOL_STATES.has(overlayPart.state)) return part;
61
+ // CAST: tool-${name} and dynamic-tool share the discriminated payload schema.
62
+ return { ...overlayPart, type: part.type } as AI.UIMessage['parts'][number];
63
+ });
64
+
65
+ const changed = parts.some((p, i) => p !== tree.parts[i]);
66
+ return changed ? { ...tree, parts } : tree;
67
+ };
68
+
69
+ const mergeMessages = (tree: AI.UIMessage[], overlay: AI.UIMessage[]): AI.UIMessage[] => {
70
+ if (overlay.length === 0) return tree;
71
+ const overlayById = new Map(overlay.map((m) => [m.id, m]));
72
+ return tree.map((treeMsg) => {
73
+ if (treeMsg.role !== 'assistant') return treeMsg;
74
+ const overlayMsg = overlayById.get(treeMsg.id);
75
+ return overlayMsg ? mergeAssistant(treeMsg, overlayMsg) : treeMsg;
76
+ });
77
+ };
78
+
79
+ // ---------------------------------------------------------------------------
80
+ // Hook
81
+ // ---------------------------------------------------------------------------
82
+
46
83
  /**
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.
84
+ * Subscribe to view updates and sync them into `useChat()`'s overlay.
52
85
  * @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.
86
+ * @param options.setMessages - The `setMessages` function from `useChat()`.
87
+ * @param options.channelName - Channel name of the provider to observe; defaults to the nearest.
55
88
  * @param options.skip - When `true`, skip all subscriptions.
56
89
  */
57
90
  export const useMessageSync = ({ setMessages, channelName, skip }: UseMessageSyncOptions): void => {
58
- const { transport, chatTransport, chatTransportError } = useChatTransport({ channelName, skip });
91
+ const { session, chatTransport, chatTransportError } = useChatTransport({ channelName, skip });
59
92
 
60
- // Only use resolved values when a provider was found and skip is false.
61
93
  const resolved = !skip && !chatTransportError;
62
- const view = resolved ? transport.view : undefined;
94
+ const view = resolved ? session.view : undefined;
63
95
  const resolvedChatTransport = resolved ? chatTransport : undefined;
64
96
 
65
97
  const [gated, setGated] = useState(false);
66
98
 
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.
99
+ // Subscribe to the ChatTransport's streaming state. Reset on transport
100
+ // change so a stale `true` doesn't permanently suppress syncs.
70
101
  useEffect(() => {
71
102
  if (!resolvedChatTransport) {
72
103
  setGated(false);
@@ -76,15 +107,20 @@ export const useMessageSync = ({ setMessages, channelName, skip }: UseMessageSyn
76
107
  return resolvedChatTransport.onStreamingChange(setGated);
77
108
  }, [resolvedChatTransport]);
78
109
 
79
- // Subscribe to view updates and sync messages, unless gated.
110
+ // Subscribe to view updates and sync, unless gated.
80
111
  useEffect(() => {
81
112
  if (!view || gated) return;
82
113
 
83
114
  const sync = (): void => {
84
- setMessages(() => view.flattenNodes().map((n) => n.message));
115
+ setMessages((overlay) =>
116
+ mergeMessages(
117
+ view.getMessages().map((m) => m.message),
118
+ overlay,
119
+ ),
120
+ );
85
121
  };
86
122
 
87
- // Sync immediately when the effect runs (covers gate-open and initial mount).
123
+ // Sync immediately to cover gate-open and initial mount.
88
124
  sync();
89
125
 
90
126
  return view.on('update', sync);