@ably/ai-transport 0.0.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +114 -116
- package/dist/ably-ai-transport.js +1743 -961
- 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 +117 -39
- package/dist/core/agent.d.ts +29 -0
- package/dist/core/codec/decoder.d.ts +20 -23
- package/dist/core/codec/encoder.d.ts +11 -8
- package/dist/core/codec/index.d.ts +1 -2
- package/dist/core/codec/lifecycle-tracker.d.ts +10 -9
- package/dist/core/codec/types.d.ts +410 -101
- package/dist/core/transport/agent-session.d.ts +10 -0
- package/dist/core/transport/branch-chain.d.ts +43 -0
- package/dist/core/transport/client-session.d.ts +13 -0
- package/dist/core/transport/decode-fold.d.ts +47 -0
- package/dist/core/transport/headers.d.ts +97 -17
- package/dist/core/transport/index.d.ts +5 -3
- package/dist/core/transport/internal/bounded-map.d.ts +20 -0
- package/dist/core/transport/invocation.d.ts +74 -0
- package/dist/core/transport/load-conversation.d.ts +128 -0
- package/dist/core/transport/load-history.d.ts +39 -0
- package/dist/core/transport/pipe-stream.d.ts +9 -8
- package/dist/core/transport/run-manager.d.ts +78 -0
- package/dist/core/transport/tree.d.ts +435 -0
- package/dist/core/transport/types/agent.d.ts +353 -0
- package/dist/core/transport/types/client.d.ts +168 -0
- package/dist/core/transport/types/shared.d.ts +24 -0
- package/dist/core/transport/types/tree.d.ts +315 -0
- package/dist/core/transport/types/view.d.ts +222 -0
- package/dist/core/transport/types.d.ts +13 -402
- package/dist/core/transport/view.d.ts +354 -0
- package/dist/errors.d.ts +37 -9
- package/dist/index.d.ts +6 -6
- package/dist/logger.d.ts +12 -0
- package/dist/react/ably-ai-transport-react.js +1164 -645
- 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/client-session-context.d.ts +36 -0
- package/dist/react/contexts/client-session-provider.d.ts +53 -0
- package/dist/react/create-session-hooks.d.ts +116 -0
- package/dist/react/index.d.ts +16 -10
- package/dist/react/internal/use-resolved-session.d.ts +36 -0
- package/dist/react/use-ably-messages.d.ts +20 -11
- package/dist/react/use-client-session.d.ts +81 -0
- package/dist/react/use-create-view.d.ts +23 -0
- package/dist/react/use-tree.d.ts +35 -0
- package/dist/react/use-view.d.ts +110 -0
- package/dist/utils.d.ts +32 -23
- package/dist/vercel/ably-ai-transport-vercel.js +2748 -1625
- 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/decoder.d.ts +5 -18
- package/dist/vercel/codec/encoder.d.ts +6 -36
- package/dist/vercel/codec/events.d.ts +51 -0
- package/dist/vercel/codec/index.d.ts +24 -12
- package/dist/vercel/codec/reducer.d.ts +144 -0
- package/dist/vercel/codec/tool-transitions.d.ts +50 -0
- package/dist/vercel/index.d.ts +4 -2
- package/dist/vercel/react/ably-ai-transport-vercel-react.js +10298 -1410
- package/dist/vercel/react/ably-ai-transport-vercel-react.js.map +1 -1
- package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs +70 -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 +33 -0
- package/dist/vercel/react/contexts/chat-transport-provider.d.ts +96 -0
- package/dist/vercel/react/index.d.ts +4 -0
- package/dist/vercel/react/use-chat-transport.d.ts +66 -21
- package/dist/vercel/react/use-message-sync.d.ts +31 -12
- package/dist/vercel/run-end-reason.d.ts +29 -0
- package/dist/vercel/transport/chat-transport.d.ts +71 -30
- package/dist/vercel/transport/index.d.ts +25 -18
- package/dist/vercel/transport/run-output-stream.d.ts +56 -0
- package/dist/version.d.ts +2 -0
- package/package.json +47 -34
- package/src/constants.ts +126 -47
- package/src/core/agent.ts +68 -0
- package/src/core/codec/decoder.ts +71 -98
- package/src/core/codec/encoder.ts +115 -58
- package/src/core/codec/index.ts +13 -6
- package/src/core/codec/lifecycle-tracker.ts +10 -9
- package/src/core/codec/types.ts +438 -106
- package/src/core/transport/agent-session.ts +1344 -0
- package/src/core/transport/branch-chain.ts +58 -0
- package/src/core/transport/client-session.ts +775 -0
- package/src/core/transport/decode-fold.ts +91 -0
- package/src/core/transport/headers.ts +182 -19
- package/src/core/transport/index.ts +29 -22
- package/src/core/transport/internal/bounded-map.ts +27 -0
- package/src/core/transport/invocation.ts +98 -0
- package/src/core/transport/load-conversation.ts +355 -0
- package/src/core/transport/load-history.ts +269 -0
- package/src/core/transport/pipe-stream.ts +58 -40
- package/src/core/transport/run-manager.ts +249 -0
- package/src/core/transport/tree.ts +1167 -0
- package/src/core/transport/types/agent.ts +407 -0
- package/src/core/transport/types/client.ts +211 -0
- package/src/core/transport/types/shared.ts +27 -0
- package/src/core/transport/types/tree.ts +344 -0
- package/src/core/transport/types/view.ts +259 -0
- package/src/core/transport/types.ts +13 -527
- package/src/core/transport/view.ts +1271 -0
- package/src/errors.ts +42 -9
- package/src/event-emitter.ts +3 -2
- package/src/index.ts +55 -39
- package/src/logger.ts +14 -1
- package/src/react/contexts/client-session-context.ts +41 -0
- package/src/react/contexts/client-session-provider.tsx +186 -0
- package/src/react/create-session-hooks.ts +141 -0
- package/src/react/index.ts +27 -10
- package/src/react/internal/use-resolved-session.ts +63 -0
- package/src/react/use-ably-messages.ts +47 -19
- package/src/react/use-client-session.ts +201 -0
- package/src/react/use-create-view.ts +72 -0
- package/src/react/use-tree.ts +84 -0
- package/src/react/use-view.ts +275 -0
- package/src/react/vite.config.ts +4 -1
- package/src/utils.ts +63 -45
- package/src/vercel/codec/decoder.ts +336 -255
- package/src/vercel/codec/encoder.ts +348 -196
- package/src/vercel/codec/events.ts +87 -0
- package/src/vercel/codec/index.ts +59 -14
- package/src/vercel/codec/reducer.ts +977 -0
- package/src/vercel/codec/tool-transitions.ts +122 -0
- package/src/vercel/index.ts +7 -3
- package/src/vercel/react/contexts/chat-transport-context.ts +41 -0
- package/src/vercel/react/contexts/chat-transport-provider.tsx +150 -0
- package/src/vercel/react/index.ts +13 -1
- package/src/vercel/react/use-chat-transport.ts +162 -42
- package/src/vercel/react/use-message-sync.ts +121 -22
- package/src/vercel/react/vite.config.ts +4 -2
- package/src/vercel/run-end-reason.ts +78 -0
- package/src/vercel/transport/chat-transport.ts +553 -113
- package/src/vercel/transport/index.ts +40 -28
- package/src/vercel/transport/run-output-stream.ts +170 -0
- package/src/version.ts +2 -0
- package/dist/core/transport/client-transport.d.ts +0 -10
- package/dist/core/transport/conversation-tree.d.ts +0 -9
- package/dist/core/transport/decode-history.d.ts +0 -41
- package/dist/core/transport/server-transport.d.ts +0 -7
- package/dist/core/transport/stream-router.d.ts +0 -19
- package/dist/core/transport/turn-manager.d.ts +0 -34
- package/dist/react/use-active-turns.d.ts +0 -8
- package/dist/react/use-client-transport.d.ts +0 -7
- 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/dist/vercel/codec/accumulator.d.ts +0 -21
- package/src/core/transport/client-transport.ts +0 -959
- package/src/core/transport/conversation-tree.ts +0 -434
- package/src/core/transport/decode-history.ts +0 -337
- package/src/core/transport/server-transport.ts +0 -458
- package/src/core/transport/stream-router.ts +0 -118
- package/src/core/transport/turn-manager.ts +0 -147
- package/src/react/use-active-turns.ts +0 -61
- package/src/react/use-client-transport.ts +0 -37
- 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
- package/src/vercel/codec/accumulator.ts +0 -603
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared tool part transition logic for the Vercel AI SDK codec.
|
|
3
|
+
*
|
|
4
|
+
* Keeps the tool output state transition logic in one place, reusable by the
|
|
5
|
+
* Vercel codec reducer and any other callers.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type * as AI from 'ai';
|
|
9
|
+
|
|
10
|
+
import { stripUndefined } from '../../utils.js';
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Tool output chunk type guard
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
/** The set of UIMessageChunk types that represent tool output transitions. */
|
|
17
|
+
export type ToolOutputChunk = Extract<
|
|
18
|
+
AI.UIMessageChunk,
|
|
19
|
+
{ type: 'tool-output-available' | 'tool-output-error' | 'tool-output-denied' | 'tool-approval-request' }
|
|
20
|
+
>;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Whether a UIMessageChunk is a tool output transition event.
|
|
24
|
+
* @param chunk - The chunk to test.
|
|
25
|
+
* @returns True if the chunk is a tool output transition type.
|
|
26
|
+
*/
|
|
27
|
+
export const isToolOutputChunk = (chunk: AI.UIMessageChunk): chunk is ToolOutputChunk =>
|
|
28
|
+
chunk.type === 'tool-output-available' ||
|
|
29
|
+
chunk.type === 'tool-output-error' ||
|
|
30
|
+
chunk.type === 'tool-output-denied' ||
|
|
31
|
+
chunk.type === 'tool-approval-request';
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Tool base helper
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
/** Fields shared by all DynamicToolUIPart state variants. */
|
|
38
|
+
interface ToolBaseFields {
|
|
39
|
+
type: 'dynamic-tool';
|
|
40
|
+
toolName: string;
|
|
41
|
+
toolCallId: string;
|
|
42
|
+
title?: string;
|
|
43
|
+
providerExecuted?: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Extract the state-independent base fields for a DynamicToolUIPart.
|
|
48
|
+
* Works with both chunks (tool-input-start, etc.) and existing parts.
|
|
49
|
+
* @param source - Any object containing the required tool identity fields.
|
|
50
|
+
* @param source.toolCallId - The tool call identifier.
|
|
51
|
+
* @param source.toolName - The tool name.
|
|
52
|
+
* @param source.title - Optional display title.
|
|
53
|
+
* @param source.providerExecuted - Whether the provider executed the tool.
|
|
54
|
+
* @returns Base fields shared across all DynamicToolUIPart state variants.
|
|
55
|
+
*/
|
|
56
|
+
export const toolBase = (source: {
|
|
57
|
+
toolCallId: string;
|
|
58
|
+
toolName: string;
|
|
59
|
+
title?: string;
|
|
60
|
+
providerExecuted?: boolean;
|
|
61
|
+
}): ToolBaseFields =>
|
|
62
|
+
stripUndefined({
|
|
63
|
+
type: 'dynamic-tool' as const,
|
|
64
|
+
toolCallId: source.toolCallId,
|
|
65
|
+
toolName: source.toolName,
|
|
66
|
+
title: source.title,
|
|
67
|
+
providerExecuted: source.providerExecuted,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
// Tool part transition
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Transition a DynamicToolUIPart to a new state based on a tool output chunk.
|
|
76
|
+
* Pure function — does not mutate the input part.
|
|
77
|
+
* @param part - The existing tool part to transition.
|
|
78
|
+
* @param chunk - The tool output chunk describing the transition.
|
|
79
|
+
* @returns A new DynamicToolUIPart in the target state.
|
|
80
|
+
*/
|
|
81
|
+
export const transitionToolPart = (part: AI.DynamicToolUIPart, chunk: ToolOutputChunk): AI.DynamicToolUIPart => {
|
|
82
|
+
const base = toolBase(part);
|
|
83
|
+
|
|
84
|
+
switch (chunk.type) {
|
|
85
|
+
case 'tool-output-available': {
|
|
86
|
+
return stripUndefined({
|
|
87
|
+
...base,
|
|
88
|
+
state: 'output-available' as const,
|
|
89
|
+
input: part.input,
|
|
90
|
+
output: chunk.output,
|
|
91
|
+
preliminary: chunk.preliminary,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
case 'tool-output-error': {
|
|
96
|
+
return {
|
|
97
|
+
...base,
|
|
98
|
+
state: 'output-error',
|
|
99
|
+
input: part.input,
|
|
100
|
+
errorText: chunk.errorText,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
case 'tool-output-denied': {
|
|
105
|
+
return {
|
|
106
|
+
...base,
|
|
107
|
+
state: 'output-denied',
|
|
108
|
+
input: part.input,
|
|
109
|
+
approval: { id: '', approved: false },
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
case 'tool-approval-request': {
|
|
114
|
+
return {
|
|
115
|
+
...base,
|
|
116
|
+
state: 'approval-requested',
|
|
117
|
+
input: part.input,
|
|
118
|
+
approval: { id: chunk.approvalId },
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
};
|
package/src/vercel/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// Vercel AI SDK codec
|
|
2
|
+
export type { VercelInput, VercelOutput, VercelProjection } from './codec/index.js';
|
|
2
3
|
export { UIMessageCodec } from './codec/index.js';
|
|
3
4
|
|
|
4
5
|
// Vercel AI SDK transport wrappers (pre-bound to UIMessageCodec)
|
|
@@ -6,7 +7,10 @@ export type {
|
|
|
6
7
|
ChatTransport,
|
|
7
8
|
ChatTransportOptions,
|
|
8
9
|
SendMessagesRequestContext,
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
VercelAgentSessionOptions,
|
|
11
|
+
VercelClientSessionOptions,
|
|
11
12
|
} from './transport/index.js';
|
|
12
|
-
export {
|
|
13
|
+
export { createAgentSession, createChatTransport, createClientSession } from './transport/index.js';
|
|
14
|
+
|
|
15
|
+
// Vercel-shaped helpers
|
|
16
|
+
export { vercelRunOutcome } from './run-end-reason.js';
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type * as Ably from 'ably';
|
|
2
|
+
import type * as AI from 'ai';
|
|
3
|
+
import { createContext } from 'react';
|
|
4
|
+
|
|
5
|
+
import type { ClientSession } from '../../../core/transport/types.js';
|
|
6
|
+
import type { VercelInput, VercelOutput, VercelProjection } from '../../codec/index.js';
|
|
7
|
+
import type { ChatTransport } from '../../transport/chat-transport.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A single entry in the chat transport registry, holding both the
|
|
11
|
+
* underlying {@link ClientSession} and the {@link ChatTransport} wrapping it.
|
|
12
|
+
*/
|
|
13
|
+
export interface ChatTransportSlot {
|
|
14
|
+
/** The underlying client session used to create the chat transport. */
|
|
15
|
+
readonly session: ClientSession<VercelInput, VercelOutput, VercelProjection, AI.UIMessage>;
|
|
16
|
+
/** Construction error from the underlying {@link ClientSession}, or `undefined` on success. */
|
|
17
|
+
readonly sessionError?: Ably.ErrorInfo | undefined;
|
|
18
|
+
/** The chat transport adapter for use with Vercel's useChat hook. */
|
|
19
|
+
readonly chatTransport: ChatTransport;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The shape of the single {@link ChatTransportContext} value.
|
|
24
|
+
* Combines the nearest slot with the full registry in one context object.
|
|
25
|
+
*/
|
|
26
|
+
export interface ChatTransportContextValue {
|
|
27
|
+
/** The slot from the nearest {@link ChatTransportProvider} in the tree. */
|
|
28
|
+
readonly nearest: ChatTransportSlot | undefined;
|
|
29
|
+
/** All registered slots, keyed by channelName. */
|
|
30
|
+
readonly providers: Readonly<Record<string, ChatTransportSlot>>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Context that carries both the nearest {@link ChatTransportSlot} and the full registry of
|
|
35
|
+
* registered slots keyed by channelName. Populated by {@link ChatTransportProvider};
|
|
36
|
+
* read by {@link useChatTransport}.
|
|
37
|
+
*/
|
|
38
|
+
export const ChatTransportContext = createContext<ChatTransportContextValue>({
|
|
39
|
+
nearest: undefined,
|
|
40
|
+
providers: {},
|
|
41
|
+
});
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ChatTransportProvider: creates a ChatTransport from a ClientSession and makes it
|
|
3
|
+
* available to descendants via ChatTransportContext.
|
|
4
|
+
*
|
|
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.
|
|
10
|
+
*
|
|
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.
|
|
15
|
+
*
|
|
16
|
+
* Multiple ChatTransportProviders can be nested using distinct channelNames.
|
|
17
|
+
* Each provider merges its session into the parent registry, so descendants
|
|
18
|
+
* can access all registered sessions via useChatTransport({ channelName }).
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type * as AI from 'ai';
|
|
22
|
+
import { type PropsWithChildren, type ReactNode, useContext, useMemo } from 'react';
|
|
23
|
+
|
|
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';
|
|
27
|
+
import { createChatTransport } from '../../transport/index.js';
|
|
28
|
+
import type { ChatTransportSlot } from './chat-transport-context.js';
|
|
29
|
+
import { ChatTransportContext } from './chat-transport-context.js';
|
|
30
|
+
|
|
31
|
+
export const { ClientSessionProvider, useAblyMessages, useClientSession, useCreateView, useTree, useView } =
|
|
32
|
+
createSessionHooks<VercelInput, VercelOutput, VercelProjection, AI.UIMessage>();
|
|
33
|
+
|
|
34
|
+
type CoreClientSessionProviderProps = Omit<
|
|
35
|
+
ClientSessionProviderProps<VercelInput, VercelOutput, VercelProjection, AI.UIMessage>,
|
|
36
|
+
'codec'
|
|
37
|
+
>;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Props for {@link ChatTransportProvider}.
|
|
41
|
+
*
|
|
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.
|
|
45
|
+
*/
|
|
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;
|
|
53
|
+
/**
|
|
54
|
+
* Optional transport options for customizing chat request construction (e.g. the `prepareSendMessagesRequest` hook).
|
|
55
|
+
* Must be stable across renders — wrap in `useMemo` or define outside the component.
|
|
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.
|
|
58
|
+
*/
|
|
59
|
+
chatOptions?: ChatTransportOptions;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const ChatTransportProviderInner = ({
|
|
63
|
+
channelName,
|
|
64
|
+
chatTransportOptions,
|
|
65
|
+
children,
|
|
66
|
+
}: {
|
|
67
|
+
channelName: string;
|
|
68
|
+
chatTransportOptions: ChatTransportOptions;
|
|
69
|
+
children: ReactNode;
|
|
70
|
+
}) => {
|
|
71
|
+
const { session, sessionError } = useClientSession();
|
|
72
|
+
const { providers: parentProviders } = useContext(ChatTransportContext);
|
|
73
|
+
const chatTransport = useMemo(
|
|
74
|
+
() => createChatTransport(session, chatTransportOptions),
|
|
75
|
+
[session, chatTransportOptions],
|
|
76
|
+
);
|
|
77
|
+
const contextValue = useMemo(() => {
|
|
78
|
+
const slot: ChatTransportSlot = { session, sessionError, chatTransport };
|
|
79
|
+
return {
|
|
80
|
+
nearest: slot,
|
|
81
|
+
providers: { ...parentProviders, [channelName]: slot },
|
|
82
|
+
};
|
|
83
|
+
}, [channelName, parentProviders, chatTransport, session, sessionError]);
|
|
84
|
+
|
|
85
|
+
return <ChatTransportContext.Provider value={contextValue}>{children}</ChatTransportContext.Provider>;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Provide a {@link ChatTransport} and its underlying {@link ClientSession} to descendant components.
|
|
90
|
+
*
|
|
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},
|
|
94
|
+
* and registers the full slot in `ChatTransportContext` under `channelName`. Descendants call
|
|
95
|
+
* {@link useChatTransport} with the same `channelName` to access both.
|
|
96
|
+
*
|
|
97
|
+
* `useClientSession` is also available inside this provider's subtree.
|
|
98
|
+
*
|
|
99
|
+
* ```tsx
|
|
100
|
+
* <ChatTransportProvider channelName="ai:demo">
|
|
101
|
+
* <Chat />
|
|
102
|
+
* </ChatTransportProvider>
|
|
103
|
+
*
|
|
104
|
+
* // Inside Chat:
|
|
105
|
+
* const { chatTransport, session } = useChatTransport();
|
|
106
|
+
* const { session } = useClientSession(); // also available
|
|
107
|
+
* ```
|
|
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.
|
|
112
|
+
* @param props.chatOptions - Optional hooks for customizing chat request construction. Must be stable (memoized) — a new reference recreates the ChatTransport.
|
|
113
|
+
* @param props.children - Descendant components that consume the chat transport via hooks.
|
|
114
|
+
* @returns A React element wrapping children with ClientSessionContext and ChatTransportContext.
|
|
115
|
+
*/
|
|
116
|
+
export const ChatTransportProvider = ({
|
|
117
|
+
api,
|
|
118
|
+
credentials,
|
|
119
|
+
fetch,
|
|
120
|
+
chatOptions,
|
|
121
|
+
children,
|
|
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}
|
|
141
|
+
>
|
|
142
|
+
<ChatTransportProviderInner
|
|
143
|
+
channelName={sessionProps.channelName}
|
|
144
|
+
chatTransportOptions={chatTransportOptions}
|
|
145
|
+
>
|
|
146
|
+
{children}
|
|
147
|
+
</ChatTransportProviderInner>
|
|
148
|
+
</ClientSessionProvider>
|
|
149
|
+
);
|
|
150
|
+
};
|
|
@@ -1,4 +1,16 @@
|
|
|
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
|
+
export type { ChatTransportProviderProps } from './contexts/chat-transport-provider.js';
|
|
4
|
+
export {
|
|
5
|
+
ChatTransportProvider,
|
|
6
|
+
ClientSessionProvider,
|
|
7
|
+
useAblyMessages,
|
|
8
|
+
useClientSession,
|
|
9
|
+
useCreateView,
|
|
10
|
+
useTree,
|
|
11
|
+
useView,
|
|
12
|
+
} from './contexts/chat-transport-provider.js';
|
|
13
|
+
export type { ChatTransportHandle, UseChatTransportOptions } from './use-chat-transport.js';
|
|
3
14
|
export { useChatTransport } from './use-chat-transport.js';
|
|
15
|
+
export type { UseMessageSyncOptions } from './use-message-sync.js';
|
|
4
16
|
export { useMessageSync } from './use-message-sync.js';
|
|
@@ -1,60 +1,180 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* useChatTransport:
|
|
3
|
-
*
|
|
2
|
+
* useChatTransport: reads a ChatTransport and its underlying ClientSession from
|
|
3
|
+
* the nearest ChatTransportProvider.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
-
* (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.
|
|
10
|
+
* Pass `channelName` to look up a specific provider by name. Omit to use the nearest
|
|
11
|
+
* provider in the tree. Pass `skip: true` to defer (e.g. when auth is not yet resolved)
|
|
12
|
+
* — returns stubs whose properties throw with a descriptive error.
|
|
15
13
|
*/
|
|
16
14
|
|
|
15
|
+
import * as Ably from 'ably';
|
|
17
16
|
import type * as AI from 'ai';
|
|
18
|
-
import {
|
|
17
|
+
import { useContext } from 'react';
|
|
18
|
+
|
|
19
|
+
import type { ClientSession, Tree, View } from '../../core/transport/types.js';
|
|
20
|
+
import { ErrorCode } from '../../errors.js';
|
|
21
|
+
import type { VercelInput, VercelOutput, VercelProjection } from '../codec/index.js';
|
|
22
|
+
import type { ChatTransport } from '../transport/index.js';
|
|
23
|
+
import { ChatTransportContext } from './contexts/chat-transport-context.js';
|
|
19
24
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
const SKIPPED_CLIENT_SESSION: ClientSession<VercelInput, VercelOutput, VercelProjection, AI.UIMessage> = {
|
|
26
|
+
get tree(): Tree<VercelOutput, VercelProjection> {
|
|
27
|
+
throw new Ably.ErrorInfo('unable to access tree; hook is skipped', ErrorCode.InvalidArgument, 400);
|
|
28
|
+
},
|
|
29
|
+
get view(): View<VercelInput, AI.UIMessage> {
|
|
30
|
+
throw new Ably.ErrorInfo('unable to access view; hook is skipped', ErrorCode.InvalidArgument, 400);
|
|
31
|
+
},
|
|
32
|
+
connect: () => {
|
|
33
|
+
throw new Ably.ErrorInfo('unable to connect; hook is skipped', ErrorCode.InvalidArgument, 400);
|
|
34
|
+
},
|
|
35
|
+
createView: (): View<VercelInput, AI.UIMessage> => {
|
|
36
|
+
throw new Ably.ErrorInfo('unable to create view; hook is skipped', ErrorCode.InvalidArgument, 400);
|
|
37
|
+
},
|
|
38
|
+
cancel: () => {
|
|
39
|
+
throw new Ably.ErrorInfo('unable to cancel; hook is skipped', ErrorCode.InvalidArgument, 400);
|
|
40
|
+
},
|
|
41
|
+
on: () => {
|
|
42
|
+
throw new Ably.ErrorInfo('unable to subscribe; hook is skipped', ErrorCode.InvalidArgument, 400);
|
|
43
|
+
},
|
|
44
|
+
close: () => {
|
|
45
|
+
throw new Ably.ErrorInfo('unable to close; hook is skipped', ErrorCode.InvalidArgument, 400);
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const SKIPPED_CHAT_TRANSPORT: ChatTransport = {
|
|
50
|
+
sendMessages: (): never => {
|
|
51
|
+
throw new Ably.ErrorInfo('unable to send messages; hook is skipped', ErrorCode.InvalidArgument, 400);
|
|
52
|
+
},
|
|
53
|
+
reconnectToStream: (): never => {
|
|
54
|
+
throw new Ably.ErrorInfo('unable to reconnect to stream; hook is skipped', ErrorCode.InvalidArgument, 400);
|
|
55
|
+
},
|
|
56
|
+
close: (): never => {
|
|
57
|
+
throw new Ably.ErrorInfo('unable to close; hook is skipped', ErrorCode.InvalidArgument, 400);
|
|
58
|
+
},
|
|
59
|
+
get streaming(): never {
|
|
60
|
+
throw new Ably.ErrorInfo('unable to access streaming; hook is skipped', ErrorCode.InvalidArgument, 400);
|
|
61
|
+
},
|
|
62
|
+
onStreamingChange: (): never => {
|
|
63
|
+
throw new Ably.ErrorInfo(
|
|
64
|
+
'unable to subscribe to streaming changes; hook is skipped',
|
|
65
|
+
ErrorCode.InvalidArgument,
|
|
66
|
+
400,
|
|
67
|
+
);
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/** Options for {@link useChatTransport}. */
|
|
72
|
+
export interface UseChatTransportOptions {
|
|
73
|
+
/** Channel name to look up; omit to use the nearest {@link ChatTransportProvider}. */
|
|
74
|
+
channelName?: string;
|
|
75
|
+
/** When `true`, return stubs that throw on any access. */
|
|
76
|
+
skip?: boolean;
|
|
77
|
+
}
|
|
25
78
|
|
|
26
79
|
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
80
|
+
* The value returned by {@link useChatTransport}.
|
|
81
|
+
* Provides both the underlying {@link ClientSession} and the {@link ChatTransport}
|
|
82
|
+
* adapter for Vercel's useChat hook.
|
|
30
83
|
*/
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
84
|
+
export interface ChatTransportHandle {
|
|
85
|
+
/**
|
|
86
|
+
* The underlying client session, also available via {@link useClientSession}.
|
|
87
|
+
* A throwing stub when `skip` is `true`, when no matching {@link ClientSessionProvider}
|
|
88
|
+
* was found in the tree, or when session construction failed. Check `sessionError` before use.
|
|
89
|
+
*/
|
|
90
|
+
session: ClientSession<VercelInput, VercelOutput, VercelProjection, AI.UIMessage>;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* The chat transport adapter for use with Vercel's `useChat` hook.
|
|
94
|
+
*
|
|
95
|
+
* A throwing stub when `skip` is `true` or when no matching
|
|
96
|
+
* {@link ChatTransportProvider} was found in the tree. When a provider is found
|
|
97
|
+
* but the underlying {@link ClientSession} failed to construct, this is the real
|
|
98
|
+
* transport and `sessionError` is set instead. Check `chatTransportError` and
|
|
99
|
+
* `sessionError` before use.
|
|
100
|
+
*/
|
|
101
|
+
chatTransport: ChatTransport;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Set when no matching {@link ClientSessionProvider} was found, when session
|
|
105
|
+
* construction failed, and `skip` is `false`.
|
|
106
|
+
* `undefined` when the session resolved successfully or when `skip` is `true`.
|
|
107
|
+
*/
|
|
108
|
+
sessionError?: Ably.ErrorInfo | undefined;
|
|
109
|
+
/**
|
|
110
|
+
* Set only when no matching {@link ChatTransportProvider} was found and `skip` is
|
|
111
|
+
* `false`.
|
|
112
|
+
* `undefined` when the chat transport resolved successfully (even if session
|
|
113
|
+
* construction failed — see `sessionError`) or when `skip` is `true`.
|
|
114
|
+
*/
|
|
115
|
+
chatTransportError?: Ably.ErrorInfo | undefined;
|
|
116
|
+
}
|
|
34
117
|
|
|
35
118
|
/**
|
|
36
|
-
*
|
|
119
|
+
* Access a {@link ChatTransport} and {@link ClientSession} from the nearest {@link ChatTransportProvider}.
|
|
37
120
|
*
|
|
38
|
-
*
|
|
39
|
-
* `
|
|
40
|
-
* @
|
|
41
|
-
*
|
|
42
|
-
* @
|
|
121
|
+
* When `channelName` is omitted, the innermost `ChatTransportProvider` in the tree is used.
|
|
122
|
+
* When `skip` is `true`, returns stubs whose every property and method throws
|
|
123
|
+
* an {@link Ably.ErrorInfo} — safe to hold in state before conditions are ready.
|
|
124
|
+
* When no provider is found, returns stubs with `chatTransportError` set instead of throwing.
|
|
125
|
+
* @param props - Options for selecting the chat transport.
|
|
126
|
+
* @param props.channelName - The channel name passed to the enclosing `ChatTransportProvider`. Omit to use the nearest.
|
|
127
|
+
* @param props.skip - When `true`, return stubs that throw on any access instead of reading from context.
|
|
128
|
+
* @returns The `ChatTransportHandle` containing both the chat transport adapter and the underlying client session.
|
|
43
129
|
*/
|
|
44
|
-
export const useChatTransport = (
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
chatTransportRef.current = createChatTransport(transport, chatOptions);
|
|
130
|
+
export const useChatTransport = ({ channelName, skip }: UseChatTransportOptions = {}): ChatTransportHandle => {
|
|
131
|
+
const { nearest, providers } = useContext(ChatTransportContext);
|
|
132
|
+
|
|
133
|
+
if (skip) {
|
|
134
|
+
return { session: SKIPPED_CLIENT_SESSION, chatTransport: SKIPPED_CHAT_TRANSPORT };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (channelName !== undefined) {
|
|
138
|
+
const slot = providers[channelName];
|
|
139
|
+
if (slot) {
|
|
140
|
+
return { session: slot.session, chatTransport: slot.chatTransport, sessionError: slot.sessionError };
|
|
56
141
|
}
|
|
142
|
+
return {
|
|
143
|
+
session: SKIPPED_CLIENT_SESSION,
|
|
144
|
+
chatTransport: SKIPPED_CHAT_TRANSPORT,
|
|
145
|
+
sessionError: new Ably.ErrorInfo(
|
|
146
|
+
`unable to use client session; no ClientSessionProvider found for channelName "${channelName}"`,
|
|
147
|
+
ErrorCode.BadRequest,
|
|
148
|
+
400,
|
|
149
|
+
),
|
|
150
|
+
chatTransportError: new Ably.ErrorInfo(
|
|
151
|
+
`unable to use chat transport; no ChatTransportProvider found for channelName "${channelName}"`,
|
|
152
|
+
ErrorCode.BadRequest,
|
|
153
|
+
400,
|
|
154
|
+
),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (nearest) {
|
|
159
|
+
return {
|
|
160
|
+
session: nearest.session,
|
|
161
|
+
chatTransport: nearest.chatTransport,
|
|
162
|
+
sessionError: nearest.sessionError,
|
|
163
|
+
};
|
|
57
164
|
}
|
|
58
165
|
|
|
59
|
-
return
|
|
166
|
+
return {
|
|
167
|
+
session: SKIPPED_CLIENT_SESSION,
|
|
168
|
+
chatTransport: SKIPPED_CHAT_TRANSPORT,
|
|
169
|
+
sessionError: new Ably.ErrorInfo(
|
|
170
|
+
'unable to use session; no ClientSessionProvider found in the tree',
|
|
171
|
+
ErrorCode.BadRequest,
|
|
172
|
+
400,
|
|
173
|
+
),
|
|
174
|
+
chatTransportError: new Ably.ErrorInfo(
|
|
175
|
+
'unable to use chat transport; no ChatTransportProvider found in the tree',
|
|
176
|
+
ErrorCode.BadRequest,
|
|
177
|
+
400,
|
|
178
|
+
),
|
|
179
|
+
};
|
|
60
180
|
};
|