content-agent 1.2.0 → 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.
- package/dist/_chunks-es/utils.js +10 -5
- package/dist/_chunks-es/utils.js.map +1 -1
- package/dist/index.d.ts +13 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/react.d.ts +29 -25
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +96 -50
- package/dist/react.js.map +1 -1
- package/package.json +3 -3
package/dist/_chunks-es/utils.js
CHANGED
|
@@ -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
|
|
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"
|
|
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":"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"}
|
|
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 (
|
|
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
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/sanity-agent-language-model.ts","../src/studio.ts","../src/utils.ts"],"mappings":";;;;;;;UAyBiB,uBAAA;EACf,QAAA;EACA,OAAA;EACA,UAAA;EACA,cAAA;EACA,OAAA,QAAe,MAAA;EACf,WAAA,GAAc,kBAAA;EACd,KAAA,UAAe,UAAA,CAAW,KAAA;AAAA;;;;;;;;;;;;;AAoE5B;;;;;;;;;;;;;;;;;;;;cAAa,iBAAA,YAA6B,eAAA;EAAA,SAC/B,oBAAA;EAAA,SACA,QAAA;EAAA,SACA,OAAA;EAAA,SACA,aAAA,EAAe,MAAA,SAAe,MAAA;EAAA,iBAEtB,MAAA;EAAA,iBACA,QAAA;EAAA,iBACA,QAAA;cAGf,QAAA,UACA,QAAA,EAAU,aAAA,cACV,MAAA,EAAQ,uBAAA;EADR;;;;;;;;;EAAA,QAmBM,uBAAA;EAkJS;;;;EAAA,QAzDT,kBAAA;EAAA,QAiBA,eAAA;EAAA,QAoBA,gBAAA;EAAA,QAgBA,kBAAA;EAIF,UAAA,CAAW,OAAA,EAAS,0BAAA,GAA6B,OAAA,CAAQ,6BAAA;EAsFyB;;;ACnV1F;EDmV0F,QA9C1E,wBAAA;EA8CR,QAAA,CAAS,OAAA,EAAS,0BAAA,GAA6B,OAAA,CAAQ,2BAAA;AAAA;;;AAlV/D;KCDY,kBAAA;yDAEV,KAAA,UAAe,UAAA,CAAW,KAAA;EDKZ;;;;ECCd,eAAA,GAAkB,IAAA,CAAK,4BAAA;AAAA;;;;;;;;;;;iBAiIT,iBAAA,CACd,MAAA,EAAQ,gBAAA,EACR,SAAA,UACA,OAAA,GAAU,kBAAA,GACT,oBAAA;;;;AD5IH;;iBEXgB,YAAA,CACd,aAAA,EAAe,MAAA,8BACf,iBAAA,GAAoB,MAAA,mBACnB,MAAA;;;;;;iBAgBa,WAAA,CAAY,OAAA,UAAiB,UAAA,UAAoB,QAAA;;;;;;;;cAWpD,gBAAA,SAAyB,KAAA;EAAA,SAC3B,MAAA;EAAA,SACA,WAAA;EAAA,SACA,IAAA,EAAM,OAAA,CAAQ,qBAAA;cAEX,MAAA,UAAgB,OAAA;EAAA,eAQb,SAAA;AAAA;;;;;;;;;;;cAmBJ,sBAAA,SAA+B,KAAA;EAAA,SACjC,IAAA;cAEG,OAAA,UAAiB,IAAA;AAAA;AAAA,cAOlB,qBAAA;AAAA,
|
|
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/dist/react.d.ts
CHANGED
|
@@ -2,10 +2,10 @@ import { b as OutputFormat } from "./_chunks-dts/types.js";
|
|
|
2
2
|
import { UIMessage } from "ai";
|
|
3
3
|
import { Application, Context } from "@sanity/agent-types";
|
|
4
4
|
import { UseChatHelpers, UseChatOptions } from "@ai-sdk/react";
|
|
5
|
-
/** `useChat` options that {@link useContentAgentChat} owns; the rest pass through. */
|
|
6
|
-
type ManagedChatOptions = 'id' | 'transport' | 'resume' | 'messages' | 'onError' | 'onFinish' | 'onData';
|
|
7
5
|
/** A value, or a getter resolved at send time (avoids stale closures). */
|
|
8
6
|
type Resolvable<T> = T | (() => T | undefined);
|
|
7
|
+
/** `useChat` options that {@link useContentAgentChat} owns; the rest pass through. */
|
|
8
|
+
type ManagedChatOptions = 'id' | 'transport' | 'resume' | 'messages' | 'onError' | 'onFinish' | 'onData';
|
|
9
9
|
/**
|
|
10
10
|
* Options for {@link useContentAgentChat}.
|
|
11
11
|
*
|
|
@@ -34,12 +34,12 @@ interface UseContentAgentChatOptions<UI_MESSAGE extends UIMessage = UIMessage> {
|
|
|
34
34
|
/** Identifies the creating client (e.g. `'dashboard'`). */
|
|
35
35
|
clientId?: string;
|
|
36
36
|
/**
|
|
37
|
-
* When true, reconnect to an in-flight stream once per thread (page reload
|
|
38
|
-
*
|
|
39
|
-
*
|
|
37
|
+
* When true, reconnect to an in-flight stream once per thread (page reload).
|
|
38
|
+
* The SDK's `resume: true` only fires on first mount, so this hook drives
|
|
39
|
+
* resume per (Chat x threadId) pair instead.
|
|
40
40
|
*/
|
|
41
41
|
autoResume?: boolean;
|
|
42
|
-
/** Initial messages (e.g. server-rendered history). */
|
|
42
|
+
/** Initial messages for a thread's first mount (e.g. server-rendered history). */
|
|
43
43
|
messages?: UI_MESSAGE[];
|
|
44
44
|
onError?: (error: Error) => void;
|
|
45
45
|
onFinish?: () => void;
|
|
@@ -48,9 +48,10 @@ interface UseContentAgentChatOptions<UI_MESSAGE extends UIMessage = UIMessage> {
|
|
|
48
48
|
data: unknown;
|
|
49
49
|
}) => void;
|
|
50
50
|
/**
|
|
51
|
-
* Any other `useChat` options to forward (e.g. `onToolCall`,
|
|
52
|
-
* `
|
|
53
|
-
*
|
|
51
|
+
* Any other `useChat`/`Chat` options to forward (e.g. `onToolCall`,
|
|
52
|
+
* `generateId`, `sendAutomaticallyWhen`). The options this hook manages
|
|
53
|
+
* (transport, id, messages, the callbacks above) take precedence. These are
|
|
54
|
+
* read once when a thread's Chat is created.
|
|
54
55
|
*/
|
|
55
56
|
chatOptions?: Omit<UseChatOptions<UI_MESSAGE>, ManagedChatOptions>;
|
|
56
57
|
}
|
|
@@ -61,36 +62,39 @@ interface UseContentAgentChatOptions<UI_MESSAGE extends UIMessage = UIMessage> {
|
|
|
61
62
|
*/
|
|
62
63
|
interface UseSanityChatResumeOptions {
|
|
63
64
|
/**
|
|
64
|
-
* When true, reconnect to an in-flight stream once per thread
|
|
65
|
-
*
|
|
66
|
-
*
|
|
65
|
+
* When true, reconnect to an in-flight stream once per thread. The SDK's
|
|
66
|
+
* `resume: true` only fires on first mount, so this drives resume per
|
|
67
|
+
* (Chat x threadId) pair instead.
|
|
67
68
|
*/
|
|
68
69
|
autoResume?: boolean;
|
|
69
70
|
}
|
|
70
71
|
/**
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
72
|
+
* Reconnect to an in-flight stream once per (Chat x threadId), when idle.
|
|
73
|
+
*
|
|
74
|
+
* Does not stop the stream on thread change or unmount: per the AI SDK
|
|
75
|
+
* resumable-stream model, client-side aborts are disconnects that should stay
|
|
76
|
+
* resumable, not cancellations. Stopping is an explicit user action (call
|
|
77
|
+
* `chat.stop()` and/or a server stop endpoint), never route or unmount
|
|
78
|
+
* cleanup. See https://ai-sdk.dev/docs/troubleshooting/abort-breaks-resumable-streams.
|
|
75
79
|
*
|
|
76
80
|
* @category React
|
|
77
81
|
*
|
|
78
82
|
* @example
|
|
79
83
|
* ```tsx
|
|
80
|
-
* const
|
|
81
|
-
* const chat = useChat({ id: threadId, transport, onToolCall })
|
|
84
|
+
* const chat = useChat({ chat: myThreadChat })
|
|
82
85
|
* useSanityChatResume(chat, { autoResume: threadExistsOnBackend })
|
|
83
86
|
* ```
|
|
84
87
|
*/
|
|
85
|
-
declare function useSanityChatResume<UI_MESSAGE extends UIMessage>(chat: Pick<UseChatHelpers<UI_MESSAGE>, 'id' | 'status' | '
|
|
88
|
+
declare function useSanityChatResume<UI_MESSAGE extends UIMessage>(chat: Pick<UseChatHelpers<UI_MESSAGE>, 'id' | 'status' | 'resumeStream'>, options?: UseSanityChatResumeOptions): void;
|
|
86
89
|
/**
|
|
87
|
-
* `useChat` wired to the Sanity Content Agent:
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
* thread change, and stream cleanup when the thread changes.
|
|
90
|
+
* `useChat` wired to the Sanity Content Agent: bearer auth, per-turn body
|
|
91
|
+
* shaping (application/context/format/clientId), `x-request-id` idempotency,
|
|
92
|
+
* and resume on reload.
|
|
91
93
|
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
+
* Holds one `Chat` instance per thread (the AI SDK's recommended pattern for
|
|
95
|
+
* multiple conversations), so switching threads mid-stream and back keeps the
|
|
96
|
+
* thread's messages and its still-running stream instead of rebuilding an empty
|
|
97
|
+
* Chat. Returns the standard `useChat` helpers.
|
|
94
98
|
*
|
|
95
99
|
* @category React
|
|
96
100
|
*
|
package/dist/react.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"react.d.ts","names":[],"sources":["../src/react.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"react.d.ts","names":[],"sources":["../src/_internal/create-thread-chat.ts","../src/react.ts"],"mappings":";;;;AASA;AAAA,KAAY,UAAA,MAAgB,CAAA,UAAW,CAAA;;KAG3B,kBAAA;AAHZ;;;;;AAAA,UCSiB,0BAAA,oBAA8C,SAAA,GAAY,SAAA;EDTpC;ECWrC,cAAA;EDXsC;ECatC,QAAA;EDV4B;;;;ECe5B,OAAA;EATe;EAWf,UAAA;EAXyC;EAazC,KAAA;EAbyE;EAezE,WAAA,GAAc,UAAA,CAAW,WAAA;EAAX;EAEd,OAAA,GAAU,UAAA,CAAW,OAAA;EAAX;EAEV,MAAA,GAAS,YAAA;EAUE;EARX,QAAA;EAkBkC;;;;;EAZlC,UAAA;EA3B0C;EA6B1C,QAAA,GAAW,UAAA;EACX,OAAA,IAAW,KAAA,EAAO,KAAA;EAClB,QAAA;EACA,MAAA,IAAU,QAAA;IAAY,IAAA;IAAc,IAAA;EAAA;EAjBpC;;;;;;EAwBA,WAAA,GAAc,IAAA,CAAK,cAAA,CAAe,UAAA,GAAa,kBAAA;AAAA;;;;;;UAQhC,0BAAA;EAjBJ;;;;;EAuBX,UAAA;AAAA;;;;;;;AANF;;;;;AA0BA;;;;;;iBAAgB,mBAAA,oBAAuC,SAAA,CAAA,CACrD,IAAA,EAAM,IAAA,CAAK,cAAA,CAAe,UAAA,sCAC1B,OAAA,GAAU,0BAAA;;;;;;;;;;;;;;AAsCZ;;;;;;;;;;iBAAgB,mBAAA,oBAAuC,SAAA,GAAY,SAAA,CAAA,CACjE,OAAA,EAAS,0BAAA,CAA2B,UAAA,IACnC,cAAA,CAAe,UAAA"}
|
package/dist/react.js
CHANGED
|
@@ -1,14 +1,76 @@
|
|
|
1
|
-
import { useChat } from "@ai-sdk/react";
|
|
2
|
-
import { useRef,
|
|
1
|
+
import { Chat, useChat } from "@ai-sdk/react";
|
|
2
|
+
import { useRef, useEffect } from "react";
|
|
3
3
|
import { createSanityChatTransport } from "./chat-transport.js";
|
|
4
4
|
import { buildSendBody } from "./messages.js";
|
|
5
|
-
function
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
function getOrCreateThreadChat(cache, threadId, create) {
|
|
6
|
+
const existing = cache.get(threadId);
|
|
7
|
+
if (existing)
|
|
8
|
+
return cache.delete(threadId), cache.set(threadId, existing), existing;
|
|
9
|
+
const created = create();
|
|
10
|
+
return cache.set(threadId, created), created;
|
|
11
|
+
}
|
|
12
|
+
function pruneThreadChats(cache, keepId, max = 25) {
|
|
13
|
+
for (const key of [...cache.keys()]) {
|
|
14
|
+
if (cache.size <= max) break;
|
|
15
|
+
if (key === keepId) continue;
|
|
16
|
+
const evicted = cache.get(key);
|
|
17
|
+
cache.delete(key), evicted?.stop().catch(() => {
|
|
9
18
|
});
|
|
10
|
-
}
|
|
11
|
-
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function createThreadChat({
|
|
22
|
+
organizationId,
|
|
23
|
+
threadId,
|
|
24
|
+
baseURL,
|
|
25
|
+
apiVersion,
|
|
26
|
+
messages,
|
|
27
|
+
chatOptions,
|
|
28
|
+
latest
|
|
29
|
+
}) {
|
|
30
|
+
const transport = createSanityChatTransport({
|
|
31
|
+
organizationId,
|
|
32
|
+
threadId,
|
|
33
|
+
baseURL,
|
|
34
|
+
apiVersion,
|
|
35
|
+
token: () => resolve(latest.current.token),
|
|
36
|
+
prepareSendMessagesRequest: ({ messages: turnMessages, body, headers }) => {
|
|
37
|
+
const current = latest.current;
|
|
38
|
+
return {
|
|
39
|
+
headers: withRequestId(headers, turnMessages),
|
|
40
|
+
body: buildSendBody({
|
|
41
|
+
messages: turnMessages,
|
|
42
|
+
body,
|
|
43
|
+
application: resolve(current.application),
|
|
44
|
+
context: resolve(current.context),
|
|
45
|
+
format: current.format,
|
|
46
|
+
clientId: current.clientId
|
|
47
|
+
})
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
return new Chat({
|
|
52
|
+
...chatOptions,
|
|
53
|
+
id: threadId,
|
|
54
|
+
transport,
|
|
55
|
+
messages,
|
|
56
|
+
onError: (error) => latest.current.onError?.(error),
|
|
57
|
+
onFinish: () => latest.current.onFinish?.(),
|
|
58
|
+
onData: (dataPart) => latest.current.onData?.(dataPart)
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
function resolve(value) {
|
|
62
|
+
return typeof value == "function" ? value() : value;
|
|
63
|
+
}
|
|
64
|
+
function withRequestId(headers, messages) {
|
|
65
|
+
const merged = {};
|
|
66
|
+
new Headers(headers).forEach((value, key) => {
|
|
67
|
+
merged[key] = value;
|
|
68
|
+
});
|
|
69
|
+
const lastUser = [...messages].reverse().find((m) => m.role === "user");
|
|
70
|
+
return lastUser && (merged["x-request-id"] = lastUser.id), merged;
|
|
71
|
+
}
|
|
72
|
+
function useSanityChatResume(chat, options) {
|
|
73
|
+
const { autoResume = !1 } = options ?? {}, { id: threadId, status, resumeStream } = chat, resumeEvaluatedForRef = useRef(void 0);
|
|
12
74
|
useEffect(() => {
|
|
13
75
|
!autoResume || !threadId || status === "ready" && resumeEvaluatedForRef.current !== threadId && (resumeEvaluatedForRef.current = threadId, resumeStream());
|
|
14
76
|
}, [autoResume, threadId, status, resumeStream]);
|
|
@@ -29,54 +91,38 @@ function useContentAgentChat(options) {
|
|
|
29
91
|
onError,
|
|
30
92
|
onFinish,
|
|
31
93
|
onData
|
|
32
|
-
} = options, latest = useRef({
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
threadId,
|
|
39
|
-
baseURL,
|
|
40
|
-
apiVersion,
|
|
41
|
-
token: () => resolve(latest.current.token),
|
|
42
|
-
prepareSendMessagesRequest: ({ messages: turnMessages, body, headers }) => {
|
|
43
|
-
const current = latest.current;
|
|
44
|
-
return {
|
|
45
|
-
headers: withRequestId(headers, turnMessages),
|
|
46
|
-
body: buildSendBody({
|
|
47
|
-
messages: turnMessages,
|
|
48
|
-
body,
|
|
49
|
-
application: resolve(current.application),
|
|
50
|
-
context: resolve(current.context),
|
|
51
|
-
format: current.format,
|
|
52
|
-
clientId: current.clientId
|
|
53
|
-
})
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
});
|
|
57
|
-
}, [organizationId, threadId, baseURL, apiVersion]), chat = useChat({
|
|
58
|
-
...options.chatOptions,
|
|
59
|
-
id: threadId,
|
|
60
|
-
transport,
|
|
61
|
-
resume: !1,
|
|
62
|
-
messages,
|
|
94
|
+
} = options, latest = useRef({
|
|
95
|
+
token,
|
|
96
|
+
application,
|
|
97
|
+
context,
|
|
98
|
+
format,
|
|
99
|
+
clientId,
|
|
63
100
|
onError,
|
|
64
101
|
onFinish,
|
|
65
102
|
onData
|
|
66
103
|
});
|
|
104
|
+
latest.current = { token, application, context, format, clientId, onError, onFinish, onData };
|
|
105
|
+
const chatsRef = useRef(/* @__PURE__ */ new Map()), fallbackRef = useRef(null);
|
|
106
|
+
fallbackRef.current ??= new Chat({});
|
|
107
|
+
const threadChat = organizationId && threadId ? getOrCreateThreadChat(
|
|
108
|
+
chatsRef.current,
|
|
109
|
+
threadId,
|
|
110
|
+
() => createThreadChat({
|
|
111
|
+
organizationId,
|
|
112
|
+
threadId,
|
|
113
|
+
baseURL,
|
|
114
|
+
apiVersion,
|
|
115
|
+
messages,
|
|
116
|
+
chatOptions: options.chatOptions,
|
|
117
|
+
latest
|
|
118
|
+
})
|
|
119
|
+
) : fallbackRef.current;
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
pruneThreadChats(chatsRef.current, threadId);
|
|
122
|
+
}, [threadId]);
|
|
123
|
+
const chat = useChat({ chat: threadChat });
|
|
67
124
|
return useSanityChatResume(chat, { autoResume }), chat;
|
|
68
125
|
}
|
|
69
|
-
function resolve(value) {
|
|
70
|
-
return typeof value == "function" ? value() : value;
|
|
71
|
-
}
|
|
72
|
-
function withRequestId(headers, messages) {
|
|
73
|
-
const merged = {};
|
|
74
|
-
new Headers(headers).forEach((value, key) => {
|
|
75
|
-
merged[key] = value;
|
|
76
|
-
});
|
|
77
|
-
const lastUser = [...messages].reverse().find((m) => m.role === "user");
|
|
78
|
-
return lastUser && (merged["x-request-id"] = lastUser.id), merged;
|
|
79
|
-
}
|
|
80
126
|
export {
|
|
81
127
|
useContentAgentChat,
|
|
82
128
|
useSanityChatResume
|
package/dist/react.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"react.js","sources":["../src/react.ts"],"sourcesContent":["import { type UseChatHelpers, type UseChatOptions, useChat } from '@ai-sdk/react'\nimport type { Application, Context } from '@sanity/agent-types'\nimport type { UIMessage } from 'ai'\nimport { useEffect, useMemo, useRef } from 'react'\nimport { createSanityChatTransport } from './_internal/chat-transport'\nimport { buildSendBody } from './_internal/messages'\nimport type { OutputFormat } from './types'\n\n/** `useChat` options that {@link useContentAgentChat} owns; the rest pass through. */\ntype ManagedChatOptions =\n | 'id'\n | 'transport'\n | 'resume'\n | 'messages'\n | 'onError'\n | 'onFinish'\n | 'onData'\n\n/** A value, or a getter resolved at send time (avoids stale closures). */\ntype Resolvable<T> = T | (() => T | undefined)\n\n/**\n * Options for {@link useContentAgentChat}.\n *\n * @category React\n */\nexport interface UseContentAgentChatOptions<UI_MESSAGE extends UIMessage = UIMessage> {\n /** Sanity organization ID. When undefined, no transport is built. */\n organizationId: string | undefined\n /** Stable thread id. When undefined, no transport is built. */\n threadId: string | undefined\n /**\n * Base URL for the Sanity API.\n * @default 'https://api.sanity.io'\n */\n baseURL?: string\n /** API version segment, e.g. `'v2026-01-12'`. */\n apiVersion?: string\n /** Bearer token, or a getter read at request time. */\n token?: string | (() => string | undefined)\n /** Target application; pass a getter to read it fresh on each turn. */\n application?: Resolvable<Application>\n /** Per-turn request context; pass a getter to read it fresh on each turn. */\n context?: Resolvable<Context>\n /** Directive output format (`'directives'` for custom client rendering). */\n format?: OutputFormat\n /** Identifies the creating client (e.g. `'dashboard'`). */\n clientId?: string\n /**\n * When true, reconnect to an in-flight stream once per thread (page reload\n * or thread switch). The SDK's `resume: true` only fires on first mount, so\n * this hook drives resume per (Chat x threadId) pair instead.\n */\n autoResume?: boolean\n /** Initial messages (e.g. server-rendered history). */\n messages?: UI_MESSAGE[]\n onError?: (error: Error) => void\n onFinish?: () => void\n onData?: (dataPart: { type: string; data: unknown }) => void\n /**\n * Any other `useChat` options to forward (e.g. `onToolCall`, `generateId`,\n * `experimental_throttle`). The options this hook manages (transport, id,\n * resume, messages, the callbacks above) take precedence.\n */\n chatOptions?: Omit<UseChatOptions<UI_MESSAGE>, ManagedChatOptions>\n}\n\n/**\n * Options for {@link useSanityChatResume}.\n *\n * @category React\n */\nexport interface UseSanityChatResumeOptions {\n /**\n * When true, reconnect to an in-flight stream once per thread (page reload\n * or thread switch). The SDK's `resume: true` only fires on first mount, so\n * this drives resume per (Chat x threadId) pair instead.\n */\n autoResume?: boolean\n}\n\n/**\n * Add Sanity resume semantics to a `useChat` instance you own: reconnect to an\n * in-flight stream once per thread, and abort the previous Chat's stream when\n * the thread changes. Use this when you need full control of the `useChat`\n * call; {@link useContentAgentChat} composes it for the turnkey path.\n *\n * @category React\n *\n * @example\n * ```tsx\n * const transport = useMemo(() => createSanityChatTransport({ ... }), [...])\n * const chat = useChat({ id: threadId, transport, onToolCall })\n * useSanityChatResume(chat, { autoResume: threadExistsOnBackend })\n * ```\n */\nexport function useSanityChatResume<UI_MESSAGE extends UIMessage>(\n chat: Pick<UseChatHelpers<UI_MESSAGE>, 'id' | 'status' | 'stop' | 'resumeStream'>,\n options?: UseSanityChatResumeOptions,\n): void {\n const { autoResume = false } = options ?? {}\n const { id: threadId, status, stop, resumeStream } = chat\n\n // Stop the previous Chat when the thread changes. The SDK recreates the Chat\n // on `id` change but leaves the old fetch running; the cleanup closure\n // captures the prior `stop`, so it aborts the OLD Chat.\n useEffect(() => {\n return () => void stop().catch(() => {})\n }, [stop])\n\n // Resume any in-flight stream once per (Chat x threadId). Skipped unless idle\n // so a concurrent send and resume don't collide.\n const resumeEvaluatedForRef = useRef<string | undefined>(undefined)\n useEffect(() => {\n if (!autoResume || !threadId) return\n if (status !== 'ready') return\n if (resumeEvaluatedForRef.current === threadId) return\n resumeEvaluatedForRef.current = threadId\n void resumeStream()\n }, [autoResume, threadId, status, resumeStream])\n}\n\n/**\n * `useChat` wired to the Sanity Content Agent: transport with bearer auth and\n * transparent resume, per-turn body shaping (last message wire-shaped, plus\n * application/context/format/clientId), `x-request-id` idempotency, resume on\n * thread change, and stream cleanup when the thread changes.\n *\n * Returns the standard `useChat` helpers, so messages, status, sendMessage,\n * stop, resumeStream, and setMessages all behave exactly as upstream.\n *\n * @category React\n *\n * @example\n * ```tsx\n * const chat = useContentAgentChat({\n * organizationId,\n * threadId,\n * token: () => client.config().token,\n * application,\n * autoResume: threadExistsOnBackend,\n * })\n * ```\n */\nexport function useContentAgentChat<UI_MESSAGE extends UIMessage = UIMessage>(\n options: UseContentAgentChatOptions<UI_MESSAGE>,\n): UseChatHelpers<UI_MESSAGE> {\n const {\n organizationId,\n threadId,\n baseURL,\n apiVersion,\n token,\n application,\n context,\n format,\n clientId,\n autoResume = false,\n messages,\n onError,\n onFinish,\n onData,\n } = options\n\n // Per-turn fields are read fresh on each send via a latest-ref so the\n // transport (and the Chat behind it) only rebuilds on URL identity, never\n // when the token refreshes or the user switches application/context.\n const latest = useRef({ token, application, context, format, clientId })\n latest.current = { token, application, context, format, clientId }\n\n const transport = useMemo(() => {\n if (!organizationId || !threadId) return undefined\n\n return createSanityChatTransport<UI_MESSAGE>({\n organizationId,\n threadId,\n baseURL,\n apiVersion,\n token: () => resolve(latest.current.token),\n prepareSendMessagesRequest: ({ messages: turnMessages, body, headers }) => {\n const current = latest.current\n return {\n headers: withRequestId(headers, turnMessages),\n body: buildSendBody({\n messages: turnMessages,\n body,\n application: resolve(current.application),\n context: resolve(current.context),\n format: current.format,\n clientId: current.clientId,\n }),\n }\n },\n })\n }, [organizationId, threadId, baseURL, apiVersion])\n\n const chat = useChat<UI_MESSAGE>({\n ...options.chatOptions,\n id: threadId,\n transport,\n resume: false,\n messages,\n onError,\n onFinish,\n onData,\n })\n\n useSanityChatResume(chat, { autoResume })\n\n return chat\n}\n\nfunction resolve<T>(value: Resolvable<T> | undefined): T | undefined {\n return typeof value === 'function' ? (value as () => T | undefined)() : value\n}\n\n/** Spread the SDK's base headers and stamp `x-request-id` from the last user\n * turn so the optimistic SDK message and the server share one id. */\nfunction withRequestId(headers: HeadersInit | undefined, messages: UIMessage[]): HeadersInit {\n const merged: Record<string, string> = {}\n new Headers(headers).forEach((value, key) => {\n merged[key] = value\n })\n const lastUser = [...messages].reverse().find((m) => m.role === 'user')\n if (lastUser) merged['x-request-id'] = lastUser.id\n return merged\n}\n"],"names":[],"mappings":";;;;AAgGO,SAAS,oBACd,MACA,SACM;AACN,QAAM,EAAE,aAAa,GAAA,IAAU,WAAW,CAAA,GACpC,EAAE,IAAI,UAAU,QAAQ,MAAM,iBAAiB;AAKrD,YAAU,MACD,MAAG;AAAQ,SAAA,EAAO,MAAM,MAAM;AAAA,IAAC,CAAC;AAAA,EAAA,GACtC,CAAC,IAAI,CAAC;AAIT,QAAM,wBAAwB,OAA2B,MAAS;AAClE,YAAU,MAAM;AACV,KAAC,cAAc,CAAC,YAChB,WAAW,WACX,sBAAsB,YAAY,aACtC,sBAAsB,UAAU,UAC3B,aAAA;AAAA,EACP,GAAG,CAAC,YAAY,UAAU,QAAQ,YAAY,CAAC;AACjD;AAwBO,SAAS,oBACd,SAC4B;AAC5B,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,aAAa;AAAA,IACb;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EAAA,IACE,SAKE,SAAS,OAAO,EAAE,OAAO,aAAa,SAAS,QAAQ,UAAU;AACvE,SAAO,UAAU,EAAE,OAAO,aAAa,SAAS,QAAQ,SAAA;AAExD,QAAM,YAAY,QAAQ,MAAM;AAC9B,QAAI,EAAA,CAAC,kBAAkB,CAAC;AAExB,aAAO,0BAAsC;AAAA,QAC3C;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA,OAAO,MAAM,QAAQ,OAAO,QAAQ,KAAK;AAAA,QACzC,4BAA4B,CAAC,EAAE,UAAU,cAAc,MAAM,cAAc;AACzE,gBAAM,UAAU,OAAO;AACvB,iBAAO;AAAA,YACL,SAAS,cAAc,SAAS,YAAY;AAAA,YAC5C,MAAM,cAAc;AAAA,cAClB,UAAU;AAAA,cACV;AAAA,cACA,aAAa,QAAQ,QAAQ,WAAW;AAAA,cACxC,SAAS,QAAQ,QAAQ,OAAO;AAAA,cAChC,QAAQ,QAAQ;AAAA,cAChB,UAAU,QAAQ;AAAA,YAAA,CACnB;AAAA,UAAA;AAAA,QAEL;AAAA,MAAA,CACD;AAAA,EACH,GAAG,CAAC,gBAAgB,UAAU,SAAS,UAAU,CAAC,GAE5C,OAAO,QAAoB;AAAA,IAC/B,GAAG,QAAQ;AAAA,IACX,IAAI;AAAA,IACJ;AAAA,IACA,QAAQ;AAAA,IACR;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EAAA,CACD;AAED,SAAA,oBAAoB,MAAM,EAAE,WAAA,CAAY,GAEjC;AACT;AAEA,SAAS,QAAW,OAAiD;AACnE,SAAO,OAAO,SAAU,aAAc,MAAA,IAAkC;AAC1E;AAIA,SAAS,cAAc,SAAkC,UAAoC;AAC3F,QAAM,SAAiC,CAAA;AACvC,MAAI,QAAQ,OAAO,EAAE,QAAQ,CAAC,OAAO,QAAQ;AAC3C,WAAO,GAAG,IAAI;AAAA,EAChB,CAAC;AACD,QAAM,WAAW,CAAC,GAAG,QAAQ,EAAE,QAAA,EAAU,KAAK,CAAC,MAAM,EAAE,SAAS,MAAM;AACtE,SAAI,aAAU,OAAO,cAAc,IAAI,SAAS,KACzC;AACT;"}
|
|
1
|
+
{"version":3,"file":"react.js","sources":["../src/_internal/chat-cache.ts","../src/_internal/create-thread-chat.ts","../src/react.ts"],"sourcesContent":["import type { Chat } from '@ai-sdk/react'\nimport type { UIMessage } from 'ai'\n\n/** Cap on cached per-thread Chat instances, to bound per-session memory. */\nexport const MAX_CACHED_CHATS = 25\n\n/**\n * Get the cached Chat for a thread, creating it via `create` on a miss.\n *\n * This is the building block that lets a thread survive being switched away\n * from: `useChat` rebuilds an empty Chat whenever its `id` changes, so the\n * turnkey hook holds one Chat per thread here and hands the same instance back\n * on return, preserving its messages and its still-running stream (SAGE-502).\n *\n * The accessed/created thread becomes most-recently-used (Map preserves\n * insertion order). Render-safe: it never aborts a stream. Call\n * {@link pruneThreadChats} from an effect to bound the cache.\n */\nexport function getOrCreateThreadChat<UI_MESSAGE extends UIMessage>(\n cache: Map<string, Chat<UI_MESSAGE>>,\n threadId: string,\n create: () => Chat<UI_MESSAGE>,\n): Chat<UI_MESSAGE> {\n const existing = cache.get(threadId)\n if (existing) {\n cache.delete(threadId)\n cache.set(threadId, existing)\n return existing\n }\n\n const created = create()\n cache.set(threadId, created)\n return created\n}\n\n/**\n * Evict least-recently-used chats beyond `max`, stopping each to free its\n * connection, while keeping `keepId`.\n *\n * Call this from an effect, never during render: `stop()` aborts a fetch, and\n * render may run speculatively.\n */\nexport function pruneThreadChats<UI_MESSAGE extends UIMessage>(\n cache: Map<string, Chat<UI_MESSAGE>>,\n keepId: string | undefined,\n max: number = MAX_CACHED_CHATS,\n): void {\n for (const key of [...cache.keys()]) {\n if (cache.size <= max) break\n if (key === keepId) continue\n const evicted = cache.get(key)\n cache.delete(key)\n void evicted?.stop().catch(() => {})\n }\n}\n","import { Chat, type UseChatOptions } from '@ai-sdk/react'\nimport type { Application, Context } from '@sanity/agent-types'\nimport type { UIMessage } from 'ai'\nimport type { RefObject } from 'react'\nimport type { OutputFormat } from '../types'\nimport { createSanityChatTransport } from './chat-transport'\nimport { buildSendBody } from './messages'\n\n/** A value, or a getter resolved at send time (avoids stale closures). */\nexport type Resolvable<T> = T | (() => T | undefined)\n\n/** `useChat` options that {@link useContentAgentChat} owns; the rest pass through. */\nexport type ManagedChatOptions =\n | 'id'\n | 'transport'\n | 'resume'\n | 'messages'\n | 'onError'\n | 'onFinish'\n | 'onData'\n\n/**\n * Per-turn config and callbacks read fresh on each send/event via a ref, so a\n * Chat created once per thread still sees the current values rather than the\n * ones captured when it was created.\n */\nexport interface LatestConfig {\n token?: string | (() => string | undefined)\n application?: Resolvable<Application>\n context?: Resolvable<Context>\n format?: OutputFormat\n clientId?: string\n onError?: (error: Error) => void\n onFinish?: () => void\n onData?: (dataPart: { type: string; data: unknown }) => void\n}\n\nexport interface CreateThreadChatArgs<UI_MESSAGE extends UIMessage> {\n organizationId: string\n threadId: string\n baseURL?: string\n apiVersion?: string\n messages?: UI_MESSAGE[]\n chatOptions?: Omit<UseChatOptions<UI_MESSAGE>, ManagedChatOptions>\n latest: RefObject<LatestConfig>\n}\n\n/**\n * Build the `Chat` for one thread: a Sanity transport (bearer auth, per-turn\n * body shaping, `x-request-id` idempotency) plus callbacks that read the latest\n * config. Called once per thread; the instance is then cached and reused.\n */\nexport function createThreadChat<UI_MESSAGE extends UIMessage>({\n organizationId,\n threadId,\n baseURL,\n apiVersion,\n messages,\n chatOptions,\n latest,\n}: CreateThreadChatArgs<UI_MESSAGE>): Chat<UI_MESSAGE> {\n const transport = createSanityChatTransport<UI_MESSAGE>({\n organizationId,\n threadId,\n baseURL,\n apiVersion,\n token: () => resolve(latest.current.token),\n prepareSendMessagesRequest: ({ messages: turnMessages, body, headers }) => {\n const current = latest.current\n return {\n headers: withRequestId(headers, turnMessages),\n body: buildSendBody({\n messages: turnMessages,\n body,\n application: resolve(current.application),\n context: resolve(current.context),\n format: current.format,\n clientId: current.clientId,\n }),\n }\n },\n })\n\n return new Chat<UI_MESSAGE>({\n ...chatOptions,\n id: threadId,\n transport,\n messages,\n onError: (error) => latest.current.onError?.(error),\n onFinish: () => latest.current.onFinish?.(),\n onData: (dataPart) => latest.current.onData?.(dataPart),\n })\n}\n\nfunction resolve<T>(value: Resolvable<T> | undefined): T | undefined {\n return typeof value === 'function' ? (value as () => T | undefined)() : value\n}\n\n/** Spread the SDK's base headers and stamp `x-request-id` from the last user\n * turn so the optimistic SDK message and the server share one id. */\nfunction withRequestId(headers: HeadersInit | undefined, messages: UIMessage[]): HeadersInit {\n const merged: Record<string, string> = {}\n new Headers(headers).forEach((value, key) => {\n merged[key] = value\n })\n const lastUser = [...messages].reverse().find((m) => m.role === 'user')\n if (lastUser) merged['x-request-id'] = lastUser.id\n return merged\n}\n","import { Chat, type UseChatHelpers, type UseChatOptions, useChat } from '@ai-sdk/react'\nimport type { Application, Context } from '@sanity/agent-types'\nimport type { UIMessage } from 'ai'\nimport { useEffect, useRef } from 'react'\nimport { getOrCreateThreadChat, pruneThreadChats } from './_internal/chat-cache'\nimport {\n createThreadChat,\n type LatestConfig,\n type ManagedChatOptions,\n type Resolvable,\n} from './_internal/create-thread-chat'\nimport type { OutputFormat } from './types'\n\n/**\n * Options for {@link useContentAgentChat}.\n *\n * @category React\n */\nexport interface UseContentAgentChatOptions<UI_MESSAGE extends UIMessage = UIMessage> {\n /** Sanity organization ID. When undefined, no transport is built. */\n organizationId: string | undefined\n /** Stable thread id. When undefined, no transport is built. */\n threadId: string | undefined\n /**\n * Base URL for the Sanity API.\n * @default 'https://api.sanity.io'\n */\n baseURL?: string\n /** API version segment, e.g. `'v2026-01-12'`. */\n apiVersion?: string\n /** Bearer token, or a getter read at request time. */\n token?: string | (() => string | undefined)\n /** Target application; pass a getter to read it fresh on each turn. */\n application?: Resolvable<Application>\n /** Per-turn request context; pass a getter to read it fresh on each turn. */\n context?: Resolvable<Context>\n /** Directive output format (`'directives'` for custom client rendering). */\n format?: OutputFormat\n /** Identifies the creating client (e.g. `'dashboard'`). */\n clientId?: string\n /**\n * When true, reconnect to an in-flight stream once per thread (page reload).\n * The SDK's `resume: true` only fires on first mount, so this hook drives\n * resume per (Chat x threadId) pair instead.\n */\n autoResume?: boolean\n /** Initial messages for a thread's first mount (e.g. server-rendered history). */\n messages?: UI_MESSAGE[]\n onError?: (error: Error) => void\n onFinish?: () => void\n onData?: (dataPart: { type: string; data: unknown }) => void\n /**\n * Any other `useChat`/`Chat` options to forward (e.g. `onToolCall`,\n * `generateId`, `sendAutomaticallyWhen`). The options this hook manages\n * (transport, id, messages, the callbacks above) take precedence. These are\n * read once when a thread's Chat is created.\n */\n chatOptions?: Omit<UseChatOptions<UI_MESSAGE>, ManagedChatOptions>\n}\n\n/**\n * Options for {@link useSanityChatResume}.\n *\n * @category React\n */\nexport interface UseSanityChatResumeOptions {\n /**\n * When true, reconnect to an in-flight stream once per thread. The SDK's\n * `resume: true` only fires on first mount, so this drives resume per\n * (Chat x threadId) pair instead.\n */\n autoResume?: boolean\n}\n\n/**\n * Reconnect to an in-flight stream once per (Chat x threadId), when idle.\n *\n * Does not stop the stream on thread change or unmount: per the AI SDK\n * resumable-stream model, client-side aborts are disconnects that should stay\n * resumable, not cancellations. Stopping is an explicit user action (call\n * `chat.stop()` and/or a server stop endpoint), never route or unmount\n * cleanup. See https://ai-sdk.dev/docs/troubleshooting/abort-breaks-resumable-streams.\n *\n * @category React\n *\n * @example\n * ```tsx\n * const chat = useChat({ chat: myThreadChat })\n * useSanityChatResume(chat, { autoResume: threadExistsOnBackend })\n * ```\n */\nexport function useSanityChatResume<UI_MESSAGE extends UIMessage>(\n chat: Pick<UseChatHelpers<UI_MESSAGE>, 'id' | 'status' | 'resumeStream'>,\n options?: UseSanityChatResumeOptions,\n): void {\n const { autoResume = false } = options ?? {}\n const { id: threadId, status, resumeStream } = chat\n\n const resumeEvaluatedForRef = useRef<string | undefined>(undefined)\n useEffect(() => {\n if (!autoResume || !threadId) return\n if (status !== 'ready') return\n if (resumeEvaluatedForRef.current === threadId) return\n resumeEvaluatedForRef.current = threadId\n void resumeStream()\n }, [autoResume, threadId, status, resumeStream])\n}\n\n/**\n * `useChat` wired to the Sanity Content Agent: bearer auth, per-turn body\n * shaping (application/context/format/clientId), `x-request-id` idempotency,\n * and resume on reload.\n *\n * Holds one `Chat` instance per thread (the AI SDK's recommended pattern for\n * multiple conversations), so switching threads mid-stream and back keeps the\n * thread's messages and its still-running stream instead of rebuilding an empty\n * Chat. Returns the standard `useChat` helpers.\n *\n * @category React\n *\n * @example\n * ```tsx\n * const chat = useContentAgentChat({\n * organizationId,\n * threadId,\n * token: () => client.config().token,\n * application,\n * autoResume: threadExistsOnBackend,\n * })\n * ```\n */\nexport function useContentAgentChat<UI_MESSAGE extends UIMessage = UIMessage>(\n options: UseContentAgentChatOptions<UI_MESSAGE>,\n): UseChatHelpers<UI_MESSAGE> {\n const {\n organizationId,\n threadId,\n baseURL,\n apiVersion,\n token,\n application,\n context,\n format,\n clientId,\n autoResume = false,\n messages,\n onError,\n onFinish,\n onData,\n } = options\n\n const latest = useRef<LatestConfig>({\n token,\n application,\n context,\n format,\n clientId,\n onError,\n onFinish,\n onData,\n })\n latest.current = { token, application, context, format, clientId, onError, onFinish, onData }\n\n // One Chat per thread, retained across switches (see getOrCreateThreadChat).\n const chatsRef = useRef(new Map<string, Chat<UI_MESSAGE>>())\n // Stable placeholder used until a thread exists, so the hook's shape holds.\n const fallbackRef = useRef<Chat<UI_MESSAGE> | null>(null)\n fallbackRef.current ??= new Chat<UI_MESSAGE>({})\n\n const threadChat =\n organizationId && threadId\n ? getOrCreateThreadChat(chatsRef.current, threadId, () =>\n createThreadChat<UI_MESSAGE>({\n organizationId,\n threadId,\n baseURL,\n apiVersion,\n messages,\n chatOptions: options.chatOptions,\n latest,\n }),\n )\n : fallbackRef.current\n\n // Bound the cache after commit, not in render: pruning aborts evicted streams.\n useEffect(() => {\n pruneThreadChats(chatsRef.current, threadId)\n }, [threadId])\n\n const chat = useChat<UI_MESSAGE>({ chat: threadChat })\n\n useSanityChatResume(chat, { autoResume })\n\n return chat\n}\n"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "content-agent",
|
|
3
|
-
"version": "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.
|
|
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,7 +58,7 @@
|
|
|
58
58
|
"react-dom": "^19.0.0",
|
|
59
59
|
"typedoc": "^0.28.17",
|
|
60
60
|
"typescript": "^5.9.3",
|
|
61
|
-
"vitest": "^4.
|
|
61
|
+
"vitest": "^4.1.10",
|
|
62
62
|
"@repo/package.config": "0.0.0",
|
|
63
63
|
"@repo/tsconfig": "0.0.0"
|
|
64
64
|
},
|