content-agent 1.2.0 → 1.2.1
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 +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/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.2.
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"description": "Vercel AI SDK provider for Sanity Content Agent API",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"sanity",
|
|
@@ -59,8 +59,8 @@
|
|
|
59
59
|
"typedoc": "^0.28.17",
|
|
60
60
|
"typescript": "^5.9.3",
|
|
61
61
|
"vitest": "^4.0.18",
|
|
62
|
-
"@repo/
|
|
63
|
-
"@repo/
|
|
62
|
+
"@repo/tsconfig": "0.0.0",
|
|
63
|
+
"@repo/package.config": "0.0.0"
|
|
64
64
|
},
|
|
65
65
|
"peerDependencies": {
|
|
66
66
|
"@ai-sdk/react": ">=3.0.0",
|