@ai-sdk/svelte 3.0.0-alpha.3 → 3.0.0-alpha.5

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/CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
1
1
  # @ai-sdk/svelte
2
2
 
3
+ ## 3.0.0-alpha.5
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [655cf3c]
8
+ - Updated dependencies [1675396]
9
+ - Updated dependencies [cf9af6e]
10
+ - Updated dependencies [ef256ed]
11
+ - Updated dependencies [1ed0287]
12
+ - Updated dependencies [825e8d7]
13
+ - Updated dependencies [7324c21]
14
+ - ai@5.0.0-alpha.5
15
+
16
+ ## 3.0.0-alpha.4
17
+
18
+ ### Patch Changes
19
+
20
+ - Updated dependencies [b32c141]
21
+ - Updated dependencies [72d7d72]
22
+ - Updated dependencies [9315076]
23
+ - Updated dependencies [7d97ab6]
24
+ - Updated dependencies [37a916d]
25
+ - Updated dependencies [5f2b3d4]
26
+ - ai@5.0.0-alpha.4
27
+ - @ai-sdk/provider-utils@3.0.0-alpha.4
28
+
3
29
  ## 3.0.0-alpha.3
4
30
 
5
31
  ### Patch Changes
@@ -0,0 +1,4 @@
1
+ import { ChatStore, type ChatStoreOptions, type UIDataPartSchemas } from 'ai';
2
+ export declare const hasChatStoreContext: () => boolean, getChatStoreContext: () => ChatStore<unknown, UIDataPartSchemas>, setChatStoreContext: (value: ChatStore<unknown, UIDataPartSchemas>) => ChatStore<unknown, UIDataPartSchemas>;
3
+ export declare function createChatStore<MESSAGE_METADATA = unknown, DATA_PART_SCHEMAS extends UIDataPartSchemas = UIDataPartSchemas>(options: ChatStoreOptions<MESSAGE_METADATA, DATA_PART_SCHEMAS>): ChatStore<MESSAGE_METADATA, DATA_PART_SCHEMAS>;
4
+ //# sourceMappingURL=chat-store.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chat-store.svelte.d.ts","sourceRoot":"","sources":["../src/chat-store.svelte.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,SAAS,EAKT,KAAK,gBAAgB,EAErB,KAAK,iBAAiB,EAGvB,MAAM,IAAI,CAAC;AAGZ,eAAO,MACO,mBAAmB,iBACnB,mBAAmB,+CACnB,mBAAmB,yFACQ,CAAC;AAqD1C,wBAAgB,eAAe,CAC7B,gBAAgB,GAAG,OAAO,EAC1B,iBAAiB,SAAS,iBAAiB,GAAG,iBAAiB,EAE/D,OAAO,EAAE,gBAAgB,CAAC,gBAAgB,EAAE,iBAAiB,CAAC,GAC7D,SAAS,CAAC,gBAAgB,EAAE,iBAAiB,CAAC,CAQhD"}
@@ -0,0 +1,43 @@
1
+ import { ChatStore, SerialJobExecutor, } from 'ai';
2
+ import { createContext } from './utils.svelte.js';
3
+ export const { hasContext: hasChatStoreContext, getContext: getChatStoreContext, setContext: setChatStoreContext, } = createContext('ChatStore');
4
+ class SvelteChat {
5
+ messages;
6
+ status = $state('ready');
7
+ error = $state(undefined);
8
+ activeResponse = undefined;
9
+ jobExecutor = new SerialJobExecutor();
10
+ constructor(messages) {
11
+ this.messages = $state(messages ?? []);
12
+ }
13
+ setStatus = (status) => {
14
+ this.status = status;
15
+ };
16
+ setError = (error) => {
17
+ this.error = error;
18
+ };
19
+ setActiveResponse = (activeResponse) => {
20
+ this.activeResponse = activeResponse;
21
+ };
22
+ setMessages = (messages) => {
23
+ this.messages = messages;
24
+ };
25
+ pushMessage = (message) => {
26
+ this.messages.push(message);
27
+ };
28
+ popMessage = () => {
29
+ this.messages.pop();
30
+ };
31
+ replaceMessage = (index, message) => {
32
+ this.messages[index] = message;
33
+ };
34
+ snapshot = (thing) => {
35
+ return $state.snapshot(thing);
36
+ };
37
+ }
38
+ export function createChatStore(options) {
39
+ return new ChatStore({
40
+ ...options,
41
+ createChat: options => new SvelteChat(options.messages),
42
+ });
43
+ }
@@ -1,13 +1,23 @@
1
- import { type ChatRequestOptions, type CreateUIMessage, type OriginalUseChatOptions, type UIMessage } from 'ai';
2
- export type ChatOptions<MESSAGE_METADATA = unknown> = Readonly<OriginalUseChatOptions<MESSAGE_METADATA>>;
1
+ import { type ChatRequestOptions, type ChatStatus, type CreateUIMessage, type InferUIDataParts, type UIDataPartSchemas, type UIMessage, type UseChatOptions } from 'ai';
2
+ export type ChatOptions<MESSAGE_METADATA = unknown, DATA_PART_SCHEMAS extends UIDataPartSchemas = UIDataPartSchemas> = Readonly<UseChatOptions<MESSAGE_METADATA, DATA_PART_SCHEMAS>>;
3
3
  export type { CreateUIMessage, UIMessage };
4
- export declare class Chat<MESSAGE_METADATA = unknown> {
4
+ export declare class Chat<MESSAGE_METADATA = unknown, DATA_PART_SCHEMAS extends UIDataPartSchemas = UIDataPartSchemas> {
5
5
  #private;
6
6
  /**
7
7
  * The id of the chat. If not provided through the constructor, a random ID will be generated
8
8
  * using the provided `generateId` function, or a built-in function if not provided.
9
9
  */
10
10
  readonly chatId: string;
11
+ /** The current value of the input. Writable, so it can be bound to form inputs. */
12
+ input: string;
13
+ /**
14
+ * Current messages in the chat.
15
+ *
16
+ * This is writable, which is useful when you want to edit the messages on the client, and then
17
+ * trigger {@link reload} to regenerate the AI response.
18
+ */
19
+ get messages(): UIMessage<MESSAGE_METADATA, InferUIDataParts<DATA_PART_SCHEMAS>>[];
20
+ set messages(messages: UIMessage<MESSAGE_METADATA, InferUIDataParts<DATA_PART_SCHEMAS>>[]);
11
21
  /**
12
22
  * Hook status:
13
23
  *
@@ -16,27 +26,19 @@ export declare class Chat<MESSAGE_METADATA = unknown> {
16
26
  * - `ready`: The full response has been received and processed; a new user message can be submitted.
17
27
  * - `error`: An error occurred during the API request, preventing successful completion.
18
28
  */
19
- get status(): "submitted" | "streaming" | "ready" | "error";
29
+ get status(): ChatStatus;
30
+ set status(value: ChatStatus);
20
31
  /** The error object of the API request */
21
32
  get error(): Error | undefined;
22
- /** The current value of the input. Writable, so it can be bound to form inputs. */
23
- input: string;
24
- /**
25
- * Current messages in the chat.
26
- *
27
- * This is writable, which is useful when you want to edit the messages on the client, and then
28
- * trigger {@link reload} to regenerate the AI response.
29
- */
30
- get messages(): UIMessage<MESSAGE_METADATA>[];
31
- set messages(value: UIMessage<MESSAGE_METADATA>[]);
32
- constructor(options?: ChatOptions<MESSAGE_METADATA>);
33
+ set error(value: Error | undefined);
34
+ constructor(options?: () => ChatOptions<MESSAGE_METADATA, DATA_PART_SCHEMAS>);
33
35
  /**
34
36
  * Append a user message to the chat list. This triggers the API call to fetch
35
37
  * the assistant's response.
36
38
  * @param message The message to append
37
39
  * @param options Additional options to pass to the API call
38
40
  */
39
- append: (message: UIMessage<MESSAGE_METADATA> | CreateUIMessage<MESSAGE_METADATA>, { headers, body }?: ChatRequestOptions) => Promise<void>;
41
+ append: (message: UIMessage<MESSAGE_METADATA, InferUIDataParts<DATA_PART_SCHEMAS>> | CreateUIMessage<MESSAGE_METADATA, InferUIDataParts<DATA_PART_SCHEMAS>>, { headers, body }?: ChatRequestOptions) => Promise<void>;
40
42
  /**
41
43
  * Reload the last AI chat response for the given chat history. If the last
42
44
  * message isn't from the assistant, it will request the API to generate a
@@ -1 +1 @@
1
- {"version":3,"file":"chat.svelte.d.ts","sourceRoot":"","sources":["../src/chat.svelte.ts"],"names":[],"mappings":"AACA,OAAO,EASL,KAAK,kBAAkB,EACvB,KAAK,eAAe,EACpB,KAAK,sBAAsB,EAC3B,KAAK,SAAS,EACf,MAAM,IAAI,CAAC;AAQZ,MAAM,MAAM,WAAW,CAAC,gBAAgB,GAAG,OAAO,IAAI,QAAQ,CAC5D,sBAAsB,CAAC,gBAAgB,CAAC,CACzC,CAAC;AAEF,YAAY,EAAE,eAAe,EAAE,SAAS,EAAE,CAAC;AAE3C,qBAAa,IAAI,CAAC,gBAAgB,GAAG,OAAO;;IAS1C;;;OAGG;IACH,QAAQ,CAAC,MAAM,SAAwD;IASvE;;;;;;;OAOG;IACH,IAAI,MAAM,kDAET;IAED,0CAA0C;IAC1C,IAAI,KAAK,sBAER;IAED,mFAAmF;IACnF,KAAK,SAAqB;IAE1B;;;;;OAKG;IACH,IAAI,QAAQ,IAAI,SAAS,CAAC,gBAAgB,CAAC,EAAE,CAE5C;IACD,IAAI,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC,gBAAgB,CAAC,EAAE,EAEhD;gBAEW,OAAO,GAAE,WAAW,CAAC,gBAAgB,CAAM;IAYvD;;;;;OAKG;IACH,MAAM,YACK,SAAS,CAAC,gBAAgB,CAAC,GAAG,eAAe,CAAC,gBAAgB,CAAC,sBACrD,kBAAkB,mBAQrC;IAEF;;;;OAIG;IACH,MAAM,uBAA6B,kBAAkB,mBAcnD;IAEF;;OAEG;IACH,IAAI,aASF;IAEF,qFAAqF;IACrF,YAAY,WACF;QAAE,cAAc,CAAC,EAAE,MAAM,IAAI,CAAA;KAAE,YAC9B,kBAAkB,GAAG;QAAE,KAAK,CAAC,EAAE,QAAQ,CAAA;KAAE,mBAwBlD;IAEF,aAAa,4BAGV;QACD,UAAU,EAAE,MAAM,CAAC;QACnB,MAAM,EAAE,OAAO,CAAC;KACjB,mBAmBC;CA4FH"}
1
+ {"version":3,"file":"chat.svelte.d.ts","sourceRoot":"","sources":["../src/chat.svelte.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,kBAAkB,EACvB,KAAK,UAAU,EACf,KAAK,eAAe,EAEpB,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACtB,KAAK,SAAS,EACd,KAAK,cAAc,EACpB,MAAM,IAAI,CAAC;AAOZ,MAAM,MAAM,WAAW,CACrB,gBAAgB,GAAG,OAAO,EAC1B,iBAAiB,SAAS,iBAAiB,GAAG,iBAAiB,IAC7D,QAAQ,CAAC,cAAc,CAAC,gBAAgB,EAAE,iBAAiB,CAAC,CAAC,CAAC;AAElE,YAAY,EAAE,eAAe,EAAE,SAAS,EAAE,CAAC;AAE3C,qBAAa,IAAI,CACf,gBAAgB,GAAG,OAAO,EAC1B,iBAAiB,SAAS,iBAAiB,GAAG,iBAAiB;;IAK/D;;;OAGG;IACH,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAExB,mFAAmF;IACnF,KAAK,SAAsB;IAE3B;;;;;OAKG;IACH,IAAI,QAAQ,IAAI,SAAS,CACvB,gBAAgB,EAChB,gBAAgB,CAAC,iBAAiB,CAAC,CACpC,EAAE,CAEF;IACD,IAAI,QAAQ,CACV,QAAQ,EAAE,SAAS,CACjB,gBAAgB,EAChB,gBAAgB,CAAC,iBAAiB,CAAC,CACpC,EAAE,EAGJ;IAED;;;;;;;OAOG;IACH,IAAI,MAAM,IAAI,UAAU,CAEvB;IACD,IAAI,MAAM,CAAC,KAAK,EAAE,UAAU,EAE3B;IAED,0CAA0C;IAC1C,IAAI,KAAK,IAAI,KAAK,GAAG,SAAS,CAE7B;IACD,IAAI,KAAK,CAAC,KAAK,EAAE,KAAK,GAAG,SAAS,EAMjC;gBAGC,OAAO,GAAE,MAAM,WAAW,CACxB,gBAAgB,EAChB,iBAAiB,CACL;IAkChB;;;;;OAKG;IACH,MAAM,YAEA,SAAS,CAAC,gBAAgB,EAAE,gBAAgB,CAAC,iBAAiB,CAAC,CAAC,GAChE,eAAe,CAAC,gBAAgB,EAAE,gBAAgB,CAAC,iBAAiB,CAAC,CAAC,sBACvD,kBAAkB,mBAWrC;IAEF;;;;OAIG;IACH,MAAM,uBAA6B,kBAAkB,mBASnD;IAEF;;OAEG;IACH,IAAI,aAEF;IAEF,qFAAqF;IACrF,YAAY,WACF;QAAE,cAAc,CAAC,EAAE,MAAM,IAAI,CAAA;KAAE,YAC9B,kBAAkB,GAAG;QAAE,KAAK,CAAC,EAAE,QAAQ,CAAA;KAAE,mBAwBlD;IAEF,aAAa,4BAGV;QACD,UAAU,EAAE,MAAM,CAAC;QACnB,MAAM,EAAE,OAAO,CAAC;KACjB,mBAMC;CACH"}
@@ -1,22 +1,28 @@
1
- import { isAbortError } from '@ai-sdk/provider-utils';
2
- import { callChatApi, convertFileListToFileUIParts, extractMaxToolInvocationStep, generateId, getToolInvocations, isAssistantMessageWithCompletedToolCalls, shouldResubmitMessages, updateToolCallResult, } from 'ai';
3
- import { untrack } from 'svelte';
4
- import { KeyedChatStore, getChatContext, hasChatContext, } from './chat-context.svelte.js';
1
+ import { ChatStore, convertFileListToFileUIParts, defaultChatStoreOptions, generateId, } from 'ai';
2
+ import { getChatStoreContext, hasChatStoreContext, createChatStore, } from './chat-store.svelte.js';
5
3
  export class Chat {
6
- #options = {};
7
- #api = $derived(this.#options.api ?? '/api/chat');
8
- #generateId = $derived(this.#options.generateId ?? generateId);
9
- #maxSteps = $derived(this.#options.maxSteps ?? 1);
10
- #streamProtocol = $derived(this.#options.streamProtocol ?? 'ui-message');
11
- #keyedStore = $state();
4
+ #options;
5
+ #generateId;
6
+ #chatStore;
12
7
  /**
13
8
  * The id of the chat. If not provided through the constructor, a random ID will be generated
14
9
  * using the provided `generateId` function, or a built-in function if not provided.
15
10
  */
16
- chatId = $derived(this.#options.chatId ?? this.#generateId());
17
- #store = $derived(this.#keyedStore.get(this.chatId));
18
- #messageMetadataSchema = $derived(this.#options.messageMetadataSchema);
19
- #abortController;
11
+ chatId;
12
+ /** The current value of the input. Writable, so it can be bound to form inputs. */
13
+ input = $state('');
14
+ /**
15
+ * Current messages in the chat.
16
+ *
17
+ * This is writable, which is useful when you want to edit the messages on the client, and then
18
+ * trigger {@link reload} to regenerate the AI response.
19
+ */
20
+ get messages() {
21
+ return this.#chatStore.getMessages(this.chatId);
22
+ }
23
+ set messages(messages) {
24
+ this.#chatStore.setMessages({ id: this.chatId, messages });
25
+ }
20
26
  /**
21
27
  * Hook status:
22
28
  *
@@ -26,36 +32,48 @@ export class Chat {
26
32
  * - `error`: An error occurred during the API request, preventing successful completion.
27
33
  */
28
34
  get status() {
29
- return this.#store.status;
35
+ return this.#chatStore.getStatus(this.chatId);
36
+ }
37
+ set status(value) {
38
+ this.#chatStore.setStatus({ id: this.chatId, status: value });
30
39
  }
31
40
  /** The error object of the API request */
32
41
  get error() {
33
- return this.#store.error;
42
+ return this.#chatStore.getError(this.chatId);
34
43
  }
35
- /** The current value of the input. Writable, so it can be bound to form inputs. */
36
- input = $state();
37
- /**
38
- * Current messages in the chat.
39
- *
40
- * This is writable, which is useful when you want to edit the messages on the client, and then
41
- * trigger {@link reload} to regenerate the AI response.
42
- */
43
- get messages() {
44
- return this.#store.messages;
45
- }
46
- set messages(value) {
47
- untrack(() => (this.#store.messages = value));
44
+ set error(value) {
45
+ this.#chatStore.setStatus({
46
+ id: this.chatId,
47
+ status: 'error',
48
+ error: value,
49
+ });
48
50
  }
49
- constructor(options = {}) {
50
- if (hasChatContext()) {
51
- this.#keyedStore = getChatContext();
51
+ constructor(options = () => ({})) {
52
+ this.#options = $derived.by(options);
53
+ this.#generateId = $derived(this.#options.generateId ?? generateId);
54
+ this.chatId = $derived(this.#options.chatId ?? this.#generateId());
55
+ if (this.#options.chatStore) {
56
+ if (typeof this.#options.chatStore === 'function') {
57
+ this.#chatStore = createChatStore(this.#options.chatStore());
58
+ }
59
+ else {
60
+ this.#chatStore = this.#options.chatStore;
61
+ }
62
+ }
63
+ else if (hasChatStoreContext()) {
64
+ this.#chatStore = getChatStoreContext();
52
65
  }
53
66
  else {
54
- this.#keyedStore = new KeyedChatStore();
67
+ this.#chatStore = createChatStore(defaultChatStoreOptions({
68
+ api: '/api/chat',
69
+ generateId: this.#options.generateId || generateId,
70
+ })());
71
+ }
72
+ this.input = this.#options.initialInput ?? '';
73
+ if (!this.#chatStore.hasChat(this.chatId)) {
74
+ const messages = $state([]);
75
+ this.#chatStore.addChat(this.chatId, messages);
55
76
  }
56
- this.#options = options;
57
- this.messages = options.initialMessages ?? [];
58
- this.input = options.initialInput ?? '';
59
77
  }
60
78
  /**
61
79
  * Append a user message to the chat list. This triggers the API call to fetch
@@ -64,11 +82,15 @@ export class Chat {
64
82
  * @param options Additional options to pass to the API call
65
83
  */
66
84
  append = async (message, { headers, body } = {}) => {
67
- const messages = this.messages.concat({
68
- ...message,
69
- id: message.id ?? this.#generateId(),
85
+ await this.#chatStore.submitMessage({
86
+ chatId: this.chatId,
87
+ message,
88
+ headers,
89
+ body,
90
+ onError: this.#options.onError,
91
+ onToolCall: this.#options.onToolCall,
92
+ onFinish: this.#options.onFinish,
70
93
  });
71
- return this.#triggerRequest({ messages, headers, body });
72
94
  };
73
95
  /**
74
96
  * Reload the last AI chat response for the given chat history. If the last
@@ -76,32 +98,20 @@ export class Chat {
76
98
  * new response.
77
99
  */
78
100
  reload = async ({ headers, body } = {}) => {
79
- if (this.messages.length === 0) {
80
- return;
81
- }
82
- const lastMessage = this.messages[this.messages.length - 1];
83
- await this.#triggerRequest({
84
- messages: lastMessage.role === 'assistant'
85
- ? this.messages.slice(0, -1)
86
- : this.messages,
101
+ await this.#chatStore.resubmitLastUserMessage({
102
+ chatId: this.chatId,
87
103
  headers,
88
104
  body,
105
+ onError: this.#options.onError,
106
+ onToolCall: this.#options.onToolCall,
107
+ onFinish: this.#options.onFinish,
89
108
  });
90
109
  };
91
110
  /**
92
111
  * Abort the current request immediately, keep the generated tokens if any.
93
112
  */
94
113
  stop = () => {
95
- try {
96
- this.#abortController?.abort();
97
- }
98
- catch {
99
- // ignore
100
- }
101
- finally {
102
- this.#store.status = 'ready';
103
- this.#abortController = undefined;
104
- }
114
+ this.#chatStore.stopStream({ chatId: this.chatId });
105
115
  };
106
116
  /** Form submission handler to automatically reset input and append a user message */
107
117
  handleSubmit = async (event, options = {}) => {
@@ -111,13 +121,11 @@ export class Chat {
111
121
  : await convertFileListToFileUIParts(options?.files);
112
122
  if (!this.input && fileParts.length === 0)
113
123
  return;
114
- const messages = this.messages.concat({
124
+ const request = this.append({
115
125
  id: this.#generateId(),
116
126
  role: 'user',
117
127
  parts: [...fileParts, { type: 'text', text: this.input }],
118
- });
119
- const request = this.#triggerRequest({
120
- messages,
128
+ }, {
121
129
  headers: options.headers,
122
130
  body: options.body,
123
131
  });
@@ -125,89 +133,10 @@ export class Chat {
125
133
  await request;
126
134
  };
127
135
  addToolResult = async ({ toolCallId, result, }) => {
128
- updateToolCallResult({
129
- messages: this.messages,
136
+ await this.#chatStore.addToolResult({
137
+ chatId: this.chatId,
130
138
  toolCallId,
131
- toolResult: result,
139
+ result,
132
140
  });
133
- // when the request is ongoing, the auto-submit will be triggered after the request is finished
134
- if (this.#store.status === 'submitted' ||
135
- this.#store.status === 'streaming') {
136
- return;
137
- }
138
- const lastMessage = this.messages[this.messages.length - 1];
139
- if (isAssistantMessageWithCompletedToolCalls(lastMessage)) {
140
- await this.#triggerRequest({ messages: this.messages });
141
- }
142
- };
143
- #triggerRequest = async (chatRequest) => {
144
- this.#store.status = 'submitted';
145
- this.#store.error = undefined;
146
- const messages = chatRequest.messages;
147
- const messageCount = messages.length;
148
- const maxStep = extractMaxToolInvocationStep(getToolInvocations(messages[messages.length - 1]));
149
- try {
150
- const abortController = new AbortController();
151
- this.#abortController = abortController;
152
- // Optimistically update messages
153
- this.messages = messages;
154
- await callChatApi({
155
- api: this.#api,
156
- body: {
157
- id: this.chatId,
158
- messages,
159
- ...$state.snapshot(this.#options.body),
160
- ...chatRequest.body,
161
- },
162
- streamProtocol: this.#streamProtocol,
163
- credentials: this.#options.credentials,
164
- headers: {
165
- ...this.#options.headers,
166
- ...chatRequest.headers,
167
- },
168
- abortController: () => abortController,
169
- onUpdate: ({ message }) => {
170
- this.#store.status = 'streaming';
171
- const replaceLastMessage = message.id === messages[messages.length - 1].id;
172
- this.messages = messages;
173
- if (replaceLastMessage) {
174
- this.messages[this.messages.length - 1] = message;
175
- }
176
- else {
177
- this.messages.push(message);
178
- }
179
- },
180
- onToolCall: this.#options.onToolCall,
181
- onFinish: this.#options.onFinish,
182
- generateId: this.#generateId,
183
- fetch: this.#options.fetch,
184
- // callChatApi calls structuredClone on the message
185
- lastMessage: $state.snapshot(this.messages[this.messages.length - 1]),
186
- messageMetadataSchema: this.#messageMetadataSchema,
187
- });
188
- this.#abortController = undefined;
189
- this.#store.status = 'ready';
190
- }
191
- catch (error) {
192
- if (isAbortError(error)) {
193
- return;
194
- }
195
- const coalescedError = error instanceof Error ? error : new Error(String(error));
196
- if (this.#options.onError) {
197
- this.#options.onError(coalescedError);
198
- }
199
- this.#store.status = 'error';
200
- this.#store.error = coalescedError;
201
- }
202
- // auto-submit when all tool calls in the last assistant message have results
203
- // and assistant has not answered yet
204
- if (shouldResubmitMessages({
205
- originalMaxToolInvocationStep: maxStep,
206
- originalMessageCount: messageCount,
207
- maxSteps: this.#maxSteps,
208
- messages: this.messages,
209
- })) {
210
- await this.#triggerRequest({ messages: this.messages });
211
- }
212
141
  };
213
142
  }
@@ -1,2 +1,4 @@
1
- export declare function createAIContext(): void;
1
+ import { type ChatStore } from 'ai';
2
+ export declare function createAIContext(chatStore?: ChatStore): void;
3
+ export declare function createChatStoreContext(chatStore?: ChatStore): void;
2
4
  //# sourceMappingURL=context-provider.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"context-provider.d.ts","sourceRoot":"","sources":["../src/context-provider.ts"],"names":[],"mappings":"AAUA,wBAAgB,eAAe,SAS9B"}
1
+ {"version":3,"file":"context-provider.d.ts","sourceRoot":"","sources":["../src/context-provider.ts"],"names":[],"mappings":"AAAA,OAAO,EAA2B,KAAK,SAAS,EAAE,MAAM,IAAI,CAAC;AAW7D,wBAAgB,eAAe,CAAC,SAAS,CAAC,EAAE,SAAS,QAQpD;AAED,wBAAgB,sBAAsB,CAAC,SAAS,CAAC,EAAE,SAAS,QAS3D"}
@@ -1,11 +1,17 @@
1
- import { KeyedChatStore, setChatContext } from './chat-context.svelte.js';
1
+ import { defaultChatStoreOptions } from 'ai';
2
+ import { createChatStore, setChatStoreContext } from './chat-store.svelte.js';
2
3
  import { KeyedCompletionStore, setCompletionContext, } from './completion-context.svelte.js';
3
4
  import { KeyedStructuredObjectStore, setStructuredObjectContext, } from './structured-object-context.svelte.js';
4
- export function createAIContext() {
5
- const chatStore = new KeyedChatStore();
6
- setChatContext(chatStore);
5
+ export function createAIContext(chatStore) {
6
+ createChatStoreContext(chatStore);
7
7
  const completionStore = new KeyedCompletionStore();
8
8
  setCompletionContext(completionStore);
9
9
  const objectStore = new KeyedStructuredObjectStore();
10
10
  setStructuredObjectContext(objectStore);
11
11
  }
12
+ export function createChatStoreContext(chatStore) {
13
+ setChatStoreContext(chatStore ??
14
+ createChatStore(defaultChatStoreOptions({
15
+ api: '/api/chat',
16
+ })()));
17
+ }
package/dist/index.d.ts CHANGED
@@ -2,4 +2,5 @@ export { Chat, type ChatOptions, type CreateUIMessage, type UIMessage, } from '.
2
2
  export { StructuredObject as Experimental_StructuredObject, type Experimental_StructuredObjectOptions, } from './structured-object.svelte.js';
3
3
  export { Completion, type CompletionOptions } from './completion.svelte.js';
4
4
  export { createAIContext } from './context-provider.js';
5
+ export { createChatStore } from './chat-store.svelte.js';
5
6
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,IAAI,EACJ,KAAK,WAAW,EAChB,KAAK,eAAe,EACpB,KAAK,SAAS,GACf,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EACL,gBAAgB,IAAI,6BAA6B,EACjD,KAAK,oCAAoC,GAC1C,MAAM,+BAA+B,CAAC;AAEvC,OAAO,EAAE,UAAU,EAAE,KAAK,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAE5E,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,IAAI,EACJ,KAAK,WAAW,EAChB,KAAK,eAAe,EACpB,KAAK,SAAS,GACf,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EACL,gBAAgB,IAAI,6BAA6B,EACjD,KAAK,oCAAoC,GAC1C,MAAM,+BAA+B,CAAC;AAEvC,OAAO,EAAE,UAAU,EAAE,KAAK,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAE5E,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAExD,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC"}
package/dist/index.js CHANGED
@@ -2,3 +2,4 @@ export { Chat, } from './chat.svelte.js';
2
2
  export { StructuredObject as Experimental_StructuredObject, } from './structured-object.svelte.js';
3
3
  export { Completion } from './completion.svelte.js';
4
4
  export { createAIContext } from './context-provider.js';
5
+ export { createChatStore } from './chat-store.svelte.js';
@@ -1,12 +1,16 @@
1
1
  <script lang="ts">
2
+ import { createChatStore } from '../chat-store.svelte.js';
2
3
  import { Chat } from '../chat.svelte.js';
4
+ import { defaultChatStoreOptions } from 'ai';
3
5
  import { createAIContext } from '../context-provider.js';
4
6
 
5
7
  let { chatId }: { chatId?: string } = $props();
6
8
 
7
- createAIContext();
8
- const chat1 = new Chat({ chatId });
9
- const chat2 = new Chat({ chatId });
9
+ createAIContext(
10
+ createChatStore(defaultChatStoreOptions({ api: '/api/chat' })()),
11
+ );
12
+ const chat1 = new Chat(() => ({ chatId }));
13
+ const chat2 = new Chat(() => ({ chatId }));
10
14
 
11
15
  export { chat1, chat2 };
12
16
  </script>
@@ -3,8 +3,8 @@ type $$ComponentProps = {
3
3
  chatId?: string;
4
4
  };
5
5
  declare const ChatSynchronization: import("svelte").Component<$$ComponentProps, {
6
- chat1: Chat<unknown>;
7
- chat2: Chat<unknown>;
6
+ chat1: Chat<unknown, import("ai").UIDataPartSchemas>;
7
+ chat2: Chat<unknown, import("ai").UIDataPartSchemas>;
8
8
  }, "">;
9
9
  type ChatSynchronization = ReturnType<typeof ChatSynchronization>;
10
10
  export default ChatSynchronization;
@@ -1 +1 @@
1
- {"version":3,"file":"chat-synchronization.svelte.d.ts","sourceRoot":"","sources":["../../src/tests/chat-synchronization.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AAGxC,KAAK,gBAAgB,GAAI;IAAE,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAgB9C,QAAA,MAAM,mBAAmB;;;MAAsC,CAAC;AAChE,KAAK,mBAAmB,GAAG,UAAU,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAClE,eAAe,mBAAmB,CAAC"}
1
+ {"version":3,"file":"chat-synchronization.svelte.d.ts","sourceRoot":"","sources":["../../src/tests/chat-synchronization.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AAIxC,KAAK,gBAAgB,GAAI;IAAE,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAoB9C,QAAA,MAAM,mBAAmB;;;MAAsC,CAAC;AAChE,KAAK,mBAAmB,GAAG,UAAU,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAClE,eAAe,mBAAmB,CAAC"}
@@ -1,4 +1,4 @@
1
- import { hasContext, getContext, setContext, untrack } from 'svelte';
1
+ import { getContext, hasContext, setContext, untrack } from 'svelte';
2
2
  import { SvelteMap } from 'svelte/reactivity';
3
3
  export function createContext(name) {
4
4
  const key = Symbol(name);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdk/svelte",
3
- "version": "3.0.0-alpha.3",
3
+ "version": "3.0.0-alpha.5",
4
4
  "license": "Apache-2.0",
5
5
  "files": [
6
6
  "dist",
@@ -27,7 +27,7 @@
27
27
  "access": "public"
28
28
  },
29
29
  "peerDependencies": {
30
- "svelte": "^5.0.0",
30
+ "svelte": "^5.31.0",
31
31
  "zod": "^3.23.8"
32
32
  },
33
33
  "peerDependenciesMeta": {
@@ -36,8 +36,8 @@
36
36
  }
37
37
  },
38
38
  "dependencies": {
39
- "ai": "5.0.0-alpha.3",
40
- "@ai-sdk/provider-utils": "3.0.0-alpha.3"
39
+ "ai": "5.0.0-alpha.5",
40
+ "@ai-sdk/provider-utils": "3.0.0-alpha.4"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@types/node": "20.17.24",
@@ -52,7 +52,7 @@
52
52
  "globals": "^16.0.0",
53
53
  "jsdom": "^26.0.0",
54
54
  "publint": "^0.3.2",
55
- "svelte": "^5.0.0",
55
+ "svelte": "^5.31.0",
56
56
  "svelte-check": "^4.0.0",
57
57
  "typescript": "^5.0.0",
58
58
  "typescript-eslint": "^8.20.0",
@@ -0,0 +1,85 @@
1
+ import {
2
+ ChatStore,
3
+ SerialJobExecutor,
4
+ type ActiveResponse,
5
+ type Chat,
6
+ type ChatStatus,
7
+ type ChatStoreOptions,
8
+ type InferUIDataParts,
9
+ type UIDataPartSchemas,
10
+ type UIDataTypes,
11
+ type UIMessage,
12
+ } from 'ai';
13
+ import { createContext } from './utils.svelte.js';
14
+
15
+ export const {
16
+ hasContext: hasChatStoreContext,
17
+ getContext: getChatStoreContext,
18
+ setContext: setChatStoreContext,
19
+ } = createContext<ChatStore>('ChatStore');
20
+
21
+ class SvelteChat<MESSAGE_METADATA, DATA_TYPES extends UIDataTypes>
22
+ implements Chat<MESSAGE_METADATA, DATA_TYPES>
23
+ {
24
+ messages: UIMessage<MESSAGE_METADATA, DATA_TYPES>[];
25
+ status = $state<ChatStatus>('ready');
26
+ error = $state<Error | undefined>(undefined);
27
+ activeResponse: ActiveResponse<MESSAGE_METADATA> | undefined = undefined;
28
+ jobExecutor = new SerialJobExecutor();
29
+
30
+ constructor(messages?: UIMessage<MESSAGE_METADATA, DATA_TYPES>[]) {
31
+ this.messages = $state(messages ?? []);
32
+ }
33
+
34
+ setStatus = (status: ChatStatus) => {
35
+ this.status = status;
36
+ };
37
+
38
+ setError = (error: Error | undefined) => {
39
+ this.error = error;
40
+ };
41
+
42
+ setActiveResponse = (
43
+ activeResponse: ActiveResponse<MESSAGE_METADATA> | undefined,
44
+ ) => {
45
+ this.activeResponse = activeResponse;
46
+ };
47
+
48
+ setMessages = (messages: UIMessage<MESSAGE_METADATA, DATA_TYPES>[]) => {
49
+ this.messages = messages;
50
+ };
51
+
52
+ pushMessage = (message: UIMessage<MESSAGE_METADATA, DATA_TYPES>) => {
53
+ this.messages.push(message);
54
+ };
55
+
56
+ popMessage = () => {
57
+ this.messages.pop();
58
+ };
59
+
60
+ replaceMessage = (
61
+ index: number,
62
+ message: UIMessage<MESSAGE_METADATA, DATA_TYPES>,
63
+ ) => {
64
+ this.messages[index] = message;
65
+ };
66
+
67
+ snapshot = <T>(thing: T): T => {
68
+ return $state.snapshot(thing) as T;
69
+ };
70
+ }
71
+
72
+ export function createChatStore<
73
+ MESSAGE_METADATA = unknown,
74
+ DATA_PART_SCHEMAS extends UIDataPartSchemas = UIDataPartSchemas,
75
+ >(
76
+ options: ChatStoreOptions<MESSAGE_METADATA, DATA_PART_SCHEMAS>,
77
+ ): ChatStore<MESSAGE_METADATA, DATA_PART_SCHEMAS> {
78
+ return new ChatStore<MESSAGE_METADATA, DATA_PART_SCHEMAS>({
79
+ ...options,
80
+ createChat: options =>
81
+ new SvelteChat<MESSAGE_METADATA, InferUIDataParts<DATA_PART_SCHEMAS>>(
82
+ options.messages,
83
+ ),
84
+ });
85
+ }
@@ -1,52 +1,66 @@
1
- import { isAbortError } from '@ai-sdk/provider-utils';
2
1
  import {
3
- callChatApi,
2
+ ChatStore,
4
3
  convertFileListToFileUIParts,
5
- extractMaxToolInvocationStep,
4
+ defaultChatStoreOptions,
6
5
  generateId,
7
- getToolInvocations,
8
- isAssistantMessageWithCompletedToolCalls,
9
- shouldResubmitMessages,
10
- updateToolCallResult,
11
6
  type ChatRequestOptions,
7
+ type ChatStatus,
12
8
  type CreateUIMessage,
13
- type OriginalUseChatOptions,
9
+ type IdGenerator,
10
+ type InferUIDataParts,
11
+ type UIDataPartSchemas,
14
12
  type UIMessage,
13
+ type UseChatOptions,
15
14
  } from 'ai';
16
- import { untrack } from 'svelte';
17
15
  import {
18
- KeyedChatStore,
19
- getChatContext,
20
- hasChatContext,
21
- } from './chat-context.svelte.js';
16
+ getChatStoreContext,
17
+ hasChatStoreContext,
18
+ createChatStore,
19
+ } from './chat-store.svelte.js';
22
20
 
23
- export type ChatOptions<MESSAGE_METADATA = unknown> = Readonly<
24
- OriginalUseChatOptions<MESSAGE_METADATA>
25
- >;
21
+ export type ChatOptions<
22
+ MESSAGE_METADATA = unknown,
23
+ DATA_PART_SCHEMAS extends UIDataPartSchemas = UIDataPartSchemas,
24
+ > = Readonly<UseChatOptions<MESSAGE_METADATA, DATA_PART_SCHEMAS>>;
26
25
 
27
26
  export type { CreateUIMessage, UIMessage };
28
27
 
29
- export class Chat<MESSAGE_METADATA = unknown> {
30
- readonly #options: ChatOptions<MESSAGE_METADATA> = {};
31
- readonly #api = $derived(this.#options.api ?? '/api/chat');
32
- readonly #generateId = $derived(this.#options.generateId ?? generateId);
33
- readonly #maxSteps = $derived(this.#options.maxSteps ?? 1);
34
- readonly #streamProtocol = $derived(
35
- this.#options.streamProtocol ?? 'ui-message',
36
- );
37
- readonly #keyedStore = $state<KeyedChatStore<MESSAGE_METADATA>>()!;
28
+ export class Chat<
29
+ MESSAGE_METADATA = unknown,
30
+ DATA_PART_SCHEMAS extends UIDataPartSchemas = UIDataPartSchemas,
31
+ > {
32
+ readonly #options: ChatOptions<MESSAGE_METADATA, DATA_PART_SCHEMAS>;
33
+ readonly #generateId: IdGenerator;
34
+ readonly #chatStore: ChatStore<MESSAGE_METADATA, DATA_PART_SCHEMAS>;
38
35
  /**
39
36
  * The id of the chat. If not provided through the constructor, a random ID will be generated
40
37
  * using the provided `generateId` function, or a built-in function if not provided.
41
38
  */
42
- readonly chatId = $derived(this.#options.chatId ?? this.#generateId());
43
- readonly #store = $derived(this.#keyedStore.get(this.chatId));
39
+ readonly chatId: string;
44
40
 
45
- readonly #messageMetadataSchema = $derived(
46
- this.#options.messageMetadataSchema,
47
- );
41
+ /** The current value of the input. Writable, so it can be bound to form inputs. */
42
+ input = $state<string>('');
48
43
 
49
- #abortController: AbortController | undefined;
44
+ /**
45
+ * Current messages in the chat.
46
+ *
47
+ * This is writable, which is useful when you want to edit the messages on the client, and then
48
+ * trigger {@link reload} to regenerate the AI response.
49
+ */
50
+ get messages(): UIMessage<
51
+ MESSAGE_METADATA,
52
+ InferUIDataParts<DATA_PART_SCHEMAS>
53
+ >[] {
54
+ return this.#chatStore.getMessages(this.chatId);
55
+ }
56
+ set messages(
57
+ messages: UIMessage<
58
+ MESSAGE_METADATA,
59
+ InferUIDataParts<DATA_PART_SCHEMAS>
60
+ >[],
61
+ ) {
62
+ this.#chatStore.setMessages({ id: this.chatId, messages });
63
+ }
50
64
 
51
65
  /**
52
66
  * Hook status:
@@ -56,41 +70,61 @@ export class Chat<MESSAGE_METADATA = unknown> {
56
70
  * - `ready`: The full response has been received and processed; a new user message can be submitted.
57
71
  * - `error`: An error occurred during the API request, preventing successful completion.
58
72
  */
59
- get status() {
60
- return this.#store.status;
73
+ get status(): ChatStatus {
74
+ return this.#chatStore.getStatus(this.chatId);
61
75
  }
62
-
63
- /** The error object of the API request */
64
- get error() {
65
- return this.#store.error;
76
+ set status(value: ChatStatus) {
77
+ this.#chatStore.setStatus({ id: this.chatId, status: value });
66
78
  }
67
79
 
68
- /** The current value of the input. Writable, so it can be bound to form inputs. */
69
- input = $state<string>()!;
70
-
71
- /**
72
- * Current messages in the chat.
73
- *
74
- * This is writable, which is useful when you want to edit the messages on the client, and then
75
- * trigger {@link reload} to regenerate the AI response.
76
- */
77
- get messages(): UIMessage<MESSAGE_METADATA>[] {
78
- return this.#store.messages;
80
+ /** The error object of the API request */
81
+ get error(): Error | undefined {
82
+ return this.#chatStore.getError(this.chatId);
79
83
  }
80
- set messages(value: UIMessage<MESSAGE_METADATA>[]) {
81
- untrack(() => (this.#store.messages = value));
84
+ set error(value: Error | undefined) {
85
+ this.#chatStore.setStatus({
86
+ id: this.chatId,
87
+ status: 'error',
88
+ error: value,
89
+ });
82
90
  }
83
91
 
84
- constructor(options: ChatOptions<MESSAGE_METADATA> = {}) {
85
- if (hasChatContext()) {
86
- this.#keyedStore = getChatContext() as KeyedChatStore<MESSAGE_METADATA>;
92
+ constructor(
93
+ options: () => ChatOptions<
94
+ MESSAGE_METADATA,
95
+ DATA_PART_SCHEMAS
96
+ > = () => ({}),
97
+ ) {
98
+ this.#options = $derived.by(options);
99
+ this.#generateId = $derived(this.#options.generateId ?? generateId);
100
+ this.chatId = $derived(this.#options.chatId ?? this.#generateId());
101
+
102
+ if (this.#options.chatStore) {
103
+ if (typeof this.#options.chatStore === 'function') {
104
+ this.#chatStore = createChatStore(this.#options.chatStore());
105
+ } else {
106
+ this.#chatStore = this.#options.chatStore;
107
+ }
108
+ } else if (hasChatStoreContext()) {
109
+ this.#chatStore = getChatStoreContext() as ChatStore<
110
+ MESSAGE_METADATA,
111
+ DATA_PART_SCHEMAS
112
+ >;
87
113
  } else {
88
- this.#keyedStore = new KeyedChatStore<MESSAGE_METADATA>();
114
+ this.#chatStore = createChatStore(
115
+ defaultChatStoreOptions<MESSAGE_METADATA, DATA_PART_SCHEMAS>({
116
+ api: '/api/chat',
117
+ generateId: this.#options.generateId || generateId,
118
+ })(),
119
+ );
89
120
  }
90
121
 
91
- this.#options = options;
92
- this.messages = options.initialMessages ?? [];
93
- this.input = options.initialInput ?? '';
122
+ this.input = this.#options.initialInput ?? '';
123
+
124
+ if (!this.#chatStore.hasChat(this.chatId)) {
125
+ const messages = $state([]);
126
+ this.#chatStore.addChat(this.chatId, messages);
127
+ }
94
128
  }
95
129
 
96
130
  /**
@@ -100,15 +134,20 @@ export class Chat<MESSAGE_METADATA = unknown> {
100
134
  * @param options Additional options to pass to the API call
101
135
  */
102
136
  append = async (
103
- message: UIMessage<MESSAGE_METADATA> | CreateUIMessage<MESSAGE_METADATA>,
137
+ message:
138
+ | UIMessage<MESSAGE_METADATA, InferUIDataParts<DATA_PART_SCHEMAS>>
139
+ | CreateUIMessage<MESSAGE_METADATA, InferUIDataParts<DATA_PART_SCHEMAS>>,
104
140
  { headers, body }: ChatRequestOptions = {},
105
141
  ) => {
106
- const messages = this.messages.concat({
107
- ...message,
108
- id: message.id ?? this.#generateId(),
142
+ await this.#chatStore.submitMessage({
143
+ chatId: this.chatId,
144
+ message,
145
+ headers,
146
+ body,
147
+ onError: this.#options.onError,
148
+ onToolCall: this.#options.onToolCall,
149
+ onFinish: this.#options.onFinish,
109
150
  });
110
-
111
- return this.#triggerRequest({ messages, headers, body });
112
151
  };
113
152
 
114
153
  /**
@@ -117,18 +156,13 @@ export class Chat<MESSAGE_METADATA = unknown> {
117
156
  * new response.
118
157
  */
119
158
  reload = async ({ headers, body }: ChatRequestOptions = {}) => {
120
- if (this.messages.length === 0) {
121
- return;
122
- }
123
-
124
- const lastMessage = this.messages[this.messages.length - 1];
125
- await this.#triggerRequest({
126
- messages:
127
- lastMessage.role === 'assistant'
128
- ? this.messages.slice(0, -1)
129
- : this.messages,
159
+ await this.#chatStore.resubmitLastUserMessage({
160
+ chatId: this.chatId,
130
161
  headers,
131
162
  body,
163
+ onError: this.#options.onError,
164
+ onToolCall: this.#options.onToolCall,
165
+ onFinish: this.#options.onFinish,
132
166
  });
133
167
  };
134
168
 
@@ -136,14 +170,7 @@ export class Chat<MESSAGE_METADATA = unknown> {
136
170
  * Abort the current request immediately, keep the generated tokens if any.
137
171
  */
138
172
  stop = () => {
139
- try {
140
- this.#abortController?.abort();
141
- } catch {
142
- // ignore
143
- } finally {
144
- this.#store.status = 'ready';
145
- this.#abortController = undefined;
146
- }
173
+ this.#chatStore.stopStream({ chatId: this.chatId });
147
174
  };
148
175
 
149
176
  /** Form submission handler to automatically reset input and append a user message */
@@ -159,17 +186,17 @@ export class Chat<MESSAGE_METADATA = unknown> {
159
186
 
160
187
  if (!this.input && fileParts.length === 0) return;
161
188
 
162
- const messages = this.messages.concat({
163
- id: this.#generateId(),
164
- role: 'user',
165
- parts: [...fileParts, { type: 'text', text: this.input }],
166
- });
167
-
168
- const request = this.#triggerRequest({
169
- messages,
170
- headers: options.headers,
171
- body: options.body,
172
- });
189
+ const request = this.append(
190
+ {
191
+ id: this.#generateId(),
192
+ role: 'user',
193
+ parts: [...fileParts, { type: 'text', text: this.input }],
194
+ },
195
+ {
196
+ headers: options.headers,
197
+ body: options.body,
198
+ },
199
+ );
173
200
 
174
201
  this.input = '';
175
202
  await request;
@@ -182,114 +209,10 @@ export class Chat<MESSAGE_METADATA = unknown> {
182
209
  toolCallId: string;
183
210
  result: unknown;
184
211
  }) => {
185
- updateToolCallResult({
186
- messages: this.messages,
212
+ await this.#chatStore.addToolResult({
213
+ chatId: this.chatId,
187
214
  toolCallId,
188
- toolResult: result,
215
+ result,
189
216
  });
190
-
191
- // when the request is ongoing, the auto-submit will be triggered after the request is finished
192
- if (
193
- this.#store.status === 'submitted' ||
194
- this.#store.status === 'streaming'
195
- ) {
196
- return;
197
- }
198
-
199
- const lastMessage = this.messages[this.messages.length - 1];
200
- if (isAssistantMessageWithCompletedToolCalls(lastMessage)) {
201
- await this.#triggerRequest({ messages: this.messages });
202
- }
203
- };
204
-
205
- #triggerRequest = async (
206
- chatRequest: ChatRequestOptions & {
207
- messages: UIMessage<MESSAGE_METADATA>[];
208
- },
209
- ) => {
210
- this.#store.status = 'submitted';
211
- this.#store.error = undefined;
212
-
213
- const messages = chatRequest.messages;
214
- const messageCount = messages.length;
215
- const maxStep = extractMaxToolInvocationStep(
216
- getToolInvocations(messages[messages.length - 1]),
217
- );
218
-
219
- try {
220
- const abortController = new AbortController();
221
- this.#abortController = abortController;
222
-
223
- // Optimistically update messages
224
- this.messages = messages;
225
-
226
- await callChatApi({
227
- api: this.#api,
228
- body: {
229
- id: this.chatId,
230
- messages,
231
- ...$state.snapshot(this.#options.body),
232
- ...chatRequest.body,
233
- },
234
- streamProtocol: this.#streamProtocol,
235
- credentials: this.#options.credentials,
236
- headers: {
237
- ...this.#options.headers,
238
- ...chatRequest.headers,
239
- },
240
- abortController: () => abortController,
241
- onUpdate: ({ message }) => {
242
- this.#store.status = 'streaming';
243
-
244
- const replaceLastMessage =
245
- message.id === messages[messages.length - 1].id;
246
-
247
- this.messages = messages;
248
- if (replaceLastMessage) {
249
- this.messages[this.messages.length - 1] = message;
250
- } else {
251
- this.messages.push(message);
252
- }
253
- },
254
- onToolCall: this.#options.onToolCall,
255
- onFinish: this.#options.onFinish,
256
- generateId: this.#generateId,
257
- fetch: this.#options.fetch,
258
- // callChatApi calls structuredClone on the message
259
- lastMessage: $state.snapshot(
260
- this.messages[this.messages.length - 1],
261
- ) as UIMessage<MESSAGE_METADATA>,
262
- messageMetadataSchema: this.#messageMetadataSchema,
263
- });
264
-
265
- this.#abortController = undefined;
266
- this.#store.status = 'ready';
267
- } catch (error) {
268
- if (isAbortError(error)) {
269
- return;
270
- }
271
-
272
- const coalescedError =
273
- error instanceof Error ? error : new Error(String(error));
274
- if (this.#options.onError) {
275
- this.#options.onError(coalescedError);
276
- }
277
-
278
- this.#store.status = 'error';
279
- this.#store.error = coalescedError;
280
- }
281
-
282
- // auto-submit when all tool calls in the last assistant message have results
283
- // and assistant has not answered yet
284
- if (
285
- shouldResubmitMessages({
286
- originalMaxToolInvocationStep: maxStep,
287
- originalMessageCount: messageCount,
288
- maxSteps: this.#maxSteps,
289
- messages: this.messages,
290
- })
291
- ) {
292
- await this.#triggerRequest({ messages: this.messages });
293
- }
294
217
  };
295
218
  }
@@ -1,4 +1,5 @@
1
- import { KeyedChatStore, setChatContext } from './chat-context.svelte.js';
1
+ import { defaultChatStoreOptions, type ChatStore } from 'ai';
2
+ import { createChatStore, setChatStoreContext } from './chat-store.svelte.js';
2
3
  import {
3
4
  KeyedCompletionStore,
4
5
  setCompletionContext,
@@ -8,9 +9,8 @@ import {
8
9
  setStructuredObjectContext,
9
10
  } from './structured-object-context.svelte.js';
10
11
 
11
- export function createAIContext() {
12
- const chatStore = new KeyedChatStore();
13
- setChatContext(chatStore);
12
+ export function createAIContext(chatStore?: ChatStore) {
13
+ createChatStoreContext(chatStore);
14
14
 
15
15
  const completionStore = new KeyedCompletionStore();
16
16
  setCompletionContext(completionStore);
@@ -18,3 +18,14 @@ export function createAIContext() {
18
18
  const objectStore = new KeyedStructuredObjectStore();
19
19
  setStructuredObjectContext(objectStore);
20
20
  }
21
+
22
+ export function createChatStoreContext(chatStore?: ChatStore) {
23
+ setChatStoreContext(
24
+ chatStore ??
25
+ createChatStore(
26
+ defaultChatStoreOptions({
27
+ api: '/api/chat',
28
+ })(),
29
+ ),
30
+ );
31
+ }
package/src/index.ts CHANGED
@@ -13,3 +13,5 @@ export {
13
13
  export { Completion, type CompletionOptions } from './completion.svelte.js';
14
14
 
15
15
  export { createAIContext } from './context-provider.js';
16
+
17
+ export { createChatStore } from './chat-store.svelte.js';
@@ -1,12 +1,16 @@
1
1
  <script lang="ts">
2
+ import { createChatStore } from '../chat-store.svelte.js';
2
3
  import { Chat } from '../chat.svelte.js';
4
+ import { defaultChatStoreOptions } from 'ai';
3
5
  import { createAIContext } from '../context-provider.js';
4
6
 
5
7
  let { chatId }: { chatId?: string } = $props();
6
8
 
7
- createAIContext();
8
- const chat1 = new Chat({ chatId });
9
- const chat2 = new Chat({ chatId });
9
+ createAIContext(
10
+ createChatStore(defaultChatStoreOptions({ api: '/api/chat' })()),
11
+ );
12
+ const chat1 = new Chat(() => ({ chatId }));
13
+ const chat2 = new Chat(() => ({ chatId }));
10
14
 
11
15
  export { chat1, chat2 };
12
16
  </script>
@@ -1,4 +1,4 @@
1
- import { hasContext, getContext, setContext, untrack } from 'svelte';
1
+ import { getContext, hasContext, setContext, untrack } from 'svelte';
2
2
  import { SvelteMap } from 'svelte/reactivity';
3
3
 
4
4
  export function createContext<T>(name: string) {
@@ -1,13 +0,0 @@
1
- import { createContext, KeyedStore } from './utils.svelte.js';
2
- class ChatStore {
3
- messages = $state([]);
4
- data = $state();
5
- status = $state('ready');
6
- error = $state();
7
- }
8
- export class KeyedChatStore extends KeyedStore {
9
- constructor(value) {
10
- super(ChatStore, value);
11
- }
12
- }
13
- export const { hasContext: hasChatContext, getContext: getChatContext, setContext: setChatContext, } = createContext('Chat');
@@ -1,28 +0,0 @@
1
- import type { JSONValue, UIMessage } from 'ai';
2
- import { createContext, KeyedStore } from './utils.svelte.js';
3
-
4
- class ChatStore<MESSAGE_METADATA = unknown> {
5
- messages = $state<UIMessage<MESSAGE_METADATA>[]>([]);
6
- data = $state<JSONValue[]>();
7
- status = $state<'submitted' | 'streaming' | 'ready' | 'error'>('ready');
8
- error = $state<Error>();
9
- }
10
-
11
- export class KeyedChatStore<MESSAGE_METADATA = unknown> extends KeyedStore<
12
- ChatStore<MESSAGE_METADATA>
13
- > {
14
- constructor(
15
- value?:
16
- | Iterable<readonly [string, ChatStore<MESSAGE_METADATA>]>
17
- | null
18
- | undefined,
19
- ) {
20
- super(ChatStore, value);
21
- }
22
- }
23
-
24
- export const {
25
- hasContext: hasChatContext,
26
- getContext: getChatContext,
27
- setContext: setChatContext,
28
- } = createContext<KeyedChatStore>('Chat');