@ably/ai-transport 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +93 -111
- package/dist/ably-ai-transport.js +2401 -1387
- 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 +116 -42
- package/dist/core/agent.d.ts +44 -0
- 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 +24 -24
- package/dist/core/codec/define-codec.d.ts +100 -0
- package/dist/core/codec/encoder.d.ts +10 -12
- 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 -2
- 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/lifecycle-tracker.d.ts +10 -9
- 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 +470 -119
- package/dist/core/codec/well-known-inputs.d.ts +52 -0
- package/dist/core/transport/agent-session.d.ts +10 -0
- package/dist/core/transport/agent-view.d.ts +296 -0
- package/dist/core/transport/client-session.d.ts +13 -0
- package/dist/core/transport/decode-fold.d.ts +55 -0
- package/dist/core/transport/headers.d.ts +121 -14
- package/dist/core/transport/index.d.ts +5 -6
- 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-history-pages.d.ts +71 -0
- package/dist/core/transport/load-history.d.ts +44 -0
- package/dist/core/transport/pipe-stream.d.ts +9 -9
- package/dist/core/transport/run-manager.d.ts +76 -0
- package/dist/core/transport/session-support.d.ts +55 -0
- package/dist/core/transport/tree.d.ts +523 -109
- package/dist/core/transport/types/agent.d.ts +375 -0
- package/dist/core/transport/types/client.d.ts +201 -0
- package/dist/core/transport/types/shared.d.ts +24 -0
- package/dist/core/transport/types/tree.d.ts +357 -0
- package/dist/core/transport/types/view.d.ts +249 -0
- package/dist/core/transport/types.d.ts +13 -553
- package/dist/core/transport/view.d.ts +390 -84
- package/dist/core/transport/wire-log.d.ts +102 -0
- package/dist/errors.d.ts +27 -10
- package/dist/index.d.ts +8 -9
- package/dist/logger.d.ts +12 -0
- package/dist/react/ably-ai-transport-react.js +1365 -1010
- 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 +37 -0
- package/dist/react/contexts/client-session-provider.d.ts +56 -0
- package/dist/react/create-session-hooks.d.ts +116 -0
- package/dist/react/index.d.ts +13 -12
- package/dist/react/internal/skipped-session.d.ts +8 -0
- package/dist/react/internal/use-resolved-session.d.ts +36 -0
- package/dist/react/use-ably-messages.d.ts +17 -14
- package/dist/react/use-client-session.d.ts +81 -0
- package/dist/react/use-create-view.d.ts +14 -13
- package/dist/react/use-tree.d.ts +30 -15
- package/dist/react/use-view.d.ts +81 -50
- package/dist/utils.d.ts +48 -71
- package/dist/vercel/ably-ai-transport-vercel.js +3257 -2499
- 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 +50 -0
- 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 +7 -20
- 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 +62 -0
- package/dist/vercel/codec/tool-transitions.d.ts +2 -8
- package/dist/vercel/codec/wire-data.d.ts +34 -0
- package/dist/vercel/index.d.ts +5 -5
- package/dist/vercel/react/ably-ai-transport-vercel-react.js +2859 -9705
- 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 -45
- package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs.map +1 -1
- package/dist/vercel/react/contexts/chat-transport-context.d.ts +9 -7
- package/dist/vercel/react/contexts/chat-transport-provider.d.ts +53 -41
- package/dist/vercel/react/index.d.ts +1 -2
- package/dist/vercel/react/use-chat-transport.d.ts +30 -26
- package/dist/vercel/react/use-message-sync.d.ts +17 -30
- package/dist/vercel/run-end-reason.d.ts +84 -0
- package/dist/vercel/tool-part.d.ts +21 -0
- package/dist/vercel/transport/chat-transport.d.ts +41 -24
- package/dist/vercel/transport/index.d.ts +24 -20
- package/dist/vercel/transport/run-output-stream.d.ts +54 -0
- package/dist/version.d.ts +2 -0
- package/package.json +31 -24
- package/src/constants.ts +124 -51
- package/src/core/agent.ts +92 -0
- package/src/core/channel-options.ts +89 -0
- package/src/core/codec/codec-event.ts +27 -0
- package/src/core/codec/decoder.ts +202 -105
- package/src/core/codec/define-codec.ts +432 -0
- package/src/core/codec/encoder.ts +114 -107
- package/src/core/codec/field-bag.ts +142 -0
- package/src/core/codec/fields.ts +193 -0
- package/src/core/codec/index.ts +56 -6
- 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/lifecycle-tracker.ts +10 -9
- 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 +505 -126
- package/src/core/codec/well-known-inputs.ts +96 -0
- package/src/core/transport/agent-session.ts +1085 -0
- package/src/core/transport/agent-view.ts +738 -0
- package/src/core/transport/client-session.ts +780 -0
- package/src/core/transport/decode-fold.ts +101 -0
- package/src/core/transport/headers.ts +234 -22
- package/src/core/transport/index.ts +27 -27
- package/src/core/transport/internal/bounded-map.ts +27 -0
- package/src/core/transport/invocation.ts +98 -0
- package/src/core/transport/load-history-pages.ts +220 -0
- package/src/core/transport/load-history.ts +271 -0
- package/src/core/transport/pipe-stream.ts +63 -39
- package/src/core/transport/run-manager.ts +243 -0
- package/src/core/transport/session-support.ts +96 -0
- package/src/core/transport/tree.ts +1293 -308
- package/src/core/transport/types/agent.ts +434 -0
- package/src/core/transport/types/client.ts +247 -0
- package/src/core/transport/types/shared.ts +27 -0
- package/src/core/transport/types/tree.ts +393 -0
- package/src/core/transport/types/view.ts +288 -0
- package/src/core/transport/types.ts +13 -706
- package/src/core/transport/view.ts +1229 -450
- package/src/core/transport/wire-log.ts +189 -0
- package/src/errors.ts +29 -9
- package/src/event-emitter.ts +3 -2
- package/src/index.ts +86 -42
- package/src/logger.ts +14 -1
- package/src/react/contexts/client-session-context.ts +41 -0
- package/src/react/contexts/client-session-provider.tsx +222 -0
- package/src/react/create-session-hooks.ts +141 -0
- package/src/react/index.ts +24 -13
- package/src/react/internal/skipped-session.ts +62 -0
- package/src/react/internal/use-resolved-session.ts +63 -0
- package/src/react/use-ably-messages.ts +32 -22
- package/src/react/use-client-session.ts +178 -0
- package/src/react/use-create-view.ts +33 -29
- package/src/react/use-tree.ts +61 -30
- package/src/react/use-view.ts +138 -96
- package/src/utils.ts +83 -131
- package/src/vercel/codec/decode-lifecycle.ts +70 -0
- package/src/vercel/codec/events.ts +85 -0
- 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 +28 -21
- 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 +191 -0
- package/src/vercel/codec/tool-transitions.ts +3 -14
- package/src/vercel/codec/wire-data.ts +64 -0
- package/src/vercel/index.ts +7 -19
- package/src/vercel/react/contexts/chat-transport-context.ts +8 -7
- package/src/vercel/react/contexts/chat-transport-provider.tsx +87 -59
- package/src/vercel/react/index.ts +3 -5
- package/src/vercel/react/use-chat-transport.ts +44 -66
- package/src/vercel/react/use-message-sync.ts +75 -39
- package/src/vercel/run-end-reason.ts +157 -0
- package/src/vercel/tool-part.ts +25 -0
- package/src/vercel/transport/chat-transport.ts +380 -98
- package/src/vercel/transport/index.ts +38 -37
- package/src/vercel/transport/run-output-stream.ts +169 -0
- package/src/version.ts +2 -0
- package/dist/core/transport/client-transport.d.ts +0 -10
- package/dist/core/transport/decode-history.d.ts +0 -43
- package/dist/core/transport/server-transport.d.ts +0 -7
- package/dist/core/transport/stream-router.d.ts +0 -29
- package/dist/core/transport/turn-manager.d.ts +0 -37
- package/dist/react/contexts/transport-context.d.ts +0 -31
- package/dist/react/contexts/transport-provider.d.ts +0 -49
- package/dist/react/create-transport-hooks.d.ts +0 -124
- package/dist/react/use-active-turns.d.ts +0 -12
- package/dist/react/use-client-transport.d.ts +0 -80
- package/dist/vercel/codec/accumulator.d.ts +0 -21
- package/dist/vercel/codec/decoder.d.ts +0 -22
- package/dist/vercel/codec/encoder.d.ts +0 -41
- package/dist/vercel/react/use-staged-add-tool-approval-response.d.ts +0 -30
- package/dist/vercel/tool-approvals.d.ts +0 -124
- package/dist/vercel/tool-events.d.ts +0 -26
- package/src/core/transport/client-transport.ts +0 -977
- package/src/core/transport/decode-history.ts +0 -485
- package/src/core/transport/server-transport.ts +0 -612
- package/src/core/transport/stream-router.ts +0 -136
- package/src/core/transport/turn-manager.ts +0 -165
- package/src/react/contexts/transport-context.ts +0 -37
- package/src/react/contexts/transport-provider.tsx +0 -164
- package/src/react/create-transport-hooks.ts +0 -144
- package/src/react/use-active-turns.ts +0 -72
- package/src/react/use-client-transport.ts +0 -197
- package/src/vercel/codec/accumulator.ts +0 -588
- package/src/vercel/codec/decoder.ts +0 -618
- package/src/vercel/codec/encoder.ts +0 -410
- package/src/vercel/react/use-staged-add-tool-approval-response.ts +0 -87
- package/src/vercel/tool-approvals.ts +0 -380
- package/src/vercel/tool-events.ts +0 -53
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool-input streaming folds: tool-input-start / -delta / -available / -error.
|
|
3
|
+
* Tool deltas arrive as raw JSON fragments accumulated in the tracker's
|
|
4
|
+
* `inputText` buffer and parsed on each delta.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type * as AI from 'ai';
|
|
8
|
+
|
|
9
|
+
import { parseJson } from '../../utils.js';
|
|
10
|
+
import { ensureMessage, ensureTrackers, getToolPart, type VercelProjection } from './reducer-state.js';
|
|
11
|
+
import { toolBase } from './tool-transitions.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Fold a tool-input streaming chunk into the projection.
|
|
15
|
+
* @param state - Projection to fold into.
|
|
16
|
+
* @param chunk - The tool-input start, delta, available, or error chunk.
|
|
17
|
+
* @param messageId - The target codec-message-id.
|
|
18
|
+
* @returns The same projection reference.
|
|
19
|
+
*/
|
|
20
|
+
export const foldToolInput = (
|
|
21
|
+
state: VercelProjection,
|
|
22
|
+
chunk: Extract<
|
|
23
|
+
AI.UIMessageChunk,
|
|
24
|
+
{ type: 'tool-input-start' | 'tool-input-delta' | 'tool-input-available' | 'tool-input-error' }
|
|
25
|
+
>,
|
|
26
|
+
messageId: string,
|
|
27
|
+
): VercelProjection => {
|
|
28
|
+
const message = ensureMessage(state, messageId);
|
|
29
|
+
const trackers = ensureTrackers(state, messageId);
|
|
30
|
+
|
|
31
|
+
switch (chunk.type) {
|
|
32
|
+
case 'tool-input-start': {
|
|
33
|
+
const partIndex = message.parts.length;
|
|
34
|
+
message.parts.push({ ...toolBase(chunk), state: 'input-streaming', input: undefined });
|
|
35
|
+
trackers.tools.set(chunk.toolCallId, { partIndex, inputText: '' });
|
|
36
|
+
return state;
|
|
37
|
+
}
|
|
38
|
+
case 'tool-input-delta': {
|
|
39
|
+
const tracker = trackers.tools.get(chunk.toolCallId);
|
|
40
|
+
if (!tracker) return state;
|
|
41
|
+
tracker.inputText += chunk.inputTextDelta;
|
|
42
|
+
|
|
43
|
+
const parsedInput = parseJson(tracker.inputText);
|
|
44
|
+
|
|
45
|
+
const found = getToolPart(message, trackers, chunk.toolCallId);
|
|
46
|
+
if (!found) return state;
|
|
47
|
+
message.parts[found.tracker.partIndex] = {
|
|
48
|
+
...toolBase(found.part),
|
|
49
|
+
state: 'input-streaming',
|
|
50
|
+
input: parsedInput,
|
|
51
|
+
};
|
|
52
|
+
return state;
|
|
53
|
+
}
|
|
54
|
+
case 'tool-input-available': {
|
|
55
|
+
const found = getToolPart(message, trackers, chunk.toolCallId);
|
|
56
|
+
if (!found) return state;
|
|
57
|
+
message.parts[found.tracker.partIndex] = {
|
|
58
|
+
...toolBase(found.part),
|
|
59
|
+
state: 'input-available',
|
|
60
|
+
input: chunk.input,
|
|
61
|
+
};
|
|
62
|
+
return state;
|
|
63
|
+
}
|
|
64
|
+
case 'tool-input-error': {
|
|
65
|
+
const found = getToolPart(message, trackers, chunk.toolCallId);
|
|
66
|
+
if (found) {
|
|
67
|
+
message.parts[found.tracker.partIndex] = {
|
|
68
|
+
...toolBase(found.part),
|
|
69
|
+
state: 'output-error',
|
|
70
|
+
input: chunk.input,
|
|
71
|
+
errorText: chunk.errorText,
|
|
72
|
+
};
|
|
73
|
+
} else {
|
|
74
|
+
const partIndex = message.parts.length;
|
|
75
|
+
message.parts.push({
|
|
76
|
+
...toolBase(chunk),
|
|
77
|
+
state: 'output-error',
|
|
78
|
+
input: chunk.input,
|
|
79
|
+
errorText: chunk.errorText,
|
|
80
|
+
});
|
|
81
|
+
trackers.tools.set(chunk.toolCallId, { partIndex, inputText: '' });
|
|
82
|
+
}
|
|
83
|
+
return state;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent-published tool-output transitions: tool-output-available /
|
|
3
|
+
* tool-output-error / tool-output-denied / tool-approval-request.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type * as AI from 'ai';
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
ensureMessage,
|
|
10
|
+
ensureTrackers,
|
|
11
|
+
getToolPart,
|
|
12
|
+
type OwnerLookup,
|
|
13
|
+
type VercelProjection,
|
|
14
|
+
} from './reducer-state.js';
|
|
15
|
+
import { transitionToolPart } from './tool-transitions.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Locate the `dynamic-tool` part for a `toolCallId` anywhere in the projection.
|
|
19
|
+
* Agent-emitted second-pass tool outputs (after an approved tool runs) are
|
|
20
|
+
* stamped with a fresh codec-message-id that differs from the assistant holding
|
|
21
|
+
* the tool call, so they can't be found via `meta.messageId` — they fold onto
|
|
22
|
+
* whichever message holds the matching tool call (created in the first pass or
|
|
23
|
+
* by an approval response).
|
|
24
|
+
* @param state - Projection to scan.
|
|
25
|
+
* @param toolCallId - The tool call to locate.
|
|
26
|
+
* @returns The owning message, tracker, and part, or `undefined` if absent.
|
|
27
|
+
*/
|
|
28
|
+
const findToolPartOwner = (state: VercelProjection, toolCallId: string): OwnerLookup | undefined => {
|
|
29
|
+
for (const entry of state.messages) {
|
|
30
|
+
const trackers = state.trackers.get(entry.codecMessageId);
|
|
31
|
+
if (!trackers) continue;
|
|
32
|
+
const found = getToolPart(entry.message, trackers, toolCallId);
|
|
33
|
+
if (found) return { message: entry.message, tracker: found.tracker, part: found.part };
|
|
34
|
+
}
|
|
35
|
+
return undefined;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Fold an agent-published tool-output chunk into the projection.
|
|
40
|
+
* @param state - Projection to fold into.
|
|
41
|
+
* @param chunk - The tool-output-available/-error/-denied or tool-approval-request chunk.
|
|
42
|
+
* @param messageId - The target codec-message-id (used for the approval-request / denied paths).
|
|
43
|
+
* @returns The same projection reference.
|
|
44
|
+
*/
|
|
45
|
+
export const foldToolOutput = (
|
|
46
|
+
state: VercelProjection,
|
|
47
|
+
chunk: Extract<
|
|
48
|
+
AI.UIMessageChunk,
|
|
49
|
+
{ type: 'tool-output-available' | 'tool-output-error' | 'tool-output-denied' | 'tool-approval-request' }
|
|
50
|
+
>,
|
|
51
|
+
messageId: string,
|
|
52
|
+
): VercelProjection => {
|
|
53
|
+
// `tool-output-available` / `tool-output-error` after an approved tool runs
|
|
54
|
+
// are emitted by streamText's continuation pass under a fresh
|
|
55
|
+
// codec-message-id that differs from the assistant holding the tool call.
|
|
56
|
+
// Resolve the owning part by toolCallId across the whole projection so the
|
|
57
|
+
// output folds onto the original message. Deliberately do NOT materialise
|
|
58
|
+
// `messageId` first — that would leave a phantom empty message behind the
|
|
59
|
+
// fresh id. Drop on miss: a tool output with no matching tool call has no
|
|
60
|
+
// anchor to attach to.
|
|
61
|
+
if (chunk.type === 'tool-output-available' || chunk.type === 'tool-output-error') {
|
|
62
|
+
const owner = findToolPartOwner(state, chunk.toolCallId);
|
|
63
|
+
if (!owner) return state;
|
|
64
|
+
owner.message.parts[owner.tracker.partIndex] = transitionToolPart(owner.part, chunk);
|
|
65
|
+
return state;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// `tool-approval-request` (first pass) creates the part on the run's own
|
|
69
|
+
// message; `tool-output-denied` transitions that same part. Both key on the
|
|
70
|
+
// stamped messageId.
|
|
71
|
+
const message = ensureMessage(state, messageId);
|
|
72
|
+
const trackers = ensureTrackers(state, messageId);
|
|
73
|
+
|
|
74
|
+
const found = getToolPart(message, trackers, chunk.toolCallId);
|
|
75
|
+
if (!found) return state;
|
|
76
|
+
|
|
77
|
+
message.parts[found.tracker.partIndex] = transitionToolPart(found.part, chunk);
|
|
78
|
+
return state;
|
|
79
|
+
};
|
|
@@ -1,35 +1,42 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Vercel AI SDK codec —
|
|
3
|
-
*
|
|
2
|
+
* Vercel AI SDK codec — `UIMessageCodec`.
|
|
3
|
+
*
|
|
4
|
+
* Assembled by `defineCodec` from the codec's parts: the reducer
|
|
5
|
+
* (`init`/`fold`/`getMessages`), the declarative output and input descriptor
|
|
6
|
+
* tables (`outputs` / `inputs`, each a builder function `defineCodec` injects
|
|
7
|
+
* the direction-scoped builder into), and the decode lifecycle policy.
|
|
8
|
+
* `defineCodec` builds the generic encoder/decoder and merges the well-known
|
|
9
|
+
* input factories internally.
|
|
4
10
|
*
|
|
5
11
|
* ```ts
|
|
6
12
|
* import { UIMessageCodec } from '@ably/ai-transport/vercel';
|
|
7
13
|
*
|
|
8
14
|
* const encoder = UIMessageCodec.createEncoder(writer, options);
|
|
9
15
|
* const decoder = UIMessageCodec.createDecoder();
|
|
10
|
-
* const
|
|
16
|
+
* const projection = UIMessageCodec.init();
|
|
11
17
|
* ```
|
|
12
18
|
*/
|
|
13
19
|
|
|
14
|
-
import
|
|
15
|
-
|
|
16
|
-
import type {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
+
import { defineCodec } from '../../core/codec/index.js';
|
|
21
|
+
import { createVercelDecodeLifecycle } from './decode-lifecycle.js';
|
|
22
|
+
import type { VercelInput, VercelOutput } from './events.js';
|
|
23
|
+
import { inputs } from './inputs.js';
|
|
24
|
+
import { outputs } from './outputs.js';
|
|
25
|
+
import { fold, getMessages, init } from './reducer.js';
|
|
20
26
|
|
|
21
27
|
/**
|
|
22
|
-
* Vercel AI SDK codec implementing
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* that map between Vercel's UIMessageChunk/UIMessage types and Ably's native
|
|
26
|
-
* message primitives.
|
|
28
|
+
* Vercel AI SDK codec implementing
|
|
29
|
+
* `Codec<VercelInput, VercelOutput, VercelProjection, UIMessage>`. `VercelProjection`
|
|
30
|
+
* and `UIMessage` are inferred from the reducer.
|
|
27
31
|
*/
|
|
28
|
-
export const UIMessageCodec
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
+
export const UIMessageCodec = defineCodec<VercelInput, VercelOutput>()({
|
|
33
|
+
// Spec: AIT-CT1a3, AIT-ST1a3 — registers this codec as an Ably agent.
|
|
34
|
+
adapterTag: 'vercel-ai-sdk-ui-message',
|
|
35
|
+
reducer: { init, fold, getMessages },
|
|
36
|
+
output: outputs,
|
|
37
|
+
input: inputs,
|
|
38
|
+
decodeLifecycle: createVercelDecodeLifecycle,
|
|
39
|
+
});
|
|
32
40
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
};
|
|
41
|
+
export type { VercelInput, VercelOutput } from './events.js';
|
|
42
|
+
export { type VercelProjection } from './reducer.js';
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vercel input (`ai-input`) descriptors — the single source of truth for the
|
|
3
|
+
* `VercelInput` wire mapping, the user-message fan-out included.
|
|
4
|
+
*
|
|
5
|
+
* `defineCodec` injects the direction-scoped `{ event, batch }` builder; the
|
|
6
|
+
* generic input drivers consume the returned array. The tool inputs are single
|
|
7
|
+
* `event`s lensed onto their nested `payload`; `regenerate` is a wire-only
|
|
8
|
+
* signal; the multi-part user message is a `batch` that fans each
|
|
9
|
+
* `UIMessage` part out into one wire event (reassembled by the reducer).
|
|
10
|
+
*
|
|
11
|
+
* Author-facing acceptance gate: the injected `event`/`batch` builders narrow
|
|
12
|
+
* each member, so every `data` / `fields` / `parts` / `assemble` callback is
|
|
13
|
+
* fully typed. The file's single `as` cast is the wire trust boundary on the
|
|
14
|
+
* inbound role header (see `assemble`).
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type * as AI from 'ai';
|
|
18
|
+
|
|
19
|
+
import { HEADER_ROLE } from '../../constants.js';
|
|
20
|
+
import type { InputBuilder, InputDescriptor } from '../../core/codec/index.js';
|
|
21
|
+
import type { VercelInput } from './events.js';
|
|
22
|
+
import { fApproved, fId, fMediaType, fMessageId, fReason, fToolCallId } from './fields.js';
|
|
23
|
+
import { asString, isClientToolResultErrorWireData, isToolOutputAvailableWireData } from './wire-data.js';
|
|
24
|
+
|
|
25
|
+
/** Fallback for a message with no encodable parts (see the `user-message` batch). */
|
|
26
|
+
const EMPTY_MESSAGE_PARTS: AI.UIMessage['parts'] = [{ type: 'text', text: '' }];
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Part types the `user-message` batch's `parts` sub-table can encode — must
|
|
30
|
+
* stay in step with that table. Parts outside this set (e.g. `step-start`,
|
|
31
|
+
* tool parts) have no wire mapping; `explode` filters them so the batch always
|
|
32
|
+
* yields at least one encodable part and the message round-trips.
|
|
33
|
+
* @param part - The UIMessage part to test.
|
|
34
|
+
* @returns Whether the part has a wire mapping in the batch's part table.
|
|
35
|
+
*/
|
|
36
|
+
const isEncodablePart = (part: AI.UIMessage['parts'][number]): boolean =>
|
|
37
|
+
part.type === 'text' || part.type === 'file' || part.type.startsWith('data-');
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The Vercel codec's `ai-input` descriptors, built from the injected
|
|
41
|
+
* direction-scoped builder.
|
|
42
|
+
* @param builder - The `{ event, batch }` builder curried on `VercelInput`.
|
|
43
|
+
* @param builder.event - Define a single-event input (payload-nested, or `wireOnly`).
|
|
44
|
+
* @param builder.batch - Define a multi-part (batch) input that fans out into one wire event per part.
|
|
45
|
+
* @returns The input descriptor table the generic input drivers consume.
|
|
46
|
+
*/
|
|
47
|
+
export const inputs = ({ event, batch }: InputBuilder<VercelInput>): readonly InputDescriptor<VercelInput>[] => [
|
|
48
|
+
// --- tool inputs: nested payload, codec-message-id-addressed ----------------
|
|
49
|
+
|
|
50
|
+
event('tool-result', {
|
|
51
|
+
fields: [fToolCallId],
|
|
52
|
+
data: {
|
|
53
|
+
encode: (p) => ({ output: p.output }),
|
|
54
|
+
// Malformed wire data decodes to undefined, which the rebuild seam strips
|
|
55
|
+
// — the folded payload then has no `output` key (reads as undefined).
|
|
56
|
+
decode: (d) => ({ output: isToolOutputAvailableWireData(d) ? d.output : undefined }),
|
|
57
|
+
},
|
|
58
|
+
}),
|
|
59
|
+
event('tool-result-error', {
|
|
60
|
+
fields: [fToolCallId],
|
|
61
|
+
data: {
|
|
62
|
+
encode: (p) => ({ message: p.message }),
|
|
63
|
+
decode: (d) => ({ message: isClientToolResultErrorWireData(d) ? (d.message ?? '') : '' }),
|
|
64
|
+
},
|
|
65
|
+
}),
|
|
66
|
+
event('tool-approval-response', { fields: [fToolCallId, fApproved, fReason] }),
|
|
67
|
+
|
|
68
|
+
// --- wire-only signal -------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
// `regenerate` carries no domain payload; `parent` / `target` ride the
|
|
71
|
+
// transport headers built by the client-session and read by the agent's
|
|
72
|
+
// input-event lookup, so it stamps only the `kind` header and decodes to [].
|
|
73
|
+
event('regenerate', { wireOnly: true }),
|
|
74
|
+
|
|
75
|
+
// --- multi-part client message ----------------------------------------------
|
|
76
|
+
|
|
77
|
+
// The user message fans out into one wire event per part, all sharing the
|
|
78
|
+
// `user-message` kind and codec-message-id, each carrying its `partType`. The
|
|
79
|
+
// message id (a codec header) and role (a transport header) are per-message,
|
|
80
|
+
// stamped on every part so the decode side can rebuild the envelope from any
|
|
81
|
+
// one; the reducer merges parts sharing a codec-message-id.
|
|
82
|
+
batch('user-message', {
|
|
83
|
+
// A message with no encodable parts (empty, or only unmapped types like
|
|
84
|
+
// step-start) still publishes one empty text part, so the codec-message-id
|
|
85
|
+
// and role survive and it round-trips to a one-part message. The driver's
|
|
86
|
+
// bare-headers fallback cannot round-trip (it carries no partType), so the
|
|
87
|
+
// ≥1-encodable-part guarantee lives here.
|
|
88
|
+
explode: (input) => {
|
|
89
|
+
const encodable = input.message.parts.filter((part) => isEncodablePart(part));
|
|
90
|
+
return encodable.length > 0 ? encodable : EMPTY_MESSAGE_PARTS;
|
|
91
|
+
},
|
|
92
|
+
partTypeOf: (part) => part.type,
|
|
93
|
+
parts: (p) => [
|
|
94
|
+
p('text', { data: { encode: (x) => x.text, decode: (d) => ({ text: asString(d) }) } }),
|
|
95
|
+
p('file', {
|
|
96
|
+
fields: [fMediaType],
|
|
97
|
+
data: { encode: (x) => x.url, decode: (d) => ({ url: asString(d) }) },
|
|
98
|
+
}),
|
|
99
|
+
p('data-*', {
|
|
100
|
+
fields: [fId],
|
|
101
|
+
data: { encode: (x) => x.data, decode: (d) => ({ data: d }) },
|
|
102
|
+
}),
|
|
103
|
+
],
|
|
104
|
+
messageHeaders: (input) => {
|
|
105
|
+
const codecHeaders: Record<string, string> = {};
|
|
106
|
+
fMessageId.write(codecHeaders, input.message.id);
|
|
107
|
+
return { codecHeaders, transportHeaders: { [HEADER_ROLE]: input.message.role } };
|
|
108
|
+
},
|
|
109
|
+
assemble: (part, { codecHeaders, transportHeaders }) => {
|
|
110
|
+
// CAST: HEADER_ROLE is wire data; the role string is trusted as a UIMessage role.
|
|
111
|
+
const role = (transportHeaders[HEADER_ROLE] ?? 'user') as AI.UIMessage['role'];
|
|
112
|
+
const id = fMessageId.read(codecHeaders) ?? '';
|
|
113
|
+
return { message: { id, role, parts: [part] } };
|
|
114
|
+
},
|
|
115
|
+
}),
|
|
116
|
+
];
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vercel output (`ai-output`) event descriptors — the single source of truth for
|
|
3
|
+
* encoding/decoding `UIMessageChunk` outputs. `defineCodec` injects the
|
|
4
|
+
* direction-scoped `{ event, stream }` builder; the generic drivers consume the
|
|
5
|
+
* returned array. Adding an ordinary output event is one entry here.
|
|
6
|
+
*
|
|
7
|
+
* Author-facing acceptance gate: this file contains **zero `as` casts**. The
|
|
8
|
+
* injected `event`/`stream` builders narrow each chunk member, so the
|
|
9
|
+
* `data`/`encode`/`decode` callbacks are fully typed.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import * as Ably from 'ably';
|
|
13
|
+
|
|
14
|
+
import type { OutputBuilder, OutputDescriptor } from '../../core/codec/index.js';
|
|
15
|
+
import { boolField, jsonField, strField } from '../../core/codec/index.js';
|
|
16
|
+
import { ErrorCode, errorInfoIs } from '../../errors.js';
|
|
17
|
+
import { parseJsonOrString, stripUndefined } from '../../utils.js';
|
|
18
|
+
import type { VercelOutput } from './events.js';
|
|
19
|
+
import {
|
|
20
|
+
fDynamic,
|
|
21
|
+
fFinishReason,
|
|
22
|
+
fId,
|
|
23
|
+
fMediaType,
|
|
24
|
+
fMessageId,
|
|
25
|
+
fMeta,
|
|
26
|
+
fProviderExecuted,
|
|
27
|
+
fSourceId,
|
|
28
|
+
fTitle,
|
|
29
|
+
fToolCallId,
|
|
30
|
+
fToolName,
|
|
31
|
+
} from './fields.js';
|
|
32
|
+
import {
|
|
33
|
+
asString,
|
|
34
|
+
isAgentToolOutputErrorWireData,
|
|
35
|
+
isToolInputErrorWireData,
|
|
36
|
+
isToolOutputAvailableWireData,
|
|
37
|
+
} from './wire-data.js';
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The Vercel codec's `ai-output` descriptors, built from the injected
|
|
41
|
+
* direction-scoped builder.
|
|
42
|
+
* @param builder - The `{ event, stream }` builder curried on `VercelOutput`.
|
|
43
|
+
* @param builder.event - Define a single discrete output event.
|
|
44
|
+
* @param builder.stream - Define a streamed output family (start / delta / end).
|
|
45
|
+
* @returns The output descriptor table the generic output drivers consume.
|
|
46
|
+
*/
|
|
47
|
+
export const outputs = ({ event, stream }: OutputBuilder<VercelOutput>): readonly OutputDescriptor<VercelOutput>[] => [
|
|
48
|
+
// --- streamed families -----------------------------------------------------
|
|
49
|
+
|
|
50
|
+
stream('text', {
|
|
51
|
+
start: 'text-start',
|
|
52
|
+
delta: 'text-delta',
|
|
53
|
+
end: 'text-end',
|
|
54
|
+
idField: 'id',
|
|
55
|
+
deltaField: 'delta',
|
|
56
|
+
fields: [fId, fMeta],
|
|
57
|
+
}),
|
|
58
|
+
|
|
59
|
+
stream('reasoning', {
|
|
60
|
+
start: 'reasoning-start',
|
|
61
|
+
delta: 'reasoning-delta',
|
|
62
|
+
end: 'reasoning-end',
|
|
63
|
+
idField: 'id',
|
|
64
|
+
deltaField: 'delta',
|
|
65
|
+
fields: [fId, fMeta],
|
|
66
|
+
}),
|
|
67
|
+
|
|
68
|
+
// tool-input streams; the close step is a close-or-discrete fallback, the end
|
|
69
|
+
// chunk reconstructs `input` from the accumulated text, and the family also
|
|
70
|
+
// decodes from a non-streamed discrete publish.
|
|
71
|
+
stream('tool-input', {
|
|
72
|
+
start: 'tool-input-start',
|
|
73
|
+
delta: 'tool-input-delta',
|
|
74
|
+
end: 'tool-input-available',
|
|
75
|
+
idField: 'toolCallId',
|
|
76
|
+
deltaField: 'inputTextDelta',
|
|
77
|
+
fields: [fToolCallId, fToolName, fDynamic, fTitle, fProviderExecuted, fMeta],
|
|
78
|
+
onEnd: async (c, core, { h, name }) => {
|
|
79
|
+
try {
|
|
80
|
+
await core.closeStream(c.toolCallId, {
|
|
81
|
+
name,
|
|
82
|
+
data: '',
|
|
83
|
+
codecHeaders: h(c, ['toolCallId', 'toolName', 'providerMetadata']),
|
|
84
|
+
});
|
|
85
|
+
} catch (error: unknown) {
|
|
86
|
+
// closeStream raises InvalidArgument when there is no active stream for
|
|
87
|
+
// this id; fall through to a discrete publish, rethrow anything else.
|
|
88
|
+
if (!(error instanceof Ably.ErrorInfo && errorInfoIs(error, ErrorCode.InvalidArgument))) {
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
await core.publishDiscrete({ name, data: c.input, codecHeaders: h(c) });
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
decodeEnd: ({ streamId, accumulated, codecHeaders, closingCodecHeaders }) => [
|
|
95
|
+
stripUndefined({
|
|
96
|
+
type: 'tool-input-available' as const,
|
|
97
|
+
toolCallId: streamId,
|
|
98
|
+
toolName: fToolName.read(closingCodecHeaders) || fToolName.read(codecHeaders),
|
|
99
|
+
input: parseJsonOrString(accumulated),
|
|
100
|
+
providerMetadata: fMeta.read(closingCodecHeaders),
|
|
101
|
+
}),
|
|
102
|
+
],
|
|
103
|
+
decodeDiscrete: ({ codecHeaders, data }) => [
|
|
104
|
+
stripUndefined({
|
|
105
|
+
type: 'tool-input-start' as const,
|
|
106
|
+
toolCallId: fToolCallId.read(codecHeaders),
|
|
107
|
+
toolName: fToolName.read(codecHeaders),
|
|
108
|
+
dynamic: fDynamic.read(codecHeaders),
|
|
109
|
+
title: fTitle.read(codecHeaders),
|
|
110
|
+
providerExecuted: fProviderExecuted.read(codecHeaders),
|
|
111
|
+
providerMetadata: fMeta.read(codecHeaders),
|
|
112
|
+
}),
|
|
113
|
+
stripUndefined({
|
|
114
|
+
type: 'tool-input-available' as const,
|
|
115
|
+
toolCallId: fToolCallId.read(codecHeaders),
|
|
116
|
+
toolName: fToolName.read(codecHeaders),
|
|
117
|
+
input: data,
|
|
118
|
+
providerMetadata: fMeta.read(codecHeaders),
|
|
119
|
+
}),
|
|
120
|
+
],
|
|
121
|
+
}),
|
|
122
|
+
|
|
123
|
+
// --- discrete lifecycle events ---------------------------------------------
|
|
124
|
+
|
|
125
|
+
// `start` injects the encoder's configured messageId as a fallback, so it
|
|
126
|
+
// builds its headers through a hatch rather than a pure descriptor.
|
|
127
|
+
event('start', {
|
|
128
|
+
fields: [fMessageId, jsonField('messageMetadata')],
|
|
129
|
+
encode: async (c, core, { h, name, messageId, opts }) => {
|
|
130
|
+
await core.publishDiscrete(
|
|
131
|
+
{ name, data: '', codecHeaders: h({ ...c, messageId: c.messageId ?? messageId }) },
|
|
132
|
+
opts,
|
|
133
|
+
);
|
|
134
|
+
},
|
|
135
|
+
}),
|
|
136
|
+
event('start-step'),
|
|
137
|
+
event('finish-step'),
|
|
138
|
+
event('finish', {
|
|
139
|
+
fields: [fFinishReason, jsonField('messageMetadata')],
|
|
140
|
+
}),
|
|
141
|
+
event('message-metadata', { fields: [jsonField('messageMetadata')] }),
|
|
142
|
+
event('error', {
|
|
143
|
+
data: { encode: (c) => c.errorText, decode: (data) => ({ errorText: asString(data) }) },
|
|
144
|
+
}),
|
|
145
|
+
|
|
146
|
+
// abort: an ordinary discrete output carrying its reason as wire data. The
|
|
147
|
+
// agent's own stream emits it on abort; run cancellation closes in-flight
|
|
148
|
+
// streams via the encoder's cancelStreams() and terminates via the transport
|
|
149
|
+
// ai-run-end event — this chunk is content, not the run terminator.
|
|
150
|
+
event('abort', {
|
|
151
|
+
data: {
|
|
152
|
+
encode: (c) => c.reason ?? '',
|
|
153
|
+
decode: (data) => (typeof data === 'string' && data ? { reason: data } : {}),
|
|
154
|
+
},
|
|
155
|
+
}),
|
|
156
|
+
|
|
157
|
+
// --- content parts ---------------------------------------------------------
|
|
158
|
+
|
|
159
|
+
event('file', {
|
|
160
|
+
fields: [fMediaType, fMeta],
|
|
161
|
+
data: { encode: (c) => c.url, decode: (data) => ({ url: asString(data) }) },
|
|
162
|
+
}),
|
|
163
|
+
event('source-url', {
|
|
164
|
+
fields: [fSourceId, fTitle, fMeta],
|
|
165
|
+
data: { encode: (c) => c.url, decode: (data) => ({ url: asString(data) }) },
|
|
166
|
+
}),
|
|
167
|
+
event('source-document', {
|
|
168
|
+
fields: [fSourceId, fMediaType, strField('title', ''), strField('filename'), fMeta],
|
|
169
|
+
}),
|
|
170
|
+
|
|
171
|
+
// --- tool lifecycle (discrete) ---------------------------------------------
|
|
172
|
+
|
|
173
|
+
event('tool-input-error', {
|
|
174
|
+
fields: [fToolCallId, fToolName, fDynamic, fTitle, fProviderExecuted, fMeta],
|
|
175
|
+
data: {
|
|
176
|
+
encode: (c) => ({ errorText: c.errorText, input: c.input }),
|
|
177
|
+
decode: (data) =>
|
|
178
|
+
isToolInputErrorWireData(data) ? { errorText: data.errorText ?? '', input: data.input } : { errorText: '' },
|
|
179
|
+
},
|
|
180
|
+
}),
|
|
181
|
+
event('tool-output-available', {
|
|
182
|
+
fields: [fToolCallId, fDynamic, fProviderExecuted, boolField('preliminary')],
|
|
183
|
+
data: {
|
|
184
|
+
encode: (c) => ({ output: c.output }),
|
|
185
|
+
decode: (data) => (isToolOutputAvailableWireData(data) ? { output: data.output } : {}),
|
|
186
|
+
},
|
|
187
|
+
}),
|
|
188
|
+
event('tool-output-error', {
|
|
189
|
+
fields: [fToolCallId, fDynamic, fProviderExecuted],
|
|
190
|
+
data: {
|
|
191
|
+
encode: (c) => ({ errorText: c.errorText }),
|
|
192
|
+
decode: (data) => ({ errorText: isAgentToolOutputErrorWireData(data) ? (data.errorText ?? '') : '' }),
|
|
193
|
+
},
|
|
194
|
+
}),
|
|
195
|
+
event('tool-approval-request', {
|
|
196
|
+
fields: [fToolCallId, strField('approvalId', '')],
|
|
197
|
+
}),
|
|
198
|
+
event('tool-output-denied', { fields: [fToolCallId] }),
|
|
199
|
+
|
|
200
|
+
// --- data-* wildcard -------------------------------------------------------
|
|
201
|
+
|
|
202
|
+
event('data-*', {
|
|
203
|
+
fields: [fId, boolField('transient')],
|
|
204
|
+
ephemeral: (c) => c.transient === true,
|
|
205
|
+
data: { encode: (c) => c.data, decode: (data) => ({ data }) },
|
|
206
|
+
}),
|
|
207
|
+
];
|