@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
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { createAgentExperience } from "./ui";
|
|
4
|
+
import { provideApprovalUi } from "./approval-ui-loader";
|
|
5
|
+
import * as approvalUiEntry from "./approval-ui-entry";
|
|
6
|
+
import type { AgentWidgetMessage, AgentWidgetStoredState } from "./types";
|
|
7
|
+
import { createDemoHistoryProvider } from "./internal/demo-history-provider";
|
|
8
|
+
|
|
9
|
+
provideApprovalUi(approvalUiEntry);
|
|
10
|
+
let controller: ReturnType<typeof createAgentExperience> | undefined;
|
|
11
|
+
afterEach(() => { controller?.destroy(); document.body.replaceChildren(); localStorage.clear(); vi.restoreAllMocks(); });
|
|
12
|
+
|
|
13
|
+
const snapshot = (): AgentWidgetStoredState => ({
|
|
14
|
+
metadata: { custom: "retained" }, draft: { text: "My unsent proposal" },
|
|
15
|
+
messages: [
|
|
16
|
+
{ id: "tool-1", role: "assistant", content: "", createdAt: "2026-01-01", sequence: 1, streaming: true, variant: "tool", agentMetadata: { executionId: "exec", awaitingLocalTool: true }, toolCall: { id: "call-1", name: "webmcp:purchase", status: "running", startedAt: 1, args: { quantity: 2 }, approvalStatus: "pending" } },
|
|
17
|
+
{ id: "tool-2", role: "assistant", content: "", createdAt: "2026-01-01", sequence: 2, variant: "tool", toolCall: { id: "call-2", name: "webmcp:search", status: "complete", result: "Found two items", durationMs: 2000 } },
|
|
18
|
+
{ id: "approval-webmcp-1", role: "assistant", content: "", createdAt: "2026-01-01", sequence: 3, variant: "approval", approval: { id: "webmcp-1", status: "pending", agentId: "", executionId: "", toolType: "webmcp", toolName: "webmcp:purchase", description: "Buy items", parameters: { quantity: 2 } } },
|
|
19
|
+
] satisfies AgentWidgetMessage[],
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
for (const source of ["sync", "async", "initial", "hook"] as const) {
|
|
23
|
+
describe.each(["card", "row"] as const)(`${source} restoration (%s)`, variant => {
|
|
24
|
+
it("renders historical controls and activity as interrupted while preserving drafts", async () => {
|
|
25
|
+
const stored = snapshot();
|
|
26
|
+
const before = structuredClone(stored);
|
|
27
|
+
const mount = document.createElement("div"); document.body.appendChild(mount);
|
|
28
|
+
const fetchSpy = vi.spyOn(globalThis, "fetch");
|
|
29
|
+
controller = createAgentExperience(mount, {
|
|
30
|
+
apiUrl: "/test", launcher: { enabled: false }, webmcp: { enabled: true },
|
|
31
|
+
...(source === "initial" ? { initialMessages: stored.messages, persistState: false } : source === "hook" ? { onStateLoaded: () => stored, persistState: false } : {
|
|
32
|
+
storageAdapter: { load: () => source === "async" ? Promise.resolve(stored) : stored, save: vi.fn(), clear: vi.fn() },
|
|
33
|
+
}),
|
|
34
|
+
features: { toolCallDisplay: { variant, grouped: true, groupedMode: "collapsible" } },
|
|
35
|
+
toolCall: { completeTextTemplate: "Finished {toolName} ~{duration}~" },
|
|
36
|
+
});
|
|
37
|
+
await vi.waitFor(() => expect(mount.textContent).toContain("chat restarted"));
|
|
38
|
+
expect(mount.querySelector('[data-action="allow"], [data-action="deny"], [data-approval-buttons]')).toBeNull();
|
|
39
|
+
expect(mount.querySelector('[data-tool-elapsed], [data-activity-state="running"], [data-activity-state="awaiting-approval"]')).toBeNull();
|
|
40
|
+
expect(mount.querySelector('[data-message-id="tool-1"]')?.textContent).toContain("Interrupted");
|
|
41
|
+
expect(mount.querySelector('[data-message-id="tool-1"]')?.textContent).not.toContain("Finished");
|
|
42
|
+
if (variant === "row") {
|
|
43
|
+
expect(mount.querySelector('[data-persona-tool-group]')?.getAttribute("data-activity-state")).toBe("interrupted");
|
|
44
|
+
expect(mount.textContent).toContain("2s");
|
|
45
|
+
}
|
|
46
|
+
if (source === "sync" || source === "async") expect(controller.getComposerState().text).toBe("My unsent proposal");
|
|
47
|
+
expect(stored).toEqual(before);
|
|
48
|
+
expect(fetchSpy).not.toHaveBeenCalled();
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
it("keeps a cardless interrupted tool visible when completed rows are removed", async () => {
|
|
54
|
+
const stored = snapshot();
|
|
55
|
+
const mount = document.createElement("div"); document.body.appendChild(mount);
|
|
56
|
+
controller = createAgentExperience(mount, {
|
|
57
|
+
launcher: { enabled: false }, persistState: false,
|
|
58
|
+
initialMessages: [stored.messages![0]],
|
|
59
|
+
features: { toolCallDisplay: { variant: "row", completedVisibility: "removed" } },
|
|
60
|
+
toolCall: { renderCollapsedSummary: ({ createElapsedElement }) => createElapsedElement() },
|
|
61
|
+
});
|
|
62
|
+
expect(mount.querySelector('[data-message-id="tool-1"]')).not.toBeNull();
|
|
63
|
+
expect(mount.querySelector('[data-tool-elapsed]')).toBeNull();
|
|
64
|
+
expect(mount.textContent).toContain("chat restarted");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("supports a custom approval renderer that only handles the original statuses", () => {
|
|
68
|
+
const labels = { pending: "Waiting", approved: "Allowed", denied: "Denied", timeout: "Expired" };
|
|
69
|
+
const mount = document.createElement("div"); document.body.appendChild(mount);
|
|
70
|
+
const render = vi.fn(({ message }: { message: AgentWidgetMessage }) => {
|
|
71
|
+
const node = document.createElement("span");
|
|
72
|
+
node.textContent = message.approval ? labels[message.approval.status] : "";
|
|
73
|
+
return node;
|
|
74
|
+
});
|
|
75
|
+
controller = createAgentExperience(mount, {
|
|
76
|
+
launcher: { enabled: false }, persistState: false,
|
|
77
|
+
initialMessages: [snapshot().messages![2]],
|
|
78
|
+
plugins: [{ id: "existing-approval-renderer", renderApproval: render }],
|
|
79
|
+
});
|
|
80
|
+
expect(mount.textContent).toContain("Expired");
|
|
81
|
+
expect(render).toHaveBeenCalled();
|
|
82
|
+
expect(render.mock.calls[0][0].message.approval).toMatchObject({ status: "timeout", interrupted: true });
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it.each(["card", "row"] as const)("loading earlier messages retires browser activity in %s display", async variant => {
|
|
86
|
+
const stored = snapshot();
|
|
87
|
+
const before = structuredClone(stored);
|
|
88
|
+
const provider = createDemoHistoryProvider();
|
|
89
|
+
const summary = { id: "restored", title: "Restored", targetId: null, preview: "", createdAt: "2026-01-01", updatedAt: "2026-01-02", messageCount: 4 };
|
|
90
|
+
vi.spyOn(provider, "prepareOpen").mockResolvedValue({ conversationId: summary.id, conversationRevision: "r1", commit: vi.fn(), discard: vi.fn() });
|
|
91
|
+
vi.spyOn(provider, "getPage").mockImplementation(async (_id, options) => ({
|
|
92
|
+
summary, conversationRevision: "r1", nextCursor: options?.cursor ? null : "older",
|
|
93
|
+
messages: options?.cursor ? stored.messages! : [{ id: "newer", role: "assistant", content: "Newer reply", createdAt: "2026-01-02" }],
|
|
94
|
+
}));
|
|
95
|
+
const mount = document.createElement("div"); document.body.appendChild(mount);
|
|
96
|
+
const fetchSpy = vi.spyOn(globalThis, "fetch");
|
|
97
|
+
controller = createAgentExperience(mount, {
|
|
98
|
+
apiUrl: "/test", launcher: { enabled: false }, persistState: false,
|
|
99
|
+
features: { history: { enabled: true, provider }, toolCallDisplay: { variant, grouped: true } },
|
|
100
|
+
});
|
|
101
|
+
await controller.openConversation(summary.id);
|
|
102
|
+
const earlier = mount.querySelector<HTMLButtonElement>("[data-persona-history-earlier]");
|
|
103
|
+
expect(earlier).not.toBeNull();
|
|
104
|
+
earlier!.click();
|
|
105
|
+
await vi.waitFor(() => expect(mount.textContent).toContain("chat restarted"));
|
|
106
|
+
expect(mount.querySelector('[data-action="allow"], [data-action="deny"], [data-tool-elapsed], [data-activity-state="running"], [data-activity-state="awaiting-approval"]')).toBeNull();
|
|
107
|
+
expect(mount.textContent).toContain("Interrupted");
|
|
108
|
+
expect(mount.textContent).toContain("Newer reply");
|
|
109
|
+
expect(stored).toEqual(before);
|
|
110
|
+
expect(fetchSpy).not.toHaveBeenCalled();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it.each(["button", "controller"] as const)("restored server WebMCP approval uses its transport via %s", async route => {
|
|
114
|
+
const message = snapshot().messages![2];
|
|
115
|
+
message.approval!.executionId = "durable-exec";
|
|
116
|
+
const onDecision = vi.fn(async () => undefined);
|
|
117
|
+
const mount = document.createElement("div"); document.body.appendChild(mount);
|
|
118
|
+
controller = createAgentExperience(mount, {
|
|
119
|
+
launcher: { enabled: false }, persistState: false, initialMessages: [message], approval: { onDecision },
|
|
120
|
+
});
|
|
121
|
+
expect(onDecision).not.toHaveBeenCalled();
|
|
122
|
+
if (route === "button") mount.querySelector<HTMLButtonElement>('[data-action="allow"]')!.click();
|
|
123
|
+
else await controller.resolveApproval(message.approval!.id, "approved");
|
|
124
|
+
await vi.waitFor(() => expect(onDecision).toHaveBeenCalledTimes(1));
|
|
125
|
+
expect(onDecision.mock.calls[0]).toEqual([{
|
|
126
|
+
approvalId: "webmcp-1", executionId: "durable-exec", agentId: "", toolName: "webmcp:purchase",
|
|
127
|
+
}, "approved", undefined]);
|
|
128
|
+
});
|
package/src/ui.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isBrowserLocalApproval } from "./utils/restored-browser-state";
|
|
1
2
|
import { activityVariant, activityDuration, createActivityLifecycle, createActivityGroup } from "./components/activity-row";
|
|
2
3
|
import { applyStatusIndicatorState, placeStatusIndicator } from "./utils/status-indicator";
|
|
3
4
|
import { usesSessionVoice } from "./utils/voice-support";
|
|
@@ -331,7 +332,8 @@ import {
|
|
|
331
332
|
defaultActionHandlers,
|
|
332
333
|
defaultJsonActionParser
|
|
333
334
|
} from "./utils/actions";
|
|
334
|
-
import { createLocalStorageAdapter } from "./utils/storage";
|
|
335
|
+
import { createLocalStorageAdapter, transcriptStorageKey } from "./utils/storage";
|
|
336
|
+
import { isLocalOnlyMessage } from "./utils/local-only-message";
|
|
335
337
|
import { createVisitorStore, type VisitorStore } from "./utils/visitor-store";
|
|
336
338
|
import { loadHistoryView } from "./history-view-loader";
|
|
337
339
|
import { getHistoryShellSync, loadHistoryShell, type HistoryShellModule } from "./history-shell-loader";
|
|
@@ -501,11 +503,13 @@ const ensureRecord = (value: unknown): Record<string, unknown> => {
|
|
|
501
503
|
return { ...(value as Record<string, unknown>) };
|
|
502
504
|
};
|
|
503
505
|
|
|
504
|
-
const
|
|
505
|
-
messages
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
506
|
+
const messagesForStorage = (messages: AgentWidgetMessage[]) =>
|
|
507
|
+
messages
|
|
508
|
+
.filter((message) => !isLocalOnlyMessage(message))
|
|
509
|
+
.map((message) => ({
|
|
510
|
+
...message,
|
|
511
|
+
streaming: false
|
|
512
|
+
}));
|
|
509
513
|
|
|
510
514
|
type Controller = {
|
|
511
515
|
update: (config: AgentWidgetConfigPatch) => void;
|
|
@@ -1064,7 +1068,12 @@ export const createAgentExperience = (
|
|
|
1064
1068
|
const storageAdapter: AgentWidgetStorageAdapter | null =
|
|
1065
1069
|
messagePersistenceDisabled
|
|
1066
1070
|
? null
|
|
1067
|
-
: (config.storageAdapter ??
|
|
1071
|
+
: (config.storageAdapter ??
|
|
1072
|
+
createLocalStorageAdapter(
|
|
1073
|
+
transcriptStorageKey(
|
|
1074
|
+
typeof config.persistState === "object" ? config.persistState.keyPrefix : undefined
|
|
1075
|
+
)
|
|
1076
|
+
));
|
|
1068
1077
|
const personaOwnsClientTokenRecovery =
|
|
1069
1078
|
Boolean(config.clientToken) && typeof config.reconnectStream !== "function";
|
|
1070
1079
|
let persistentMetadata: Record<string, unknown> = {};
|
|
@@ -3055,8 +3064,8 @@ export const createAgentExperience = (
|
|
|
3055
3064
|
|
|
3056
3065
|
// WebMCP gate approvals resolve a local Promise the bridge is parked on
|
|
3057
3066
|
// (no server round-trip); server-driven approvals call the API. The
|
|
3058
|
-
// `toolType` marker
|
|
3059
|
-
if (approvalMessage.approval
|
|
3067
|
+
// `toolType` marker and absent execution id identify browser-local gates.
|
|
3068
|
+
if (isBrowserLocalApproval(approvalMessage.approval)) {
|
|
3060
3069
|
session.resolveWebMcpApproval(messageId, decision);
|
|
3061
3070
|
} else {
|
|
3062
3071
|
session.resolveApproval(approvalMessage.approval, decision);
|
|
@@ -5159,7 +5168,7 @@ export const createAgentExperience = (
|
|
|
5159
5168
|
|
|
5160
5169
|
const getMessagesForPersistence = () =>
|
|
5161
5170
|
session
|
|
5162
|
-
?
|
|
5171
|
+
? messagesForStorage(session.getMessages()).filter(msg => !(msg as any).__skipPersist)
|
|
5163
5172
|
: [];
|
|
5164
5173
|
|
|
5165
5174
|
let storageMutationTail: Promise<void> | null = null;
|
|
@@ -5209,7 +5218,7 @@ export const createAgentExperience = (
|
|
|
5209
5218
|
|
|
5210
5219
|
// Allow saving even if session doesn't exist yet (for metadata during init)
|
|
5211
5220
|
const messages = messagesOverride
|
|
5212
|
-
?
|
|
5221
|
+
? messagesForStorage(messagesOverride)
|
|
5213
5222
|
: session
|
|
5214
5223
|
? getMessagesForPersistence()
|
|
5215
5224
|
: [];
|
|
@@ -6239,7 +6248,8 @@ export const createAgentExperience = (
|
|
|
6239
6248
|
return (
|
|
6240
6249
|
dropCompletedToolCalls &&
|
|
6241
6250
|
message.variant === "tool" &&
|
|
6242
|
-
message.toolCall?.status === "complete"
|
|
6251
|
+
message.toolCall?.status === "complete" &&
|
|
6252
|
+
!message.toolCall.interrupted
|
|
6243
6253
|
);
|
|
6244
6254
|
};
|
|
6245
6255
|
|
|
@@ -6507,7 +6517,7 @@ export const createAgentExperience = (
|
|
|
6507
6517
|
?.getMessages()
|
|
6508
6518
|
.find((m) => m.id === approvalMessageId);
|
|
6509
6519
|
if (!live?.approval) return;
|
|
6510
|
-
if (live.approval
|
|
6520
|
+
if (isBrowserLocalApproval(live.approval)) {
|
|
6511
6521
|
sessionRef.current?.resolveWebMcpApproval(live.id, decision);
|
|
6512
6522
|
} else {
|
|
6513
6523
|
sessionRef.current?.resolveApproval(live.approval, decision, options);
|
|
@@ -6801,7 +6811,7 @@ export const createAgentExperience = (
|
|
|
6801
6811
|
// A tool call hidden by `completedVisibility: "removed"` does not
|
|
6802
6812
|
// create a visible break in the transcript, so it should not split
|
|
6803
6813
|
// an otherwise contiguous group.
|
|
6804
|
-
if (dropCompletedToolCalls && message.toolCall.status === "complete") {
|
|
6814
|
+
if (dropCompletedToolCalls && message.toolCall.status === "complete" && !message.toolCall.interrupted) {
|
|
6805
6815
|
return;
|
|
6806
6816
|
}
|
|
6807
6817
|
currentGroup.push(message);
|
|
@@ -6859,10 +6869,12 @@ export const createAgentExperience = (
|
|
|
6859
6869
|
id, name: `${group.length} tools`, status: active ? "running" : "complete",
|
|
6860
6870
|
chunks: group.flatMap(item => item.toolCall?.chunks ?? []),
|
|
6861
6871
|
success: !group.some(item => item.toolCall?.success === false),
|
|
6872
|
+
interrupted: !active && group.some(item => item.toolCall?.interrupted),
|
|
6862
6873
|
} };
|
|
6863
6874
|
activityIds.add(id);
|
|
6864
6875
|
activityLifecycle.observe(synthetic, "tool", toolExpansionState, config.features.toolCallDisplay);
|
|
6865
|
-
const label =
|
|
6876
|
+
const label = synthetic.toolCall?.interrupted ? `${group.length} tools · Interrupted`
|
|
6877
|
+
: `${active ? "Using" : "Used"} ${group.length} tools`;
|
|
6866
6878
|
const custom = config.toolCall?.renderGroupedSummary?.({ messages: group, toolCalls: group.map(item => item.toolCall!), defaultSummary: label, config });
|
|
6867
6879
|
const row = createActivityGroup(synthetic, config, toolExpansionState.has(id), custom ?? label);
|
|
6868
6880
|
row.bubble.classList.add("persona-tool-group");
|
|
@@ -6888,7 +6900,9 @@ export const createAgentExperience = (
|
|
|
6888
6900
|
"persona-tool-group-summary persona-text-xs persona-text-persona-muted";
|
|
6889
6901
|
summary.id = `tool-group-summary-${group[0].id}`;
|
|
6890
6902
|
|
|
6891
|
-
const
|
|
6903
|
+
const toolCount = `${group.length} ${group.length === 1 ? "tool" : "tools"}`;
|
|
6904
|
+
const defaultSummary = group.some(item => item.toolCall?.interrupted)
|
|
6905
|
+
? `${toolCount} · Interrupted` : `Called ${toolCount}`;
|
|
6892
6906
|
const renderedSummary = config.toolCall?.renderGroupedSummary?.({
|
|
6893
6907
|
messages: group,
|
|
6894
6908
|
toolCalls: group
|
|
@@ -13277,8 +13291,8 @@ export const createAgentExperience = (
|
|
|
13277
13291
|
// Mirror the in-panel click handler: WebMCP gate bubbles resolve a local
|
|
13278
13292
|
// Promise the bridge is parked on (no server round-trip and they carry an
|
|
13279
13293
|
// empty executionId/agentId), so they must NOT hit the server approval
|
|
13280
|
-
// API.
|
|
13281
|
-
if (approvalMessage.approval
|
|
13294
|
+
// API. Server-backed approvals retain their execution id and use the API.
|
|
13295
|
+
if (isBrowserLocalApproval(approvalMessage.approval)) {
|
|
13282
13296
|
session.resolveWebMcpApproval(approvalMessage.id, decision);
|
|
13283
13297
|
return;
|
|
13284
13298
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { createJsonStreamParser, parseFormattedTemplate, computeReasoningElapsed } from "./formatting";
|
|
2
|
+
import { createJsonStreamParser, parseFormattedTemplate, computeReasoningElapsed, computeToolElapsed } from "./formatting";
|
|
3
3
|
|
|
4
4
|
describe("JSON Stream Parser", () => {
|
|
5
5
|
it("should extract text field incrementally as JSON streams in", () => {
|
|
@@ -268,3 +268,11 @@ describe("computeReasoningElapsed", () => {
|
|
|
268
268
|
expect(result).toBe("<0.1s");
|
|
269
269
|
});
|
|
270
270
|
});
|
|
271
|
+
|
|
272
|
+
it("never invents an interrupted tool's duration, including elapsed render hooks", () => {
|
|
273
|
+
const tool = { id: "browser", status: "complete" as const, interrupted: true };
|
|
274
|
+
expect(computeToolElapsed(tool)).toBe("");
|
|
275
|
+
expect(computeToolElapsed({ ...tool, startedAt: 1 })).toBe("");
|
|
276
|
+
expect(computeToolElapsed({ ...tool, durationMs: 2000 })).toBe("2s");
|
|
277
|
+
expect(computeToolElapsed({ ...tool, startedAt: 1000, completedAt: 4000 })).toBe("3s");
|
|
278
|
+
});
|
package/src/utils/formatting.ts
CHANGED
|
@@ -124,6 +124,10 @@ export const formatElapsedMs = (ms: number): string => {
|
|
|
124
124
|
* Computes the current elapsed time string for a tool call.
|
|
125
125
|
*/
|
|
126
126
|
export const computeToolElapsed = (tool: AgentWidgetToolCall): string => {
|
|
127
|
+
if (
|
|
128
|
+
tool.interrupted && tool.duration === undefined && tool.durationMs === undefined &&
|
|
129
|
+
(tool.startedAt === undefined || tool.completedAt === undefined)
|
|
130
|
+
) return "";
|
|
127
131
|
const durationMs =
|
|
128
132
|
typeof tool.duration === "number"
|
|
129
133
|
? tool.duration
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { AgentWidgetMessage } from "../types";
|
|
2
|
+
|
|
3
|
+
/** Id prefix of the bubble the durable reconnect loop posts when it gives up. */
|
|
4
|
+
export const RECONNECT_FAILED_ID_PREFIX = "reconnect-failed-";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A widget-generated notice that is shown in this browser only: never sent as
|
|
8
|
+
* conversation history and never written to the storage adapter. The id check
|
|
9
|
+
* covers reconnect-failure bubbles persisted before `localOnly` existed.
|
|
10
|
+
*/
|
|
11
|
+
export const isLocalOnlyMessage = (message: AgentWidgetMessage): boolean =>
|
|
12
|
+
message.localOnly === true ||
|
|
13
|
+
(message.role === "assistant" &&
|
|
14
|
+
typeof message.id === "string" &&
|
|
15
|
+
message.id.startsWith(RECONNECT_FAILED_ID_PREFIX));
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { AgentWidgetApproval, AgentWidgetMessage, AgentWidgetToolCall } from "../types";
|
|
2
|
+
import { isWebMcpToolName } from "../webmcp-bridge";
|
|
3
|
+
|
|
4
|
+
export const BROWSER_TOOL_INTERRUPTION =
|
|
5
|
+
"The chat restarted and this browser tool was interrupted. Request a fresh proposal to try again.";
|
|
6
|
+
|
|
7
|
+
export function isBrowserLocalApproval(approval: AgentWidgetApproval): boolean {
|
|
8
|
+
return approval.toolType === "webmcp" && !approval.executionId;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** History cannot restore a browser callback. Server continuations stay live. */
|
|
12
|
+
export function restoreBrowserToolState(message: AgentWidgetMessage): AgentWidgetMessage {
|
|
13
|
+
const approval = message.approval;
|
|
14
|
+
// Server approvals can also name webmcp tools. An execution id identifies
|
|
15
|
+
// their durable continuation; native browser gates have no execution id.
|
|
16
|
+
const interruptedApproval = approval?.status === "pending" && isBrowserLocalApproval(approval);
|
|
17
|
+
let hasBrowserTool = false;
|
|
18
|
+
const settle = (tool: AgentWidgetToolCall): AgentWidgetToolCall => {
|
|
19
|
+
if (!tool.name || !isWebMcpToolName(tool.name)) return tool;
|
|
20
|
+
hasBrowserTool = true;
|
|
21
|
+
// Older await frames used complete for the wire step before the browser
|
|
22
|
+
// had executed anything. A recorded outcome, however, remains historical.
|
|
23
|
+
const unfinished = tool.status !== "complete" ||
|
|
24
|
+
(message.agentMetadata?.awaitingLocalTool === true &&
|
|
25
|
+
tool.result === undefined && tool.success === undefined);
|
|
26
|
+
if (!unfinished) return tool;
|
|
27
|
+
return {
|
|
28
|
+
...tool,
|
|
29
|
+
status: "complete",
|
|
30
|
+
interrupted: true,
|
|
31
|
+
success: false,
|
|
32
|
+
error: BROWSER_TOOL_INTERRUPTION,
|
|
33
|
+
approvalStatus: tool.approvalStatus === "pending" ? "timeout" : tool.approvalStatus,
|
|
34
|
+
// Retain recorded durations and paired timestamps. Never fabricate an
|
|
35
|
+
// end timestamp or derive a duration from the time spent off the page.
|
|
36
|
+
startedAt: tool.completedAt === undefined ? undefined : tool.startedAt,
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
const toolCall = message.toolCall ? settle(message.toolCall) : undefined;
|
|
40
|
+
const tools = message.tools?.map(settle);
|
|
41
|
+
return {
|
|
42
|
+
...message,
|
|
43
|
+
streaming: hasBrowserTool || interruptedApproval ? false : message.streaming,
|
|
44
|
+
...(interruptedApproval ? { approval: { ...approval, status: "timeout" as const, interrupted: true } } : {}),
|
|
45
|
+
...(toolCall ? { toolCall } : {}),
|
|
46
|
+
...(tools ? { tools } : {}),
|
|
47
|
+
...(hasBrowserTool && message.agentMetadata?.awaitingLocalTool
|
|
48
|
+
? { agentMetadata: { ...message.agentMetadata, awaitingLocalTool: false } } : {}),
|
|
49
|
+
};
|
|
50
|
+
}
|
package/src/utils/storage.ts
CHANGED
|
@@ -30,8 +30,19 @@ const sanitizeArtifacts = (artifacts: PersonaArtifactRecord[]) =>
|
|
|
30
30
|
status: "complete" as const
|
|
31
31
|
}));
|
|
32
32
|
|
|
33
|
+
const DEFAULT_PERSIST_KEY_PREFIX = "persona-";
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The widget's default transcript key, scoped by `persistState.keyPrefix` so
|
|
37
|
+
* two widgets on one origin with distinct prefixes keep separate transcripts.
|
|
38
|
+
* The default prefix yields the legacy `"persona-state"` key, so unprefixed
|
|
39
|
+
* widgets keep their stored conversation; prefixed widgets never adopt it.
|
|
40
|
+
*/
|
|
41
|
+
export const transcriptStorageKey = (keyPrefix?: string): string =>
|
|
42
|
+
`${keyPrefix ?? DEFAULT_PERSIST_KEY_PREFIX}state`;
|
|
43
|
+
|
|
33
44
|
export const createLocalStorageAdapter = (
|
|
34
|
-
key =
|
|
45
|
+
key = transcriptStorageKey()
|
|
35
46
|
): AgentWidgetStorageAdapter => {
|
|
36
47
|
const getStorage = () => {
|
|
37
48
|
if (typeof window === "undefined" || !window.localStorage) {
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { AgentWidgetMessage } from "../types";
|
|
2
|
+
|
|
3
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
4
|
+
typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Tool rows for the page tools a voice server already ran before handing the
|
|
8
|
+
* turn to the chat (`delegation_started.input.priorToolResults`). Each row is
|
|
9
|
+
* a completed call carrying `clientToolAnswer`, so proxy/agent payloads replay
|
|
10
|
+
* it to the model as an assistant `toolCalls` + `tool` `toolResults` pair under
|
|
11
|
+
* the same `toolCallId`. Nothing marks it awaiting, so it never runs again.
|
|
12
|
+
* Malformed entries and repeats of a `toolCallId` are skipped.
|
|
13
|
+
*/
|
|
14
|
+
export const voicePriorToolRows = (value: unknown, createdAt: string): AgentWidgetMessage[] => {
|
|
15
|
+
const seen = new Set<string>();
|
|
16
|
+
return (Array.isArray(value) ? value : []).flatMap((entry): AgentWidgetMessage[] => {
|
|
17
|
+
if (!isRecord(entry)) return [];
|
|
18
|
+
const { toolCallId, name } = entry;
|
|
19
|
+
if (typeof toolCallId !== "string" || !toolCallId || typeof name !== "string" || !name) return [];
|
|
20
|
+
if (seen.has(toolCallId)) return [];
|
|
21
|
+
seen.add(toolCallId);
|
|
22
|
+
const args = isRecord(entry.arguments) ? entry.arguments : {};
|
|
23
|
+
const result = entry.result ?? null;
|
|
24
|
+
return [
|
|
25
|
+
{
|
|
26
|
+
id: `voice-tool-${toolCallId}`,
|
|
27
|
+
role: "assistant",
|
|
28
|
+
variant: "tool",
|
|
29
|
+
content: "",
|
|
30
|
+
createdAt,
|
|
31
|
+
streaming: false,
|
|
32
|
+
toolCall: { id: toolCallId, name, status: "complete", args, result },
|
|
33
|
+
agentMetadata: { clientToolAnswer: { toolCallId, toolName: name, args, result } },
|
|
34
|
+
},
|
|
35
|
+
];
|
|
36
|
+
});
|
|
37
|
+
};
|
|
@@ -99,7 +99,10 @@
|
|
|
99
99
|
// sent), `failed`, or `timeout` (past `timeoutMs`, which bounds the gate too).
|
|
100
100
|
// Then `delegation_completed{route:'direct', text}` for a delegation that never
|
|
101
101
|
// started here writes the heard user turn and `text` into the chat. When the
|
|
102
|
-
// server falls through, the delegation arrives as today's `delegation_started
|
|
102
|
+
// server falls through, the delegation arrives as today's `delegation_started`;
|
|
103
|
+
// its `input.priorToolResults` (page tools the server already ran; sent only
|
|
104
|
+
// with `prior_tool_results`, declared beside `client_tools`) ride along to the
|
|
105
|
+
// chat turn as completed calls, never run again here.
|
|
103
106
|
|
|
104
107
|
import type {
|
|
105
108
|
VoiceClientToolsSession,
|
|
@@ -519,17 +522,17 @@ export class RuntypeVoiceProvider implements VoiceProvider {
|
|
|
519
522
|
* the URL, and these params are not secrets.
|
|
520
523
|
*/
|
|
521
524
|
#voiceSocketUrl(host: string, agentId: string, attach?: URLSearchParams): string {
|
|
522
|
-
|
|
525
|
+
let capabilities = "partial_transcript,context";
|
|
523
526
|
if (this.bridge && this.config?.clientDelegation !== false) {
|
|
524
|
-
capabilities
|
|
525
|
-
if (this.bridge.clientTools?.()) capabilities
|
|
527
|
+
capabilities += ",client_delegation,delegation_update,delegation_read_back,delegation_stream";
|
|
528
|
+
if (this.bridge.clientTools?.()) capabilities += ",client_tools,prior_tool_results";
|
|
526
529
|
}
|
|
527
|
-
if (attach) capabilities
|
|
530
|
+
if (attach) capabilities = `attach,${capabilities}`;
|
|
528
531
|
const params = new URLSearchParams({
|
|
529
532
|
voiceProtocol: VOICE_PROTOCOL,
|
|
530
533
|
clientVersion: `persona/${VERSION}`,
|
|
531
534
|
voiceCapabilities: FULL_DUPLEX_CAPABILITY,
|
|
532
|
-
clientCapabilities: capabilities
|
|
535
|
+
clientCapabilities: capabilities,
|
|
533
536
|
});
|
|
534
537
|
attach?.forEach((value, key) => params.set(key, value));
|
|
535
538
|
return `${toWsBase(host)}/ws/agents/${encodeURIComponent(agentId)}/voice?${params}`;
|
|
@@ -877,9 +880,10 @@ export class RuntypeVoiceProvider implements VoiceProvider {
|
|
|
877
880
|
this.#pendingContext = this.#buildContextText();
|
|
878
881
|
}
|
|
879
882
|
if (this.#clientDelegation && this.#capabilities.has("client_tools") && !this.#fastTier) {
|
|
883
|
+
// INVARIANT: #clientDelegation implies a bridge.
|
|
880
884
|
this.#fastTier =
|
|
881
|
-
this.bridge
|
|
882
|
-
|
|
885
|
+
this.bridge!
|
|
886
|
+
.clientTools?.()
|
|
883
887
|
?.open({
|
|
884
888
|
send: (frame) => {
|
|
885
889
|
const ws = this.#ws;
|
|
@@ -974,6 +978,7 @@ export class RuntypeVoiceProvider implements VoiceProvider {
|
|
|
974
978
|
? input.userUtteranceIds.filter((id: unknown): id is string => typeof id === "string")
|
|
975
979
|
: [],
|
|
976
980
|
messages: Array.isArray(input.messages) ? input.messages : [],
|
|
981
|
+
priorToolResults: input.priorToolResults,
|
|
977
982
|
},
|
|
978
983
|
generation,
|
|
979
984
|
);
|
|
@@ -81,8 +81,7 @@ export const wireSessionVoice = (
|
|
|
81
81
|
busy: () => p.busy(),
|
|
82
82
|
parked: () => s.webMcpApprovalResolvers.size > 0,
|
|
83
83
|
claim: (text, userUtteranceIds) => p.keyed?.claimUserTurn(text, userUtteranceIds) ?? null,
|
|
84
|
-
send: (text,
|
|
85
|
-
s.sendMessage(text, { viaVoice: true, voiceTurn: { userMessageId } }),
|
|
84
|
+
send: (text, voiceTurn) => s.sendMessage(text, { viaVoice: true, voiceTurn }),
|
|
86
85
|
track: (capture) => {
|
|
87
86
|
p.setDelegation(capture);
|
|
88
87
|
},
|
|
@@ -45,8 +45,11 @@ export interface VoiceDelegationHost {
|
|
|
45
45
|
parked(): boolean;
|
|
46
46
|
/** The transcript bubble the request came from (see KeyedVoiceTranscript.claimUserTurn). */
|
|
47
47
|
claim(userText: string, userUtteranceIds: string[]): string | null;
|
|
48
|
-
/**
|
|
49
|
-
|
|
48
|
+
/**
|
|
49
|
+
* sendMessage as a voice turn, submitting `userMessageId`'s bubble when
|
|
50
|
+
* given and recording `priorToolResults` as already-run tool calls.
|
|
51
|
+
*/
|
|
52
|
+
send(userText: string, voiceTurn: { userMessageId?: string | null; priorToolResults?: unknown }): Promise<void>;
|
|
50
53
|
/** Route the chat stream's assistant messages and failures into `capture` (or stop). */
|
|
51
54
|
track(capture: VoiceDelegationCapture | null): void;
|
|
52
55
|
/** Deny a pending approval (server gate or WebMCP) by its message id. */
|
|
@@ -240,6 +243,7 @@ export function createVoiceSessionBridge(host: VoiceDelegationHost): VoiceSessio
|
|
|
240
243
|
userText,
|
|
241
244
|
userUtteranceIds,
|
|
242
245
|
stream,
|
|
246
|
+
priorToolResults,
|
|
243
247
|
}: VoiceDelegationRequest): Promise<VoiceDelegationResult> {
|
|
244
248
|
const live = parks.filter((park) => pendingOf(park).length);
|
|
245
249
|
const pendingIds = live.flatMap(pendingOf);
|
|
@@ -306,7 +310,10 @@ export function createVoiceSessionBridge(host: VoiceDelegationHost): VoiceSessio
|
|
|
306
310
|
active = capture;
|
|
307
311
|
retrack();
|
|
308
312
|
try {
|
|
309
|
-
await host.send(userText,
|
|
313
|
+
await host.send(userText, {
|
|
314
|
+
userMessageId: host.claim(userText, userUtteranceIds),
|
|
315
|
+
priorToolResults,
|
|
316
|
+
});
|
|
310
317
|
// Local tools and approvals may continue the turn past the first stream.
|
|
311
318
|
await settled(capture);
|
|
312
319
|
} catch {
|
package/src/voice/voice.test.ts
CHANGED
|
@@ -1762,7 +1762,7 @@ describe('RuntypeVoiceProvider (realtime streaming)', () => {
|
|
|
1762
1762
|
);
|
|
1763
1763
|
const results = (ws: MockWebSocket) => sentJson(ws).filter((f) => f.type === 'client_tool_result');
|
|
1764
1764
|
|
|
1765
|
-
it('declares client_tools only when the bridge has WebMCP tools', async () => {
|
|
1765
|
+
it('declares client_tools and prior_tool_results only when the bridge has WebMCP tools', async () => {
|
|
1766
1766
|
const { ws } = await startFastCall();
|
|
1767
1767
|
expect(ws.url).toBe(
|
|
1768
1768
|
voiceUrl('wss://api.example.com', 'a1', [
|
|
@@ -1772,12 +1772,15 @@ describe('RuntypeVoiceProvider (realtime streaming)', () => {
|
|
|
1772
1772
|
'delegation_read_back',
|
|
1773
1773
|
'delegation_stream',
|
|
1774
1774
|
'client_tools',
|
|
1775
|
+
'prior_tool_results',
|
|
1775
1776
|
]),
|
|
1776
1777
|
);
|
|
1777
1778
|
const provider = new RuntypeVoiceProvider(baseConfig());
|
|
1778
1779
|
provider.setSessionBridge({ ...makeBridge().bridge, clientTools: () => null });
|
|
1779
1780
|
await provider.startListening();
|
|
1780
|
-
|
|
1781
|
+
const declared = new URL(lastWs().url).searchParams.get('clientCapabilities');
|
|
1782
|
+
expect(declared).not.toContain('client_tools');
|
|
1783
|
+
expect(declared).not.toContain('prior_tool_results');
|
|
1781
1784
|
});
|
|
1782
1785
|
|
|
1783
1786
|
it('sends client_tools after session_config with full webmcp: names and the dispatch fields', async () => {
|
|
@@ -2002,6 +2005,39 @@ describe('RuntypeVoiceProvider (realtime streaming)', () => {
|
|
|
2002
2005
|
]);
|
|
2003
2006
|
});
|
|
2004
2007
|
|
|
2008
|
+
it('hands a fall-through its priorToolResults without running them again on the page', async () => {
|
|
2009
|
+
const { ws, calls, executed, recorded, pending } = await startFastCall([...FAST_CAPS, 'prior_tool_results']);
|
|
2010
|
+
const prior = [
|
|
2011
|
+
{ toolCallId: 'vtc_01', name: 'webmcp:jump_to_slide', arguments: { position: 5 }, result: { slideTitle: 'Pricing' } },
|
|
2012
|
+
];
|
|
2013
|
+
heard(ws);
|
|
2014
|
+
toolCall(ws);
|
|
2015
|
+
await flush();
|
|
2016
|
+
expect(executed).toHaveLength(1);
|
|
2017
|
+
ws.triggerMessage(
|
|
2018
|
+
JSON.stringify({
|
|
2019
|
+
type: 'delegation_started',
|
|
2020
|
+
delegationId: 'd1',
|
|
2021
|
+
input: { text: 'Jump to the pricing slide', userUtteranceIds: ['u1'], messages: [], priorToolResults: prior },
|
|
2022
|
+
}),
|
|
2023
|
+
);
|
|
2024
|
+
await flush();
|
|
2025
|
+
expect(calls).toHaveLength(1);
|
|
2026
|
+
expect(calls[0].priorToolResults).toEqual(prior);
|
|
2027
|
+
pending[0]({ status: 'completed', text: 'You are on Pricing.' });
|
|
2028
|
+
await flush();
|
|
2029
|
+
directCompleted(ws);
|
|
2030
|
+
expect(executed).toHaveLength(1);
|
|
2031
|
+
expect(recorded).toEqual([]);
|
|
2032
|
+
});
|
|
2033
|
+
|
|
2034
|
+
it('leaves priorToolResults unset on a fall-through without them', async () => {
|
|
2035
|
+
const { ws, calls } = await startFastCall();
|
|
2036
|
+
started(ws, 'd1');
|
|
2037
|
+
await flush();
|
|
2038
|
+
expect(calls[0].priorToolResults).toBeUndefined();
|
|
2039
|
+
});
|
|
2040
|
+
|
|
2005
2041
|
it('ignores a direct completion when client_tools was not negotiated', async () => {
|
|
2006
2042
|
const { ws, recorded } = await startFastCall(ALL_CAPS);
|
|
2007
2043
|
heard(ws);
|
package/dist/chunk-B6zZsT6F.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
const e=[1e3,2e3,4e3,8e3,8e3];function t(t){let n=0,r=null,i=null,a=!1,o=()=>{if(i&&=(clearTimeout(i),null),r){let e=r;r=null,e()}},s=()=>{let e=t.getStatus();(e===`resuming`||e===`paused`)&&o()},c=()=>{(typeof document>`u`||document.visibilityState!==`hidden`)&&s()},l=()=>{s()},u=()=>{a||=(typeof document<`u`&&document.addEventListener(`visibilitychange`,c),typeof window<`u`&&window.addEventListener(`online`,l),!0)},d=()=>{a&&=(typeof document<`u`&&document.removeEventListener(`visibilitychange`,c),typeof window<`u`&&window.removeEventListener(`online`,l),!1)},f=e=>new Promise(t=>{r=t,i=setTimeout(()=>{i=null,r=null,t()},e)}),p=()=>{let e=t.getResumable();t.clearResumable(),t.setReconnecting(!1),n=0,d(),t.setAbortController(null);let r=!1;for(let e of t.getMessages())e.streaming&&(e.streaming=!1,r=!0);let i=t.buildErrorContent(`Connection lost and the response could not be resumed.`);i?t.appendMessage({id:`reconnect-failed-${e?.executionId??t.nextSequence()}`,role:`assistant`,content:i,createdAt:(/* @__PURE__ */ new Date()).toISOString(),streaming:!1,sequence:t.nextSequence()}):r&&t.notifyMessagesChanged(),t.setStreaming(!1),t.setStatus(`idle`),t.onError(/* @__PURE__ */ Error(`Durable session reconnect failed.`))},m=async()=>{let r=t.config.reconnect?.backoffMs??e,i=t.config.reconnect?.maxAttempts??r.length,a=t.config.reconnectStream;if(!a)t.setReconnecting(!1);else{for(;t.getResumable()&&n<i;){n+=1,t.setStatus(`resuming`);let e=t.getResumable(),o=e.lastEventId,s=new AbortController;t.setAbortController(s),t.emitReconnect({phase:`resuming`,handle:e,attempt:n});let c=null;try{let t=await a({executionId:e.executionId,after:e.lastEventId,signal:s.signal});t&&t.ok&&t.body&&(c=t.body)}catch{c=null}if(s.signal.aborted)return;if(c){let r=t.getMessages().find(t=>t.id===e.assistantMessageId)?.content,i=typeof r==`string`?r:``;try{await t.resumeConnect(c,e.assistantMessageId,i)}catch{}if(s.signal.aborted)return;if(!t.getResumable()){t.setReconnecting(!1),n=0,d(),t.emitReconnect({phase:`resumed`,handle:e});return}t.getResumable().lastEventId!==o&&(n=0)}if(t.getResumable()&&n<i&&(t.setStatus(`paused`),await f(r[Math.min(n-1,r.length-1)]??1e3),s.signal.aborted))return}t.getResumable()&&p()}};return{begin(){n=0,u(),m()},teardown(){i&&=(clearTimeout(i),null),r=null,n=0,d()},wake:o}}export{t as createReconnectController};
|
package/dist/chunk-CIVsNcrk.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
const e=`4.29.0`;export{e as t};
|