@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
|
@@ -11,11 +11,12 @@
|
|
|
11
11
|
* with no instance state. Mutation in place is allowed — the projection
|
|
12
12
|
* is single-owner.
|
|
13
13
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* `
|
|
14
|
+
* The reducer does not dedup or reorder. The transport sequences events
|
|
15
|
+
* canonically — ascending by wire serial across messages, in decode order
|
|
16
|
+
* within a wire — and delivers each exactly once, so the reducer folds
|
|
17
|
+
* unconditionally. Last-writer-wins for events competing over the same
|
|
18
|
+
* logical state (e.g. two `tool-output-available` for one `toolCallId`)
|
|
19
|
+
* falls out of fold order: the highest-serial event folds last.
|
|
19
20
|
*
|
|
20
21
|
* Client-published tool resolutions (`ToolResult`, `ToolResultError`,
|
|
21
22
|
* `ToolApprovalResponse`) carry `codecMessageId` targeting the assistant
|
|
@@ -23,129 +24,31 @@
|
|
|
23
24
|
* `dynamic-tool` part directly. If the assistant has not yet arrived in
|
|
24
25
|
* the projection (out-of-order delivery), the resolution is buffered in
|
|
25
26
|
* `pendingToolResolutions` and re-evaluated on each subsequent fold.
|
|
26
|
-
*/
|
|
27
|
-
|
|
28
|
-
import type * as AI from 'ai';
|
|
29
|
-
|
|
30
|
-
import type {
|
|
31
|
-
CodecMessage,
|
|
32
|
-
ReducerMeta,
|
|
33
|
-
ToolApprovalResponse,
|
|
34
|
-
ToolResult,
|
|
35
|
-
ToolResultError,
|
|
36
|
-
} from '../../core/codec/types.js';
|
|
37
|
-
import { stripUndefined } from '../../utils.js';
|
|
38
|
-
import type {
|
|
39
|
-
VercelInput,
|
|
40
|
-
VercelOutput,
|
|
41
|
-
VercelToolApprovalResponsePayload,
|
|
42
|
-
VercelToolResultErrorPayload,
|
|
43
|
-
VercelToolResultPayload,
|
|
44
|
-
} from './events.js';
|
|
45
|
-
import { toolBase, transitionToolPart } from './tool-transitions.js';
|
|
46
|
-
|
|
47
|
-
// ---------------------------------------------------------------------------
|
|
48
|
-
// Internal tracker state
|
|
49
|
-
// ---------------------------------------------------------------------------
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Tracks an in-progress tool part within a UIMessage. Text and reasoning
|
|
53
|
-
* parts don't need this — we write to them directly via partIndex. Tool
|
|
54
|
-
* parts need an extra `inputText` buffer because deltas arrive as raw
|
|
55
|
-
* JSON fragments that must be accumulated before parsing.
|
|
56
|
-
*/
|
|
57
|
-
interface ToolPartTracker {
|
|
58
|
-
/** Index in the message's parts array. */
|
|
59
|
-
partIndex: number;
|
|
60
|
-
/** Accumulated streaming input text (for JSON parsing on completion). */
|
|
61
|
-
inputText: string;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/** Per-codecMessageId tracking state for in-progress streams within a UIMessage. */
|
|
65
|
-
interface MessageTrackers {
|
|
66
|
-
/** Text stream id → partIndex. */
|
|
67
|
-
text: Map<string, number>;
|
|
68
|
-
/** Reasoning stream id → partIndex. */
|
|
69
|
-
reasoning: Map<string, number>;
|
|
70
|
-
/** Tool call id → tracker. */
|
|
71
|
-
tools: Map<string, ToolPartTracker>;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// ---------------------------------------------------------------------------
|
|
75
|
-
// Projection
|
|
76
|
-
// ---------------------------------------------------------------------------
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* The per-Run state produced by the Vercel codec's reducer.
|
|
80
27
|
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
28
|
+
* This file is the reducer's public facade and dispatch: `init`,
|
|
29
|
+
* `getMessages`, `fold`, and the output-chunk router. The per-concern fold
|
|
30
|
+
* logic lives in the sibling `fold-*` modules over a shared `reducer-state`
|
|
31
|
+
* base; the import graph is an acyclic DAG rooted here.
|
|
85
32
|
*/
|
|
86
|
-
export interface VercelProjection {
|
|
87
|
-
/**
|
|
88
|
-
* UIMessages produced or modified in this Run, in publication order,
|
|
89
|
-
* each paired with its codec-message-id. The reducer correlates strictly
|
|
90
|
-
* on `codecMessageId`; `message.id` is preserved verbatim from the source
|
|
91
|
-
* (the AI SDK stream's `start.messageId` for assistants, the caller's id
|
|
92
|
-
* for user messages) and is never used as an identity key.
|
|
93
|
-
*/
|
|
94
|
-
messages: CodecMessage<AI.UIMessage>[];
|
|
95
|
-
/**
|
|
96
|
-
* Per-conflict-key high-water-marks. Maps a codec-derived conflict key
|
|
97
|
-
* (see `_conflictKeyOf`) to the highest `meta.serial` already folded for
|
|
98
|
-
* that key. Events whose serial is `<=` the stored value are dropped as
|
|
99
|
-
* duplicates of an already-incorporated operation. Events that have no
|
|
100
|
-
* conflict key (additive content, lifecycle markers) are folded
|
|
101
|
-
* unconditionally.
|
|
102
|
-
*/
|
|
103
|
-
conflictSerials: Map<string, string>;
|
|
104
|
-
/** Per-codecMessageId tracker state for streamed parts. Internal — do not access. */
|
|
105
|
-
trackers: Map<string, MessageTrackers>;
|
|
106
|
-
/**
|
|
107
|
-
* Tool-resolution events that arrived before any assistant in this
|
|
108
|
-
* projection had a matching `toolCallId`. Re-evaluated on every
|
|
109
|
-
* subsequent fold so that an out-of-order tool output is folded as
|
|
110
|
-
* soon as the corresponding assistant lands.
|
|
111
|
-
*/
|
|
112
|
-
pendingToolResolutions: PendingToolResolution[];
|
|
113
|
-
}
|
|
114
33
|
|
|
115
|
-
|
|
116
|
-
* A buffered tool resolution waiting for its assistant message to arrive.
|
|
117
|
-
* The reducer scans pending entries after every successful fold so an
|
|
118
|
-
* out-of-order tool output is promoted as soon as the matching assistant
|
|
119
|
-
* is added to the projection.
|
|
120
|
-
*/
|
|
121
|
-
interface PendingToolResolution {
|
|
122
|
-
/** The codec-message-id of the assistant the resolution targets. */
|
|
123
|
-
targetCodecMessageId: string;
|
|
124
|
-
/** Tool call this resolution targets. */
|
|
125
|
-
toolCallId: string;
|
|
126
|
-
/** Serial of the wire message — used by the conflict-key check on promotion. */
|
|
127
|
-
serial: string;
|
|
128
|
-
/** Variant of the tool-resolution used to transition the assistant's tool part. */
|
|
129
|
-
resolution:
|
|
130
|
-
| { kind: 'tool-result'; output: unknown }
|
|
131
|
-
| { kind: 'tool-result-error'; message: string }
|
|
132
|
-
| { kind: 'tool-approval-response'; approved: boolean; reason?: string };
|
|
133
|
-
}
|
|
34
|
+
import type * as AI from 'ai';
|
|
134
35
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
36
|
+
import type { CodecEvent, CodecMessage, ReducerMeta } from '../../core/codec/index.js';
|
|
37
|
+
import type { VercelInput, VercelOutput } from './events.js';
|
|
38
|
+
import { foldContentPart } from './fold-content.js';
|
|
39
|
+
import { foldDataPart } from './fold-data.js';
|
|
40
|
+
import {
|
|
41
|
+
foldClientToolResult,
|
|
42
|
+
foldClientToolResultError,
|
|
43
|
+
foldToolApprovalResponse,
|
|
44
|
+
foldUserMessage,
|
|
45
|
+
retryPendingResolutions,
|
|
46
|
+
} from './fold-input.js';
|
|
47
|
+
import { foldLifecycle } from './fold-lifecycle.js';
|
|
48
|
+
import { foldTextOrReasoning } from './fold-text.js';
|
|
49
|
+
import { foldToolInput } from './fold-tool-input.js';
|
|
50
|
+
import { foldToolOutput } from './fold-tool-output.js';
|
|
51
|
+
import type { VercelProjection } from './reducer-state.js';
|
|
149
52
|
|
|
150
53
|
// ---------------------------------------------------------------------------
|
|
151
54
|
// fold
|
|
@@ -155,12 +58,11 @@ export const init = (): VercelProjection => ({
|
|
|
155
58
|
* Fold one input or output event into the projection. Mutates and returns
|
|
156
59
|
* `state`.
|
|
157
60
|
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
* toolCallId) are dropped silently inside the per-variant fold helpers.
|
|
61
|
+
* The transport invokes `fold` exactly once per event, in canonical order,
|
|
62
|
+
* so the reducer folds unconditionally — no dedup or high-water-mark here.
|
|
63
|
+
* Competing events resolve by order (the highest-serial event folds last
|
|
64
|
+
* and wins). Orphan events (e.g. tool-output for an unknown toolCallId) are
|
|
65
|
+
* dropped silently inside the per-variant fold helpers.
|
|
164
66
|
* @param state - Projection to fold into (may be mutated in place).
|
|
165
67
|
* @param event - Input or output event to fold.
|
|
166
68
|
* @param meta - Transport-derived metadata (serial, optional messageId).
|
|
@@ -168,24 +70,14 @@ export const init = (): VercelProjection => ({
|
|
|
168
70
|
*/
|
|
169
71
|
export const fold = (
|
|
170
72
|
state: VercelProjection,
|
|
171
|
-
event: VercelInput
|
|
73
|
+
event: CodecEvent<VercelInput, VercelOutput>,
|
|
172
74
|
meta: ReducerMeta,
|
|
173
75
|
): VercelProjection => {
|
|
174
|
-
if (
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
const seen = state.conflictSerials.get(key);
|
|
178
|
-
if (seen !== undefined && meta.serial <= seen) {
|
|
179
|
-
return state;
|
|
180
|
-
}
|
|
181
|
-
state.conflictSerials.set(key, meta.serial);
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
if (_isInput(event)) {
|
|
186
|
-
switch (event.kind) {
|
|
76
|
+
if (event.direction === 'input') {
|
|
77
|
+
const input = event.event;
|
|
78
|
+
switch (input.kind) {
|
|
187
79
|
case 'user-message': {
|
|
188
|
-
|
|
80
|
+
foldUserMessage(state, input.message, meta);
|
|
189
81
|
break;
|
|
190
82
|
}
|
|
191
83
|
case 'regenerate': {
|
|
@@ -195,377 +87,37 @@ export const fold = (
|
|
|
195
87
|
break;
|
|
196
88
|
}
|
|
197
89
|
case 'tool-result': {
|
|
198
|
-
|
|
90
|
+
foldClientToolResult(state, input);
|
|
199
91
|
break;
|
|
200
92
|
}
|
|
201
93
|
case 'tool-result-error': {
|
|
202
|
-
|
|
94
|
+
foldClientToolResultError(state, input);
|
|
203
95
|
break;
|
|
204
96
|
}
|
|
205
97
|
case 'tool-approval-response': {
|
|
206
|
-
|
|
98
|
+
foldToolApprovalResponse(state, input);
|
|
207
99
|
break;
|
|
208
100
|
}
|
|
209
101
|
}
|
|
210
102
|
} else {
|
|
211
|
-
|
|
103
|
+
foldChunk(state, event.event, meta);
|
|
212
104
|
}
|
|
213
105
|
|
|
214
106
|
// Re-evaluate pending tool resolutions in case the just-folded event
|
|
215
107
|
// produced the assistant they were waiting on. Cheap when the list is
|
|
216
108
|
// empty (the common case).
|
|
217
109
|
if (state.pendingToolResolutions.length > 0) {
|
|
218
|
-
|
|
110
|
+
retryPendingResolutions(state);
|
|
219
111
|
}
|
|
220
112
|
|
|
221
113
|
return state;
|
|
222
114
|
};
|
|
223
115
|
|
|
224
|
-
/**
|
|
225
|
-
* Narrow the union to TInput vs TOutput by the discriminator field name.
|
|
226
|
-
* VercelInput variants carry `kind`; VercelOutput variants carry `type`.
|
|
227
|
-
* @param event - The event to narrow.
|
|
228
|
-
* @returns True when the event is a VercelInput, false for VercelOutput.
|
|
229
|
-
*/
|
|
230
|
-
const _isInput = (event: VercelInput | VercelOutput): event is VercelInput => 'kind' in event;
|
|
231
|
-
|
|
232
|
-
// ---------------------------------------------------------------------------
|
|
233
|
-
// Conflict-key derivation
|
|
234
|
-
// ---------------------------------------------------------------------------
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* Derive a per-event conflict key, or `undefined` if the event doesn't
|
|
238
|
-
* compete with any other event for shared state. Used by `fold` to scope
|
|
239
|
-
* the high-water-mark check to genuine conflicts (e.g. two
|
|
240
|
-
* `tool-output-available` for the same `toolCallId`) rather than to every
|
|
241
|
-
* event in the stream.
|
|
242
|
-
* @param event - The event being folded.
|
|
243
|
-
* @param meta - Transport-derived metadata (used for events keyed by codec-message-id).
|
|
244
|
-
* @returns The conflict key, or `undefined` if the event is additive / independent.
|
|
245
|
-
*/
|
|
246
|
-
const _conflictKeyOf = (event: VercelInput | VercelOutput, meta: ReducerMeta): string | undefined => {
|
|
247
|
-
if (_isInput(event)) {
|
|
248
|
-
switch (event.kind) {
|
|
249
|
-
case 'user-message': {
|
|
250
|
-
// Dedup re-publishes of the same user message by its wire
|
|
251
|
-
// codec-message-id, never by the domain `message.id`. Without a
|
|
252
|
-
// codec-message-id there is nothing to correlate on, so the fold
|
|
253
|
-
// is left unconditional.
|
|
254
|
-
return meta.messageId === undefined ? undefined : `user-msg:${meta.messageId}`;
|
|
255
|
-
}
|
|
256
|
-
case 'tool-approval-response': {
|
|
257
|
-
return `tool-approval:${event.payload.toolCallId}`;
|
|
258
|
-
}
|
|
259
|
-
// Client tool results compete for the same final state of the tool
|
|
260
|
-
// call (against agent-side `tool-output-available`/`tool-output-error`
|
|
261
|
-
// chunks and against `tool-output-denied`/`tool-approval-request`).
|
|
262
|
-
// Highest serial wins. Shares the `tool-output:` namespace with the
|
|
263
|
-
// agent-side chunks below.
|
|
264
|
-
case 'tool-result':
|
|
265
|
-
case 'tool-result-error': {
|
|
266
|
-
return `tool-output:${event.payload.toolCallId}`;
|
|
267
|
-
}
|
|
268
|
-
case 'regenerate': {
|
|
269
|
-
return undefined;
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
switch (event.type) {
|
|
275
|
-
// Tool-input state machine, keyed by toolCallId.
|
|
276
|
-
case 'tool-input-start':
|
|
277
|
-
case 'tool-input-available':
|
|
278
|
-
case 'tool-input-error': {
|
|
279
|
-
return `${event.type}:${event.toolCallId}`;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
// All "tool-output-ish" output variants compete for the same final
|
|
283
|
-
// state of the tool call. Shares the `tool-output:` namespace with
|
|
284
|
-
// the client-published input variants above.
|
|
285
|
-
case 'tool-output-available':
|
|
286
|
-
case 'tool-output-error':
|
|
287
|
-
case 'tool-output-denied':
|
|
288
|
-
case 'tool-approval-request': {
|
|
289
|
-
return `tool-output:${event.toolCallId}`;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
// Per-stream start/end markers: duplicates would create phantom parts
|
|
293
|
-
// or wipe accumulated text. Keyed by (codec-message-id, stream-id).
|
|
294
|
-
case 'text-start':
|
|
295
|
-
case 'text-end':
|
|
296
|
-
case 'reasoning-start':
|
|
297
|
-
case 'reasoning-end': {
|
|
298
|
-
return `${event.type}:${meta.messageId ?? ''}:${event.id}`;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
// Message-level markers, keyed by codec-message-id.
|
|
302
|
-
case 'finish':
|
|
303
|
-
case 'message-metadata': {
|
|
304
|
-
return `${event.type}:${meta.messageId ?? ''}`;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
// Purely additive or independent — never dedup:
|
|
308
|
-
// text-delta / reasoning-delta / tool-input-delta (additive content)
|
|
309
|
-
// start / start-step / finish-step / abort / error (lifecycle)
|
|
310
|
-
// file / source-url / source-document (independent attachments)
|
|
311
|
-
// data-* (opaque to the reducer)
|
|
312
|
-
default: {
|
|
313
|
-
return undefined;
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
};
|
|
317
|
-
|
|
318
116
|
// ---------------------------------------------------------------------------
|
|
319
|
-
//
|
|
117
|
+
// UIMessageChunk dispatch
|
|
320
118
|
// ---------------------------------------------------------------------------
|
|
321
119
|
|
|
322
|
-
const
|
|
323
|
-
// Correlate the projection entry on the wire codec-message-id; the
|
|
324
|
-
// caller-supplied `message.id` is preserved verbatim and surfaced to the
|
|
325
|
-
// application unchanged. Without a codec-message-id the message has no
|
|
326
|
-
// identity to key on, so it is appended as a fresh entry.
|
|
327
|
-
const codecMessageId = meta.messageId;
|
|
328
|
-
if (codecMessageId === undefined) {
|
|
329
|
-
state.messages.push({ codecMessageId: message.id, message });
|
|
330
|
-
return state;
|
|
331
|
-
}
|
|
332
|
-
const existingIdx = state.messages.findIndex((e) => e.codecMessageId === codecMessageId);
|
|
333
|
-
if (existingIdx === -1) {
|
|
334
|
-
state.messages.push({ codecMessageId, message });
|
|
335
|
-
} else {
|
|
336
|
-
state.messages[existingIdx] = { codecMessageId, message };
|
|
337
|
-
}
|
|
338
|
-
return state;
|
|
339
|
-
};
|
|
340
|
-
|
|
341
|
-
/**
|
|
342
|
-
* Fold a client-published `ToolResult`. The input carries
|
|
343
|
-
* `codecMessageId` pointing at the assistant whose `dynamic-tool` part
|
|
344
|
-
* holds the matching `toolCallId`. If the assistant and its matching
|
|
345
|
-
* `dynamic-tool` part are both present, fold directly; otherwise pend
|
|
346
|
-
* until that tool part arrives.
|
|
347
|
-
* @param state - Projection to fold into.
|
|
348
|
-
* @param event - The tool-result input (codecMessageId + domain payload).
|
|
349
|
-
* @param meta - Transport-derived metadata.
|
|
350
|
-
* @returns The same projection reference.
|
|
351
|
-
*/
|
|
352
|
-
const _foldClientToolResult = (
|
|
353
|
-
state: VercelProjection,
|
|
354
|
-
event: ToolResult<VercelToolResultPayload>,
|
|
355
|
-
meta: ReducerMeta,
|
|
356
|
-
): VercelProjection => {
|
|
357
|
-
const { toolCallId, output } = event.payload;
|
|
358
|
-
const owner = _findOwner(state, event.codecMessageId, toolCallId);
|
|
359
|
-
if (owner) {
|
|
360
|
-
owner.message.parts[owner.tracker.partIndex] = transitionToolPart(owner.part, {
|
|
361
|
-
type: 'tool-output-available',
|
|
362
|
-
toolCallId,
|
|
363
|
-
output,
|
|
364
|
-
});
|
|
365
|
-
return state;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
state.pendingToolResolutions.push({
|
|
369
|
-
targetCodecMessageId: event.codecMessageId,
|
|
370
|
-
toolCallId,
|
|
371
|
-
serial: meta.serial,
|
|
372
|
-
resolution: { kind: 'tool-result', output },
|
|
373
|
-
});
|
|
374
|
-
return state;
|
|
375
|
-
};
|
|
376
|
-
|
|
377
|
-
/**
|
|
378
|
-
* Fold a client-published `ToolResultError`. Mirrors
|
|
379
|
-
* {@link _foldClientToolResult} but with the error transition.
|
|
380
|
-
* @param state - Projection to fold into.
|
|
381
|
-
* @param event - The tool-result-error input (codecMessageId + domain payload).
|
|
382
|
-
* @param meta - Transport-derived metadata.
|
|
383
|
-
* @returns The same projection reference.
|
|
384
|
-
*/
|
|
385
|
-
const _foldClientToolResultError = (
|
|
386
|
-
state: VercelProjection,
|
|
387
|
-
event: ToolResultError<VercelToolResultErrorPayload>,
|
|
388
|
-
meta: ReducerMeta,
|
|
389
|
-
): VercelProjection => {
|
|
390
|
-
const { toolCallId, message } = event.payload;
|
|
391
|
-
const owner = _findOwner(state, event.codecMessageId, toolCallId);
|
|
392
|
-
if (owner) {
|
|
393
|
-
owner.message.parts[owner.tracker.partIndex] = transitionToolPart(owner.part, {
|
|
394
|
-
type: 'tool-output-error',
|
|
395
|
-
toolCallId,
|
|
396
|
-
errorText: message,
|
|
397
|
-
});
|
|
398
|
-
return state;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
state.pendingToolResolutions.push({
|
|
402
|
-
targetCodecMessageId: event.codecMessageId,
|
|
403
|
-
toolCallId,
|
|
404
|
-
serial: meta.serial,
|
|
405
|
-
resolution: { kind: 'tool-result-error', message },
|
|
406
|
-
});
|
|
407
|
-
return state;
|
|
408
|
-
};
|
|
409
|
-
|
|
410
|
-
/**
|
|
411
|
-
* Fold a client-published `ToolApprovalResponse`. The input carries
|
|
412
|
-
* `codecMessageId` pointing at the assistant whose `dynamic-tool` part
|
|
413
|
-
* holds the matching `toolCallId`. Approval → `approval-responded`;
|
|
414
|
-
* denial → `output-denied` via {@link transitionToolPart}.
|
|
415
|
-
* @param state - Projection to fold into.
|
|
416
|
-
* @param event - The approval-response input.
|
|
417
|
-
* @param meta - Transport-derived metadata.
|
|
418
|
-
* @returns The same projection reference.
|
|
419
|
-
*/
|
|
420
|
-
const _foldToolApprovalResponse = (
|
|
421
|
-
state: VercelProjection,
|
|
422
|
-
event: ToolApprovalResponse<VercelToolApprovalResponsePayload>,
|
|
423
|
-
meta: ReducerMeta,
|
|
424
|
-
): VercelProjection => {
|
|
425
|
-
const { toolCallId, approved, reason } = event.payload;
|
|
426
|
-
const owner = _findOwner(state, event.codecMessageId, toolCallId);
|
|
427
|
-
if (owner) {
|
|
428
|
-
owner.message.parts[owner.tracker.partIndex] = _approvalTransition(owner.part, approved, reason);
|
|
429
|
-
return state;
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
state.pendingToolResolutions.push({
|
|
433
|
-
targetCodecMessageId: event.codecMessageId,
|
|
434
|
-
toolCallId,
|
|
435
|
-
serial: meta.serial,
|
|
436
|
-
resolution: {
|
|
437
|
-
kind: 'tool-approval-response',
|
|
438
|
-
approved,
|
|
439
|
-
...(reason === undefined ? {} : { reason }),
|
|
440
|
-
},
|
|
441
|
-
});
|
|
442
|
-
return state;
|
|
443
|
-
};
|
|
444
|
-
|
|
445
|
-
interface OwnerLookup {
|
|
446
|
-
message: AI.UIMessage;
|
|
447
|
-
tracker: ToolPartTracker;
|
|
448
|
-
part: AI.DynamicToolUIPart;
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
const _findOwner = (state: VercelProjection, codecMessageId: string, toolCallId: string): OwnerLookup | undefined => {
|
|
452
|
-
const entry = state.messages.find((e) => e.codecMessageId === codecMessageId);
|
|
453
|
-
if (!entry) return undefined;
|
|
454
|
-
const trackers = _ensureTrackers(state, codecMessageId);
|
|
455
|
-
const found = _getToolPart(entry.message, trackers, toolCallId);
|
|
456
|
-
if (!found) return undefined;
|
|
457
|
-
return { message: entry.message, tracker: found.tracker, part: found.part };
|
|
458
|
-
};
|
|
459
|
-
|
|
460
|
-
/**
|
|
461
|
-
* Locate the `dynamic-tool` part for a `toolCallId` anywhere in the projection.
|
|
462
|
-
* Agent-emitted second-pass tool outputs (after an approved tool runs) are
|
|
463
|
-
* stamped with a fresh codec-message-id that differs from the assistant holding
|
|
464
|
-
* the tool call, so they can't be found via `meta.messageId` — they fold onto
|
|
465
|
-
* whichever message holds the matching tool call (created in the first pass or
|
|
466
|
-
* by an approval response).
|
|
467
|
-
* @param state - Projection to scan.
|
|
468
|
-
* @param toolCallId - The tool call to locate.
|
|
469
|
-
* @returns The owning message, tracker, and part, or `undefined` if absent.
|
|
470
|
-
*/
|
|
471
|
-
const _findToolPartOwner = (state: VercelProjection, toolCallId: string): OwnerLookup | undefined => {
|
|
472
|
-
for (const entry of state.messages) {
|
|
473
|
-
const trackers = state.trackers.get(entry.codecMessageId);
|
|
474
|
-
if (!trackers) continue;
|
|
475
|
-
const found = _getToolPart(entry.message, trackers, toolCallId);
|
|
476
|
-
if (found) return { message: entry.message, tracker: found.tracker, part: found.part };
|
|
477
|
-
}
|
|
478
|
-
return undefined;
|
|
479
|
-
};
|
|
480
|
-
|
|
481
|
-
/**
|
|
482
|
-
* Build the next `dynamic-tool` part shape for an approval response.
|
|
483
|
-
*
|
|
484
|
-
* For `approved=true`, transition to `approval-responded` so the AI SDK's
|
|
485
|
-
* multi-step loop will auto-run the tool on the next step.
|
|
486
|
-
* `transitionToolPart` has no shape for this transition, so we synthesize
|
|
487
|
-
* the part directly.
|
|
488
|
-
*
|
|
489
|
-
* For `approved=false`, delegate to `transitionToolPart` with a synthetic
|
|
490
|
-
* `tool-output-denied` chunk so denial mirrors the chunk-driven path.
|
|
491
|
-
* @param part - The existing `dynamic-tool` part being transitioned.
|
|
492
|
-
* @param approved - Whether the user approved the tool execution.
|
|
493
|
-
* @param reason - Optional human-readable reason.
|
|
494
|
-
* @returns The replacement `dynamic-tool` part.
|
|
495
|
-
*/
|
|
496
|
-
const _approvalTransition = (
|
|
497
|
-
part: AI.DynamicToolUIPart,
|
|
498
|
-
approved: boolean,
|
|
499
|
-
reason: string | undefined,
|
|
500
|
-
): AI.DynamicToolUIPart => {
|
|
501
|
-
if (approved) {
|
|
502
|
-
return {
|
|
503
|
-
...toolBase(part),
|
|
504
|
-
state: 'approval-responded',
|
|
505
|
-
input: 'input' in part ? part.input : undefined,
|
|
506
|
-
approval: {
|
|
507
|
-
id: 'approval' in part && part.approval ? part.approval.id : '',
|
|
508
|
-
approved: true,
|
|
509
|
-
...(reason === undefined ? {} : { reason }),
|
|
510
|
-
},
|
|
511
|
-
};
|
|
512
|
-
}
|
|
513
|
-
return transitionToolPart(part, {
|
|
514
|
-
type: 'tool-output-denied',
|
|
515
|
-
toolCallId: part.toolCallId,
|
|
516
|
-
...(reason === undefined ? {} : { reason }),
|
|
517
|
-
});
|
|
518
|
-
};
|
|
519
|
-
|
|
520
|
-
/**
|
|
521
|
-
* Re-attempt every pending tool resolution against the current projection.
|
|
522
|
-
* Successfully promoted entries are removed from the pending list. Cheap:
|
|
523
|
-
* bounded by the number of pending entries.
|
|
524
|
-
* @param state - Projection to walk and mutate.
|
|
525
|
-
*/
|
|
526
|
-
const _retryPendingResolutions = (state: VercelProjection): void => {
|
|
527
|
-
const next: PendingToolResolution[] = [];
|
|
528
|
-
for (const pending of state.pendingToolResolutions) {
|
|
529
|
-
const owner = _findOwner(state, pending.targetCodecMessageId, pending.toolCallId);
|
|
530
|
-
if (!owner) {
|
|
531
|
-
next.push(pending);
|
|
532
|
-
continue;
|
|
533
|
-
}
|
|
534
|
-
switch (pending.resolution.kind) {
|
|
535
|
-
case 'tool-result': {
|
|
536
|
-
owner.message.parts[owner.tracker.partIndex] = transitionToolPart(owner.part, {
|
|
537
|
-
type: 'tool-output-available',
|
|
538
|
-
toolCallId: pending.toolCallId,
|
|
539
|
-
output: pending.resolution.output,
|
|
540
|
-
});
|
|
541
|
-
break;
|
|
542
|
-
}
|
|
543
|
-
case 'tool-result-error': {
|
|
544
|
-
owner.message.parts[owner.tracker.partIndex] = transitionToolPart(owner.part, {
|
|
545
|
-
type: 'tool-output-error',
|
|
546
|
-
toolCallId: pending.toolCallId,
|
|
547
|
-
errorText: pending.resolution.message,
|
|
548
|
-
});
|
|
549
|
-
break;
|
|
550
|
-
}
|
|
551
|
-
case 'tool-approval-response': {
|
|
552
|
-
owner.message.parts[owner.tracker.partIndex] = _approvalTransition(
|
|
553
|
-
owner.part,
|
|
554
|
-
pending.resolution.approved,
|
|
555
|
-
pending.resolution.reason,
|
|
556
|
-
);
|
|
557
|
-
break;
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
state.pendingToolResolutions = next;
|
|
562
|
-
};
|
|
563
|
-
|
|
564
|
-
// ---------------------------------------------------------------------------
|
|
565
|
-
// UIMessageChunk fold
|
|
566
|
-
// ---------------------------------------------------------------------------
|
|
567
|
-
|
|
568
|
-
const _foldChunk = (state: VercelProjection, chunk: VercelOutput, meta: ReducerMeta): VercelProjection => {
|
|
120
|
+
const foldChunk = (state: VercelProjection, chunk: VercelOutput, meta: ReducerMeta): VercelProjection => {
|
|
569
121
|
const messageId = meta.messageId;
|
|
570
122
|
if (messageId === undefined) {
|
|
571
123
|
// Without a target codec-message-id, a chunk has nowhere to land. Drop.
|
|
@@ -580,7 +132,7 @@ const _foldChunk = (state: VercelProjection, chunk: VercelOutput, meta: ReducerM
|
|
|
580
132
|
case 'abort':
|
|
581
133
|
case 'error':
|
|
582
134
|
case 'message-metadata': {
|
|
583
|
-
return
|
|
135
|
+
return foldLifecycle(state, chunk, messageId);
|
|
584
136
|
}
|
|
585
137
|
|
|
586
138
|
case 'text-start':
|
|
@@ -589,378 +141,38 @@ const _foldChunk = (state: VercelProjection, chunk: VercelOutput, meta: ReducerM
|
|
|
589
141
|
case 'reasoning-start':
|
|
590
142
|
case 'reasoning-delta':
|
|
591
143
|
case 'reasoning-end': {
|
|
592
|
-
return
|
|
144
|
+
return foldTextOrReasoning(state, chunk, messageId);
|
|
593
145
|
}
|
|
594
146
|
|
|
595
147
|
case 'tool-input-start':
|
|
596
148
|
case 'tool-input-delta':
|
|
597
149
|
case 'tool-input-available':
|
|
598
150
|
case 'tool-input-error': {
|
|
599
|
-
return
|
|
151
|
+
return foldToolInput(state, chunk, messageId);
|
|
600
152
|
}
|
|
601
153
|
|
|
602
154
|
case 'tool-output-available':
|
|
603
155
|
case 'tool-output-error':
|
|
604
156
|
case 'tool-output-denied':
|
|
605
157
|
case 'tool-approval-request': {
|
|
606
|
-
return
|
|
158
|
+
return foldToolOutput(state, chunk, messageId);
|
|
607
159
|
}
|
|
608
160
|
|
|
609
161
|
case 'file':
|
|
610
162
|
case 'source-url':
|
|
611
163
|
case 'source-document': {
|
|
612
|
-
return
|
|
164
|
+
return foldContentPart(state, chunk, messageId);
|
|
613
165
|
}
|
|
614
166
|
|
|
615
167
|
default: {
|
|
616
168
|
if (chunk.type.startsWith('data-')) {
|
|
617
|
-
return
|
|
169
|
+
return foldDataPart(state, chunk, messageId);
|
|
618
170
|
}
|
|
619
171
|
return state;
|
|
620
172
|
}
|
|
621
173
|
}
|
|
622
174
|
};
|
|
623
175
|
|
|
624
|
-
// ---------------------------------------------------------------------------
|
|
625
|
-
// Message + tracker helpers
|
|
626
|
-
// ---------------------------------------------------------------------------
|
|
627
|
-
|
|
628
|
-
const _ensureMessage = (state: VercelProjection, codecMessageId: string): AI.UIMessage => {
|
|
629
|
-
let entry = state.messages.find((e) => e.codecMessageId === codecMessageId);
|
|
630
|
-
if (!entry) {
|
|
631
|
-
// No source id seen yet — seed the domain `message.id` with the
|
|
632
|
-
// codec-message-id as a fallback. The `start` chunk overwrites it with
|
|
633
|
-
// the stream's `messageId` when the stream provides one.
|
|
634
|
-
entry = { codecMessageId, message: { id: codecMessageId, role: 'assistant', parts: [] } };
|
|
635
|
-
state.messages.push(entry);
|
|
636
|
-
}
|
|
637
|
-
return entry.message;
|
|
638
|
-
};
|
|
639
|
-
|
|
640
|
-
const _ensureTrackers = (state: VercelProjection, messageId: string): MessageTrackers => {
|
|
641
|
-
let trackers = state.trackers.get(messageId);
|
|
642
|
-
if (!trackers) {
|
|
643
|
-
trackers = { text: new Map(), reasoning: new Map(), tools: new Map() };
|
|
644
|
-
state.trackers.set(messageId, trackers);
|
|
645
|
-
}
|
|
646
|
-
return trackers;
|
|
647
|
-
};
|
|
648
|
-
|
|
649
|
-
const _getToolPart = (
|
|
650
|
-
message: AI.UIMessage,
|
|
651
|
-
trackers: MessageTrackers,
|
|
652
|
-
toolCallId: string,
|
|
653
|
-
): { tracker: ToolPartTracker; part: AI.DynamicToolUIPart } | undefined => {
|
|
654
|
-
const tracker = trackers.tools.get(toolCallId);
|
|
655
|
-
if (!tracker) return undefined;
|
|
656
|
-
const part = message.parts[tracker.partIndex];
|
|
657
|
-
if (part?.type !== 'dynamic-tool') return undefined;
|
|
658
|
-
return { tracker, part };
|
|
659
|
-
};
|
|
660
|
-
|
|
661
|
-
// ---------------------------------------------------------------------------
|
|
662
|
-
// Lifecycle events
|
|
663
|
-
// ---------------------------------------------------------------------------
|
|
664
|
-
|
|
665
|
-
const _foldLifecycle = (
|
|
666
|
-
state: VercelProjection,
|
|
667
|
-
chunk: Extract<
|
|
668
|
-
AI.UIMessageChunk,
|
|
669
|
-
{ type: 'start' | 'start-step' | 'finish-step' | 'finish' | 'abort' | 'error' | 'message-metadata' }
|
|
670
|
-
>,
|
|
671
|
-
messageId: string,
|
|
672
|
-
): VercelProjection => {
|
|
673
|
-
switch (chunk.type) {
|
|
674
|
-
case 'start': {
|
|
675
|
-
// The projection entry is keyed on the wire codec-message-id
|
|
676
|
-
// (`messageId`); every subsequent chunk for this message correlates on
|
|
677
|
-
// that, independent of `message.id`. So we faithfully reproduce the
|
|
678
|
-
// stream's own `messageId` on the reconstructed `UIMessage.id` (the
|
|
679
|
-
// value surfaced to the application) without risk of orphaning later
|
|
680
|
-
// chunks. When the stream omits it, the codec-message-id seeded by
|
|
681
|
-
// `_ensureMessage` stands as the fallback id.
|
|
682
|
-
const message = _ensureMessage(state, messageId);
|
|
683
|
-
if (chunk.messageId !== undefined) message.id = chunk.messageId;
|
|
684
|
-
if (chunk.messageMetadata !== undefined) message.metadata = chunk.messageMetadata;
|
|
685
|
-
return state;
|
|
686
|
-
}
|
|
687
|
-
case 'start-step': {
|
|
688
|
-
const message = _ensureMessage(state, messageId);
|
|
689
|
-
message.parts.push({ type: 'step-start' });
|
|
690
|
-
return state;
|
|
691
|
-
}
|
|
692
|
-
case 'finish-step': {
|
|
693
|
-
// Reset text/reasoning stream trackers so a follow-up step can start
|
|
694
|
-
// new parts with potentially-reused stream ids.
|
|
695
|
-
const trackers = state.trackers.get(messageId);
|
|
696
|
-
if (trackers) {
|
|
697
|
-
trackers.text.clear();
|
|
698
|
-
trackers.reasoning.clear();
|
|
699
|
-
}
|
|
700
|
-
return state;
|
|
701
|
-
}
|
|
702
|
-
case 'finish': {
|
|
703
|
-
const message = state.messages.find((e) => e.codecMessageId === messageId)?.message;
|
|
704
|
-
if (message && chunk.messageMetadata !== undefined) {
|
|
705
|
-
message.metadata = chunk.messageMetadata;
|
|
706
|
-
}
|
|
707
|
-
// Tracker state retained — late events still resolvable; cleanup happens at Run end.
|
|
708
|
-
return state;
|
|
709
|
-
}
|
|
710
|
-
case 'abort':
|
|
711
|
-
case 'error': {
|
|
712
|
-
// No state mutation — run termination is observed via the wire run-end
|
|
713
|
-
// event, not the projection.
|
|
714
|
-
return state;
|
|
715
|
-
}
|
|
716
|
-
case 'message-metadata': {
|
|
717
|
-
const message = state.messages.find((e) => e.codecMessageId === messageId)?.message;
|
|
718
|
-
if (message && chunk.messageMetadata !== undefined) {
|
|
719
|
-
message.metadata = chunk.messageMetadata;
|
|
720
|
-
}
|
|
721
|
-
return state;
|
|
722
|
-
}
|
|
723
|
-
}
|
|
724
|
-
};
|
|
725
|
-
|
|
726
|
-
// ---------------------------------------------------------------------------
|
|
727
|
-
// Text and reasoning streaming
|
|
728
|
-
// ---------------------------------------------------------------------------
|
|
729
|
-
|
|
730
|
-
const _foldTextOrReasoning = (
|
|
731
|
-
state: VercelProjection,
|
|
732
|
-
chunk: Extract<
|
|
733
|
-
AI.UIMessageChunk,
|
|
734
|
-
{ type: 'text-start' | 'text-delta' | 'text-end' | 'reasoning-start' | 'reasoning-delta' | 'reasoning-end' }
|
|
735
|
-
>,
|
|
736
|
-
messageId: string,
|
|
737
|
-
): VercelProjection => {
|
|
738
|
-
const message = _ensureMessage(state, messageId);
|
|
739
|
-
const trackers = _ensureTrackers(state, messageId);
|
|
740
|
-
|
|
741
|
-
const isText = chunk.type.startsWith('text-');
|
|
742
|
-
const partType = isText ? 'text' : 'reasoning';
|
|
743
|
-
const activeMap = isText ? trackers.text : trackers.reasoning;
|
|
744
|
-
|
|
745
|
-
switch (chunk.type) {
|
|
746
|
-
case 'text-start':
|
|
747
|
-
case 'reasoning-start': {
|
|
748
|
-
activeMap.set(chunk.id, message.parts.length);
|
|
749
|
-
message.parts.push({ type: partType, text: '' });
|
|
750
|
-
return state;
|
|
751
|
-
}
|
|
752
|
-
case 'text-delta':
|
|
753
|
-
case 'reasoning-delta': {
|
|
754
|
-
const idx = activeMap.get(chunk.id);
|
|
755
|
-
if (idx === undefined) return state;
|
|
756
|
-
const part = message.parts[idx];
|
|
757
|
-
if (part?.type === partType) {
|
|
758
|
-
part.text += chunk.delta;
|
|
759
|
-
}
|
|
760
|
-
return state;
|
|
761
|
-
}
|
|
762
|
-
case 'text-end':
|
|
763
|
-
case 'reasoning-end': {
|
|
764
|
-
activeMap.delete(chunk.id);
|
|
765
|
-
return state;
|
|
766
|
-
}
|
|
767
|
-
}
|
|
768
|
-
};
|
|
769
|
-
|
|
770
|
-
// ---------------------------------------------------------------------------
|
|
771
|
-
// Tool input streaming
|
|
772
|
-
// ---------------------------------------------------------------------------
|
|
773
|
-
|
|
774
|
-
const _foldToolInput = (
|
|
775
|
-
state: VercelProjection,
|
|
776
|
-
chunk: Extract<
|
|
777
|
-
AI.UIMessageChunk,
|
|
778
|
-
{ type: 'tool-input-start' | 'tool-input-delta' | 'tool-input-available' | 'tool-input-error' }
|
|
779
|
-
>,
|
|
780
|
-
messageId: string,
|
|
781
|
-
): VercelProjection => {
|
|
782
|
-
const message = _ensureMessage(state, messageId);
|
|
783
|
-
const trackers = _ensureTrackers(state, messageId);
|
|
784
|
-
|
|
785
|
-
switch (chunk.type) {
|
|
786
|
-
case 'tool-input-start': {
|
|
787
|
-
const partIndex = message.parts.length;
|
|
788
|
-
message.parts.push({ ...toolBase(chunk), state: 'input-streaming', input: undefined });
|
|
789
|
-
trackers.tools.set(chunk.toolCallId, { partIndex, inputText: '' });
|
|
790
|
-
return state;
|
|
791
|
-
}
|
|
792
|
-
case 'tool-input-delta': {
|
|
793
|
-
const tracker = trackers.tools.get(chunk.toolCallId);
|
|
794
|
-
if (!tracker) return state;
|
|
795
|
-
tracker.inputText += chunk.inputTextDelta;
|
|
796
|
-
|
|
797
|
-
let parsedInput: unknown;
|
|
798
|
-
try {
|
|
799
|
-
// CAST: JSON.parse returns any; unknown is the safe trust-boundary type.
|
|
800
|
-
parsedInput = JSON.parse(tracker.inputText) as unknown;
|
|
801
|
-
} catch {
|
|
802
|
-
parsedInput = undefined;
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
const found = _getToolPart(message, trackers, chunk.toolCallId);
|
|
806
|
-
if (!found) return state;
|
|
807
|
-
message.parts[found.tracker.partIndex] = {
|
|
808
|
-
...toolBase(found.part),
|
|
809
|
-
state: 'input-streaming',
|
|
810
|
-
input: parsedInput,
|
|
811
|
-
};
|
|
812
|
-
return state;
|
|
813
|
-
}
|
|
814
|
-
case 'tool-input-available': {
|
|
815
|
-
const found = _getToolPart(message, trackers, chunk.toolCallId);
|
|
816
|
-
if (!found) return state;
|
|
817
|
-
message.parts[found.tracker.partIndex] = {
|
|
818
|
-
...toolBase(found.part),
|
|
819
|
-
state: 'input-available',
|
|
820
|
-
input: chunk.input,
|
|
821
|
-
};
|
|
822
|
-
return state;
|
|
823
|
-
}
|
|
824
|
-
case 'tool-input-error': {
|
|
825
|
-
const found = _getToolPart(message, trackers, chunk.toolCallId);
|
|
826
|
-
if (found) {
|
|
827
|
-
message.parts[found.tracker.partIndex] = {
|
|
828
|
-
...toolBase(found.part),
|
|
829
|
-
state: 'output-error',
|
|
830
|
-
input: chunk.input,
|
|
831
|
-
errorText: chunk.errorText,
|
|
832
|
-
};
|
|
833
|
-
} else {
|
|
834
|
-
const partIndex = message.parts.length;
|
|
835
|
-
message.parts.push({
|
|
836
|
-
...toolBase(chunk),
|
|
837
|
-
state: 'output-error',
|
|
838
|
-
input: chunk.input,
|
|
839
|
-
errorText: chunk.errorText,
|
|
840
|
-
});
|
|
841
|
-
trackers.tools.set(chunk.toolCallId, { partIndex, inputText: '' });
|
|
842
|
-
}
|
|
843
|
-
return state;
|
|
844
|
-
}
|
|
845
|
-
}
|
|
846
|
-
};
|
|
847
|
-
|
|
848
|
-
// ---------------------------------------------------------------------------
|
|
849
|
-
// Tool output transitions (agent-published chunks)
|
|
850
|
-
// ---------------------------------------------------------------------------
|
|
851
|
-
|
|
852
|
-
const _foldToolOutput = (
|
|
853
|
-
state: VercelProjection,
|
|
854
|
-
chunk: Extract<
|
|
855
|
-
AI.UIMessageChunk,
|
|
856
|
-
{ type: 'tool-output-available' | 'tool-output-error' | 'tool-output-denied' | 'tool-approval-request' }
|
|
857
|
-
>,
|
|
858
|
-
messageId: string,
|
|
859
|
-
): VercelProjection => {
|
|
860
|
-
// `tool-output-available` / `tool-output-error` after an approved tool runs
|
|
861
|
-
// are emitted by streamText's continuation pass under a fresh
|
|
862
|
-
// codec-message-id that differs from the assistant holding the tool call.
|
|
863
|
-
// Resolve the owning part by toolCallId across the whole projection so the
|
|
864
|
-
// output folds onto the original message. Deliberately do NOT materialise
|
|
865
|
-
// `messageId` first — that would leave a phantom empty message behind the
|
|
866
|
-
// fresh id. Drop on miss: a tool output with no matching tool call has no
|
|
867
|
-
// anchor to attach to.
|
|
868
|
-
if (chunk.type === 'tool-output-available' || chunk.type === 'tool-output-error') {
|
|
869
|
-
const owner = _findToolPartOwner(state, chunk.toolCallId);
|
|
870
|
-
if (!owner) return state;
|
|
871
|
-
owner.message.parts[owner.tracker.partIndex] = transitionToolPart(owner.part, chunk);
|
|
872
|
-
return state;
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
// `tool-approval-request` (first pass) creates the part on the run's own
|
|
876
|
-
// message; `tool-output-denied` transitions that same part. Both key on the
|
|
877
|
-
// stamped messageId.
|
|
878
|
-
const message = _ensureMessage(state, messageId);
|
|
879
|
-
const trackers = _ensureTrackers(state, messageId);
|
|
880
|
-
|
|
881
|
-
const found = _getToolPart(message, trackers, chunk.toolCallId);
|
|
882
|
-
if (!found) return state;
|
|
883
|
-
|
|
884
|
-
message.parts[found.tracker.partIndex] = transitionToolPart(found.part, chunk);
|
|
885
|
-
return state;
|
|
886
|
-
};
|
|
887
|
-
|
|
888
|
-
// ---------------------------------------------------------------------------
|
|
889
|
-
// File / source content parts
|
|
890
|
-
// ---------------------------------------------------------------------------
|
|
891
|
-
|
|
892
|
-
const _foldContentPart = (
|
|
893
|
-
state: VercelProjection,
|
|
894
|
-
chunk: Extract<AI.UIMessageChunk, { type: 'file' | 'source-url' | 'source-document' }>,
|
|
895
|
-
messageId: string,
|
|
896
|
-
): VercelProjection => {
|
|
897
|
-
const message = _ensureMessage(state, messageId);
|
|
898
|
-
|
|
899
|
-
switch (chunk.type) {
|
|
900
|
-
case 'file': {
|
|
901
|
-
message.parts.push({ type: 'file', mediaType: chunk.mediaType, url: chunk.url });
|
|
902
|
-
return state;
|
|
903
|
-
}
|
|
904
|
-
case 'source-url': {
|
|
905
|
-
message.parts.push(
|
|
906
|
-
stripUndefined({
|
|
907
|
-
type: 'source-url' as const,
|
|
908
|
-
sourceId: chunk.sourceId,
|
|
909
|
-
url: chunk.url,
|
|
910
|
-
title: chunk.title,
|
|
911
|
-
}),
|
|
912
|
-
);
|
|
913
|
-
return state;
|
|
914
|
-
}
|
|
915
|
-
case 'source-document': {
|
|
916
|
-
message.parts.push(
|
|
917
|
-
stripUndefined({
|
|
918
|
-
type: 'source-document' as const,
|
|
919
|
-
sourceId: chunk.sourceId,
|
|
920
|
-
mediaType: chunk.mediaType,
|
|
921
|
-
title: chunk.title,
|
|
922
|
-
filename: chunk.filename,
|
|
923
|
-
}),
|
|
924
|
-
);
|
|
925
|
-
return state;
|
|
926
|
-
}
|
|
927
|
-
}
|
|
928
|
-
};
|
|
929
|
-
|
|
930
|
-
// ---------------------------------------------------------------------------
|
|
931
|
-
// data-* parts
|
|
932
|
-
// ---------------------------------------------------------------------------
|
|
933
|
-
|
|
934
|
-
const _foldDataPart = (
|
|
935
|
-
state: VercelProjection,
|
|
936
|
-
chunk: Extract<AI.UIMessageChunk, { type: `data-${string}` }>,
|
|
937
|
-
messageId: string,
|
|
938
|
-
): VercelProjection => {
|
|
939
|
-
if (chunk.transient) return state;
|
|
940
|
-
|
|
941
|
-
const message = _ensureMessage(state, messageId);
|
|
942
|
-
|
|
943
|
-
// CAST: chunk.type is `data-${string}` which satisfies DataUIPart, but
|
|
944
|
-
// TypeScript cannot verify the template literal matches a specific
|
|
945
|
-
// UIMessagePart variant at the type level.
|
|
946
|
-
const dataPart = stripUndefined({
|
|
947
|
-
type: chunk.type,
|
|
948
|
-
id: chunk.id,
|
|
949
|
-
data: chunk.data,
|
|
950
|
-
}) as AI.UIMessage['parts'][number];
|
|
951
|
-
|
|
952
|
-
if (chunk.id !== undefined) {
|
|
953
|
-
const idx = message.parts.findIndex((p) => p.type === chunk.type && 'id' in p && p.id === chunk.id);
|
|
954
|
-
if (idx !== -1) {
|
|
955
|
-
message.parts[idx] = dataPart;
|
|
956
|
-
return state;
|
|
957
|
-
}
|
|
958
|
-
}
|
|
959
|
-
|
|
960
|
-
message.parts.push(dataPart);
|
|
961
|
-
return state;
|
|
962
|
-
};
|
|
963
|
-
|
|
964
176
|
// ---------------------------------------------------------------------------
|
|
965
177
|
// getMessages
|
|
966
178
|
// ---------------------------------------------------------------------------
|
|
@@ -975,3 +187,5 @@ const _foldDataPart = (
|
|
|
975
187
|
* @returns The visible messages with their codec-message-ids, in publication order.
|
|
976
188
|
*/
|
|
977
189
|
export const getMessages = (projection: VercelProjection): CodecMessage<AI.UIMessage>[] => projection.messages;
|
|
190
|
+
|
|
191
|
+
export { init, type VercelProjection } from './reducer-state.js';
|