@runtypelabs/persona 3.28.0 → 3.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/{types-CxvHw0X6.d.cts → types-B_Qazlm4.d.cts} +6 -0
- package/dist/animations/{types-CxvHw0X6.d.ts → types-B_Qazlm4.d.ts} +6 -0
- package/dist/animations/wipe.d.cts +1 -1
- package/dist/animations/wipe.d.ts +1 -1
- package/dist/codegen.cjs +11 -11
- package/dist/codegen.d.cts +9 -0
- package/dist/codegen.d.ts +9 -0
- package/dist/codegen.js +12 -12
- package/dist/index.cjs +40 -40
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +24 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.global.js +47 -47
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +40 -40
- package/dist/index.js.map +1 -1
- package/dist/smart-dom-reader.d.cts +6 -0
- package/dist/smart-dom-reader.d.ts +6 -0
- package/dist/theme-editor.cjs +30 -30
- package/dist/theme-editor.d.cts +6 -0
- package/dist/theme-editor.d.ts +6 -0
- package/dist/theme-editor.js +30 -30
- package/package.json +1 -1
- package/src/client.test.ts +40 -0
- package/src/client.ts +18 -4
- package/src/codegen.test.ts +43 -0
- package/src/generated/runtype-openapi-contract.ts +12 -0
- package/src/session.ts +190 -29
- package/src/session.webmcp.test.ts +326 -6
- package/src/utils/code-generators.ts +38 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@runtypelabs/persona",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.29.1",
|
|
4
4
|
"description": "Themeable, pluggable streaming agent widget for websites, in plain JS with support for voice input and reasoning / tool output.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.cjs",
|
package/src/client.test.ts
CHANGED
|
@@ -3056,6 +3056,46 @@ describe('AgentWidgetClient — step_await parsing', () => {
|
|
|
3056
3056
|
expect(toolMsg!.agentMetadata?.awaitingLocalTool).toBe(true);
|
|
3057
3057
|
});
|
|
3058
3058
|
|
|
3059
|
+
it('emits a running tool message for WebMCP local_tool_required until the browser tool resolves', async () => {
|
|
3060
|
+
global.fetch = vi.fn().mockResolvedValue({
|
|
3061
|
+
ok: true,
|
|
3062
|
+
body: buildStepAwaitStream({
|
|
3063
|
+
awaitReason: 'local_tool_required',
|
|
3064
|
+
id: 'step-1',
|
|
3065
|
+
name: 'Test Step',
|
|
3066
|
+
stepType: 'prompt',
|
|
3067
|
+
index: 0,
|
|
3068
|
+
toolCallId: 'tc_webmcp_1',
|
|
3069
|
+
toolName: 'webmcp:get_product_by_url',
|
|
3070
|
+
executionId: 'exec_abc',
|
|
3071
|
+
startedAt: 1234,
|
|
3072
|
+
parameters: { path: '/jade/' },
|
|
3073
|
+
}),
|
|
3074
|
+
});
|
|
3075
|
+
|
|
3076
|
+
const client = new AgentWidgetClient({ apiUrl: 'http://localhost:8000' });
|
|
3077
|
+
const events: AgentWidgetEvent[] = [];
|
|
3078
|
+
await client.dispatch(
|
|
3079
|
+
{ messages: [{ id: 'u1', role: 'user', content: 'hi', createdAt: new Date().toISOString() }] },
|
|
3080
|
+
(e) => events.push(e)
|
|
3081
|
+
);
|
|
3082
|
+
|
|
3083
|
+
const toolMsg = events
|
|
3084
|
+
.filter((e) => e.type === 'message')
|
|
3085
|
+
.map((e) => (e as { message: AgentWidgetMessage }).message)
|
|
3086
|
+
.find((m) => m.variant === 'tool' && m.toolCall?.name === 'webmcp:get_product_by_url');
|
|
3087
|
+
|
|
3088
|
+
expect(toolMsg).toBeDefined();
|
|
3089
|
+
expect(toolMsg!.toolCall!.id).toBe('tc_webmcp_1');
|
|
3090
|
+
expect(toolMsg!.toolCall!.status).toBe('running');
|
|
3091
|
+
expect(toolMsg!.toolCall!.startedAt).toBe(1234);
|
|
3092
|
+
expect(toolMsg!.toolCall!.completedAt).toBeUndefined();
|
|
3093
|
+
expect(toolMsg!.toolCall!.durationMs).toBeUndefined();
|
|
3094
|
+
expect(toolMsg!.toolCall!.args).toEqual({ path: '/jade/' });
|
|
3095
|
+
expect(toolMsg!.agentMetadata?.executionId).toBe('exec_abc');
|
|
3096
|
+
expect(toolMsg!.agentMetadata?.awaitingLocalTool).toBe(true);
|
|
3097
|
+
});
|
|
3098
|
+
|
|
3059
3099
|
it('ignores step_await events whose awaitReason is not local_tool_required', async () => {
|
|
3060
3100
|
global.fetch = vi.fn().mockResolvedValue({
|
|
3061
3101
|
ok: true,
|
package/src/client.ts
CHANGED
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
ContentPart,
|
|
24
24
|
WebMcpConfirmHandler
|
|
25
25
|
} from "./types";
|
|
26
|
-
import { WebMcpBridge, computeClientToolsFingerprint } from "./webmcp-bridge";
|
|
26
|
+
import { WebMcpBridge, computeClientToolsFingerprint, isWebMcpToolName } from "./webmcp-bridge";
|
|
27
27
|
import {
|
|
28
28
|
extractTextFromJson,
|
|
29
29
|
createPlainTextParser,
|
|
@@ -2026,14 +2026,28 @@ export class AgentWidgetClient {
|
|
|
2026
2026
|
const toolId =
|
|
2027
2027
|
toolCallId ?? (payload.toolId as string) ?? `local-${nextSequence()}`;
|
|
2028
2028
|
const toolMessage = ensureToolMessage(toolId);
|
|
2029
|
+
const toolName = payload.toolName as string;
|
|
2030
|
+
const webMcpTool = isWebMcpToolName(toolName);
|
|
2029
2031
|
const tool = toolMessage.toolCall ?? { id: toolId, status: "pending" as const };
|
|
2030
|
-
tool.name =
|
|
2032
|
+
tool.name = toolName;
|
|
2031
2033
|
tool.args = payload.parameters;
|
|
2032
|
-
|
|
2034
|
+
// WebMCP tools are executed asynchronously by the browser AFTER this
|
|
2035
|
+
// `step_await` arrives. Keep them running until session.ts resolves
|
|
2036
|
+
// the page tool and records its actual elapsed time. Other local
|
|
2037
|
+
// tools (for example ask_user_question) keep the existing complete
|
|
2038
|
+
// state because they are waiting for a user interaction, not an
|
|
2039
|
+
// automatic page-tool execution.
|
|
2040
|
+
tool.status = webMcpTool ? "running" : "complete";
|
|
2033
2041
|
tool.chunks = tool.chunks ?? [];
|
|
2034
2042
|
tool.startedAt =
|
|
2035
2043
|
tool.startedAt ?? resolveTimestamp(payload.startedAt ?? payload.timestamp);
|
|
2036
|
-
|
|
2044
|
+
if (webMcpTool) {
|
|
2045
|
+
tool.completedAt = undefined;
|
|
2046
|
+
tool.duration = undefined;
|
|
2047
|
+
tool.durationMs = undefined;
|
|
2048
|
+
} else {
|
|
2049
|
+
tool.completedAt = tool.completedAt ?? tool.startedAt;
|
|
2050
|
+
}
|
|
2037
2051
|
toolMessage.toolCall = tool;
|
|
2038
2052
|
toolMessage.streaming = false;
|
|
2039
2053
|
toolMessage.agentMetadata = {
|
package/src/codegen.test.ts
CHANGED
|
@@ -36,4 +36,47 @@ describe("codegen subpath", () => {
|
|
|
36
36
|
expect(code).toContain(`@runtypelabs/persona@${VERSION}/dist/install.global.js`);
|
|
37
37
|
expect(code).not.toContain("@latest");
|
|
38
38
|
});
|
|
39
|
+
|
|
40
|
+
describe("mount target option", () => {
|
|
41
|
+
it("defaults to body when no target is given", () => {
|
|
42
|
+
expect(fromSubpath(config, "react-component")).toContain("target: 'body'");
|
|
43
|
+
expect(fromSubpath(config, "script-manual")).toContain("target: 'body'");
|
|
44
|
+
// installer omits the target key entirely (installer falls back to body)
|
|
45
|
+
expect(fromSubpath(config, "script-installer")).not.toContain('"target"');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("emits the selector as the initAgentWidget target for code formats", () => {
|
|
49
|
+
const opts = { target: "#chat" };
|
|
50
|
+
expect(fromSubpath(config, "esm", opts)).toContain("target: '#chat'");
|
|
51
|
+
expect(fromSubpath(config, "react-component", opts)).toContain("target: '#chat'");
|
|
52
|
+
expect(fromSubpath(config, "script-manual", opts)).toContain("target: '#chat'");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("serializes the selector as a top-level installer option (sibling of config)", () => {
|
|
56
|
+
// install.ts reads the mount point from the TOP LEVEL of data-config, not
|
|
57
|
+
// from inside the widget config. Parse the emitted data-config and assert
|
|
58
|
+
// the structure so a regression to the nested form is caught.
|
|
59
|
+
const code = fromSubpath(config, "script-installer", { target: "#chat" });
|
|
60
|
+
const json = code.match(/data-config='([^']*)'/)?.[1];
|
|
61
|
+
expect(json).toBeTruthy();
|
|
62
|
+
const parsed = JSON.parse(json!);
|
|
63
|
+
expect(parsed.target).toBe("#chat");
|
|
64
|
+
expect(parsed.config).toBeTruthy();
|
|
65
|
+
// target must NOT be buried inside the widget config (where it's ignored)
|
|
66
|
+
expect(parsed.config.target).toBeUndefined();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("keeps target as a top-level sibling alongside windowKey", () => {
|
|
70
|
+
const code = fromSubpath(config, "script-installer", { target: "#chat", windowKey: "myWidget" });
|
|
71
|
+
const parsed = JSON.parse(code.match(/data-config='([^']*)'/)![1]);
|
|
72
|
+
expect(parsed.target).toBe("#chat");
|
|
73
|
+
expect(parsed.windowKey).toBe("myWidget");
|
|
74
|
+
expect(parsed.config).toBeTruthy();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("escapes single quotes in the selector so it can't break the literal", () => {
|
|
78
|
+
const code = fromSubpath(config, "react-component", { target: "[data-x='y']" });
|
|
79
|
+
expect(code).toContain("target: '[data-x=\\'y\\']'");
|
|
80
|
+
});
|
|
81
|
+
});
|
|
39
82
|
});
|
|
@@ -206,6 +206,7 @@ export type RuntypeAgentSSEEvent = {
|
|
|
206
206
|
iteration: number;
|
|
207
207
|
reflection?: string;
|
|
208
208
|
seq: number;
|
|
209
|
+
timestamp?: string;
|
|
209
210
|
type: "agent_reflection";
|
|
210
211
|
} | {
|
|
211
212
|
activatedCapabilities: Array<string>;
|
|
@@ -213,6 +214,7 @@ export type RuntypeAgentSSEEvent = {
|
|
|
213
214
|
iteration: number;
|
|
214
215
|
seq: number;
|
|
215
216
|
skill: string;
|
|
217
|
+
timestamp?: string;
|
|
216
218
|
toolCallId: string;
|
|
217
219
|
type: "agent_skill_loaded";
|
|
218
220
|
} | ({
|
|
@@ -222,6 +224,7 @@ export type RuntypeAgentSSEEvent = {
|
|
|
222
224
|
proposalId?: string;
|
|
223
225
|
seq: number;
|
|
224
226
|
skill: string;
|
|
227
|
+
timestamp?: string;
|
|
225
228
|
toolCallId: string;
|
|
226
229
|
type: "agent_skill_proposed";
|
|
227
230
|
}) | ({
|
|
@@ -255,6 +258,7 @@ export type RuntypeAgentSSEEvent = {
|
|
|
255
258
|
iteration?: number;
|
|
256
259
|
recoverable: boolean;
|
|
257
260
|
seq: number;
|
|
261
|
+
timestamp?: string;
|
|
258
262
|
type: "agent_error";
|
|
259
263
|
} | {
|
|
260
264
|
executionId: string;
|
|
@@ -289,6 +293,7 @@ export type RuntypeFlowSSEEvent = {
|
|
|
289
293
|
failedSteps?: number;
|
|
290
294
|
finalOutput?: string;
|
|
291
295
|
flowId?: string;
|
|
296
|
+
flowName?: string;
|
|
292
297
|
output?: unknown;
|
|
293
298
|
seq?: number;
|
|
294
299
|
source?: string;
|
|
@@ -340,6 +345,7 @@ export type RuntypeFlowSSEEvent = {
|
|
|
340
345
|
id?: string;
|
|
341
346
|
index?: number;
|
|
342
347
|
name?: string;
|
|
348
|
+
outputVariable?: string;
|
|
343
349
|
seq?: number;
|
|
344
350
|
startedAt: string;
|
|
345
351
|
stepId?: string;
|
|
@@ -751,6 +757,7 @@ export type RuntypeDispatchSSEEvent = {
|
|
|
751
757
|
iteration: number;
|
|
752
758
|
reflection?: string;
|
|
753
759
|
seq: number;
|
|
760
|
+
timestamp?: string;
|
|
754
761
|
type: "agent_reflection";
|
|
755
762
|
} | {
|
|
756
763
|
activatedCapabilities: Array<string>;
|
|
@@ -758,6 +765,7 @@ export type RuntypeDispatchSSEEvent = {
|
|
|
758
765
|
iteration: number;
|
|
759
766
|
seq: number;
|
|
760
767
|
skill: string;
|
|
768
|
+
timestamp?: string;
|
|
761
769
|
toolCallId: string;
|
|
762
770
|
type: "agent_skill_loaded";
|
|
763
771
|
} | ({
|
|
@@ -767,6 +775,7 @@ export type RuntypeDispatchSSEEvent = {
|
|
|
767
775
|
proposalId?: string;
|
|
768
776
|
seq: number;
|
|
769
777
|
skill: string;
|
|
778
|
+
timestamp?: string;
|
|
770
779
|
toolCallId: string;
|
|
771
780
|
type: "agent_skill_proposed";
|
|
772
781
|
}) | ({
|
|
@@ -800,6 +809,7 @@ export type RuntypeDispatchSSEEvent = {
|
|
|
800
809
|
iteration?: number;
|
|
801
810
|
recoverable: boolean;
|
|
802
811
|
seq: number;
|
|
812
|
+
timestamp?: string;
|
|
803
813
|
type: "agent_error";
|
|
804
814
|
} | {
|
|
805
815
|
executionId: string;
|
|
@@ -832,6 +842,7 @@ export type RuntypeDispatchSSEEvent = {
|
|
|
832
842
|
failedSteps?: number;
|
|
833
843
|
finalOutput?: string;
|
|
834
844
|
flowId?: string;
|
|
845
|
+
flowName?: string;
|
|
835
846
|
output?: unknown;
|
|
836
847
|
seq?: number;
|
|
837
848
|
source?: string;
|
|
@@ -883,6 +894,7 @@ export type RuntypeDispatchSSEEvent = {
|
|
|
883
894
|
id?: string;
|
|
884
895
|
index?: number;
|
|
885
896
|
name?: string;
|
|
897
|
+
outputVariable?: string;
|
|
886
898
|
seq?: number;
|
|
887
899
|
startedAt: string;
|
|
888
900
|
stepId?: string;
|
package/src/session.ts
CHANGED
|
@@ -76,6 +76,20 @@ function buildDispatchErrorContent(
|
|
|
76
76
|
return err.message ? `${base}\n\n_Details: ${err.message}_` : base;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
const buildWebMcpErrorResult = (message: string) => ({
|
|
80
|
+
isError: true,
|
|
81
|
+
content: [{ type: "text" as const, text: message }],
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const getWebMcpErrorMessage = (
|
|
85
|
+
error: unknown,
|
|
86
|
+
fallback = "WebMCP tool execution failed.",
|
|
87
|
+
): string => {
|
|
88
|
+
if (error instanceof Error && error.message) return error.message;
|
|
89
|
+
if (typeof error === "string" && error) return error;
|
|
90
|
+
return fallback;
|
|
91
|
+
};
|
|
92
|
+
|
|
79
93
|
export class AgentWidgetSession {
|
|
80
94
|
private client: AgentWidgetClient;
|
|
81
95
|
private messages: AgentWidgetMessage[];
|
|
@@ -1595,6 +1609,78 @@ export class AgentWidgetSession {
|
|
|
1595
1609
|
}
|
|
1596
1610
|
}
|
|
1597
1611
|
|
|
1612
|
+
private resolveWebMcpToolStartedAt(
|
|
1613
|
+
toolMessage: AgentWidgetMessage,
|
|
1614
|
+
): number {
|
|
1615
|
+
const stored = this.messages.find((m) => m.id === toolMessage.id);
|
|
1616
|
+
const candidates = [
|
|
1617
|
+
stored?.toolCall?.startedAt,
|
|
1618
|
+
toolMessage.toolCall?.startedAt,
|
|
1619
|
+
];
|
|
1620
|
+
for (const candidate of candidates) {
|
|
1621
|
+
if (typeof candidate === "number" && Number.isFinite(candidate)) {
|
|
1622
|
+
return candidate;
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
return Date.now();
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
private markWebMcpToolRunning(
|
|
1629
|
+
toolMessage: AgentWidgetMessage,
|
|
1630
|
+
): number {
|
|
1631
|
+
const startedAt = this.resolveWebMcpToolStartedAt(toolMessage);
|
|
1632
|
+
this.upsertMessage({
|
|
1633
|
+
...toolMessage,
|
|
1634
|
+
streaming: true,
|
|
1635
|
+
agentMetadata: {
|
|
1636
|
+
...toolMessage.agentMetadata,
|
|
1637
|
+
awaitingLocalTool: false,
|
|
1638
|
+
},
|
|
1639
|
+
toolCall: toolMessage.toolCall
|
|
1640
|
+
? {
|
|
1641
|
+
...toolMessage.toolCall,
|
|
1642
|
+
status: "running",
|
|
1643
|
+
startedAt,
|
|
1644
|
+
completedAt: undefined,
|
|
1645
|
+
duration: undefined,
|
|
1646
|
+
durationMs: undefined,
|
|
1647
|
+
}
|
|
1648
|
+
: toolMessage.toolCall,
|
|
1649
|
+
});
|
|
1650
|
+
return startedAt;
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
private markWebMcpToolComplete(
|
|
1654
|
+
toolMessage: AgentWidgetMessage,
|
|
1655
|
+
result: unknown,
|
|
1656
|
+
startedAt: number,
|
|
1657
|
+
completedAt = Date.now(),
|
|
1658
|
+
): void {
|
|
1659
|
+
// A teardown such as clearMessages()/hydrateMessages()/new send can remove
|
|
1660
|
+
// the bubble while an aborted WebMCP promise is settling. Never resurrect a
|
|
1661
|
+
// cleared message just to mark the old resolve complete.
|
|
1662
|
+
if (!this.messages.some((message) => message.id === toolMessage.id)) return;
|
|
1663
|
+
this.upsertMessage({
|
|
1664
|
+
...toolMessage,
|
|
1665
|
+
streaming: false,
|
|
1666
|
+
agentMetadata: {
|
|
1667
|
+
...toolMessage.agentMetadata,
|
|
1668
|
+
awaitingLocalTool: false,
|
|
1669
|
+
},
|
|
1670
|
+
toolCall: toolMessage.toolCall
|
|
1671
|
+
? {
|
|
1672
|
+
...toolMessage.toolCall,
|
|
1673
|
+
status: "complete",
|
|
1674
|
+
result,
|
|
1675
|
+
startedAt,
|
|
1676
|
+
completedAt,
|
|
1677
|
+
duration: undefined,
|
|
1678
|
+
durationMs: Math.max(0, completedAt - startedAt),
|
|
1679
|
+
}
|
|
1680
|
+
: toolMessage.toolCall,
|
|
1681
|
+
});
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1598
1684
|
/**
|
|
1599
1685
|
* Resolve TWO OR MORE parallel local-tool awaits sharing one paused
|
|
1600
1686
|
* executionId with a SINGLE `/resume` (core#3878). Each call is executed
|
|
@@ -1613,6 +1699,14 @@ export class AgentWidgetSession {
|
|
|
1613
1699
|
executionId: string,
|
|
1614
1700
|
snapshots: AgentWidgetMessage[],
|
|
1615
1701
|
): Promise<void> {
|
|
1702
|
+
type ExecutedWebMcpTool = {
|
|
1703
|
+
dedupeKey: string;
|
|
1704
|
+
resumeKey: string;
|
|
1705
|
+
output: unknown;
|
|
1706
|
+
toolMessage: AgentWidgetMessage;
|
|
1707
|
+
startedAt: number;
|
|
1708
|
+
completedAt: number;
|
|
1709
|
+
};
|
|
1616
1710
|
const claimedKeys: string[] = [];
|
|
1617
1711
|
const controllers: AbortController[] = [];
|
|
1618
1712
|
// Dedicated controller for the shared resume fetch so cancel() can abort it
|
|
@@ -1640,14 +1734,10 @@ export class AgentWidgetSession {
|
|
|
1640
1734
|
this.webMcpInflightKeys.add(dedupeKey);
|
|
1641
1735
|
claimedKeys.push(dedupeKey);
|
|
1642
1736
|
|
|
1643
|
-
// Clear the awaiting flag
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
...toolMessage.agentMetadata,
|
|
1648
|
-
awaitingLocalTool: false,
|
|
1649
|
-
},
|
|
1650
|
-
});
|
|
1737
|
+
// Clear the awaiting flag and keep the tool bubble running while the
|
|
1738
|
+
// browser-side WebMCP promise is in flight. The initial `step_await`
|
|
1739
|
+
// only means the server paused for a local tool; it is not completion.
|
|
1740
|
+
const startedAt = this.markWebMcpToolRunning(toolMessage);
|
|
1651
1741
|
|
|
1652
1742
|
const controller = new AbortController();
|
|
1653
1743
|
this.webMcpResolveControllers.add(controller);
|
|
@@ -1686,24 +1776,43 @@ export class AgentWidgetSession {
|
|
|
1686
1776
|
error instanceof Error ? error : new Error(String(error)),
|
|
1687
1777
|
);
|
|
1688
1778
|
}
|
|
1779
|
+
this.markWebMcpToolComplete(
|
|
1780
|
+
toolMessage,
|
|
1781
|
+
buildWebMcpErrorResult(
|
|
1782
|
+
isAbortError
|
|
1783
|
+
? "Aborted by cancel()"
|
|
1784
|
+
: getWebMcpErrorMessage(error),
|
|
1785
|
+
),
|
|
1786
|
+
startedAt,
|
|
1787
|
+
);
|
|
1689
1788
|
// Release the dedupe claim so a re-emit can retry this call.
|
|
1690
1789
|
this.webMcpInflightKeys.delete(dedupeKey);
|
|
1691
1790
|
return null;
|
|
1692
1791
|
}
|
|
1693
1792
|
}
|
|
1694
1793
|
if (controller.signal.aborted) {
|
|
1794
|
+
this.markWebMcpToolComplete(
|
|
1795
|
+
toolMessage,
|
|
1796
|
+
buildWebMcpErrorResult("Aborted by cancel()"),
|
|
1797
|
+
startedAt,
|
|
1798
|
+
);
|
|
1695
1799
|
this.webMcpInflightKeys.delete(dedupeKey);
|
|
1696
1800
|
return null;
|
|
1697
1801
|
}
|
|
1698
|
-
return {
|
|
1802
|
+
return {
|
|
1803
|
+
dedupeKey,
|
|
1804
|
+
resumeKey,
|
|
1805
|
+
output,
|
|
1806
|
+
toolMessage,
|
|
1807
|
+
startedAt,
|
|
1808
|
+
completedAt: Date.now(),
|
|
1809
|
+
};
|
|
1699
1810
|
}),
|
|
1700
1811
|
);
|
|
1701
1812
|
|
|
1813
|
+
let ready: ExecutedWebMcpTool[] = [];
|
|
1702
1814
|
try {
|
|
1703
|
-
|
|
1704
|
-
(r): r is { dedupeKey: string; resumeKey: string; output: unknown } =>
|
|
1705
|
-
r !== null,
|
|
1706
|
-
);
|
|
1815
|
+
ready = executed.filter((r): r is ExecutedWebMcpTool => r !== null);
|
|
1707
1816
|
// Everything deduped/aborted/failed — nothing to post.
|
|
1708
1817
|
if (ready.length === 0) return;
|
|
1709
1818
|
|
|
@@ -1723,9 +1832,17 @@ export class AgentWidgetSession {
|
|
|
1723
1832
|
throw new Error(errorData?.error ?? `Resume failed: ${response.status}`);
|
|
1724
1833
|
}
|
|
1725
1834
|
// Server accepted the batch — mark every included call resolved so stale
|
|
1726
|
-
// re-emits don't re-execute the page tool.
|
|
1835
|
+
// re-emits don't re-execute the page tool, then complete each bubble.
|
|
1836
|
+
// Do this only after /resume HTTP success; if /resume fails, the server
|
|
1837
|
+
// may still be paused and the retry path must not show a final result.
|
|
1727
1838
|
for (const r of ready) {
|
|
1728
1839
|
this.webMcpResolvedKeys.add(r.dedupeKey);
|
|
1840
|
+
this.markWebMcpToolComplete(
|
|
1841
|
+
r.toolMessage,
|
|
1842
|
+
r.output,
|
|
1843
|
+
r.startedAt,
|
|
1844
|
+
r.completedAt,
|
|
1845
|
+
);
|
|
1729
1846
|
}
|
|
1730
1847
|
if (response.body) {
|
|
1731
1848
|
await this.connectStream(response.body, { allowReentry: true });
|
|
@@ -1740,6 +1857,14 @@ export class AgentWidgetSession {
|
|
|
1740
1857
|
this.callbacks.onError?.(
|
|
1741
1858
|
error instanceof Error ? error : new Error(String(error)),
|
|
1742
1859
|
);
|
|
1860
|
+
} else {
|
|
1861
|
+
for (const r of ready) {
|
|
1862
|
+
this.markWebMcpToolComplete(
|
|
1863
|
+
r.toolMessage,
|
|
1864
|
+
buildWebMcpErrorResult("Aborted by cancel()"),
|
|
1865
|
+
r.startedAt,
|
|
1866
|
+
);
|
|
1867
|
+
}
|
|
1743
1868
|
}
|
|
1744
1869
|
} finally {
|
|
1745
1870
|
for (const key of claimedKeys) {
|
|
@@ -1842,14 +1967,10 @@ export class AgentWidgetSession {
|
|
|
1842
1967
|
|
|
1843
1968
|
// Mark resolved on the message so the UI's local-tool sheet (if any
|
|
1844
1969
|
// generic one ever lands) does not show — this is a fully-automatic
|
|
1845
|
-
// tool from the user's perspective, modulo the confirm bubble.
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
...toolMessage.agentMetadata,
|
|
1850
|
-
awaitingLocalTool: false,
|
|
1851
|
-
},
|
|
1852
|
-
});
|
|
1970
|
+
// tool from the user's perspective, modulo the confirm bubble. Keep the
|
|
1971
|
+
// tool bubble running until the browser-side promise resolves; the
|
|
1972
|
+
// initial step_await was only the server pause, not tool completion.
|
|
1973
|
+
const startedAt = this.markWebMcpToolRunning(toolMessage);
|
|
1853
1974
|
|
|
1854
1975
|
// Per-resolve AbortController, NOT the shared `this.abortController`.
|
|
1855
1976
|
// A single turn can produce multiple `webmcp:*` step_await messages —
|
|
@@ -1877,6 +1998,8 @@ export class AgentWidgetSession {
|
|
|
1877
1998
|
signal,
|
|
1878
1999
|
);
|
|
1879
2000
|
|
|
2001
|
+
let phase: "execute" | "resume" = "execute";
|
|
2002
|
+
let completedAt = startedAt;
|
|
1880
2003
|
try {
|
|
1881
2004
|
let resumeOutput: unknown;
|
|
1882
2005
|
if (!execPromise) {
|
|
@@ -1891,6 +2014,7 @@ export class AgentWidgetSession {
|
|
|
1891
2014
|
} else {
|
|
1892
2015
|
resumeOutput = await execPromise;
|
|
1893
2016
|
}
|
|
2017
|
+
completedAt = Date.now();
|
|
1894
2018
|
// If cancel() fired during execute, the bridge returned an aborted
|
|
1895
2019
|
// result — don't post it. The server's SSE has been torn down; a
|
|
1896
2020
|
// /resume now would just produce an orphan dispatch on the server.
|
|
@@ -1898,6 +2022,11 @@ export class AgentWidgetSession {
|
|
|
1898
2022
|
// the resolve set) so we don't clobber a sibling resolve or a live
|
|
1899
2023
|
// dispatch's controller here.
|
|
1900
2024
|
if (signal.aborted) {
|
|
2025
|
+
this.markWebMcpToolComplete(
|
|
2026
|
+
toolMessage,
|
|
2027
|
+
buildWebMcpErrorResult("Aborted by cancel()"),
|
|
2028
|
+
startedAt,
|
|
2029
|
+
);
|
|
1901
2030
|
return;
|
|
1902
2031
|
}
|
|
1903
2032
|
// Mark resolved as soon as the HTTP /resume returns OK — not after the
|
|
@@ -1913,9 +2042,16 @@ export class AgentWidgetSession {
|
|
|
1913
2042
|
// call (only same-tool PARALLEL calls could collide on the name).
|
|
1914
2043
|
const resumeKey =
|
|
1915
2044
|
toolMessage.agentMetadata?.webMcpToolCallId ?? wireToolName;
|
|
2045
|
+
phase = "resume";
|
|
1916
2046
|
await this.resumeWithToolOutput(executionId, resumeKey, resumeOutput, {
|
|
1917
2047
|
onHttpOk: () => {
|
|
1918
2048
|
this.webMcpResolvedKeys.add(dedupeKey);
|
|
2049
|
+
this.markWebMcpToolComplete(
|
|
2050
|
+
toolMessage,
|
|
2051
|
+
resumeOutput,
|
|
2052
|
+
startedAt,
|
|
2053
|
+
completedAt,
|
|
2054
|
+
);
|
|
1919
2055
|
},
|
|
1920
2056
|
signal,
|
|
1921
2057
|
});
|
|
@@ -1928,6 +2064,17 @@ export class AgentWidgetSession {
|
|
|
1928
2064
|
// Streaming/teardown handled by the shared `finally` (gated on the
|
|
1929
2065
|
// resolve set) — do NOT null the shared `this.abortController` here; it
|
|
1930
2066
|
// may belong to a live dispatch or sibling resolve, not to us.
|
|
2067
|
+
if (phase === "execute" || isAbortError || signal.aborted) {
|
|
2068
|
+
this.markWebMcpToolComplete(
|
|
2069
|
+
toolMessage,
|
|
2070
|
+
buildWebMcpErrorResult(
|
|
2071
|
+
isAbortError || signal.aborted
|
|
2072
|
+
? "Aborted by cancel()"
|
|
2073
|
+
: getWebMcpErrorMessage(error),
|
|
2074
|
+
),
|
|
2075
|
+
startedAt,
|
|
2076
|
+
);
|
|
2077
|
+
}
|
|
1931
2078
|
if (!isAbortError) {
|
|
1932
2079
|
// The bridge normalizes tool errors into result objects, so reaching
|
|
1933
2080
|
// here means a network failure during `/resume` itself, OR a stream
|
|
@@ -2504,9 +2651,11 @@ export class AgentWidgetSession {
|
|
|
2504
2651
|
// WebMCP equivalent: once a `webmcp:*` tool has started resolving
|
|
2505
2652
|
// (inflight) or resolved, a duplicate `step_await` re-emit must not
|
|
2506
2653
|
// flip `awaitingLocalTool` back to true and resurrect the "waiting on
|
|
2507
|
-
// local tool" UI.
|
|
2508
|
-
//
|
|
2509
|
-
//
|
|
2654
|
+
// local tool" UI. It also must not overwrite an existing running or
|
|
2655
|
+
// completed toolCall with the fresh running skeleton emitted by client.ts
|
|
2656
|
+
// for every step_await. resolveWebMcpToolCall's dedupe path returns
|
|
2657
|
+
// without re-touching the message, so correct the merge here (also avoids
|
|
2658
|
+
// a one-frame flash before that microtask runs).
|
|
2510
2659
|
const reTcName = withSequence.toolCall?.name;
|
|
2511
2660
|
const reExecId = withSequence.agentMetadata?.executionId;
|
|
2512
2661
|
const reTcId = withSequence.toolCall?.id;
|
|
@@ -2518,14 +2667,26 @@ export class AgentWidgetSession {
|
|
|
2518
2667
|
withSequence.agentMetadata?.awaitingLocalTool === true
|
|
2519
2668
|
) {
|
|
2520
2669
|
const reKey = `${reExecId}:${reTcId}`;
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2670
|
+
const isInflight = this.webMcpInflightKeys.has(reKey);
|
|
2671
|
+
const isResolved = this.webMcpResolvedKeys.has(reKey);
|
|
2672
|
+
const existingToolName = existing.toolCall?.name;
|
|
2673
|
+
const hasCompletedTool =
|
|
2674
|
+
existing.agentMetadata?.executionId === reExecId &&
|
|
2675
|
+
existing.toolCall?.id === reTcId &&
|
|
2676
|
+
existingToolName !== undefined &&
|
|
2677
|
+
isWebMcpToolName(existingToolName) &&
|
|
2678
|
+
existing.toolCall?.status === "complete";
|
|
2679
|
+
if (isInflight || isResolved || hasCompletedTool) {
|
|
2525
2680
|
merged.agentMetadata = {
|
|
2526
2681
|
...(merged.agentMetadata ?? {}),
|
|
2527
2682
|
awaitingLocalTool: false,
|
|
2528
2683
|
};
|
|
2684
|
+
// Preserve the in-flight/completed tool state. For in-flight calls,
|
|
2685
|
+
// this keeps the original `startedAt`; for completed calls, it keeps
|
|
2686
|
+
// the measured duration/result even when the call completed with a
|
|
2687
|
+
// local/browser error and therefore was not promoted to resolved.
|
|
2688
|
+
merged.toolCall = existing.toolCall;
|
|
2689
|
+
merged.streaming = existing.streaming;
|
|
2529
2690
|
}
|
|
2530
2691
|
}
|
|
2531
2692
|
return merged;
|