@genkit-ai/vercel-ai 0.2.0-rc.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/lib/client.mjs ADDED
@@ -0,0 +1,345 @@
1
+ import { applyPatch, streamFlow } from "genkit/beta/client";
2
+ import {
3
+ asRestartInterrupt,
4
+ currentTurnResolvedTools,
5
+ findLastNonEmptyUserMessage,
6
+ mapUIMessageToGenkit
7
+ } from "./mapping.mjs";
8
+ import {
9
+ mapGenkitMessageToUI,
10
+ messagesFromSnapshot,
11
+ restartInterrupt
12
+ } from "./mapping.mjs";
13
+ const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
14
+ function errorTextFromUnknown(err) {
15
+ if (err instanceof Error) {
16
+ return err.message || "Agent execution failed";
17
+ }
18
+ if (typeof err === "string") {
19
+ return err || "Agent execution failed";
20
+ }
21
+ if (err === void 0 || err === null) {
22
+ return "Agent execution failed";
23
+ }
24
+ try {
25
+ return JSON.stringify(err);
26
+ } catch {
27
+ return String(err);
28
+ }
29
+ }
30
+ function errorStream(errorText) {
31
+ return new ReadableStream({
32
+ start(controller) {
33
+ controller.enqueue({
34
+ type: "start",
35
+ messageId: `msg-${crypto.randomUUID()}`
36
+ });
37
+ controller.enqueue({ type: "error", errorText });
38
+ controller.enqueue({ type: "finish" });
39
+ controller.close();
40
+ }
41
+ });
42
+ }
43
+ class GenkitChatTransport {
44
+ url;
45
+ headersConfig;
46
+ constructor(config) {
47
+ this.url = config.url;
48
+ this.headersConfig = config.headers;
49
+ }
50
+ resolveHeaders(extra) {
51
+ const base = typeof this.headersConfig === "function" ? this.headersConfig() : this.headersConfig;
52
+ let extraRecord;
53
+ if (extra instanceof Headers) {
54
+ extraRecord = {};
55
+ extra.forEach((value, key) => {
56
+ extraRecord[key] = value;
57
+ });
58
+ } else {
59
+ extraRecord = extra;
60
+ }
61
+ if (!base && !extraRecord) return void 0;
62
+ return { ...base, ...extraRecord };
63
+ }
64
+ // The `trigger` parameter is either 'submit-message' (a new user turn) or
65
+ // 'regenerate-message' (re-run the last assistant response). Because state
66
+ // is server-managed by `sessionId`, regeneration is treated as a fresh turn
67
+ // from the current session state (there is no client-side snapshot pointer
68
+ // to rewind to).
69
+ async sendMessages({
70
+ trigger,
71
+ chatId,
72
+ messages,
73
+ abortSignal,
74
+ headers
75
+ }) {
76
+ if (!UUID_PATTERN.test(chatId)) {
77
+ return errorStream(
78
+ `Invalid chatId '${chatId}': the useChat 'id' is used as the Genkit sessionId and must be a UUID. Generate one with crypto.randomUUID().`
79
+ );
80
+ }
81
+ const isRegenerate = trigger === "regenerate-message";
82
+ const resolvedTools = isRegenerate ? [] : currentTurnResolvedTools(messages);
83
+ const isResume = resolvedTools.length > 0;
84
+ const init = { sessionId: chatId };
85
+ let agentInput;
86
+ const respondedInterruptIds = /* @__PURE__ */ new Set();
87
+ if (isResume) {
88
+ const respond = [];
89
+ const restart = [];
90
+ for (const tr of resolvedTools) {
91
+ const asRestart = asRestartInterrupt(tr.result);
92
+ if (asRestart) {
93
+ restart.push({
94
+ toolRequest: {
95
+ name: tr.toolName,
96
+ ref: tr.toolCallId,
97
+ input: tr.input
98
+ },
99
+ metadata: { resumed: asRestart.metadata ?? true }
100
+ });
101
+ } else {
102
+ respondedInterruptIds.add(tr.toolCallId);
103
+ respond.push({
104
+ toolResponse: {
105
+ name: tr.toolName,
106
+ ref: tr.toolCallId,
107
+ output: tr.result
108
+ }
109
+ });
110
+ }
111
+ }
112
+ agentInput = {
113
+ resume: {
114
+ ...respond.length > 0 && { respond },
115
+ ...restart.length > 0 && { restart }
116
+ }
117
+ };
118
+ } else {
119
+ const lastUserMsg = findLastNonEmptyUserMessage(messages);
120
+ if (!lastUserMsg) {
121
+ return errorStream("No user message found");
122
+ }
123
+ const genkitMsg = mapUIMessageToGenkit(lastUserMsg);
124
+ agentInput = { message: genkitMsg };
125
+ }
126
+ const response = streamFlow({
127
+ url: this.url,
128
+ input: agentInput,
129
+ init,
130
+ headers: this.resolveHeaders(headers),
131
+ abortSignal
132
+ });
133
+ let aborted = false;
134
+ return new ReadableStream({
135
+ start: async (controller) => {
136
+ const onAbort = () => {
137
+ aborted = true;
138
+ };
139
+ abortSignal?.addEventListener("abort", onAbort, { once: true });
140
+ let started = false;
141
+ let textBlockId = null;
142
+ let reasoningBlockId = null;
143
+ let inStep = false;
144
+ let customState;
145
+ const emittedToolInputs = /* @__PURE__ */ new Set();
146
+ const pendingRefByToolName = /* @__PURE__ */ new Map();
147
+ const correlateRequestId = (name) => {
148
+ const id = crypto.randomUUID();
149
+ const queue = pendingRefByToolName.get(name) ?? [];
150
+ queue.push(id);
151
+ pendingRefByToolName.set(name, queue);
152
+ return id;
153
+ };
154
+ const correlateResponseId = (name) => {
155
+ const queue = pendingRefByToolName.get(name);
156
+ if (queue && queue.length > 0) {
157
+ return queue.shift();
158
+ }
159
+ return crypto.randomUUID();
160
+ };
161
+ const enqueue = (chunk) => {
162
+ try {
163
+ controller.enqueue(chunk);
164
+ } catch {
165
+ }
166
+ };
167
+ const ensureStarted = () => {
168
+ if (!started) {
169
+ enqueue({
170
+ type: "start",
171
+ messageId: `msg-${crypto.randomUUID()}`
172
+ });
173
+ started = true;
174
+ }
175
+ };
176
+ const ensureStep = () => {
177
+ if (!inStep) {
178
+ enqueue({ type: "start-step" });
179
+ inStep = true;
180
+ }
181
+ };
182
+ const closeTextBlock = () => {
183
+ if (textBlockId) {
184
+ enqueue({ type: "text-end", id: textBlockId });
185
+ textBlockId = null;
186
+ }
187
+ };
188
+ const closeReasoningBlock = () => {
189
+ if (reasoningBlockId) {
190
+ enqueue({ type: "reasoning-end", id: reasoningBlockId });
191
+ reasoningBlockId = null;
192
+ }
193
+ };
194
+ const closeStep = () => {
195
+ if (inStep) {
196
+ enqueue({ type: "finish-step" });
197
+ inStep = false;
198
+ }
199
+ };
200
+ try {
201
+ for await (const chunk of response.stream) {
202
+ if (aborted) break;
203
+ if (chunk?.customPatch) {
204
+ customState = applyPatch(customState, chunk.customPatch);
205
+ ensureStarted();
206
+ enqueue({
207
+ type: "data-custom",
208
+ data: customState,
209
+ transient: true
210
+ });
211
+ }
212
+ const mc = chunk?.modelChunk;
213
+ if (!mc?.content) continue;
214
+ ensureStarted();
215
+ for (const part of mc.content) {
216
+ if (typeof part.reasoning === "string" && part.reasoning !== "") {
217
+ ensureStep();
218
+ if (!reasoningBlockId) {
219
+ reasoningBlockId = `reasoning-${crypto.randomUUID()}`;
220
+ enqueue({ type: "reasoning-start", id: reasoningBlockId });
221
+ }
222
+ enqueue({
223
+ type: "reasoning-delta",
224
+ id: reasoningBlockId,
225
+ delta: part.reasoning
226
+ });
227
+ }
228
+ if (part.text !== void 0 && part.text !== "") {
229
+ closeReasoningBlock();
230
+ ensureStep();
231
+ if (!textBlockId) {
232
+ textBlockId = `text-${crypto.randomUUID()}`;
233
+ enqueue({ type: "text-start", id: textBlockId });
234
+ }
235
+ enqueue({
236
+ type: "text-delta",
237
+ id: textBlockId,
238
+ delta: part.text
239
+ });
240
+ }
241
+ if (part.toolRequest) {
242
+ const tr = part.toolRequest;
243
+ const toolCallId = tr.ref ?? correlateRequestId(tr.name);
244
+ if (!emittedToolInputs.has(toolCallId)) {
245
+ emittedToolInputs.add(toolCallId);
246
+ closeReasoningBlock();
247
+ closeTextBlock();
248
+ ensureStep();
249
+ enqueue({
250
+ type: "tool-input-start",
251
+ toolCallId,
252
+ toolName: tr.name
253
+ });
254
+ enqueue({
255
+ type: "tool-input-available",
256
+ toolCallId,
257
+ toolName: tr.name,
258
+ input: tr.input
259
+ });
260
+ }
261
+ }
262
+ if (part.toolResponse) {
263
+ const tr = part.toolResponse;
264
+ const toolCallId = tr.ref ?? correlateResponseId(tr.name);
265
+ if (!respondedInterruptIds.has(toolCallId)) {
266
+ enqueue({
267
+ type: "tool-output-available",
268
+ toolCallId,
269
+ output: tr.output
270
+ });
271
+ closeStep();
272
+ }
273
+ }
274
+ }
275
+ if (chunk?.turnEnd) {
276
+ closeReasoningBlock();
277
+ closeTextBlock();
278
+ closeStep();
279
+ }
280
+ }
281
+ if (aborted) {
282
+ ensureStarted();
283
+ closeReasoningBlock();
284
+ closeTextBlock();
285
+ closeStep();
286
+ enqueue({ type: "abort", reason: "Request was aborted" });
287
+ enqueue({ type: "finish" });
288
+ try {
289
+ controller.close();
290
+ } catch {
291
+ }
292
+ return;
293
+ }
294
+ await response.output;
295
+ } catch (err) {
296
+ if (err instanceof DOMException && err.name === "AbortError") {
297
+ ensureStarted();
298
+ closeReasoningBlock();
299
+ closeTextBlock();
300
+ closeStep();
301
+ enqueue({ type: "abort", reason: "Request was aborted" });
302
+ } else {
303
+ ensureStarted();
304
+ enqueue({
305
+ type: "error",
306
+ errorText: errorTextFromUnknown(err)
307
+ });
308
+ }
309
+ } finally {
310
+ closeReasoningBlock();
311
+ closeTextBlock();
312
+ closeStep();
313
+ ensureStarted();
314
+ enqueue({ type: "finish" });
315
+ abortSignal?.removeEventListener("abort", onAbort);
316
+ try {
317
+ controller.close();
318
+ } catch {
319
+ }
320
+ }
321
+ },
322
+ cancel: () => {
323
+ aborted = true;
324
+ }
325
+ });
326
+ }
327
+ /**
328
+ * Genkit transport does not support stream reconnection.
329
+ *
330
+ * Genkit has a durable streaming feature on the server side; support
331
+ * for reconnection may be added in a future version.
332
+ *
333
+ * @returns Always returns `null`.
334
+ */
335
+ async reconnectToStream() {
336
+ return null;
337
+ }
338
+ }
339
+ export {
340
+ GenkitChatTransport,
341
+ mapGenkitMessageToUI,
342
+ messagesFromSnapshot,
343
+ restartInterrupt
344
+ };
345
+ //# sourceMappingURL=client.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/client.ts"],"sourcesContent":["/**\n * Copyright 2026 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { ChatTransport, UIMessage, UIMessageChunk } from 'ai';\n// Type-only imports are erased at compile time, so importing from `genkit`\n// keeps this module browser-safe (no Node-only runtime code is pulled in)\n// while staying in sync with the canonical agent protocol types.\nimport type { AgentOutput, AgentStreamChunk } from 'genkit/beta';\nimport { applyPatch, streamFlow } from 'genkit/beta/client';\n\nimport {\n asRestartInterrupt,\n currentTurnResolvedTools,\n findLastNonEmptyUserMessage,\n mapUIMessageToGenkit,\n} from './mapping.mjs';\n\nexport {\n mapGenkitMessageToUI,\n messagesFromSnapshot,\n restartInterrupt,\n} from './mapping.mjs';\nexport type { ResolvedToolResult, RestartInterruptOutput } from './mapping.mjs';\n\nexport type { ChatTransport, UIMessage, UIMessageChunk };\n\n// A bare RFC-4122 UUID, used to validate `chatId` (which doubles as the Genkit\n// `sessionId`). The agent server requires session ids to be bare UUIDs, so we\n// validate client-side too for a clearer, earlier error.\nconst UUID_PATTERN =\n /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\n/**\n * Derives a human-readable error message from an unknown thrown value,\n * preserving as much context as possible.\n *\n * `streamFlow` throws `Error`s whose `.message` already embeds the HTTP status\n * and any Genkit error status/message/details, so those are returned verbatim.\n * Non-Error throwables are stringified (JSON when possible) instead of being\n * collapsed to a generic label, so detail isn't silently lost.\n */\nfunction errorTextFromUnknown(err: unknown): string {\n if (err instanceof Error) {\n return err.message || 'Agent execution failed';\n }\n if (typeof err === 'string') {\n return err || 'Agent execution failed';\n }\n if (err === undefined || err === null) {\n return 'Agent execution failed';\n }\n try {\n return JSON.stringify(err);\n } catch {\n return String(err);\n }\n}\n\n/**\n * Builds a `ReadableStream` that emits a single error inside the standard\n * `start`/`error`/`finish` envelope, matching the shape of a normal stream.\n */\nfunction errorStream(errorText: string): ReadableStream<UIMessageChunk> {\n return new ReadableStream<UIMessageChunk>({\n start(controller) {\n controller.enqueue({\n type: 'start',\n messageId: `msg-${crypto.randomUUID()}`,\n });\n controller.enqueue({ type: 'error', errorText });\n controller.enqueue({ type: 'finish' });\n controller.close();\n },\n });\n}\n\n// ---------------------------------------------------------------------------\n// GenkitChatTransport\n// ---------------------------------------------------------------------------\n\n/**\n * Configuration for {@link GenkitChatTransport}.\n */\nexport interface GenkitChatTransportConfig {\n /** URL of the Genkit agent endpoint (e.g. `/api/weatherAgent`). */\n url: string;\n\n /**\n * Additional HTTP headers to include in every request.\n * Can be a static object or a function that returns headers (useful for\n * dynamic auth tokens).\n */\n headers?: Record<string, string> | (() => Record<string, string>);\n}\n\n/**\n * A Vercel AI SDK `ChatTransport` implementation that communicates with a\n * Genkit agent using Genkit's native streaming protocol via `streamFlow`.\n *\n * Conversation state is **fully server-managed**: the transport sends the\n * `useChat` `id` (the `chatId`) to the agent as its `sessionId`, and the agent\n * persists per-session state in its configured `SessionStore`. Each turn\n * resumes the session's latest snapshot automatically — there is no\n * client-side snapshot bookkeeping to manage or persist.\n *\n * Because the agent server requires session ids to be bare UUIDs, the\n * `chatId` (the `id` you pass to `useChat`) must be a UUID. Generate one with\n * `crypto.randomUUID()`.\n *\n * Interrupt resumes are derived directly from the `messages` the SDK already\n * holds: when the latest turn's assistant message carries resolved tool\n * outputs (e.g. via `addToolResult`), the transport sends them back to the\n * agent as a `resume` payload.\n *\n * @example\n * ```tsx\n * import { useMemo } from 'react';\n * import { useChat } from '@ai-sdk/react';\n * import { GenkitChatTransport } from '@genkit-ai/vercel-ai/client';\n *\n * const chatId = useMemo(() => crypto.randomUUID(), []);\n * const { messages, sendMessage } = useChat({\n * id: chatId,\n * transport: new GenkitChatTransport({\n * url: '/api/weatherAgent',\n * }),\n * });\n * ```\n */\nexport class GenkitChatTransport implements ChatTransport<UIMessage> {\n private readonly url: string;\n private readonly headersConfig?:\n | Record<string, string>\n | (() => Record<string, string>);\n\n constructor(config: GenkitChatTransportConfig) {\n this.url = config.url;\n this.headersConfig = config.headers;\n }\n\n private resolveHeaders(\n extra?: Record<string, string> | Headers\n ): Record<string, string> | undefined {\n const base =\n typeof this.headersConfig === 'function'\n ? this.headersConfig()\n : this.headersConfig;\n // Normalize Headers objects to plain records.\n let extraRecord: Record<string, string> | undefined;\n if (extra instanceof Headers) {\n extraRecord = {};\n extra.forEach((value, key) => {\n extraRecord![key] = value;\n });\n } else {\n extraRecord = extra;\n }\n if (!base && !extraRecord) return undefined;\n return { ...base, ...extraRecord };\n }\n\n // The `trigger` parameter is either 'submit-message' (a new user turn) or\n // 'regenerate-message' (re-run the last assistant response). Because state\n // is server-managed by `sessionId`, regeneration is treated as a fresh turn\n // from the current session state (there is no client-side snapshot pointer\n // to rewind to).\n async sendMessages({\n trigger,\n chatId,\n messages,\n abortSignal,\n headers,\n }: {\n trigger: 'submit-message' | 'regenerate-message';\n chatId: string;\n messageId: string | undefined;\n messages: UIMessage[];\n abortSignal: AbortSignal | undefined;\n headers?: Record<string, string> | Headers;\n body?: object;\n metadata?: unknown;\n }): Promise<ReadableStream<UIMessageChunk>> {\n // `chatId` doubles as the Genkit session id and must be a bare UUID.\n if (!UUID_PATTERN.test(chatId)) {\n return errorStream(\n `Invalid chatId '${chatId}': the useChat 'id' is used as the Genkit ` +\n `sessionId and must be a UUID. Generate one with crypto.randomUUID().`\n );\n }\n\n const isRegenerate = trigger === 'regenerate-message';\n\n // Detect an interrupt resume from the message history alone: the current\n // turn's assistant message(s) carry resolved tool outputs that have not\n // been followed by a new user message. Regeneration never resumes an\n // interrupt — it replays from the current session state.\n const resolvedTools = isRegenerate\n ? []\n : currentTurnResolvedTools(messages);\n const isResume = resolvedTools.length > 0;\n\n // The session id is always sent so the agent resumes (or seeds) the\n // server-managed session for this chat.\n const init: Record<string, unknown> = { sessionId: chatId };\n let agentInput: Record<string, unknown>;\n\n // Tool call ids that were resolved with a user-supplied *response* (not a\n // restart). Their outputs are already on the client, so the transport\n // suppresses re-emitting them. Restarted tools, by contrast, re-execute\n // server-side and produce a *fresh* output that the client must receive.\n const respondedInterruptIds = new Set<string>();\n\n if (isResume) {\n // Split resolutions into restarts (re-run the tool) and responds\n // (user-supplied output). A single resume turn can carry both.\n const respond: Array<Record<string, unknown>> = [];\n const restart: Array<Record<string, unknown>> = [];\n for (const tr of resolvedTools) {\n const asRestart = asRestartInterrupt(tr.result);\n if (asRestart) {\n // A restart re-runs the tool with the *original* input (the server\n // validates it matches the interrupted request exactly) and passes\n // any metadata through as the tool's `resumed` value.\n restart.push({\n toolRequest: {\n name: tr.toolName,\n ref: tr.toolCallId,\n input: tr.input,\n },\n metadata: { resumed: asRestart.metadata ?? true },\n });\n } else {\n respondedInterruptIds.add(tr.toolCallId);\n respond.push({\n toolResponse: {\n name: tr.toolName,\n ref: tr.toolCallId,\n output: tr.result,\n },\n });\n }\n }\n agentInput = {\n resume: {\n ...(respond.length > 0 && { respond }),\n ...(restart.length > 0 && { restart }),\n },\n };\n } else {\n const lastUserMsg = findLastNonEmptyUserMessage(messages);\n if (!lastUserMsg) {\n return errorStream('No user message found');\n }\n\n const genkitMsg = mapUIMessageToGenkit(lastUserMsg);\n agentInput = { message: genkitMsg };\n }\n\n // Call streamFlow — Genkit's native streaming protocol.\n const response = streamFlow<AgentOutput, AgentStreamChunk>({\n url: this.url,\n input: agentInput,\n init,\n headers: this.resolveHeaders(headers),\n abortSignal,\n });\n\n // Track whether the stream was aborted so we can emit a clean abort chunk.\n let aborted = false;\n\n return new ReadableStream<UIMessageChunk>({\n start: async (controller) => {\n // Listen for abort to break out of the streaming loop cleanly.\n const onAbort = () => {\n aborted = true;\n };\n abortSignal?.addEventListener('abort', onAbort, { once: true });\n\n // Streaming state machine.\n let started = false;\n let textBlockId: string | null = null;\n let reasoningBlockId: string | null = null;\n let inStep = false;\n // Locally tracked agent custom state, kept live by applying each\n // streamed RFC 6902 JSON Patch (`chunk.customPatch`). The first patch\n // of a turn is a whole-document replace that re-bases us onto the\n // server's current baseline.\n let customState: unknown;\n const emittedToolInputs = new Set<string>();\n\n // Deterministic fallback for tool calls that arrive without a `ref`.\n // The model may omit refs, in which case the request and its matching\n // response must still share the same `toolCallId`.\n //\n // We pair each ref-less response with the *oldest* unmatched ref-less\n // request for the same tool name (FIFO by arrival order). Allocating a\n // fresh id per request — rather than reusing one id per tool name —\n // means two ref-less calls to the *same* tool in one turn don't share\n // an id and cross-wire each other's request/response. (In practice\n // Genkit tool requests carry refs, so this is a safety net.)\n const pendingRefByToolName = new Map<string, string[]>();\n const correlateRequestId = (name: string): string => {\n const id = crypto.randomUUID();\n const queue = pendingRefByToolName.get(name) ?? [];\n queue.push(id);\n pendingRefByToolName.set(name, queue);\n return id;\n };\n const correlateResponseId = (name: string): string => {\n const queue = pendingRefByToolName.get(name);\n if (queue && queue.length > 0) {\n return queue.shift()!;\n }\n // No matching request seen (shouldn't happen) — mint a new id.\n return crypto.randomUUID();\n };\n\n const enqueue = (chunk: UIMessageChunk) => {\n try {\n controller.enqueue(chunk);\n } catch {\n // Controller already closed (e.g. consumer cancelled).\n }\n };\n\n const ensureStarted = () => {\n if (!started) {\n enqueue({\n type: 'start',\n messageId: `msg-${crypto.randomUUID()}`,\n });\n started = true;\n }\n };\n\n const ensureStep = () => {\n if (!inStep) {\n enqueue({ type: 'start-step' });\n inStep = true;\n }\n };\n\n const closeTextBlock = () => {\n if (textBlockId) {\n enqueue({ type: 'text-end', id: textBlockId });\n textBlockId = null;\n }\n };\n\n const closeReasoningBlock = () => {\n if (reasoningBlockId) {\n enqueue({ type: 'reasoning-end', id: reasoningBlockId });\n reasoningBlockId = null;\n }\n };\n\n const closeStep = () => {\n if (inStep) {\n enqueue({ type: 'finish-step' });\n inStep = false;\n }\n };\n\n try {\n for await (const chunk of response.stream) {\n // If aborted during iteration, break out cleanly.\n if (aborted) break;\n\n // Agent custom-state update. The runtime auto-emits a `customPatch`\n // (RFC 6902 JSON Patch) whenever the session's custom state\n // mutates. Apply it to our locally tracked copy and surface the\n // full, post-patch state as a transient `data-custom` UI chunk so\n // the React app can render live progress. Must run BEFORE the\n // `modelChunk` early-continue below, since custom-only chunks carry\n // no `modelChunk`.\n if (chunk?.customPatch) {\n customState = applyPatch(customState, chunk.customPatch);\n ensureStarted();\n enqueue({\n type: 'data-custom',\n data: customState,\n transient: true,\n } as UIMessageChunk);\n }\n\n const mc = chunk?.modelChunk;\n if (!mc?.content) continue;\n\n ensureStarted();\n\n for (const part of mc.content) {\n // Reasoning delta (model's \"thinking\"). Genkit reasoning parts\n // use the `reasoning` field; map them to AI SDK reasoning chunks\n // so they can be surfaced separately from the final answer.\n if (typeof part.reasoning === 'string' && part.reasoning !== '') {\n ensureStep();\n if (!reasoningBlockId) {\n reasoningBlockId = `reasoning-${crypto.randomUUID()}`;\n enqueue({ type: 'reasoning-start', id: reasoningBlockId });\n }\n enqueue({\n type: 'reasoning-delta',\n id: reasoningBlockId,\n delta: part.reasoning,\n });\n }\n\n // Text delta\n if (part.text !== undefined && part.text !== '') {\n // Reasoning precedes the answer; close any open reasoning\n // block before streaming visible text.\n closeReasoningBlock();\n ensureStep();\n if (!textBlockId) {\n textBlockId = `text-${crypto.randomUUID()}`;\n enqueue({ type: 'text-start', id: textBlockId });\n }\n enqueue({\n type: 'text-delta',\n id: textBlockId,\n delta: part.text,\n });\n }\n\n // Tool request (tool call from model)\n if (part.toolRequest) {\n const tr = part.toolRequest;\n const toolCallId = tr.ref ?? correlateRequestId(tr.name);\n if (!emittedToolInputs.has(toolCallId)) {\n emittedToolInputs.add(toolCallId);\n closeReasoningBlock();\n closeTextBlock();\n ensureStep();\n enqueue({\n type: 'tool-input-start',\n toolCallId,\n toolName: tr.name,\n });\n enqueue({\n type: 'tool-input-available',\n toolCallId,\n toolName: tr.name,\n input: tr.input,\n });\n }\n }\n\n // Tool response (tool executed by agent)\n if (part.toolResponse) {\n const tr = part.toolResponse;\n const toolCallId = tr.ref ?? correlateResponseId(tr.name);\n\n // Skip interrupt responses the client already supplied — it\n // has the result and re-emitting would confuse the UI SDK.\n // Restarted tools are NOT skipped: they re-execute server-side\n // and produce a fresh output the client must receive.\n if (!respondedInterruptIds.has(toolCallId)) {\n enqueue({\n type: 'tool-output-available',\n toolCallId,\n output: tr.output,\n });\n closeStep();\n }\n }\n }\n\n // Handle turn end signals.\n if (chunk?.turnEnd) {\n closeReasoningBlock();\n closeTextBlock();\n closeStep();\n }\n }\n\n // If aborted, emit an abort chunk and skip output processing.\n if (aborted) {\n ensureStarted();\n closeReasoningBlock();\n closeTextBlock();\n closeStep();\n enqueue({ type: 'abort', reason: 'Request was aborted' });\n enqueue({ type: 'finish' });\n try {\n controller.close();\n } catch {\n /* already closed */\n }\n return;\n }\n\n // Wait for final output (drains the stream / surfaces server errors).\n await response.output;\n } catch (err: unknown) {\n // If the error is an AbortError, treat it as an abort, not an error.\n if (err instanceof DOMException && err.name === 'AbortError') {\n ensureStarted();\n closeReasoningBlock();\n closeTextBlock();\n closeStep();\n enqueue({ type: 'abort', reason: 'Request was aborted' });\n } else {\n ensureStarted();\n // `streamFlow` throws plain `Error`s whose message already embeds\n // the HTTP status and any Genkit error status/message/details\n // (e.g. `\"Server returned: 500: ...\"` or `\"<status>: <message>\\n\n // <details>\"`), so `err.message` preserves that context. For\n // non-Error throwables, stringify rather than collapsing to a\n // generic label so detail isn't lost.\n enqueue({\n type: 'error',\n errorText: errorTextFromUnknown(err),\n });\n }\n } finally {\n closeReasoningBlock();\n closeTextBlock();\n closeStep();\n ensureStarted();\n enqueue({ type: 'finish' });\n abortSignal?.removeEventListener('abort', onAbort);\n try {\n controller.close();\n } catch {\n /* already closed */\n }\n }\n },\n\n cancel: () => {\n // The ReadableStream consumer cancelled (e.g. useChat unmounted).\n // The abort signal (if provided) will handle the fetch cancellation.\n aborted = true;\n },\n });\n }\n\n /**\n * Genkit transport does not support stream reconnection.\n *\n * Genkit has a durable streaming feature on the server side; support\n * for reconnection may be added in a future version.\n *\n * @returns Always returns `null`.\n */\n async reconnectToStream(): Promise<ReadableStream<UIMessageChunk> | null> {\n return null;\n }\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,3 @@
1
+ export { UIMessage } from 'ai';
2
+ export { ResolvedToolResult, RestartInterruptOutput, currentTurnResolvedTools, extractResolvedToolResults, findLastNonEmptyUserMessage, findLastUserMessage, mapGenkitMessageToUI, mapUIMessageToGenkit, mapUIPartToGenkit, messagesFromSnapshot, restartInterrupt } from './mapping.mjs';
3
+ import 'genkit';
package/lib/index.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ export { UIMessage } from 'ai';
2
+ export { ResolvedToolResult, RestartInterruptOutput, currentTurnResolvedTools, extractResolvedToolResults, findLastNonEmptyUserMessage, findLastUserMessage, mapGenkitMessageToUI, mapUIMessageToGenkit, mapUIPartToGenkit, messagesFromSnapshot, restartInterrupt } from './mapping.js';
3
+ import 'genkit';
package/lib/index.js ADDED
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var index_exports = {};
20
+ __export(index_exports, {
21
+ currentTurnResolvedTools: () => import_mapping.currentTurnResolvedTools,
22
+ extractResolvedToolResults: () => import_mapping.extractResolvedToolResults,
23
+ findLastNonEmptyUserMessage: () => import_mapping.findLastNonEmptyUserMessage,
24
+ findLastUserMessage: () => import_mapping.findLastUserMessage,
25
+ mapGenkitMessageToUI: () => import_mapping.mapGenkitMessageToUI,
26
+ mapUIMessageToGenkit: () => import_mapping.mapUIMessageToGenkit,
27
+ mapUIPartToGenkit: () => import_mapping.mapUIPartToGenkit,
28
+ messagesFromSnapshot: () => import_mapping.messagesFromSnapshot,
29
+ restartInterrupt: () => import_mapping.restartInterrupt
30
+ });
31
+ module.exports = __toCommonJS(index_exports);
32
+ var import_mapping = require("./mapping.js");
33
+ // Annotate the CommonJS export names for ESM import in node:
34
+ 0 && (module.exports = {
35
+ currentTurnResolvedTools,
36
+ extractResolvedToolResults,
37
+ findLastNonEmptyUserMessage,
38
+ findLastUserMessage,
39
+ mapGenkitMessageToUI,
40
+ mapUIMessageToGenkit,
41
+ mapUIPartToGenkit,
42
+ messagesFromSnapshot,
43
+ restartInterrupt
44
+ });
45
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * Copyright 2026 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nexport type { UIMessage } from 'ai';\n\nexport {\n currentTurnResolvedTools,\n extractResolvedToolResults,\n findLastNonEmptyUserMessage,\n findLastUserMessage,\n mapGenkitMessageToUI,\n mapUIMessageToGenkit,\n mapUIPartToGenkit,\n messagesFromSnapshot,\n restartInterrupt,\n type ResolvedToolResult,\n type RestartInterruptOutput,\n} from './mapping.js';\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAkBA,qBAYO;","names":[]}
package/lib/index.mjs ADDED
@@ -0,0 +1,23 @@
1
+ import {
2
+ currentTurnResolvedTools,
3
+ extractResolvedToolResults,
4
+ findLastNonEmptyUserMessage,
5
+ findLastUserMessage,
6
+ mapGenkitMessageToUI,
7
+ mapUIMessageToGenkit,
8
+ mapUIPartToGenkit,
9
+ messagesFromSnapshot,
10
+ restartInterrupt
11
+ } from "./mapping.mjs";
12
+ export {
13
+ currentTurnResolvedTools,
14
+ extractResolvedToolResults,
15
+ findLastNonEmptyUserMessage,
16
+ findLastUserMessage,
17
+ mapGenkitMessageToUI,
18
+ mapUIMessageToGenkit,
19
+ mapUIPartToGenkit,
20
+ messagesFromSnapshot,
21
+ restartInterrupt
22
+ };
23
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * Copyright 2026 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nexport type { UIMessage } from 'ai';\n\nexport {\n currentTurnResolvedTools,\n extractResolvedToolResults,\n findLastNonEmptyUserMessage,\n findLastUserMessage,\n mapGenkitMessageToUI,\n mapUIMessageToGenkit,\n mapUIPartToGenkit,\n messagesFromSnapshot,\n restartInterrupt,\n type ResolvedToolResult,\n type RestartInterruptOutput,\n} from './mapping.mjs';\n"],"mappings":"AAkBA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAGK;","names":[]}
@@ -0,0 +1,162 @@
1
+ import { UIMessage } from 'ai';
2
+ import { MessageData, Part } from 'genkit';
3
+
4
+ /**
5
+ * Copyright 2026 Google LLC
6
+ *
7
+ * Licensed under the Apache License, Version 2.0 (the "License");
8
+ * you may not use this file except in compliance with the License.
9
+ * You may obtain a copy of the License at
10
+ *
11
+ * http://www.apache.org/licenses/LICENSE-2.0
12
+ *
13
+ * Unless required by applicable law or agreed to in writing, software
14
+ * distributed under the License is distributed on an "AS IS" BASIS,
15
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ * See the License for the specific language governing permissions and
17
+ * limitations under the License.
18
+ */
19
+
20
+ /**
21
+ * Maps a single Vercel AI SDK v6 UIMessagePart to zero or more Genkit Parts.
22
+ *
23
+ * Handles `text`, `file`, and tool parts (both `ToolUIPart` with
24
+ * `type: 'tool-<name>'` and `DynamicToolUIPart` with `type: 'dynamic-tool'`).
25
+ *
26
+ * A tool part with state `output-available` produces both a `toolRequest`
27
+ * and a `toolResponse` part (matching Genkit's conversation model).
28
+ */
29
+ declare function mapUIPartToGenkit(part: UIMessage['parts'][number]): Part[];
30
+ /**
31
+ * Maps a Vercel AI SDK UIMessage to a Genkit MessageData.
32
+ */
33
+ declare function mapUIMessageToGenkit(msg: UIMessage): MessageData;
34
+ /**
35
+ * Maps a single Genkit {@link MessageData} to an AI SDK {@link UIMessage}.
36
+ *
37
+ * Pass `responsesByRef` (built across the whole conversation) so each
38
+ * `toolRequest` can be paired with the `toolResponse` that resolved it, even
39
+ * though they live in different messages. When omitted, all tool parts are
40
+ * emitted in the `input-available` state.
41
+ */
42
+ declare function mapGenkitMessageToUI(msg: MessageData, responsesByRef?: Map<string, unknown>, id?: string): UIMessage;
43
+ /**
44
+ * Converts the `messages` of a Genkit `SessionSnapshot` (an array of
45
+ * {@link MessageData}) into AI SDK {@link UIMessage}s suitable for seeding
46
+ * `useChat` (via its `messages` option or `setMessages`) to rehydrate a
47
+ * previous conversation after a reload.
48
+ *
49
+ * Tool requests and their responses (which Genkit stores in separate
50
+ * `model`/`tool` messages) are merged into single AI SDK tool parts, so the
51
+ * UI renders each tool call as one element with both its input and output.
52
+ * `tool`-role messages are therefore omitted from the result. Unresolved tool
53
+ * requests (e.g. a pending interrupt) are emitted in the `input-available`
54
+ * state.
55
+ *
56
+ * Conversation state is server-managed by `sessionId` (the `useChat` `id`), so
57
+ * to resume the *next* turn you only need to reuse the same chat `id` — there
58
+ * is no snapshot to restore on the client.
59
+ *
60
+ * @example
61
+ * ```ts
62
+ * const snapshot = await runFlow({ url: '/api/chat/weather/state', input: sessionId });
63
+ * const messages = messagesFromSnapshot(snapshot.state.messages);
64
+ * // pass `messages` to useChat({ id: sessionId, messages })
65
+ * ```
66
+ */
67
+ declare function messagesFromSnapshot(messages: MessageData[]): UIMessage[];
68
+ /**
69
+ * Marker placed on a tool output to signal that the user chose to *restart*
70
+ * (re-run) an interrupted tool rather than supply a final response.
71
+ *
72
+ * `Symbol.for` is intentionally avoided because tool outputs are serialized to
73
+ * JSON before reaching the transport; a string marker survives that round-trip.
74
+ */
75
+ declare const RESTART_MARKER = "__genkitVercelAiRestart__";
76
+ /**
77
+ * The serialized shape produced by {@link restartInterrupt}.
78
+ */
79
+ interface RestartInterruptOutput {
80
+ [RESTART_MARKER]: true;
81
+ /**
82
+ * Optional metadata surfaced to the tool as its `resumed` argument when it
83
+ * re-runs server-side. Defaults to `true` when omitted.
84
+ */
85
+ metadata?: unknown;
86
+ }
87
+ /**
88
+ * Builds a tool output that instructs the Genkit agent to **restart** (re-run)
89
+ * an interrupted tool instead of accepting a user-supplied response.
90
+ *
91
+ * Pass the result to the AI SDK's `addToolResult` as the tool `output`:
92
+ *
93
+ * ```tsx
94
+ * addToolResult({
95
+ * tool: 'userApproval',
96
+ * toolCallId,
97
+ * output: restartInterrupt({ note: 'user revised the request' }),
98
+ * });
99
+ * ```
100
+ *
101
+ * The transport detects this marker and emits a `resume.restart` entry (with
102
+ * the original tool input, which the server requires to match exactly) instead
103
+ * of a `resume.respond` entry. Any `metadata` is passed through to the tool as
104
+ * its `resumed` value.
105
+ */
106
+ declare function restartInterrupt(metadata?: unknown): RestartInterruptOutput;
107
+ /**
108
+ * Returns the restart payload if `output` was produced by
109
+ * {@link restartInterrupt}, otherwise `undefined`.
110
+ */
111
+ declare function asRestartInterrupt(output: unknown): RestartInterruptOutput | undefined;
112
+ /**
113
+ * A resolved tool invocation extracted from the frontend message history.
114
+ */
115
+ interface ResolvedToolResult {
116
+ toolCallId: string;
117
+ toolName: string;
118
+ /** The original tool input (needed to build a `resume.restart`). */
119
+ input: unknown;
120
+ /** The user-supplied output (or a {@link restartInterrupt} marker). */
121
+ result: unknown;
122
+ }
123
+ /**
124
+ * Extracts resolved tool invocations from a UIMessage array.
125
+ *
126
+ * Supports the v6 per-tool format (`type === 'tool-<toolName>'`) as well as
127
+ * dynamic tools (`type === 'dynamic-tool'`, with an explicit `toolName`),
128
+ * matching `state === 'output-available'`.
129
+ *
130
+ * Scans **all** messages (most-recent first) and returns every resolved tool
131
+ * result, de-duplicated by `toolCallId` (keeping the most recent).
132
+ *
133
+ * Prefer {@link currentTurnResolvedTools} for interrupt-resume detection — it
134
+ * scopes the scan to the most recent turn so prior turns' (already-completed)
135
+ * tool calls are not re-sent as a resume payload.
136
+ */
137
+ declare function extractResolvedToolResults(messages: UIMessage[]): ResolvedToolResult[];
138
+ /**
139
+ * Returns the resolved tool invocations that constitute an interrupt resume
140
+ * for the *current* turn, or an empty array if this is a fresh user turn.
141
+ *
142
+ * An interrupt resume is identified by the last assistant message carrying
143
+ * resolved tool outputs (e.g. supplied via `addToolResult`) with no *non-empty*
144
+ * user message after it. A real user message following the assistant turn means
145
+ * the user started a new turn, so no resume is performed.
146
+ *
147
+ * This deliberately scopes detection to the most recent turn: tool calls from
148
+ * earlier turns are already part of the server-side session state and must not
149
+ * be replayed as a resume payload.
150
+ */
151
+ declare function currentTurnResolvedTools(messages: UIMessage[]): ResolvedToolResult[];
152
+ /**
153
+ * Finds the last user message in a UIMessage array.
154
+ */
155
+ declare function findLastUserMessage(messages: UIMessage[]): UIMessage | undefined;
156
+ /**
157
+ * Finds the last *non-empty* user message in a UIMessage array, skipping
158
+ * phantom empty messages produced by `sendMessage({ text: '' })`.
159
+ */
160
+ declare function findLastNonEmptyUserMessage(messages: UIMessage[]): UIMessage | undefined;
161
+
162
+ export { type ResolvedToolResult, type RestartInterruptOutput, asRestartInterrupt, currentTurnResolvedTools, extractResolvedToolResults, findLastNonEmptyUserMessage, findLastUserMessage, mapGenkitMessageToUI, mapUIMessageToGenkit, mapUIPartToGenkit, messagesFromSnapshot, restartInterrupt };