@assistant-ui/ai-sdk 0.0.7 → 0.0.9
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/LICENSE +1 -1
- package/README.md +1 -1
- package/dist/adapters/aiSDKFormatAdapter.d.ts +2 -2
- package/dist/adapters/aiSDKFormatAdapter.d.ts.map +1 -0
- package/dist/adapters/vercelAttachmentAdapter.d.ts +1 -3
- package/dist/adapters/vercelAttachmentAdapter.d.ts.map +1 -1
- package/dist/adapters/vercelAttachmentAdapter.js +3 -3
- package/dist/adapters/vercelAttachmentAdapter.js.map +1 -1
- package/dist/aiSDKExtras.d.ts +4 -6
- package/dist/aiSDKExtras.d.ts.map +1 -1
- package/dist/converters/convertMessage.d.ts +34 -24
- package/dist/converters/convertMessage.d.ts.map +1 -1
- package/dist/converters/convertMessage.js +56 -12
- package/dist/converters/convertMessage.js.map +1 -1
- package/dist/converters/modelContentEnvelope.d.ts +6 -7
- package/dist/converters/modelContentEnvelope.d.ts.map +1 -1
- package/dist/converters/toCreateMessage.d.ts +2 -4
- package/dist/converters/toCreateMessage.d.ts.map +1 -1
- package/dist/converters/toolOutputConversion.d.ts +20 -22
- package/dist/converters/toolOutputConversion.d.ts.map +1 -1
- package/dist/hooks.d.ts +2 -4
- package/dist/hooks.d.ts.map +1 -1
- package/dist/index.d.ts +17 -14
- package/dist/index.d.ts.map +1 -0
- package/dist/index.native.d.ts +15 -12
- package/dist/index.native.d.ts.map +1 -0
- package/dist/model-context/injectInteractableContext.d.ts +2 -4
- package/dist/model-context/injectInteractableContext.d.ts.map +1 -1
- package/dist/model-context/injectQuoteContext.d.ts +1 -3
- package/dist/model-context/injectQuoteContext.d.ts.map +1 -1
- package/dist/runtime/AISDKChat.d.ts +2 -4
- package/dist/runtime/AISDKChat.d.ts.map +1 -1
- package/dist/runtime/AISDKChat.js +1 -1
- package/dist/runtime/AISDKChat.js.map +1 -1
- package/dist/runtime/AISDKThreads.d.ts +29 -31
- package/dist/runtime/AISDKThreads.d.ts.map +1 -1
- package/dist/runtime/AISDKThreads.js +22 -23
- package/dist/runtime/AISDKThreads.js.map +1 -1
- package/dist/runtime/sdkIdentity.d.ts +1 -3
- package/dist/runtime/sdkIdentity.d.ts.map +1 -1
- package/dist/runtime/sdkIdentity.js +1 -1
- package/dist/runtime/useAISDKRuntime.d.ts +76 -70
- package/dist/runtime/useAISDKRuntime.d.ts.map +1 -1
- package/dist/runtime/useAISDKRuntime.js +311 -77
- package/dist/runtime/useAISDKRuntime.js.map +1 -1
- package/dist/runtime/useChatRuntime.d.ts +6 -8
- package/dist/runtime/useChatRuntime.d.ts.map +1 -1
- package/dist/runtime/useChatRuntime.js +14 -10
- package/dist/runtime/useChatRuntime.js.map +1 -1
- package/dist/runtime/useChatThread.d.ts +94 -89
- package/dist/runtime/useChatThread.d.ts.map +1 -1
- package/dist/runtime/useChatThread.js +44 -15
- package/dist/runtime/useChatThread.js.map +1 -1
- package/dist/runtime/useExternalHistory.d.ts +7 -7
- package/dist/runtime/useExternalHistory.d.ts.map +1 -1
- package/dist/runtime/useExternalHistory.js +231 -60
- package/dist/runtime/useExternalHistory.js.map +1 -1
- package/dist/runtime/useResourceCleanup.d.ts +1 -3
- package/dist/runtime/useResourceCleanup.d.ts.map +1 -1
- package/dist/runtime/useResourceCleanup.js +38 -14
- package/dist/runtime/useResourceCleanup.js.map +1 -1
- package/dist/runtime/useStreamingTiming.d.ts +3 -5
- package/dist/runtime/useStreamingTiming.d.ts.map +1 -1
- package/dist/runtime/useStreamingTiming.js +1 -1
- package/dist/tools/frontendTools.d.ts +21 -23
- package/dist/tools/frontendTools.d.ts.map +1 -1
- package/dist/tools/generativeTools.d.ts +24 -26
- package/dist/tools/generativeTools.d.ts.map +1 -1
- package/dist/tools/mcp-stdio.node.d.ts +2 -2
- package/dist/tools/mcp-stdio.node.d.ts.map +1 -0
- package/dist/tools/mcp-stdio.unsupported.d.ts +2 -4
- package/dist/tools/mcp-stdio.unsupported.d.ts.map +1 -1
- package/dist/transport/AssistantChatTransport.d.ts +14 -16
- package/dist/transport/AssistantChatTransport.d.ts.map +1 -1
- package/dist/transport/AssistantChatTransport.js +9 -2
- package/dist/transport/AssistantChatTransport.js.map +1 -1
- package/dist/transport/resumable.d.ts +25 -27
- package/dist/transport/resumable.d.ts.map +1 -1
- package/dist/usage.d.ts +8 -10
- package/dist/usage.d.ts.map +1 -1
- package/dist/usage.js.map +1 -1
- package/dist/utils/getVercelAIMessages.d.ts +2 -4
- package/dist/utils/getVercelAIMessages.d.ts.map +1 -1
- package/dist/utils/sliceMessagesUntil.d.ts +1 -3
- package/dist/utils/sliceMessagesUntil.d.ts.map +1 -1
- package/package.json +13 -9
- package/src/adapters/vercelAttachmentAdapter.test.ts +23 -0
- package/src/adapters/vercelAttachmentAdapter.ts +2 -2
- package/src/converters/convertMessage.test.ts +541 -3
- package/src/converters/convertMessage.ts +123 -15
- package/src/index.native.ts +1 -1
- package/src/index.ts +1 -1
- package/src/runtime/AISDKChat.ts +0 -4
- package/src/runtime/AISDKThreads.cloud.test.ts +60 -2
- package/src/runtime/AISDKThreads.test.ts +105 -46
- package/src/runtime/AISDKThreads.ts +1 -1
- package/src/runtime/useAISDKRuntime.approval.integration.test.tsx +1639 -19
- package/src/runtime/useAISDKRuntime.approval.test.tsx +252 -9
- package/src/runtime/useAISDKRuntime.fast-refresh.test.tsx +182 -0
- package/src/runtime/useAISDKRuntime.test.ts +275 -5
- package/src/runtime/useAISDKRuntime.ts +533 -60
- package/src/runtime/useAISDKRuntime.voice.test.tsx +50 -1
- package/src/runtime/useChatRuntime.fast-refresh.test.tsx +94 -0
- package/src/runtime/useChatRuntime.integration.test.tsx +143 -65
- package/src/runtime/useChatRuntime.test.ts +11 -7
- package/src/runtime/useChatRuntime.ts +6 -2
- package/src/runtime/useChatThread.test.ts +166 -2
- package/src/runtime/useChatThread.transport.test.tsx +152 -0
- package/src/runtime/useChatThread.ts +89 -22
- package/src/runtime/useExternalHistory.test.ts +943 -3
- package/src/runtime/useExternalHistory.ts +378 -15
- package/src/runtime/useResourceCleanup.ts +41 -15
- package/src/transport/AssistantChatTransport.test.ts +164 -0
- package/src/transport/AssistantChatTransport.ts +22 -2
- package/src/usage.ts +1 -1
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
useRuntimeAdapters,
|
|
21
21
|
type JoinStrategy,
|
|
22
22
|
} from "@assistant-ui/core/react";
|
|
23
|
+
import { useReplaySafeEffect } from "@assistant-ui/store/internal";
|
|
23
24
|
import type {
|
|
24
25
|
SuggestionAdapter,
|
|
25
26
|
ThreadSuggestion,
|
|
@@ -38,12 +39,14 @@ import type {
|
|
|
38
39
|
RunConfig,
|
|
39
40
|
McpAppMetadata,
|
|
40
41
|
RespondToToolApprovalOptions,
|
|
42
|
+
Unstable_ToolInteractionLog,
|
|
41
43
|
} from "@assistant-ui/core";
|
|
42
44
|
import {
|
|
43
45
|
getExternalStoreMessages,
|
|
44
46
|
pickExternalStoreSharedOptions,
|
|
45
47
|
} from "@assistant-ui/core";
|
|
46
48
|
import {
|
|
49
|
+
appendToolInteraction,
|
|
47
50
|
consumeSuggestionResult,
|
|
48
51
|
MessageRepository,
|
|
49
52
|
} from "@assistant-ui/core/internal";
|
|
@@ -95,7 +98,7 @@ const toVoiceTranscriptUIMessage = <UI_MESSAGE extends UIMessage>(
|
|
|
95
98
|
.filter((part) => part.type === "text")
|
|
96
99
|
.map((part) => ({ type: "text", text: part.text })),
|
|
97
100
|
metadata: {
|
|
98
|
-
modality:
|
|
101
|
+
...(message.metadata.modality && { modality: message.metadata.modality }),
|
|
99
102
|
...(Object.keys(message.metadata.custom).length > 0 && {
|
|
100
103
|
custom: message.metadata.custom,
|
|
101
104
|
}),
|
|
@@ -112,6 +115,13 @@ export type AISDKRuntimeAdapter<UI_MESSAGE extends UIMessage = UIMessage> =
|
|
|
112
115
|
| undefined;
|
|
113
116
|
toCreateMessage?: CustomToCreateMessageFunction;
|
|
114
117
|
unstable_messageRepositoryInstance?: MessageRepository | undefined;
|
|
118
|
+
/**
|
|
119
|
+
* The object a host answer belongs to, normally the `Chat` the runtime
|
|
120
|
+
* renders. A host answer never reaches the `useChat` messages, so without
|
|
121
|
+
* an owner it lives only as long as this runtime. With an owner, it
|
|
122
|
+
* survives runtime remounts over the same chat.
|
|
123
|
+
*/
|
|
124
|
+
unstable_hostApprovalOwner?: object | undefined;
|
|
115
125
|
/**
|
|
116
126
|
* Whether to automatically cancel pending interactive tool calls when the user sends a new message.
|
|
117
127
|
*
|
|
@@ -139,9 +149,9 @@ export type AISDKRuntimeAdapter<UI_MESSAGE extends UIMessage = UIMessage> =
|
|
|
139
149
|
/**
|
|
140
150
|
* Answers tool approval requests through a host-owned channel instead of the AI SDK's `addToolApprovalResponse`.
|
|
141
151
|
*
|
|
142
|
-
* Called for every approval request in the thread with the complete response, including option and free-form answers. Hand requests the host does not own to `respondViaAISDK`, which is what runs when this option is omitted. The answer applies to the approval when the handler starts and is removed if it throws. It is never written into the `useChat` messages, so `sendAutomaticallyWhen` cannot forward it,
|
|
152
|
+
* Called for every approval request in the thread with the complete response, including option and free-form answers. Hand requests the host does not own to `respondViaAISDK`, which is what runs when this option is omitted. The answer applies to the approval when the handler starts and is removed if it throws. It is never written into the `useChat` messages, so `sendAutomaticallyWhen` cannot forward it. With a history adapter, the answer is stored with its message once the handler resolves and returns on reload. With `unstable_hostApprovalOwner`, in-memory answers survive runtime remounts over the same chat; without an owner or history adapter, they last only as long as the runtime. Bring the resumed run back into the chat, for example with `resumeStream`, and have the endpoint refuse a second resume.
|
|
143
153
|
*
|
|
144
|
-
* While a handler is set, an approval's `display`, `allowFreeform` and `options` reach the renderer, because the handler can receive answers the AI SDK cannot carry.
|
|
154
|
+
* While a handler is set, an approval's `display`, `allowFreeform`, `dismissible` and `options` reach the renderer, because the handler can receive answers the AI SDK cannot carry. A stream declares them through the `approvalDescriptor` of its `tool-approval-request` chunk, the one approval field the AI SDK keeps opaque; the converter reads the request and answer fields from that descriptor when the approval itself lacks them.
|
|
145
155
|
*/
|
|
146
156
|
onRespondToToolApproval?:
|
|
147
157
|
| ((
|
|
@@ -248,7 +258,7 @@ const useGeneratedSuggestions = (
|
|
|
248
258
|
})();
|
|
249
259
|
}, [hasAdapter, isRunning]);
|
|
250
260
|
|
|
251
|
-
|
|
261
|
+
useReplaySafeEffect(() => {
|
|
252
262
|
return () => {
|
|
253
263
|
controllerRef.current?.abort();
|
|
254
264
|
};
|
|
@@ -259,11 +269,188 @@ const useGeneratedSuggestions = (
|
|
|
259
269
|
|
|
260
270
|
const NO_CANCELLED_MESSAGE_IDS: ReadonlySet<string> = new Set();
|
|
261
271
|
|
|
272
|
+
const NO_SUPERSEDED_APPROVAL_PROJECTION = Object.freeze({
|
|
273
|
+
approvalIds: Object.freeze(new Set<string>()),
|
|
274
|
+
statusMessageIds: Object.freeze(new Set<string>()),
|
|
275
|
+
});
|
|
276
|
+
|
|
262
277
|
const NO_TOOL_APPROVAL_RESPONSES: ReadonlyMap<
|
|
263
278
|
string,
|
|
264
279
|
RespondToToolApprovalOptions
|
|
265
280
|
> = new Map();
|
|
266
281
|
|
|
282
|
+
/**
|
|
283
|
+
* A host answer is deliberately kept out of the `useChat` messages, so nothing
|
|
284
|
+
* in the chat records it. Held in runtime state it would die with the runtime,
|
|
285
|
+
* and a runtime mounted again over the same chat would show the request open
|
|
286
|
+
* and take a second answer. Keyed on the chat instead, the answer lives as
|
|
287
|
+
* long as the chat it belongs to, and is collected with it.
|
|
288
|
+
*/
|
|
289
|
+
type OwnedApproval = {
|
|
290
|
+
response: RespondToToolApprovalOptions;
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
const hostToolApprovalsByChat = new WeakMap<
|
|
294
|
+
object,
|
|
295
|
+
Map<string, OwnedApproval>
|
|
296
|
+
>();
|
|
297
|
+
|
|
298
|
+
const toApprovalResponses = (
|
|
299
|
+
owned: ReadonlyMap<string, OwnedApproval> | undefined,
|
|
300
|
+
): ReadonlyMap<string, RespondToToolApprovalOptions> =>
|
|
301
|
+
owned && owned.size > 0
|
|
302
|
+
? new Map([...owned].map(([id, entry]) => [id, entry.response]))
|
|
303
|
+
: NO_TOOL_APPROVAL_RESPONSES;
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* The answers live on the owner, but each mounted runtime renders them from
|
|
307
|
+
* its own state, so a write has to be announced: the runtime that performed it
|
|
308
|
+
* may already be unmounted (a rollback resolving after a remount), and another
|
|
309
|
+
* runtime may be mounted over the same owner.
|
|
310
|
+
*/
|
|
311
|
+
const hostApprovalListenersByChat = new WeakMap<object, Set<() => void>>();
|
|
312
|
+
|
|
313
|
+
const subscribeToHostApprovals = (owner: object, listener: () => void) => {
|
|
314
|
+
const listeners = hostApprovalListenersByChat.get(owner) ?? new Set();
|
|
315
|
+
hostApprovalListenersByChat.set(owner, listeners);
|
|
316
|
+
listeners.add(listener);
|
|
317
|
+
return () => {
|
|
318
|
+
listeners.delete(listener);
|
|
319
|
+
};
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
const notifyHostApprovals = (owner: object) => {
|
|
323
|
+
for (const listener of [...(hostApprovalListenersByChat.get(owner) ?? [])]) {
|
|
324
|
+
listener();
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
const getSupersededApprovalProjection = <UI_MESSAGE extends UIMessage>(
|
|
329
|
+
messages: readonly UI_MESSAGE[],
|
|
330
|
+
hostApprovalIds: ReadonlySet<string>,
|
|
331
|
+
joinStrategy: JoinStrategy | undefined,
|
|
332
|
+
isRunning: boolean,
|
|
333
|
+
) => {
|
|
334
|
+
const approvalIds = new Set<string>();
|
|
335
|
+
const statusMessageIds = new Set<string>();
|
|
336
|
+
const lastIndex = messages.length - 1;
|
|
337
|
+
let lastAssistant: UI_MESSAGE | undefined;
|
|
338
|
+
let hasSupersededApproval = false;
|
|
339
|
+
let previousWasVoice = false;
|
|
340
|
+
|
|
341
|
+
const flush = () => {
|
|
342
|
+
const hasOpenToolPart =
|
|
343
|
+
lastAssistant === messages[lastIndex] &&
|
|
344
|
+
lastAssistant?.parts?.some((part) => {
|
|
345
|
+
if (
|
|
346
|
+
!isToolUIPart(part) ||
|
|
347
|
+
part.state === "output-available" ||
|
|
348
|
+
part.state === "output-error" ||
|
|
349
|
+
part.state === "output-denied"
|
|
350
|
+
)
|
|
351
|
+
return false;
|
|
352
|
+
|
|
353
|
+
const approval = (
|
|
354
|
+
part as {
|
|
355
|
+
approval?: {
|
|
356
|
+
resolution?: unknown;
|
|
357
|
+
descriptor?: unknown;
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
).approval;
|
|
361
|
+
const resolution =
|
|
362
|
+
approval?.resolution ??
|
|
363
|
+
(approval?.descriptor as { resolution?: unknown } | undefined)
|
|
364
|
+
?.resolution;
|
|
365
|
+
return resolution !== "cancelled" && resolution !== "expired";
|
|
366
|
+
});
|
|
367
|
+
if (
|
|
368
|
+
lastAssistant &&
|
|
369
|
+
hasSupersededApproval &&
|
|
370
|
+
!hasOpenToolPart &&
|
|
371
|
+
!(isRunning && lastAssistant === messages[lastIndex])
|
|
372
|
+
) {
|
|
373
|
+
statusMessageIds.add(lastAssistant.id);
|
|
374
|
+
}
|
|
375
|
+
lastAssistant = undefined;
|
|
376
|
+
hasSupersededApproval = false;
|
|
377
|
+
previousWasVoice = false;
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
for (let index = 0; index < messages.length; index++) {
|
|
381
|
+
const message = messages[index]!;
|
|
382
|
+
if (message.role !== "assistant") {
|
|
383
|
+
flush();
|
|
384
|
+
} else {
|
|
385
|
+
const isVoice =
|
|
386
|
+
(message.metadata as { modality?: unknown } | undefined)?.modality ===
|
|
387
|
+
"voice";
|
|
388
|
+
if (isVoice || previousWasVoice || joinStrategy === "none") flush();
|
|
389
|
+
lastAssistant = message;
|
|
390
|
+
previousWasVoice = isVoice;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
for (const part of message.parts ?? []) {
|
|
394
|
+
if (!isToolUIPart(part) || part.state !== "approval-requested") continue;
|
|
395
|
+
|
|
396
|
+
const approval = part.approval;
|
|
397
|
+
if (!approval) continue;
|
|
398
|
+
|
|
399
|
+
const approvalId = approval.id;
|
|
400
|
+
if (hostApprovalIds.has(approvalId)) continue;
|
|
401
|
+
|
|
402
|
+
const resolution =
|
|
403
|
+
(approval as { resolution?: unknown }).resolution ??
|
|
404
|
+
(approval.descriptor as { resolution?: unknown } | null | undefined)
|
|
405
|
+
?.resolution;
|
|
406
|
+
if (resolution === "cancelled" || resolution === "expired") continue;
|
|
407
|
+
|
|
408
|
+
if (index !== lastIndex) {
|
|
409
|
+
approvalIds.add(approvalId);
|
|
410
|
+
if (message.role === "assistant") hasSupersededApproval = true;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
flush();
|
|
415
|
+
|
|
416
|
+
if (approvalIds.size === 0 && statusMessageIds.size === 0)
|
|
417
|
+
return NO_SUPERSEDED_APPROVAL_PROJECTION;
|
|
418
|
+
|
|
419
|
+
return { approvalIds, statusMessageIds };
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
const findRawToolMessageIndex = <UI_MESSAGE extends UIMessage>(
|
|
423
|
+
messages: readonly UI_MESSAGE[],
|
|
424
|
+
messageId: string,
|
|
425
|
+
toolCallId: string,
|
|
426
|
+
joinStrategy: JoinStrategy | undefined,
|
|
427
|
+
) => {
|
|
428
|
+
const containsToolCall = (message: UI_MESSAGE) =>
|
|
429
|
+
message.parts?.some(
|
|
430
|
+
(part) => isToolUIPart(part) && part.toolCallId === toolCallId,
|
|
431
|
+
) === true;
|
|
432
|
+
|
|
433
|
+
const messageIndex = messages.findIndex(
|
|
434
|
+
(message) => message.id === messageId,
|
|
435
|
+
);
|
|
436
|
+
if (messageIndex === -1) return -1;
|
|
437
|
+
if (containsToolCall(messages[messageIndex]!)) return messageIndex;
|
|
438
|
+
if (joinStrategy === "none" || messages[messageIndex]?.role !== "assistant")
|
|
439
|
+
return -1;
|
|
440
|
+
|
|
441
|
+
let start = messageIndex;
|
|
442
|
+
while (start > 0 && messages[start - 1]?.role === "assistant") start--;
|
|
443
|
+
|
|
444
|
+
let end = messageIndex;
|
|
445
|
+
while (end + 1 < messages.length && messages[end + 1]?.role === "assistant")
|
|
446
|
+
end++;
|
|
447
|
+
|
|
448
|
+
for (let index = start; index <= end; index++) {
|
|
449
|
+
if (containsToolCall(messages[index]!)) return index;
|
|
450
|
+
}
|
|
451
|
+
return -1;
|
|
452
|
+
};
|
|
453
|
+
|
|
267
454
|
const toChatError = (error: Error): AssistantError => {
|
|
268
455
|
const code = (error as { code?: unknown }).code;
|
|
269
456
|
return {
|
|
@@ -301,18 +488,91 @@ export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
|
|
|
301
488
|
chatId: string;
|
|
302
489
|
ids: ReadonlySet<string>;
|
|
303
490
|
} | null>(null);
|
|
491
|
+
// Set by hosts that own the chat across runtime lifetimes, so a host answer
|
|
492
|
+
// outlives a remount over that same chat.
|
|
493
|
+
const approvalOwner = adapter.unstable_hostApprovalOwner;
|
|
494
|
+
const ownedApprovals = approvalOwner
|
|
495
|
+
? (hostToolApprovalsByChat.get(approvalOwner) ??
|
|
496
|
+
(() => {
|
|
497
|
+
const created = new Map<string, OwnedApproval>();
|
|
498
|
+
hostToolApprovalsByChat.set(approvalOwner, created);
|
|
499
|
+
return created;
|
|
500
|
+
})())
|
|
501
|
+
: undefined;
|
|
304
502
|
const [toolApprovalResponses, setToolApprovalResponses] = useState<
|
|
305
503
|
ReadonlyMap<string, RespondToToolApprovalOptions>
|
|
306
|
-
>(
|
|
307
|
-
const
|
|
504
|
+
>(() => toApprovalResponses(ownedApprovals));
|
|
505
|
+
const [toolArtifactEpoch, setToolArtifactEpoch] = useState(0);
|
|
506
|
+
const [toolInteractionEpoch, setToolInteractionEpoch] = useState(0);
|
|
507
|
+
const toolApprovalResponsesRef = useRef(
|
|
508
|
+
new Map(toApprovalResponses(ownedApprovals)),
|
|
509
|
+
);
|
|
510
|
+
const ownedApprovalIdsRef = useRef(new Set(ownedApprovals?.keys()));
|
|
511
|
+
const hostApprovalIdsRef = useRef(new Set<string>(ownedApprovals?.keys()));
|
|
512
|
+
|
|
513
|
+
// The owner's record is shared, so this runtime re-reads it whenever it is
|
|
514
|
+
// written rather than only at mount: the write may come from a runtime that
|
|
515
|
+
// has since unmounted, or from another runtime mounted over the same owner.
|
|
516
|
+
useEffect(() => {
|
|
517
|
+
if (!approvalOwner || !ownedApprovals) return undefined;
|
|
518
|
+
const sync = () => {
|
|
519
|
+
for (const id of ownedApprovalIdsRef.current)
|
|
520
|
+
toolApprovalResponsesRef.current.delete(id);
|
|
521
|
+
for (const [id, entry] of ownedApprovals)
|
|
522
|
+
toolApprovalResponsesRef.current.set(id, entry.response);
|
|
523
|
+
ownedApprovalIdsRef.current = new Set(ownedApprovals.keys());
|
|
524
|
+
hostApprovalIdsRef.current = new Set(
|
|
525
|
+
toolApprovalResponsesRef.current.keys(),
|
|
526
|
+
);
|
|
527
|
+
setToolApprovalResponses((prev) => {
|
|
528
|
+
const next = new Map(toolApprovalResponsesRef.current);
|
|
529
|
+
const unchanged =
|
|
530
|
+
prev.size === next.size &&
|
|
531
|
+
[...next].every(([id, response]) => prev.get(id) === response);
|
|
532
|
+
return unchanged ? prev : next;
|
|
533
|
+
});
|
|
534
|
+
};
|
|
535
|
+
const unsubscribe = subscribeToHostApprovals(approvalOwner, sync);
|
|
536
|
+
// The state was seeded during render, so a write landing between then and
|
|
537
|
+
// this subscription would otherwise never be seen.
|
|
538
|
+
sync();
|
|
539
|
+
return unsubscribe;
|
|
540
|
+
}, [approvalOwner, ownedApprovals]);
|
|
541
|
+
|
|
542
|
+
// A runtime kept mounted across a change of owner must not carry the
|
|
543
|
+
// previous chat's answers: a reused approval id would render as already
|
|
544
|
+
// answered and reject a genuine response.
|
|
545
|
+
const lastApprovalOwnerRef = useRef(approvalOwner);
|
|
546
|
+
if (lastApprovalOwnerRef.current !== approvalOwner) {
|
|
547
|
+
lastApprovalOwnerRef.current = approvalOwner;
|
|
548
|
+
hostApprovalIdsRef.current = new Set<string>(ownedApprovals?.keys());
|
|
549
|
+
toolApprovalResponsesRef.current = new Map(
|
|
550
|
+
toApprovalResponses(ownedApprovals),
|
|
551
|
+
);
|
|
552
|
+
ownedApprovalIdsRef.current = new Set(ownedApprovals?.keys());
|
|
553
|
+
setToolApprovalResponses(new Map(toolApprovalResponsesRef.current));
|
|
554
|
+
}
|
|
308
555
|
const toolArgsKeyOrderCacheRef = useRef<Map<string, Map<string, string[]>>>(
|
|
309
556
|
new Map(),
|
|
310
557
|
);
|
|
311
558
|
const toolLastInputCacheRef = useRef<Map<string, ReadonlyJSONObject>>(
|
|
312
559
|
new Map(),
|
|
313
560
|
);
|
|
561
|
+
const toolArgsTextCacheRef = useRef<
|
|
562
|
+
WeakMap<ReadonlyJSONObject, Map<string, string>>
|
|
563
|
+
>(new WeakMap());
|
|
314
564
|
const mcpAppMetadataCacheRef = useRef<Map<string, McpAppMetadata>>(new Map());
|
|
565
|
+
const toolArtifactsRef = useRef<Map<string, unknown>>(new Map());
|
|
566
|
+
const toolInteractionsRef = useRef<Map<string, Unstable_ToolInteractionLog>>(
|
|
567
|
+
new Map(),
|
|
568
|
+
);
|
|
315
569
|
const lastRunConfigRef = useRef<RunConfig | undefined>(undefined);
|
|
570
|
+
const markToolArtifactsChanged = useCallback(() => {
|
|
571
|
+
setToolArtifactEpoch((epoch) => epoch + 1);
|
|
572
|
+
}, []);
|
|
573
|
+
const markToolInteractionsChanged = useCallback(() => {
|
|
574
|
+
setToolInteractionEpoch((epoch) => epoch + 1);
|
|
575
|
+
}, []);
|
|
316
576
|
|
|
317
577
|
const hasExecutingTools = Object.values(toolStatuses).some(
|
|
318
578
|
(s) => s?.type === "executing",
|
|
@@ -336,11 +596,44 @@ export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
|
|
|
336
596
|
: NO_CANCELLED_MESSAGE_IDS;
|
|
337
597
|
const supportsRichToolApprovalResponses =
|
|
338
598
|
customOnRespondToToolApproval != null;
|
|
599
|
+
const supersededApprovalProjectionRef = useRef(
|
|
600
|
+
NO_SUPERSEDED_APPROVAL_PROJECTION,
|
|
601
|
+
);
|
|
602
|
+
const supersededApprovalProjection = useMemo(
|
|
603
|
+
() => {
|
|
604
|
+
const projection = getSupersededApprovalProjection(
|
|
605
|
+
chatHelpers.messages,
|
|
606
|
+
hostApprovalIdsRef.current,
|
|
607
|
+
joinStrategy,
|
|
608
|
+
isRunning,
|
|
609
|
+
);
|
|
610
|
+
const previous = supersededApprovalProjectionRef.current;
|
|
611
|
+
if (
|
|
612
|
+
projection.approvalIds.size === previous.approvalIds.size &&
|
|
613
|
+
projection.statusMessageIds.size === previous.statusMessageIds.size &&
|
|
614
|
+
[...projection.approvalIds].every((id) =>
|
|
615
|
+
previous.approvalIds.has(id),
|
|
616
|
+
) &&
|
|
617
|
+
[...projection.statusMessageIds].every((id) =>
|
|
618
|
+
previous.statusMessageIds.has(id),
|
|
619
|
+
)
|
|
620
|
+
)
|
|
621
|
+
return previous;
|
|
622
|
+
|
|
623
|
+
supersededApprovalProjectionRef.current = projection;
|
|
624
|
+
return projection;
|
|
625
|
+
},
|
|
626
|
+
// oxlint-disable-next-line react/exhaustive-deps -- hostApprovalIdsRef changes alongside toolApprovalResponses, which invalidates the projection
|
|
627
|
+
[chatHelpers.messages, joinStrategy, isRunning, toolApprovalResponses],
|
|
628
|
+
);
|
|
339
629
|
|
|
340
630
|
const toThreadMessages = useCallback(
|
|
341
631
|
(sourceMessages: UI_MESSAGE[]) => {
|
|
342
632
|
const metadata: AISDKMessageConverterMetadata = {
|
|
343
633
|
supportsRichToolApprovalResponses,
|
|
634
|
+
toolArtifacts: toolArtifactsRef.current,
|
|
635
|
+
toolInteractions: toolInteractionsRef.current,
|
|
636
|
+
toolApprovalResponses: toolApprovalResponsesRef.current,
|
|
344
637
|
};
|
|
345
638
|
return AISDKMessageConverter.toThreadMessages(
|
|
346
639
|
sourceMessages,
|
|
@@ -391,9 +684,15 @@ export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
|
|
|
391
684
|
toolStatuses,
|
|
392
685
|
messageTiming,
|
|
393
686
|
toolArgsKeyOrderCache: toolArgsKeyOrderCacheRef.current,
|
|
687
|
+
toolArgsTextCache: toolArgsTextCacheRef.current,
|
|
394
688
|
toolLastInputCache: toolLastInputCacheRef.current,
|
|
395
689
|
mcpAppMetadataCache: mcpAppMetadataCacheRef.current,
|
|
690
|
+
toolArtifacts: toolArtifactsRef.current,
|
|
691
|
+
toolInteractions: toolInteractionsRef.current,
|
|
396
692
|
supportsRichToolApprovalResponses,
|
|
693
|
+
cancelledToolApprovalIds: supersededApprovalProjection.approvalIds,
|
|
694
|
+
cancelledStatusMessageIds:
|
|
695
|
+
supersededApprovalProjection.statusMessageIds,
|
|
397
696
|
...(optimisticMessageId && { optimisticMessageId }),
|
|
398
697
|
...(chatHelpers.error && {
|
|
399
698
|
error: toChatError(chatHelpers.error),
|
|
@@ -409,6 +708,9 @@ export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
|
|
|
409
708
|
cancelledMessageIds,
|
|
410
709
|
toolApprovalResponses,
|
|
411
710
|
supportsRichToolApprovalResponses,
|
|
711
|
+
supersededApprovalProjection,
|
|
712
|
+
toolArtifactEpoch,
|
|
713
|
+
toolInteractionEpoch,
|
|
412
714
|
],
|
|
413
715
|
),
|
|
414
716
|
});
|
|
@@ -434,7 +736,12 @@ export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
|
|
|
434
736
|
},
|
|
435
737
|
}));
|
|
436
738
|
|
|
437
|
-
const {
|
|
739
|
+
const {
|
|
740
|
+
isLoading,
|
|
741
|
+
deleteMessage: deleteHistoryMessage,
|
|
742
|
+
persistToolInteractions,
|
|
743
|
+
persistToolApprovalResponses,
|
|
744
|
+
} = useExternalHistory(
|
|
438
745
|
runtimeRef,
|
|
439
746
|
adapters?.history ?? contextAdapters?.history,
|
|
440
747
|
toThreadMessages,
|
|
@@ -445,6 +752,20 @@ export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
|
|
|
445
752
|
(messages) => {
|
|
446
753
|
chatHelpers.setMessages(messages);
|
|
447
754
|
},
|
|
755
|
+
toolArtifactsRef.current,
|
|
756
|
+
markToolArtifactsChanged,
|
|
757
|
+
toolInteractionsRef.current,
|
|
758
|
+
markToolInteractionsChanged,
|
|
759
|
+
toolApprovalResponsesRef.current,
|
|
760
|
+
() => {
|
|
761
|
+
for (const [id, entry] of ownedApprovals ?? []) {
|
|
762
|
+
toolApprovalResponsesRef.current.set(id, entry.response);
|
|
763
|
+
}
|
|
764
|
+
hostApprovalIdsRef.current = new Set(
|
|
765
|
+
toolApprovalResponsesRef.current.keys(),
|
|
766
|
+
);
|
|
767
|
+
setToolApprovalResponses(new Map(toolApprovalResponsesRef.current));
|
|
768
|
+
},
|
|
448
769
|
);
|
|
449
770
|
|
|
450
771
|
const {
|
|
@@ -467,35 +788,43 @@ export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
|
|
|
467
788
|
if (!cancelPendingToolCallsOnSend) return;
|
|
468
789
|
|
|
469
790
|
// The runtime auto-aborts in-flight tool invocations when a new run
|
|
470
|
-
// is dispatched (append() / startRun())
|
|
471
|
-
//
|
|
472
|
-
|
|
473
|
-
//
|
|
791
|
+
// is dispatched (append() / startRun()), so this only has to mark the
|
|
792
|
+
// abandoned tools cancelled in the UI message list. Every non-terminal
|
|
793
|
+
// tool call qualifies wherever it sits: the run that produced it is over,
|
|
794
|
+
// and a staged `startRun: false` message can sit between it and the tail.
|
|
795
|
+
// Uses setMessages to avoid triggering sendAutomaticallyWhen.
|
|
474
796
|
chatHelpers.setMessages((messages) => {
|
|
475
|
-
const lastMessage = messages.at(-1);
|
|
476
|
-
if (lastMessage?.role !== "assistant") return messages;
|
|
477
|
-
|
|
478
797
|
let hasChanges = false;
|
|
479
|
-
const parts = lastMessage.parts?.map((part) => {
|
|
480
|
-
if (!isToolUIPart(part)) return part;
|
|
481
|
-
if (
|
|
482
|
-
part.state === "output-available" ||
|
|
483
|
-
part.state === "output-error" ||
|
|
484
|
-
part.state === "output-denied"
|
|
485
|
-
)
|
|
486
|
-
return part;
|
|
487
798
|
|
|
799
|
+
const next = messages.map((message) => {
|
|
800
|
+
if (message.role !== "assistant") return message;
|
|
801
|
+
|
|
802
|
+
let messageChanged = false;
|
|
803
|
+
const parts = message.parts?.map((part) => {
|
|
804
|
+
if (!isToolUIPart(part)) return part;
|
|
805
|
+
if (
|
|
806
|
+
part.state === "output-available" ||
|
|
807
|
+
part.state === "output-error" ||
|
|
808
|
+
part.state === "output-denied"
|
|
809
|
+
)
|
|
810
|
+
return part;
|
|
811
|
+
|
|
812
|
+
messageChanged = true;
|
|
813
|
+
const { approval: _approval, ...rest } = part;
|
|
814
|
+
return {
|
|
815
|
+
...rest,
|
|
816
|
+
state: "output-error" as const,
|
|
817
|
+
errorText: "User cancelled tool call by sending a new message.",
|
|
818
|
+
};
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
if (!messageChanged) return message;
|
|
488
822
|
hasChanges = true;
|
|
489
|
-
|
|
490
|
-
return {
|
|
491
|
-
...rest,
|
|
492
|
-
state: "output-error" as const,
|
|
493
|
-
errorText: "User cancelled tool call by sending a new message.",
|
|
494
|
-
};
|
|
823
|
+
return { ...message, parts };
|
|
495
824
|
});
|
|
496
825
|
|
|
497
826
|
if (!hasChanges) return messages;
|
|
498
|
-
return
|
|
827
|
+
return next;
|
|
499
828
|
});
|
|
500
829
|
};
|
|
501
830
|
|
|
@@ -519,12 +848,15 @@ export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
|
|
|
519
848
|
) => {
|
|
520
849
|
const { approvalId } = response;
|
|
521
850
|
const requested = chatHelpers.messages
|
|
522
|
-
.flatMap((message) =>
|
|
523
|
-
|
|
851
|
+
.flatMap((message) =>
|
|
852
|
+
message.parts.flatMap((part) =>
|
|
853
|
+
isToolUIPart(part) ? [{ messageId: message.id, part }] : [],
|
|
854
|
+
),
|
|
855
|
+
)
|
|
524
856
|
.find(
|
|
525
|
-
(part) =>
|
|
857
|
+
({ part }) =>
|
|
526
858
|
part.state === "approval-requested" &&
|
|
527
|
-
part.approval
|
|
859
|
+
part.approval?.id === approvalId,
|
|
528
860
|
);
|
|
529
861
|
if (!requested || hostApprovalIdsRef.current.has(approvalId))
|
|
530
862
|
throw new Error(
|
|
@@ -532,22 +864,47 @@ export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
|
|
|
532
864
|
);
|
|
533
865
|
|
|
534
866
|
// A host answer stays out of the useChat messages, where sendAutomaticallyWhen would forward it to the chat route.
|
|
867
|
+
// The owner can change while a response is in flight, and the id set is a
|
|
868
|
+
// ref that is reseeded when it does. Both writes are therefore scoped to
|
|
869
|
+
// the record this response started under, so a rollback never reaches a
|
|
870
|
+
// different chat's state.
|
|
871
|
+
const startedWith = ownedApprovals;
|
|
872
|
+
const startedOwner = approvalOwner;
|
|
873
|
+
const startedResponses = toolApprovalResponsesRef.current;
|
|
874
|
+
// Whether this response is currently applied, tracked here rather than
|
|
875
|
+
// read back from the id ref: that ref follows the chat on screen and is
|
|
876
|
+
// reseeded when the owner changes, so it cannot answer for this response.
|
|
877
|
+
let isApplied = false;
|
|
535
878
|
const applyResponse = (applied: boolean) => {
|
|
879
|
+
isApplied = applied;
|
|
880
|
+
// The captured record is always corrected, so a rollback reaches the
|
|
881
|
+
// chat the response belongs to even after the owner moved on.
|
|
882
|
+
if (applied)
|
|
883
|
+
startedWith?.set(approvalId, {
|
|
884
|
+
response,
|
|
885
|
+
});
|
|
886
|
+
else startedWith?.delete(approvalId);
|
|
887
|
+
|
|
888
|
+
if (startedOwner) {
|
|
889
|
+
// Every runtime mounted over that owner re-reads the record, including
|
|
890
|
+
// one mounted after this response started.
|
|
891
|
+
notifyHostApprovals(startedOwner);
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
if (lastApprovalOwnerRef.current !== startedOwner) return;
|
|
536
896
|
if (applied) hostApprovalIdsRef.current.add(approvalId);
|
|
537
897
|
else hostApprovalIdsRef.current.delete(approvalId);
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
else responses.delete(approvalId);
|
|
542
|
-
return responses;
|
|
543
|
-
});
|
|
898
|
+
if (applied) toolApprovalResponsesRef.current.set(approvalId, response);
|
|
899
|
+
else toolApprovalResponsesRef.current.delete(approvalId);
|
|
900
|
+
setToolApprovalResponses(new Map(toolApprovalResponsesRef.current));
|
|
544
901
|
};
|
|
545
902
|
|
|
546
903
|
applyResponse(true);
|
|
547
904
|
try {
|
|
548
905
|
await onRespond(response, {
|
|
549
|
-
toolCallId: requested.toolCallId,
|
|
550
|
-
toolName: getToolName(requested),
|
|
906
|
+
toolCallId: requested.part.toolCallId,
|
|
907
|
+
toolName: getToolName(requested.part),
|
|
551
908
|
respondViaAISDK: async () => {
|
|
552
909
|
try {
|
|
553
910
|
await respondViaAISDK(response);
|
|
@@ -557,9 +914,14 @@ export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
|
|
|
557
914
|
},
|
|
558
915
|
});
|
|
559
916
|
} catch (error) {
|
|
560
|
-
if (
|
|
917
|
+
if (isApplied) applyResponse(false);
|
|
561
918
|
throw error;
|
|
562
919
|
}
|
|
920
|
+
const remainsApplied =
|
|
921
|
+
startedWith?.has(approvalId) ?? startedResponses.has(approvalId);
|
|
922
|
+
if (isApplied && remainsApplied) {
|
|
923
|
+
await persistToolApprovalResponses(requested.messageId);
|
|
924
|
+
}
|
|
563
925
|
};
|
|
564
926
|
|
|
565
927
|
const hasSeededRepositoryRef = useRef(false);
|
|
@@ -569,6 +931,7 @@ export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
|
|
|
569
931
|
messages.length === 0;
|
|
570
932
|
|
|
571
933
|
const runtime = useExternalStoreRuntime({
|
|
934
|
+
unstable_persistsHistory: true,
|
|
572
935
|
isRunning: providerIsRunning,
|
|
573
936
|
...(shouldFeedRepository
|
|
574
937
|
? { messageRepository: exportedMessageRepository }
|
|
@@ -714,6 +1077,36 @@ export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
|
|
|
714
1077
|
|
|
715
1078
|
await deleteHistoryMessage(messageId);
|
|
716
1079
|
|
|
1080
|
+
let removedToolArtifact = false;
|
|
1081
|
+
let removedToolInteractions = false;
|
|
1082
|
+
let removedToolApprovalResponse = false;
|
|
1083
|
+
let removedHostApprovalId = false;
|
|
1084
|
+
for (const part of threadMessages[messageIndex]!.content) {
|
|
1085
|
+
if (part.type === "tool-call") {
|
|
1086
|
+
removedToolArtifact =
|
|
1087
|
+
toolArtifactsRef.current.delete(part.toolCallId) ||
|
|
1088
|
+
removedToolArtifact;
|
|
1089
|
+
removedToolInteractions =
|
|
1090
|
+
toolInteractionsRef.current.delete(part.toolCallId) ||
|
|
1091
|
+
removedToolInteractions;
|
|
1092
|
+
if (part.approval) {
|
|
1093
|
+
ownedApprovals?.delete(part.approval.id);
|
|
1094
|
+
removedToolApprovalResponse =
|
|
1095
|
+
toolApprovalResponsesRef.current.delete(part.approval.id) ||
|
|
1096
|
+
removedToolApprovalResponse;
|
|
1097
|
+
removedHostApprovalId =
|
|
1098
|
+
hostApprovalIdsRef.current.delete(part.approval.id) ||
|
|
1099
|
+
removedHostApprovalId;
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
if (removedToolArtifact) markToolArtifactsChanged();
|
|
1104
|
+
if (removedToolInteractions) markToolInteractionsChanged();
|
|
1105
|
+
if (removedToolApprovalResponse || removedHostApprovalId) {
|
|
1106
|
+
if (approvalOwner) notifyHostApprovals(approvalOwner);
|
|
1107
|
+
setToolApprovalResponses(new Map(toolApprovalResponsesRef.current));
|
|
1108
|
+
}
|
|
1109
|
+
|
|
717
1110
|
const deleteIds = new Set(
|
|
718
1111
|
getExternalStoreMessages<UI_MESSAGE>(threadMessages[messageIndex]!).map(
|
|
719
1112
|
(message) => message.id,
|
|
@@ -731,12 +1124,74 @@ export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
|
|
|
731
1124
|
await chatHelpers.regenerate({ metadata: config.runConfig });
|
|
732
1125
|
},
|
|
733
1126
|
onAddToolResult: ({
|
|
1127
|
+
messageId,
|
|
734
1128
|
toolCallId,
|
|
735
1129
|
toolName,
|
|
736
1130
|
result,
|
|
737
1131
|
isError,
|
|
1132
|
+
artifact,
|
|
738
1133
|
modelContent,
|
|
739
1134
|
}) => {
|
|
1135
|
+
if (artifact !== undefined) {
|
|
1136
|
+
toolArtifactsRef.current.set(toolCallId, artifact);
|
|
1137
|
+
markToolArtifactsChanged();
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
const targetIndex = findRawToolMessageIndex(
|
|
1141
|
+
chatHelpers.messages,
|
|
1142
|
+
messageId,
|
|
1143
|
+
toolCallId,
|
|
1144
|
+
joinStrategy,
|
|
1145
|
+
);
|
|
1146
|
+
|
|
1147
|
+
const errorText =
|
|
1148
|
+
typeof result === "string" ? result : JSON.stringify(result);
|
|
1149
|
+
const output =
|
|
1150
|
+
!isError && modelContent !== undefined
|
|
1151
|
+
? wrapModelContentEnvelope(result, modelContent)
|
|
1152
|
+
: result;
|
|
1153
|
+
|
|
1154
|
+
if (targetIndex >= 0 && targetIndex !== chatHelpers.messages.length - 1) {
|
|
1155
|
+
const target = chatHelpers.messages[targetIndex]!;
|
|
1156
|
+
const targetPart = target.parts.find(
|
|
1157
|
+
(part) => isToolUIPart(part) && part.toolCallId === toolCallId,
|
|
1158
|
+
) as { state?: string; preliminary?: boolean } | undefined;
|
|
1159
|
+
// An earlier message's settled output may already have reached the model, as the error a cancelling send writes does.
|
|
1160
|
+
if (
|
|
1161
|
+
targetPart?.state === "output-error" ||
|
|
1162
|
+
targetPart?.state === "output-denied" ||
|
|
1163
|
+
(targetPart?.state === "output-available" && !targetPart.preliminary)
|
|
1164
|
+
)
|
|
1165
|
+
return Promise.resolve();
|
|
1166
|
+
|
|
1167
|
+
const targetMessageId = target.id;
|
|
1168
|
+
chatHelpers.setMessages((current) =>
|
|
1169
|
+
current.map((message) => {
|
|
1170
|
+
if (message.id !== targetMessageId) return message;
|
|
1171
|
+
|
|
1172
|
+
return {
|
|
1173
|
+
...message,
|
|
1174
|
+
parts: message.parts.map((part) => {
|
|
1175
|
+
if (!isToolUIPart(part) || part.toolCallId !== toolCallId)
|
|
1176
|
+
return part;
|
|
1177
|
+
|
|
1178
|
+
const { preliminary: _preliminary, ...finalPart } =
|
|
1179
|
+
part as typeof part & { preliminary?: boolean };
|
|
1180
|
+
return {
|
|
1181
|
+
...finalPart,
|
|
1182
|
+
state: isError
|
|
1183
|
+
? ("output-error" as const)
|
|
1184
|
+
: ("output-available" as const),
|
|
1185
|
+
output: isError ? undefined : output,
|
|
1186
|
+
errorText: isError ? errorText : undefined,
|
|
1187
|
+
} as typeof part;
|
|
1188
|
+
}),
|
|
1189
|
+
};
|
|
1190
|
+
}),
|
|
1191
|
+
);
|
|
1192
|
+
return Promise.resolve();
|
|
1193
|
+
}
|
|
1194
|
+
|
|
740
1195
|
const options = { metadata: lastRunConfigRef.current };
|
|
741
1196
|
if (isError) {
|
|
742
1197
|
return Promise.resolve(
|
|
@@ -744,29 +1199,47 @@ export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
|
|
|
744
1199
|
state: "output-error",
|
|
745
1200
|
tool: toolName ?? toolCallId,
|
|
746
1201
|
toolCallId,
|
|
747
|
-
errorText
|
|
748
|
-
typeof result === "string" ? result : JSON.stringify(result),
|
|
749
|
-
options,
|
|
750
|
-
}),
|
|
751
|
-
);
|
|
752
|
-
} else {
|
|
753
|
-
const output =
|
|
754
|
-
modelContent !== undefined
|
|
755
|
-
? wrapModelContentEnvelope(result, modelContent)
|
|
756
|
-
: result;
|
|
757
|
-
return Promise.resolve(
|
|
758
|
-
chatHelpers.addToolOutput({
|
|
759
|
-
tool: toolName,
|
|
760
|
-
toolCallId,
|
|
761
|
-
output,
|
|
1202
|
+
errorText,
|
|
762
1203
|
options,
|
|
763
1204
|
}),
|
|
764
1205
|
);
|
|
765
1206
|
}
|
|
1207
|
+
|
|
1208
|
+
return Promise.resolve(
|
|
1209
|
+
chatHelpers.addToolOutput({
|
|
1210
|
+
tool: toolName,
|
|
1211
|
+
toolCallId,
|
|
1212
|
+
output,
|
|
1213
|
+
options,
|
|
1214
|
+
}),
|
|
1215
|
+
);
|
|
1216
|
+
},
|
|
1217
|
+
onRespondToToolApproval: (response) => {
|
|
1218
|
+
if (supersededApprovalProjection.approvalIds.has(response.approvalId))
|
|
1219
|
+
return Promise.reject(
|
|
1220
|
+
new Error(
|
|
1221
|
+
`Tool approval ${response.approvalId} is not waiting for a response.`,
|
|
1222
|
+
),
|
|
1223
|
+
);
|
|
1224
|
+
return customOnRespondToToolApproval
|
|
1225
|
+
? respondViaHost(customOnRespondToToolApproval, response)
|
|
1226
|
+
: respondViaAISDK(response);
|
|
1227
|
+
},
|
|
1228
|
+
unstable_onRecordToolInteraction: ({
|
|
1229
|
+
messageId,
|
|
1230
|
+
toolCallId,
|
|
1231
|
+
interaction,
|
|
1232
|
+
}) => {
|
|
1233
|
+
toolInteractionsRef.current.set(
|
|
1234
|
+
toolCallId,
|
|
1235
|
+
appendToolInteraction(
|
|
1236
|
+
toolInteractionsRef.current.get(toolCallId),
|
|
1237
|
+
interaction,
|
|
1238
|
+
),
|
|
1239
|
+
);
|
|
1240
|
+
markToolInteractionsChanged();
|
|
1241
|
+
return persistToolInteractions(messageId);
|
|
766
1242
|
},
|
|
767
|
-
onRespondToToolApproval: customOnRespondToToolApproval
|
|
768
|
-
? (response) => respondViaHost(customOnRespondToToolApproval, response)
|
|
769
|
-
: respondViaAISDK,
|
|
770
1243
|
...pickExternalStoreSharedOptions(adapter),
|
|
771
1244
|
...(adapter.unstable_messageRepositoryInstance && {
|
|
772
1245
|
unstable_messageRepositoryInstance:
|