content-agent 1.2.1 → 1.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.
@@ -34,7 +34,7 @@ class SanityAgentStreamError extends Error {
34
34
  super(message), this.name = "SanityAgentStreamError", this.code = code;
35
35
  }
36
36
  }
37
- const GENERIC_ERROR_MESSAGE = "Sorry, something went wrong. Please try again.";
37
+ const GENERIC_ERROR_MESSAGE = "Sorry, something went wrong. Please try again.", STREAM_INCOMPLETE_CODE = "STREAM_INCOMPLETE", STREAM_INCOMPLETE_MESSAGE = "Something went wrong and I lost the response on the way back. Please try again.";
38
38
  function mapErrorCode(code) {
39
39
  switch (code) {
40
40
  case ERROR_CODES.NO_COMPATIBLE_APPLICATIONS:
@@ -49,6 +49,8 @@ function mapErrorCode(code) {
49
49
  return { type: "usage_limit_exceeded" };
50
50
  case ERROR_CODES.STREAMING_ERROR:
51
51
  return { type: "streaming_error" };
52
+ case STREAM_INCOMPLETE_CODE:
53
+ return { type: "stream_incomplete" };
52
54
  default:
53
55
  return null;
54
56
  }
@@ -129,7 +131,7 @@ function parseStreamLine(line, textContentId) {
129
131
  const trimmed = line.trim();
130
132
  if (!trimmed.startsWith("data: ")) return null;
131
133
  const payload = trimmed.slice(6);
132
- if (payload === "[DONE]") return null;
134
+ if (payload === "[DONE]") return { kind: "done" };
133
135
  let part;
134
136
  try {
135
137
  part = JSON.parse(payload);
@@ -215,7 +217,7 @@ function isRetryableStreamError(error) {
215
217
  const status = error?.status;
216
218
  if (status && status >= 500 && status < 600 || status === 408 || status === 425 || status === 429) return !0;
217
219
  const message = error?.message ?? "";
218
- if (/network|fetch failed|socket|ECONN|ETIMEDOUT|EPIPE|UND_ERR/i.test(message)) return !0;
220
+ if (/network|fetch failed|socket|ECONN|ETIMEDOUT|EPIPE|UND_ERR/i.test(message) || message === "terminated") return !0;
219
221
  const cause = error?.cause;
220
222
  return cause && cause !== error ? isRetryableStreamError(cause) : !1;
221
223
  }
@@ -226,7 +228,7 @@ function createSSEStream(responseBody, warnings, optionsOrOnData) {
226
228
  const opts = typeof optionsOrOnData == "function" ? { onData: optionsOrOnData } : optionsOrOnData ?? {}, { onData, reconnect, maxRetries = 3 } = opts;
227
229
  let finishReason = { unified: "other", raw: void 0 };
228
230
  const fallbackTextId = crypto.randomUUID();
229
- let hasToolCalls = !1, lastSeq = 0, attempt = 0;
231
+ let hasToolCalls = !1, lastSeq = 0, attempt = 0, sawTerminal = !1;
230
232
  return new ReadableStream({
231
233
  async start(controller) {
232
234
  const decoder = new TextDecoder();
@@ -239,7 +241,7 @@ function createSSEStream(responseBody, warnings, optionsOrOnData) {
239
241
  return;
240
242
  }
241
243
  const parsed = parseStreamLine(line, fallbackTextId);
242
- parsed?.kind === "part" ? (parsed.part.type === "tool-call" && (hasToolCalls = !0), controller.enqueue(parsed.part)) : parsed?.kind === "finish" ? finishReason = parsed.finishReason : parsed?.kind === "data" && onData?.(parsed.type, parsed.data);
244
+ parsed?.kind === "part" ? (parsed.part.type === "tool-call" && (hasToolCalls = !0), controller.enqueue(parsed.part)) : parsed?.kind === "finish" ? (sawTerminal = !0, finishReason = parsed.finishReason) : parsed?.kind === "data" ? onData?.(parsed.type, parsed.data) : parsed?.kind === "done" && (sawTerminal = !0);
243
245
  };
244
246
  controller.enqueue({ type: "stream-start", warnings });
245
247
  try {
@@ -265,6 +267,8 @@ function createSSEStream(responseBody, warnings, optionsOrOnData) {
265
267
  attempt++, await new Promise((r) => setTimeout(r, backoffMs(attempt))), buffer = "";
266
268
  const next = await reconnect(lastSeq);
267
269
  if (next === null) {
270
+ if (!sawTerminal)
271
+ throw new SanityAgentStreamError(STREAM_INCOMPLETE_MESSAGE, STREAM_INCOMPLETE_CODE);
268
272
  currentBody = null, bodyDone = !0;
269
273
  continue;
270
274
  }
@@ -291,6 +295,7 @@ function createSSEStream(responseBody, warnings, optionsOrOnData) {
291
295
  }
292
296
  export {
293
297
  GENERIC_ERROR_MESSAGE,
298
+ STREAM_INCOMPLETE_CODE,
294
299
  SanityAgentError,
295
300
  SanityAgentStreamError,
296
301
  buildApiUrl,
@@ -1 +1 @@
1
- {"version":3,"file":"utils.js","sources":["../../src/utils.ts"],"sourcesContent":["import type {\n LanguageModelV3FinishReason,\n LanguageModelV3StreamPart,\n LanguageModelV3Usage,\n SharedV3Warning,\n} from '@ai-sdk/provider'\nimport { ERROR_CODES, type HeadlessErrorResponse } from '@sanity/agent-types'\nimport { isString, pickBy } from 'es-toolkit'\n\n/**\n * Builds headers for API requests, filtering out undefined values.\n *\n * @category Utilities\n */\nexport function buildHeaders(\n configHeaders: Record<string, string | undefined>,\n additionalHeaders?: Record<string, string>,\n): Record<string, string> {\n return pickBy(\n {\n 'Content-Type': 'application/json',\n ...additionalHeaders,\n ...configHeaders,\n },\n isString,\n )\n}\n\n/**\n * Constructs the full API URL for an endpoint.\n *\n * @category Utilities\n */\nexport function buildApiUrl(baseURL: string, apiVersion: string, endpoint: string): string {\n return `${baseURL}/${apiVersion}/agent${endpoint}`\n}\n\n/**\n * Custom error class for Sanity Agent API errors.\n * The {@link body} is parsed from the JSON response and follows the\n * {@link HeadlessErrorResponse} shape defined in `@sanity/agent-types`.\n *\n * @category Utilities\n */\nexport class SanityAgentError extends Error {\n readonly status: number\n readonly isRetryable: boolean\n readonly body: Partial<HeadlessErrorResponse>\n\n constructor(status: number, message: string) {\n super(`Sanity Agent API error: ${status} ${message}`)\n this.name = 'SanityAgentError'\n this.status = status\n this.isRetryable = status >= 500\n this.body = SanityAgentError.parseBody(message)\n }\n\n private static parseBody(message: string): Partial<HeadlessErrorResponse> {\n try {\n return JSON.parse(message)\n } catch {\n return {}\n }\n }\n}\n\n/**\n * Error class for errors received via SSE stream events.\n * Unlike {@link SanityAgentError} (which is thrown for HTTP-level errors before\n * streaming starts), this wraps errors received mid-stream as SSE `error` events.\n *\n * The {@link code} carries the backend's error classification (e.g. `UPSTREAM_OVERLOADED`)\n * so clients can take targeted action without parsing the message string.\n *\n * @category Utilities\n */\nexport class SanityAgentStreamError extends Error {\n readonly code: string | undefined\n\n constructor(message: string, code?: string) {\n super(message)\n this.name = 'SanityAgentStreamError'\n this.code = code\n }\n}\n\nexport const GENERIC_ERROR_MESSAGE = 'Sorry, something went wrong. Please try again.'\n\nexport type AgentErrorType =\n | { type: 'no_applications' }\n | { type: 'application_not_found' }\n | { type: 'upstream_overloaded' }\n | { type: 'upstream_error' }\n | { type: 'usage_limit_exceeded' }\n | { type: 'streaming_error' }\n | { type: 'unknown' }\n\nfunction mapErrorCode(code: string | undefined): AgentErrorType | null {\n switch (code) {\n case ERROR_CODES.NO_COMPATIBLE_APPLICATIONS:\n return { type: 'no_applications' }\n case ERROR_CODES.APPLICATION_NOT_FOUND:\n return { type: 'application_not_found' }\n case ERROR_CODES.UPSTREAM_OVERLOADED:\n return { type: 'upstream_overloaded' }\n case ERROR_CODES.UPSTREAM_ERROR:\n return { type: 'upstream_error' }\n case ERROR_CODES.AI_USAGE_LIMIT_EXCEEDED:\n return { type: 'usage_limit_exceeded' }\n case ERROR_CODES.STREAMING_ERROR:\n return { type: 'streaming_error' }\n default:\n return null\n }\n}\n\n/**\n * Classifies a caught error from the Content Agent API into a known error type.\n * Useful for clients that need to take different actions per error (e.g. show\n * resource-selection buttons). For the user-facing message, use {@link getAgentErrorMessage}.\n *\n * @category Utilities\n */\nexport function classifyAgentError(error: unknown): AgentErrorType {\n // Handle SSE stream errors (from parseStreamLine)\n if (error instanceof SanityAgentStreamError && error.code) {\n return mapErrorCode(error.code) ?? { type: 'unknown' }\n }\n\n // Handle HTTP-level errors (from fetchApi)\n if (error instanceof SanityAgentError) {\n if (error.status !== 400) {\n return { type: 'unknown' }\n }\n return mapErrorCode(error.body.code) ?? { type: 'unknown' }\n }\n\n return { type: 'unknown' }\n}\n\nexport function isAppRelatedError(classified: AgentErrorType): boolean {\n return classified.type === 'no_applications' || classified.type === 'application_not_found'\n}\n\nexport type AgentErrorMessageContext = {\n orgName?: string | null\n}\n\n/**\n * Resolves a user-facing error message for a classified agent error.\n * For errors that benefit from additional context (e.g. org name), the optional\n * {@link resolveContext} callback is invoked lazily — only when the error type\n * needs it. Non-contextual errors return the backend message as-is.\n *\n * @category Utilities\n */\nexport async function resolveAgentErrorMessage(\n classified: AgentErrorType,\n error: unknown,\n resolveContext?: () => Promise<AgentErrorMessageContext>,\n): Promise<string> {\n const fallback = getAgentErrorMessage(error)\n\n if (!resolveContext || !isAppRelatedError(classified)) return fallback\n\n const ctx = await resolveContext()\n if (!ctx.orgName) return fallback\n\n switch (classified.type) {\n case 'no_applications':\n return `Studio action required: To use the Content Agent in \"${ctx.orgName}\", you must have at least one Sanity Studio running version 5.1.0 or above. After upgrading, open your studio to connect it to the agent.`\n case 'application_not_found':\n return `The requested studio is not available in \"${ctx.orgName}\". You may not have access, or it may belong to a different organization.`\n default:\n return fallback\n }\n}\n\n/**\n * Extracts a user-facing error message from an agent API error.\n * Returns the message sent by the API when available, otherwise a generic fallback.\n *\n * @category Utilities\n */\nexport function getAgentErrorMessage(error: unknown): string {\n if (error instanceof SanityAgentStreamError) {\n return error.message\n }\n if (error instanceof SanityAgentError && error.body.message) {\n return error.body.message\n }\n return GENERIC_ERROR_MESSAGE\n}\n\nexport interface FetchOptions {\n url: string\n method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD'\n body?: unknown\n headers: Record<string, string>\n credentials?: RequestCredentials\n signal?: AbortSignal\n fetch?: typeof globalThis.fetch\n}\n\n/**\n * Makes an API request and returns the response.\n * Throws SanityAgentError if the response is not ok.\n *\n * @category Utilities\n */\nexport async function fetchApi(options: FetchOptions): Promise<Response> {\n const fetchFn = options.fetch ?? globalThis.fetch\n const method = options.method ?? 'POST'\n const hasBody = method !== 'GET' && method !== 'HEAD'\n\n const response = await fetchFn(options.url, {\n method,\n headers: options.headers,\n ...(hasBody && { body: JSON.stringify(options.body) }),\n signal: options.signal,\n ...(options.credentials && { credentials: options.credentials }),\n })\n\n if (!response.ok) {\n const errorText = await response.text()\n throw new SanityAgentError(response.status, errorText)\n }\n\n return response\n}\n\n/**\n * Extracts text content from message parts (user or assistant).\n *\n * @category Utilities\n */\nexport function extractTextFromParts(\n content: string | Array<{ type: string; text?: string }>,\n): string {\n if (typeof content === 'string') {\n return content\n }\n\n return content\n .filter(\n (part): part is { type: 'text'; text: string } =>\n part.type === 'text' && typeof part.text === 'string',\n )\n .map((part) => part.text)\n .join('\\n')\n}\n\n/**\n * Creates an empty V3 usage object. Our API does not return token counts.\n *\n * @category Utilities\n */\nexport function createEmptyUsage(): LanguageModelV3Usage {\n return {\n inputTokens: {\n total: 0,\n noCache: undefined,\n cacheRead: undefined,\n cacheWrite: undefined,\n },\n outputTokens: {\n total: 0,\n text: undefined,\n reasoning: undefined,\n },\n }\n}\n\n/**\n * Maps API finish reasons to LanguageModelV3FinishReason.\n *\n * @category Utilities\n */\nexport function mapFinishReason(reason: string | undefined): LanguageModelV3FinishReason {\n const unified = (() => {\n switch (reason) {\n case 'stop':\n case 'length':\n case 'content-filter':\n case 'tool-calls':\n case 'error':\n return reason\n default:\n return 'other' as const\n }\n })()\n\n return { unified, raw: reason }\n}\n\nexport type ParsedStreamLine =\n | { kind: 'part'; part: LanguageModelV3StreamPart }\n | { kind: 'finish'; finishReason: LanguageModelV3FinishReason }\n | { kind: 'data'; type: string; data: unknown }\n\n/**\n * Parses a single SSE line from the AI SDK v6 UI Message Stream protocol.\n * Returns null if the line cannot be parsed or should be ignored.\n *\n * Format: `data: <json>\\n`\n *\n * Supported types:\n * text-start – Text block start (forwarded as-is)\n * text-delta – Text chunk\n * text-end – Text block end (forwarded as-is)\n * tool-input-available – Tool call (client tools only, without providerExecuted)\n * tool-output-available – Tool result (informational, skipped)\n * finish – Finish event with reason\n * error – Error event\n * start – Stream start (skipped)\n * [DONE] – Stream termination (skipped)\n *\n * @see https://ai-sdk.dev/docs/ai-sdk-ui/stream-protocol\n *\n * @param line A single line from the SSE byte stream.\n * @param textContentId Fallback ID for text parts that lack an explicit id.\n *\n * @category Utilities\n */\nexport function parseStreamLine(line: string, textContentId: string): ParsedStreamLine | null {\n const trimmed = line.trim()\n if (!trimmed.startsWith('data: ')) return null\n\n const payload = trimmed.slice(6)\n if (payload === '[DONE]') return null\n\n let part: Record<string, unknown>\n try {\n part = JSON.parse(payload) as Record<string, unknown>\n } catch {\n return null\n }\n\n const type = part.type as string | undefined\n if (!type) return null\n\n switch (type) {\n case 'text-start': {\n const id = (part.id as string) || textContentId\n return { kind: 'part', part: { type: 'text-start', id } }\n }\n\n case 'text-delta': {\n const id = (part.id as string) || textContentId\n const delta = part.delta as string\n if (typeof delta !== 'string') return null\n return { kind: 'part', part: { type: 'text-delta', id, delta } }\n }\n\n case 'text-end': {\n const id = (part.id as string) || textContentId\n return { kind: 'part', part: { type: 'text-end', id } }\n }\n\n case 'tool-input-available': {\n // Forward both client- and server-executed tools to the consumer's\n // streamText loop. Stop conditions like `hasToolCall('final_answer')`\n // need to see the terminal `providerExecuted` tool call to fire; the\n // consumer's local-execute path skips tools without an `execute`\n // function (or that are not in the consumer's tool map), so a server\n // tool surfaced here cannot be double-executed. See SAGE-182.\n //\n // Server-executed tools (`providerExecuted: true`) are also marked\n // `dynamic: true`. The AI SDK's `parseToolCall` only accepts a\n // tool that is missing from the consumer's tool map when both\n // `providerExecuted` and `dynamic` are set — otherwise it throws\n // `NoSuchToolError` and emits a phantom `tool-error` chunk for a\n // tool that already executed server-side. The agent server's tools\n // are not exposed to the consumer at all (server-execute is the\n // whole point), so `dynamic` is the right flag to set here.\n const input = part.input\n const isServerExecuted = part.providerExecuted === true\n return {\n kind: 'part',\n part: {\n type: 'tool-call',\n toolCallId: part.toolCallId as string,\n toolName: part.toolName as string,\n input: typeof input === 'string' ? input : JSON.stringify(input),\n ...(isServerExecuted && { providerExecuted: true, dynamic: true }),\n },\n }\n }\n\n case 'tool-output-available': {\n // Forward the result of a provider-executed tool (e.g. `final_answer`)\n // so the AI SDK loop can pair it with the matching `tool-call` and\n // include it in `currentModelResponse.content`. Without the result\n // chunk the loop will request another step to satisfy the dangling\n // tool call, defeating the natural termination Phase 1 enables.\n //\n // Only forward when `providerExecuted` is set. Client-tool results\n // are produced locally on the consumer side via `tool.execute`, not\n // streamed from the server, so any `tool-output-available` we see\n // for a non-providerExecuted tool would be unexpected — drop it.\n if (part.providerExecuted !== true) return null\n // The AI SDK's `tool-result` part requires a non-null JSON-serializable\n // result. Fall back to an empty object when the server omits one (e.g.\n // tools that signal completion via side effects).\n const result = (part.output ?? {}) as NonNullable<typeof part.output>\n return {\n kind: 'part',\n part: {\n type: 'tool-result',\n toolCallId: part.toolCallId as string,\n toolName: part.toolName as string,\n result,\n dynamic: true,\n },\n }\n }\n\n case 'error': {\n const errorText = (part.errorText as string) ?? 'Unknown error'\n const errorCode = part.errorCode as string | undefined\n return {\n kind: 'part',\n part: {\n type: 'error',\n error: new SanityAgentStreamError(errorText, errorCode),\n },\n }\n }\n\n case 'abort': {\n return { kind: 'data', type: 'abort', data: { reason: part.reason } }\n }\n\n case 'finish': {\n return {\n kind: 'finish',\n finishReason: mapFinishReason(part.finishReason as string | undefined),\n }\n }\n\n default: {\n if (type.startsWith('data-')) {\n return { kind: 'data', type: type.slice(5), data: part.data }\n }\n return null\n }\n }\n}\n\n/**\n * Extracts accumulated text from a streaming response body.\n * Used for simple streaming cases where only the final text is needed.\n *\n * @category Utilities\n */\nexport async function extractTextFromStream(\n responseBody: ReadableStream<Uint8Array>,\n): Promise<string> {\n const reader = responseBody.getReader()\n const decoder = new TextDecoder()\n const textContentId = 'extract'\n let text = ''\n\n while (true) {\n const { done, value } = await reader.read()\n if (done) break\n\n const chunk = decoder.decode(value, { stream: true })\n for (const line of chunk.split('\\n')) {\n const parsed = parseStreamLine(line, textContentId)\n if (parsed?.kind === 'part' && parsed.part.type === 'text-delta') {\n text += parsed.part.delta\n }\n }\n }\n\n return text\n}\n\n/** @category Utilities */\nexport type CreateSSEStreamOptions = {\n /**\n * Hook to receive `data-*` chunks the AI SDK doesn't surface natively.\n */\n onData?: (type: string, data: unknown) => void\n /**\n * Optional reconnect callback. When set, retryable read errors trigger a\n * call with the last seen `id:` sequence number, expecting a fresh\n * `Response` whose body continues the stream from that frame. Return\n * `null` to signal a clean end (HTTP 204 from the server).\n */\n reconnect?: (afterSeq: number) => Promise<Response | null>\n maxRetries?: number\n}\n\n/**\n * Retryable error predicate. Matches the same network/5xx/AbortError logic\n * as `@repo/shared/client/retryable`, inlined here so `agent-provider` stays\n * dependency-free.\n */\nfunction isRetryableStreamError(error: unknown): boolean {\n if (!error) return false\n if ((error as { name?: string })?.name === 'AbortError') return false\n\n const status = (error as { status?: number })?.status\n if (status && status >= 500 && status < 600) return true\n if (status === 408 || status === 425 || status === 429) return true\n\n const message = (error as { message?: string })?.message ?? ''\n if (/network|fetch failed|socket|ECONN|ETIMEDOUT|EPIPE|UND_ERR/i.test(message)) return true\n\n const cause = (error as { cause?: unknown })?.cause\n if (cause && cause !== error) return isRetryableStreamError(cause)\n\n return false\n}\n\nfunction backoffMs(attempt: number): number {\n return 500 * 2 ** (attempt - 1)\n}\n\n/**\n * Creates a ReadableStream that parses AI SDK v6 UI Message Stream SSE data\n * from a response body and emits LanguageModelV3 stream parts.\n *\n * Text lifecycle events (text-start / text-end) are forwarded from the server.\n * Tool calls are tracked to ensure the finish reason is set correctly.\n *\n * @category Utilities\n */\nexport function createSSEStream(\n responseBody: ReadableStream<Uint8Array>,\n warnings: SharedV3Warning[],\n optionsOrOnData?: CreateSSEStreamOptions | ((type: string, data: unknown) => void),\n): ReadableStream<LanguageModelV3StreamPart> {\n // Backwards compat: callers used to pass `onData` as the third positional arg.\n const opts: CreateSSEStreamOptions =\n typeof optionsOrOnData === 'function' ? { onData: optionsOrOnData } : (optionsOrOnData ?? {})\n const { onData, reconnect, maxRetries = 3 } = opts\n\n let finishReason: LanguageModelV3FinishReason = { unified: 'other', raw: undefined }\n const fallbackTextId = crypto.randomUUID()\n let hasToolCalls = false\n // Last seen SSE `id:` — forwarded to `reconnect` (vercel/ai#14688).\n let lastSeq = 0\n let attempt = 0\n\n return new ReadableStream<LanguageModelV3StreamPart>({\n async start(controller) {\n const decoder = new TextDecoder()\n let buffer = ''\n let currentBody: ReadableStream<Uint8Array> | null = responseBody\n\n const processLine = (line: string): void => {\n // Peek at `id:` lines for resume cursor; parseStreamLine ignores them.\n const idMatch = line.match(/^id: (\\d+)$/)\n if (idMatch) {\n const seq = Number(idMatch[1])\n if (seq > lastSeq) lastSeq = seq\n return\n }\n\n const parsed = parseStreamLine(line, fallbackTextId)\n if (parsed?.kind === 'part') {\n if (parsed.part.type === 'tool-call') {\n hasToolCalls = true\n }\n controller.enqueue(parsed.part)\n } else if (parsed?.kind === 'finish') {\n finishReason = parsed.finishReason\n } else if (parsed?.kind === 'data') {\n onData?.(parsed.type, parsed.data)\n }\n }\n\n controller.enqueue({ type: 'stream-start', warnings })\n\n try {\n while (currentBody) {\n const reader = currentBody.getReader()\n let bodyDone = false\n let chunksRead = 0\n try {\n while (true) {\n const { done, value } = await reader.read()\n if (done) {\n bodyDone = true\n break\n }\n chunksRead++\n\n buffer += decoder.decode(value, { stream: true })\n const lines = buffer.split('\\n')\n buffer = lines.pop() ?? ''\n\n for (const line of lines) processLine(line)\n }\n } catch (err) {\n if (!reconnect || attempt >= maxRetries || !isRetryableStreamError(err)) {\n throw err\n }\n attempt++\n await new Promise((r) => setTimeout(r, backoffMs(attempt)))\n // Discard partial line — resume replays complete frames.\n buffer = ''\n const next = await reconnect(lastSeq)\n if (next === null) {\n currentBody = null\n bodyDone = true\n continue\n }\n currentBody = next.body\n continue\n }\n // Reset retry budget after successful delivery.\n if (chunksRead > 0) attempt = 0\n if (bodyDone) break\n }\n\n if (buffer.trim()) processLine(buffer)\n\n // If tool calls were emitted, override finish reason so streamText\n // knows to execute tools and continue with the next step.\n if (hasToolCalls && finishReason.unified !== 'tool-calls') {\n finishReason = { unified: 'tool-calls', raw: finishReason.raw ?? 'tool-calls' }\n }\n\n controller.enqueue({\n type: 'finish',\n finishReason,\n usage: createEmptyUsage(),\n })\n } catch (error) {\n controller.enqueue({\n type: 'error',\n error: error instanceof Error ? error : new Error(String(error)),\n })\n } finally {\n controller.close()\n }\n },\n })\n}\n"],"names":[],"mappings":";;AAcO,SAAS,aACd,eACA,mBACwB;AACxB,SAAO;AAAA,IACL;AAAA,MACE,gBAAgB;AAAA,MAChB,GAAG;AAAA,MACH,GAAG;AAAA,IAAA;AAAA,IAEL;AAAA,EAAA;AAEJ;AAOO,SAAS,YAAY,SAAiB,YAAoB,UAA0B;AACzF,SAAO,GAAG,OAAO,IAAI,UAAU,SAAS,QAAQ;AAClD;AASO,MAAM,yBAAyB,MAAM;AAAA,EACjC;AAAA,EACA;AAAA,EACA;AAAA,EAET,YAAY,QAAgB,SAAiB;AAC3C,UAAM,2BAA2B,MAAM,IAAI,OAAO,EAAE,GACpD,KAAK,OAAO,oBACZ,KAAK,SAAS,QACd,KAAK,cAAc,UAAU,KAC7B,KAAK,OAAO,iBAAiB,UAAU,OAAO;AAAA,EAChD;AAAA,EAEA,OAAe,UAAU,SAAiD;AACxE,QAAI;AACF,aAAO,KAAK,MAAM,OAAO;AAAA,IAC3B,QAAQ;AACN,aAAO,CAAA;AAAA,IACT;AAAA,EACF;AACF;AAYO,MAAM,+BAA+B,MAAM;AAAA,EACvC;AAAA,EAET,YAAY,SAAiB,MAAe;AAC1C,UAAM,OAAO,GACb,KAAK,OAAO,0BACZ,KAAK,OAAO;AAAA,EACd;AACF;AAEO,MAAM,wBAAwB;AAWrC,SAAS,aAAa,MAAiD;AACrE,UAAQ,MAAA;AAAA,IACN,KAAK,YAAY;AACf,aAAO,EAAE,MAAM,kBAAA;AAAA,IACjB,KAAK,YAAY;AACf,aAAO,EAAE,MAAM,wBAAA;AAAA,IACjB,KAAK,YAAY;AACf,aAAO,EAAE,MAAM,sBAAA;AAAA,IACjB,KAAK,YAAY;AACf,aAAO,EAAE,MAAM,iBAAA;AAAA,IACjB,KAAK,YAAY;AACf,aAAO,EAAE,MAAM,uBAAA;AAAA,IACjB,KAAK,YAAY;AACf,aAAO,EAAE,MAAM,kBAAA;AAAA,IACjB;AACE,aAAO;AAAA,EAAA;AAEb;AASO,SAAS,mBAAmB,OAAgC;AAEjE,SAAI,iBAAiB,0BAA0B,MAAM,OAC5C,aAAa,MAAM,IAAI,KAAK,EAAE,MAAM,UAAA,IAIzC,iBAAiB,mBACf,MAAM,WAAW,MACZ,EAAE,MAAM,UAAA,IAEV,aAAa,MAAM,KAAK,IAAI,KAAK,EAAE,MAAM,UAAA,IAG3C,EAAE,MAAM,UAAA;AACjB;AAEO,SAAS,kBAAkB,YAAqC;AACrE,SAAO,WAAW,SAAS,qBAAqB,WAAW,SAAS;AACtE;AAcA,eAAsB,yBACpB,YACA,OACA,gBACiB;AACjB,QAAM,WAAW,qBAAqB,KAAK;AAE3C,MAAI,CAAC,kBAAkB,CAAC,kBAAkB,UAAU,EAAG,QAAO;AAE9D,QAAM,MAAM,MAAM,eAAA;AAClB,MAAI,CAAC,IAAI,QAAS,QAAO;AAEzB,UAAQ,WAAW,MAAA;AAAA,IACjB,KAAK;AACH,aAAO,wDAAwD,IAAI,OAAO;AAAA,IAC5E,KAAK;AACH,aAAO,6CAA6C,IAAI,OAAO;AAAA,IACjE;AACE,aAAO;AAAA,EAAA;AAEb;AAQO,SAAS,qBAAqB,OAAwB;AAC3D,SAAI,iBAAiB,yBACZ,MAAM,UAEX,iBAAiB,oBAAoB,MAAM,KAAK,UAC3C,MAAM,KAAK,UAEb;AACT;AAkBA,eAAsB,SAAS,SAA0C;AACvE,QAAM,UAAU,QAAQ,SAAS,WAAW,OACtC,SAAS,QAAQ,UAAU,QAC3B,UAAU,WAAW,SAAS,WAAW,QAEzC,WAAW,MAAM,QAAQ,QAAQ,KAAK;AAAA,IAC1C;AAAA,IACA,SAAS,QAAQ;AAAA,IACjB,GAAI,WAAW,EAAE,MAAM,KAAK,UAAU,QAAQ,IAAI,EAAA;AAAA,IAClD,QAAQ,QAAQ;AAAA,IAChB,GAAI,QAAQ,eAAe,EAAE,aAAa,QAAQ,YAAA;AAAA,EAAY,CAC/D;AAED,MAAI,CAAC,SAAS,IAAI;AAChB,UAAM,YAAY,MAAM,SAAS,KAAA;AACjC,UAAM,IAAI,iBAAiB,SAAS,QAAQ,SAAS;AAAA,EACvD;AAEA,SAAO;AACT;AAOO,SAAS,qBACd,SACQ;AACR,SAAI,OAAO,WAAY,WACd,UAGF,QACJ;AAAA,IACC,CAAC,SACC,KAAK,SAAS,UAAU,OAAO,KAAK,QAAS;AAAA,EAAA,EAEhD,IAAI,CAAC,SAAS,KAAK,IAAI,EACvB,KAAK;AAAA,CAAI;AACd;AAOO,SAAS,mBAAyC;AACvD,SAAO;AAAA,IACL,aAAa;AAAA,MACX,OAAO;AAAA,MACP,SAAS;AAAA,MACT,WAAW;AAAA,MACX,YAAY;AAAA,IAAA;AAAA,IAEd,cAAc;AAAA,MACZ,OAAO;AAAA,MACP,MAAM;AAAA,MACN,WAAW;AAAA,IAAA;AAAA,EACb;AAEJ;AAOO,SAAS,gBAAgB,QAAyD;AAcvF,SAAO,EAAE,UAbQ,MAAM;AACrB,YAAQ,QAAA;AAAA,MACN,KAAK;AAAA,MACL,KAAK;AAAA,MACL,KAAK;AAAA,MACL,KAAK;AAAA,MACL,KAAK;AACH,eAAO;AAAA,MACT;AACE,eAAO;AAAA,IAAA;AAAA,EAEb,GAAA,GAEkB,KAAK,OAAA;AACzB;AA+BO,SAAS,gBAAgB,MAAc,eAAgD;AAC5F,QAAM,UAAU,KAAK,KAAA;AACrB,MAAI,CAAC,QAAQ,WAAW,QAAQ,EAAG,QAAO;AAE1C,QAAM,UAAU,QAAQ,MAAM,CAAC;AAC/B,MAAI,YAAY,SAAU,QAAO;AAEjC,MAAI;AACJ,MAAI;AACF,WAAO,KAAK,MAAM,OAAO;AAAA,EAC3B,QAAQ;AACN,WAAO;AAAA,EACT;AAEA,QAAM,OAAO,KAAK;AAClB,MAAI,CAAC,KAAM,QAAO;AAElB,UAAQ,MAAA;AAAA,IACN,KAAK;AAEH,aAAO,EAAE,MAAM,QAAQ,MAAM,EAAE,MAAM,cAAc,IADvC,KAAK,MAAiB,cAAA,EACoB;AAAA,IAGxD,KAAK,cAAc;AACjB,YAAM,KAAM,KAAK,MAAiB,eAC5B,QAAQ,KAAK;AACnB,aAAI,OAAO,SAAU,WAAiB,OAC/B,EAAE,MAAM,QAAQ,MAAM,EAAE,MAAM,cAAc,IAAI,QAAM;AAAA,IAC/D;AAAA,IAEA,KAAK;AAEH,aAAO,EAAE,MAAM,QAAQ,MAAM,EAAE,MAAM,YAAY,IADrC,KAAK,MAAiB,cAAA,EACkB;AAAA,IAGtD,KAAK,wBAAwB;AAgB3B,YAAM,QAAQ,KAAK,OACb,mBAAmB,KAAK,qBAAqB;AACnD,aAAO;AAAA,QACL,MAAM;AAAA,QACN,MAAM;AAAA,UACJ,MAAM;AAAA,UACN,YAAY,KAAK;AAAA,UACjB,UAAU,KAAK;AAAA,UACf,OAAO,OAAO,SAAU,WAAW,QAAQ,KAAK,UAAU,KAAK;AAAA,UAC/D,GAAI,oBAAoB,EAAE,kBAAkB,IAAM,SAAS,GAAA;AAAA,QAAK;AAAA,MAClE;AAAA,IAEJ;AAAA,IAEA,KAAK,yBAAyB;AAW5B,UAAI,KAAK,qBAAqB,GAAM,QAAO;AAI3C,YAAM,SAAU,KAAK,UAAU,CAAA;AAC/B,aAAO;AAAA,QACL,MAAM;AAAA,QACN,MAAM;AAAA,UACJ,MAAM;AAAA,UACN,YAAY,KAAK;AAAA,UACjB,UAAU,KAAK;AAAA,UACf;AAAA,UACA,SAAS;AAAA,QAAA;AAAA,MACX;AAAA,IAEJ;AAAA,IAEA,KAAK,SAAS;AACZ,YAAM,YAAa,KAAK,aAAwB,iBAC1C,YAAY,KAAK;AACvB,aAAO;AAAA,QACL,MAAM;AAAA,QACN,MAAM;AAAA,UACJ,MAAM;AAAA,UACN,OAAO,IAAI,uBAAuB,WAAW,SAAS;AAAA,QAAA;AAAA,MACxD;AAAA,IAEJ;AAAA,IAEA,KAAK;AACH,aAAO,EAAE,MAAM,QAAQ,MAAM,SAAS,MAAM,EAAE,QAAQ,KAAK,SAAO;AAAA,IAGpE,KAAK;AACH,aAAO;AAAA,QACL,MAAM;AAAA,QACN,cAAc,gBAAgB,KAAK,YAAkC;AAAA,MAAA;AAAA,IAIzE;AACE,aAAI,KAAK,WAAW,OAAO,IAClB,EAAE,MAAM,QAAQ,MAAM,KAAK,MAAM,CAAC,GAAG,MAAM,KAAK,SAElD;AAAA,EAAA;AAGb;AAQA,eAAsB,sBACpB,cACiB;AACjB,QAAM,SAAS,aAAa,UAAA,GACtB,UAAU,IAAI,YAAA,GACd,gBAAgB;AACtB,MAAI,OAAO;AAEX,aAAa;AACX,UAAM,EAAE,MAAM,MAAA,IAAU,MAAM,OAAO,KAAA;AACrC,QAAI,KAAM;AAEV,UAAM,QAAQ,QAAQ,OAAO,OAAO,EAAE,QAAQ,IAAM;AACpD,eAAW,QAAQ,MAAM,MAAM;AAAA,CAAI,GAAG;AACpC,YAAM,SAAS,gBAAgB,MAAM,aAAa;AAC9C,cAAQ,SAAS,UAAU,OAAO,KAAK,SAAS,iBAClD,QAAQ,OAAO,KAAK;AAAA,IAExB;AAAA,EACF;AAEA,SAAO;AACT;AAuBA,SAAS,uBAAuB,OAAyB;AAEvD,MADI,CAAC,SACA,OAA6B,SAAS,aAAc,QAAO;AAEhE,QAAM,SAAU,OAA+B;AAE/C,MADI,UAAU,UAAU,OAAO,SAAS,OACpC,WAAW,OAAO,WAAW,OAAO,WAAW,IAAK,QAAO;AAE/D,QAAM,UAAW,OAAgC,WAAW;AAC5D,MAAI,6DAA6D,KAAK,OAAO,EAAG,QAAO;AAEvF,QAAM,QAAS,OAA+B;AAC9C,SAAI,SAAS,UAAU,QAAc,uBAAuB,KAAK,IAE1D;AACT;AAEA,SAAS,UAAU,SAAyB;AAC1C,SAAO,MAAM,MAAM,UAAU;AAC/B;AAWO,SAAS,gBACd,cACA,UACA,iBAC2C;AAE3C,QAAM,OACJ,OAAO,mBAAoB,aAAa,EAAE,QAAQ,gBAAA,IAAqB,mBAAmB,CAAA,GACtF,EAAE,QAAQ,WAAW,aAAa,MAAM;AAE9C,MAAI,eAA4C,EAAE,SAAS,SAAS,KAAK,OAAA;AACzE,QAAM,iBAAiB,OAAO,WAAA;AAC9B,MAAI,eAAe,IAEf,UAAU,GACV,UAAU;AAEd,SAAO,IAAI,eAA0C;AAAA,IACnD,MAAM,MAAM,YAAY;AACtB,YAAM,UAAU,IAAI,YAAA;AACpB,UAAI,SAAS,IACT,cAAiD;AAErD,YAAM,cAAc,CAAC,SAAuB;AAE1C,cAAM,UAAU,KAAK,MAAM,aAAa;AACxC,YAAI,SAAS;AACX,gBAAM,MAAM,OAAO,QAAQ,CAAC,CAAC;AACzB,gBAAM,YAAS,UAAU;AAC7B;AAAA,QACF;AAEA,cAAM,SAAS,gBAAgB,MAAM,cAAc;AAC/C,gBAAQ,SAAS,UACf,OAAO,KAAK,SAAS,gBACvB,eAAe,KAEjB,WAAW,QAAQ,OAAO,IAAI,KACrB,QAAQ,SAAS,WAC1B,eAAe,OAAO,eACb,QAAQ,SAAS,UAC1B,SAAS,OAAO,MAAM,OAAO,IAAI;AAAA,MAErC;AAEA,iBAAW,QAAQ,EAAE,MAAM,gBAAgB,UAAU;AAErD,UAAI;AACF,eAAO,eAAa;AAClB,gBAAM,SAAS,YAAY,UAAA;AAC3B,cAAI,WAAW,IACX,aAAa;AACjB,cAAI;AACF,uBAAa;AACX,oBAAM,EAAE,MAAM,MAAA,IAAU,MAAM,OAAO,KAAA;AACrC,kBAAI,MAAM;AACR,2BAAW;AACX;AAAA,cACF;AACA,4BAEA,UAAU,QAAQ,OAAO,OAAO,EAAE,QAAQ,IAAM;AAChD,oBAAM,QAAQ,OAAO,MAAM;AAAA,CAAI;AAC/B,uBAAS,MAAM,SAAS;AAExB,yBAAW,QAAQ,MAAO,aAAY,IAAI;AAAA,YAC5C;AAAA,UACF,SAAS,KAAK;AACZ,gBAAI,CAAC,aAAa,WAAW,cAAc,CAAC,uBAAuB,GAAG;AACpE,oBAAM;AAER,uBACA,MAAM,IAAI,QAAQ,CAAC,MAAM,WAAW,GAAG,UAAU,OAAO,CAAC,CAAC,GAE1D,SAAS;AACT,kBAAM,OAAO,MAAM,UAAU,OAAO;AACpC,gBAAI,SAAS,MAAM;AACjB,4BAAc,MACd,WAAW;AACX;AAAA,YACF;AACA,0BAAc,KAAK;AACnB;AAAA,UACF;AAGA,cADI,aAAa,MAAG,UAAU,IAC1B,SAAU;AAAA,QAChB;AAEI,eAAO,UAAQ,YAAY,MAAM,GAIjC,gBAAgB,aAAa,YAAY,iBAC3C,eAAe,EAAE,SAAS,cAAc,KAAK,aAAa,OAAO,aAAA,IAGnE,WAAW,QAAQ;AAAA,UACjB,MAAM;AAAA,UACN;AAAA,UACA,OAAO,iBAAA;AAAA,QAAiB,CACzB;AAAA,MACH,SAAS,OAAO;AACd,mBAAW,QAAQ;AAAA,UACjB,MAAM;AAAA,UACN,OAAO,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,KAAK,CAAC;AAAA,QAAA,CAChE;AAAA,MACH,UAAA;AACE,mBAAW,MAAA;AAAA,MACb;AAAA,IACF;AAAA,EAAA,CACD;AACH;"}
1
+ {"version":3,"file":"utils.js","sources":["../../src/utils.ts"],"sourcesContent":["import type {\n LanguageModelV3FinishReason,\n LanguageModelV3StreamPart,\n LanguageModelV3Usage,\n SharedV3Warning,\n} from '@ai-sdk/provider'\nimport { ERROR_CODES, type HeadlessErrorResponse } from '@sanity/agent-types'\nimport { isString, pickBy } from 'es-toolkit'\n\n/**\n * Builds headers for API requests, filtering out undefined values.\n *\n * @category Utilities\n */\nexport function buildHeaders(\n configHeaders: Record<string, string | undefined>,\n additionalHeaders?: Record<string, string>,\n): Record<string, string> {\n return pickBy(\n {\n 'Content-Type': 'application/json',\n ...additionalHeaders,\n ...configHeaders,\n },\n isString,\n )\n}\n\n/**\n * Constructs the full API URL for an endpoint.\n *\n * @category Utilities\n */\nexport function buildApiUrl(baseURL: string, apiVersion: string, endpoint: string): string {\n return `${baseURL}/${apiVersion}/agent${endpoint}`\n}\n\n/**\n * Custom error class for Sanity Agent API errors.\n * The {@link body} is parsed from the JSON response and follows the\n * {@link HeadlessErrorResponse} shape defined in `@sanity/agent-types`.\n *\n * @category Utilities\n */\nexport class SanityAgentError extends Error {\n readonly status: number\n readonly isRetryable: boolean\n readonly body: Partial<HeadlessErrorResponse>\n\n constructor(status: number, message: string) {\n super(`Sanity Agent API error: ${status} ${message}`)\n this.name = 'SanityAgentError'\n this.status = status\n this.isRetryable = status >= 500\n this.body = SanityAgentError.parseBody(message)\n }\n\n private static parseBody(message: string): Partial<HeadlessErrorResponse> {\n try {\n return JSON.parse(message)\n } catch {\n return {}\n }\n }\n}\n\n/**\n * Error class for errors received via SSE stream events.\n * Unlike {@link SanityAgentError} (which is thrown for HTTP-level errors before\n * streaming starts), this wraps errors received mid-stream as SSE `error` events.\n *\n * The {@link code} carries the backend's error classification (e.g. `UPSTREAM_OVERLOADED`)\n * so clients can take targeted action without parsing the message string.\n *\n * @category Utilities\n */\nexport class SanityAgentStreamError extends Error {\n readonly code: string | undefined\n\n constructor(message: string, code?: string) {\n super(message)\n this.name = 'SanityAgentStreamError'\n this.code = code\n }\n}\n\nexport const GENERIC_ERROR_MESSAGE = 'Sorry, something went wrong. Please try again.'\n\n/**\n * Synthesized client-side by {@link createSSEStream} when a resume attempt\n * returns 204 before a terminal frame was seen — the server has nothing more\n * to replay, so part of the response is unrecoverable. Never sent by the\n * server, hence not part of `@sanity/agent-types` ERROR_CODES.\n */\nexport const STREAM_INCOMPLETE_CODE = 'STREAM_INCOMPLETE'\n\nconst STREAM_INCOMPLETE_MESSAGE =\n 'Something went wrong and I lost the response on the way back. Please try again.'\n\nexport type AgentErrorType =\n | { type: 'no_applications' }\n | { type: 'application_not_found' }\n | { type: 'upstream_overloaded' }\n | { type: 'upstream_error' }\n | { type: 'usage_limit_exceeded' }\n | { type: 'streaming_error' }\n | { type: 'stream_incomplete' }\n | { type: 'unknown' }\n\nfunction mapErrorCode(code: string | undefined): AgentErrorType | null {\n switch (code) {\n case ERROR_CODES.NO_COMPATIBLE_APPLICATIONS:\n return { type: 'no_applications' }\n case ERROR_CODES.APPLICATION_NOT_FOUND:\n return { type: 'application_not_found' }\n case ERROR_CODES.UPSTREAM_OVERLOADED:\n return { type: 'upstream_overloaded' }\n case ERROR_CODES.UPSTREAM_ERROR:\n return { type: 'upstream_error' }\n case ERROR_CODES.AI_USAGE_LIMIT_EXCEEDED:\n return { type: 'usage_limit_exceeded' }\n case ERROR_CODES.STREAMING_ERROR:\n return { type: 'streaming_error' }\n case STREAM_INCOMPLETE_CODE:\n return { type: 'stream_incomplete' }\n default:\n return null\n }\n}\n\n/**\n * Classifies a caught error from the Content Agent API into a known error type.\n * Useful for clients that need to take different actions per error (e.g. show\n * resource-selection buttons). For the user-facing message, use {@link getAgentErrorMessage}.\n *\n * @category Utilities\n */\nexport function classifyAgentError(error: unknown): AgentErrorType {\n // Handle SSE stream errors (from parseStreamLine)\n if (error instanceof SanityAgentStreamError && error.code) {\n return mapErrorCode(error.code) ?? { type: 'unknown' }\n }\n\n // Handle HTTP-level errors (from fetchApi)\n if (error instanceof SanityAgentError) {\n if (error.status !== 400) {\n return { type: 'unknown' }\n }\n return mapErrorCode(error.body.code) ?? { type: 'unknown' }\n }\n\n return { type: 'unknown' }\n}\n\nexport function isAppRelatedError(classified: AgentErrorType): boolean {\n return classified.type === 'no_applications' || classified.type === 'application_not_found'\n}\n\nexport type AgentErrorMessageContext = {\n orgName?: string | null\n}\n\n/**\n * Resolves a user-facing error message for a classified agent error.\n * For errors that benefit from additional context (e.g. org name), the optional\n * {@link resolveContext} callback is invoked lazily — only when the error type\n * needs it. Non-contextual errors return the backend message as-is.\n *\n * @category Utilities\n */\nexport async function resolveAgentErrorMessage(\n classified: AgentErrorType,\n error: unknown,\n resolveContext?: () => Promise<AgentErrorMessageContext>,\n): Promise<string> {\n const fallback = getAgentErrorMessage(error)\n\n if (!resolveContext || !isAppRelatedError(classified)) return fallback\n\n const ctx = await resolveContext()\n if (!ctx.orgName) return fallback\n\n switch (classified.type) {\n case 'no_applications':\n return `Studio action required: To use the Content Agent in \"${ctx.orgName}\", you must have at least one Sanity Studio running version 5.1.0 or above. After upgrading, open your studio to connect it to the agent.`\n case 'application_not_found':\n return `The requested studio is not available in \"${ctx.orgName}\". You may not have access, or it may belong to a different organization.`\n default:\n return fallback\n }\n}\n\n/**\n * Extracts a user-facing error message from an agent API error.\n * Returns the message sent by the API when available, otherwise a generic fallback.\n *\n * @category Utilities\n */\nexport function getAgentErrorMessage(error: unknown): string {\n if (error instanceof SanityAgentStreamError) {\n return error.message\n }\n if (error instanceof SanityAgentError && error.body.message) {\n return error.body.message\n }\n return GENERIC_ERROR_MESSAGE\n}\n\nexport interface FetchOptions {\n url: string\n method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD'\n body?: unknown\n headers: Record<string, string>\n credentials?: RequestCredentials\n signal?: AbortSignal\n fetch?: typeof globalThis.fetch\n}\n\n/**\n * Makes an API request and returns the response.\n * Throws SanityAgentError if the response is not ok.\n *\n * @category Utilities\n */\nexport async function fetchApi(options: FetchOptions): Promise<Response> {\n const fetchFn = options.fetch ?? globalThis.fetch\n const method = options.method ?? 'POST'\n const hasBody = method !== 'GET' && method !== 'HEAD'\n\n const response = await fetchFn(options.url, {\n method,\n headers: options.headers,\n ...(hasBody && { body: JSON.stringify(options.body) }),\n signal: options.signal,\n ...(options.credentials && { credentials: options.credentials }),\n })\n\n if (!response.ok) {\n const errorText = await response.text()\n throw new SanityAgentError(response.status, errorText)\n }\n\n return response\n}\n\n/**\n * Extracts text content from message parts (user or assistant).\n *\n * @category Utilities\n */\nexport function extractTextFromParts(\n content: string | Array<{ type: string; text?: string }>,\n): string {\n if (typeof content === 'string') {\n return content\n }\n\n return content\n .filter(\n (part): part is { type: 'text'; text: string } =>\n part.type === 'text' && typeof part.text === 'string',\n )\n .map((part) => part.text)\n .join('\\n')\n}\n\n/**\n * Creates an empty V3 usage object. Our API does not return token counts.\n *\n * @category Utilities\n */\nexport function createEmptyUsage(): LanguageModelV3Usage {\n return {\n inputTokens: {\n total: 0,\n noCache: undefined,\n cacheRead: undefined,\n cacheWrite: undefined,\n },\n outputTokens: {\n total: 0,\n text: undefined,\n reasoning: undefined,\n },\n }\n}\n\n/**\n * Maps API finish reasons to LanguageModelV3FinishReason.\n *\n * @category Utilities\n */\nexport function mapFinishReason(reason: string | undefined): LanguageModelV3FinishReason {\n const unified = (() => {\n switch (reason) {\n case 'stop':\n case 'length':\n case 'content-filter':\n case 'tool-calls':\n case 'error':\n return reason\n default:\n return 'other' as const\n }\n })()\n\n return { unified, raw: reason }\n}\n\nexport type ParsedStreamLine =\n | { kind: 'part'; part: LanguageModelV3StreamPart }\n | { kind: 'finish'; finishReason: LanguageModelV3FinishReason }\n | { kind: 'data'; type: string; data: unknown }\n | { kind: 'done' }\n\n/**\n * Parses a single SSE line from the AI SDK v6 UI Message Stream protocol.\n * Returns null if the line cannot be parsed or should be ignored.\n *\n * Format: `data: <json>\\n`\n *\n * Supported types:\n * text-start – Text block start (forwarded as-is)\n * text-delta – Text chunk\n * text-end – Text block end (forwarded as-is)\n * tool-input-available – Tool call (client tools only, without providerExecuted)\n * tool-output-available – Tool result (informational, skipped)\n * finish – Finish event with reason\n * error – Error event\n * start – Stream start (skipped)\n * [DONE] – Stream termination (surfaced as `{ kind: 'done' }`)\n *\n * @see https://ai-sdk.dev/docs/ai-sdk-ui/stream-protocol\n *\n * @param line A single line from the SSE byte stream.\n * @param textContentId Fallback ID for text parts that lack an explicit id.\n *\n * @category Utilities\n */\nexport function parseStreamLine(line: string, textContentId: string): ParsedStreamLine | null {\n const trimmed = line.trim()\n if (!trimmed.startsWith('data: ')) return null\n\n const payload = trimmed.slice(6)\n if (payload === '[DONE]') return { kind: 'done' }\n\n let part: Record<string, unknown>\n try {\n part = JSON.parse(payload) as Record<string, unknown>\n } catch {\n return null\n }\n\n const type = part.type as string | undefined\n if (!type) return null\n\n switch (type) {\n case 'text-start': {\n const id = (part.id as string) || textContentId\n return { kind: 'part', part: { type: 'text-start', id } }\n }\n\n case 'text-delta': {\n const id = (part.id as string) || textContentId\n const delta = part.delta as string\n if (typeof delta !== 'string') return null\n return { kind: 'part', part: { type: 'text-delta', id, delta } }\n }\n\n case 'text-end': {\n const id = (part.id as string) || textContentId\n return { kind: 'part', part: { type: 'text-end', id } }\n }\n\n case 'tool-input-available': {\n // Forward both client- and server-executed tools to the consumer's\n // streamText loop. Stop conditions like `hasToolCall('final_answer')`\n // need to see the terminal `providerExecuted` tool call to fire; the\n // consumer's local-execute path skips tools without an `execute`\n // function (or that are not in the consumer's tool map), so a server\n // tool surfaced here cannot be double-executed. See SAGE-182.\n //\n // Server-executed tools (`providerExecuted: true`) are also marked\n // `dynamic: true`. The AI SDK's `parseToolCall` only accepts a\n // tool that is missing from the consumer's tool map when both\n // `providerExecuted` and `dynamic` are set — otherwise it throws\n // `NoSuchToolError` and emits a phantom `tool-error` chunk for a\n // tool that already executed server-side. The agent server's tools\n // are not exposed to the consumer at all (server-execute is the\n // whole point), so `dynamic` is the right flag to set here.\n const input = part.input\n const isServerExecuted = part.providerExecuted === true\n return {\n kind: 'part',\n part: {\n type: 'tool-call',\n toolCallId: part.toolCallId as string,\n toolName: part.toolName as string,\n input: typeof input === 'string' ? input : JSON.stringify(input),\n ...(isServerExecuted && { providerExecuted: true, dynamic: true }),\n },\n }\n }\n\n case 'tool-output-available': {\n // Forward the result of a provider-executed tool (e.g. `final_answer`)\n // so the AI SDK loop can pair it with the matching `tool-call` and\n // include it in `currentModelResponse.content`. Without the result\n // chunk the loop will request another step to satisfy the dangling\n // tool call, defeating the natural termination Phase 1 enables.\n //\n // Only forward when `providerExecuted` is set. Client-tool results\n // are produced locally on the consumer side via `tool.execute`, not\n // streamed from the server, so any `tool-output-available` we see\n // for a non-providerExecuted tool would be unexpected — drop it.\n if (part.providerExecuted !== true) return null\n // The AI SDK's `tool-result` part requires a non-null JSON-serializable\n // result. Fall back to an empty object when the server omits one (e.g.\n // tools that signal completion via side effects).\n const result = (part.output ?? {}) as NonNullable<typeof part.output>\n return {\n kind: 'part',\n part: {\n type: 'tool-result',\n toolCallId: part.toolCallId as string,\n toolName: part.toolName as string,\n result,\n dynamic: true,\n },\n }\n }\n\n case 'error': {\n const errorText = (part.errorText as string) ?? 'Unknown error'\n const errorCode = part.errorCode as string | undefined\n return {\n kind: 'part',\n part: {\n type: 'error',\n error: new SanityAgentStreamError(errorText, errorCode),\n },\n }\n }\n\n case 'abort': {\n return { kind: 'data', type: 'abort', data: { reason: part.reason } }\n }\n\n case 'finish': {\n return {\n kind: 'finish',\n finishReason: mapFinishReason(part.finishReason as string | undefined),\n }\n }\n\n default: {\n if (type.startsWith('data-')) {\n return { kind: 'data', type: type.slice(5), data: part.data }\n }\n return null\n }\n }\n}\n\n/**\n * Extracts accumulated text from a streaming response body.\n * Used for simple streaming cases where only the final text is needed.\n *\n * @category Utilities\n */\nexport async function extractTextFromStream(\n responseBody: ReadableStream<Uint8Array>,\n): Promise<string> {\n const reader = responseBody.getReader()\n const decoder = new TextDecoder()\n const textContentId = 'extract'\n let text = ''\n\n while (true) {\n const { done, value } = await reader.read()\n if (done) break\n\n const chunk = decoder.decode(value, { stream: true })\n for (const line of chunk.split('\\n')) {\n const parsed = parseStreamLine(line, textContentId)\n if (parsed?.kind === 'part' && parsed.part.type === 'text-delta') {\n text += parsed.part.delta\n }\n }\n }\n\n return text\n}\n\n/** @category Utilities */\nexport type CreateSSEStreamOptions = {\n /**\n * Hook to receive `data-*` chunks the AI SDK doesn't surface natively.\n */\n onData?: (type: string, data: unknown) => void\n /**\n * Optional reconnect callback. When set, retryable read errors trigger a\n * call with the last seen `id:` sequence number, expecting a fresh\n * `Response` whose body continues the stream from that frame. Return\n * `null` to signal a clean end (HTTP 204 from the server).\n */\n reconnect?: (afterSeq: number) => Promise<Response | null>\n maxRetries?: number\n}\n\n/**\n * Retryable error predicate. Matches the same network/5xx/AbortError logic\n * as `@repo/shared/client/retryable`, inlined here so `agent-provider` stays\n * dependency-free.\n */\nfunction isRetryableStreamError(error: unknown): boolean {\n if (!error) return false\n if ((error as { name?: string })?.name === 'AbortError') return false\n\n const status = (error as { status?: number })?.status\n if (status && status >= 500 && status < 600) return true\n if (status === 408 || status === 425 || status === 429) return true\n\n const message = (error as { message?: string })?.message ?? ''\n if (/network|fetch failed|socket|ECONN|ETIMEDOUT|EPIPE|UND_ERR/i.test(message)) return true\n\n // undici throws `TypeError: terminated` when the connection drops mid-body\n // (Fetch.onAborted), often with no cause attached.\n if (message === 'terminated') return true\n\n const cause = (error as { cause?: unknown })?.cause\n if (cause && cause !== error) return isRetryableStreamError(cause)\n\n return false\n}\n\nfunction backoffMs(attempt: number): number {\n return 500 * 2 ** (attempt - 1)\n}\n\n/**\n * Creates a ReadableStream that parses AI SDK v6 UI Message Stream SSE data\n * from a response body and emits LanguageModelV3 stream parts.\n *\n * Text lifecycle events (text-start / text-end) are forwarded from the server.\n * Tool calls are tracked to ensure the finish reason is set correctly.\n *\n * @category Utilities\n */\nexport function createSSEStream(\n responseBody: ReadableStream<Uint8Array>,\n warnings: SharedV3Warning[],\n optionsOrOnData?: CreateSSEStreamOptions | ((type: string, data: unknown) => void),\n): ReadableStream<LanguageModelV3StreamPart> {\n // Backwards compat: callers used to pass `onData` as the third positional arg.\n const opts: CreateSSEStreamOptions =\n typeof optionsOrOnData === 'function' ? { onData: optionsOrOnData } : (optionsOrOnData ?? {})\n const { onData, reconnect, maxRetries = 3 } = opts\n\n let finishReason: LanguageModelV3FinishReason = { unified: 'other', raw: undefined }\n const fallbackTextId = crypto.randomUUID()\n let hasToolCalls = false\n // Last seen SSE `id:` — forwarded to `reconnect` (vercel/ai#14688).\n let lastSeq = 0\n let attempt = 0\n // Whether a terminal frame (`finish` or `[DONE]`) arrived. A resume 204\n // before this means the tail of the response is unrecoverable.\n let sawTerminal = false\n\n return new ReadableStream<LanguageModelV3StreamPart>({\n async start(controller) {\n const decoder = new TextDecoder()\n let buffer = ''\n let currentBody: ReadableStream<Uint8Array> | null = responseBody\n\n const processLine = (line: string): void => {\n // Peek at `id:` lines for resume cursor; parseStreamLine ignores them.\n const idMatch = line.match(/^id: (\\d+)$/)\n if (idMatch) {\n const seq = Number(idMatch[1])\n if (seq > lastSeq) lastSeq = seq\n return\n }\n\n const parsed = parseStreamLine(line, fallbackTextId)\n if (parsed?.kind === 'part') {\n if (parsed.part.type === 'tool-call') {\n hasToolCalls = true\n }\n controller.enqueue(parsed.part)\n } else if (parsed?.kind === 'finish') {\n sawTerminal = true\n finishReason = parsed.finishReason\n } else if (parsed?.kind === 'data') {\n onData?.(parsed.type, parsed.data)\n } else if (parsed?.kind === 'done') {\n sawTerminal = true\n }\n }\n\n controller.enqueue({ type: 'stream-start', warnings })\n\n try {\n while (currentBody) {\n const reader = currentBody.getReader()\n let bodyDone = false\n let chunksRead = 0\n try {\n while (true) {\n const { done, value } = await reader.read()\n if (done) {\n bodyDone = true\n break\n }\n chunksRead++\n\n buffer += decoder.decode(value, { stream: true })\n const lines = buffer.split('\\n')\n buffer = lines.pop() ?? ''\n\n for (const line of lines) processLine(line)\n }\n } catch (err) {\n if (!reconnect || attempt >= maxRetries || !isRetryableStreamError(err)) {\n throw err\n }\n attempt++\n await new Promise((r) => setTimeout(r, backoffMs(attempt)))\n // Discard partial line — resume replays complete frames.\n buffer = ''\n const next = await reconnect(lastSeq)\n if (next === null) {\n // 204: nothing left to replay. If no terminal frame arrived,\n // the response tail is lost — surface it instead of fabricating\n // a clean finish (SAGE-837).\n if (!sawTerminal) {\n throw new SanityAgentStreamError(STREAM_INCOMPLETE_MESSAGE, STREAM_INCOMPLETE_CODE)\n }\n currentBody = null\n bodyDone = true\n continue\n }\n currentBody = next.body\n continue\n }\n // Reset retry budget after successful delivery.\n if (chunksRead > 0) attempt = 0\n if (bodyDone) break\n }\n\n if (buffer.trim()) processLine(buffer)\n\n // If tool calls were emitted, override finish reason so streamText\n // knows to execute tools and continue with the next step.\n if (hasToolCalls && finishReason.unified !== 'tool-calls') {\n finishReason = { unified: 'tool-calls', raw: finishReason.raw ?? 'tool-calls' }\n }\n\n controller.enqueue({\n type: 'finish',\n finishReason,\n usage: createEmptyUsage(),\n })\n } catch (error) {\n controller.enqueue({\n type: 'error',\n error: error instanceof Error ? error : new Error(String(error)),\n })\n } finally {\n controller.close()\n }\n },\n })\n}\n"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -144,6 +144,13 @@ declare class SanityAgentStreamError extends Error {
144
144
  constructor(message: string, code?: string);
145
145
  }
146
146
  declare const GENERIC_ERROR_MESSAGE = "Sorry, something went wrong. Please try again.";
147
+ /**
148
+ * Synthesized client-side by {@link createSSEStream} when a resume attempt
149
+ * returns 204 before a terminal frame was seen — the server has nothing more
150
+ * to replay, so part of the response is unrecoverable. Never sent by the
151
+ * server, hence not part of `@sanity/agent-types` ERROR_CODES.
152
+ */
153
+ declare const STREAM_INCOMPLETE_CODE = "STREAM_INCOMPLETE";
147
154
  type AgentErrorType = {
148
155
  type: 'no_applications';
149
156
  } | {
@@ -156,6 +163,8 @@ type AgentErrorType = {
156
163
  type: 'usage_limit_exceeded';
157
164
  } | {
158
165
  type: 'streaming_error';
166
+ } | {
167
+ type: 'stream_incomplete';
159
168
  } | {
160
169
  type: 'unknown';
161
170
  };
@@ -228,6 +237,8 @@ type ParsedStreamLine = {
228
237
  kind: 'data';
229
238
  type: string;
230
239
  data: unknown;
240
+ } | {
241
+ kind: 'done';
231
242
  };
232
243
  /**
233
244
  * Parses a single SSE line from the AI SDK v6 UI Message Stream protocol.
@@ -244,7 +255,7 @@ type ParsedStreamLine = {
244
255
  * finish – Finish event with reason
245
256
  * error – Error event
246
257
  * start – Stream start (skipped)
247
- * [DONE] – Stream termination (skipped)
258
+ * [DONE] – Stream termination (surfaced as `{ kind: 'done' }`)
248
259
  *
249
260
  * @see https://ai-sdk.dev/docs/ai-sdk-ui/stream-protocol
250
261
  *
@@ -286,5 +297,5 @@ type CreateSSEStreamOptions = {
286
297
  * @category Utilities
287
298
  */
288
299
  declare function createSSEStream(responseBody: ReadableStream<Uint8Array>, warnings: SharedV3Warning[], optionsOrOnData?: CreateSSEStreamOptions | ((type: string, data: unknown) => void)): ReadableStream<LanguageModelV3StreamPart>;
289
- export { type AgentErrorMessageContext, type AgentErrorType, type AgentSettings, type Application, type ApplicationResource, type Capabilities, type Capability, type CapabilityPreset, type CapabilityPresetValue, type ChatRequest, type ChatResponse, type ClientDerivedConfig, type ClientToolDefinition, type Config, ContentAgentModel, type ContentAgentModelConfig, type ContentAgentProvider, type ContentAgentProviderSettings, type ErrorResponse, type Filter, GENERIC_ERROR_MESSAGE, type GenerateOptions, type GenerateRequest, type GenerateResult, type GenerateSettings, type Message, type MessageRole, type OutputFormat, type ParsedStreamLine, SanityAgentError, SanityAgentStreamError, type SanityClientConfig, type SanityClientLike, type StudioAgentOptions, type UserMessageContext, type ValidationIssue, buildApiUrl, buildHeaders, classifyAgentError, type components, createContentAgent, createSSEStream, createStudioAgent, extractTextFromParts, extractTextFromStream, fetchApi, fromClient, getAgentErrorMessage, getBaseUrl, isAppRelatedError, mapFinishReason, type operations, parseStreamLine, type paths, resolveAgentErrorMessage };
300
+ export { type AgentErrorMessageContext, type AgentErrorType, type AgentSettings, type Application, type ApplicationResource, type Capabilities, type Capability, type CapabilityPreset, type CapabilityPresetValue, type ChatRequest, type ChatResponse, type ClientDerivedConfig, type ClientToolDefinition, type Config, ContentAgentModel, type ContentAgentModelConfig, type ContentAgentProvider, type ContentAgentProviderSettings, type ErrorResponse, type Filter, GENERIC_ERROR_MESSAGE, type GenerateOptions, type GenerateRequest, type GenerateResult, type GenerateSettings, type Message, type MessageRole, type OutputFormat, type ParsedStreamLine, STREAM_INCOMPLETE_CODE, SanityAgentError, SanityAgentStreamError, type SanityClientConfig, type SanityClientLike, type StudioAgentOptions, type UserMessageContext, type ValidationIssue, buildApiUrl, buildHeaders, classifyAgentError, type components, createContentAgent, createSSEStream, createStudioAgent, extractTextFromParts, extractTextFromStream, fetchApi, fromClient, getAgentErrorMessage, getBaseUrl, isAppRelatedError, mapFinishReason, type operations, parseStreamLine, type paths, resolveAgentErrorMessage };
290
301
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../src/sanity-agent-language-model.ts","../src/studio.ts","../src/utils.ts"],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/sanity-agent-language-model.ts","../src/studio.ts","../src/utils.ts"],"mappings":"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"}
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { groupBy } from "es-toolkit";
2
2
  import { extractTextFromParts, fetchApi, buildHeaders, createEmptyUsage, parseStreamLine, createSSEStream, buildApiUrl, SanityAgentError, extractTextFromStream } from "./_chunks-es/utils.js";
3
- import { GENERIC_ERROR_MESSAGE, SanityAgentStreamError, classifyAgentError, getAgentErrorMessage, isAppRelatedError, mapFinishReason, resolveAgentErrorMessage } from "./_chunks-es/utils.js";
3
+ import { GENERIC_ERROR_MESSAGE, STREAM_INCOMPLETE_CODE, SanityAgentStreamError, classifyAgentError, getAgentErrorMessage, isAppRelatedError, mapFinishReason, resolveAgentErrorMessage } from "./_chunks-es/utils.js";
4
4
  function getBaseUrl(client) {
5
5
  return new URL(client.getUrl("/")).origin;
6
6
  }
@@ -407,6 +407,7 @@ function createStudioAgent(client, workspace, options) {
407
407
  export {
408
408
  ContentAgentModel,
409
409
  GENERIC_ERROR_MESSAGE,
410
+ STREAM_INCOMPLETE_CODE,
410
411
  SanityAgentError,
411
412
  SanityAgentStreamError,
412
413
  buildApiUrl,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "content-agent",
3
- "version": "1.2.1",
3
+ "version": "1.3.0",
4
4
  "description": "Vercel AI SDK provider for Sanity Content Agent API",
5
5
  "keywords": [
6
6
  "sanity",
@@ -46,7 +46,7 @@
46
46
  "@sanity/agent-types": "0.1.4"
47
47
  },
48
48
  "devDependencies": {
49
- "@ai-sdk/react": "^3.0.0",
49
+ "@ai-sdk/react": "^3.0.248",
50
50
  "@sanity/pkg-utils": "^10.4.1",
51
51
  "@testing-library/react": "^16.0.0",
52
52
  "@types/node": "^22.19.7",
@@ -58,9 +58,9 @@
58
58
  "react-dom": "^19.0.0",
59
59
  "typedoc": "^0.28.17",
60
60
  "typescript": "^5.9.3",
61
- "vitest": "^4.0.18",
62
- "@repo/tsconfig": "0.0.0",
63
- "@repo/package.config": "0.0.0"
61
+ "vitest": "^4.1.10",
62
+ "@repo/package.config": "0.0.0",
63
+ "@repo/tsconfig": "0.0.0"
64
64
  },
65
65
  "peerDependencies": {
66
66
  "@ai-sdk/react": ">=3.0.0",