@runtypelabs/persona 4.29.0 → 4.29.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/animations/glyph-cycle.d.cts +1 -1
- package/dist/animations/glyph-cycle.d.ts +1 -1
- package/dist/animations/wipe.d.cts +1 -1
- package/dist/animations/wipe.d.ts +1 -1
- package/dist/approval-ui.cjs +3 -3
- package/dist/approval-ui.d.cts +1 -1
- package/dist/approval-ui.d.ts +1 -1
- package/dist/approval-ui.js +3 -3
- package/dist/artifacts-ui.d.cts +2 -2
- package/dist/artifacts-ui.d.ts +2 -2
- package/dist/chunk-B1-_pp-Y.js +4 -0
- package/dist/{chunk-DjOvgFsn.js → chunk-BcLIgi1h.js} +1 -1
- package/dist/chunk-Bm2-bemv.js +1 -0
- package/dist/chunk-BvhTpG6k.js +1 -0
- package/dist/chunk-Csks3Qw1.js +1 -0
- package/dist/chunk-D5B629Mj.js +1 -0
- package/dist/chunk-DGlFKA0r.js +9 -0
- package/dist/chunk-DO9_WSpy.js +1 -0
- package/dist/chunk-DTDHeUsG.js +1 -0
- package/dist/chunk-DjFpGFGA.js +1 -0
- package/dist/{chunk-dRBlrLd7.js → chunk-DtmtfIkd.js} +1 -1
- package/dist/client-history.js +1 -1
- package/dist/client-stream.js +1 -1
- package/dist/codegen.cjs +1 -1
- package/dist/codegen.js +1 -1
- package/dist/{composer-input-D3sB8_P_.d.cts → composer-input-B0P4OtNi.d.cts} +1 -1
- package/dist/{composer-input-D3sB8_P_.d.ts → composer-input-B0P4OtNi.d.ts} +1 -1
- package/dist/{context-mention-manager-D2qxE5op.d.cts → context-mention-manager-DmLI_RM6.d.cts} +1 -1
- package/dist/{context-mention-manager-D2qxE5op.d.ts → context-mention-manager-DmLI_RM6.d.ts} +1 -1
- package/dist/context-mentions-inline.d.cts +2 -2
- package/dist/context-mentions-inline.d.ts +2 -2
- package/dist/context-mentions.d.cts +3 -3
- package/dist/context-mentions.d.ts +3 -3
- package/dist/{defaults-Dsw66XqE.d.cts → defaults-DVo1wbZs.d.cts} +1 -1
- package/dist/{defaults-Dsw66XqE.d.ts → defaults-DVo1wbZs.d.ts} +1 -1
- package/dist/event-stream-view.d.cts +1 -1
- package/dist/event-stream-view.d.ts +1 -1
- package/dist/{fallback-speech-engine-DJpJcWAk.d.cts → fallback-speech-engine-BMnlmKnY.d.cts} +1 -1
- package/dist/{fallback-speech-engine-DJpJcWAk.d.ts → fallback-speech-engine-BMnlmKnY.d.ts} +1 -1
- package/dist/forms-ui.d.cts +2 -2
- package/dist/forms-ui.d.ts +2 -2
- package/dist/history-view.d.cts +1 -1
- package/dist/history-view.d.ts +1 -1
- package/dist/index.cjs +56 -56
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -6
- package/dist/index.d.ts +6 -6
- package/dist/index.global.js +2 -3
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +38 -38
- package/dist/index.js.map +1 -1
- package/dist/{markdown-parsers-loader-CCY7T4Up.d.cts → markdown-parsers-loader-CeovcyYf.d.cts} +1 -1
- package/dist/{markdown-parsers-loader-CCY7T4Up.d.ts → markdown-parsers-loader-CeovcyYf.d.ts} +1 -1
- package/dist/{preview-DKKpfTo7.d.cts → preview-Bkb4UrQC.d.cts} +2 -2
- package/dist/{preview-DKKpfTo7.d.ts → preview-Bkb4UrQC.d.ts} +2 -2
- package/dist/session-actions.js +1 -1
- package/dist/{session-DleMr9dZ.d.ts → session-iLyKipoM.d.cts} +21 -9
- package/dist/{session-DleMr9dZ.d.cts → session-iLyKipoM.d.ts} +21 -9
- package/dist/session-reconnect.cjs +1 -1
- package/dist/session-reconnect.d.cts +8 -2
- package/dist/session-reconnect.d.ts +8 -2
- package/dist/session-reconnect.js +1 -1
- package/dist/smart-dom-reader.d.cts +1 -1
- package/dist/smart-dom-reader.d.ts +1 -1
- package/dist/theme-editor-preview.cjs +61 -61
- package/dist/theme-editor-preview.d.cts +1 -1
- package/dist/theme-editor-preview.d.ts +1 -1
- package/dist/theme-editor-preview.js +40 -40
- package/dist/theme-editor.d.cts +2 -2
- package/dist/theme-editor.d.ts +2 -2
- package/dist/theme-reference.d.cts +2 -2
- package/dist/theme-reference.d.ts +2 -2
- package/dist/{types-oAZ1VReS.d.cts → types-BuDirJkQ.d.cts} +35 -3
- package/dist/{types-oAZ1VReS.d.ts → types-BuDirJkQ.d.ts} +35 -3
- package/dist/voice-client-tools.d.cts +1 -1
- package/dist/voice-client-tools.d.ts +1 -1
- package/dist/voice-runtime.cjs +3 -3
- package/dist/voice-runtime.d.cts +10 -4
- package/dist/voice-runtime.d.ts +10 -4
- package/dist/voice-runtime.js +5 -5
- package/dist/voice-worklet-player.d.cts +2 -2
- package/dist/voice-worklet-player.d.ts +2 -2
- package/dist/{webmcp-runtime-entry-Deg0NfmB.d.cts → webmcp-runtime-entry-CXLjRt1k.d.cts} +1 -1
- package/dist/{webmcp-runtime-entry-Deg0NfmB.d.ts → webmcp-runtime-entry-CXLjRt1k.d.ts} +1 -1
- package/dist/webmcp-runtime.d.cts +1 -1
- package/dist/webmcp-runtime.d.ts +1 -1
- package/dist/widget.css +1 -1
- package/package.json +1 -1
- package/src/client-approve.test.ts +17 -1
- package/src/client-resume.ts +15 -3
- package/src/client.interrupt.test.ts +21 -0
- package/src/client.ts +24 -4
- package/src/components/activity-row.ts +3 -2
- package/src/components/approval-actions.test.ts +8 -0
- package/src/components/approval-actions.ts +6 -2
- package/src/components/approval-bubble.test.ts +14 -0
- package/src/components/approval-bubble.ts +8 -1
- package/src/components/tool-bubble.ts +8 -5
- package/src/generated/runtype-openapi-contract.ts +1 -1
- package/src/resume-dedupe-local-only.test.ts +273 -0
- package/src/runtime/persist-state.test.ts +86 -0
- package/src/session-actions.ts +20 -6
- package/src/session-reconnect.ts +18 -1
- package/src/session.restored-browser-tools.test.ts +272 -0
- package/src/session.ts +176 -29
- package/src/session.voice.test.ts +169 -0
- package/src/styles/widget.css +4 -2
- package/src/types.ts +37 -2
- package/src/ui.restored-browser-tools.test.ts +128 -0
- package/src/ui.ts +32 -18
- package/src/utils/formatting.test.ts +9 -1
- package/src/utils/formatting.ts +4 -0
- package/src/utils/local-only-message.ts +15 -0
- package/src/utils/restored-browser-state.ts +50 -0
- package/src/utils/storage.ts +12 -1
- package/src/utils/voice-prior-tool-results.ts +37 -0
- package/src/voice/runtype-voice-provider.ts +13 -8
- package/src/voice/session-voice-wiring.ts +1 -2
- package/src/voice/voice-delegation.ts +10 -3
- package/src/voice/voice.test.ts +38 -2
- package/dist/chunk-B6zZsT6F.js +0 -1
- package/dist/chunk-CIVsNcrk.js +0 -1
- package/dist/chunk-CKdlNyzM.js +0 -9
- package/dist/chunk-DbhxjyOB.js +0 -4
- package/dist/chunk-DsdqLStH.js +0 -1
- package/dist/chunk-GIqbL2Eg.js +0 -1
package/src/session.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
type SSEEventCallback,
|
|
5
5
|
} from "./client";
|
|
6
6
|
import { isWebMcpToolName } from "./webmcp-bridge";
|
|
7
|
+
import { isBrowserLocalApproval, restoreBrowserToolState } from "./utils/restored-browser-state";
|
|
7
8
|
import type { SessionActionsHost, SessionActionsInternals } from "./session-actions";
|
|
8
9
|
import {
|
|
9
10
|
getSessionActionsSync,
|
|
@@ -42,12 +43,14 @@ import {
|
|
|
42
43
|
generateAssistantMessageId
|
|
43
44
|
} from "./utils/message-id";
|
|
44
45
|
import { IMAGE_ONLY_MESSAGE_FALLBACK_TEXT, createTextPart, hasImages } from "./utils/content";
|
|
46
|
+
import { voicePriorToolRows } from "./utils/voice-prior-tool-results";
|
|
45
47
|
import {
|
|
46
48
|
buildArtifactRefRawContent,
|
|
47
49
|
resolveArtifactDisplayMode
|
|
48
50
|
} from "./utils/artifact-display";
|
|
49
51
|
import type {
|
|
50
52
|
VoiceProvider,
|
|
53
|
+
VoicePriorToolResult,
|
|
51
54
|
VoiceStatus,
|
|
52
55
|
VoiceConfig,
|
|
53
56
|
ReadAloudState,
|
|
@@ -437,7 +440,7 @@ export class AgentWidgetSession {
|
|
|
437
440
|
) {
|
|
438
441
|
this.historyInternals = this.#composeHistoryInternals(historyInternals);
|
|
439
442
|
this.messages = [...(config.initialMessages ?? [])].map((message) => ({
|
|
440
|
-
...message,
|
|
443
|
+
...restoreBrowserToolState(message),
|
|
441
444
|
sequence: message.sequence ?? this.#nextSequence()
|
|
442
445
|
}));
|
|
443
446
|
this.messages = this.#sortMessages(this.messages);
|
|
@@ -1343,7 +1346,8 @@ export class AgentWidgetSession {
|
|
|
1343
1346
|
if (this.#activeConversationId && this.#activeConversationId !== id) {
|
|
1344
1347
|
return { messages: [], nextMessageCursor: page.nextCursor };
|
|
1345
1348
|
}
|
|
1346
|
-
const
|
|
1349
|
+
const restored = this.#restoreHistoryPage(page.messages);
|
|
1350
|
+
const merged = mergeWireMessagesById(this.messages, restored);
|
|
1347
1351
|
this.messages = this.#sortMessages(
|
|
1348
1352
|
merged.map((message) => this.#ensureSequence(message))
|
|
1349
1353
|
);
|
|
@@ -1354,7 +1358,30 @@ export class AgentWidgetSession {
|
|
|
1354
1358
|
this.historyInternals.setStoredMessageCursor?.(page.nextCursor);
|
|
1355
1359
|
this.callbacks.onMessagesChanged([...this.messages]);
|
|
1356
1360
|
this.#emitHistoryState();
|
|
1357
|
-
return { messages:
|
|
1361
|
+
return { messages: restored, nextMessageCursor: page.nextCursor };
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
/** Normalize incoming history without retiring callbacks owned by this chat. */
|
|
1365
|
+
#restoreHistoryPage(messages: AgentWidgetMessage[]): AgentWidgetMessage[] {
|
|
1366
|
+
const current = new Map(this.messages.map(message => [message.id, message]));
|
|
1367
|
+
return messages.map(message => {
|
|
1368
|
+
const live = current.get(message.id);
|
|
1369
|
+
if (live) {
|
|
1370
|
+
if (this.webMcpApprovalResolvers.has(live.id)) return live;
|
|
1371
|
+
// A decision made while the history fetch was in flight must not be
|
|
1372
|
+
// replaced by the page's stale pending copy of the same browser card.
|
|
1373
|
+
if (live.approval && isBrowserLocalApproval(live.approval) &&
|
|
1374
|
+
live.approval.id === message.approval?.id &&
|
|
1375
|
+
live.approval.status !== "pending" && message.approval?.status === "pending") return live;
|
|
1376
|
+
const executionId = live.agentMetadata?.executionId;
|
|
1377
|
+
const tools = [...(live.toolCall ? [live.toolCall] : []), ...(live.tools ?? [])];
|
|
1378
|
+
if (executionId && tools.some(tool =>
|
|
1379
|
+
this.webMcpInflightKeys.has(`${executionId}:${tool.id}`) ||
|
|
1380
|
+
this.webMcpResolvedKeys.has(`${executionId}:${tool.id}`)
|
|
1381
|
+
)) return live;
|
|
1382
|
+
}
|
|
1383
|
+
return restoreBrowserToolState(message);
|
|
1384
|
+
});
|
|
1358
1385
|
}
|
|
1359
1386
|
|
|
1360
1387
|
/**
|
|
@@ -1800,6 +1827,7 @@ export class AgentWidgetSession {
|
|
|
1800
1827
|
}
|
|
1801
1828
|
|
|
1802
1829
|
const page = await provider.getPage(conversationId, { context });
|
|
1830
|
+
const restored = this.#restoreHistoryPage(page.messages);
|
|
1803
1831
|
const serverIds = new Set(page.messages.map((message) => message.id));
|
|
1804
1832
|
const overlaps = this.messages.some((message) => serverIds.has(message.id));
|
|
1805
1833
|
const pageOldest = page.messages[0]?.createdAt;
|
|
@@ -1807,7 +1835,7 @@ export class AgentWidgetSession {
|
|
|
1807
1835
|
let reconciled: AgentWidgetMessage[];
|
|
1808
1836
|
if (!overlaps) {
|
|
1809
1837
|
// No shared id: the local transcript is not this record's newest window.
|
|
1810
|
-
reconciled =
|
|
1838
|
+
reconciled = restored.map((message) => this.#ensureSequence(message));
|
|
1811
1839
|
} else {
|
|
1812
1840
|
const durableId = this.#resumable ? this.#activeAssistantMessageId : null;
|
|
1813
1841
|
const survivors = this.messages.filter((message) => {
|
|
@@ -1816,7 +1844,7 @@ export class AgentWidgetSession {
|
|
|
1816
1844
|
// Older than the server window is legitimately absent from this page.
|
|
1817
1845
|
return pageOldest !== undefined && message.createdAt < pageOldest;
|
|
1818
1846
|
});
|
|
1819
|
-
reconciled = mergeWireMessagesById(survivors,
|
|
1847
|
+
reconciled = mergeWireMessagesById(survivors, restored).map(
|
|
1820
1848
|
(message) => this.#ensureSequence(message)
|
|
1821
1849
|
);
|
|
1822
1850
|
}
|
|
@@ -2042,7 +2070,12 @@ export class AgentWidgetSession {
|
|
|
2042
2070
|
controller: AbortController,
|
|
2043
2071
|
assistantMessageId: string,
|
|
2044
2072
|
userMessageId: string,
|
|
2045
|
-
turnOptions?: {
|
|
2073
|
+
turnOptions?: {
|
|
2074
|
+
composerOptions?: ComposerOptionsPayload;
|
|
2075
|
+
interrupt?: boolean;
|
|
2076
|
+
voiceSpoken?: boolean;
|
|
2077
|
+
voicePriorToolResults?: VoicePriorToolResult[];
|
|
2078
|
+
}
|
|
2046
2079
|
): Promise<void> {
|
|
2047
2080
|
this.#recordDispatchedProjections(snapshot);
|
|
2048
2081
|
try {
|
|
@@ -2053,7 +2086,8 @@ export class AgentWidgetSession {
|
|
|
2053
2086
|
assistantMessageId,
|
|
2054
2087
|
composerOptions: turnOptions?.composerOptions,
|
|
2055
2088
|
interrupt: turnOptions?.interrupt,
|
|
2056
|
-
voiceSpoken: turnOptions?.voiceSpoken
|
|
2089
|
+
voiceSpoken: turnOptions?.voiceSpoken,
|
|
2090
|
+
voicePriorToolResults: turnOptions?.voicePriorToolResults
|
|
2057
2091
|
},
|
|
2058
2092
|
this.handleEvent
|
|
2059
2093
|
);
|
|
@@ -2072,6 +2106,7 @@ export class AgentWidgetSession {
|
|
|
2072
2106
|
assistantMessageId,
|
|
2073
2107
|
composerOptions: turnOptions?.composerOptions,
|
|
2074
2108
|
voiceSpoken: turnOptions?.voiceSpoken,
|
|
2109
|
+
voicePriorToolResults: turnOptions?.voicePriorToolResults,
|
|
2075
2110
|
},
|
|
2076
2111
|
this.handleEvent
|
|
2077
2112
|
);
|
|
@@ -2535,9 +2570,11 @@ export class AgentWidgetSession {
|
|
|
2535
2570
|
* Internal (voice client delegation): this is the delegated voice turn.
|
|
2536
2571
|
* `userMessageId` names an existing transcript bubble to submit as the
|
|
2537
2572
|
* turn's user message instead of appending a new one, so the spoken
|
|
2538
|
-
* request appears (and is sent) exactly once.
|
|
2573
|
+
* request appears (and is sent) exactly once. `priorToolResults`: page
|
|
2574
|
+
* tools the voice server already ran for this turn, recorded once as
|
|
2575
|
+
* completed tool rows after the request (see voicePriorToolRows).
|
|
2539
2576
|
*/
|
|
2540
|
-
voiceTurn?: { userMessageId?: string };
|
|
2577
|
+
voiceTurn?: { userMessageId?: string | null; priorToolResults?: unknown };
|
|
2541
2578
|
}
|
|
2542
2579
|
) {
|
|
2543
2580
|
const input = rawInput.trim();
|
|
@@ -2692,12 +2729,25 @@ export class AgentWidgetSession {
|
|
|
2692
2729
|
|
|
2693
2730
|
// A submitted voice bubble may sit above captions or a turn that finished
|
|
2694
2731
|
// while it waited: the request still ends on it.
|
|
2732
|
+
const sentAt = new Date().toISOString();
|
|
2695
2733
|
const snapshot = voiceBubble
|
|
2696
|
-
? [
|
|
2697
|
-
...this.messages.filter((m) => m !== voiceBubble),
|
|
2698
|
-
{ ...voiceBubble, createdAt: new Date().toISOString() }
|
|
2699
|
-
]
|
|
2734
|
+
? [...this.messages.filter((m) => m !== voiceBubble), { ...voiceBubble, createdAt: sentAt }]
|
|
2700
2735
|
: [...this.messages];
|
|
2736
|
+
// Tools the voice server already ran answer this request and are never run
|
|
2737
|
+
// again. Stored at the request's own time, so every later replay keeps them
|
|
2738
|
+
// right after it; this request (moved to `sentAt`) sends them at `sentAt`.
|
|
2739
|
+
const requestAt = (voiceBubble ?? this.messages.find((m) => m.id === userMessageId))?.createdAt ?? sentAt;
|
|
2740
|
+
const priorRows = voicePriorToolRows(options?.voiceTurn?.priorToolResults, requestAt);
|
|
2741
|
+
for (const row of priorRows) {
|
|
2742
|
+
if (this.messages.some((m) => m.id === row.id)) continue;
|
|
2743
|
+
this.#appendMessage(row);
|
|
2744
|
+
snapshot.push({ ...row, createdAt: sentAt });
|
|
2745
|
+
}
|
|
2746
|
+
// Client-token chat replays from the server's transcript: it gets them as a field.
|
|
2747
|
+
const voicePriorToolResults = priorRows.map(({ agentMetadata }) => {
|
|
2748
|
+
const { toolCallId, toolName, args, result } = agentMetadata!.clientToolAnswer!;
|
|
2749
|
+
return { toolCallId, name: toolName, arguments: args as Record<string, unknown>, result };
|
|
2750
|
+
});
|
|
2701
2751
|
|
|
2702
2752
|
try {
|
|
2703
2753
|
await this.#dispatchWithDeletedRecovery(
|
|
@@ -2709,7 +2759,8 @@ export class AgentWidgetSession {
|
|
|
2709
2759
|
composerOptions: options?.composerOptions,
|
|
2710
2760
|
interrupt: options?.interrupt,
|
|
2711
2761
|
// The delegated voice turn's reply is read aloud (spoken-reply hint).
|
|
2712
|
-
voiceSpoken: !!options?.voiceTurn
|
|
2762
|
+
voiceSpoken: !!options?.voiceTurn,
|
|
2763
|
+
...(voicePriorToolResults.length > 0 && { voicePriorToolResults })
|
|
2713
2764
|
}
|
|
2714
2765
|
);
|
|
2715
2766
|
} catch (error) {
|
|
@@ -2740,6 +2791,7 @@ export class AgentWidgetSession {
|
|
|
2740
2791
|
role: "assistant",
|
|
2741
2792
|
createdAt: new Date().toISOString(),
|
|
2742
2793
|
content,
|
|
2794
|
+
localOnly: true,
|
|
2743
2795
|
sequence: this.#nextSequence()
|
|
2744
2796
|
};
|
|
2745
2797
|
|
|
@@ -2928,6 +2980,7 @@ export class AgentWidgetSession {
|
|
|
2928
2980
|
role: "assistant",
|
|
2929
2981
|
createdAt: new Date().toISOString(),
|
|
2930
2982
|
content,
|
|
2983
|
+
localOnly: true,
|
|
2931
2984
|
sequence: this.#nextSequence()
|
|
2932
2985
|
};
|
|
2933
2986
|
|
|
@@ -3078,8 +3131,14 @@ export class AgentWidgetSession {
|
|
|
3078
3131
|
}
|
|
3079
3132
|
if (info.signal?.aborted) return Promise.resolve(false);
|
|
3080
3133
|
|
|
3134
|
+
let approvalId: string;
|
|
3135
|
+
do {
|
|
3136
|
+
approvalId = `webmcp-${++this.#webMcpApprovalSeq}`;
|
|
3137
|
+
} while (this.messages.some(message =>
|
|
3138
|
+
message.id === `approval-${approvalId}` || message.approval?.id === approvalId
|
|
3139
|
+
));
|
|
3081
3140
|
const approval: AgentWidgetApproval = {
|
|
3082
|
-
id:
|
|
3141
|
+
id: approvalId,
|
|
3083
3142
|
status: "pending",
|
|
3084
3143
|
agentId: "",
|
|
3085
3144
|
executionId: "",
|
|
@@ -3666,7 +3725,7 @@ export class AgentWidgetSession {
|
|
|
3666
3725
|
this.webMcpResolvedKeys.clear();
|
|
3667
3726
|
this.messages = this.#sortMessages(
|
|
3668
3727
|
messages.map((message) => ({
|
|
3669
|
-
...message,
|
|
3728
|
+
...restoreBrowserToolState(message),
|
|
3670
3729
|
streaming: false,
|
|
3671
3730
|
sequence: message.sequence ?? this.#nextSequence()
|
|
3672
3731
|
}))
|
|
@@ -3773,7 +3832,9 @@ export class AgentWidgetSession {
|
|
|
3773
3832
|
resolveFollowUpsFeature(this.config).enabled));
|
|
3774
3833
|
if (
|
|
3775
3834
|
event.message.agentMetadata?.awaitingLocalTool === true &&
|
|
3776
|
-
autoResolvable
|
|
3835
|
+
autoResolvable &&
|
|
3836
|
+
this.messages.find(message => message.id === event.message.id)
|
|
3837
|
+
?.agentMetadata?.awaitingLocalTool === true
|
|
3777
3838
|
) {
|
|
3778
3839
|
// Collect the await into its executionId's batch instead of resolving
|
|
3779
3840
|
// it on the spot. Parallel same-tool calls (core#3878) arrive as
|
|
@@ -4029,6 +4090,7 @@ export class AgentWidgetSession {
|
|
|
4029
4090
|
seedContent,
|
|
4030
4091
|
}),
|
|
4031
4092
|
appendMessage: (message) => session.#appendMessage(message),
|
|
4093
|
+
settleParked: (executionId) => session.#settleParkedReconnect(executionId),
|
|
4032
4094
|
nextSequence: () => session.#nextSequence(),
|
|
4033
4095
|
emitReconnect: (event) => session.callbacks.onReconnect?.(event),
|
|
4034
4096
|
buildErrorContent: (message) =>
|
|
@@ -4060,7 +4122,28 @@ export class AgentWidgetSession {
|
|
|
4060
4122
|
if (typeof this.config.reconnectStream !== "function") return;
|
|
4061
4123
|
if (this.#reconnecting) return;
|
|
4062
4124
|
|
|
4063
|
-
|
|
4125
|
+
// A run parked on an await the restored transcript can still answer (an
|
|
4126
|
+
// ask_user_question card, a server approval) has no stream to replay, and
|
|
4127
|
+
// reconnecting would spin into a "Connection lost" bubble. Keep the cursor
|
|
4128
|
+
// so the answer's `/resume` attaches after the await frame, as in-session.
|
|
4129
|
+
if (this.#isAwaitPending()) {
|
|
4130
|
+
this.#adoptBootExecution(resume.executionId);
|
|
4131
|
+
this.#resumable = {
|
|
4132
|
+
executionId: resume.executionId,
|
|
4133
|
+
lastEventId: resume.after,
|
|
4134
|
+
assistantMessageId: this.#activeAssistantMessageId ?? "",
|
|
4135
|
+
status: "running",
|
|
4136
|
+
};
|
|
4137
|
+
return;
|
|
4138
|
+
}
|
|
4139
|
+
|
|
4140
|
+
// Parked on a WebMCP call the reload interrupted: still reconnect once, so
|
|
4141
|
+
// a replayed `await` re-dispatches the call on this page, but don't reopen
|
|
4142
|
+
// the pre-tool text bubble or paint an empty one. The reconnect loop ends
|
|
4143
|
+
// the turn quietly when nothing replays (`#settleParkedReconnect`).
|
|
4144
|
+
let assistantId = this.#hasInterruptedBrowserTool(resume.executionId)
|
|
4145
|
+
? generateAssistantMessageId()
|
|
4146
|
+
: this.#reopenTrailingAssistant();
|
|
4064
4147
|
if (!assistantId) {
|
|
4065
4148
|
assistantId = generateAssistantMessageId();
|
|
4066
4149
|
this.#appendMessage({
|
|
@@ -4073,16 +4156,7 @@ export class AgentWidgetSession {
|
|
|
4073
4156
|
});
|
|
4074
4157
|
}
|
|
4075
4158
|
this.#activeAssistantMessageId = assistantId;
|
|
4076
|
-
|
|
4077
|
-
this.agentExecution = {
|
|
4078
|
-
executionId: resume.executionId,
|
|
4079
|
-
agentId: "",
|
|
4080
|
-
agentName: "",
|
|
4081
|
-
status: "running",
|
|
4082
|
-
currentIteration: 0,
|
|
4083
|
-
maxTurns: 0,
|
|
4084
|
-
};
|
|
4085
|
-
}
|
|
4159
|
+
this.#adoptBootExecution(resume.executionId);
|
|
4086
4160
|
this.#resumable = {
|
|
4087
4161
|
executionId: resume.executionId,
|
|
4088
4162
|
lastEventId: resume.after,
|
|
@@ -4092,6 +4166,63 @@ export class AgentWidgetSession {
|
|
|
4092
4166
|
this.#beginReconnect();
|
|
4093
4167
|
}
|
|
4094
4168
|
|
|
4169
|
+
/**
|
|
4170
|
+
* A reconnect attempt left the run parked rather than dropped: the turn is
|
|
4171
|
+
* waiting on the visitor or a page tool (keep the cursor, as an in-session
|
|
4172
|
+
* await pause does), or on a WebMCP call the reload interrupted (nothing can
|
|
4173
|
+
* answer it, so drop the handle). Either way stop reconnecting without a
|
|
4174
|
+
* failure bubble. Returns false for an ordinary drop, which keeps retrying.
|
|
4175
|
+
*/
|
|
4176
|
+
#settleParkedReconnect(executionId: string): boolean {
|
|
4177
|
+
const pageToolBusy =
|
|
4178
|
+
this.webMcpResolveControllers.size > 0 || this.webMcpAwaitBatches.size > 0;
|
|
4179
|
+
if (!pageToolBusy && !this.#isAwaitPending()) {
|
|
4180
|
+
if (!this.#hasInterruptedBrowserTool(executionId)) return false;
|
|
4181
|
+
this.#clearResumable();
|
|
4182
|
+
}
|
|
4183
|
+
this.#reconnecting = false;
|
|
4184
|
+
if (!pageToolBusy) {
|
|
4185
|
+
let changed = false;
|
|
4186
|
+
for (const msg of this.messages) {
|
|
4187
|
+
if (msg.streaming) {
|
|
4188
|
+
msg.streaming = false;
|
|
4189
|
+
changed = true;
|
|
4190
|
+
}
|
|
4191
|
+
}
|
|
4192
|
+
if (changed) this.callbacks.onMessagesChanged([...this.messages]);
|
|
4193
|
+
this.#setStreaming(false);
|
|
4194
|
+
this.abortController = null;
|
|
4195
|
+
}
|
|
4196
|
+
this.#setStatus("idle");
|
|
4197
|
+
return true;
|
|
4198
|
+
}
|
|
4199
|
+
|
|
4200
|
+
/** Restore the running execution a boot resume refers to, if none is live. */
|
|
4201
|
+
#adoptBootExecution(executionId: string): void {
|
|
4202
|
+
if (this.agentExecution) return;
|
|
4203
|
+
this.agentExecution = {
|
|
4204
|
+
executionId,
|
|
4205
|
+
agentId: "",
|
|
4206
|
+
agentName: "",
|
|
4207
|
+
status: "running",
|
|
4208
|
+
currentIteration: 0,
|
|
4209
|
+
maxTurns: 0,
|
|
4210
|
+
};
|
|
4211
|
+
}
|
|
4212
|
+
|
|
4213
|
+
/**
|
|
4214
|
+
* True when restore settled one of this execution's WebMCP calls as
|
|
4215
|
+
* interrupted (see `restoreBrowserToolState`): the run is parked on a
|
|
4216
|
+
* browser callback that no longer exists.
|
|
4217
|
+
*/
|
|
4218
|
+
#hasInterruptedBrowserTool(executionId: string): boolean {
|
|
4219
|
+
return this.messages.some(
|
|
4220
|
+
(m) =>
|
|
4221
|
+
m.agentMetadata?.executionId === executionId &&
|
|
4222
|
+
[m.toolCall, ...(m.tools ?? [])].some((tool) => tool?.interrupted === true)
|
|
4223
|
+
);
|
|
4224
|
+
}
|
|
4225
|
+
|
|
4095
4226
|
/**
|
|
4096
4227
|
* Reopen the trailing assistant bubble (the persisted partial) so replayed
|
|
4097
4228
|
* deltas append to it. Returns its id, or null if the last turn doesn't end
|
|
@@ -4335,6 +4466,21 @@ export class AgentWidgetSession {
|
|
|
4335
4466
|
|
|
4336
4467
|
this.messages = this.messages.map((existing, idx) => {
|
|
4337
4468
|
if (idx !== index) return existing;
|
|
4469
|
+
// An unnamed skeleton precedes tool_start/await on a fresh parser. Keep
|
|
4470
|
+
// history until the populated event identifies the continuation. Also
|
|
4471
|
+
// retain actual terminal browser outcomes across replayed starts/deltas:
|
|
4472
|
+
// those events must never turn a completed mutation back into an await.
|
|
4473
|
+
if (
|
|
4474
|
+
existing.toolCall?.name && isWebMcpToolName(existing.toolCall.name) &&
|
|
4475
|
+
existing.toolCall.status === "complete" &&
|
|
4476
|
+
withSequence.toolCall?.id === existing.toolCall.id &&
|
|
4477
|
+
withSequence.toolCall.status !== "complete" &&
|
|
4478
|
+
((withSequence.toolCall.status === "pending" && !withSequence.toolCall.name) ||
|
|
4479
|
+
(!existing.toolCall.interrupted &&
|
|
4480
|
+
(existing.toolCall.result !== undefined || existing.toolCall.success !== undefined) &&
|
|
4481
|
+
(!withSequence.agentMetadata?.executionId ||
|
|
4482
|
+
withSequence.agentMetadata.executionId === existing.agentMetadata?.executionId)))
|
|
4483
|
+
) return existing;
|
|
4338
4484
|
const merged = { ...existing, ...withSequence };
|
|
4339
4485
|
// Preserve `ask_user_question` answered state across re-emissions.
|
|
4340
4486
|
// Top-level merge would otherwise replace `agentMetadata` wholesale: // post-resume events (e.g. `tool_complete` re-emitted from a stale
|
|
@@ -4441,7 +4587,8 @@ export class AgentWidgetSession {
|
|
|
4441
4587
|
existing.toolCall?.id === reTcId &&
|
|
4442
4588
|
existingToolName !== undefined &&
|
|
4443
4589
|
isAutoResolvedLocalToolName(existingToolName) &&
|
|
4444
|
-
existing.toolCall?.status === "complete"
|
|
4590
|
+
existing.toolCall?.status === "complete" &&
|
|
4591
|
+
!existing.toolCall.interrupted;
|
|
4445
4592
|
if (isInflight || isResolved || hasCompletedTool) {
|
|
4446
4593
|
merged.agentMetadata = {
|
|
4447
4594
|
...(merged.agentMetadata ?? {}),
|
|
@@ -666,6 +666,175 @@ describe('AgentWidgetSession - voice client delegation bridge', () => {
|
|
|
666
666
|
]);
|
|
667
667
|
});
|
|
668
668
|
|
|
669
|
+
describe('prior tool results on a hand-back', () => {
|
|
670
|
+
const jump = {
|
|
671
|
+
toolCallId: 'vtc_1',
|
|
672
|
+
name: 'webmcp:jump_to_slide',
|
|
673
|
+
arguments: { position: 5 },
|
|
674
|
+
result: { slidePosition: 5, slideTitle: 'Pricing' },
|
|
675
|
+
};
|
|
676
|
+
const toolRows = () => messages.filter((m) => m.variant === 'tool');
|
|
677
|
+
|
|
678
|
+
it('records the executed call once, under the request and before the reply, as a completed call', async () => {
|
|
679
|
+
h.state.transcriptCb!('user', 'Jump to the pricing slide', true, { turnId: 'u1', caption: true });
|
|
680
|
+
dispatch.mockImplementation(async (_options, onEvent) => reply(onEvent, 'You are on Pricing.'));
|
|
681
|
+
await h.state.bridge!.runDelegatedTurn(
|
|
682
|
+
req({ delegationId: 'd1', userText: 'Jump to the pricing slide', userUtteranceIds: ['u1'], priorToolResults: [jump] }),
|
|
683
|
+
);
|
|
684
|
+
|
|
685
|
+
expect(messages.map((m) => [m.role, m.variant ?? '', m.content])).toEqual([
|
|
686
|
+
['assistant', '', 'Welcome! How can I help?'],
|
|
687
|
+
['user', '', 'Jump to the pricing slide'],
|
|
688
|
+
['assistant', 'tool', ''],
|
|
689
|
+
['assistant', '', 'You are on Pricing.'],
|
|
690
|
+
]);
|
|
691
|
+
const [row] = toolRows();
|
|
692
|
+
expect(row.toolCall).toEqual({
|
|
693
|
+
id: 'vtc_1',
|
|
694
|
+
name: 'webmcp:jump_to_slide',
|
|
695
|
+
status: 'complete',
|
|
696
|
+
args: { position: 5 },
|
|
697
|
+
result: { slidePosition: 5, slideTitle: 'Pricing' },
|
|
698
|
+
});
|
|
699
|
+
expect(row.agentMetadata).toEqual({
|
|
700
|
+
clientToolAnswer: {
|
|
701
|
+
toolCallId: 'vtc_1',
|
|
702
|
+
toolName: 'webmcp:jump_to_slide',
|
|
703
|
+
args: { position: 5 },
|
|
704
|
+
result: { slidePosition: 5, slideTitle: 'Pricing' },
|
|
705
|
+
},
|
|
706
|
+
});
|
|
707
|
+
// Nothing marks it awaiting, so the WebMCP resolve path never runs it.
|
|
708
|
+
expect(row.agentMetadata?.awaitingLocalTool).toBeUndefined();
|
|
709
|
+
|
|
710
|
+
// The request ends on the user turn followed by the executed call.
|
|
711
|
+
const sent = dispatch.mock.calls[0][0].messages.filter((m) => !m.voiceCaption);
|
|
712
|
+
expect(sent.slice(-2).map((m) => [m.role, m.variant ?? '', m.content])).toEqual([
|
|
713
|
+
['user', '', 'Jump to the pricing slide'],
|
|
714
|
+
['assistant', 'tool', ''],
|
|
715
|
+
]);
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
it('records nothing when the field is absent or holds no well-formed entry', async () => {
|
|
719
|
+
dispatch.mockImplementation(async (_options, onEvent) => reply(onEvent, 'Done.'));
|
|
720
|
+
await h.state.bridge!.runDelegatedTurn(req({ delegationId: 'd1', userText: 'next slide' }));
|
|
721
|
+
await h.state.bridge!.runDelegatedTurn(
|
|
722
|
+
req({
|
|
723
|
+
delegationId: 'd2',
|
|
724
|
+
userText: 'next slide',
|
|
725
|
+
priorToolResults: [{ name: 'webmcp:next_slide' }, { toolCallId: 'vtc_2' }, 'junk', null],
|
|
726
|
+
}),
|
|
727
|
+
);
|
|
728
|
+
await h.state.bridge!.runDelegatedTurn(req({ delegationId: 'd3', userText: 'next slide', priorToolResults: 'nope' }));
|
|
729
|
+
expect(toolRows()).toEqual([]);
|
|
730
|
+
expect(dispatch).toHaveBeenCalledTimes(3);
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
it('defaults missing arguments and result, and never duplicates a call already recorded', async () => {
|
|
734
|
+
dispatch.mockImplementation(async (_options, onEvent) => reply(onEvent, 'Done.'));
|
|
735
|
+
const bare = { toolCallId: 'vtc_3', name: 'webmcp:next_slide', arguments: 'bad' };
|
|
736
|
+
await h.state.bridge!.runDelegatedTurn(req({ delegationId: 'd1', userText: 'next slide', priorToolResults: [bare] }));
|
|
737
|
+
await h.state.bridge!.runDelegatedTurn(req({ delegationId: 'd1', userText: 'next slide', priorToolResults: [bare] }));
|
|
738
|
+
await session.sendMessage('thanks');
|
|
739
|
+
expect(toolRows()).toHaveLength(1);
|
|
740
|
+
expect(toolRows()[0].toolCall).toMatchObject({ args: {}, result: null });
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
it('hands the client-token request the executed calls only when the hand-back carried some', async () => {
|
|
744
|
+
dispatch.mockImplementation(async (_options, onEvent) => reply(onEvent, 'Done.'));
|
|
745
|
+
await h.state.bridge!.runDelegatedTurn(
|
|
746
|
+
req({ delegationId: 'd1', userText: 'jump', priorToolResults: [jump, 'junk', { ...jump, result: 'repeat' }] }),
|
|
747
|
+
);
|
|
748
|
+
await h.state.bridge!.runDelegatedTurn(req({ delegationId: 'd2', userText: 'next slide' }));
|
|
749
|
+
await session.sendMessage('typed');
|
|
750
|
+
const options = dispatch.mock.calls.map(([o]) => o as { voicePriorToolResults?: unknown });
|
|
751
|
+
expect(options[0].voicePriorToolResults).toEqual([jump]);
|
|
752
|
+
expect(toolRows()).toHaveLength(1);
|
|
753
|
+
expect(options[1].voicePriorToolResults).toBeUndefined();
|
|
754
|
+
expect(options[2].voicePriorToolResults).toBeUndefined();
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
it('keeps the executed call right after its request on later replays, even past a reply that finished first', async () => {
|
|
758
|
+
const bodies: Array<{ messages: Array<Record<string, unknown>> }> = [];
|
|
759
|
+
vi.stubGlobal(
|
|
760
|
+
'fetch',
|
|
761
|
+
vi.fn(async (_url: string, init?: RequestInit) => {
|
|
762
|
+
bodies.push(JSON.parse(String(init?.body)));
|
|
763
|
+
return new Response('', { status: 200, headers: { 'Content-Type': 'text/event-stream' } });
|
|
764
|
+
}),
|
|
765
|
+
);
|
|
766
|
+
vi.useFakeTimers({ toFake: ['Date'] });
|
|
767
|
+
try {
|
|
768
|
+
delete (session as unknown as { client: { dispatch?: Dispatch } }).client.dispatch;
|
|
769
|
+
vi.setSystemTime(new Date('2026-10-10T10:00:00.000Z'));
|
|
770
|
+
h.state.transcriptCb!('user', 'Jump to the pricing slide', true, { turnId: 'u1', caption: true });
|
|
771
|
+
vi.setSystemTime(new Date('2026-10-10T10:00:01.000Z'));
|
|
772
|
+
session.injectMessage({ role: 'assistant', content: 'An earlier answer.' });
|
|
773
|
+
vi.setSystemTime(new Date('2026-10-10T10:00:02.000Z'));
|
|
774
|
+
await h.state.bridge!.runDelegatedTurn(
|
|
775
|
+
req({ delegationId: 'd1', userText: 'Jump to the pricing slide', userUtteranceIds: ['u1'], priorToolResults: [jump] }),
|
|
776
|
+
);
|
|
777
|
+
vi.setSystemTime(new Date('2026-10-10T10:00:03.000Z'));
|
|
778
|
+
await session.sendMessage('and the next one?');
|
|
779
|
+
} finally {
|
|
780
|
+
vi.useRealTimers();
|
|
781
|
+
vi.unstubAllGlobals();
|
|
782
|
+
}
|
|
783
|
+
const roles = (body: { messages: Array<Record<string, unknown>> }) =>
|
|
784
|
+
body.messages.map((m) => (m.toolCalls ? 'calls' : m.toolResults ? 'results' : m.content));
|
|
785
|
+
expect(roles(bodies[0])).toEqual([
|
|
786
|
+
'Welcome! How can I help?',
|
|
787
|
+
'An earlier answer.',
|
|
788
|
+
'Jump to the pricing slide',
|
|
789
|
+
'calls',
|
|
790
|
+
'results',
|
|
791
|
+
]);
|
|
792
|
+
expect(roles(bodies[1])).toEqual([
|
|
793
|
+
'Welcome! How can I help?',
|
|
794
|
+
'Jump to the pricing slide',
|
|
795
|
+
'calls',
|
|
796
|
+
'results',
|
|
797
|
+
'An earlier answer.',
|
|
798
|
+
'and the next one?',
|
|
799
|
+
]);
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
it('replays the executed call to the model as a toolCalls/toolResults pair after the request (proxy mode)', async () => {
|
|
803
|
+
const bodies: Array<{ messages: Array<Record<string, unknown>> }> = [];
|
|
804
|
+
const fetchMock = vi.fn(async (_url: string, init?: RequestInit) => {
|
|
805
|
+
bodies.push(JSON.parse(String(init?.body)));
|
|
806
|
+
return new Response('', { status: 200, headers: { 'Content-Type': 'text/event-stream' } });
|
|
807
|
+
});
|
|
808
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
809
|
+
try {
|
|
810
|
+
delete (session as unknown as { client: { dispatch?: Dispatch } }).client.dispatch;
|
|
811
|
+
h.state.transcriptCb!('user', 'Jump to the pricing slide', true, { turnId: 'u1', caption: true });
|
|
812
|
+
await h.state.bridge!.runDelegatedTurn(
|
|
813
|
+
req({ delegationId: 'd1', userText: 'Jump to the pricing slide', userUtteranceIds: ['u1'], priorToolResults: [jump] }),
|
|
814
|
+
);
|
|
815
|
+
} finally {
|
|
816
|
+
vi.unstubAllGlobals();
|
|
817
|
+
}
|
|
818
|
+
expect(bodies).toHaveLength(1);
|
|
819
|
+
expect(bodies[0].messages.map(({ createdAt: _at, ...m }) => m)).toEqual([
|
|
820
|
+
{ role: 'assistant', content: 'Welcome! How can I help?' },
|
|
821
|
+
{ role: 'user', content: 'Jump to the pricing slide' },
|
|
822
|
+
{
|
|
823
|
+
role: 'assistant',
|
|
824
|
+
content: '',
|
|
825
|
+
toolCalls: [{ toolCallId: 'vtc_1', toolName: 'webmcp_jump_to_slide', args: { position: 5 } }],
|
|
826
|
+
},
|
|
827
|
+
{
|
|
828
|
+
role: 'tool',
|
|
829
|
+
content: '',
|
|
830
|
+
toolResults: [
|
|
831
|
+
{ toolCallId: 'vtc_1', toolName: 'webmcp_jump_to_slide', result: { slidePosition: 5, slideTitle: 'Pricing' } },
|
|
832
|
+
],
|
|
833
|
+
},
|
|
834
|
+
]);
|
|
835
|
+
});
|
|
836
|
+
});
|
|
837
|
+
|
|
669
838
|
it('trims GPT-Live\'s leading space from bubbles and the submitted request', async () => {
|
|
670
839
|
h.state.transcriptCb!('user', ' What are your opening hours', true, { turnId: 'u1', caption: true });
|
|
671
840
|
dispatch.mockImplementation(async (_options, onEvent) => reply(onEvent, 'Nine to five.'));
|
package/src/styles/widget.css
CHANGED
|
@@ -4750,14 +4750,16 @@
|
|
|
4750
4750
|
|
|
4751
4751
|
.persona-approval-resolved {
|
|
4752
4752
|
display: flex;
|
|
4753
|
-
align-items:
|
|
4753
|
+
align-items: flex-start;
|
|
4754
4754
|
gap: 0.45rem;
|
|
4755
4755
|
margin: 0.5rem 0;
|
|
4756
4756
|
padding: 0.3rem 0.15rem;
|
|
4757
4757
|
font-size: 0.8rem;
|
|
4758
|
+
line-height: 1.4;
|
|
4758
4759
|
color: var(--persona-muted, #6b7280);
|
|
4759
4760
|
}
|
|
4760
|
-
.persona-approval-resolved svg { width: 0.95rem; height: 0.95rem; display: block; flex-shrink: 0; }
|
|
4761
|
+
.persona-approval-resolved svg { width: 0.95rem; height: 0.95rem; display: block; flex-shrink: 0; margin-top: 0.085rem; }
|
|
4762
|
+
.persona-approval-resolved-text { min-width: 0; overflow-wrap: anywhere; }
|
|
4761
4763
|
.persona-approval-resolved-name { font-weight: 600; }
|
|
4762
4764
|
|
|
4763
4765
|
.persona-approval-actions {
|
package/src/types.ts
CHANGED
|
@@ -5267,6 +5267,13 @@ export type VoiceDelegationRequest = {
|
|
|
5267
5267
|
userUtteranceIds: string[];
|
|
5268
5268
|
/** The voice model's view of the conversation it delegated from. */
|
|
5269
5269
|
messages: Array<{ role: string; content: unknown }>;
|
|
5270
|
+
/**
|
|
5271
|
+
* `delegation_started.input.priorToolResults` as received: page tools the
|
|
5272
|
+
* server already ran for this turn, each `{ toolCallId, name, arguments,
|
|
5273
|
+
* result }`. Unvalidated. The chat turn records them as completed calls and
|
|
5274
|
+
* never runs them again.
|
|
5275
|
+
*/
|
|
5276
|
+
priorToolResults?: unknown;
|
|
5270
5277
|
/**
|
|
5271
5278
|
* Set when the server streams delegated answers (`delegation_stream`): called
|
|
5272
5279
|
* with each speakable piece of the turn's answer, in order, while it renders.
|
|
@@ -6404,7 +6411,23 @@ export type ClientChatRequest = {
|
|
|
6404
6411
|
* reads aloud: the server asks the agent for a speakable answer (the direct
|
|
6405
6412
|
* answer first, in plain sentences). Not persisted into history.
|
|
6406
6413
|
*/
|
|
6407
|
-
voice?: {
|
|
6414
|
+
voice?: {
|
|
6415
|
+
spoken: true;
|
|
6416
|
+
/**
|
|
6417
|
+
* Page tools the voice server already ran for this hand-back
|
|
6418
|
+
* (`delegation_started.input.priorToolResults`), sent only when it carried
|
|
6419
|
+
* some. The agent narrates their results instead of calling them again.
|
|
6420
|
+
*/
|
|
6421
|
+
priorToolResults?: VoicePriorToolResult[];
|
|
6422
|
+
};
|
|
6423
|
+
};
|
|
6424
|
+
|
|
6425
|
+
/** A page tool call the voice server already ran, as `/v1/client/chat` takes it. */
|
|
6426
|
+
export type VoicePriorToolResult = {
|
|
6427
|
+
toolCallId: string;
|
|
6428
|
+
name: string;
|
|
6429
|
+
arguments: Record<string, unknown>;
|
|
6430
|
+
result: unknown;
|
|
6408
6431
|
};
|
|
6409
6432
|
|
|
6410
6433
|
/**
|
|
@@ -7206,7 +7229,8 @@ export type AgentWidgetPersistStateConfig = {
|
|
|
7206
7229
|
*/
|
|
7207
7230
|
storage?: 'local' | 'session';
|
|
7208
7231
|
/**
|
|
7209
|
-
* Prefix for storage keys.
|
|
7232
|
+
* Prefix for storage keys. Also scopes the default transcript key
|
|
7233
|
+
* (`${keyPrefix}state` in localStorage) when no `storageAdapter` is given.
|
|
7210
7234
|
* @default 'persona-'
|
|
7211
7235
|
*/
|
|
7212
7236
|
keyPrefix?: string;
|
|
@@ -8354,6 +8378,8 @@ export type AgentWidgetReasoning = {
|
|
|
8354
8378
|
export type AgentWidgetToolCall = {
|
|
8355
8379
|
/** Optional approval state for activity-row status chrome. */
|
|
8356
8380
|
approvalStatus?: "pending" | "approved" | "denied" | "timeout";
|
|
8381
|
+
/** Terminal browser work whose callback was lost when the chat restarted. */
|
|
8382
|
+
interrupted?: boolean;
|
|
8357
8383
|
id: string;
|
|
8358
8384
|
name?: string;
|
|
8359
8385
|
status: "pending" | "running" | "complete";
|
|
@@ -8381,6 +8407,8 @@ export type AgentWidgetToolCall = {
|
|
|
8381
8407
|
export type AgentWidgetApproval = {
|
|
8382
8408
|
id: string;
|
|
8383
8409
|
status: "pending" | "approved" | "denied" | "timeout";
|
|
8410
|
+
/** A browser callback lost on restart. The approval expires with status `timeout`. */
|
|
8411
|
+
interrupted?: boolean;
|
|
8384
8412
|
agentId: string;
|
|
8385
8413
|
executionId: string;
|
|
8386
8414
|
toolName: string;
|
|
@@ -8505,6 +8533,13 @@ export type AgentWidgetMessage = {
|
|
|
8505
8533
|
* model as conversation.
|
|
8506
8534
|
*/
|
|
8507
8535
|
voiceCaption?: boolean;
|
|
8536
|
+
/**
|
|
8537
|
+
* Set on widget-generated failure notices (a failed dispatch, a rejected
|
|
8538
|
+
* approval decision, a reconnect that gave up). The bubble renders in this
|
|
8539
|
+
* browser only: it is never sent to the model as conversation history and
|
|
8540
|
+
* never written to the storage adapter.
|
|
8541
|
+
*/
|
|
8542
|
+
localOnly?: boolean;
|
|
8508
8543
|
/**
|
|
8509
8544
|
* Raw structured payload for this message (e.g., JSON action response).
|
|
8510
8545
|
* Populated automatically when structured parsers run.
|