@runtypelabs/persona 3.29.0 → 3.30.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 +55 -0
- 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 +6 -6
- package/dist/codegen.js +4 -4
- package/dist/index.cjs +47 -47
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +127 -7
- package/dist/index.d.ts +127 -7
- package/dist/index.global.js +60 -60
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +47 -47
- package/dist/index.js.map +1 -1
- package/dist/launcher.global.js +2 -2
- package/dist/launcher.global.js.map +1 -1
- package/dist/plugin-kit.cjs +1 -0
- package/dist/plugin-kit.d.cts +98 -0
- package/dist/plugin-kit.d.ts +98 -0
- package/dist/plugin-kit.js +1 -0
- package/dist/smart-dom-reader.d.cts +113 -4
- package/dist/smart-dom-reader.d.ts +113 -4
- package/dist/theme-editor.cjs +38 -38
- package/dist/theme-editor.d.cts +117 -6
- package/dist/theme-editor.d.ts +117 -6
- package/dist/theme-editor.js +38 -38
- package/dist/theme-reference.cjs +1 -1
- package/dist/theme-reference.d.cts +2 -0
- package/dist/theme-reference.d.ts +2 -0
- package/dist/theme-reference.js +1 -1
- package/package.json +8 -2
- package/src/client.test.ts +40 -0
- package/src/client.ts +18 -4
- package/src/components/approval-bubble.test.ts +268 -0
- package/src/components/approval-bubble.ts +170 -20
- package/src/components/launcher.ts +6 -3
- package/src/components/tool-bubble.test.ts +39 -0
- package/src/components/tool-bubble.ts +4 -0
- package/src/generated/runtype-openapi-contract.ts +12 -0
- package/src/index-core.ts +1 -0
- package/src/plugin-kit.test.ts +230 -0
- package/src/plugin-kit.ts +294 -0
- package/src/plugins/types.ts +49 -2
- package/src/session.test.ts +99 -0
- package/src/session.ts +204 -32
- package/src/session.webmcp.test.ts +326 -6
- package/src/theme-editor/preview-utils.test.ts +10 -0
- package/src/theme-editor/preview-utils.ts +29 -1
- package/src/theme-editor/sections.test.ts +17 -0
- package/src/theme-editor/sections.ts +31 -0
- package/src/theme-reference.ts +1 -1
- package/src/types/theme.ts +2 -0
- package/src/types.ts +59 -2
- package/src/ui.approval-plugin.test.ts +204 -0
- package/src/ui.ts +149 -16
- package/src/utils/theme.test.ts +6 -2
- package/src/utils/theme.ts +0 -8
- package/src/utils/tokens.ts +6 -1
- package/src/webmcp-bridge.test.ts +66 -0
- package/src/webmcp-bridge.ts +49 -0
package/src/session.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
AgentWidgetEvent,
|
|
6
6
|
AgentWidgetMessage,
|
|
7
7
|
AgentWidgetApproval,
|
|
8
|
+
AgentWidgetApprovalDecisionOptions,
|
|
8
9
|
WebMcpConfirmInfo,
|
|
9
10
|
AgentExecutionState,
|
|
10
11
|
ClientSession,
|
|
@@ -76,6 +77,20 @@ function buildDispatchErrorContent(
|
|
|
76
77
|
return err.message ? `${base}\n\n_Details: ${err.message}_` : base;
|
|
77
78
|
}
|
|
78
79
|
|
|
80
|
+
const buildWebMcpErrorResult = (message: string) => ({
|
|
81
|
+
isError: true,
|
|
82
|
+
content: [{ type: "text" as const, text: message }],
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const getWebMcpErrorMessage = (
|
|
86
|
+
error: unknown,
|
|
87
|
+
fallback = "WebMCP tool execution failed.",
|
|
88
|
+
): string => {
|
|
89
|
+
if (error instanceof Error && error.message) return error.message;
|
|
90
|
+
if (typeof error === "string" && error) return error;
|
|
91
|
+
return fallback;
|
|
92
|
+
};
|
|
93
|
+
|
|
79
94
|
export class AgentWidgetSession {
|
|
80
95
|
private client: AgentWidgetClient;
|
|
81
96
|
private messages: AgentWidgetMessage[];
|
|
@@ -1182,7 +1197,8 @@ export class AgentWidgetSession {
|
|
|
1182
1197
|
*/
|
|
1183
1198
|
public async resolveApproval(
|
|
1184
1199
|
approval: AgentWidgetApproval,
|
|
1185
|
-
decision: 'approved' | 'denied'
|
|
1200
|
+
decision: 'approved' | 'denied',
|
|
1201
|
+
options?: AgentWidgetApprovalDecisionOptions
|
|
1186
1202
|
): Promise<void> {
|
|
1187
1203
|
// 1. Update approval message status immediately for responsive UI
|
|
1188
1204
|
const approvalMessageId = `approval-${approval.id}`;
|
|
@@ -1191,11 +1207,19 @@ export class AgentWidgetSession {
|
|
|
1191
1207
|
status: decision,
|
|
1192
1208
|
resolvedAt: Date.now(),
|
|
1193
1209
|
};
|
|
1210
|
+
// Anchor the bubble where the agent paused for permission. An approval is a
|
|
1211
|
+
// timeline checkpoint, not a "now" event, so resolving it must preserve the
|
|
1212
|
+
// original message's createdAt/sequence — otherwise sortMessages (which
|
|
1213
|
+
// orders by createdAt first) would re-stamp it to now and float it past any
|
|
1214
|
+
// message created later (e.g. a long-pending approval resolved after more
|
|
1215
|
+
// conversation, or restored/replayed transcripts).
|
|
1216
|
+
const existing = this.messages.find((m) => m.id === approvalMessageId);
|
|
1194
1217
|
const updatedMessage: AgentWidgetMessage = {
|
|
1195
1218
|
id: approvalMessageId,
|
|
1196
1219
|
role: "assistant",
|
|
1197
1220
|
content: "",
|
|
1198
|
-
createdAt: new Date().toISOString(),
|
|
1221
|
+
createdAt: existing?.createdAt ?? new Date().toISOString(),
|
|
1222
|
+
...(existing?.sequence !== undefined ? { sequence: existing.sequence } : {}),
|
|
1199
1223
|
streaming: false,
|
|
1200
1224
|
variant: "approval",
|
|
1201
1225
|
approval: updatedApproval,
|
|
@@ -1224,7 +1248,8 @@ export class AgentWidgetSession {
|
|
|
1224
1248
|
agentId: approval.agentId,
|
|
1225
1249
|
toolName: approval.toolName,
|
|
1226
1250
|
},
|
|
1227
|
-
decision
|
|
1251
|
+
decision,
|
|
1252
|
+
options
|
|
1228
1253
|
);
|
|
1229
1254
|
} else {
|
|
1230
1255
|
response = await this.client.resolveApproval(
|
|
@@ -1595,6 +1620,78 @@ export class AgentWidgetSession {
|
|
|
1595
1620
|
}
|
|
1596
1621
|
}
|
|
1597
1622
|
|
|
1623
|
+
private resolveWebMcpToolStartedAt(
|
|
1624
|
+
toolMessage: AgentWidgetMessage,
|
|
1625
|
+
): number {
|
|
1626
|
+
const stored = this.messages.find((m) => m.id === toolMessage.id);
|
|
1627
|
+
const candidates = [
|
|
1628
|
+
stored?.toolCall?.startedAt,
|
|
1629
|
+
toolMessage.toolCall?.startedAt,
|
|
1630
|
+
];
|
|
1631
|
+
for (const candidate of candidates) {
|
|
1632
|
+
if (typeof candidate === "number" && Number.isFinite(candidate)) {
|
|
1633
|
+
return candidate;
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
return Date.now();
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
private markWebMcpToolRunning(
|
|
1640
|
+
toolMessage: AgentWidgetMessage,
|
|
1641
|
+
): number {
|
|
1642
|
+
const startedAt = this.resolveWebMcpToolStartedAt(toolMessage);
|
|
1643
|
+
this.upsertMessage({
|
|
1644
|
+
...toolMessage,
|
|
1645
|
+
streaming: true,
|
|
1646
|
+
agentMetadata: {
|
|
1647
|
+
...toolMessage.agentMetadata,
|
|
1648
|
+
awaitingLocalTool: false,
|
|
1649
|
+
},
|
|
1650
|
+
toolCall: toolMessage.toolCall
|
|
1651
|
+
? {
|
|
1652
|
+
...toolMessage.toolCall,
|
|
1653
|
+
status: "running",
|
|
1654
|
+
startedAt,
|
|
1655
|
+
completedAt: undefined,
|
|
1656
|
+
duration: undefined,
|
|
1657
|
+
durationMs: undefined,
|
|
1658
|
+
}
|
|
1659
|
+
: toolMessage.toolCall,
|
|
1660
|
+
});
|
|
1661
|
+
return startedAt;
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
private markWebMcpToolComplete(
|
|
1665
|
+
toolMessage: AgentWidgetMessage,
|
|
1666
|
+
result: unknown,
|
|
1667
|
+
startedAt: number,
|
|
1668
|
+
completedAt = Date.now(),
|
|
1669
|
+
): void {
|
|
1670
|
+
// A teardown such as clearMessages()/hydrateMessages()/new send can remove
|
|
1671
|
+
// the bubble while an aborted WebMCP promise is settling. Never resurrect a
|
|
1672
|
+
// cleared message just to mark the old resolve complete.
|
|
1673
|
+
if (!this.messages.some((message) => message.id === toolMessage.id)) return;
|
|
1674
|
+
this.upsertMessage({
|
|
1675
|
+
...toolMessage,
|
|
1676
|
+
streaming: false,
|
|
1677
|
+
agentMetadata: {
|
|
1678
|
+
...toolMessage.agentMetadata,
|
|
1679
|
+
awaitingLocalTool: false,
|
|
1680
|
+
},
|
|
1681
|
+
toolCall: toolMessage.toolCall
|
|
1682
|
+
? {
|
|
1683
|
+
...toolMessage.toolCall,
|
|
1684
|
+
status: "complete",
|
|
1685
|
+
result,
|
|
1686
|
+
startedAt,
|
|
1687
|
+
completedAt,
|
|
1688
|
+
duration: undefined,
|
|
1689
|
+
durationMs: Math.max(0, completedAt - startedAt),
|
|
1690
|
+
}
|
|
1691
|
+
: toolMessage.toolCall,
|
|
1692
|
+
});
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1598
1695
|
/**
|
|
1599
1696
|
* Resolve TWO OR MORE parallel local-tool awaits sharing one paused
|
|
1600
1697
|
* executionId with a SINGLE `/resume` (core#3878). Each call is executed
|
|
@@ -1613,6 +1710,14 @@ export class AgentWidgetSession {
|
|
|
1613
1710
|
executionId: string,
|
|
1614
1711
|
snapshots: AgentWidgetMessage[],
|
|
1615
1712
|
): Promise<void> {
|
|
1713
|
+
type ExecutedWebMcpTool = {
|
|
1714
|
+
dedupeKey: string;
|
|
1715
|
+
resumeKey: string;
|
|
1716
|
+
output: unknown;
|
|
1717
|
+
toolMessage: AgentWidgetMessage;
|
|
1718
|
+
startedAt: number;
|
|
1719
|
+
completedAt: number;
|
|
1720
|
+
};
|
|
1616
1721
|
const claimedKeys: string[] = [];
|
|
1617
1722
|
const controllers: AbortController[] = [];
|
|
1618
1723
|
// Dedicated controller for the shared resume fetch so cancel() can abort it
|
|
@@ -1640,14 +1745,10 @@ export class AgentWidgetSession {
|
|
|
1640
1745
|
this.webMcpInflightKeys.add(dedupeKey);
|
|
1641
1746
|
claimedKeys.push(dedupeKey);
|
|
1642
1747
|
|
|
1643
|
-
// Clear the awaiting flag
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
...toolMessage.agentMetadata,
|
|
1648
|
-
awaitingLocalTool: false,
|
|
1649
|
-
},
|
|
1650
|
-
});
|
|
1748
|
+
// Clear the awaiting flag and keep the tool bubble running while the
|
|
1749
|
+
// browser-side WebMCP promise is in flight. The initial `step_await`
|
|
1750
|
+
// only means the server paused for a local tool; it is not completion.
|
|
1751
|
+
const startedAt = this.markWebMcpToolRunning(toolMessage);
|
|
1651
1752
|
|
|
1652
1753
|
const controller = new AbortController();
|
|
1653
1754
|
this.webMcpResolveControllers.add(controller);
|
|
@@ -1686,24 +1787,43 @@ export class AgentWidgetSession {
|
|
|
1686
1787
|
error instanceof Error ? error : new Error(String(error)),
|
|
1687
1788
|
);
|
|
1688
1789
|
}
|
|
1790
|
+
this.markWebMcpToolComplete(
|
|
1791
|
+
toolMessage,
|
|
1792
|
+
buildWebMcpErrorResult(
|
|
1793
|
+
isAbortError
|
|
1794
|
+
? "Aborted by cancel()"
|
|
1795
|
+
: getWebMcpErrorMessage(error),
|
|
1796
|
+
),
|
|
1797
|
+
startedAt,
|
|
1798
|
+
);
|
|
1689
1799
|
// Release the dedupe claim so a re-emit can retry this call.
|
|
1690
1800
|
this.webMcpInflightKeys.delete(dedupeKey);
|
|
1691
1801
|
return null;
|
|
1692
1802
|
}
|
|
1693
1803
|
}
|
|
1694
1804
|
if (controller.signal.aborted) {
|
|
1805
|
+
this.markWebMcpToolComplete(
|
|
1806
|
+
toolMessage,
|
|
1807
|
+
buildWebMcpErrorResult("Aborted by cancel()"),
|
|
1808
|
+
startedAt,
|
|
1809
|
+
);
|
|
1695
1810
|
this.webMcpInflightKeys.delete(dedupeKey);
|
|
1696
1811
|
return null;
|
|
1697
1812
|
}
|
|
1698
|
-
return {
|
|
1813
|
+
return {
|
|
1814
|
+
dedupeKey,
|
|
1815
|
+
resumeKey,
|
|
1816
|
+
output,
|
|
1817
|
+
toolMessage,
|
|
1818
|
+
startedAt,
|
|
1819
|
+
completedAt: Date.now(),
|
|
1820
|
+
};
|
|
1699
1821
|
}),
|
|
1700
1822
|
);
|
|
1701
1823
|
|
|
1824
|
+
let ready: ExecutedWebMcpTool[] = [];
|
|
1702
1825
|
try {
|
|
1703
|
-
|
|
1704
|
-
(r): r is { dedupeKey: string; resumeKey: string; output: unknown } =>
|
|
1705
|
-
r !== null,
|
|
1706
|
-
);
|
|
1826
|
+
ready = executed.filter((r): r is ExecutedWebMcpTool => r !== null);
|
|
1707
1827
|
// Everything deduped/aborted/failed — nothing to post.
|
|
1708
1828
|
if (ready.length === 0) return;
|
|
1709
1829
|
|
|
@@ -1723,9 +1843,17 @@ export class AgentWidgetSession {
|
|
|
1723
1843
|
throw new Error(errorData?.error ?? `Resume failed: ${response.status}`);
|
|
1724
1844
|
}
|
|
1725
1845
|
// Server accepted the batch — mark every included call resolved so stale
|
|
1726
|
-
// re-emits don't re-execute the page tool.
|
|
1846
|
+
// re-emits don't re-execute the page tool, then complete each bubble.
|
|
1847
|
+
// Do this only after /resume HTTP success; if /resume fails, the server
|
|
1848
|
+
// may still be paused and the retry path must not show a final result.
|
|
1727
1849
|
for (const r of ready) {
|
|
1728
1850
|
this.webMcpResolvedKeys.add(r.dedupeKey);
|
|
1851
|
+
this.markWebMcpToolComplete(
|
|
1852
|
+
r.toolMessage,
|
|
1853
|
+
r.output,
|
|
1854
|
+
r.startedAt,
|
|
1855
|
+
r.completedAt,
|
|
1856
|
+
);
|
|
1729
1857
|
}
|
|
1730
1858
|
if (response.body) {
|
|
1731
1859
|
await this.connectStream(response.body, { allowReentry: true });
|
|
@@ -1740,6 +1868,14 @@ export class AgentWidgetSession {
|
|
|
1740
1868
|
this.callbacks.onError?.(
|
|
1741
1869
|
error instanceof Error ? error : new Error(String(error)),
|
|
1742
1870
|
);
|
|
1871
|
+
} else {
|
|
1872
|
+
for (const r of ready) {
|
|
1873
|
+
this.markWebMcpToolComplete(
|
|
1874
|
+
r.toolMessage,
|
|
1875
|
+
buildWebMcpErrorResult("Aborted by cancel()"),
|
|
1876
|
+
r.startedAt,
|
|
1877
|
+
);
|
|
1878
|
+
}
|
|
1743
1879
|
}
|
|
1744
1880
|
} finally {
|
|
1745
1881
|
for (const key of claimedKeys) {
|
|
@@ -1842,14 +1978,10 @@ export class AgentWidgetSession {
|
|
|
1842
1978
|
|
|
1843
1979
|
// Mark resolved on the message so the UI's local-tool sheet (if any
|
|
1844
1980
|
// 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
|
-
});
|
|
1981
|
+
// tool from the user's perspective, modulo the confirm bubble. Keep the
|
|
1982
|
+
// tool bubble running until the browser-side promise resolves; the
|
|
1983
|
+
// initial step_await was only the server pause, not tool completion.
|
|
1984
|
+
const startedAt = this.markWebMcpToolRunning(toolMessage);
|
|
1853
1985
|
|
|
1854
1986
|
// Per-resolve AbortController, NOT the shared `this.abortController`.
|
|
1855
1987
|
// A single turn can produce multiple `webmcp:*` step_await messages —
|
|
@@ -1877,6 +2009,8 @@ export class AgentWidgetSession {
|
|
|
1877
2009
|
signal,
|
|
1878
2010
|
);
|
|
1879
2011
|
|
|
2012
|
+
let phase: "execute" | "resume" = "execute";
|
|
2013
|
+
let completedAt = startedAt;
|
|
1880
2014
|
try {
|
|
1881
2015
|
let resumeOutput: unknown;
|
|
1882
2016
|
if (!execPromise) {
|
|
@@ -1891,6 +2025,7 @@ export class AgentWidgetSession {
|
|
|
1891
2025
|
} else {
|
|
1892
2026
|
resumeOutput = await execPromise;
|
|
1893
2027
|
}
|
|
2028
|
+
completedAt = Date.now();
|
|
1894
2029
|
// If cancel() fired during execute, the bridge returned an aborted
|
|
1895
2030
|
// result — don't post it. The server's SSE has been torn down; a
|
|
1896
2031
|
// /resume now would just produce an orphan dispatch on the server.
|
|
@@ -1898,6 +2033,11 @@ export class AgentWidgetSession {
|
|
|
1898
2033
|
// the resolve set) so we don't clobber a sibling resolve or a live
|
|
1899
2034
|
// dispatch's controller here.
|
|
1900
2035
|
if (signal.aborted) {
|
|
2036
|
+
this.markWebMcpToolComplete(
|
|
2037
|
+
toolMessage,
|
|
2038
|
+
buildWebMcpErrorResult("Aborted by cancel()"),
|
|
2039
|
+
startedAt,
|
|
2040
|
+
);
|
|
1901
2041
|
return;
|
|
1902
2042
|
}
|
|
1903
2043
|
// Mark resolved as soon as the HTTP /resume returns OK — not after the
|
|
@@ -1913,9 +2053,16 @@ export class AgentWidgetSession {
|
|
|
1913
2053
|
// call (only same-tool PARALLEL calls could collide on the name).
|
|
1914
2054
|
const resumeKey =
|
|
1915
2055
|
toolMessage.agentMetadata?.webMcpToolCallId ?? wireToolName;
|
|
2056
|
+
phase = "resume";
|
|
1916
2057
|
await this.resumeWithToolOutput(executionId, resumeKey, resumeOutput, {
|
|
1917
2058
|
onHttpOk: () => {
|
|
1918
2059
|
this.webMcpResolvedKeys.add(dedupeKey);
|
|
2060
|
+
this.markWebMcpToolComplete(
|
|
2061
|
+
toolMessage,
|
|
2062
|
+
resumeOutput,
|
|
2063
|
+
startedAt,
|
|
2064
|
+
completedAt,
|
|
2065
|
+
);
|
|
1919
2066
|
},
|
|
1920
2067
|
signal,
|
|
1921
2068
|
});
|
|
@@ -1928,6 +2075,17 @@ export class AgentWidgetSession {
|
|
|
1928
2075
|
// Streaming/teardown handled by the shared `finally` (gated on the
|
|
1929
2076
|
// resolve set) — do NOT null the shared `this.abortController` here; it
|
|
1930
2077
|
// may belong to a live dispatch or sibling resolve, not to us.
|
|
2078
|
+
if (phase === "execute" || isAbortError || signal.aborted) {
|
|
2079
|
+
this.markWebMcpToolComplete(
|
|
2080
|
+
toolMessage,
|
|
2081
|
+
buildWebMcpErrorResult(
|
|
2082
|
+
isAbortError || signal.aborted
|
|
2083
|
+
? "Aborted by cancel()"
|
|
2084
|
+
: getWebMcpErrorMessage(error),
|
|
2085
|
+
),
|
|
2086
|
+
startedAt,
|
|
2087
|
+
);
|
|
2088
|
+
}
|
|
1931
2089
|
if (!isAbortError) {
|
|
1932
2090
|
// The bridge normalizes tool errors into result objects, so reaching
|
|
1933
2091
|
// here means a network failure during `/resume` itself, OR a stream
|
|
@@ -2504,9 +2662,11 @@ export class AgentWidgetSession {
|
|
|
2504
2662
|
// WebMCP equivalent: once a `webmcp:*` tool has started resolving
|
|
2505
2663
|
// (inflight) or resolved, a duplicate `step_await` re-emit must not
|
|
2506
2664
|
// flip `awaitingLocalTool` back to true and resurrect the "waiting on
|
|
2507
|
-
// local tool" UI.
|
|
2508
|
-
//
|
|
2509
|
-
//
|
|
2665
|
+
// local tool" UI. It also must not overwrite an existing running or
|
|
2666
|
+
// completed toolCall with the fresh running skeleton emitted by client.ts
|
|
2667
|
+
// for every step_await. resolveWebMcpToolCall's dedupe path returns
|
|
2668
|
+
// without re-touching the message, so correct the merge here (also avoids
|
|
2669
|
+
// a one-frame flash before that microtask runs).
|
|
2510
2670
|
const reTcName = withSequence.toolCall?.name;
|
|
2511
2671
|
const reExecId = withSequence.agentMetadata?.executionId;
|
|
2512
2672
|
const reTcId = withSequence.toolCall?.id;
|
|
@@ -2518,14 +2678,26 @@ export class AgentWidgetSession {
|
|
|
2518
2678
|
withSequence.agentMetadata?.awaitingLocalTool === true
|
|
2519
2679
|
) {
|
|
2520
2680
|
const reKey = `${reExecId}:${reTcId}`;
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2681
|
+
const isInflight = this.webMcpInflightKeys.has(reKey);
|
|
2682
|
+
const isResolved = this.webMcpResolvedKeys.has(reKey);
|
|
2683
|
+
const existingToolName = existing.toolCall?.name;
|
|
2684
|
+
const hasCompletedTool =
|
|
2685
|
+
existing.agentMetadata?.executionId === reExecId &&
|
|
2686
|
+
existing.toolCall?.id === reTcId &&
|
|
2687
|
+
existingToolName !== undefined &&
|
|
2688
|
+
isWebMcpToolName(existingToolName) &&
|
|
2689
|
+
existing.toolCall?.status === "complete";
|
|
2690
|
+
if (isInflight || isResolved || hasCompletedTool) {
|
|
2525
2691
|
merged.agentMetadata = {
|
|
2526
2692
|
...(merged.agentMetadata ?? {}),
|
|
2527
2693
|
awaitingLocalTool: false,
|
|
2528
2694
|
};
|
|
2695
|
+
// Preserve the in-flight/completed tool state. For in-flight calls,
|
|
2696
|
+
// this keeps the original `startedAt`; for completed calls, it keeps
|
|
2697
|
+
// the measured duration/result even when the call completed with a
|
|
2698
|
+
// local/browser error and therefore was not promoted to resolved.
|
|
2699
|
+
merged.toolCall = existing.toolCall;
|
|
2700
|
+
merged.streaming = existing.streaming;
|
|
2529
2701
|
}
|
|
2530
2702
|
}
|
|
2531
2703
|
return merged;
|