@assistant-ui/ai-sdk 0.0.1 → 0.0.3
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/converters/convertMessage.d.ts.map +1 -1
- package/dist/converters/toCreateMessage.d.ts.map +1 -1
- package/dist/converters/toCreateMessage.js +6 -2
- package/dist/converters/toCreateMessage.js.map +1 -1
- package/dist/model-context/injectInteractableContext.d.ts +1 -1
- package/dist/model-context/injectInteractableContext.js +1 -1
- package/dist/model-context/injectInteractableContext.js.map +1 -1
- package/dist/model-context/injectQuoteContext.d.ts +1 -1
- package/dist/model-context/injectQuoteContext.js +1 -1
- package/dist/model-context/injectQuoteContext.js.map +1 -1
- package/dist/runtime/useAISDKRuntime.d.ts +16 -3
- package/dist/runtime/useAISDKRuntime.d.ts.map +1 -1
- package/dist/runtime/useAISDKRuntime.js +29 -6
- package/dist/runtime/useAISDKRuntime.js.map +1 -1
- package/dist/runtime/useChatThread.d.ts +7 -1
- package/dist/runtime/useChatThread.d.ts.map +1 -1
- package/dist/runtime/useChatThread.js +9 -3
- package/dist/runtime/useChatThread.js.map +1 -1
- package/dist/runtime/useExternalHistory.d.ts.map +1 -1
- package/dist/runtime/useExternalHistory.js +10 -0
- package/dist/runtime/useExternalHistory.js.map +1 -1
- package/package.json +11 -11
- package/src/converters/toCreateMessage.ts +6 -5
- package/src/model-context/injectInteractableContext.ts +1 -1
- package/src/model-context/injectQuoteContext.ts +1 -1
- package/src/runtime/useAISDKRuntime.denied-tool.test.tsx +122 -0
- package/src/runtime/useAISDKRuntime.test.ts +191 -7
- package/src/runtime/useAISDKRuntime.ts +110 -51
- package/src/runtime/useChatRuntime.test.ts +16 -0
- package/src/runtime/useChatThread.ts +15 -0
- package/src/runtime/useExternalHistory.test.ts +79 -1
- package/src/runtime/useExternalHistory.ts +13 -0
|
@@ -35,7 +35,10 @@ import {
|
|
|
35
35
|
getExternalStoreMessages,
|
|
36
36
|
pickExternalStoreSharedOptions,
|
|
37
37
|
} from "@assistant-ui/core";
|
|
38
|
-
import {
|
|
38
|
+
import {
|
|
39
|
+
consumeSuggestionResult,
|
|
40
|
+
MessageRepository,
|
|
41
|
+
} from "@assistant-ui/core/internal";
|
|
39
42
|
import type { ReadonlyJSONObject } from "assistant-stream/utils";
|
|
40
43
|
import { sliceMessagesUntil } from "../utils/sliceMessagesUntil";
|
|
41
44
|
import { toCreateMessage } from "../converters/toCreateMessage";
|
|
@@ -70,48 +73,62 @@ const toUIMessage = <UI_MESSAGE extends UIMessage>(
|
|
|
70
73
|
role: createMessage.role ?? fallbackRole,
|
|
71
74
|
}) as UI_MESSAGE;
|
|
72
75
|
|
|
73
|
-
export type AISDKRuntimeAdapter =
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
76
|
+
export type AISDKRuntimeAdapter<UI_MESSAGE extends UIMessage = UIMessage> =
|
|
77
|
+
ExternalStoreSharedOptions & {
|
|
78
|
+
adapters?:
|
|
79
|
+
| (NonNullable<ExternalStoreAdapter["adapters"]> & {
|
|
80
|
+
history?: ThreadHistoryAdapter | undefined;
|
|
81
|
+
suggestion?: SuggestionAdapter | undefined;
|
|
82
|
+
})
|
|
83
|
+
| undefined;
|
|
84
|
+
toCreateMessage?: CustomToCreateMessageFunction;
|
|
85
|
+
/**
|
|
86
|
+
* Whether to automatically cancel pending interactive tool calls when the user sends a new message.
|
|
87
|
+
*
|
|
88
|
+
* When enabled (default), the pending tool calls will be marked as failed with an error message
|
|
89
|
+
* indicating the user cancelled the tool call by sending a new message.
|
|
90
|
+
*
|
|
91
|
+
* @default true
|
|
92
|
+
*/
|
|
93
|
+
cancelPendingToolCallsOnSend?: boolean | undefined;
|
|
94
|
+
/**
|
|
95
|
+
* Called when `runtime.thread.resumeRun(config)` is invoked.
|
|
96
|
+
*
|
|
97
|
+
* When omitted, `resumeRun` throws `"Runtime does not support resuming runs."`.
|
|
98
|
+
* Provide this to bridge resume invocations into a custom replay channel
|
|
99
|
+
* (for example, an SSE reconnect endpoint keyed by turn id).
|
|
100
|
+
*/
|
|
101
|
+
onResume?: ExternalStoreAdapter["onResume"];
|
|
102
|
+
/**
|
|
103
|
+
* Called when `runtime.thread.resumeToolCall(options)` is invoked for a tool call the in-process tracker does not own.
|
|
104
|
+
*
|
|
105
|
+
* When omitted, `resumeToolCall` throws `"Tool call ${toolCallId} is not waiting for resume."`.
|
|
106
|
+
* Provide this to bridge resume-tool-call invocations into a custom handler.
|
|
107
|
+
*/
|
|
108
|
+
onResumeToolCall?: ExternalStoreAdapter["onResumeToolCall"];
|
|
109
|
+
/**
|
|
110
|
+
* How consecutive assistant messages are rendered.
|
|
111
|
+
*
|
|
112
|
+
* `"concat-content"` (the default) merges them into a single thread message.
|
|
113
|
+
* `"none"` keeps each assistant message as its own thread message, which is
|
|
114
|
+
* useful when a backend persists proactive or consecutive assistant messages
|
|
115
|
+
* as separate entries.
|
|
116
|
+
*/
|
|
117
|
+
joinStrategy?: JoinStrategy | undefined;
|
|
118
|
+
/**
|
|
119
|
+
* A branch-aware AI SDK message tree seeded once when `useChat` is empty.
|
|
120
|
+
* After that seed, live updates come only from `useChat`. A later empty
|
|
121
|
+
* chat or a new object identity does not reload the tree.
|
|
122
|
+
*/
|
|
123
|
+
messageRepository?: MessageFormatRepository<UI_MESSAGE>;
|
|
124
|
+
/**
|
|
125
|
+
* Called after an explicit `switchToBranch` (for example a BranchPicker
|
|
126
|
+
* click). Complements `setMessages` and does not enable switching by itself.
|
|
127
|
+
*
|
|
128
|
+
* @deprecated This API is still under active development and might change without notice.
|
|
129
|
+
*/
|
|
130
|
+
unstable_onBranchChange?: ExternalStoreAdapter["unstable_onBranchChange"];
|
|
131
|
+
};
|
|
115
132
|
|
|
116
133
|
const EMPTY_SUGGESTIONS: readonly ThreadSuggestion[] = [];
|
|
117
134
|
|
|
@@ -190,7 +207,7 @@ const useGeneratedSuggestions = (
|
|
|
190
207
|
|
|
191
208
|
export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
|
|
192
209
|
chatHelpers: ReturnType<typeof useChat<UI_MESSAGE>>,
|
|
193
|
-
adapter: AISDKRuntimeAdapter = {},
|
|
210
|
+
adapter: AISDKRuntimeAdapter<UI_MESSAGE> = {},
|
|
194
211
|
) => {
|
|
195
212
|
const {
|
|
196
213
|
adapters,
|
|
@@ -199,6 +216,8 @@ export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
|
|
|
199
216
|
onResume,
|
|
200
217
|
onResumeToolCall,
|
|
201
218
|
joinStrategy,
|
|
219
|
+
messageRepository,
|
|
220
|
+
unstable_onBranchChange,
|
|
202
221
|
} = adapter;
|
|
203
222
|
const suggestionAdapter = adapters?.suggestion;
|
|
204
223
|
const contextAdapters = useRuntimeAdapters();
|
|
@@ -217,10 +236,9 @@ export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
|
|
|
217
236
|
const hasExecutingTools = Object.values(toolStatuses).some(
|
|
218
237
|
(s) => s?.type === "executing",
|
|
219
238
|
);
|
|
220
|
-
const
|
|
221
|
-
chatHelpers.status === "submitted" ||
|
|
222
|
-
|
|
223
|
-
hasExecutingTools;
|
|
239
|
+
const providerIsRunning =
|
|
240
|
+
chatHelpers.status === "submitted" || chatHelpers.status === "streaming";
|
|
241
|
+
const isRunning = providerIsRunning || hasExecutingTools;
|
|
224
242
|
|
|
225
243
|
const messageTiming = useStreamingTiming(chatHelpers.messages, isRunning);
|
|
226
244
|
|
|
@@ -248,6 +266,17 @@ export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
|
|
|
248
266
|
),
|
|
249
267
|
});
|
|
250
268
|
|
|
269
|
+
const exportedMessageRepository = useMemo(() => {
|
|
270
|
+
if (!messageRepository) return undefined;
|
|
271
|
+
const converted = toExportedMessageRepository(
|
|
272
|
+
AISDKMessageConverter.toThreadMessages as (
|
|
273
|
+
messages: UI_MESSAGE[],
|
|
274
|
+
) => ThreadMessage[],
|
|
275
|
+
messageRepository,
|
|
276
|
+
);
|
|
277
|
+
return converted.messages.length > 0 ? converted : undefined;
|
|
278
|
+
}, [messageRepository]);
|
|
279
|
+
|
|
251
280
|
const generatedSuggestions = useGeneratedSuggestions(
|
|
252
281
|
suggestionAdapter,
|
|
253
282
|
messages,
|
|
@@ -306,7 +335,11 @@ export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
|
|
|
306
335
|
let hasChanges = false;
|
|
307
336
|
const parts = lastMessage.parts?.map((part) => {
|
|
308
337
|
if (!isToolUIPart(part)) return part;
|
|
309
|
-
if (
|
|
338
|
+
if (
|
|
339
|
+
part.state === "output-available" ||
|
|
340
|
+
part.state === "output-error" ||
|
|
341
|
+
part.state === "output-denied"
|
|
342
|
+
)
|
|
310
343
|
return part;
|
|
311
344
|
|
|
312
345
|
hasChanges = true;
|
|
@@ -323,9 +356,17 @@ export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
|
|
|
323
356
|
});
|
|
324
357
|
};
|
|
325
358
|
|
|
359
|
+
const hasSeededRepositoryRef = useRef(false);
|
|
360
|
+
const shouldFeedRepository =
|
|
361
|
+
exportedMessageRepository != null &&
|
|
362
|
+
!hasSeededRepositoryRef.current &&
|
|
363
|
+
messages.length === 0;
|
|
364
|
+
|
|
326
365
|
const runtime = useExternalStoreRuntime({
|
|
327
|
-
isRunning,
|
|
328
|
-
|
|
366
|
+
isRunning: providerIsRunning,
|
|
367
|
+
...(shouldFeedRepository
|
|
368
|
+
? { messageRepository: exportedMessageRepository }
|
|
369
|
+
: { messages }),
|
|
329
370
|
unstable_enableToolInvocations: true,
|
|
330
371
|
setToolStatuses,
|
|
331
372
|
setMessages: (messages) =>
|
|
@@ -511,6 +552,7 @@ export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
|
|
|
511
552
|
...(suggestionAdapter ? { suggestions: generatedSuggestions } : {}),
|
|
512
553
|
...(onResume && { onResume }),
|
|
513
554
|
...(onResumeToolCall && { onResumeToolCall }),
|
|
555
|
+
...(unstable_onBranchChange && { unstable_onBranchChange }),
|
|
514
556
|
adapters: {
|
|
515
557
|
attachments: vercelAttachmentAdapter,
|
|
516
558
|
...contextAdapters,
|
|
@@ -520,5 +562,22 @@ export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
|
|
|
520
562
|
isLoading,
|
|
521
563
|
});
|
|
522
564
|
|
|
565
|
+
const setMessagesRef = useRef(chatHelpers.setMessages);
|
|
566
|
+
setMessagesRef.current = chatHelpers.setMessages;
|
|
567
|
+
|
|
568
|
+
useEffect(() => {
|
|
569
|
+
if (hasSeededRepositoryRef.current) return;
|
|
570
|
+
if (!exportedMessageRepository) return;
|
|
571
|
+
if (chatHelpers.messages.length > 0) {
|
|
572
|
+
hasSeededRepositoryRef.current = true;
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
const tempRepo = new MessageRepository();
|
|
576
|
+
tempRepo.import(exportedMessageRepository);
|
|
577
|
+
setMessagesRef.current(
|
|
578
|
+
tempRepo.getMessages().flatMap(getExternalStoreMessages<UI_MESSAGE>),
|
|
579
|
+
);
|
|
580
|
+
hasSeededRepositoryRef.current = true;
|
|
581
|
+
}, [exportedMessageRepository, chatHelpers.messages.length]);
|
|
523
582
|
return runtime;
|
|
524
583
|
};
|
|
@@ -96,6 +96,22 @@ describe("useChatRuntime", () => {
|
|
|
96
96
|
window.sessionStorage.clear();
|
|
97
97
|
});
|
|
98
98
|
|
|
99
|
+
it("forwards a defined chat update throttle to useChat", () => {
|
|
100
|
+
mocks.useChat.mockReturnValue({
|
|
101
|
+
resumeStream: vi.fn(),
|
|
102
|
+
status: "ready",
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
renderHook(() => useChatRuntime({ throttle: 50 }));
|
|
106
|
+
renderHook(() => useChatRuntime({ throttle: undefined }));
|
|
107
|
+
|
|
108
|
+
expect(mocks.useChat).toHaveBeenNthCalledWith(
|
|
109
|
+
1,
|
|
110
|
+
expect.objectContaining({ throttle: 50 }),
|
|
111
|
+
);
|
|
112
|
+
expect(mocks.useChat.mock.calls[1]?.[0]).not.toHaveProperty("throttle");
|
|
113
|
+
});
|
|
114
|
+
|
|
99
115
|
it("waits for external history to load before resuming a stream", async () => {
|
|
100
116
|
mocks.state.isLoadingHistory = true;
|
|
101
117
|
const resumeStream = vi.fn().mockResolvedValue(undefined);
|
|
@@ -32,6 +32,7 @@ import { useResourceCleanup } from "./useResourceCleanup";
|
|
|
32
32
|
export type ChatThreadOptions<UI_MESSAGE extends UIMessage = UIMessage> =
|
|
33
33
|
ChatInit<UI_MESSAGE> &
|
|
34
34
|
ExternalStoreSharedOptions & {
|
|
35
|
+
throttle?: number | undefined;
|
|
35
36
|
adapters?: AISDKRuntimeAdapter["adapters"] | undefined;
|
|
36
37
|
toCreateMessage?: CustomToCreateMessageFunction;
|
|
37
38
|
onResume?: AISDKRuntimeAdapter["onResume"];
|
|
@@ -44,6 +45,8 @@ export type ChatThreadOptions<UI_MESSAGE extends UIMessage = UIMessage> =
|
|
|
44
45
|
*/
|
|
45
46
|
onResumeError?: ((error: unknown) => void) | undefined;
|
|
46
47
|
joinStrategy?: AISDKRuntimeAdapter["joinStrategy"];
|
|
48
|
+
messageRepository?: AISDKRuntimeAdapter<UI_MESSAGE>["messageRepository"];
|
|
49
|
+
unstable_onBranchChange?: AISDKRuntimeAdapter["unstable_onBranchChange"];
|
|
47
50
|
};
|
|
48
51
|
|
|
49
52
|
export type ChatThreadEnvironment<UI_MESSAGE extends UIMessage = UIMessage> = {
|
|
@@ -122,6 +125,7 @@ export const splitChatThreadOptions = <UI_MESSAGE extends UIMessage>(
|
|
|
122
125
|
const {
|
|
123
126
|
adapters,
|
|
124
127
|
transport,
|
|
128
|
+
throttle,
|
|
125
129
|
toCreateMessage,
|
|
126
130
|
isDisabled: _isDisabled,
|
|
127
131
|
isSendDisabled: _isSendDisabled,
|
|
@@ -131,6 +135,8 @@ export const splitChatThreadOptions = <UI_MESSAGE extends UIMessage>(
|
|
|
131
135
|
onResumeToolCall,
|
|
132
136
|
onResumeError,
|
|
133
137
|
joinStrategy,
|
|
138
|
+
messageRepository,
|
|
139
|
+
unstable_onBranchChange,
|
|
134
140
|
...chatInit
|
|
135
141
|
} = options ?? {};
|
|
136
142
|
// peel guard: any shared key left in `chatInit` collapses this to `never`
|
|
@@ -141,11 +147,14 @@ export const splitChatThreadOptions = <UI_MESSAGE extends UIMessage>(
|
|
|
141
147
|
return {
|
|
142
148
|
adapters,
|
|
143
149
|
transport,
|
|
150
|
+
throttle,
|
|
144
151
|
toCreateMessage,
|
|
145
152
|
onResume,
|
|
146
153
|
onResumeToolCall,
|
|
147
154
|
onResumeError,
|
|
148
155
|
joinStrategy,
|
|
156
|
+
messageRepository,
|
|
157
|
+
unstable_onBranchChange,
|
|
149
158
|
chatInit,
|
|
150
159
|
};
|
|
151
160
|
};
|
|
@@ -157,11 +166,14 @@ export const useChatThread = <UI_MESSAGE extends UIMessage = UIMessage>(
|
|
|
157
166
|
const {
|
|
158
167
|
adapters,
|
|
159
168
|
transport: transportOptions,
|
|
169
|
+
throttle,
|
|
160
170
|
toCreateMessage,
|
|
161
171
|
onResume,
|
|
162
172
|
onResumeToolCall,
|
|
163
173
|
onResumeError,
|
|
164
174
|
joinStrategy,
|
|
175
|
+
messageRepository,
|
|
176
|
+
unstable_onBranchChange,
|
|
165
177
|
chatInit: chatOptions,
|
|
166
178
|
} = splitChatThreadOptions(options);
|
|
167
179
|
|
|
@@ -181,6 +193,7 @@ export const useChatThread = <UI_MESSAGE extends UIMessage = UIMessage>(
|
|
|
181
193
|
...chatOptions,
|
|
182
194
|
id,
|
|
183
195
|
transport,
|
|
196
|
+
...(throttle !== undefined && { throttle }),
|
|
184
197
|
...(externalChat !== undefined && { chat: externalChat }),
|
|
185
198
|
});
|
|
186
199
|
|
|
@@ -195,6 +208,8 @@ export const useChatThread = <UI_MESSAGE extends UIMessage = UIMessage>(
|
|
|
195
208
|
...(onResume && { onResume }),
|
|
196
209
|
...(onResumeToolCall && { onResumeToolCall }),
|
|
197
210
|
...(joinStrategy && { joinStrategy }),
|
|
211
|
+
...(messageRepository && { messageRepository }),
|
|
212
|
+
...(unstable_onBranchChange && { unstable_onBranchChange }),
|
|
198
213
|
});
|
|
199
214
|
|
|
200
215
|
if (sourceTransport instanceof AssistantChatTransport) {
|
|
@@ -392,6 +392,7 @@ describe("useExternalHistory persistence", () => {
|
|
|
392
392
|
options?: {
|
|
393
393
|
loadMessages?: MessageFormatRepository<InnerMessage>;
|
|
394
394
|
toThreadMessages?: (messages: InnerMessage[]) => ThreadMessage[];
|
|
395
|
+
initialIsRunning?: boolean;
|
|
395
396
|
},
|
|
396
397
|
) => {
|
|
397
398
|
const append = vi.fn(
|
|
@@ -426,7 +427,7 @@ describe("useExternalHistory persistence", () => {
|
|
|
426
427
|
};
|
|
427
428
|
|
|
428
429
|
let listener: (() => void) | undefined;
|
|
429
|
-
let isRunning = false;
|
|
430
|
+
let isRunning = options?.initialIsRunning ?? false;
|
|
430
431
|
let messages: ThreadMessage[] = [];
|
|
431
432
|
const getState = vi.fn(() => ({ isRunning, messages }));
|
|
432
433
|
const thread = {
|
|
@@ -497,6 +498,83 @@ describe("useExternalHistory persistence", () => {
|
|
|
497
498
|
};
|
|
498
499
|
};
|
|
499
500
|
|
|
501
|
+
it("persists a settled turn when the history adapter becomes active after it", async () => {
|
|
502
|
+
let listener: (() => void) | undefined;
|
|
503
|
+
let isRunning = false;
|
|
504
|
+
let messages: ThreadMessage[] = [];
|
|
505
|
+
const append = vi.fn(async () => {});
|
|
506
|
+
const formattedAdapter = {
|
|
507
|
+
load: vi.fn().mockResolvedValue({ messages: [] }),
|
|
508
|
+
append,
|
|
509
|
+
};
|
|
510
|
+
const historyAdapter: ThreadHistoryAdapter = {
|
|
511
|
+
load: vi.fn(),
|
|
512
|
+
append: vi.fn(),
|
|
513
|
+
withFormat: vi.fn().mockReturnValue(formattedAdapter),
|
|
514
|
+
};
|
|
515
|
+
let activeAdapter: ThreadHistoryAdapter | undefined;
|
|
516
|
+
const thread = {
|
|
517
|
+
subscribe: (next: () => void) => {
|
|
518
|
+
listener = next;
|
|
519
|
+
return () => {};
|
|
520
|
+
},
|
|
521
|
+
getState: () => ({ isRunning, messages }),
|
|
522
|
+
import: vi.fn(),
|
|
523
|
+
export: vi.fn(() => ({ headId: null, messages: [] })),
|
|
524
|
+
} as unknown as AssistantRuntime["thread"];
|
|
525
|
+
const persistenceRuntimeRef = {
|
|
526
|
+
current: { thread } as AssistantRuntime,
|
|
527
|
+
};
|
|
528
|
+
const message = createAssistantMessage(
|
|
529
|
+
{ type: "complete", reason: "stop" },
|
|
530
|
+
[{ id: "inner-1", parts: ["answer"] }],
|
|
531
|
+
);
|
|
532
|
+
|
|
533
|
+
mocks.hasThreadListItem = true;
|
|
534
|
+
mocks.remoteId = "remote-thread";
|
|
535
|
+
|
|
536
|
+
const { rerender } = renderHook(() =>
|
|
537
|
+
useExternalHistory(
|
|
538
|
+
persistenceRuntimeRef,
|
|
539
|
+
activeAdapter,
|
|
540
|
+
() => [],
|
|
541
|
+
persistenceStorageFormat,
|
|
542
|
+
() => {},
|
|
543
|
+
),
|
|
544
|
+
);
|
|
545
|
+
|
|
546
|
+
await act(async () => {
|
|
547
|
+
messages = [message];
|
|
548
|
+
isRunning = true;
|
|
549
|
+
listener?.();
|
|
550
|
+
isRunning = false;
|
|
551
|
+
listener?.();
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
activeAdapter = historyAdapter;
|
|
555
|
+
await act(async () => rerender());
|
|
556
|
+
|
|
557
|
+
await waitFor(() => expect(append).toHaveBeenCalledTimes(1));
|
|
558
|
+
expect(append).toHaveBeenCalledWith({
|
|
559
|
+
parentId: null,
|
|
560
|
+
message: { id: "inner-1", parts: ["answer"] },
|
|
561
|
+
});
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
it("persists a turn that is already running when the subscription starts", async () => {
|
|
565
|
+
const { append, step } = createPersistenceHarness(false, {
|
|
566
|
+
initialIsRunning: true,
|
|
567
|
+
});
|
|
568
|
+
const message = createAssistantMessage(
|
|
569
|
+
{ type: "complete", reason: "stop" },
|
|
570
|
+
[{ id: "inner-1", parts: ["answer"] }],
|
|
571
|
+
);
|
|
572
|
+
|
|
573
|
+
await step({ isRunning: false, messages: [message] });
|
|
574
|
+
|
|
575
|
+
await waitFor(() => expect(append).toHaveBeenCalledTimes(1));
|
|
576
|
+
});
|
|
577
|
+
|
|
500
578
|
it("retries a failed append on the next persistence pass", async () => {
|
|
501
579
|
const consoleError = vi
|
|
502
580
|
.spyOn(console, "error")
|
|
@@ -234,6 +234,19 @@ export const useExternalHistory = <TMessage>(
|
|
|
234
234
|
}, 0);
|
|
235
235
|
});
|
|
236
236
|
|
|
237
|
+
const initialThreadState = runtimeRef.current.thread.getState();
|
|
238
|
+
wasRunningRef.current = initialThreadState.isRunning;
|
|
239
|
+
if (initialThreadState.isRunning) {
|
|
240
|
+
if (runStartRef.current == null) {
|
|
241
|
+
runStartRef.current = Date.now();
|
|
242
|
+
stepBoundariesRef.current = [];
|
|
243
|
+
toolCallCountRef.current = 0;
|
|
244
|
+
adapter.pin?.();
|
|
245
|
+
}
|
|
246
|
+
} else if (initialThreadState.messages.length > 0) {
|
|
247
|
+
persistSettled(true);
|
|
248
|
+
}
|
|
249
|
+
|
|
237
250
|
function persistSettled(ignoreRunning: boolean) {
|
|
238
251
|
persistTimerRef.current = null;
|
|
239
252
|
const latest = runtimeRef.current.thread.getState();
|