@ably/ai-transport 0.2.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.
- package/README.md +10 -19
- package/dist/ably-ai-transport.js +1790 -1091
- 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 +2 -2
- package/dist/core/agent.d.ts +20 -5
- package/dist/core/channel-options.d.ts +57 -0
- package/dist/core/codec/codec-event.d.ts +9 -0
- package/dist/core/codec/decoder.d.ts +4 -1
- package/dist/core/codec/define-codec.d.ts +100 -0
- package/dist/core/codec/encoder.d.ts +2 -7
- package/dist/core/codec/field-bag.d.ts +85 -0
- package/dist/core/codec/fields.d.ts +141 -0
- package/dist/core/codec/index.d.ts +8 -1
- package/dist/core/codec/input-descriptor-decoder.d.ts +19 -0
- package/dist/core/codec/input-descriptor-encoder.d.ts +22 -0
- package/dist/core/codec/input-descriptors.d.ts +281 -0
- package/dist/core/codec/output-descriptor-decoder.d.ts +29 -0
- package/dist/core/codec/output-descriptor-encoder.d.ts +31 -0
- package/dist/core/codec/output-descriptors.d.ts +237 -0
- package/dist/core/codec/types.d.ts +95 -36
- package/dist/core/codec/well-known-inputs.d.ts +52 -0
- package/dist/core/transport/agent-view.d.ts +296 -0
- package/dist/core/transport/decode-fold.d.ts +40 -32
- package/dist/core/transport/headers.d.ts +30 -1
- package/dist/core/transport/index.d.ts +1 -1
- package/dist/core/transport/invocation.d.ts +1 -1
- package/dist/core/transport/load-history-pages.d.ts +71 -0
- package/dist/core/transport/load-history.d.ts +21 -16
- package/dist/core/transport/run-manager.d.ts +9 -11
- package/dist/core/transport/session-support.d.ts +55 -0
- package/dist/core/transport/tree.d.ts +165 -15
- package/dist/core/transport/types/agent.d.ts +120 -98
- package/dist/core/transport/types/client.d.ts +45 -12
- package/dist/core/transport/types/tree.d.ts +52 -10
- package/dist/core/transport/types/view.d.ts +55 -28
- package/dist/core/transport/view.d.ts +176 -58
- package/dist/core/transport/wire-log.d.ts +102 -0
- package/dist/errors.d.ts +10 -4
- package/dist/index.d.ts +6 -5
- package/dist/react/ably-ai-transport-react.js +784 -415
- 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 +2 -1
- package/dist/react/contexts/client-session-provider.d.ts +3 -0
- package/dist/react/index.d.ts +2 -1
- package/dist/react/internal/skipped-session.d.ts +8 -0
- package/dist/react/use-view.d.ts +3 -3
- package/dist/utils.d.ts +22 -54
- package/dist/vercel/ably-ai-transport-vercel.js +2297 -2026
- 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/decode-lifecycle.d.ts +9 -0
- package/dist/vercel/codec/events.d.ts +1 -2
- package/dist/vercel/codec/fields.d.ts +44 -0
- package/dist/vercel/codec/fold-content.d.ts +16 -0
- package/dist/vercel/codec/fold-data.d.ts +16 -0
- package/dist/vercel/codec/fold-input.d.ts +67 -0
- package/dist/vercel/codec/fold-lifecycle.d.ts +16 -0
- package/dist/vercel/codec/fold-text.d.ts +16 -0
- package/dist/vercel/codec/fold-tool-input.d.ts +17 -0
- package/dist/vercel/codec/fold-tool-output.d.ts +16 -0
- package/dist/vercel/codec/index.d.ts +5 -30
- package/dist/vercel/codec/inputs.d.ts +11 -0
- package/dist/vercel/codec/outputs.d.ts +11 -0
- package/dist/vercel/codec/reducer-state.d.ts +121 -0
- package/dist/vercel/codec/reducer.d.ts +20 -102
- package/dist/vercel/codec/tool-transitions.d.ts +0 -6
- package/dist/vercel/codec/wire-data.d.ts +34 -0
- package/dist/vercel/index.d.ts +1 -0
- package/dist/vercel/react/ably-ai-transport-vercel-react.js +2013 -9500
- package/dist/vercel/react/ably-ai-transport-vercel-react.js.map +1 -1
- package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs +1 -70
- package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs.map +1 -1
- package/dist/vercel/react/contexts/chat-transport-context.d.ts +2 -1
- package/dist/vercel/run-end-reason.d.ts +66 -11
- package/dist/vercel/tool-part.d.ts +21 -0
- package/dist/vercel/transport/chat-transport.d.ts +0 -2
- package/dist/vercel/transport/index.d.ts +1 -1
- package/dist/vercel/transport/run-output-stream.d.ts +6 -8
- package/dist/version.d.ts +1 -1
- package/package.json +2 -2
- package/src/constants.ts +2 -2
- package/src/core/agent.ts +43 -19
- package/src/core/channel-options.ts +89 -0
- package/src/core/codec/codec-event.ts +27 -0
- package/src/core/codec/decoder.ts +145 -21
- package/src/core/codec/define-codec.ts +432 -0
- package/src/core/codec/encoder.ts +13 -54
- package/src/core/codec/field-bag.ts +142 -0
- package/src/core/codec/fields.ts +193 -0
- package/src/core/codec/index.ts +43 -0
- package/src/core/codec/input-descriptor-decoder.ts +97 -0
- package/src/core/codec/input-descriptor-encoder.ts +150 -0
- package/src/core/codec/input-descriptors.ts +373 -0
- package/src/core/codec/output-descriptor-decoder.ts +139 -0
- package/src/core/codec/output-descriptor-encoder.ts +101 -0
- package/src/core/codec/output-descriptors.ts +307 -0
- package/src/core/codec/types.ts +99 -36
- package/src/core/codec/well-known-inputs.ts +96 -0
- package/src/core/transport/agent-session.ts +330 -589
- package/src/core/transport/agent-view.ts +738 -0
- package/src/core/transport/client-session.ts +74 -69
- package/src/core/transport/decode-fold.ts +57 -47
- package/src/core/transport/headers.ts +57 -4
- package/src/core/transport/index.ts +2 -1
- package/src/core/transport/invocation.ts +1 -1
- package/src/core/transport/load-history-pages.ts +220 -0
- package/src/core/transport/load-history.ts +63 -61
- package/src/core/transport/pipe-stream.ts +10 -1
- package/src/core/transport/run-manager.ts +25 -31
- package/src/core/transport/session-support.ts +96 -0
- package/src/core/transport/tree.ts +414 -47
- package/src/core/transport/types/agent.ts +129 -102
- package/src/core/transport/types/client.ts +49 -13
- package/src/core/transport/types/tree.ts +61 -12
- package/src/core/transport/types/view.ts +57 -28
- package/src/core/transport/view.ts +520 -172
- package/src/core/transport/wire-log.ts +189 -0
- package/src/errors.ts +10 -3
- package/src/index.ts +44 -11
- package/src/react/contexts/client-session-context.ts +1 -1
- package/src/react/contexts/client-session-provider.tsx +38 -2
- package/src/react/index.ts +2 -1
- package/src/react/internal/skipped-session.ts +62 -0
- package/src/react/use-client-session.ts +7 -30
- package/src/react/use-view.ts +3 -3
- package/src/utils.ts +31 -97
- package/src/vercel/codec/decode-lifecycle.ts +70 -0
- package/src/vercel/codec/events.ts +1 -3
- package/src/vercel/codec/fields.ts +58 -0
- package/src/vercel/codec/fold-content.ts +54 -0
- package/src/vercel/codec/fold-data.ts +46 -0
- package/src/vercel/codec/fold-input.ts +255 -0
- package/src/vercel/codec/fold-lifecycle.ts +85 -0
- package/src/vercel/codec/fold-text.ts +55 -0
- package/src/vercel/codec/fold-tool-input.ts +86 -0
- package/src/vercel/codec/fold-tool-output.ts +79 -0
- package/src/vercel/codec/index.ts +23 -63
- package/src/vercel/codec/inputs.ts +116 -0
- package/src/vercel/codec/outputs.ts +207 -0
- package/src/vercel/codec/reducer-state.ts +169 -0
- package/src/vercel/codec/reducer.ts +52 -838
- package/src/vercel/codec/tool-transitions.ts +1 -12
- package/src/vercel/codec/wire-data.ts +64 -0
- package/src/vercel/index.ts +1 -0
- package/src/vercel/react/contexts/chat-transport-context.ts +1 -1
- package/src/vercel/react/use-chat-transport.ts +8 -28
- package/src/vercel/react/use-message-sync.ts +5 -10
- package/src/vercel/run-end-reason.ts +95 -16
- package/src/vercel/tool-part.ts +25 -0
- package/src/vercel/transport/chat-transport.ts +10 -22
- package/src/vercel/transport/index.ts +1 -1
- package/src/vercel/transport/run-output-stream.ts +7 -8
- package/src/version.ts +1 -1
- package/dist/core/transport/branch-chain.d.ts +0 -43
- package/dist/core/transport/load-conversation.d.ts +0 -128
- package/dist/vercel/codec/decoder.d.ts +0 -9
- package/dist/vercel/codec/encoder.d.ts +0 -11
- package/src/core/transport/branch-chain.ts +0 -58
- package/src/core/transport/load-conversation.ts +0 -355
- package/src/vercel/codec/decoder.ts +0 -696
- package/src/vercel/codec/encoder.ts +0 -548
|
@@ -10,7 +10,7 @@ import type * as AI from 'ai';
|
|
|
10
10
|
import { stripUndefined } from '../../utils.js';
|
|
11
11
|
|
|
12
12
|
// ---------------------------------------------------------------------------
|
|
13
|
-
// Tool output chunk type
|
|
13
|
+
// Tool output chunk type
|
|
14
14
|
// ---------------------------------------------------------------------------
|
|
15
15
|
|
|
16
16
|
/** The set of UIMessageChunk types that represent tool output transitions. */
|
|
@@ -19,17 +19,6 @@ export type ToolOutputChunk = Extract<
|
|
|
19
19
|
{ type: 'tool-output-available' | 'tool-output-error' | 'tool-output-denied' | 'tool-approval-request' }
|
|
20
20
|
>;
|
|
21
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
22
|
// ---------------------------------------------------------------------------
|
|
34
23
|
// Tool base helper
|
|
35
24
|
// ---------------------------------------------------------------------------
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire-data shapes and runtime guards for the tool payloads whose `data`
|
|
3
|
+
* envelope is JSON-parsed from the network (a trust boundary). The guards
|
|
4
|
+
* validate the typed envelope fields; tool-defined `output`/`input` stay
|
|
5
|
+
* unconstrained. Shared by the output and input descriptor tables.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Wire format for the agent-side `tool-input-error` chunk data payload. */
|
|
9
|
+
export interface ToolInputErrorWireData {
|
|
10
|
+
errorText?: string;
|
|
11
|
+
input?: unknown;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Wire format for the `tool-output-available` (agent) / `tool-result` (client) data payload. */
|
|
15
|
+
export interface ToolOutputAvailableWireData {
|
|
16
|
+
output?: unknown;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Wire format for the agent-side `tool-output-error` chunk data payload. */
|
|
20
|
+
export interface AgentToolOutputErrorWireData {
|
|
21
|
+
errorText?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Wire format for the client-side `tool-result-error` input data payload. */
|
|
25
|
+
export interface ClientToolResultErrorWireData {
|
|
26
|
+
message?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Narrow JSON-parsed wire data to a record. The encoder is expected to publish
|
|
30
|
+
// an object for these payloads, but a malformed publish could carry a primitive
|
|
31
|
+
// or null — callers fall back to field defaults when these guards reject.
|
|
32
|
+
const isRecord = (data: unknown): data is Record<string, unknown> => typeof data === 'object' && data !== null;
|
|
33
|
+
|
|
34
|
+
// Validate that `data` is a record whose named field is absent or a string. The
|
|
35
|
+
// optional-string check for the typed error fields below lives here once so the
|
|
36
|
+
// guards can't drift. No `as` needed: `isRecord` narrows `data` to a record, so
|
|
37
|
+
// string-key indexing is well-typed.
|
|
38
|
+
const isRecordWithOptionalString = (data: unknown, key: string): boolean =>
|
|
39
|
+
isRecord(data) && (data[key] === undefined || typeof data[key] === 'string');
|
|
40
|
+
|
|
41
|
+
// Validates the typed `errorText` field; `input` is tool-defined and
|
|
42
|
+
// intentionally left unconstrained.
|
|
43
|
+
/**
|
|
44
|
+
* Coerce wire `data` to a string, falling back to `''` for any non-string
|
|
45
|
+
* payload — the defensive read for descriptors whose data is plain text.
|
|
46
|
+
* @param data - The inbound wire data.
|
|
47
|
+
* @returns The string payload, or `''` when the data is not a string.
|
|
48
|
+
*/
|
|
49
|
+
export const asString = (data: unknown): string => (typeof data === 'string' ? data : '');
|
|
50
|
+
|
|
51
|
+
export const isToolInputErrorWireData = (data: unknown): data is ToolInputErrorWireData =>
|
|
52
|
+
isRecordWithOptionalString(data, 'errorText');
|
|
53
|
+
|
|
54
|
+
// The sole field `output` is tool-defined and intentionally unconstrained, so
|
|
55
|
+
// this asserts only that the payload is an object envelope.
|
|
56
|
+
export const isToolOutputAvailableWireData = (data: unknown): data is ToolOutputAvailableWireData => isRecord(data);
|
|
57
|
+
|
|
58
|
+
// Validates the typed `errorText` field.
|
|
59
|
+
export const isAgentToolOutputErrorWireData = (data: unknown): data is AgentToolOutputErrorWireData =>
|
|
60
|
+
isRecordWithOptionalString(data, 'errorText');
|
|
61
|
+
|
|
62
|
+
// Validates the typed `message` field.
|
|
63
|
+
export const isClientToolResultErrorWireData = (data: unknown): data is ClientToolResultErrorWireData =>
|
|
64
|
+
isRecordWithOptionalString(data, 'message');
|
package/src/vercel/index.ts
CHANGED
|
@@ -13,4 +13,5 @@ export type {
|
|
|
13
13
|
export { createAgentSession, createChatTransport, createClientSession } from './transport/index.js';
|
|
14
14
|
|
|
15
15
|
// Vercel-shaped helpers
|
|
16
|
+
export type { VercelRunOutcome } from './run-end-reason.js';
|
|
16
17
|
export { vercelRunOutcome } from './run-end-reason.js';
|
|
@@ -23,7 +23,7 @@ export interface ChatTransportSlot {
|
|
|
23
23
|
* The shape of the single {@link ChatTransportContext} value.
|
|
24
24
|
* Combines the nearest slot with the full registry in one context object.
|
|
25
25
|
*/
|
|
26
|
-
|
|
26
|
+
interface ChatTransportContextValue {
|
|
27
27
|
/** The slot from the nearest {@link ChatTransportProvider} in the tree. */
|
|
28
28
|
readonly nearest: ChatTransportSlot | undefined;
|
|
29
29
|
/** All registered slots, keyed by channelName. */
|
|
@@ -16,36 +16,13 @@ import * as Ably from 'ably';
|
|
|
16
16
|
import type * as AI from 'ai';
|
|
17
17
|
import { useContext } from 'react';
|
|
18
18
|
|
|
19
|
-
import type { ClientSession
|
|
19
|
+
import type { ClientSession } from '../../core/transport/types.js';
|
|
20
20
|
import { ErrorCode } from '../../errors.js';
|
|
21
|
+
import { makeSkippedClientSession } from '../../react/internal/skipped-session.js';
|
|
21
22
|
import type { VercelInput, VercelOutput, VercelProjection } from '../codec/index.js';
|
|
22
23
|
import type { ChatTransport } from '../transport/index.js';
|
|
23
24
|
import { ChatTransportContext } from './contexts/chat-transport-context.js';
|
|
24
25
|
|
|
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
26
|
const SKIPPED_CHAT_TRANSPORT: ChatTransport = {
|
|
50
27
|
sendMessages: (): never => {
|
|
51
28
|
throw new Ably.ErrorInfo('unable to send messages; hook is skipped', ErrorCode.InvalidArgument, 400);
|
|
@@ -131,7 +108,10 @@ export const useChatTransport = ({ channelName, skip }: UseChatTransportOptions
|
|
|
131
108
|
const { nearest, providers } = useContext(ChatTransportContext);
|
|
132
109
|
|
|
133
110
|
if (skip) {
|
|
134
|
-
return {
|
|
111
|
+
return {
|
|
112
|
+
session: makeSkippedClientSession<VercelInput, VercelOutput, VercelProjection, AI.UIMessage>(),
|
|
113
|
+
chatTransport: SKIPPED_CHAT_TRANSPORT,
|
|
114
|
+
};
|
|
135
115
|
}
|
|
136
116
|
|
|
137
117
|
if (channelName !== undefined) {
|
|
@@ -140,7 +120,7 @@ export const useChatTransport = ({ channelName, skip }: UseChatTransportOptions
|
|
|
140
120
|
return { session: slot.session, chatTransport: slot.chatTransport, sessionError: slot.sessionError };
|
|
141
121
|
}
|
|
142
122
|
return {
|
|
143
|
-
session:
|
|
123
|
+
session: makeSkippedClientSession<VercelInput, VercelOutput, VercelProjection, AI.UIMessage>(),
|
|
144
124
|
chatTransport: SKIPPED_CHAT_TRANSPORT,
|
|
145
125
|
sessionError: new Ably.ErrorInfo(
|
|
146
126
|
`unable to use client session; no ClientSessionProvider found for channelName "${channelName}"`,
|
|
@@ -164,7 +144,7 @@ export const useChatTransport = ({ channelName, skip }: UseChatTransportOptions
|
|
|
164
144
|
}
|
|
165
145
|
|
|
166
146
|
return {
|
|
167
|
-
session:
|
|
147
|
+
session: makeSkippedClientSession<VercelInput, VercelOutput, VercelProjection, AI.UIMessage>(),
|
|
168
148
|
chatTransport: SKIPPED_CHAT_TRANSPORT,
|
|
169
149
|
sessionError: new Ably.ErrorInfo(
|
|
170
150
|
'unable to use session; no ClientSessionProvider found in the tree',
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
import type * as AI from 'ai';
|
|
17
17
|
import { useEffect, useState } from 'react';
|
|
18
18
|
|
|
19
|
+
import { isToolPart, type ToolPart } from '../tool-part.js';
|
|
19
20
|
import { useChatTransport } from './use-chat-transport.js';
|
|
20
21
|
|
|
21
22
|
/** Options for {@link useMessageSync}. */
|
|
@@ -38,19 +39,13 @@ export interface UseMessageSyncOptions {
|
|
|
38
39
|
// Tool-resolution merge
|
|
39
40
|
// ---------------------------------------------------------------------------
|
|
40
41
|
//
|
|
41
|
-
// The
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
// `dynamic-tool` keep working.
|
|
46
|
-
|
|
47
|
-
type ToolPart = AI.DynamicToolUIPart | AI.ToolUIPart;
|
|
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.
|
|
48
46
|
|
|
49
47
|
const RESOLVED_TOOL_STATES = new Set(['output-available', 'output-error', 'approval-responded', 'output-denied']);
|
|
50
48
|
|
|
51
|
-
const isToolPart = (part: AI.UIMessage['parts'][number]): part is ToolPart =>
|
|
52
|
-
(part.type === 'dynamic-tool' || part.type.startsWith('tool-')) && 'toolCallId' in part && 'state' in part;
|
|
53
|
-
|
|
54
49
|
const mergeAssistant = (tree: AI.UIMessage, overlay: AI.UIMessage): AI.UIMessage => {
|
|
55
50
|
const overlayByCallId = new Map<string, ToolPart>();
|
|
56
51
|
for (const part of overlay.parts) {
|
|
@@ -1,16 +1,76 @@
|
|
|
1
|
+
import * as Ably from 'ably';
|
|
1
2
|
import type * as AI from 'ai';
|
|
2
3
|
|
|
3
4
|
import type { RunEndReason, StreamResult } from '../core/transport/types.js';
|
|
5
|
+
import { ErrorCode } from '../errors.js';
|
|
4
6
|
|
|
5
7
|
/**
|
|
6
|
-
*
|
|
7
|
-
* `
|
|
8
|
-
* `
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* Vercel
|
|
12
|
-
*
|
|
13
|
-
*
|
|
8
|
+
* The outcome of a Vercel `streamText` response piped through `Run.pipe`.
|
|
9
|
+
* Discriminated on `reason`: `'suspend'` means the run should pause; the
|
|
10
|
+
* non-`'suspend'` arms describe how it terminated, and an `'error'` outcome
|
|
11
|
+
* always carries `error`.
|
|
12
|
+
*
|
|
13
|
+
* This is a *description of what the Vercel run resulted in*, not a command to
|
|
14
|
+
* the SDK. The common case maps cleanly onto one transport action — `'suspend'`
|
|
15
|
+
* → `Run.suspend()`, everything else → `Run.end()` — and to make that case a
|
|
16
|
+
* one-liner the non-`'suspend'` arms are deliberately assignable to
|
|
17
|
+
* {@link RunEndParams}, so after a `suspend` guard the whole object passes
|
|
18
|
+
* straight to `Run.end(outcome)`. That assignability is a convenience for this
|
|
19
|
+
* adapter, not a constraint on what an outcome can mean: responding to an
|
|
20
|
+
* outcome may also involve work outside this SDK (persisting a result,
|
|
21
|
+
* notifying a human, triggering a downstream workflow), and the developer is
|
|
22
|
+
* free to do that around the terminal call.
|
|
23
|
+
*
|
|
24
|
+
* The type is Vercel-specific by design. Outcomes are the layer where agent
|
|
25
|
+
* SDKs diverge most — both in what they report (the `'suspend'` arm exists only
|
|
26
|
+
* because Vercel surfaces unexecuted tool calls as a non-terminal finish) and
|
|
27
|
+
* in what a developer must do in response. A different SDK's outcome type would
|
|
28
|
+
* have different arms; hence each adapter names its own rather than sharing a
|
|
29
|
+
* single core `RunOutcome`. The vocabulary it bottoms out in
|
|
30
|
+
* ({@link RunEndParams}, `Run.suspend`/`Run.end`) is the shared, codec-agnostic
|
|
31
|
+
* part that does live in core.
|
|
32
|
+
*/
|
|
33
|
+
export type VercelRunOutcome =
|
|
34
|
+
| {
|
|
35
|
+
/**
|
|
36
|
+
* The LLM requested tools the SDK did not auto-execute, so the run
|
|
37
|
+
* pauses rather than ending — call `Run.suspend()`.
|
|
38
|
+
*/
|
|
39
|
+
reason: 'suspend';
|
|
40
|
+
/** Never present for a suspend outcome. */
|
|
41
|
+
error?: never;
|
|
42
|
+
}
|
|
43
|
+
| {
|
|
44
|
+
/** A non-error terminal reason; pass the outcome to `Run.end()`. */
|
|
45
|
+
reason: Exclude<RunEndReason, 'error'>;
|
|
46
|
+
/** Never present for a non-error outcome. */
|
|
47
|
+
error?: never;
|
|
48
|
+
}
|
|
49
|
+
| {
|
|
50
|
+
/** The run ended in error; pass the outcome to `Run.end()`. */
|
|
51
|
+
reason: Extract<RunEndReason, 'error'>;
|
|
52
|
+
/**
|
|
53
|
+
* The terminal error: the underlying stream / `finishReason` failure
|
|
54
|
+
* wrapped as an `Ably.ErrorInfo` (code `StreamError`).
|
|
55
|
+
*/
|
|
56
|
+
error: Ably.ErrorInfo;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Derive the {@link VercelRunOutcome} for a Vercel `streamText` response that
|
|
61
|
+
* was piped through `Run.pipe`. Preserves transport-level outcomes
|
|
62
|
+
* (`'cancelled'`, `'error'`) from the pipe result; when the pipe completed
|
|
63
|
+
* naturally, awaits Vercel's `finishReason` and returns `'suspend'` for
|
|
64
|
+
* `'tool-calls'` (the LLM requested tools the SDK did not auto-execute, so the
|
|
65
|
+
* run should suspend rather than end), or `'complete'` otherwise.
|
|
66
|
+
*
|
|
67
|
+
* Surfaces the failure for both error shapes so the caller can forward it to
|
|
68
|
+
* `Run.end(reason, error)`: a stream that threw (`pipeResult.error`) and a
|
|
69
|
+
* `finishReason` that rejected with a non-abort error (e.g.
|
|
70
|
+
* `NoOutputGeneratedError`, network blow-ups). The error is wrapped as an
|
|
71
|
+
* `Ably.ErrorInfo` (code `StreamError`). A stream that already produced a
|
|
72
|
+
* codec-level error chunk is unaffected — stamping run-end is the
|
|
73
|
+
* codec-agnostic baseline that any consumer can read.
|
|
14
74
|
*
|
|
15
75
|
* Tolerates `finishReason` rejection. Vercel AI SDK v6 rejects
|
|
16
76
|
* `streamText().finishReason` with the abort signal's reason when the stream
|
|
@@ -25,13 +85,13 @@ import type { RunEndReason, StreamResult } from '../core/transport/types.js';
|
|
|
25
85
|
* of every route handler.
|
|
26
86
|
* @param pipeResult - The result returned by `Run.pipe`.
|
|
27
87
|
* @param finishReason - The `finishReason` promise from a `streamText` result.
|
|
28
|
-
* @returns
|
|
29
|
-
*
|
|
88
|
+
* @returns The {@link VercelRunOutcome}: the terminal `reason` (or `'suspend'`)
|
|
89
|
+
* and, when `reason` is `'error'`, the wrapped `error` to pass to `Run.end`.
|
|
30
90
|
*/
|
|
31
91
|
export const vercelRunOutcome = async (
|
|
32
92
|
pipeResult: StreamResult,
|
|
33
93
|
finishReason: PromiseLike<AI.FinishReason>,
|
|
34
|
-
): Promise<
|
|
94
|
+
): Promise<VercelRunOutcome> => {
|
|
35
95
|
if (pipeResult.reason !== 'complete') {
|
|
36
96
|
// Vercel's `result.finishReason` getter creates the underlying Promise
|
|
37
97
|
// eagerly, before the caller hands it to us. When `streamText` is
|
|
@@ -46,22 +106,41 @@ export const vercelRunOutcome = async (
|
|
|
46
106
|
Promise.resolve(finishReason).catch(() => {
|
|
47
107
|
/* intentionally discarded; reason already known from pipeResult */
|
|
48
108
|
});
|
|
49
|
-
|
|
109
|
+
if (pipeResult.reason === 'error') {
|
|
110
|
+
return { reason: 'error', error: _toErrorInfo(pipeResult.error) };
|
|
111
|
+
}
|
|
112
|
+
return { reason: pipeResult.reason };
|
|
50
113
|
}
|
|
51
114
|
try {
|
|
52
115
|
const finish = await finishReason;
|
|
53
|
-
|
|
116
|
+
if (finish === 'tool-calls') return { reason: 'suspend' };
|
|
117
|
+
return { reason: 'complete' };
|
|
54
118
|
} catch (error) {
|
|
55
119
|
// Abort-shaped rejections are surfaced from streamText when the run was
|
|
56
120
|
// cancelled before any step finished — treat the run as cancelled so the
|
|
57
121
|
// observable lifecycle matches the cancel that triggered it. Everything
|
|
58
122
|
// else is a real error (e.g. NoOutputGeneratedError, network blow-ups);
|
|
59
|
-
// surface it as such so the
|
|
60
|
-
// silent cancel.
|
|
61
|
-
|
|
123
|
+
// surface it as such — wrapped so the caller can stamp it on run-end — so
|
|
124
|
+
// the developer sees the failure rather than a silent cancel.
|
|
125
|
+
if (_isAbortLikeError(error)) return { reason: 'cancelled' };
|
|
126
|
+
return { reason: 'error', error: _toErrorInfo(error) };
|
|
62
127
|
}
|
|
63
128
|
};
|
|
64
129
|
|
|
130
|
+
/**
|
|
131
|
+
* Wrap a caught stream / `finishReason` failure as an `Ably.ErrorInfo` so it
|
|
132
|
+
* can be passed to `Run.end(reason, error)`. An error that is already an
|
|
133
|
+
* `Ably.ErrorInfo` is returned unchanged; anything else is wrapped with code
|
|
134
|
+
* `StreamError`, mirroring how `Run.pipe` wraps stream errors for `onError`.
|
|
135
|
+
* @param error - The caught error (or `undefined` when the stream reported none).
|
|
136
|
+
* @returns The error as an `Ably.ErrorInfo`.
|
|
137
|
+
*/
|
|
138
|
+
const _toErrorInfo = (error: unknown): Ably.ErrorInfo => {
|
|
139
|
+
if (error instanceof Ably.ErrorInfo) return error;
|
|
140
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
141
|
+
return new Ably.ErrorInfo(`unable to complete run; ${message}`, ErrorCode.StreamError, 500);
|
|
142
|
+
};
|
|
143
|
+
|
|
65
144
|
/**
|
|
66
145
|
* Heuristic for "this error came from an AbortSignal aborting".
|
|
67
146
|
* Covers `DOMException` aborts (browser / Node 20+ `streamText`),
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared tool-part type guard for the Vercel layer.
|
|
3
|
+
*
|
|
4
|
+
* The codec normalises every tool part to the `dynamic-tool` shape, but the AI
|
|
5
|
+
* SDK emits `tool-${name}` parts for statically-declared tools. Both shapes
|
|
6
|
+
* carry `toolCallId` and `state`. The guard accepts either representation so
|
|
7
|
+
* the transport's unresolved-tool detection and the React overlay merge can
|
|
8
|
+
* match tool parts uniformly — and so the cross-representation rule lives in
|
|
9
|
+
* one place rather than being re-spelled per call site.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type * as AI from 'ai';
|
|
13
|
+
|
|
14
|
+
/** A UIMessage tool part in either the `dynamic-tool` or `tool-${name}` representation. */
|
|
15
|
+
export type ToolPart = AI.DynamicToolUIPart | AI.ToolUIPart;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Whether a UIMessage part is a tool part of either representation. The
|
|
19
|
+
* `toolCallId`/`state` shape check is defensive against a future AI SDK release
|
|
20
|
+
* introducing a non-tool variant under the `tool-` prefix (none exists today).
|
|
21
|
+
* @param part - The UIMessage part to inspect.
|
|
22
|
+
* @returns True when the part is a tool part.
|
|
23
|
+
*/
|
|
24
|
+
export const isToolPart = (part: AI.UIMessage['parts'][number]): part is ToolPart =>
|
|
25
|
+
(part.type === 'dynamic-tool' || part.type.startsWith('tool-')) && 'toolCallId' in part && 'state' in part;
|
|
@@ -31,13 +31,15 @@
|
|
|
31
31
|
import * as Ably from 'ably';
|
|
32
32
|
import type * as AI from 'ai';
|
|
33
33
|
|
|
34
|
-
import type { CodecMessage } from '../../core/codec/
|
|
34
|
+
import type { CodecMessage } from '../../core/codec/index.js';
|
|
35
35
|
import type { ActiveRun, ClientSession, SendOptions } from '../../core/transport/types.js';
|
|
36
36
|
import { ErrorCode } from '../../errors.js';
|
|
37
37
|
import { EventEmitter } from '../../event-emitter.js';
|
|
38
38
|
import { LogLevel, makeLogger } from '../../logger.js';
|
|
39
|
+
import { errorCause, errorMessage } from '../../utils.js';
|
|
39
40
|
import type { VercelInput, VercelOutput, VercelProjection } from '../codec/index.js';
|
|
40
41
|
import { UIMessageCodec } from '../codec/index.js';
|
|
42
|
+
import { isToolPart, type ToolPart } from '../tool-part.js';
|
|
41
43
|
import { createRunOutputStream } from './run-output-stream.js';
|
|
42
44
|
|
|
43
45
|
// ---------------------------------------------------------------------------
|
|
@@ -71,7 +73,7 @@ export interface SendMessagesRequestContext {
|
|
|
71
73
|
}
|
|
72
74
|
|
|
73
75
|
/** Default agent endpoint the transport POSTs invocations to — mirrors Vercel's DefaultChatTransport. */
|
|
74
|
-
|
|
76
|
+
const DEFAULT_VERCEL_API = '/api/chat';
|
|
75
77
|
|
|
76
78
|
/** Options for customizing the ChatTransport behavior. */
|
|
77
79
|
export interface ChatTransportOptions {
|
|
@@ -226,18 +228,6 @@ const wrapStreamWithDone = <T>(
|
|
|
226
228
|
// Unresolved tool call detection
|
|
227
229
|
// ---------------------------------------------------------------------------
|
|
228
230
|
|
|
229
|
-
/**
|
|
230
|
-
* Whether a UIMessage part is a tool part — either the codec-normalised
|
|
231
|
-
* `dynamic-tool` shape or the AI SDK's statically-declared `tool-${name}`
|
|
232
|
-
* shape. Both carry `toolCallId` and `state`; the shape check at the end
|
|
233
|
-
* is defensive against a future AI SDK release introducing a non-tool
|
|
234
|
-
* variant under the `tool-` prefix (none exists today).
|
|
235
|
-
* @param part - The UIMessage part to inspect.
|
|
236
|
-
* @returns True when the part is a tool part of either representation.
|
|
237
|
-
*/
|
|
238
|
-
const _isToolPart = (part: AI.UIMessage['parts'][number]): part is AI.DynamicToolUIPart | AI.ToolUIPart =>
|
|
239
|
-
(part.type === 'dynamic-tool' || part.type.startsWith('tool-')) && 'toolCallId' in part && 'state' in part;
|
|
240
|
-
|
|
241
231
|
/**
|
|
242
232
|
* Whether an assistant message has a `dynamic-tool` part that can't resolve
|
|
243
233
|
* without further user action. Matches:
|
|
@@ -254,7 +244,7 @@ const hasUnresolvedToolCall = (msg: AI.UIMessage): boolean =>
|
|
|
254
244
|
msg.role === 'assistant' &&
|
|
255
245
|
msg.parts.some(
|
|
256
246
|
(p) =>
|
|
257
|
-
|
|
247
|
+
isToolPart(p) &&
|
|
258
248
|
(p.state === 'input-streaming' || p.state === 'input-available' || p.state === 'approval-requested'),
|
|
259
249
|
);
|
|
260
250
|
|
|
@@ -280,7 +270,7 @@ const UNRESOLVED_TOOL_STATES = new Set(['input-streaming', 'input-available', 'a
|
|
|
280
270
|
*
|
|
281
271
|
* The resulting inputs are passed alongside the continuation `view.send`
|
|
282
272
|
* so the channel publish and the continuation POST land as ONE atomic
|
|
283
|
-
* operation — the agent's `
|
|
273
|
+
* operation — the agent's `loadConversation()` history walk is guaranteed
|
|
284
274
|
* to see them because the channel publish happens before the POST inside
|
|
285
275
|
* `_internalSend`.
|
|
286
276
|
*
|
|
@@ -314,15 +304,14 @@ const deriveContinuationInputs = (
|
|
|
314
304
|
const { codecMessageId, message: treeMessage } = treeEntry;
|
|
315
305
|
|
|
316
306
|
for (const overlayPart of overlay.parts) {
|
|
317
|
-
if (!
|
|
307
|
+
if (!isToolPart(overlayPart)) continue;
|
|
318
308
|
// The codec normalises every tool part to `dynamic-tool`, but the
|
|
319
309
|
// AI SDK's useChat overlay emits `tool-${name}` parts for statically
|
|
320
310
|
// declared tools. Match by toolCallId rather than the type prefix
|
|
321
311
|
// so the cross-representation comparison works regardless of which
|
|
322
312
|
// side the tool was declared on.
|
|
323
313
|
const treePart = treeMessage.parts.find(
|
|
324
|
-
(p: AI.UIMessage['parts'][number]): p is
|
|
325
|
-
_isToolPart(p) && p.toolCallId === overlayPart.toolCallId,
|
|
314
|
+
(p: AI.UIMessage['parts'][number]): p is ToolPart => isToolPart(p) && p.toolCallId === overlayPart.toolCallId,
|
|
326
315
|
);
|
|
327
316
|
|
|
328
317
|
// Approval response: useChat's `addToolApprovalResponse` flipped the
|
|
@@ -678,13 +667,12 @@ export const createChatTransport = (
|
|
|
678
667
|
}
|
|
679
668
|
})
|
|
680
669
|
.catch((error: unknown) => {
|
|
681
|
-
const cause = error instanceof Ably.ErrorInfo ? error : undefined;
|
|
682
670
|
fail(
|
|
683
671
|
new Ably.ErrorInfo(
|
|
684
|
-
`unable to send; HTTP POST to ${api} failed: ${
|
|
672
|
+
`unable to send; HTTP POST to ${api} failed: ${errorMessage(error)}`,
|
|
685
673
|
ErrorCode.SessionSendFailed,
|
|
686
674
|
500,
|
|
687
|
-
|
|
675
|
+
errorCause(error),
|
|
688
676
|
),
|
|
689
677
|
);
|
|
690
678
|
});
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
// Chat transport adapter
|
|
16
16
|
export type { ChatTransport, ChatTransportOptions, SendMessagesRequestContext } from './chat-transport.js';
|
|
17
|
-
export { createChatTransport
|
|
17
|
+
export { createChatTransport } from './chat-transport.js';
|
|
18
18
|
|
|
19
19
|
import type * as AI from 'ai';
|
|
20
20
|
|
|
@@ -41,21 +41,20 @@ type VercelSession = ClientSession<VercelInput, VercelOutput, VercelProjection,
|
|
|
41
41
|
const isTerminalChunk = (output: VercelOutput): boolean =>
|
|
42
42
|
output.type === 'finish' || output.type === 'error' || output.type === 'abort';
|
|
43
43
|
|
|
44
|
-
/** A consumer-facing run output stream plus the
|
|
45
|
-
|
|
44
|
+
/** A consumer-facing run output stream plus the handle to close it externally. */
|
|
45
|
+
interface RunOutputStream {
|
|
46
46
|
/** The stream of decoded outputs for the run, as `useChat` consumes it. */
|
|
47
47
|
stream: ReadableStream<VercelOutput>;
|
|
48
48
|
/** Close the stream now (e.g. on local cancel). Idempotent. */
|
|
49
49
|
close: () => void;
|
|
50
|
-
/** Error the stream now (e.g. on a failed agent-invocation POST). Idempotent. */
|
|
51
|
-
error: (reason: Ably.ErrorInfo) => void;
|
|
52
50
|
}
|
|
53
51
|
|
|
54
52
|
/**
|
|
55
53
|
* Create a consumer-facing output stream for a send, sourced from the session
|
|
56
54
|
* Tree's events. See the module docs for close/error semantics. The returned
|
|
57
|
-
* `close
|
|
58
|
-
*
|
|
55
|
+
* `close` lets the caller settle the stream for conditions the Tree doesn't
|
|
56
|
+
* surface (local cancel). Session errors are wired internally to error the
|
|
57
|
+
* stream.
|
|
59
58
|
*
|
|
60
59
|
* Outputs route PURELY by the triggering input's codec-message-id — the key the
|
|
61
60
|
* client owns from send time, before the agent mints the runId. The agent's
|
|
@@ -66,7 +65,7 @@ export interface RunOutputStream {
|
|
|
66
65
|
* Used only by the run-end safety-net; routing keys on `inputCodecMessageId`.
|
|
67
66
|
* @param inputCodecMessageId - The triggering input's codec-message-id. An
|
|
68
67
|
* output routes to this stream when it carries this id.
|
|
69
|
-
* @returns The stream and its external
|
|
68
|
+
* @returns The stream and its external close handle.
|
|
70
69
|
*/
|
|
71
70
|
export const createRunOutputStream = (
|
|
72
71
|
session: VercelSession,
|
|
@@ -166,5 +165,5 @@ export const createRunOutputStream = (
|
|
|
166
165
|
}),
|
|
167
166
|
);
|
|
168
167
|
|
|
169
|
-
return { stream, close
|
|
168
|
+
return { stream, close };
|
|
170
169
|
};
|
package/src/version.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** SDK version. Kept in sync with `package.json` by the `/release` workflow. */
|
|
2
|
-
export const VERSION = '0.
|
|
2
|
+
export const VERSION = '0.3.0';
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* buildBranchChain — order a single conversation branch by walking
|
|
3
|
-
* codec-message-id parent links upward from an anchor node to the root.
|
|
4
|
-
*
|
|
5
|
-
* This is the shared ordering spine of the agent's conversation
|
|
6
|
-
* reconstruction and of history decode: both need the same root→anchor
|
|
7
|
-
* sequence of nodes before folding each node's projection. Keeping the walk
|
|
8
|
-
* here — pure, with no codec, no I/O, no logger — lets it be proven in
|
|
9
|
-
* isolation and reused by both engines without drift.
|
|
10
|
-
*
|
|
11
|
-
* Branch selection is implicit: a node reaches only its own ancestors via
|
|
12
|
-
* `parentCodecMessageId`, so sibling branches (edits / regenerates that the
|
|
13
|
-
* anchor did not descend from) are never visited. There is no separate
|
|
14
|
-
* fork/regenerate filtering step — the un-taken sibling is simply unreachable.
|
|
15
|
-
*/
|
|
16
|
-
/**
|
|
17
|
-
* The single field {@link buildBranchChain} reads from a node. Richer node-meta
|
|
18
|
-
* shapes (carrying run-id, fork-of, regenerates, …) satisfy this structurally,
|
|
19
|
-
* so callers can pass their full index map directly.
|
|
20
|
-
*/
|
|
21
|
-
export interface BranchChainNode {
|
|
22
|
-
/**
|
|
23
|
-
* Codec-message-id of this node's structural parent — the node it hangs off
|
|
24
|
-
* — or `undefined` for a root node. This is the only edge the walk follows.
|
|
25
|
-
*/
|
|
26
|
-
parentCodecMessageId: string | undefined;
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* Walk `parentCodecMessageId` links upward from `anchorCodecMessageId` and
|
|
30
|
-
* return the branch it sits on, ordered root-first (oldest) to anchor (newest,
|
|
31
|
-
* last). The anchor is always the final element.
|
|
32
|
-
*
|
|
33
|
-
* The walk stops at the root (a node with no parent), at a dangling parent
|
|
34
|
-
* (a parent id absent from `nodeMeta` is still included as the chain head,
|
|
35
|
-
* then the walk ends), or on revisiting a node (a cycle in malformed data is
|
|
36
|
-
* broken best-effort rather than looping forever).
|
|
37
|
-
* @param nodeMeta - Lookup from codec-message-id to its node meta. Need not
|
|
38
|
-
* contain the anchor or every ancestor; missing entries simply end the walk.
|
|
39
|
-
* @param anchorCodecMessageId - The codec-message-id to start the walk from
|
|
40
|
-
* (the newest node on the branch; included in the result).
|
|
41
|
-
* @returns The branch's codec-message-ids ordered root-first to anchor-last.
|
|
42
|
-
*/
|
|
43
|
-
export declare const buildBranchChain: (nodeMeta: ReadonlyMap<string, BranchChainNode>, anchorCodecMessageId: string) => string[];
|