@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.
- package/README.md +54 -47
- package/dist/ably-ai-transport.js +1006 -539
- package/dist/ably-ai-transport.js.map +1 -1
- package/dist/ably-ai-transport.umd.cjs +1 -1
- package/dist/ably-ai-transport.umd.cjs.map +1 -1
- package/dist/constants.d.ts +4 -0
- package/dist/core/codec/types.d.ts +19 -2
- package/dist/core/transport/decode-history.d.ts +8 -6
- package/dist/core/transport/headers.d.ts +4 -2
- package/dist/core/transport/index.d.ts +4 -1
- package/dist/core/transport/pipe-stream.d.ts +3 -2
- package/dist/core/transport/stream-router.d.ts +11 -1
- package/dist/core/transport/tree.d.ts +171 -0
- package/dist/core/transport/turn-manager.d.ts +4 -1
- package/dist/core/transport/types.d.ts +270 -119
- package/dist/core/transport/view.d.ts +166 -0
- package/dist/errors.d.ts +19 -2
- package/dist/index.d.ts +3 -1
- package/dist/react/ably-ai-transport-react.js +1019 -486
- package/dist/react/ably-ai-transport-react.js.map +1 -1
- package/dist/react/ably-ai-transport-react.umd.cjs +1 -1
- package/dist/react/ably-ai-transport-react.umd.cjs.map +1 -1
- package/dist/react/contexts/transport-context.d.ts +31 -0
- package/dist/react/contexts/transport-provider.d.ts +49 -0
- package/dist/react/create-transport-hooks.d.ts +124 -0
- package/dist/react/index.d.ts +14 -8
- package/dist/react/use-ably-messages.d.ts +14 -8
- package/dist/react/use-active-turns.d.ts +7 -3
- package/dist/react/use-client-transport.d.ts +78 -5
- package/dist/react/use-create-view.d.ts +22 -0
- package/dist/react/use-tree.d.ts +20 -0
- package/dist/react/use-view.d.ts +79 -0
- package/dist/vercel/ably-ai-transport-vercel.js +1478 -842
- package/dist/vercel/ably-ai-transport-vercel.js.map +1 -1
- package/dist/vercel/ably-ai-transport-vercel.umd.cjs +1 -1
- package/dist/vercel/ably-ai-transport-vercel.umd.cjs.map +1 -1
- package/dist/vercel/codec/tool-transitions.d.ts +50 -0
- package/dist/vercel/index.d.ts +3 -0
- package/dist/vercel/react/ably-ai-transport-vercel-react.js +9099 -852
- package/dist/vercel/react/ably-ai-transport-vercel-react.js.map +1 -1
- package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs +45 -1
- package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs.map +1 -1
- package/dist/vercel/react/contexts/chat-transport-context.d.ts +32 -0
- package/dist/vercel/react/contexts/chat-transport-provider.d.ts +84 -0
- package/dist/vercel/react/index.d.ts +5 -0
- package/dist/vercel/react/use-chat-transport.d.ts +61 -20
- package/dist/vercel/react/use-message-sync.d.ts +41 -9
- package/dist/vercel/react/use-staged-add-tool-approval-response.d.ts +30 -0
- package/dist/vercel/tool-approvals.d.ts +124 -0
- package/dist/vercel/tool-events.d.ts +26 -0
- package/dist/vercel/transport/chat-transport.d.ts +33 -11
- package/dist/vercel/transport/index.d.ts +5 -2
- package/package.json +23 -17
- package/src/constants.ts +6 -0
- package/src/core/codec/encoder.ts +10 -1
- package/src/core/codec/types.ts +19 -3
- package/src/core/transport/client-transport.ts +382 -364
- package/src/core/transport/decode-history.ts +229 -81
- package/src/core/transport/headers.ts +6 -2
- package/src/core/transport/index.ts +13 -5
- package/src/core/transport/pipe-stream.ts +8 -5
- package/src/core/transport/server-transport.ts +212 -58
- package/src/core/transport/stream-router.ts +21 -3
- package/src/core/transport/{conversation-tree.ts → tree.ts} +192 -77
- package/src/core/transport/turn-manager.ts +28 -10
- package/src/core/transport/types.ts +318 -139
- package/src/core/transport/view.ts +840 -0
- package/src/errors.ts +21 -1
- package/src/index.ts +10 -5
- package/src/react/contexts/transport-context.ts +37 -0
- package/src/react/contexts/transport-provider.tsx +164 -0
- package/src/react/create-transport-hooks.ts +144 -0
- package/src/react/index.ts +15 -8
- package/src/react/use-ably-messages.ts +34 -16
- package/src/react/use-active-turns.ts +28 -17
- package/src/react/use-client-transport.ts +184 -24
- package/src/react/use-create-view.ts +68 -0
- package/src/react/use-tree.ts +53 -0
- package/src/react/use-view.ts +233 -0
- package/src/react/vite.config.ts +4 -1
- package/src/vercel/codec/accumulator.ts +64 -79
- package/src/vercel/codec/decoder.ts +11 -8
- package/src/vercel/codec/encoder.ts +68 -54
- package/src/vercel/codec/index.ts +0 -2
- package/src/vercel/codec/tool-transitions.ts +122 -0
- package/src/vercel/index.ts +17 -0
- package/src/vercel/react/contexts/chat-transport-context.ts +40 -0
- package/src/vercel/react/contexts/chat-transport-provider.tsx +122 -0
- package/src/vercel/react/index.ts +14 -0
- package/src/vercel/react/use-chat-transport.ts +164 -42
- package/src/vercel/react/use-message-sync.ts +77 -19
- package/src/vercel/react/use-staged-add-tool-approval-response.ts +87 -0
- package/src/vercel/react/vite.config.ts +4 -2
- package/src/vercel/tool-approvals.ts +380 -0
- package/src/vercel/tool-events.ts +53 -0
- package/src/vercel/transport/chat-transport.ts +225 -79
- package/src/vercel/transport/index.ts +14 -3
- package/dist/core/transport/conversation-tree.d.ts +0 -9
- package/dist/react/use-conversation-tree.d.ts +0 -20
- package/dist/react/use-edit.d.ts +0 -7
- package/dist/react/use-history.d.ts +0 -19
- package/dist/react/use-messages.d.ts +0 -7
- package/dist/react/use-regenerate.d.ts +0 -7
- package/dist/react/use-send.d.ts +0 -7
- package/src/react/use-conversation-tree.ts +0 -71
- package/src/react/use-edit.ts +0 -24
- package/src/react/use-history.ts +0 -111
- package/src/react/use-messages.ts +0 -32
- package/src/react/use-regenerate.ts +0 -24
- 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:
|
|
3
|
-
*
|
|
2
|
+
* useChatTransport: reads a ChatTransport and its underlying ClientTransport from
|
|
3
|
+
* the nearest ChatTransportProvider.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
-
*
|
|
10
|
-
* (e.g.
|
|
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 {
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
*
|
|
121
|
+
* Access a {@link ChatTransport} and {@link ClientTransport} from the nearest {@link ChatTransportProvider}.
|
|
37
122
|
*
|
|
38
|
-
*
|
|
39
|
-
* `
|
|
40
|
-
* @
|
|
41
|
-
*
|
|
42
|
-
* @
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
|
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 '
|
|
5
|
-
* with the
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
21
|
-
*
|
|
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
|
|
25
|
-
|
|
26
|
-
|
|
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 (!
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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', '
|
|
22
|
+
external: ['ably', 'ably/react', 'react', 'react/jsx-runtime', 'react/jsx-dev-runtime'],
|
|
23
23
|
output: {
|
|
24
24
|
globals: {
|
|
25
25
|
ably: 'Ably',
|
|
26
|
-
|
|
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
|
},
|