content-agent 1.3.1 → 1.4.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/react.d.ts +5 -0
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +4 -0
- package/dist/react.js.map +1 -1
- package/package.json +2 -2
package/dist/react.d.ts
CHANGED
|
@@ -25,6 +25,11 @@ interface UseContentAgentChatOptions<UI_MESSAGE extends UIMessage = UIMessage> {
|
|
|
25
25
|
apiVersion?: string;
|
|
26
26
|
/** Bearer token, or a getter read at request time. */
|
|
27
27
|
token?: string | (() => string | undefined);
|
|
28
|
+
/**
|
|
29
|
+
* Fetch credentials mode for chat requests. Pass `'omit'` to authenticate with `token` only.
|
|
30
|
+
* @default 'include'
|
|
31
|
+
*/
|
|
32
|
+
credentials?: RequestCredentials;
|
|
28
33
|
/** Target application; pass a getter to read it fresh on each turn. */
|
|
29
34
|
application?: Resolvable<Application>;
|
|
30
35
|
/** Per-turn request context; pass a getter to read it fresh on each turn. */
|
package/dist/react.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
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;
|
|
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;;;;EAkBzE,WAAA,GAAc,kBAAA;EAIJ;EAFV,WAAA,GAAc,UAAA,CAAW,WAAA;EAcd;EAZX,OAAA,GAAU,UAAA,CAAW,OAAA;EAsBa;EApBlC,MAAA,GAAS,YAAA;EAoBsC;EAlB/C,QAAA;EAkBkB;;;;;EAZlB,UAAA;EA5BA;EA8BA,QAAA,GAAW,UAAA;EACX,OAAA,IAAW,KAAA,EAAO,KAAA;EAClB,QAAA;EACA,MAAA,IAAU,QAAA;IAAY,IAAA;IAAc,IAAA;EAAA;EAjBX;;;;;;EAwBzB,WAAA,GAAc,IAAA,CAAK,cAAA,CAAe,UAAA,GAAa,kBAAA;AAAA;;;;;;UAQhC,0BAAA;EAff;;;;;EAqBA,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
|
@@ -23,6 +23,7 @@ function createThreadChat({
|
|
|
23
23
|
threadId,
|
|
24
24
|
baseURL,
|
|
25
25
|
apiVersion,
|
|
26
|
+
credentials,
|
|
26
27
|
messages,
|
|
27
28
|
chatOptions,
|
|
28
29
|
latest
|
|
@@ -32,6 +33,7 @@ function createThreadChat({
|
|
|
32
33
|
threadId,
|
|
33
34
|
baseURL,
|
|
34
35
|
apiVersion,
|
|
36
|
+
credentials,
|
|
35
37
|
token: () => resolve(latest.current.token),
|
|
36
38
|
prepareSendMessagesRequest: ({ messages: turnMessages, body, headers }) => {
|
|
37
39
|
const current = latest.current;
|
|
@@ -82,6 +84,7 @@ function useContentAgentChat(options) {
|
|
|
82
84
|
baseURL,
|
|
83
85
|
apiVersion,
|
|
84
86
|
token,
|
|
87
|
+
credentials,
|
|
85
88
|
application,
|
|
86
89
|
context,
|
|
87
90
|
format,
|
|
@@ -112,6 +115,7 @@ function useContentAgentChat(options) {
|
|
|
112
115
|
threadId,
|
|
113
116
|
baseURL,
|
|
114
117
|
apiVersion,
|
|
118
|
+
credentials,
|
|
115
119
|
messages,
|
|
116
120
|
chatOptions: options.chatOptions,
|
|
117
121
|
latest
|
package/dist/react.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
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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"}
|
|
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 credentials?: RequestCredentials\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 credentials,\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 credentials,\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 /**\n * Fetch credentials mode for chat requests. Pass `'omit'` to authenticate with `token` only.\n * @default 'include'\n */\n credentials?: RequestCredentials\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 credentials,\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 credentials,\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.4.0",
|
|
4
4
|
"description": "Vercel AI SDK provider for Sanity Content Agent API",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"sanity",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"@testing-library/react": "^16.0.0",
|
|
52
52
|
"@types/node": "^22.19.7",
|
|
53
53
|
"@types/react": "^19.0.0",
|
|
54
|
-
"ai": "^6.0.
|
|
54
|
+
"ai": "^6.0.286",
|
|
55
55
|
"jsdom": "^26.0.0",
|
|
56
56
|
"openapi-typescript": "^7.10.1",
|
|
57
57
|
"react": "^19.0.0",
|