@tanstack/ai-solid 0.7.2 → 0.9.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/README.md CHANGED
@@ -71,7 +71,10 @@ interface UseChatOptions {
71
71
  // Configuration
72
72
  initialMessages?: UIMessage[] // Starting messages
73
73
  id?: string // Unique chat ID
74
- body?: Record<string, any> // Extra data to send
74
+ threadId?: string // AG-UI thread ID (auto-generated if omitted)
75
+ forwardedProps?: Record<string, any> // Forwarded to AG-UI RunAgentInput.forwardedProps
76
+ /** @deprecated Use `forwardedProps` instead. */
77
+ body?: Record<string, any>
75
78
 
76
79
  // Callbacks
77
80
  onResponse?: (response?: Response) => void
@@ -256,7 +259,7 @@ const chat = useChat({
256
259
  'X-Custom-Header': 'value',
257
260
  },
258
261
  }),
259
- body: {
262
+ forwardedProps: {
260
263
  userId: '123',
261
264
  sessionId: 'abc',
262
265
  },
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { ChatRequestBody, UIMessage, UseChatOptions, UseChatReturn } from "./types.js";
1
+ import { ChatRequestBody, DeepPartial, UIMessage, UseChatOptions, UseChatReturn } from "./types.js";
2
2
  import { useChat } from "./use-chat.js";
3
3
  import { UseGenerationOptions, UseGenerationReturn, useGeneration } from "./use-generation.js";
4
4
  import { UseGenerateImageOptions, UseGenerateImageReturn, useGenerateImage } from "./use-generate-image.js";
@@ -8,4 +8,4 @@ import { UseTranscriptionOptions, UseTranscriptionReturn, useTranscription } fro
8
8
  import { UseSummarizeOptions, UseSummarizeReturn, useSummarize } from "./use-summarize.js";
9
9
  import { UseGenerateVideoOptions, UseGenerateVideoReturn, useGenerateVideo } from "./use-generate-video.js";
10
10
  import { AudioGenerateInput, ConnectionAdapter, FetchConnectionOptions, GenerationClientState, ImageGenerateInput, InferChatMessages, SpeechGenerateInput, SummarizeGenerateInput, TranscriptionGenerateInput, VideoGenerateInput, VideoGenerateResult, VideoStatusInfo, createChatClientOptions, fetchHttpStream, fetchServerSentEvents, stream } from "@tanstack/ai-client";
11
- export { type AudioGenerateInput, type ChatRequestBody, type ConnectionAdapter, type FetchConnectionOptions, type GenerationClientState, type ImageGenerateInput, type InferChatMessages, type SpeechGenerateInput, type SummarizeGenerateInput, type TranscriptionGenerateInput, type UIMessage, type UseChatOptions, type UseChatReturn, type UseGenerateAudioOptions, type UseGenerateAudioReturn, type UseGenerateImageOptions, type UseGenerateImageReturn, type UseGenerateSpeechOptions, type UseGenerateSpeechReturn, type UseGenerateVideoOptions, type UseGenerateVideoReturn, type UseGenerationOptions, type UseGenerationReturn, type UseSummarizeOptions, type UseSummarizeReturn, type UseTranscriptionOptions, type UseTranscriptionReturn, type VideoGenerateInput, type VideoGenerateResult, type VideoStatusInfo, createChatClientOptions, fetchHttpStream, fetchServerSentEvents, stream, useChat, useGenerateAudio, useGenerateImage, useGenerateSpeech, useGenerateVideo, useGeneration, useSummarize, useTranscription };
11
+ export { type AudioGenerateInput, type ChatRequestBody, type ConnectionAdapter, type DeepPartial, type FetchConnectionOptions, type GenerationClientState, type ImageGenerateInput, type InferChatMessages, type SpeechGenerateInput, type SummarizeGenerateInput, type TranscriptionGenerateInput, type UIMessage, type UseChatOptions, type UseChatReturn, type UseGenerateAudioOptions, type UseGenerateAudioReturn, type UseGenerateImageOptions, type UseGenerateImageReturn, type UseGenerateSpeechOptions, type UseGenerateSpeechReturn, type UseGenerateVideoOptions, type UseGenerateVideoReturn, type UseGenerationOptions, type UseGenerationReturn, type UseSummarizeOptions, type UseSummarizeReturn, type UseTranscriptionOptions, type UseTranscriptionReturn, type VideoGenerateInput, type VideoGenerateResult, type VideoStatusInfo, createChatClientOptions, fetchHttpStream, fetchServerSentEvents, stream, useChat, useGenerateAudio, useGenerateImage, useGenerateSpeech, useGenerateVideo, useGeneration, useSummarize, useTranscription };
package/dist/types.d.ts CHANGED
@@ -1,9 +1,15 @@
1
1
  import { Accessor } from "solid-js";
2
2
  import { ChatClientOptions, ChatClientState, ChatRequestBody, ConnectionStatus, MultimodalContent, UIMessage } from "@tanstack/ai-client";
3
- import { AnyClientTool, ModelMessage } from "@tanstack/ai";
3
+ import { AnyClientTool, InferSchemaType, ModelMessage, SchemaInput } from "@tanstack/ai";
4
4
 
5
5
  //#region src/types.d.ts
6
6
 
7
+ /**
8
+ * Recursive partial — every property and every nested array element is optional.
9
+ * Used to type the in-flight `partial` accessor while a structured-output
10
+ * stream is still arriving.
11
+ */
12
+ type DeepPartial<T> = T extends ReadonlyArray<infer U> ? Array<DeepPartial<U>> : T extends object ? { [K in keyof T]?: DeepPartial<T[K]> } : T;
7
13
  /**
8
14
  * Options for the useChat hook.
9
15
  *
@@ -17,13 +23,38 @@ import { AnyClientTool, ModelMessage } from "@tanstack/ai";
17
23
  * All other callbacks (onResponse, onChunk, onFinish, onError) are
18
24
  * passed through to the underlying ChatClient and can be used for side effects.
19
25
  *
26
+ * When `outputSchema` is supplied, the hook returns typed `partial` and `final`
27
+ * accessors. The schema is used purely for type inference; server-side
28
+ * validation still runs against the schema passed to `chat({ outputSchema })`
29
+ * on the server route.
30
+ *
20
31
  * Note: Connection and body changes will recreate the ChatClient instance.
21
32
  * To update these options, remount the component or use a key prop.
22
33
  */
23
- type UseChatOptions<TTools extends ReadonlyArray<AnyClientTool> = any> = Omit<ChatClientOptions<TTools>, 'onMessagesChange' | 'onLoadingChange' | 'onErrorChange' | 'onStatusChange' | 'onSubscriptionChange' | 'onConnectionStatusChange' | 'onSessionGeneratingChange'> & {
34
+ type UseChatOptions<TTools extends ReadonlyArray<AnyClientTool> = any, TSchema extends SchemaInput | undefined = undefined> = Omit<ChatClientOptions<TTools>, 'onMessagesChange' | 'onLoadingChange' | 'onErrorChange' | 'onStatusChange' | 'onSubscriptionChange' | 'onConnectionStatusChange' | 'onSessionGeneratingChange'> & {
24
35
  live?: boolean;
36
+ /**
37
+ * Standard-schema-compatible schema (Zod, Valibot, ArkType, or plain JSON
38
+ * Schema). Used to infer the shape of `partial` and `final`.
39
+ */
40
+ outputSchema?: TSchema;
25
41
  };
26
- interface UseChatReturn<TTools extends ReadonlyArray<AnyClientTool> = any> {
42
+ /**
43
+ * Discriminated return shape: when `outputSchema` is supplied, the hook adds
44
+ * typed `partial` / `final` accessors; otherwise the return is unchanged.
45
+ */
46
+ type UseChatReturn<TTools extends ReadonlyArray<AnyClientTool> = any, TSchema extends SchemaInput | undefined = undefined> = BaseUseChatReturn<TTools> & (TSchema extends SchemaInput ? {
47
+ /**
48
+ * Live progressively-parsed structured output. Resets on every new run.
49
+ */
50
+ partial: Accessor<DeepPartial<InferSchemaType<TSchema>>>;
51
+ /**
52
+ * Final, schema-validated structured output. `null` until the terminal
53
+ * `structured-output.complete` event arrives. Resets on every new run.
54
+ */
55
+ final: Accessor<InferSchemaType<TSchema> | null>;
56
+ } : Record<never, never>);
57
+ interface BaseUseChatReturn<TTools extends ReadonlyArray<AnyClientTool> = any> {
27
58
  /**
28
59
  * Current messages in the conversation
29
60
  */
@@ -99,5 +130,5 @@ interface UseChatReturn<TTools extends ReadonlyArray<AnyClientTool> = any> {
99
130
  sessionGenerating: Accessor<boolean>;
100
131
  }
101
132
  //#endregion
102
- export { type ChatRequestBody, type UIMessage, UseChatOptions, UseChatReturn };
133
+ export { type ChatRequestBody, DeepPartial, type UIMessage, UseChatOptions, UseChatReturn };
103
134
  //# sourceMappingURL=types.d.ts.map
@@ -1,8 +1,8 @@
1
1
  import { UseChatOptions, UseChatReturn } from "./types.js";
2
- import { AnyClientTool } from "@tanstack/ai";
2
+ import { AnyClientTool, SchemaInput } from "@tanstack/ai";
3
3
 
4
4
  //#region src/use-chat.d.ts
5
- declare function useChat<TTools extends ReadonlyArray<AnyClientTool> = any>(options?: UseChatOptions<TTools>): UseChatReturn<TTools>;
5
+ declare function useChat<TTools extends ReadonlyArray<AnyClientTool> = any, TSchema extends SchemaInput | undefined = undefined>(options?: UseChatOptions<TTools, TSchema>): UseChatReturn<TTools, TSchema>;
6
6
  //#endregion
7
7
  export { useChat };
8
8
  //# sourceMappingURL=use-chat.d.ts.map
package/dist/use-chat.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { createEffect, createMemo, createSignal, createUniqueId, onCleanup } from "solid-js";
2
2
  import { ChatClient } from "@tanstack/ai-client";
3
+ import { parsePartialJSON } from "@tanstack/ai";
3
4
 
4
5
  //#region src/use-chat.ts
5
6
  function useChat(options = {}) {
@@ -12,14 +13,34 @@ function useChat(options = {}) {
12
13
  const [isSubscribed, setIsSubscribed] = createSignal(false);
13
14
  const [connectionStatus, setConnectionStatus] = createSignal("disconnected");
14
15
  const [sessionGenerating, setSessionGenerating] = createSignal(false);
16
+ const [partial, setPartial] = createSignal({});
17
+ const [final, setFinal] = createSignal(null);
18
+ let rawJson = "";
15
19
  const client = createMemo(() => {
16
20
  return new ChatClient({
17
21
  connection: options.connection,
18
22
  id: clientId,
19
23
  initialMessages: options.initialMessages,
20
24
  body: options.body,
25
+ forwardedProps: options.forwardedProps,
21
26
  onResponse: (response) => options.onResponse?.(response),
22
- onChunk: (chunk) => options.onChunk?.(chunk),
27
+ onChunk: (chunk) => {
28
+ if (options.outputSchema !== void 0) {
29
+ if (chunk.type === "RUN_STARTED") {
30
+ rawJson = "";
31
+ setPartial({});
32
+ setFinal(null);
33
+ } else if (chunk.type === "TEXT_MESSAGE_CONTENT" && chunk.delta) {
34
+ rawJson += chunk.delta;
35
+ const progressive = parsePartialJSON(rawJson);
36
+ if (progressive && typeof progressive === "object") setPartial(() => progressive);
37
+ } else if (chunk.type === "CUSTOM" && chunk.name === "structured-output.complete") {
38
+ const value = chunk.value;
39
+ setFinal(() => value.object);
40
+ }
41
+ }
42
+ options.onChunk?.(chunk);
43
+ },
23
44
  onFinish: (message) => {
24
45
  options.onFinish?.(message);
25
46
  },
@@ -53,8 +74,10 @@ function useChat(options = {}) {
53
74
  });
54
75
  }, [clientId]);
55
76
  createEffect(() => {
56
- const currentBody = options.body;
57
- client().updateOptions({ body: currentBody });
77
+ client().updateOptions({
78
+ body: options.body,
79
+ forwardedProps: options.forwardedProps
80
+ });
58
81
  });
59
82
  createEffect(() => {
60
83
  if (options.initialMessages && options.initialMessages.length > 0) {
@@ -110,7 +133,9 @@ function useChat(options = {}) {
110
133
  setMessages: setMessagesManually,
111
134
  clear,
112
135
  addToolResult,
113
- addToolApprovalResponse
136
+ addToolApprovalResponse,
137
+ partial,
138
+ final
114
139
  };
115
140
  }
116
141
 
@@ -1 +1 @@
1
- {"version":3,"file":"use-chat.js","names":[],"sources":["../src/use-chat.ts"],"sourcesContent":["import {\n createEffect,\n createMemo,\n createSignal,\n createUniqueId,\n onCleanup,\n} from 'solid-js'\n\nimport { ChatClient } from '@tanstack/ai-client'\nimport type { ChatClientState, ConnectionStatus } from '@tanstack/ai-client'\nimport type { AnyClientTool, ModelMessage } from '@tanstack/ai'\nimport type {\n MultimodalContent,\n UIMessage,\n UseChatOptions,\n UseChatReturn,\n} from './types'\n\nexport function useChat<TTools extends ReadonlyArray<AnyClientTool> = any>(\n options: UseChatOptions<TTools> = {} as UseChatOptions<TTools>,\n): UseChatReturn<TTools> {\n const hookId = createUniqueId()\n const clientId = options.id || hookId\n\n const [messages, setMessages] = createSignal<Array<UIMessage<TTools>>>(\n options.initialMessages || [],\n )\n const [isLoading, setIsLoading] = createSignal(false)\n const [error, setError] = createSignal<Error | undefined>(undefined)\n const [status, setStatus] = createSignal<ChatClientState>('ready')\n const [isSubscribed, setIsSubscribed] = createSignal(false)\n const [connectionStatus, setConnectionStatus] =\n createSignal<ConnectionStatus>('disconnected')\n const [sessionGenerating, setSessionGenerating] = createSignal(false)\n\n // Create ChatClient instance with callbacks to sync state.\n // Every user-provided callback is wrapped so the LATEST `options.xxx` value\n // is read at call time. Direct assignment would freeze the callback to the\n // reference we saw at creation; the wrapper lets reactive `options` or\n // in-place mutations propagate. When the user clears a callback (sets it to\n // undefined), `?.` no-ops.\n const client = createMemo(() => {\n return new ChatClient({\n connection: options.connection,\n id: clientId,\n initialMessages: options.initialMessages,\n body: options.body,\n onResponse: (response) => options.onResponse?.(response),\n onChunk: (chunk) => options.onChunk?.(chunk),\n onFinish: (message) => {\n options.onFinish?.(message)\n },\n onError: (err) => {\n options.onError?.(err)\n },\n tools: options.tools,\n onCustomEvent: (eventType, data, context) =>\n options.onCustomEvent?.(eventType, data, context),\n streamProcessor: options.streamProcessor,\n onMessagesChange: (newMessages: Array<UIMessage<TTools>>) => {\n setMessages(newMessages)\n },\n onLoadingChange: (newIsLoading: boolean) => {\n setIsLoading(newIsLoading)\n },\n onStatusChange: (newStatus: ChatClientState) => {\n setStatus(newStatus)\n },\n onErrorChange: (newError: Error | undefined) => {\n setError(newError)\n },\n onSubscriptionChange: (nextIsSubscribed: boolean) => {\n setIsSubscribed(nextIsSubscribed)\n },\n onConnectionStatusChange: (nextStatus: ConnectionStatus) => {\n setConnectionStatus(nextStatus)\n },\n onSessionGeneratingChange: (isGenerating: boolean) => {\n setSessionGenerating(isGenerating)\n },\n })\n // Only recreate when clientId changes\n // Connection and other options are captured at creation time\n }, [clientId])\n\n // Sync body changes to the client\n // This allows dynamic body values (like model selection) to be updated without recreating the client\n createEffect(() => {\n const currentBody = options.body\n client().updateOptions({ body: currentBody })\n })\n\n // Sync initial messages on mount only\n // Note: initialMessages are passed to ChatClient constructor, but we also\n // set them here to ensure React state is in sync\n createEffect(() => {\n if (options.initialMessages && options.initialMessages.length > 0) {\n // Only set if current messages are empty (initial state)\n if (messages().length === 0) {\n client().setMessagesManually(options.initialMessages)\n }\n }\n }) // Only run on mount - initialMessages are handled by ChatClient constructor\n\n // Apply initial live mode immediately on hook creation.\n if (options.live) {\n client().subscribe()\n } else {\n client().unsubscribe()\n }\n\n createEffect(() => {\n if (options.live) {\n client().subscribe()\n } else {\n client().unsubscribe()\n }\n })\n\n // Cleanup on unmount: stop any in-flight requests.\n onCleanup(() => {\n if (options.live) {\n client().unsubscribe()\n } else {\n client().stop()\n }\n })\n\n // Callback options are read through `options.xxx` at call time, so reactive\n // or mutated options propagate without recreating the client.\n\n const sendMessage = async (content: string | MultimodalContent) => {\n await client().sendMessage(content)\n }\n\n const append = async (message: ModelMessage | UIMessage<TTools>) => {\n await client().append(message)\n }\n\n const reload = async () => {\n await client().reload()\n }\n\n const stop = () => {\n client().stop()\n }\n\n const clear = () => {\n client().clear()\n }\n\n const setMessagesManually = (newMessages: Array<UIMessage<TTools>>) => {\n client().setMessagesManually(newMessages)\n }\n\n const addToolResult = async (result: {\n toolCallId: string\n tool: string\n output: any\n state?: 'output-available' | 'output-error'\n errorText?: string\n }) => {\n await client().addToolResult(result)\n }\n\n const addToolApprovalResponse = async (response: {\n id: string\n approved: boolean\n }) => {\n await client().addToolApprovalResponse(response)\n }\n\n return {\n messages,\n sendMessage,\n append,\n reload,\n stop,\n isLoading,\n error,\n status,\n isSubscribed,\n connectionStatus,\n sessionGenerating,\n setMessages: setMessagesManually,\n clear,\n addToolResult,\n addToolApprovalResponse,\n }\n}\n"],"mappings":";;;;AAkBA,SAAgB,QACd,UAAkC,EAAE,EACb;CACvB,MAAM,SAAS,gBAAgB;CAC/B,MAAM,WAAW,QAAQ,MAAM;CAE/B,MAAM,CAAC,UAAU,eAAe,aAC9B,QAAQ,mBAAmB,EAAE,CAC9B;CACD,MAAM,CAAC,WAAW,gBAAgB,aAAa,MAAM;CACrD,MAAM,CAAC,OAAO,YAAY,aAAgC,OAAU;CACpE,MAAM,CAAC,QAAQ,aAAa,aAA8B,QAAQ;CAClE,MAAM,CAAC,cAAc,mBAAmB,aAAa,MAAM;CAC3D,MAAM,CAAC,kBAAkB,uBACvB,aAA+B,eAAe;CAChD,MAAM,CAAC,mBAAmB,wBAAwB,aAAa,MAAM;CAQrE,MAAM,SAAS,iBAAiB;AAC9B,SAAO,IAAI,WAAW;GACpB,YAAY,QAAQ;GACpB,IAAI;GACJ,iBAAiB,QAAQ;GACzB,MAAM,QAAQ;GACd,aAAa,aAAa,QAAQ,aAAa,SAAS;GACxD,UAAU,UAAU,QAAQ,UAAU,MAAM;GAC5C,WAAW,YAAY;AACrB,YAAQ,WAAW,QAAQ;;GAE7B,UAAU,QAAQ;AAChB,YAAQ,UAAU,IAAI;;GAExB,OAAO,QAAQ;GACf,gBAAgB,WAAW,MAAM,YAC/B,QAAQ,gBAAgB,WAAW,MAAM,QAAQ;GACnD,iBAAiB,QAAQ;GACzB,mBAAmB,gBAA0C;AAC3D,gBAAY,YAAY;;GAE1B,kBAAkB,iBAA0B;AAC1C,iBAAa,aAAa;;GAE5B,iBAAiB,cAA+B;AAC9C,cAAU,UAAU;;GAEtB,gBAAgB,aAAgC;AAC9C,aAAS,SAAS;;GAEpB,uBAAuB,qBAA8B;AACnD,oBAAgB,iBAAiB;;GAEnC,2BAA2B,eAAiC;AAC1D,wBAAoB,WAAW;;GAEjC,4BAA4B,iBAA0B;AACpD,yBAAqB,aAAa;;GAErC,CAAC;IAGD,CAAC,SAAS,CAAC;AAId,oBAAmB;EACjB,MAAM,cAAc,QAAQ;AAC5B,UAAQ,CAAC,cAAc,EAAE,MAAM,aAAa,CAAC;GAC7C;AAKF,oBAAmB;AACjB,MAAI,QAAQ,mBAAmB,QAAQ,gBAAgB,SAAS,GAE9D;OAAI,UAAU,CAAC,WAAW,EACxB,SAAQ,CAAC,oBAAoB,QAAQ,gBAAgB;;GAGzD;AAGF,KAAI,QAAQ,KACV,SAAQ,CAAC,WAAW;KAEpB,SAAQ,CAAC,aAAa;AAGxB,oBAAmB;AACjB,MAAI,QAAQ,KACV,SAAQ,CAAC,WAAW;MAEpB,SAAQ,CAAC,aAAa;GAExB;AAGF,iBAAgB;AACd,MAAI,QAAQ,KACV,SAAQ,CAAC,aAAa;MAEtB,SAAQ,CAAC,MAAM;GAEjB;CAKF,MAAM,cAAc,OAAO,YAAwC;AACjE,QAAM,QAAQ,CAAC,YAAY,QAAQ;;CAGrC,MAAM,SAAS,OAAO,YAA8C;AAClE,QAAM,QAAQ,CAAC,OAAO,QAAQ;;CAGhC,MAAM,SAAS,YAAY;AACzB,QAAM,QAAQ,CAAC,QAAQ;;CAGzB,MAAM,aAAa;AACjB,UAAQ,CAAC,MAAM;;CAGjB,MAAM,cAAc;AAClB,UAAQ,CAAC,OAAO;;CAGlB,MAAM,uBAAuB,gBAA0C;AACrE,UAAQ,CAAC,oBAAoB,YAAY;;CAG3C,MAAM,gBAAgB,OAAO,WAMvB;AACJ,QAAM,QAAQ,CAAC,cAAc,OAAO;;CAGtC,MAAM,0BAA0B,OAAO,aAGjC;AACJ,QAAM,QAAQ,CAAC,wBAAwB,SAAS;;AAGlD,QAAO;EACL;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,aAAa;EACb;EACA;EACA;EACD"}
1
+ {"version":3,"file":"use-chat.js","names":[],"sources":["../src/use-chat.ts"],"sourcesContent":["import {\n createEffect,\n createMemo,\n createSignal,\n createUniqueId,\n onCleanup,\n} from 'solid-js'\n\nimport { ChatClient } from '@tanstack/ai-client'\nimport { parsePartialJSON } from '@tanstack/ai'\nimport type { ChatClientState, ConnectionStatus } from '@tanstack/ai-client'\nimport type {\n AnyClientTool,\n InferSchemaType,\n ModelMessage,\n SchemaInput,\n StreamChunk,\n} from '@tanstack/ai'\nimport type {\n DeepPartial,\n MultimodalContent,\n UIMessage,\n UseChatOptions,\n UseChatReturn,\n} from './types'\n\nexport function useChat<\n TTools extends ReadonlyArray<AnyClientTool> = any,\n TSchema extends SchemaInput | undefined = undefined,\n>(\n options: UseChatOptions<TTools, TSchema> = {} as UseChatOptions<\n TTools,\n TSchema\n >,\n): UseChatReturn<TTools, TSchema> {\n const hookId = createUniqueId()\n const clientId = options.id || hookId\n\n const [messages, setMessages] = createSignal<Array<UIMessage<TTools>>>(\n options.initialMessages || [],\n )\n const [isLoading, setIsLoading] = createSignal(false)\n const [error, setError] = createSignal<Error | undefined>(undefined)\n const [status, setStatus] = createSignal<ChatClientState>('ready')\n const [isSubscribed, setIsSubscribed] = createSignal(false)\n const [connectionStatus, setConnectionStatus] =\n createSignal<ConnectionStatus>('disconnected')\n const [sessionGenerating, setSessionGenerating] = createSignal(false)\n\n // Structured-output state. Runtime always tracks them — the conditional\n // return type hides them from callers that didn't supply `outputSchema`.\n type Partial = DeepPartial<InferSchemaType<NonNullable<TSchema>>>\n type Final = InferSchemaType<NonNullable<TSchema>>\n const [partial, setPartial] = createSignal<Partial>({} as Partial)\n const [final, setFinal] = createSignal<Final | null>(null)\n // Raw JSON accumulator — kept outside the signal to avoid extra re-runs.\n let rawJson = ''\n\n // Create ChatClient instance with callbacks to sync state.\n // Every user-provided callback is wrapped so the LATEST `options.xxx` value\n // is read at call time. Direct assignment would freeze the callback to the\n // reference we saw at creation; the wrapper lets reactive `options` or\n // in-place mutations propagate. When the user clears a callback (sets it to\n // undefined), `?.` no-ops.\n const client = createMemo(() => {\n return new ChatClient({\n connection: options.connection,\n id: clientId,\n initialMessages: options.initialMessages,\n body: options.body,\n forwardedProps: options.forwardedProps,\n onResponse: (response) => options.onResponse?.(response),\n onChunk: (chunk: StreamChunk) => {\n if (options.outputSchema !== undefined) {\n if (chunk.type === 'RUN_STARTED') {\n rawJson = ''\n setPartial({} as Partial)\n setFinal(null)\n } else if (chunk.type === 'TEXT_MESSAGE_CONTENT' && chunk.delta) {\n rawJson += chunk.delta\n const progressive = parsePartialJSON(rawJson)\n if (progressive && typeof progressive === 'object') {\n setPartial(() => progressive as Partial)\n }\n } else if (\n chunk.type === 'CUSTOM' &&\n chunk.name === 'structured-output.complete'\n ) {\n const value = chunk.value as { object: unknown }\n setFinal(() => value.object as Final)\n }\n }\n options.onChunk?.(chunk)\n },\n onFinish: (message) => {\n options.onFinish?.(message)\n },\n onError: (err) => {\n options.onError?.(err)\n },\n tools: options.tools,\n onCustomEvent: (eventType, data, context) =>\n options.onCustomEvent?.(eventType, data, context),\n streamProcessor: options.streamProcessor,\n onMessagesChange: (newMessages: Array<UIMessage<TTools>>) => {\n setMessages(newMessages)\n },\n onLoadingChange: (newIsLoading: boolean) => {\n setIsLoading(newIsLoading)\n },\n onStatusChange: (newStatus: ChatClientState) => {\n setStatus(newStatus)\n },\n onErrorChange: (newError: Error | undefined) => {\n setError(newError)\n },\n onSubscriptionChange: (nextIsSubscribed: boolean) => {\n setIsSubscribed(nextIsSubscribed)\n },\n onConnectionStatusChange: (nextStatus: ConnectionStatus) => {\n setConnectionStatus(nextStatus)\n },\n onSessionGeneratingChange: (isGenerating: boolean) => {\n setSessionGenerating(isGenerating)\n },\n })\n // Only recreate when clientId changes\n // Connection and other options are captured at creation time\n }, [clientId])\n\n // Sync body / forwardedProps changes to the client.\n // Both populate the same wire payload; `forwardedProps` is preferred\n // and `body` is deprecated but still supported.\n createEffect(() => {\n client().updateOptions({\n body: options.body,\n forwardedProps: options.forwardedProps,\n })\n })\n\n // Sync initial messages on mount only\n // Note: initialMessages are passed to ChatClient constructor, but we also\n // set them here to ensure React state is in sync\n createEffect(() => {\n if (options.initialMessages && options.initialMessages.length > 0) {\n // Only set if current messages are empty (initial state)\n if (messages().length === 0) {\n client().setMessagesManually(options.initialMessages)\n }\n }\n }) // Only run on mount - initialMessages are handled by ChatClient constructor\n\n // Apply initial live mode immediately on hook creation.\n if (options.live) {\n client().subscribe()\n } else {\n client().unsubscribe()\n }\n\n createEffect(() => {\n if (options.live) {\n client().subscribe()\n } else {\n client().unsubscribe()\n }\n })\n\n // Cleanup on unmount: stop any in-flight requests.\n onCleanup(() => {\n if (options.live) {\n client().unsubscribe()\n } else {\n client().stop()\n }\n })\n\n // Callback options are read through `options.xxx` at call time, so reactive\n // or mutated options propagate without recreating the client.\n\n const sendMessage = async (content: string | MultimodalContent) => {\n await client().sendMessage(content)\n }\n\n const append = async (message: ModelMessage | UIMessage<TTools>) => {\n await client().append(message)\n }\n\n const reload = async () => {\n await client().reload()\n }\n\n const stop = () => {\n client().stop()\n }\n\n const clear = () => {\n client().clear()\n }\n\n const setMessagesManually = (newMessages: Array<UIMessage<TTools>>) => {\n client().setMessagesManually(newMessages)\n }\n\n const addToolResult = async (result: {\n toolCallId: string\n tool: string\n output: any\n state?: 'output-available' | 'output-error'\n errorText?: string\n }) => {\n await client().addToolResult(result)\n }\n\n const addToolApprovalResponse = async (response: {\n id: string\n approved: boolean\n }) => {\n await client().addToolApprovalResponse(response)\n }\n\n // partial / final are runtime-tracked unconditionally; the conditional\n // return type hides them when no `outputSchema` is supplied.\n return {\n messages,\n sendMessage,\n append,\n reload,\n stop,\n isLoading,\n error,\n status,\n isSubscribed,\n connectionStatus,\n sessionGenerating,\n setMessages: setMessagesManually,\n clear,\n addToolResult,\n addToolApprovalResponse,\n partial,\n final,\n } as unknown as UseChatReturn<TTools, TSchema>\n}\n"],"mappings":";;;;;AA0BA,SAAgB,QAId,UAA2C,EAAE,EAIb;CAChC,MAAM,SAAS,gBAAgB;CAC/B,MAAM,WAAW,QAAQ,MAAM;CAE/B,MAAM,CAAC,UAAU,eAAe,aAC9B,QAAQ,mBAAmB,EAAE,CAC9B;CACD,MAAM,CAAC,WAAW,gBAAgB,aAAa,MAAM;CACrD,MAAM,CAAC,OAAO,YAAY,aAAgC,OAAU;CACpE,MAAM,CAAC,QAAQ,aAAa,aAA8B,QAAQ;CAClE,MAAM,CAAC,cAAc,mBAAmB,aAAa,MAAM;CAC3D,MAAM,CAAC,kBAAkB,uBACvB,aAA+B,eAAe;CAChD,MAAM,CAAC,mBAAmB,wBAAwB,aAAa,MAAM;CAMrE,MAAM,CAAC,SAAS,cAAc,aAAsB,EAAE,CAAY;CAClE,MAAM,CAAC,OAAO,YAAY,aAA2B,KAAK;CAE1D,IAAI,UAAU;CAQd,MAAM,SAAS,iBAAiB;AAC9B,SAAO,IAAI,WAAW;GACpB,YAAY,QAAQ;GACpB,IAAI;GACJ,iBAAiB,QAAQ;GACzB,MAAM,QAAQ;GACd,gBAAgB,QAAQ;GACxB,aAAa,aAAa,QAAQ,aAAa,SAAS;GACxD,UAAU,UAAuB;AAC/B,QAAI,QAAQ,iBAAiB,QAC3B;SAAI,MAAM,SAAS,eAAe;AAChC,gBAAU;AACV,iBAAW,EAAE,CAAY;AACzB,eAAS,KAAK;gBACL,MAAM,SAAS,0BAA0B,MAAM,OAAO;AAC/D,iBAAW,MAAM;MACjB,MAAM,cAAc,iBAAiB,QAAQ;AAC7C,UAAI,eAAe,OAAO,gBAAgB,SACxC,kBAAiB,YAAuB;gBAG1C,MAAM,SAAS,YACf,MAAM,SAAS,8BACf;MACA,MAAM,QAAQ,MAAM;AACpB,qBAAe,MAAM,OAAgB;;;AAGzC,YAAQ,UAAU,MAAM;;GAE1B,WAAW,YAAY;AACrB,YAAQ,WAAW,QAAQ;;GAE7B,UAAU,QAAQ;AAChB,YAAQ,UAAU,IAAI;;GAExB,OAAO,QAAQ;GACf,gBAAgB,WAAW,MAAM,YAC/B,QAAQ,gBAAgB,WAAW,MAAM,QAAQ;GACnD,iBAAiB,QAAQ;GACzB,mBAAmB,gBAA0C;AAC3D,gBAAY,YAAY;;GAE1B,kBAAkB,iBAA0B;AAC1C,iBAAa,aAAa;;GAE5B,iBAAiB,cAA+B;AAC9C,cAAU,UAAU;;GAEtB,gBAAgB,aAAgC;AAC9C,aAAS,SAAS;;GAEpB,uBAAuB,qBAA8B;AACnD,oBAAgB,iBAAiB;;GAEnC,2BAA2B,eAAiC;AAC1D,wBAAoB,WAAW;;GAEjC,4BAA4B,iBAA0B;AACpD,yBAAqB,aAAa;;GAErC,CAAC;IAGD,CAAC,SAAS,CAAC;AAKd,oBAAmB;AACjB,UAAQ,CAAC,cAAc;GACrB,MAAM,QAAQ;GACd,gBAAgB,QAAQ;GACzB,CAAC;GACF;AAKF,oBAAmB;AACjB,MAAI,QAAQ,mBAAmB,QAAQ,gBAAgB,SAAS,GAE9D;OAAI,UAAU,CAAC,WAAW,EACxB,SAAQ,CAAC,oBAAoB,QAAQ,gBAAgB;;GAGzD;AAGF,KAAI,QAAQ,KACV,SAAQ,CAAC,WAAW;KAEpB,SAAQ,CAAC,aAAa;AAGxB,oBAAmB;AACjB,MAAI,QAAQ,KACV,SAAQ,CAAC,WAAW;MAEpB,SAAQ,CAAC,aAAa;GAExB;AAGF,iBAAgB;AACd,MAAI,QAAQ,KACV,SAAQ,CAAC,aAAa;MAEtB,SAAQ,CAAC,MAAM;GAEjB;CAKF,MAAM,cAAc,OAAO,YAAwC;AACjE,QAAM,QAAQ,CAAC,YAAY,QAAQ;;CAGrC,MAAM,SAAS,OAAO,YAA8C;AAClE,QAAM,QAAQ,CAAC,OAAO,QAAQ;;CAGhC,MAAM,SAAS,YAAY;AACzB,QAAM,QAAQ,CAAC,QAAQ;;CAGzB,MAAM,aAAa;AACjB,UAAQ,CAAC,MAAM;;CAGjB,MAAM,cAAc;AAClB,UAAQ,CAAC,OAAO;;CAGlB,MAAM,uBAAuB,gBAA0C;AACrE,UAAQ,CAAC,oBAAoB,YAAY;;CAG3C,MAAM,gBAAgB,OAAO,WAMvB;AACJ,QAAM,QAAQ,CAAC,cAAc,OAAO;;CAGtC,MAAM,0BAA0B,OAAO,aAGjC;AACJ,QAAM,QAAQ,CAAC,wBAAwB,SAAS;;AAKlD,QAAO;EACL;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,aAAa;EACb;EACA;EACA;EACA;EACA;EACD"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/ai-solid",
3
- "version": "0.7.2",
3
+ "version": "0.9.0",
4
4
  "description": "Solid hooks for TanStack AI",
5
5
  "author": "",
6
6
  "license": "MIT",
@@ -31,14 +31,15 @@
31
31
  "streaming"
32
32
  ],
33
33
  "dependencies": {
34
- "@tanstack/ai-client": "0.9.1"
34
+ "@tanstack/ai-client": "0.10.0"
35
35
  },
36
36
  "peerDependencies": {
37
37
  "solid-js": ">=1.9.10",
38
- "@tanstack/ai": "^0.16.0"
38
+ "@tanstack/ai": "^0.18.0"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@solidjs/testing-library": "^0.8.10",
42
+ "@standard-schema/spec": "^1.1.0",
42
43
  "@types/node": "^24.10.1",
43
44
  "@vitest/coverage-v8": "4.0.14",
44
45
  "jsdom": "^27.2.0",
@@ -46,7 +47,7 @@
46
47
  "tsdown": "^0.17.0-beta.6",
47
48
  "typescript": "5.9.3",
48
49
  "vitest": "^4.0.14",
49
- "@tanstack/ai": "0.16.0"
50
+ "@tanstack/ai": "0.18.0"
50
51
  },
51
52
  "scripts": {
52
53
  "clean": "premove ./build ./dist",
package/src/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export { useChat } from './use-chat'
2
2
  export type {
3
+ DeepPartial,
3
4
  UseChatOptions,
4
5
  UseChatReturn,
5
6
  UIMessage,
package/src/types.ts CHANGED
@@ -1,4 +1,9 @@
1
- import type { AnyClientTool, ModelMessage } from '@tanstack/ai'
1
+ import type {
2
+ AnyClientTool,
3
+ InferSchemaType,
4
+ ModelMessage,
5
+ SchemaInput,
6
+ } from '@tanstack/ai'
2
7
  import type {
3
8
  ChatClientOptions,
4
9
  ChatClientState,
@@ -12,6 +17,18 @@ import type { Accessor } from 'solid-js'
12
17
  // Re-export types from ai-client
13
18
  export type { ChatRequestBody, MultimodalContent, UIMessage }
14
19
 
20
+ /**
21
+ * Recursive partial — every property and every nested array element is optional.
22
+ * Used to type the in-flight `partial` accessor while a structured-output
23
+ * stream is still arriving.
24
+ */
25
+ export type DeepPartial<T> =
26
+ T extends ReadonlyArray<infer U>
27
+ ? Array<DeepPartial<U>>
28
+ : T extends object
29
+ ? { [K in keyof T]?: DeepPartial<T[K]> }
30
+ : T
31
+
15
32
  /**
16
33
  * Options for the useChat hook.
17
34
  *
@@ -25,26 +42,58 @@ export type { ChatRequestBody, MultimodalContent, UIMessage }
25
42
  * All other callbacks (onResponse, onChunk, onFinish, onError) are
26
43
  * passed through to the underlying ChatClient and can be used for side effects.
27
44
  *
45
+ * When `outputSchema` is supplied, the hook returns typed `partial` and `final`
46
+ * accessors. The schema is used purely for type inference; server-side
47
+ * validation still runs against the schema passed to `chat({ outputSchema })`
48
+ * on the server route.
49
+ *
28
50
  * Note: Connection and body changes will recreate the ChatClient instance.
29
51
  * To update these options, remount the component or use a key prop.
30
52
  */
31
- export type UseChatOptions<TTools extends ReadonlyArray<AnyClientTool> = any> =
32
- Omit<
33
- ChatClientOptions<TTools>,
34
- | 'onMessagesChange'
35
- | 'onLoadingChange'
36
- | 'onErrorChange'
37
- | 'onStatusChange'
38
- | 'onSubscriptionChange'
39
- | 'onConnectionStatusChange'
40
- | 'onSessionGeneratingChange'
41
- > & {
42
- live?: boolean
43
- }
44
-
45
- export interface UseChatReturn<
53
+ export type UseChatOptions<
54
+ TTools extends ReadonlyArray<AnyClientTool> = any,
55
+ TSchema extends SchemaInput | undefined = undefined,
56
+ > = Omit<
57
+ ChatClientOptions<TTools>,
58
+ | 'onMessagesChange'
59
+ | 'onLoadingChange'
60
+ | 'onErrorChange'
61
+ | 'onStatusChange'
62
+ | 'onSubscriptionChange'
63
+ | 'onConnectionStatusChange'
64
+ | 'onSessionGeneratingChange'
65
+ > & {
66
+ live?: boolean
67
+ /**
68
+ * Standard-schema-compatible schema (Zod, Valibot, ArkType, or plain JSON
69
+ * Schema). Used to infer the shape of `partial` and `final`.
70
+ */
71
+ outputSchema?: TSchema
72
+ }
73
+
74
+ /**
75
+ * Discriminated return shape: when `outputSchema` is supplied, the hook adds
76
+ * typed `partial` / `final` accessors; otherwise the return is unchanged.
77
+ */
78
+ export type UseChatReturn<
46
79
  TTools extends ReadonlyArray<AnyClientTool> = any,
47
- > {
80
+ TSchema extends SchemaInput | undefined = undefined,
81
+ > = BaseUseChatReturn<TTools> &
82
+ (TSchema extends SchemaInput
83
+ ? {
84
+ /**
85
+ * Live progressively-parsed structured output. Resets on every new run.
86
+ */
87
+ partial: Accessor<DeepPartial<InferSchemaType<TSchema>>>
88
+ /**
89
+ * Final, schema-validated structured output. `null` until the terminal
90
+ * `structured-output.complete` event arrives. Resets on every new run.
91
+ */
92
+ final: Accessor<InferSchemaType<TSchema> | null>
93
+ }
94
+ : Record<never, never>)
95
+
96
+ interface BaseUseChatReturn<TTools extends ReadonlyArray<AnyClientTool> = any> {
48
97
  /**
49
98
  * Current messages in the conversation
50
99
  */
package/src/use-chat.ts CHANGED
@@ -7,18 +7,32 @@ import {
7
7
  } from 'solid-js'
8
8
 
9
9
  import { ChatClient } from '@tanstack/ai-client'
10
+ import { parsePartialJSON } from '@tanstack/ai'
10
11
  import type { ChatClientState, ConnectionStatus } from '@tanstack/ai-client'
11
- import type { AnyClientTool, ModelMessage } from '@tanstack/ai'
12
12
  import type {
13
+ AnyClientTool,
14
+ InferSchemaType,
15
+ ModelMessage,
16
+ SchemaInput,
17
+ StreamChunk,
18
+ } from '@tanstack/ai'
19
+ import type {
20
+ DeepPartial,
13
21
  MultimodalContent,
14
22
  UIMessage,
15
23
  UseChatOptions,
16
24
  UseChatReturn,
17
25
  } from './types'
18
26
 
19
- export function useChat<TTools extends ReadonlyArray<AnyClientTool> = any>(
20
- options: UseChatOptions<TTools> = {} as UseChatOptions<TTools>,
21
- ): UseChatReturn<TTools> {
27
+ export function useChat<
28
+ TTools extends ReadonlyArray<AnyClientTool> = any,
29
+ TSchema extends SchemaInput | undefined = undefined,
30
+ >(
31
+ options: UseChatOptions<TTools, TSchema> = {} as UseChatOptions<
32
+ TTools,
33
+ TSchema
34
+ >,
35
+ ): UseChatReturn<TTools, TSchema> {
22
36
  const hookId = createUniqueId()
23
37
  const clientId = options.id || hookId
24
38
 
@@ -33,6 +47,15 @@ export function useChat<TTools extends ReadonlyArray<AnyClientTool> = any>(
33
47
  createSignal<ConnectionStatus>('disconnected')
34
48
  const [sessionGenerating, setSessionGenerating] = createSignal(false)
35
49
 
50
+ // Structured-output state. Runtime always tracks them — the conditional
51
+ // return type hides them from callers that didn't supply `outputSchema`.
52
+ type Partial = DeepPartial<InferSchemaType<NonNullable<TSchema>>>
53
+ type Final = InferSchemaType<NonNullable<TSchema>>
54
+ const [partial, setPartial] = createSignal<Partial>({} as Partial)
55
+ const [final, setFinal] = createSignal<Final | null>(null)
56
+ // Raw JSON accumulator — kept outside the signal to avoid extra re-runs.
57
+ let rawJson = ''
58
+
36
59
  // Create ChatClient instance with callbacks to sync state.
37
60
  // Every user-provided callback is wrapped so the LATEST `options.xxx` value
38
61
  // is read at call time. Direct assignment would freeze the callback to the
@@ -45,8 +68,30 @@ export function useChat<TTools extends ReadonlyArray<AnyClientTool> = any>(
45
68
  id: clientId,
46
69
  initialMessages: options.initialMessages,
47
70
  body: options.body,
71
+ forwardedProps: options.forwardedProps,
48
72
  onResponse: (response) => options.onResponse?.(response),
49
- onChunk: (chunk) => options.onChunk?.(chunk),
73
+ onChunk: (chunk: StreamChunk) => {
74
+ if (options.outputSchema !== undefined) {
75
+ if (chunk.type === 'RUN_STARTED') {
76
+ rawJson = ''
77
+ setPartial({} as Partial)
78
+ setFinal(null)
79
+ } else if (chunk.type === 'TEXT_MESSAGE_CONTENT' && chunk.delta) {
80
+ rawJson += chunk.delta
81
+ const progressive = parsePartialJSON(rawJson)
82
+ if (progressive && typeof progressive === 'object') {
83
+ setPartial(() => progressive as Partial)
84
+ }
85
+ } else if (
86
+ chunk.type === 'CUSTOM' &&
87
+ chunk.name === 'structured-output.complete'
88
+ ) {
89
+ const value = chunk.value as { object: unknown }
90
+ setFinal(() => value.object as Final)
91
+ }
92
+ }
93
+ options.onChunk?.(chunk)
94
+ },
50
95
  onFinish: (message) => {
51
96
  options.onFinish?.(message)
52
97
  },
@@ -83,11 +128,14 @@ export function useChat<TTools extends ReadonlyArray<AnyClientTool> = any>(
83
128
  // Connection and other options are captured at creation time
84
129
  }, [clientId])
85
130
 
86
- // Sync body changes to the client
87
- // This allows dynamic body values (like model selection) to be updated without recreating the client
131
+ // Sync body / forwardedProps changes to the client.
132
+ // Both populate the same wire payload; `forwardedProps` is preferred
133
+ // and `body` is deprecated but still supported.
88
134
  createEffect(() => {
89
- const currentBody = options.body
90
- client().updateOptions({ body: currentBody })
135
+ client().updateOptions({
136
+ body: options.body,
137
+ forwardedProps: options.forwardedProps,
138
+ })
91
139
  })
92
140
 
93
141
  // Sync initial messages on mount only
@@ -170,6 +218,8 @@ export function useChat<TTools extends ReadonlyArray<AnyClientTool> = any>(
170
218
  await client().addToolApprovalResponse(response)
171
219
  }
172
220
 
221
+ // partial / final are runtime-tracked unconditionally; the conditional
222
+ // return type hides them when no `outputSchema` is supplied.
173
223
  return {
174
224
  messages,
175
225
  sendMessage,
@@ -186,5 +236,7 @@ export function useChat<TTools extends ReadonlyArray<AnyClientTool> = any>(
186
236
  clear,
187
237
  addToolResult,
188
238
  addToolApprovalResponse,
189
- }
239
+ partial,
240
+ final,
241
+ } as unknown as UseChatReturn<TTools, TSchema>
190
242
  }