@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
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lifecycle chunk folds: start, start-step, finish-step, finish, abort,
|
|
3
|
+
* error, message-metadata.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type * as AI from 'ai';
|
|
7
|
+
|
|
8
|
+
import { ensureMessage, type VercelProjection } from './reducer-state.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Set a message's metadata from a chunk when both the message exists and the
|
|
12
|
+
* chunk carries metadata. Shared by the `finish` and `message-metadata` cases,
|
|
13
|
+
* which apply it identically. The `start` case is not routed through here — it
|
|
14
|
+
* creates the message via `ensureMessage` first.
|
|
15
|
+
* @param state - Projection holding the message.
|
|
16
|
+
* @param messageId - The target codec-message-id.
|
|
17
|
+
* @param metadata - The chunk's `messageMetadata`, or undefined to leave it unchanged.
|
|
18
|
+
*/
|
|
19
|
+
const applyMessageMetadata = (state: VercelProjection, messageId: string, metadata: AI.UIMessage['metadata']): void => {
|
|
20
|
+
if (metadata === undefined) return;
|
|
21
|
+
const message = state.messages.find((e) => e.codecMessageId === messageId)?.message;
|
|
22
|
+
if (message) message.metadata = metadata;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Fold a message-lifecycle chunk into the projection.
|
|
27
|
+
* @param state - Projection to fold into.
|
|
28
|
+
* @param chunk - The lifecycle chunk.
|
|
29
|
+
* @param messageId - The target codec-message-id.
|
|
30
|
+
* @returns The same projection reference.
|
|
31
|
+
*/
|
|
32
|
+
export const foldLifecycle = (
|
|
33
|
+
state: VercelProjection,
|
|
34
|
+
chunk: Extract<
|
|
35
|
+
AI.UIMessageChunk,
|
|
36
|
+
{ type: 'start' | 'start-step' | 'finish-step' | 'finish' | 'abort' | 'error' | 'message-metadata' }
|
|
37
|
+
>,
|
|
38
|
+
messageId: string,
|
|
39
|
+
): VercelProjection => {
|
|
40
|
+
switch (chunk.type) {
|
|
41
|
+
case 'start': {
|
|
42
|
+
// The projection entry is keyed on the wire codec-message-id
|
|
43
|
+
// (`messageId`); every subsequent chunk for this message correlates on
|
|
44
|
+
// that, independent of `message.id`. So we faithfully reproduce the
|
|
45
|
+
// stream's own `messageId` on the reconstructed `UIMessage.id` (the
|
|
46
|
+
// value surfaced to the application) without risk of orphaning later
|
|
47
|
+
// chunks. When the stream omits it, the codec-message-id seeded by
|
|
48
|
+
// `ensureMessage` stands as the fallback id.
|
|
49
|
+
const message = ensureMessage(state, messageId);
|
|
50
|
+
if (chunk.messageId !== undefined) message.id = chunk.messageId;
|
|
51
|
+
if (chunk.messageMetadata !== undefined) message.metadata = chunk.messageMetadata;
|
|
52
|
+
return state;
|
|
53
|
+
}
|
|
54
|
+
case 'start-step': {
|
|
55
|
+
const message = ensureMessage(state, messageId);
|
|
56
|
+
message.parts.push({ type: 'step-start' });
|
|
57
|
+
return state;
|
|
58
|
+
}
|
|
59
|
+
case 'finish-step': {
|
|
60
|
+
// Reset text/reasoning stream trackers so a follow-up step can start
|
|
61
|
+
// new parts with potentially-reused stream ids.
|
|
62
|
+
const trackers = state.trackers.get(messageId);
|
|
63
|
+
if (trackers) {
|
|
64
|
+
trackers.text.clear();
|
|
65
|
+
trackers.reasoning.clear();
|
|
66
|
+
}
|
|
67
|
+
return state;
|
|
68
|
+
}
|
|
69
|
+
case 'finish': {
|
|
70
|
+
applyMessageMetadata(state, messageId, chunk.messageMetadata);
|
|
71
|
+
// Tracker state retained — late events still resolvable; cleanup happens at Run end.
|
|
72
|
+
return state;
|
|
73
|
+
}
|
|
74
|
+
case 'abort':
|
|
75
|
+
case 'error': {
|
|
76
|
+
// No state mutation — run termination is observed via the wire run-end
|
|
77
|
+
// event, not the projection.
|
|
78
|
+
return state;
|
|
79
|
+
}
|
|
80
|
+
case 'message-metadata': {
|
|
81
|
+
applyMessageMetadata(state, messageId, chunk.messageMetadata);
|
|
82
|
+
return state;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text and reasoning streaming folds: the {start, delta, end} lifecycle for
|
|
3
|
+
* both `text-*` and `reasoning-*` chunks, which share the same shape.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type * as AI from 'ai';
|
|
7
|
+
|
|
8
|
+
import { ensureMessage, ensureTrackers, type VercelProjection } from './reducer-state.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Fold a text or reasoning streaming chunk into the projection.
|
|
12
|
+
* @param state - Projection to fold into.
|
|
13
|
+
* @param chunk - The text/reasoning start, delta, or end chunk.
|
|
14
|
+
* @param messageId - The target codec-message-id.
|
|
15
|
+
* @returns The same projection reference.
|
|
16
|
+
*/
|
|
17
|
+
export const foldTextOrReasoning = (
|
|
18
|
+
state: VercelProjection,
|
|
19
|
+
chunk: Extract<
|
|
20
|
+
AI.UIMessageChunk,
|
|
21
|
+
{ type: 'text-start' | 'text-delta' | 'text-end' | 'reasoning-start' | 'reasoning-delta' | 'reasoning-end' }
|
|
22
|
+
>,
|
|
23
|
+
messageId: string,
|
|
24
|
+
): VercelProjection => {
|
|
25
|
+
const message = ensureMessage(state, messageId);
|
|
26
|
+
const trackers = ensureTrackers(state, messageId);
|
|
27
|
+
|
|
28
|
+
const isText = chunk.type.startsWith('text-');
|
|
29
|
+
const partType = isText ? 'text' : 'reasoning';
|
|
30
|
+
const activeMap = isText ? trackers.text : trackers.reasoning;
|
|
31
|
+
|
|
32
|
+
switch (chunk.type) {
|
|
33
|
+
case 'text-start':
|
|
34
|
+
case 'reasoning-start': {
|
|
35
|
+
activeMap.set(chunk.id, message.parts.length);
|
|
36
|
+
message.parts.push({ type: partType, text: '' });
|
|
37
|
+
return state;
|
|
38
|
+
}
|
|
39
|
+
case 'text-delta':
|
|
40
|
+
case 'reasoning-delta': {
|
|
41
|
+
const idx = activeMap.get(chunk.id);
|
|
42
|
+
if (idx === undefined) return state;
|
|
43
|
+
const part = message.parts[idx];
|
|
44
|
+
if (part?.type === partType) {
|
|
45
|
+
part.text += chunk.delta;
|
|
46
|
+
}
|
|
47
|
+
return state;
|
|
48
|
+
}
|
|
49
|
+
case 'text-end':
|
|
50
|
+
case 'reasoning-end': {
|
|
51
|
+
activeMap.delete(chunk.id);
|
|
52
|
+
return state;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
};
|
|
@@ -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,10 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Vercel AI SDK codec —
|
|
3
|
-
* `Codec<VercelInput, VercelOutput, VercelProjection, UIMessage>`.
|
|
2
|
+
* Vercel AI SDK codec — `UIMessageCodec`.
|
|
4
3
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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.
|
|
8
10
|
*
|
|
9
11
|
* ```ts
|
|
10
12
|
* import { UIMessageCodec } from '@ably/ai-transport/vercel';
|
|
@@ -15,68 +17,26 @@
|
|
|
15
17
|
* ```
|
|
16
18
|
*/
|
|
17
19
|
|
|
18
|
-
import
|
|
19
|
-
|
|
20
|
-
import type {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import
|
|
24
|
-
VercelInput,
|
|
25
|
-
VercelOutput,
|
|
26
|
-
VercelToolApprovalResponsePayload,
|
|
27
|
-
VercelToolResultErrorPayload,
|
|
28
|
-
VercelToolResultPayload,
|
|
29
|
-
} from './events.js';
|
|
30
|
-
import { fold, getMessages, init, type VercelProjection } from './reducer.js';
|
|
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';
|
|
31
26
|
|
|
32
27
|
/**
|
|
33
28
|
* Vercel AI SDK codec implementing
|
|
34
|
-
* `Codec<VercelInput, VercelOutput, VercelProjection, UIMessage>`.
|
|
35
|
-
*
|
|
36
|
-
* Folds `VercelInput`s and `VercelOutput`s into a `VercelProjection`
|
|
37
|
-
* carrying `UIMessage[]`. Encoder and decoder factories handle the wire
|
|
38
|
-
* mapping for both directions.
|
|
29
|
+
* `Codec<VercelInput, VercelOutput, VercelProjection, UIMessage>`. `VercelProjection`
|
|
30
|
+
* and `UIMessage` are inferred from the reducer.
|
|
39
31
|
*/
|
|
40
|
-
const
|
|
41
|
-
//
|
|
42
|
-
adapterTag: 'vercel-ai-sdk-ui-message'
|
|
43
|
-
init,
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
createUserMessage: (message: AI.UIMessage): VercelInput => ({ kind: 'user-message', message }),
|
|
49
|
-
createRegenerate: (target: string, parent: string): VercelInput => ({
|
|
50
|
-
kind: 'regenerate',
|
|
51
|
-
target,
|
|
52
|
-
parent,
|
|
53
|
-
}),
|
|
54
|
-
createToolResult: (codecMessageId: string, payload: VercelToolResultPayload): VercelInput => ({
|
|
55
|
-
kind: 'tool-result',
|
|
56
|
-
codecMessageId,
|
|
57
|
-
payload,
|
|
58
|
-
}),
|
|
59
|
-
createToolResultError: (codecMessageId: string, payload: VercelToolResultErrorPayload): VercelInput => ({
|
|
60
|
-
kind: 'tool-result-error',
|
|
61
|
-
codecMessageId,
|
|
62
|
-
payload,
|
|
63
|
-
}),
|
|
64
|
-
createToolApprovalResponse: (codecMessageId: string, payload: VercelToolApprovalResponsePayload): VercelInput => ({
|
|
65
|
-
kind: 'tool-approval-response',
|
|
66
|
-
codecMessageId,
|
|
67
|
-
payload,
|
|
68
|
-
}),
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
// Validate Codec conformance via `satisfies` on the variable (no excess-property
|
|
72
|
-
// check, so the internal `adapterTag` is permitted) while keeping the concrete
|
|
73
|
-
// type so the codec-specific factories (createToolResult, etc.) stay callable.
|
|
74
|
-
export const UIMessageCodec = uiMessageCodecImpl satisfies Codec<
|
|
75
|
-
VercelInput,
|
|
76
|
-
VercelOutput,
|
|
77
|
-
VercelProjection,
|
|
78
|
-
AI.UIMessage
|
|
79
|
-
>;
|
|
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
|
+
});
|
|
80
40
|
|
|
81
41
|
export type { VercelInput, VercelOutput } from './events.js';
|
|
82
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
|
+
];
|