@runtypelabs/persona 3.31.1 → 3.34.0
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/README.md +17 -10
- package/dist/animations/glyph-cycle.d.cts +1 -1
- package/dist/animations/glyph-cycle.d.ts +1 -1
- package/dist/animations/{types-quh7NmYD.d.cts → types-CthJFfNx.d.cts} +7 -0
- package/dist/animations/{types-quh7NmYD.d.ts → types-CthJFfNx.d.ts} +7 -0
- package/dist/animations/wipe.d.cts +1 -1
- package/dist/animations/wipe.d.ts +1 -1
- package/dist/codegen.cjs +1 -1
- package/dist/codegen.js +1 -1
- package/dist/index.cjs +43 -43
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +320 -8
- package/dist/index.d.ts +320 -8
- package/dist/index.global.js +193 -192
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +43 -43
- package/dist/index.js.map +1 -1
- package/dist/launcher.global.js +117 -117
- package/dist/launcher.global.js.map +1 -1
- package/dist/smart-dom-reader.d.cts +110 -2
- package/dist/smart-dom-reader.d.ts +110 -2
- package/dist/theme-editor.cjs +30 -30
- package/dist/theme-editor.d.cts +124 -5
- package/dist/theme-editor.d.ts +124 -5
- package/dist/theme-editor.js +30 -30
- package/package.json +3 -3
- package/src/ask-user-question-tool.test.ts +148 -0
- package/src/ask-user-question-tool.ts +138 -0
- package/src/client.ts +43 -9
- package/src/codegen.test.ts +0 -14
- package/src/components/messages.ts +10 -0
- package/src/components/suggestions.ts +49 -6
- package/src/index-core.ts +15 -0
- package/src/runtime/host-layout.test.ts +206 -9
- package/src/runtime/host-layout.ts +121 -8
- package/src/runtime/init.test.ts +2 -2
- package/src/session.ts +188 -32
- package/src/session.webmcp.test.ts +48 -0
- package/src/suggest-replies-tool.test.ts +445 -0
- package/src/suggest-replies-tool.ts +152 -0
- package/src/theme-editor/index.ts +2 -0
- package/src/theme-editor/webmcp/index.ts +2 -0
- package/src/theme-editor/webmcp/types.ts +16 -3
- package/src/types.ts +108 -2
- package/src/ui.docked.test.ts +2 -2
- package/src/ui.suggest-replies.test.ts +237 -0
- package/src/ui.ts +57 -13
- package/src/utils/dock.test.ts +23 -1
- package/src/utils/dock.ts +2 -0
- package/src/voice/voice.test.ts +0 -51
- package/src/install-config.test.ts +0 -38
package/src/session.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { AgentWidgetClient, type SSEEventCallback } from "./client";
|
|
2
2
|
import { isWebMcpToolName } from "./webmcp-bridge";
|
|
3
|
+
import {
|
|
4
|
+
SUGGEST_REPLIES_TOOL_NAME,
|
|
5
|
+
suggestRepliesToolResult,
|
|
6
|
+
} from "./suggest-replies-tool";
|
|
3
7
|
import {
|
|
4
8
|
AgentWidgetConfig,
|
|
5
9
|
AgentWidgetEvent,
|
|
@@ -40,6 +44,49 @@ export type AgentWidgetSessionStatus =
|
|
|
40
44
|
| "connected"
|
|
41
45
|
| "error";
|
|
42
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Config fields the `AgentWidgetClient` reads to shape the connection and each
|
|
49
|
+
* request. When NONE of these change, `updateConfig` can refresh the live
|
|
50
|
+
* client in place (preserving the WebMCP bridge and any in-flight stream/resume)
|
|
51
|
+
* instead of swapping in a fresh client and tearing down WebMCP state. Fields
|
|
52
|
+
* outside this list (theme, copy, layout, suggestionChips, iterationDisplay,
|
|
53
|
+
* postprocessMessage, feature display toggles, …) are display-only and safe to
|
|
54
|
+
* apply mid-turn — which is what a self-styling widget needs when a `webmcp:*`
|
|
55
|
+
* theme tool re-renders the widget while the agent's turn is still streaming.
|
|
56
|
+
*
|
|
57
|
+
* Compared by identity (`!==`): primitives by value, functions/objects by
|
|
58
|
+
* reference. A consumer that rebuilds these objects on every render simply
|
|
59
|
+
* takes the (still-correct) full-rebuild path. The default is therefore safe:
|
|
60
|
+
* anything not explicitly listed here can never strand a paused turn.
|
|
61
|
+
*/
|
|
62
|
+
const CONNECTION_CONFIG_KEYS = [
|
|
63
|
+
"apiUrl",
|
|
64
|
+
"clientToken",
|
|
65
|
+
"flowId",
|
|
66
|
+
"agent",
|
|
67
|
+
"agentOptions",
|
|
68
|
+
"headers",
|
|
69
|
+
"getHeaders",
|
|
70
|
+
"webmcp",
|
|
71
|
+
"streamParser",
|
|
72
|
+
"parserType",
|
|
73
|
+
"contextProviders",
|
|
74
|
+
"requestMiddleware",
|
|
75
|
+
"customFetch",
|
|
76
|
+
"parseSSEEvent",
|
|
77
|
+
"onSessionInit",
|
|
78
|
+
"onSessionExpired",
|
|
79
|
+
"getStoredSessionId",
|
|
80
|
+
"setStoredSessionId",
|
|
81
|
+
] as const satisfies ReadonlyArray<keyof AgentWidgetConfig>;
|
|
82
|
+
|
|
83
|
+
function connectionConfigChanged(
|
|
84
|
+
prev: AgentWidgetConfig,
|
|
85
|
+
next: AgentWidgetConfig,
|
|
86
|
+
): boolean {
|
|
87
|
+
return CONNECTION_CONFIG_KEYS.some((key) => prev[key] !== next[key]);
|
|
88
|
+
}
|
|
89
|
+
|
|
43
90
|
type SessionCallbacks = {
|
|
44
91
|
onMessagesChanged: (messages: AgentWidgetMessage[]) => void;
|
|
45
92
|
onStatusChanged: (status: AgentWidgetSessionStatus) => void;
|
|
@@ -91,6 +138,15 @@ const getWebMcpErrorMessage = (
|
|
|
91
138
|
return fallback;
|
|
92
139
|
};
|
|
93
140
|
|
|
141
|
+
/**
|
|
142
|
+
* Tool names whose `step_await` the widget resolves automatically (no user
|
|
143
|
+
* pill click): `webmcp:*` page tools and the built-in fire-and-forget
|
|
144
|
+
* `suggest_replies`. These share the await-batch / dedupe / resume machinery;
|
|
145
|
+
* `ask_user_question` is NOT one of them — it blocks on the answer sheet.
|
|
146
|
+
*/
|
|
147
|
+
const isAutoResolvedLocalToolName = (name: string): boolean =>
|
|
148
|
+
isWebMcpToolName(name) || name === SUGGEST_REPLIES_TOOL_NAME;
|
|
149
|
+
|
|
94
150
|
export class AgentWidgetSession {
|
|
95
151
|
private client: AgentWidgetClient;
|
|
96
152
|
private messages: AgentWidgetMessage[];
|
|
@@ -615,6 +671,21 @@ export class AgentWidgetSession {
|
|
|
615
671
|
}
|
|
616
672
|
|
|
617
673
|
public updateConfig(next: AgentWidgetConfig) {
|
|
674
|
+
const merged = { ...this.config, ...next };
|
|
675
|
+
|
|
676
|
+
// Connection/request-shaping change (apiUrl, clientToken, webmcp, headers,
|
|
677
|
+
// parser, agent, …) → full client rebuild. UI-only change (theme, copy,
|
|
678
|
+
// layout, suggestions, …) → refresh in place so the live stream, WebMCP
|
|
679
|
+
// bridge, and any in-flight resolve survive. The latter is what makes a
|
|
680
|
+
// self-styling widget work: a `webmcp:*` theme tool mutates config and
|
|
681
|
+
// re-renders mid-turn; recreating the client there would abort the very
|
|
682
|
+
// turn that's restyling the widget and strand the paused execution.
|
|
683
|
+
if (!connectionConfigChanged(this.config, merged)) {
|
|
684
|
+
this.config = merged;
|
|
685
|
+
this.client.updateConfig(merged);
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
|
|
618
689
|
// Replacing the client invalidates every in-flight WebMCP resolve, buffered
|
|
619
690
|
// parallel-await batch, and pending approval bubble tied to the OLD client/
|
|
620
691
|
// session. Tear them down BEFORE the swap (the new client has no session
|
|
@@ -626,7 +697,7 @@ export class AgentWidgetSession {
|
|
|
626
697
|
this.webMcpInflightKeys.clear();
|
|
627
698
|
this.webMcpResolvedKeys.clear();
|
|
628
699
|
const prevSSECallback = this.client.getSSEEventCallback();
|
|
629
|
-
this.config =
|
|
700
|
+
this.config = merged;
|
|
630
701
|
this.client = new AgentWidgetClient(this.config);
|
|
631
702
|
this.wireDefaultWebMcpConfirm();
|
|
632
703
|
if (prevSSECallback) {
|
|
@@ -1535,7 +1606,8 @@ export class AgentWidgetSession {
|
|
|
1535
1606
|
}
|
|
1536
1607
|
|
|
1537
1608
|
/**
|
|
1538
|
-
* Collect
|
|
1609
|
+
* Collect an auto-resolving LOCAL-tool `step_await` (`webmcp:*` page tools
|
|
1610
|
+
* and the built-in `suggest_replies`) into a per-executionId batch
|
|
1539
1611
|
* and schedule a single deferred flush. Parallel calls (core#3878) emit
|
|
1540
1612
|
* several `step_await`s for ONE paused execution within the same stream tick;
|
|
1541
1613
|
* buffering them and flushing once lets us post ONE `/resume` keyed by the
|
|
@@ -1636,6 +1708,26 @@ export class AgentWidgetSession {
|
|
|
1636
1708
|
return Date.now();
|
|
1637
1709
|
}
|
|
1638
1710
|
|
|
1711
|
+
/**
|
|
1712
|
+
* Persisted-resolution guard for `suggest_replies`. The in-memory dedupe
|
|
1713
|
+
* sets (`webMcpInflightKeys` / `webMcpResolvedKeys`) are cleared by
|
|
1714
|
+
* hydrateMessages/clearMessages/cancel, but `suggestRepliesResolved`
|
|
1715
|
+
* survives on the stored message — so a stale `step_await` re-emit after a
|
|
1716
|
+
* hydration must not re-POST `/resume` for an already-resolved call (the
|
|
1717
|
+
* historical double-resume failure mode the batching work exists to avoid).
|
|
1718
|
+
* Checks the LIVE message first; the handleEvent snapshot is a fresh wire
|
|
1719
|
+
* skeleton whose metadata never carries the flag.
|
|
1720
|
+
*/
|
|
1721
|
+
private isSuggestRepliesAlreadyResolved(
|
|
1722
|
+
toolMessage: AgentWidgetMessage,
|
|
1723
|
+
): boolean {
|
|
1724
|
+
if (toolMessage.toolCall?.name !== SUGGEST_REPLIES_TOOL_NAME) return false;
|
|
1725
|
+
const stored = this.messages.find((m) => m.id === toolMessage.id);
|
|
1726
|
+
return (
|
|
1727
|
+
(stored ?? toolMessage).agentMetadata?.suggestRepliesResolved === true
|
|
1728
|
+
);
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1639
1731
|
private markWebMcpToolRunning(
|
|
1640
1732
|
toolMessage: AgentWidgetMessage,
|
|
1641
1733
|
): number {
|
|
@@ -1666,6 +1758,9 @@ export class AgentWidgetSession {
|
|
|
1666
1758
|
result: unknown,
|
|
1667
1759
|
startedAt: number,
|
|
1668
1760
|
completedAt = Date.now(),
|
|
1761
|
+
extraMetadata?: Partial<
|
|
1762
|
+
NonNullable<AgentWidgetMessage["agentMetadata"]>
|
|
1763
|
+
>,
|
|
1669
1764
|
): void {
|
|
1670
1765
|
// A teardown such as clearMessages()/hydrateMessages()/new send can remove
|
|
1671
1766
|
// the bubble while an aborted WebMCP promise is settling. Never resurrect a
|
|
@@ -1677,6 +1772,7 @@ export class AgentWidgetSession {
|
|
|
1677
1772
|
agentMetadata: {
|
|
1678
1773
|
...toolMessage.agentMetadata,
|
|
1679
1774
|
awaitingLocalTool: false,
|
|
1775
|
+
...extraMetadata,
|
|
1680
1776
|
},
|
|
1681
1777
|
toolCall: toolMessage.toolCall
|
|
1682
1778
|
? {
|
|
@@ -1738,7 +1834,8 @@ export class AgentWidgetSession {
|
|
|
1738
1834
|
const dedupeKey = `${executionId}:${callId}`;
|
|
1739
1835
|
if (
|
|
1740
1836
|
this.webMcpInflightKeys.has(dedupeKey) ||
|
|
1741
|
-
this.webMcpResolvedKeys.has(dedupeKey)
|
|
1837
|
+
this.webMcpResolvedKeys.has(dedupeKey) ||
|
|
1838
|
+
this.isSuggestRepliesAlreadyResolved(toolMessage)
|
|
1742
1839
|
) {
|
|
1743
1840
|
return null;
|
|
1744
1841
|
}
|
|
@@ -1750,15 +1847,29 @@ export class AgentWidgetSession {
|
|
|
1750
1847
|
// only means the server paused for a local tool; it is not completion.
|
|
1751
1848
|
const startedAt = this.markWebMcpToolRunning(toolMessage);
|
|
1752
1849
|
|
|
1753
|
-
const controller = new AbortController();
|
|
1754
|
-
this.webMcpResolveControllers.add(controller);
|
|
1755
|
-
controllers.push(controller);
|
|
1756
|
-
|
|
1757
1850
|
// Per-call id wins for resume keying; fall back to the wire tool name
|
|
1758
1851
|
// for legacy servers that don't emit `webMcpToolCallId`.
|
|
1759
1852
|
const resumeKey =
|
|
1760
1853
|
toolMessage.agentMetadata?.webMcpToolCallId ?? wireToolName;
|
|
1761
1854
|
|
|
1855
|
+
// Built-in fire-and-forget tool: no bridge, no confirm gate, no
|
|
1856
|
+
// browser-side execution — the chips render from the message list and
|
|
1857
|
+
// the canned output joins the batch's single /resume.
|
|
1858
|
+
if (wireToolName === SUGGEST_REPLIES_TOOL_NAME) {
|
|
1859
|
+
return {
|
|
1860
|
+
dedupeKey,
|
|
1861
|
+
resumeKey,
|
|
1862
|
+
output: suggestRepliesToolResult(),
|
|
1863
|
+
toolMessage,
|
|
1864
|
+
startedAt,
|
|
1865
|
+
completedAt: Date.now(),
|
|
1866
|
+
};
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
const controller = new AbortController();
|
|
1870
|
+
this.webMcpResolveControllers.add(controller);
|
|
1871
|
+
controllers.push(controller);
|
|
1872
|
+
|
|
1762
1873
|
const execPromise = this.client.executeWebMcpToolCall(
|
|
1763
1874
|
wireToolName,
|
|
1764
1875
|
toolMessage.toolCall?.args,
|
|
@@ -1853,6 +1964,9 @@ export class AgentWidgetSession {
|
|
|
1853
1964
|
r.output,
|
|
1854
1965
|
r.startedAt,
|
|
1855
1966
|
r.completedAt,
|
|
1967
|
+
r.toolMessage.toolCall?.name === SUGGEST_REPLIES_TOOL_NAME
|
|
1968
|
+
? { suggestRepliesResolved: true }
|
|
1969
|
+
: undefined,
|
|
1856
1970
|
);
|
|
1857
1971
|
}
|
|
1858
1972
|
if (response.body) {
|
|
@@ -1892,12 +2006,16 @@ export class AgentWidgetSession {
|
|
|
1892
2006
|
}
|
|
1893
2007
|
|
|
1894
2008
|
/**
|
|
1895
|
-
* Resolve a paused
|
|
1896
|
-
*
|
|
2009
|
+
* Resolve a paused auto-resolving LOCAL tool call and post the result to
|
|
2010
|
+
* `/resume`: `webmcp:*` calls execute against the host page's tool
|
|
2011
|
+
* registry; the built-in `suggest_replies` skips execution entirely and
|
|
2012
|
+
* resumes with a canned "shown" result (the chips render from the message
|
|
2013
|
+
* list, not from this resolve).
|
|
1897
2014
|
*
|
|
1898
2015
|
* Triggered automatically from `handleEvent` when a `step_await`-derived
|
|
1899
|
-
* message arrives
|
|
1900
|
-
*
|
|
2016
|
+
* message arrives for such a tool — the user does not click a pill; the
|
|
2017
|
+
* bridge's confirm-bubble gate (WebMCP only) is the only interactive
|
|
2018
|
+
* surface.
|
|
1901
2019
|
*
|
|
1902
2020
|
* Idempotent on the message's `toolCall.id`: re-emits of the same step_await
|
|
1903
2021
|
* (e.g. from message coalescing) won't double-fire `tool.execute`. Failure
|
|
@@ -1918,8 +2036,9 @@ export class AgentWidgetSession {
|
|
|
1918
2036
|
// via onError so an operator can react. This is a server-side
|
|
1919
2037
|
// wire-shape bug — Persona can't recover it from the client.
|
|
1920
2038
|
// - no wireToolName: defensive guard — handleEvent only calls us
|
|
1921
|
-
//
|
|
1922
|
-
//
|
|
2039
|
+
// for an auto-resolving local tool name (`webmcp:*` or
|
|
2040
|
+
// `suggest_replies`), so this path indicates a direct caller
|
|
2041
|
+
// misuse. Silent return.
|
|
1923
2042
|
// - no toolCallId: dedupe key falls apart, but the server can still
|
|
1924
2043
|
// advance if we post an isError for the wireToolName. Do that
|
|
1925
2044
|
// and bail before the dedupe path.
|
|
@@ -1966,11 +2085,14 @@ export class AgentWidgetSession {
|
|
|
1966
2085
|
}
|
|
1967
2086
|
|
|
1968
2087
|
// Dedupe key scoped by executionId — see `webMcpInflightKeys` doc comment
|
|
1969
|
-
// for the failure-recovery + cross-dispatch rationale.
|
|
2088
|
+
// for the failure-recovery + cross-dispatch rationale. The persisted
|
|
2089
|
+
// `suggestRepliesResolved` guard backs the in-memory sets across
|
|
2090
|
+
// hydrations.
|
|
1970
2091
|
const dedupeKey = `${executionId}:${toolCallId}`;
|
|
1971
2092
|
if (
|
|
1972
2093
|
this.webMcpInflightKeys.has(dedupeKey) ||
|
|
1973
|
-
this.webMcpResolvedKeys.has(dedupeKey)
|
|
2094
|
+
this.webMcpResolvedKeys.has(dedupeKey) ||
|
|
2095
|
+
this.isSuggestRepliesAlreadyResolved(toolMessage)
|
|
1974
2096
|
) {
|
|
1975
2097
|
return;
|
|
1976
2098
|
}
|
|
@@ -1999,21 +2121,27 @@ export class AgentWidgetSession {
|
|
|
1999
2121
|
const { signal } = resolveController;
|
|
2000
2122
|
this.setStreaming(true);
|
|
2001
2123
|
|
|
2124
|
+
// Built-in fire-and-forget tool: no bridge, no confirm gate, no
|
|
2125
|
+
// browser-side execution — the chips render from the message list and the
|
|
2126
|
+
// canned output resumes the execution immediately. Branch BEFORE any
|
|
2127
|
+
// bridge access so the missing-bridge error path can never fire for it.
|
|
2128
|
+
const isSuggestReplies = wireToolName === SUGGEST_REPLIES_TOOL_NAME;
|
|
2129
|
+
|
|
2002
2130
|
const args = toolMessage.toolCall?.args;
|
|
2003
2131
|
// Thread the signal INTO the bridge — short-circuits the confirm bubble
|
|
2004
2132
|
// and the execute() race on cancel(), so a late confirm-approval after
|
|
2005
2133
|
// cancel() cannot fire a host-page side effect with no matching /resume.
|
|
2006
|
-
const execPromise =
|
|
2007
|
-
|
|
2008
|
-
args,
|
|
2009
|
-
signal,
|
|
2010
|
-
);
|
|
2134
|
+
const execPromise = isSuggestReplies
|
|
2135
|
+
? null
|
|
2136
|
+
: this.client.executeWebMcpToolCall(wireToolName, args, signal);
|
|
2011
2137
|
|
|
2012
2138
|
let phase: "execute" | "resume" = "execute";
|
|
2013
2139
|
let completedAt = startedAt;
|
|
2014
2140
|
try {
|
|
2015
2141
|
let resumeOutput: unknown;
|
|
2016
|
-
if (
|
|
2142
|
+
if (isSuggestReplies) {
|
|
2143
|
+
resumeOutput = suggestRepliesToolResult();
|
|
2144
|
+
} else if (!execPromise) {
|
|
2017
2145
|
// Client has no bridge (config.webmcp.enabled !== true). Resume with
|
|
2018
2146
|
// an error so the dispatch can advance instead of hanging.
|
|
2019
2147
|
resumeOutput = {
|
|
@@ -2062,6 +2190,7 @@ export class AgentWidgetSession {
|
|
|
2062
2190
|
resumeOutput,
|
|
2063
2191
|
startedAt,
|
|
2064
2192
|
completedAt,
|
|
2193
|
+
isSuggestReplies ? { suggestRepliesResolved: true } : undefined,
|
|
2065
2194
|
);
|
|
2066
2195
|
},
|
|
2067
2196
|
signal,
|
|
@@ -2380,10 +2509,13 @@ export class AgentWidgetSession {
|
|
|
2380
2509
|
if (event.type === "message") {
|
|
2381
2510
|
this.upsertMessage(event.message);
|
|
2382
2511
|
|
|
2383
|
-
//
|
|
2384
|
-
// for a `webmcp:*` tool
|
|
2385
|
-
//
|
|
2386
|
-
//
|
|
2512
|
+
// Local-tool auto-resolve: when a step_await emits a tool-variant
|
|
2513
|
+
// message for a `webmcp:*` tool — or the built-in fire-and-forget
|
|
2514
|
+
// `suggest_replies` — resolve it and post the result to /resume.
|
|
2515
|
+
// Unlike ask_user_question, no user pill click is required; for WebMCP
|
|
2516
|
+
// the bridge's confirm bubble is the only interactive surface, and
|
|
2517
|
+
// suggest_replies resumes with a canned "shown" result while the chips
|
|
2518
|
+
// render above the composer.
|
|
2387
2519
|
//
|
|
2388
2520
|
// Defer via `queueMicrotask` so handleEvent returns FIRST. The current
|
|
2389
2521
|
// SSE consumer is still mid-loop; once we return, the dispatch's
|
|
@@ -2397,11 +2529,19 @@ export class AgentWidgetSession {
|
|
|
2397
2529
|
// if the bridge is non-operational. Otherwise the dispatch stays paused
|
|
2398
2530
|
// indefinitely — `resolveWebMcpToolCall` translates the missing-bridge
|
|
2399
2531
|
// case into an isError result that resumes the flow cleanly.
|
|
2532
|
+
// `suggest_replies`, by contrast, is gated on its feature flag: when
|
|
2533
|
+
// `features.suggestReplies.enabled === false` the widget neither
|
|
2534
|
+
// renders chips nor resumes — the same parked-execution posture as a
|
|
2535
|
+
// server-declared ask_user_question with its sheet disabled.
|
|
2400
2536
|
const tc = event.message.toolCall;
|
|
2537
|
+
const autoResolvable =
|
|
2538
|
+
!!tc?.name &&
|
|
2539
|
+
(isWebMcpToolName(tc.name) ||
|
|
2540
|
+
(tc.name === SUGGEST_REPLIES_TOOL_NAME &&
|
|
2541
|
+
this.config.features?.suggestReplies?.enabled !== false));
|
|
2401
2542
|
if (
|
|
2402
2543
|
event.message.agentMetadata?.awaitingLocalTool === true &&
|
|
2403
|
-
|
|
2404
|
-
isWebMcpToolName(tc.name)
|
|
2544
|
+
autoResolvable
|
|
2405
2545
|
) {
|
|
2406
2546
|
// Collect the await into its executionId's batch instead of resolving
|
|
2407
2547
|
// it on the spot. Parallel same-tool calls (core#3878) arrive as
|
|
@@ -2659,6 +2799,21 @@ export class AgentWidgetSession {
|
|
|
2659
2799
|
awaitingLocalTool: false,
|
|
2660
2800
|
};
|
|
2661
2801
|
}
|
|
2802
|
+
// suggest_replies equivalent: preserve the persisted fire-and-forget
|
|
2803
|
+
// resolution across re-emissions. It is the only dedupe signal that
|
|
2804
|
+
// survives a hydration (the in-memory key sets are cleared), so a
|
|
2805
|
+
// stale step_await re-emit must not wipe it before
|
|
2806
|
+
// `isSuggestRepliesAlreadyResolved` checks it in the resolve path.
|
|
2807
|
+
if (
|
|
2808
|
+
existing.agentMetadata?.suggestRepliesResolved === true &&
|
|
2809
|
+
withSequence.agentMetadata
|
|
2810
|
+
) {
|
|
2811
|
+
merged.agentMetadata = {
|
|
2812
|
+
...(merged.agentMetadata ?? withSequence.agentMetadata),
|
|
2813
|
+
suggestRepliesResolved: true,
|
|
2814
|
+
awaitingLocalTool: false,
|
|
2815
|
+
};
|
|
2816
|
+
}
|
|
2662
2817
|
// Approval equivalent: `agent_approval_complete` carries only the
|
|
2663
2818
|
// resolution (approvalId, decision, resolvedBy) — the runtime does not
|
|
2664
2819
|
// re-send toolName/description/toolType/reason/parameters, so client.ts
|
|
@@ -2686,9 +2841,10 @@ export class AgentWidgetSession {
|
|
|
2686
2841
|
parameters: incoming.parameters ?? prior.parameters,
|
|
2687
2842
|
};
|
|
2688
2843
|
}
|
|
2689
|
-
//
|
|
2690
|
-
//
|
|
2691
|
-
//
|
|
2844
|
+
// Auto-resolved local-tool equivalent (`webmcp:*` and the built-in
|
|
2845
|
+
// `suggest_replies`): once such a tool has started resolving (inflight)
|
|
2846
|
+
// or resolved, a duplicate `step_await` re-emit must not flip
|
|
2847
|
+
// `awaitingLocalTool` back to true and resurrect the "waiting on
|
|
2692
2848
|
// local tool" UI. It also must not overwrite an existing running or
|
|
2693
2849
|
// completed toolCall with the fresh running skeleton emitted by client.ts
|
|
2694
2850
|
// for every step_await. resolveWebMcpToolCall's dedupe path returns
|
|
@@ -2699,7 +2855,7 @@ export class AgentWidgetSession {
|
|
|
2699
2855
|
const reTcId = withSequence.toolCall?.id;
|
|
2700
2856
|
if (
|
|
2701
2857
|
reTcName &&
|
|
2702
|
-
|
|
2858
|
+
isAutoResolvedLocalToolName(reTcName) &&
|
|
2703
2859
|
reExecId &&
|
|
2704
2860
|
reTcId &&
|
|
2705
2861
|
withSequence.agentMetadata?.awaitingLocalTool === true
|
|
@@ -2712,7 +2868,7 @@ export class AgentWidgetSession {
|
|
|
2712
2868
|
existing.agentMetadata?.executionId === reExecId &&
|
|
2713
2869
|
existing.toolCall?.id === reTcId &&
|
|
2714
2870
|
existingToolName !== undefined &&
|
|
2715
|
-
|
|
2871
|
+
isAutoResolvedLocalToolName(existingToolName) &&
|
|
2716
2872
|
existing.toolCall?.status === "complete";
|
|
2717
2873
|
if (isInflight || isResolved || hasCompletedTool) {
|
|
2718
2874
|
merged.agentMetadata = {
|
|
@@ -1075,6 +1075,54 @@ describe("AgentWidgetSession — WebMCP parallel batched resume (core#3878)", ()
|
|
|
1075
1075
|
expect(s.webMcpApprovalResolvers.size).toBe(0);
|
|
1076
1076
|
});
|
|
1077
1077
|
|
|
1078
|
+
it("a UI-only updateConfig preserves the client, bridge, and in-flight WebMCP state", async () => {
|
|
1079
|
+
// Self-styling widget: a `webmcp:*` theme tool mutates config and re-renders
|
|
1080
|
+
// mid-turn. updateConfig must NOT swap the client when only display fields
|
|
1081
|
+
// (theme/copy/…) change — doing so would abort the very turn that's
|
|
1082
|
+
// restyling the widget and strand the paused execution. (Inverse of the
|
|
1083
|
+
// teardown test above.)
|
|
1084
|
+
const { session } = makeSession();
|
|
1085
|
+
const s = session as unknown as {
|
|
1086
|
+
client: unknown;
|
|
1087
|
+
config: { copy?: { welcomeTitle?: string } };
|
|
1088
|
+
webMcpAwaitBatches: Map<string, unknown>;
|
|
1089
|
+
webMcpApprovalResolvers: Map<string, unknown>;
|
|
1090
|
+
};
|
|
1091
|
+
const clientBefore = s.client;
|
|
1092
|
+
feed(session, parallelAwait("toolu_A", "SHOE-001"));
|
|
1093
|
+
feed(session, parallelAwait("toolu_B", "SHOE-007"));
|
|
1094
|
+
session.requestWebMcpApproval({
|
|
1095
|
+
toolName: "add_to_cart",
|
|
1096
|
+
args: { sku: "SHOE-001" },
|
|
1097
|
+
} as WebMcpConfirmInfo);
|
|
1098
|
+
expect(s.webMcpAwaitBatches.size).toBe(1);
|
|
1099
|
+
expect(s.webMcpApprovalResolvers.size).toBe(1);
|
|
1100
|
+
|
|
1101
|
+
// No apiUrl / webmcp in the patch → connection unchanged → in-place refresh.
|
|
1102
|
+
session.updateConfig({ copy: { welcomeTitle: "Recolored" } });
|
|
1103
|
+
|
|
1104
|
+
expect(s.client).toBe(clientBefore); // same client instance, not swapped
|
|
1105
|
+
expect(s.config.copy?.welcomeTitle).toBe("Recolored"); // display change applied
|
|
1106
|
+
expect(s.webMcpAwaitBatches.size).toBe(1); // batch survives
|
|
1107
|
+
expect(s.webMcpApprovalResolvers.size).toBe(1); // approval still pending
|
|
1108
|
+
});
|
|
1109
|
+
|
|
1110
|
+
it("a connection change (apiUrl) still swaps the client and tears down WebMCP state", () => {
|
|
1111
|
+
const { session } = makeSession();
|
|
1112
|
+
const s = session as unknown as {
|
|
1113
|
+
client: unknown;
|
|
1114
|
+
webMcpAwaitBatches: Map<string, unknown>;
|
|
1115
|
+
};
|
|
1116
|
+
const clientBefore = s.client;
|
|
1117
|
+
feed(session, parallelAwait("toolu_A", "SHOE-001"));
|
|
1118
|
+
expect(s.webMcpAwaitBatches.size).toBe(1);
|
|
1119
|
+
|
|
1120
|
+
session.updateConfig({ apiUrl: "http://other" });
|
|
1121
|
+
|
|
1122
|
+
expect(s.client).not.toBe(clientBefore); // fresh client
|
|
1123
|
+
expect(s.webMcpAwaitBatches.size).toBe(0); // batch torn down
|
|
1124
|
+
});
|
|
1125
|
+
|
|
1078
1126
|
it("a teardown before the stream-end flush strands the batch", async () => {
|
|
1079
1127
|
const { session, executeSpy, resumeSpy } = makeSession();
|
|
1080
1128
|
feed(session, parallelAwait("toolu_A", "SHOE-001"));
|