@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.
Files changed (221) hide show
  1. package/README.md +93 -111
  2. package/dist/ably-ai-transport.js +2401 -1387
  3. package/dist/ably-ai-transport.js.map +1 -1
  4. package/dist/ably-ai-transport.umd.cjs +1 -1
  5. package/dist/ably-ai-transport.umd.cjs.map +1 -1
  6. package/dist/constants.d.ts +116 -42
  7. package/dist/core/agent.d.ts +44 -0
  8. package/dist/core/channel-options.d.ts +57 -0
  9. package/dist/core/codec/codec-event.d.ts +9 -0
  10. package/dist/core/codec/decoder.d.ts +24 -24
  11. package/dist/core/codec/define-codec.d.ts +100 -0
  12. package/dist/core/codec/encoder.d.ts +10 -12
  13. package/dist/core/codec/field-bag.d.ts +85 -0
  14. package/dist/core/codec/fields.d.ts +141 -0
  15. package/dist/core/codec/index.d.ts +8 -2
  16. package/dist/core/codec/input-descriptor-decoder.d.ts +19 -0
  17. package/dist/core/codec/input-descriptor-encoder.d.ts +22 -0
  18. package/dist/core/codec/input-descriptors.d.ts +281 -0
  19. package/dist/core/codec/lifecycle-tracker.d.ts +10 -9
  20. package/dist/core/codec/output-descriptor-decoder.d.ts +29 -0
  21. package/dist/core/codec/output-descriptor-encoder.d.ts +31 -0
  22. package/dist/core/codec/output-descriptors.d.ts +237 -0
  23. package/dist/core/codec/types.d.ts +470 -119
  24. package/dist/core/codec/well-known-inputs.d.ts +52 -0
  25. package/dist/core/transport/agent-session.d.ts +10 -0
  26. package/dist/core/transport/agent-view.d.ts +296 -0
  27. package/dist/core/transport/client-session.d.ts +13 -0
  28. package/dist/core/transport/decode-fold.d.ts +55 -0
  29. package/dist/core/transport/headers.d.ts +121 -14
  30. package/dist/core/transport/index.d.ts +5 -6
  31. package/dist/core/transport/internal/bounded-map.d.ts +20 -0
  32. package/dist/core/transport/invocation.d.ts +74 -0
  33. package/dist/core/transport/load-history-pages.d.ts +71 -0
  34. package/dist/core/transport/load-history.d.ts +44 -0
  35. package/dist/core/transport/pipe-stream.d.ts +9 -9
  36. package/dist/core/transport/run-manager.d.ts +76 -0
  37. package/dist/core/transport/session-support.d.ts +55 -0
  38. package/dist/core/transport/tree.d.ts +523 -109
  39. package/dist/core/transport/types/agent.d.ts +375 -0
  40. package/dist/core/transport/types/client.d.ts +201 -0
  41. package/dist/core/transport/types/shared.d.ts +24 -0
  42. package/dist/core/transport/types/tree.d.ts +357 -0
  43. package/dist/core/transport/types/view.d.ts +249 -0
  44. package/dist/core/transport/types.d.ts +13 -553
  45. package/dist/core/transport/view.d.ts +390 -84
  46. package/dist/core/transport/wire-log.d.ts +102 -0
  47. package/dist/errors.d.ts +27 -10
  48. package/dist/index.d.ts +8 -9
  49. package/dist/logger.d.ts +12 -0
  50. package/dist/react/ably-ai-transport-react.js +1365 -1010
  51. package/dist/react/ably-ai-transport-react.js.map +1 -1
  52. package/dist/react/ably-ai-transport-react.umd.cjs +1 -1
  53. package/dist/react/ably-ai-transport-react.umd.cjs.map +1 -1
  54. package/dist/react/contexts/client-session-context.d.ts +37 -0
  55. package/dist/react/contexts/client-session-provider.d.ts +56 -0
  56. package/dist/react/create-session-hooks.d.ts +116 -0
  57. package/dist/react/index.d.ts +13 -12
  58. package/dist/react/internal/skipped-session.d.ts +8 -0
  59. package/dist/react/internal/use-resolved-session.d.ts +36 -0
  60. package/dist/react/use-ably-messages.d.ts +17 -14
  61. package/dist/react/use-client-session.d.ts +81 -0
  62. package/dist/react/use-create-view.d.ts +14 -13
  63. package/dist/react/use-tree.d.ts +30 -15
  64. package/dist/react/use-view.d.ts +81 -50
  65. package/dist/utils.d.ts +48 -71
  66. package/dist/vercel/ably-ai-transport-vercel.js +3257 -2499
  67. package/dist/vercel/ably-ai-transport-vercel.js.map +1 -1
  68. package/dist/vercel/ably-ai-transport-vercel.umd.cjs +1 -1
  69. package/dist/vercel/ably-ai-transport-vercel.umd.cjs.map +1 -1
  70. package/dist/vercel/codec/decode-lifecycle.d.ts +9 -0
  71. package/dist/vercel/codec/events.d.ts +50 -0
  72. package/dist/vercel/codec/fields.d.ts +44 -0
  73. package/dist/vercel/codec/fold-content.d.ts +16 -0
  74. package/dist/vercel/codec/fold-data.d.ts +16 -0
  75. package/dist/vercel/codec/fold-input.d.ts +67 -0
  76. package/dist/vercel/codec/fold-lifecycle.d.ts +16 -0
  77. package/dist/vercel/codec/fold-text.d.ts +16 -0
  78. package/dist/vercel/codec/fold-tool-input.d.ts +17 -0
  79. package/dist/vercel/codec/fold-tool-output.d.ts +16 -0
  80. package/dist/vercel/codec/index.d.ts +7 -20
  81. package/dist/vercel/codec/inputs.d.ts +11 -0
  82. package/dist/vercel/codec/outputs.d.ts +11 -0
  83. package/dist/vercel/codec/reducer-state.d.ts +121 -0
  84. package/dist/vercel/codec/reducer.d.ts +62 -0
  85. package/dist/vercel/codec/tool-transitions.d.ts +2 -8
  86. package/dist/vercel/codec/wire-data.d.ts +34 -0
  87. package/dist/vercel/index.d.ts +5 -5
  88. package/dist/vercel/react/ably-ai-transport-vercel-react.js +2859 -9705
  89. package/dist/vercel/react/ably-ai-transport-vercel-react.js.map +1 -1
  90. package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs +1 -45
  91. package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs.map +1 -1
  92. package/dist/vercel/react/contexts/chat-transport-context.d.ts +9 -7
  93. package/dist/vercel/react/contexts/chat-transport-provider.d.ts +53 -41
  94. package/dist/vercel/react/index.d.ts +1 -2
  95. package/dist/vercel/react/use-chat-transport.d.ts +30 -26
  96. package/dist/vercel/react/use-message-sync.d.ts +17 -30
  97. package/dist/vercel/run-end-reason.d.ts +84 -0
  98. package/dist/vercel/tool-part.d.ts +21 -0
  99. package/dist/vercel/transport/chat-transport.d.ts +41 -24
  100. package/dist/vercel/transport/index.d.ts +24 -20
  101. package/dist/vercel/transport/run-output-stream.d.ts +54 -0
  102. package/dist/version.d.ts +2 -0
  103. package/package.json +31 -24
  104. package/src/constants.ts +124 -51
  105. package/src/core/agent.ts +92 -0
  106. package/src/core/channel-options.ts +89 -0
  107. package/src/core/codec/codec-event.ts +27 -0
  108. package/src/core/codec/decoder.ts +202 -105
  109. package/src/core/codec/define-codec.ts +432 -0
  110. package/src/core/codec/encoder.ts +114 -107
  111. package/src/core/codec/field-bag.ts +142 -0
  112. package/src/core/codec/fields.ts +193 -0
  113. package/src/core/codec/index.ts +56 -6
  114. package/src/core/codec/input-descriptor-decoder.ts +97 -0
  115. package/src/core/codec/input-descriptor-encoder.ts +150 -0
  116. package/src/core/codec/input-descriptors.ts +373 -0
  117. package/src/core/codec/lifecycle-tracker.ts +10 -9
  118. package/src/core/codec/output-descriptor-decoder.ts +139 -0
  119. package/src/core/codec/output-descriptor-encoder.ts +101 -0
  120. package/src/core/codec/output-descriptors.ts +307 -0
  121. package/src/core/codec/types.ts +505 -126
  122. package/src/core/codec/well-known-inputs.ts +96 -0
  123. package/src/core/transport/agent-session.ts +1085 -0
  124. package/src/core/transport/agent-view.ts +738 -0
  125. package/src/core/transport/client-session.ts +780 -0
  126. package/src/core/transport/decode-fold.ts +101 -0
  127. package/src/core/transport/headers.ts +234 -22
  128. package/src/core/transport/index.ts +27 -27
  129. package/src/core/transport/internal/bounded-map.ts +27 -0
  130. package/src/core/transport/invocation.ts +98 -0
  131. package/src/core/transport/load-history-pages.ts +220 -0
  132. package/src/core/transport/load-history.ts +271 -0
  133. package/src/core/transport/pipe-stream.ts +63 -39
  134. package/src/core/transport/run-manager.ts +243 -0
  135. package/src/core/transport/session-support.ts +96 -0
  136. package/src/core/transport/tree.ts +1293 -308
  137. package/src/core/transport/types/agent.ts +434 -0
  138. package/src/core/transport/types/client.ts +247 -0
  139. package/src/core/transport/types/shared.ts +27 -0
  140. package/src/core/transport/types/tree.ts +393 -0
  141. package/src/core/transport/types/view.ts +288 -0
  142. package/src/core/transport/types.ts +13 -706
  143. package/src/core/transport/view.ts +1229 -450
  144. package/src/core/transport/wire-log.ts +189 -0
  145. package/src/errors.ts +29 -9
  146. package/src/event-emitter.ts +3 -2
  147. package/src/index.ts +86 -42
  148. package/src/logger.ts +14 -1
  149. package/src/react/contexts/client-session-context.ts +41 -0
  150. package/src/react/contexts/client-session-provider.tsx +222 -0
  151. package/src/react/create-session-hooks.ts +141 -0
  152. package/src/react/index.ts +24 -13
  153. package/src/react/internal/skipped-session.ts +62 -0
  154. package/src/react/internal/use-resolved-session.ts +63 -0
  155. package/src/react/use-ably-messages.ts +32 -22
  156. package/src/react/use-client-session.ts +178 -0
  157. package/src/react/use-create-view.ts +33 -29
  158. package/src/react/use-tree.ts +61 -30
  159. package/src/react/use-view.ts +138 -96
  160. package/src/utils.ts +83 -131
  161. package/src/vercel/codec/decode-lifecycle.ts +70 -0
  162. package/src/vercel/codec/events.ts +85 -0
  163. package/src/vercel/codec/fields.ts +58 -0
  164. package/src/vercel/codec/fold-content.ts +54 -0
  165. package/src/vercel/codec/fold-data.ts +46 -0
  166. package/src/vercel/codec/fold-input.ts +255 -0
  167. package/src/vercel/codec/fold-lifecycle.ts +85 -0
  168. package/src/vercel/codec/fold-text.ts +55 -0
  169. package/src/vercel/codec/fold-tool-input.ts +86 -0
  170. package/src/vercel/codec/fold-tool-output.ts +79 -0
  171. package/src/vercel/codec/index.ts +28 -21
  172. package/src/vercel/codec/inputs.ts +116 -0
  173. package/src/vercel/codec/outputs.ts +207 -0
  174. package/src/vercel/codec/reducer-state.ts +169 -0
  175. package/src/vercel/codec/reducer.ts +191 -0
  176. package/src/vercel/codec/tool-transitions.ts +3 -14
  177. package/src/vercel/codec/wire-data.ts +64 -0
  178. package/src/vercel/index.ts +7 -19
  179. package/src/vercel/react/contexts/chat-transport-context.ts +8 -7
  180. package/src/vercel/react/contexts/chat-transport-provider.tsx +87 -59
  181. package/src/vercel/react/index.ts +3 -5
  182. package/src/vercel/react/use-chat-transport.ts +44 -66
  183. package/src/vercel/react/use-message-sync.ts +75 -39
  184. package/src/vercel/run-end-reason.ts +157 -0
  185. package/src/vercel/tool-part.ts +25 -0
  186. package/src/vercel/transport/chat-transport.ts +380 -98
  187. package/src/vercel/transport/index.ts +38 -37
  188. package/src/vercel/transport/run-output-stream.ts +169 -0
  189. package/src/version.ts +2 -0
  190. package/dist/core/transport/client-transport.d.ts +0 -10
  191. package/dist/core/transport/decode-history.d.ts +0 -43
  192. package/dist/core/transport/server-transport.d.ts +0 -7
  193. package/dist/core/transport/stream-router.d.ts +0 -29
  194. package/dist/core/transport/turn-manager.d.ts +0 -37
  195. package/dist/react/contexts/transport-context.d.ts +0 -31
  196. package/dist/react/contexts/transport-provider.d.ts +0 -49
  197. package/dist/react/create-transport-hooks.d.ts +0 -124
  198. package/dist/react/use-active-turns.d.ts +0 -12
  199. package/dist/react/use-client-transport.d.ts +0 -80
  200. package/dist/vercel/codec/accumulator.d.ts +0 -21
  201. package/dist/vercel/codec/decoder.d.ts +0 -22
  202. package/dist/vercel/codec/encoder.d.ts +0 -41
  203. package/dist/vercel/react/use-staged-add-tool-approval-response.d.ts +0 -30
  204. package/dist/vercel/tool-approvals.d.ts +0 -124
  205. package/dist/vercel/tool-events.d.ts +0 -26
  206. package/src/core/transport/client-transport.ts +0 -977
  207. package/src/core/transport/decode-history.ts +0 -485
  208. package/src/core/transport/server-transport.ts +0 -612
  209. package/src/core/transport/stream-router.ts +0 -136
  210. package/src/core/transport/turn-manager.ts +0 -165
  211. package/src/react/contexts/transport-context.ts +0 -37
  212. package/src/react/contexts/transport-provider.tsx +0 -164
  213. package/src/react/create-transport-hooks.ts +0 -144
  214. package/src/react/use-active-turns.ts +0 -72
  215. package/src/react/use-client-transport.ts +0 -197
  216. package/src/vercel/codec/accumulator.ts +0 -588
  217. package/src/vercel/codec/decoder.ts +0 -618
  218. package/src/vercel/codec/encoder.ts +0 -410
  219. package/src/vercel/react/use-staged-add-tool-approval-response.ts +0 -87
  220. package/src/vercel/tool-approvals.ts +0 -380
  221. package/src/vercel/tool-events.ts +0 -53
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Vercel codec input/output unions.
3
+ *
4
+ * The codec splits cleanly along the protocol's `ai-input` / `ai-output`
5
+ * wire seam:
6
+ *
7
+ * - **`VercelOutput`** = `AI.UIMessageChunk` — the AI SDK's streamed-output
8
+ * domain model, published by the agent on `ai-output`.
9
+ * - **`VercelInput`** = a discriminated union of the SDK's well-known
10
+ * input shapes — published by the client on `ai-input`. The Vercel
11
+ * codec has no codec-local input variants today: every variant comes
12
+ * from `@ably/ai-transport`'s well-known set.
13
+ */
14
+
15
+ import type * as AI from 'ai';
16
+
17
+ import type {
18
+ Regenerate,
19
+ ToolApprovalResponse,
20
+ ToolResult,
21
+ ToolResultError,
22
+ UserMessage,
23
+ } from '../../core/codec/index.js';
24
+
25
+ // ---------------------------------------------------------------------------
26
+ // Domain payloads
27
+ //
28
+ // The core well-known tool variants are domain-independent: the Vercel
29
+ // layer supplies the concrete payload shapes. Tool outputs are inherently
30
+ // tool-defined, so `output` stays `unknown` — but confined here, never in
31
+ // the core.
32
+ // ---------------------------------------------------------------------------
33
+
34
+ /** Vercel domain payload for a {@link ToolResult}. */
35
+ export interface VercelToolResultPayload {
36
+ /** The tool call this result corresponds to. */
37
+ toolCallId: string;
38
+ /** The tool's output value. Tool-defined shape. */
39
+ output: unknown;
40
+ }
41
+
42
+ /** Vercel domain payload for a {@link ToolResultError}. */
43
+ export interface VercelToolResultErrorPayload {
44
+ /** The tool call this error corresponds to. */
45
+ toolCallId: string;
46
+ /** Human-readable description of the failure. */
47
+ message: string;
48
+ }
49
+
50
+ /** Vercel domain payload for a {@link ToolApprovalResponse}. */
51
+ export interface VercelToolApprovalResponsePayload {
52
+ /** The tool call this approval responds to. */
53
+ toolCallId: string;
54
+ /** Whether the user approved the tool execution. */
55
+ approved: boolean;
56
+ /** Optional human-readable reason (typically used on denial). */
57
+ reason?: string;
58
+ }
59
+
60
+ // ---------------------------------------------------------------------------
61
+ // Unions
62
+ // ---------------------------------------------------------------------------
63
+
64
+ /**
65
+ * The Vercel codec's `TInput` — every record-shape a client publishes on
66
+ * the `ai-input` wire. Composed from the SDK's well-known input shapes,
67
+ * with the tool variants parameterized by the Vercel domain payloads above.
68
+ */
69
+ export type VercelInput =
70
+ | UserMessage<AI.UIMessage>
71
+ | Regenerate
72
+ | ToolResult<VercelToolResultPayload>
73
+ | ToolResultError<VercelToolResultErrorPayload>
74
+ | ToolApprovalResponse<VercelToolApprovalResponsePayload>;
75
+
76
+ /**
77
+ * The Vercel codec's `TOutput` — every record-shape the agent publishes
78
+ * on the `ai-output` wire. The Vercel codec passes the AI SDK's
79
+ * `UIMessageChunk` through unchanged.
80
+ */
81
+ export type VercelOutput = AI.UIMessageChunk;
82
+
83
+ // ---------------------------------------------------------------------------
84
+ // Projection re-export
85
+ // ---------------------------------------------------------------------------
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Shared Vercel codec header-field bindings.
3
+ *
4
+ * Each field binds a codec header key to its value type once (see
5
+ * {@link HeaderField}); the output/input descriptors and escape hatches all
6
+ * read and write through these bindings, so a header key cannot drift between
7
+ * the encode and decode side. Domain field names live in the Vercel layer, not
8
+ * core, per the header-discipline rule.
9
+ */
10
+
11
+ import type * as AI from 'ai';
12
+
13
+ import { boolField, enumField, type HeaderField, jsonField, strField } from '../../core/codec/index.js';
14
+
15
+ /** Stream / message id (text & reasoning streams). */
16
+ export const fId = strField('id');
17
+ /**
18
+ * Provider metadata envelope, typed to the AI SDK shape. Annotated explicitly:
19
+ * the inferred type resolves to the AI SDK's internal `SharedV3ProviderMetadata`
20
+ * alias, which isn't portably nameable across the package boundary.
21
+ */
22
+ export const fMeta: HeaderField<AI.ProviderMetadata | undefined, 'providerMetadata'> = jsonField<
23
+ AI.ProviderMetadata,
24
+ 'providerMetadata'
25
+ >('providerMetadata');
26
+ /** Tool call id — defaulted to total: an absent header reads as `''`. */
27
+ export const fToolCallId = strField('toolCallId', '');
28
+ /** Tool name — defaulted to total. */
29
+ export const fToolName = strField('toolName', '');
30
+ /** Whether the tool is a dynamic tool. */
31
+ export const fDynamic = boolField('dynamic');
32
+ /** Optional human-readable title. */
33
+ export const fTitle = strField('title');
34
+ /** Whether the provider executed the tool. */
35
+ export const fProviderExecuted = boolField('providerExecuted');
36
+ /** Media type for file / source-document parts — defaulted to total. */
37
+ export const fMediaType = strField('mediaType', '');
38
+ /** Source id for source-url / source-document parts — defaulted to total. */
39
+ export const fSourceId = strField('sourceId', '');
40
+
41
+ // --- input-side bindings (shared by the input descriptors' encode/decode) ---
42
+
43
+ /** Domain message id (`message.id`) stamped on every user-message part — distinct from the wire codec-message-id transport header. */
44
+ export const fMessageId = strField('messageId');
45
+ /** Whether the user approved a tool execution — defaulted to total so an absent header reads `false`. */
46
+ export const fApproved = boolField('approved', false);
47
+ /** Optional human-readable reason on a tool-approval response. */
48
+ export const fReason = strField('reason');
49
+
50
+ /**
51
+ * Validated finish reason. Mirrors the AI SDK's `FinishReason` literals and
52
+ * falls back to `'stop'` for an absent or unrecognized value.
53
+ */
54
+ export const fFinishReason = enumField(
55
+ 'finishReason',
56
+ ['stop', 'length', 'content-filter', 'tool-calls', 'error', 'other'] as const,
57
+ 'stop',
58
+ );
@@ -0,0 +1,54 @@
1
+ /**
2
+ * File and source content-part folds: file / source-url / source-document.
3
+ * These are independent attachments — each appends a part, never dedups.
4
+ */
5
+
6
+ import type * as AI from 'ai';
7
+
8
+ import { stripUndefined } from '../../utils.js';
9
+ import { ensureMessage, type VercelProjection } from './reducer-state.js';
10
+
11
+ /**
12
+ * Fold a file or source content chunk into the projection.
13
+ * @param state - Projection to fold into.
14
+ * @param chunk - The file, source-url, or source-document chunk.
15
+ * @param messageId - The target codec-message-id.
16
+ * @returns The same projection reference.
17
+ */
18
+ export const foldContentPart = (
19
+ state: VercelProjection,
20
+ chunk: Extract<AI.UIMessageChunk, { type: 'file' | 'source-url' | 'source-document' }>,
21
+ messageId: string,
22
+ ): VercelProjection => {
23
+ const message = ensureMessage(state, messageId);
24
+
25
+ switch (chunk.type) {
26
+ case 'file': {
27
+ message.parts.push({ type: 'file', mediaType: chunk.mediaType, url: chunk.url });
28
+ return state;
29
+ }
30
+ case 'source-url': {
31
+ message.parts.push(
32
+ stripUndefined({
33
+ type: 'source-url' as const,
34
+ sourceId: chunk.sourceId,
35
+ url: chunk.url,
36
+ title: chunk.title,
37
+ }),
38
+ );
39
+ return state;
40
+ }
41
+ case 'source-document': {
42
+ message.parts.push(
43
+ stripUndefined({
44
+ type: 'source-document' as const,
45
+ sourceId: chunk.sourceId,
46
+ mediaType: chunk.mediaType,
47
+ title: chunk.title,
48
+ filename: chunk.filename,
49
+ }),
50
+ );
51
+ return state;
52
+ }
53
+ }
54
+ };
@@ -0,0 +1,46 @@
1
+ /**
2
+ * data-* part folds. Transient data parts are dropped; persistent ones are
3
+ * appended, or replaced in place when a matching `id` is already present.
4
+ */
5
+
6
+ import type * as AI from 'ai';
7
+
8
+ import { stripUndefined } from '../../utils.js';
9
+ import { ensureMessage, type VercelProjection } from './reducer-state.js';
10
+
11
+ /**
12
+ * Fold a `data-*` chunk into the projection.
13
+ * @param state - Projection to fold into.
14
+ * @param chunk - The data-* chunk.
15
+ * @param messageId - The target codec-message-id.
16
+ * @returns The same projection reference.
17
+ */
18
+ export const foldDataPart = (
19
+ state: VercelProjection,
20
+ chunk: Extract<AI.UIMessageChunk, { type: `data-${string}` }>,
21
+ messageId: string,
22
+ ): VercelProjection => {
23
+ if (chunk.transient) return state;
24
+
25
+ const message = ensureMessage(state, messageId);
26
+
27
+ // CAST: chunk.type is `data-${string}` which satisfies DataUIPart, but
28
+ // TypeScript cannot verify the template literal matches a specific
29
+ // UIMessagePart variant at the type level.
30
+ const dataPart = stripUndefined({
31
+ type: chunk.type,
32
+ id: chunk.id,
33
+ data: chunk.data,
34
+ }) as AI.UIMessage['parts'][number];
35
+
36
+ if (chunk.id !== undefined) {
37
+ const idx = message.parts.findIndex((p) => p.type === chunk.type && 'id' in p && p.id === chunk.id);
38
+ if (idx !== -1) {
39
+ message.parts[idx] = dataPart;
40
+ return state;
41
+ }
42
+ }
43
+
44
+ message.parts.push(dataPart);
45
+ return state;
46
+ };
@@ -0,0 +1,255 @@
1
+ /**
2
+ * Client-published input folds and the pending-resolution buffering.
3
+ *
4
+ * Tool resolutions (`ToolResult`, `ToolResultError`, `ToolApprovalResponse`)
5
+ * carry a `codecMessageId` targeting the assistant they amend. When that
6
+ * assistant (or its tool part) has not yet arrived, the resolution is buffered
7
+ * in `pendingToolResolutions` and {@link retryPendingResolutions} re-evaluates
8
+ * it after every subsequent fold.
9
+ */
10
+
11
+ import type * as AI from 'ai';
12
+
13
+ import type { ReducerMeta, ToolApprovalResponse, ToolResult, ToolResultError } from '../../core/codec/index.js';
14
+ import type {
15
+ VercelToolApprovalResponsePayload,
16
+ VercelToolResultErrorPayload,
17
+ VercelToolResultPayload,
18
+ } from './events.js';
19
+ import {
20
+ ensureTrackers,
21
+ getToolPart,
22
+ type OwnerLookup,
23
+ type PendingToolResolution,
24
+ type VercelProjection,
25
+ } from './reducer-state.js';
26
+ import { toolBase, transitionToolPart } from './tool-transitions.js';
27
+
28
+ /**
29
+ * Fold a user message into the projection, correlating on the wire
30
+ * codec-message-id (the caller's `message.id` is preserved verbatim). A
31
+ * multi-part user message fans out into one wire event per part, all sharing
32
+ * the codec-message-id — folding appends the incoming parts to the existing
33
+ * entry, reassembling the message part by part. The transport delivers each
34
+ * wire exactly once (its per-message version high-water-mark drops replays),
35
+ * so the merge sees every part once and stays consistent.
36
+ *
37
+ * Optimistic (serial-less) seeds need no special handling here: the transport
38
+ * refolds the node from its log when the echo's serial arrives, rebuilding the
39
+ * projection from a fresh `init` so the seed never coexists with its echo.
40
+ * @param state - Projection to fold into.
41
+ * @param message - The user message (or one decoded part of it) to add or merge.
42
+ * @param meta - Transport-derived metadata carrying the codec-message-id.
43
+ * @returns The same projection reference.
44
+ */
45
+ export const foldUserMessage = (
46
+ state: VercelProjection,
47
+ message: AI.UIMessage,
48
+ meta: ReducerMeta,
49
+ ): VercelProjection => {
50
+ // Correlate the projection entry on the wire codec-message-id; the
51
+ // caller-supplied `message.id` is preserved verbatim and surfaced to the
52
+ // application unchanged. Without a codec-message-id the message has no
53
+ // identity to key on, so it is appended as a fresh entry.
54
+ const codecMessageId = meta.messageId;
55
+ if (codecMessageId === undefined) {
56
+ state.messages.push({ codecMessageId: message.id, message });
57
+ return state;
58
+ }
59
+ const existing = state.messages.find((e) => e.codecMessageId === codecMessageId);
60
+ if (existing === undefined) {
61
+ state.messages.push({ codecMessageId, message });
62
+ } else {
63
+ // Merge by codec-message-id: keep the existing envelope (id and role are
64
+ // stamped identically on every part of one message) and append the
65
+ // incoming parts in fold order — wire serials preserve publish order.
66
+ existing.message.parts.push(...message.parts);
67
+ }
68
+ return state;
69
+ };
70
+
71
+ /**
72
+ * Fold a client-published `ToolResult`. The input carries
73
+ * `codecMessageId` pointing at the assistant whose `dynamic-tool` part
74
+ * holds the matching `toolCallId`. If the assistant and its matching
75
+ * `dynamic-tool` part are both present, fold directly; otherwise pend
76
+ * until that tool part arrives.
77
+ * @param state - Projection to fold into.
78
+ * @param event - The tool-result input (codecMessageId + domain payload).
79
+ * @returns The same projection reference.
80
+ */
81
+ export const foldClientToolResult = (
82
+ state: VercelProjection,
83
+ event: ToolResult<VercelToolResultPayload>,
84
+ ): VercelProjection => {
85
+ const { toolCallId, output } = event.payload;
86
+ return resolveOrPend(state, event.codecMessageId, toolCallId, { kind: 'tool-result', output });
87
+ };
88
+
89
+ /**
90
+ * Fold a client-published `ToolResultError`. Mirrors
91
+ * {@link foldClientToolResult} but with the error transition.
92
+ * @param state - Projection to fold into.
93
+ * @param event - The tool-result-error input (codecMessageId + domain payload).
94
+ * @returns The same projection reference.
95
+ */
96
+ export const foldClientToolResultError = (
97
+ state: VercelProjection,
98
+ event: ToolResultError<VercelToolResultErrorPayload>,
99
+ ): VercelProjection => {
100
+ const { toolCallId, message } = event.payload;
101
+ return resolveOrPend(state, event.codecMessageId, toolCallId, { kind: 'tool-result-error', message });
102
+ };
103
+
104
+ /**
105
+ * Fold a client-published `ToolApprovalResponse`. The input carries
106
+ * `codecMessageId` pointing at the assistant whose `dynamic-tool` part
107
+ * holds the matching `toolCallId`. Approval → `approval-responded`;
108
+ * denial → `output-denied` via {@link transitionToolPart}.
109
+ * @param state - Projection to fold into.
110
+ * @param event - The approval-response input.
111
+ * @returns The same projection reference.
112
+ */
113
+ export const foldToolApprovalResponse = (
114
+ state: VercelProjection,
115
+ event: ToolApprovalResponse<VercelToolApprovalResponsePayload>,
116
+ ): VercelProjection => {
117
+ const { toolCallId, approved, reason } = event.payload;
118
+ return resolveOrPend(state, event.codecMessageId, toolCallId, {
119
+ kind: 'tool-approval-response',
120
+ approved,
121
+ ...(reason === undefined ? {} : { reason }),
122
+ });
123
+ };
124
+
125
+ /**
126
+ * Apply a resolution when its tool part is present, otherwise buffer it in
127
+ * `pendingToolResolutions` for {@link retryPendingResolutions}.
128
+ * @param state - Projection to fold into.
129
+ * @param codecMessageId - The assistant the resolution targets.
130
+ * @param toolCallId - The tool call being resolved.
131
+ * @param resolution - The resolution variant to apply or buffer.
132
+ * @returns The same projection reference.
133
+ */
134
+ const resolveOrPend = (
135
+ state: VercelProjection,
136
+ codecMessageId: string,
137
+ toolCallId: string,
138
+ resolution: PendingToolResolution['resolution'],
139
+ ): VercelProjection => {
140
+ const owner = findOwner(state, codecMessageId, toolCallId);
141
+ if (owner) {
142
+ applyResolution(owner, toolCallId, resolution);
143
+ } else {
144
+ state.pendingToolResolutions.push({ targetCodecMessageId: codecMessageId, toolCallId, resolution });
145
+ }
146
+ return state;
147
+ };
148
+
149
+ /**
150
+ * Apply one tool resolution onto its located `dynamic-tool` part, replacing
151
+ * the part with the transitioned shape — the single application point shared
152
+ * by the direct folds and {@link retryPendingResolutions}.
153
+ * @param owner - The located owner (message + tracker + part).
154
+ * @param toolCallId - The tool call being resolved.
155
+ * @param resolution - The resolution variant to apply.
156
+ */
157
+ const applyResolution = (
158
+ owner: OwnerLookup,
159
+ toolCallId: string,
160
+ resolution: PendingToolResolution['resolution'],
161
+ ): void => {
162
+ switch (resolution.kind) {
163
+ case 'tool-result': {
164
+ owner.message.parts[owner.tracker.partIndex] = transitionToolPart(owner.part, {
165
+ type: 'tool-output-available',
166
+ toolCallId,
167
+ output: resolution.output,
168
+ });
169
+ break;
170
+ }
171
+ case 'tool-result-error': {
172
+ owner.message.parts[owner.tracker.partIndex] = transitionToolPart(owner.part, {
173
+ type: 'tool-output-error',
174
+ toolCallId,
175
+ errorText: resolution.message,
176
+ });
177
+ break;
178
+ }
179
+ case 'tool-approval-response': {
180
+ owner.message.parts[owner.tracker.partIndex] = approvalTransition(
181
+ owner.part,
182
+ resolution.approved,
183
+ resolution.reason,
184
+ );
185
+ break;
186
+ }
187
+ }
188
+ };
189
+
190
+ /**
191
+ * Re-attempt every pending tool resolution against the current projection.
192
+ * Successfully promoted entries are removed from the pending list. Cheap:
193
+ * bounded by the number of pending entries.
194
+ * @param state - Projection to walk and mutate.
195
+ */
196
+ export const retryPendingResolutions = (state: VercelProjection): void => {
197
+ const next: VercelProjection['pendingToolResolutions'] = [];
198
+ for (const pending of state.pendingToolResolutions) {
199
+ const owner = findOwner(state, pending.targetCodecMessageId, pending.toolCallId);
200
+ if (!owner) {
201
+ next.push(pending);
202
+ continue;
203
+ }
204
+ applyResolution(owner, pending.toolCallId, pending.resolution);
205
+ }
206
+ state.pendingToolResolutions = next;
207
+ };
208
+
209
+ const findOwner = (state: VercelProjection, codecMessageId: string, toolCallId: string): OwnerLookup | undefined => {
210
+ const entry = state.messages.find((e) => e.codecMessageId === codecMessageId);
211
+ if (!entry) return undefined;
212
+ const trackers = ensureTrackers(state, codecMessageId);
213
+ const found = getToolPart(entry.message, trackers, toolCallId);
214
+ if (!found) return undefined;
215
+ return { message: entry.message, tracker: found.tracker, part: found.part };
216
+ };
217
+
218
+ /**
219
+ * Build the next `dynamic-tool` part shape for an approval response.
220
+ *
221
+ * For `approved=true`, transition to `approval-responded` so the AI SDK's
222
+ * multi-step loop will auto-run the tool on the next step.
223
+ * `transitionToolPart` has no shape for this transition, so we synthesize
224
+ * the part directly.
225
+ *
226
+ * For `approved=false`, delegate to `transitionToolPart` with a synthetic
227
+ * `tool-output-denied` chunk so denial mirrors the chunk-driven path.
228
+ * @param part - The existing `dynamic-tool` part being transitioned.
229
+ * @param approved - Whether the user approved the tool execution.
230
+ * @param reason - Optional human-readable reason.
231
+ * @returns The replacement `dynamic-tool` part.
232
+ */
233
+ const approvalTransition = (
234
+ part: AI.DynamicToolUIPart,
235
+ approved: boolean,
236
+ reason: string | undefined,
237
+ ): AI.DynamicToolUIPart => {
238
+ if (approved) {
239
+ return {
240
+ ...toolBase(part),
241
+ state: 'approval-responded',
242
+ input: 'input' in part ? part.input : undefined,
243
+ approval: {
244
+ id: 'approval' in part && part.approval ? part.approval.id : '',
245
+ approved: true,
246
+ ...(reason === undefined ? {} : { reason }),
247
+ },
248
+ };
249
+ }
250
+ return transitionToolPart(part, {
251
+ type: 'tool-output-denied',
252
+ toolCallId: part.toolCallId,
253
+ ...(reason === undefined ? {} : { reason }),
254
+ });
255
+ };
@@ -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
+ };