@runtypelabs/persona 4.6.1 → 4.8.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/dist/animations/glyph-cycle.cjs +2 -2
- package/dist/animations/glyph-cycle.d.cts +1 -1
- package/dist/animations/glyph-cycle.d.ts +1 -1
- package/dist/animations/glyph-cycle.js +2 -2
- package/dist/animations/{types-CSmiKRVa.d.cts → types-BsZtXPKK.d.cts} +43 -3
- package/dist/animations/{types-CSmiKRVa.d.ts → types-BsZtXPKK.d.ts} +43 -3
- package/dist/animations/wipe.cjs +2 -2
- package/dist/animations/wipe.d.cts +1 -1
- package/dist/animations/wipe.d.ts +1 -1
- package/dist/chunk-5EIIHQLQ.js +1 -0
- package/dist/codegen.cjs +12 -12
- package/dist/codegen.js +14 -14
- package/dist/index.cjs +91 -68
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +918 -174
- package/dist/index.d.ts +918 -174
- package/dist/index.global.js +81 -57
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +89 -66
- package/dist/index.js.map +1 -1
- package/dist/install.global.js +1 -1
- package/dist/install.global.js.map +1 -1
- package/dist/launcher.global.js +3 -2
- package/dist/launcher.global.js.map +1 -1
- package/dist/markdown-parsers.js +24 -24
- package/dist/plugin-kit.cjs +1 -1
- package/dist/plugin-kit.js +1 -1
- package/dist/runtype-tts-entry-UJAEF7NZ.js +1 -0
- package/dist/runtype-tts.js +1 -1
- package/dist/session-reconnect-JKIJBHS5.js +1 -0
- package/dist/smart-dom-reader.cjs +17 -17
- package/dist/smart-dom-reader.d.cts +753 -15
- package/dist/smart-dom-reader.d.ts +753 -15
- package/dist/smart-dom-reader.js +17 -17
- package/dist/testing.cjs +3 -3
- package/dist/testing.js +3 -3
- package/dist/theme-editor-preview.cjs +81 -58
- package/dist/theme-editor-preview.d.cts +761 -15
- package/dist/theme-editor-preview.d.ts +761 -15
- package/dist/theme-editor-preview.js +81 -58
- package/dist/theme-editor.cjs +6 -6
- package/dist/theme-editor.d.cts +753 -15
- package/dist/theme-editor.d.ts +753 -15
- package/dist/theme-editor.js +10 -10
- package/dist/theme-reference.cjs +1 -1
- package/dist/theme-reference.d.cts +74 -0
- package/dist/theme-reference.d.ts +74 -0
- package/dist/theme-reference.js +1 -1
- package/dist/voice-worklet-player.cjs +2 -2
- package/dist/voice-worklet-player.js +2 -2
- package/dist/webmcp-polyfill.js +2 -2
- package/dist/widget.css +1 -1
- package/package.json +2 -3
- package/src/artifacts-session.test.ts +178 -0
- package/src/client.test.ts +500 -1
- package/src/client.ts +285 -93
- package/src/components/artifact-card.test.ts +333 -0
- package/src/components/artifact-card.ts +75 -28
- package/src/components/artifact-inline.test.ts +1328 -0
- package/src/components/artifact-inline.ts +920 -0
- package/src/components/artifact-pane.test.ts +1042 -0
- package/src/components/artifact-pane.ts +440 -131
- package/src/components/artifact-preview.test.ts +1155 -0
- package/src/components/artifact-preview.ts +994 -0
- package/src/components/pill-composer-builder.test.ts +6 -2
- package/src/components/pill-composer-builder.ts +6 -6
- package/src/components/reasoning-bubble.ts +1 -13
- package/src/components/registry.ts +38 -3
- package/src/components/tool-bubble.ts +1 -13
- package/src/defaults.ts +1 -0
- package/src/generated/runtype-openapi-contract.ts +55 -3
- package/src/index-core.ts +20 -1
- package/src/index.ts +8 -0
- package/src/markdown-parsers-loader.test.ts +158 -0
- package/src/markdown-parsers-loader.ts +74 -9
- package/src/runtime/host-layout.test.ts +163 -0
- package/src/runtime/host-layout.ts +110 -7
- package/src/runtime/init.ts +18 -61
- package/src/runtime/persist-state.test.ts +118 -0
- package/src/session.ts +76 -22
- package/src/styles/widget.css +773 -26
- package/src/theme-editor/preview.ts +2 -0
- package/src/theme-editor/sections.test.ts +26 -1
- package/src/theme-editor/sections.ts +10 -2
- package/src/theme-reference.ts +2 -2
- package/src/tool-call-display-defaults.test.ts +1 -0
- package/src/types/theme.ts +77 -0
- package/src/types.ts +516 -17
- package/src/ui.artifact-pane-gating.test.ts +636 -0
- package/src/ui.component-directive.test.ts +104 -0
- package/src/ui.composer-bar.test.ts +60 -2
- package/src/ui.detached-panel.test.ts +1049 -0
- package/src/ui.scroll-additive.test.ts +1 -1
- package/src/ui.streaming-coalescing.test.ts +312 -0
- package/src/ui.tool-display.test.ts +51 -0
- package/src/ui.ts +933 -214
- package/src/utils/artifact-custom-actions.ts +128 -0
- package/src/utils/artifact-display.test.ts +42 -0
- package/src/utils/artifact-display.ts +84 -0
- package/src/utils/artifact-file.test.ts +116 -0
- package/src/utils/artifact-file.ts +117 -0
- package/src/utils/artifact-gate.test.ts +112 -5
- package/src/utils/artifact-gate.ts +39 -14
- package/src/utils/artifact-loading-status.ts +55 -0
- package/src/utils/artifact-status-label.ts +190 -0
- package/src/utils/buttons.ts +7 -1
- package/src/utils/code-highlight.test.ts +186 -0
- package/src/utils/code-highlight.ts +400 -0
- package/src/utils/icons.ts +2 -0
- package/src/utils/roving-tablist.test.ts +152 -0
- package/src/utils/roving-tablist.ts +111 -0
- package/src/utils/spinner.ts +45 -0
- package/src/utils/theme.test.ts +48 -0
- package/src/utils/theme.ts +7 -0
- package/src/utils/tokens.ts +91 -0
- package/src/utils/tool-loading-animation.test.ts +32 -0
- package/src/utils/tool-loading-animation.ts +24 -0
- package/dist/chunk-DFBSCFYN.js +0 -1
- package/dist/runtype-tts-entry-HFUV2UF7.js +0 -1
- package/dist/session-reconnect-U77QFUR7.js +0 -1
package/src/client.ts
CHANGED
|
@@ -17,13 +17,19 @@ import {
|
|
|
17
17
|
ClientSession,
|
|
18
18
|
ClientInitResponse,
|
|
19
19
|
ClientChatRequest,
|
|
20
|
+
ClientToolDefinition,
|
|
20
21
|
ClientFeedbackRequest,
|
|
21
22
|
ClientFeedbackType,
|
|
22
23
|
PersonaArtifactKind,
|
|
24
|
+
PersonaArtifactFileMeta,
|
|
23
25
|
ContentPart,
|
|
24
26
|
WebMcpConfirmHandler
|
|
25
27
|
} from "./types";
|
|
26
28
|
import { WebMcpBridge, computeClientToolsFingerprint, isWebMcpToolName } from "./webmcp-bridge";
|
|
29
|
+
import {
|
|
30
|
+
buildArtifactRefRawContent,
|
|
31
|
+
resolveArtifactDisplayMode
|
|
32
|
+
} from "./utils/artifact-display";
|
|
27
33
|
import { resolveTarget } from "./utils/target";
|
|
28
34
|
import { builtInClientToolsForDispatch } from "./ask-user-question-tool";
|
|
29
35
|
import {
|
|
@@ -180,6 +186,14 @@ export class AgentWidgetClient {
|
|
|
180
186
|
// (silent re-init / expiry) forces a fresh full send.
|
|
181
187
|
private lastSentClientToolsFingerprint: string | null = null;
|
|
182
188
|
private clientToolsFingerprintSessionId: string | null = null;
|
|
189
|
+
// Session under which a non-empty clientTools[] was last committed and not
|
|
190
|
+
// yet confirmed cleared server-side. Distinct from the fingerprint above:
|
|
191
|
+
// an empty-tool chat turn commits a null fingerprint, but OMITTING the
|
|
192
|
+
// fields on chat doesn't clear the tools persisted for a still-paused
|
|
193
|
+
// execution — so a later resume with an empty registry must still send the
|
|
194
|
+
// explicit `clientTools: []` replace. Reset only by an explicit [] replace,
|
|
195
|
+
// a session change, or a conversation reset.
|
|
196
|
+
private sentNonEmptyClientToolsSessionId: string | null = null;
|
|
183
197
|
|
|
184
198
|
// WebMCP: page-discovered tool consumption (see ./webmcp-bridge).
|
|
185
199
|
// Constructed lazily: null when `config.webmcp?.enabled !== true`.
|
|
@@ -438,6 +452,7 @@ export class AgentWidgetClient {
|
|
|
438
452
|
public resetClientToolsFingerprint(): void {
|
|
439
453
|
this.lastSentClientToolsFingerprint = null;
|
|
440
454
|
this.clientToolsFingerprintSessionId = null;
|
|
455
|
+
this.sentNonEmptyClientToolsSessionId = null;
|
|
441
456
|
}
|
|
442
457
|
|
|
443
458
|
/**
|
|
@@ -614,6 +629,7 @@ export class AgentWidgetClient {
|
|
|
614
629
|
* Send a message - handles both proxy and client token modes
|
|
615
630
|
*/
|
|
616
631
|
public async dispatch(options: DispatchOptions, onEvent: SSEHandler) {
|
|
632
|
+
options.signal?.throwIfAborted();
|
|
617
633
|
if (this.isClientTokenMode()) {
|
|
618
634
|
return this.dispatchClientToken(options, onEvent);
|
|
619
635
|
}
|
|
@@ -627,11 +643,6 @@ export class AgentWidgetClient {
|
|
|
627
643
|
* Client token mode dispatch
|
|
628
644
|
*/
|
|
629
645
|
private async dispatchClientToken(options: DispatchOptions, onEvent: SSEHandler) {
|
|
630
|
-
const controller = new AbortController();
|
|
631
|
-
if (options.signal) {
|
|
632
|
-
options.signal.addEventListener("abort", () => controller.abort());
|
|
633
|
-
}
|
|
634
|
-
|
|
635
646
|
onEvent({ type: "status", status: "connecting" });
|
|
636
647
|
|
|
637
648
|
try {
|
|
@@ -678,71 +689,32 @@ export class AgentWidgetClient {
|
|
|
678
689
|
};
|
|
679
690
|
|
|
680
691
|
// Diff-only / send-once WebMCP tool dispatch. `buildPayload()` already
|
|
681
|
-
// snapshotted the full set;
|
|
682
|
-
//
|
|
683
|
-
//
|
|
684
|
-
//
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
hasClientTools && sameSession && this.lastSentClientToolsFingerprint === clientToolsFingerprint;
|
|
694
|
-
|
|
695
|
-
// `forceFull` flips to true after a 409 cache-miss so the single retry
|
|
696
|
-
// resends the full list. Capture any error body read inside the loop so
|
|
697
|
-
// the `!response.ok` handler below doesn't re-consume the stream.
|
|
698
|
-
let forceFull = false;
|
|
699
|
-
let errorData: { error?: string; hint?: string } | null = null;
|
|
700
|
-
let response: Response;
|
|
701
|
-
for (let attempt = 0; ; attempt++) {
|
|
702
|
-
const sendFull = hasClientTools && (forceFull || !unchanged);
|
|
703
|
-
const chatRequest: ClientChatRequest = {
|
|
704
|
-
...baseChatRequest,
|
|
705
|
-
...(sendFull && fullClientTools ? { clientTools: fullClientTools } : {}),
|
|
706
|
-
...(clientToolsFingerprint ? { clientToolsFingerprint } : {}),
|
|
707
|
-
};
|
|
708
|
-
|
|
709
|
-
if (this.debug) {
|
|
710
|
-
// eslint-disable-next-line no-console
|
|
711
|
-
console.debug("[AgentWidgetClient] client token dispatch", chatRequest);
|
|
712
|
-
}
|
|
692
|
+
// snapshotted the full set; `sendWithClientToolsDiff` decides whether to
|
|
693
|
+
// ship it again or just its fingerprint (retrying once on a 409 registry
|
|
694
|
+
// miss). The cache is committed only after a successful stream start
|
|
695
|
+
// (below), so a 409/failure leaves it untouched.
|
|
696
|
+
const { response, commit: commitClientToolsFingerprint } =
|
|
697
|
+
await this.sendWithClientToolsDiff(session.sessionId, basePayload.clientTools, (toolFields) => {
|
|
698
|
+
const chatRequest: ClientChatRequest = { ...baseChatRequest, ...toolFields };
|
|
699
|
+
|
|
700
|
+
if (this.debug) {
|
|
701
|
+
// eslint-disable-next-line no-console
|
|
702
|
+
console.debug("[AgentWidgetClient] client token dispatch", chatRequest);
|
|
703
|
+
}
|
|
713
704
|
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
705
|
+
return fetch(this.getClientApiUrl('chat'), {
|
|
706
|
+
method: 'POST',
|
|
707
|
+
headers: {
|
|
708
|
+
'Content-Type': 'application/json',
|
|
709
|
+
'X-Persona-Version': VERSION,
|
|
710
|
+
},
|
|
711
|
+
body: JSON.stringify(chatRequest),
|
|
712
|
+
signal: options.signal,
|
|
713
|
+
});
|
|
722
714
|
});
|
|
723
715
|
|
|
724
|
-
// Diff-only cache miss: the server has no stored tool set matching our
|
|
725
|
-
// fingerprint. Retry exactly once with the full list. A second miss
|
|
726
|
-
// falls through to the normal error handling below (no infinite loop).
|
|
727
|
-
if (response.status === 409 && attempt === 0 && hasClientTools) {
|
|
728
|
-
const body = (await response.json().catch(() => null)) as
|
|
729
|
-
| { error?: string; hint?: string }
|
|
730
|
-
| null;
|
|
731
|
-
if (body?.error === 'client_tools_resend_required') {
|
|
732
|
-
forceFull = true;
|
|
733
|
-
// Invalidate so future turns also resend until a clean success
|
|
734
|
-
// commits a fresh fingerprint.
|
|
735
|
-
this.lastSentClientToolsFingerprint = null;
|
|
736
|
-
continue;
|
|
737
|
-
}
|
|
738
|
-
// Some other 409: keep the parsed body for the handler below.
|
|
739
|
-
errorData = body ?? { error: 'Chat request failed' };
|
|
740
|
-
}
|
|
741
|
-
break;
|
|
742
|
-
}
|
|
743
|
-
|
|
744
716
|
if (!response.ok) {
|
|
745
|
-
const data =
|
|
717
|
+
const data = await response.json().catch(() => ({ error: 'Chat request failed' }));
|
|
746
718
|
|
|
747
719
|
if (response.status === 401) {
|
|
748
720
|
// Session expired
|
|
@@ -773,8 +745,7 @@ export class AgentWidgetClient {
|
|
|
773
745
|
// Stream is good: the server now holds this tool set under this
|
|
774
746
|
// fingerprint for the session. Commit the cache so unchanged follow-up
|
|
775
747
|
// turns can send fingerprint-only.
|
|
776
|
-
|
|
777
|
-
this.clientToolsFingerprintSessionId = session.sessionId;
|
|
748
|
+
commitClientToolsFingerprint();
|
|
778
749
|
|
|
779
750
|
onEvent({ type: "status", status: "connected" });
|
|
780
751
|
|
|
@@ -798,11 +769,6 @@ export class AgentWidgetClient {
|
|
|
798
769
|
* Proxy mode dispatch (original implementation)
|
|
799
770
|
*/
|
|
800
771
|
private async dispatchProxy(options: DispatchOptions, onEvent: SSEHandler) {
|
|
801
|
-
const controller = new AbortController();
|
|
802
|
-
if (options.signal) {
|
|
803
|
-
options.signal.addEventListener("abort", () => controller.abort());
|
|
804
|
-
}
|
|
805
|
-
|
|
806
772
|
onEvent({ type: "status", status: "connecting" });
|
|
807
773
|
|
|
808
774
|
const payload = await this.buildPayload(options.messages);
|
|
@@ -836,7 +802,7 @@ export class AgentWidgetClient {
|
|
|
836
802
|
method: "POST",
|
|
837
803
|
headers,
|
|
838
804
|
body: JSON.stringify(payload),
|
|
839
|
-
signal:
|
|
805
|
+
signal: options.signal
|
|
840
806
|
},
|
|
841
807
|
payload
|
|
842
808
|
);
|
|
@@ -850,7 +816,7 @@ export class AgentWidgetClient {
|
|
|
850
816
|
method: "POST",
|
|
851
817
|
headers,
|
|
852
818
|
body: JSON.stringify(payload),
|
|
853
|
-
signal:
|
|
819
|
+
signal: options.signal
|
|
854
820
|
});
|
|
855
821
|
}
|
|
856
822
|
|
|
@@ -874,11 +840,6 @@ export class AgentWidgetClient {
|
|
|
874
840
|
* Agent mode dispatch
|
|
875
841
|
*/
|
|
876
842
|
private async dispatchAgent(options: DispatchOptions, onEvent: SSEHandler) {
|
|
877
|
-
const controller = new AbortController();
|
|
878
|
-
if (options.signal) {
|
|
879
|
-
options.signal.addEventListener("abort", () => controller.abort());
|
|
880
|
-
}
|
|
881
|
-
|
|
882
843
|
onEvent({ type: "status", status: "connecting" });
|
|
883
844
|
|
|
884
845
|
const payload = await this.buildAgentPayload(options.messages);
|
|
@@ -912,7 +873,7 @@ export class AgentWidgetClient {
|
|
|
912
873
|
method: "POST",
|
|
913
874
|
headers,
|
|
914
875
|
body: JSON.stringify(payload),
|
|
915
|
-
signal:
|
|
876
|
+
signal: options.signal
|
|
916
877
|
},
|
|
917
878
|
payload as unknown as AgentWidgetRequestPayload
|
|
918
879
|
);
|
|
@@ -926,7 +887,7 @@ export class AgentWidgetClient {
|
|
|
926
887
|
method: "POST",
|
|
927
888
|
headers,
|
|
928
889
|
body: JSON.stringify(payload),
|
|
929
|
-
signal:
|
|
890
|
+
signal: options.signal
|
|
930
891
|
});
|
|
931
892
|
}
|
|
932
893
|
|
|
@@ -1007,8 +968,13 @@ export class AgentWidgetClient {
|
|
|
1007
968
|
* - **client-token mode**: POST `${apiBase}/v1/client/resume` (the
|
|
1008
969
|
* session-authenticated sibling of `/v1/client/chat`; runtypelabs/core#3889),
|
|
1009
970
|
* with the active `sessionId` in the body and no Bearer key: a browser
|
|
1010
|
-
* client-token page holds no secret.
|
|
1011
|
-
*
|
|
971
|
+
* client-token page holds no secret. The page's tool registry is
|
|
972
|
+
* re-snapshotted and sent alongside `toolOutputs` via the same diff-only
|
|
973
|
+
* `clientTools` / `clientToolsFingerprint` protocol as `/v1/client/chat`
|
|
974
|
+
* (runtypelabs/core#5361), so tools registered by a mid-run page
|
|
975
|
+
* navigation replace the run's dispatch-time set and become callable on
|
|
976
|
+
* the next model turn. Old servers strip the unknown fields and keep the
|
|
977
|
+
* frozen-at-dispatch behavior.
|
|
1012
978
|
* - **dispatch / proxy mode**: POST `${apiUrl}/resume`: Runtype mounts
|
|
1013
979
|
* resume as a child of `/v1/dispatch`, so the URL is `${apiUrl}/resume`,
|
|
1014
980
|
* and proxies follow the same shape (`/api/chat/dispatch/resume`).
|
|
@@ -1020,6 +986,113 @@ export class AgentWidgetClient {
|
|
|
1020
986
|
* @param toolOutputs - Map keyed by per-call `toolCallId` (core#3878),
|
|
1021
987
|
* falling back to tool name for legacy servers → the tool's result value.
|
|
1022
988
|
*/
|
|
989
|
+
/**
|
|
990
|
+
* Diff-only / send-once WebMCP clientTools transport, shared by the
|
|
991
|
+
* client-token chat (`/v1/client/chat`) and resume (`/v1/client/resume`)
|
|
992
|
+
* paths — both routes speak the same protocol (runtypelabs/core#5361).
|
|
993
|
+
*
|
|
994
|
+
* Decides, against the shared fingerprint cache, whether this request ships
|
|
995
|
+
* the full `clientTools[]` + fingerprint (first send under this session, or
|
|
996
|
+
* a changed set) or the fingerprint alone (unchanged set; the server reuses
|
|
997
|
+
* its stored copy). Runs `doFetch` with the chosen fields and retries
|
|
998
|
+
* EXACTLY once with the full array on a
|
|
999
|
+
* `409 { error: 'client_tools_resend_required' }` registry miss — the retry
|
|
1000
|
+
* is 409-*triggered*, never 409-*expected*, so servers predating the
|
|
1001
|
+
* protocol (which strip the unknown fields and never 409) work unchanged.
|
|
1002
|
+
* The 409 body is probed on a `clone()` so the original response body stays
|
|
1003
|
+
* readable by the caller's error handling.
|
|
1004
|
+
*
|
|
1005
|
+
* The cache is NOT committed here: callers invoke the returned `commit()`
|
|
1006
|
+
* only after the server has accepted the request (response OK / stream
|
|
1007
|
+
* started), so a failed request can never record a fingerprint the server
|
|
1008
|
+
* never stored. A resend-required miss invalidates the cached fingerprint
|
|
1009
|
+
* immediately so later turns keep resending in full until a clean success
|
|
1010
|
+
* commits a fresh one.
|
|
1011
|
+
*
|
|
1012
|
+
* `emptyMeansReplace` (resume only): when the live snapshot is empty but a
|
|
1013
|
+
* non-empty set was committed under this session and never explicitly
|
|
1014
|
+
* cleared (the paused tool navigated to a page with no tool registry), ship
|
|
1015
|
+
* an explicit `clientTools: []` so the server REPLACES the persisted
|
|
1016
|
+
* dispatch-time set with nothing and clears its stored registry. Chat keeps
|
|
1017
|
+
* its omit-when-empty behavior: on `/chat`, absent fields already mean "no
|
|
1018
|
+
* tools this turn", whereas on `/resume` absence means "keep the frozen
|
|
1019
|
+
* dispatch-time set".
|
|
1020
|
+
*/
|
|
1021
|
+
private async sendWithClientToolsDiff(
|
|
1022
|
+
sessionId: string,
|
|
1023
|
+
fullClientTools: ClientToolDefinition[] | undefined,
|
|
1024
|
+
doFetch: (
|
|
1025
|
+
toolFields: Pick<ClientChatRequest, 'clientTools' | 'clientToolsFingerprint'>
|
|
1026
|
+
) => Promise<Response>,
|
|
1027
|
+
opts?: { emptyMeansReplace?: boolean }
|
|
1028
|
+
): Promise<{ response: Response; commit: () => void }> {
|
|
1029
|
+
const hasClientTools = !!(fullClientTools && fullClientTools.length > 0);
|
|
1030
|
+
const clientToolsFingerprint = hasClientTools
|
|
1031
|
+
? computeClientToolsFingerprint(fullClientTools!)
|
|
1032
|
+
: undefined;
|
|
1033
|
+
const sameSession = this.clientToolsFingerprintSessionId === sessionId;
|
|
1034
|
+
const unchanged =
|
|
1035
|
+
hasClientTools && sameSession && this.lastSentClientToolsFingerprint === clientToolsFingerprint;
|
|
1036
|
+
// Keyed on `sentNonEmptyClientToolsSessionId`, NOT the fingerprint: an
|
|
1037
|
+
// interleaved empty-tool chat turn commits a null fingerprint without
|
|
1038
|
+
// clearing the tools persisted for a still-paused execution, so the
|
|
1039
|
+
// fingerprint alone would lose the pending clear. The dedicated flag
|
|
1040
|
+
// survives omitted-empty commits and is reset only once an explicit []
|
|
1041
|
+
// replace is confirmed.
|
|
1042
|
+
const sendEmptyReplace =
|
|
1043
|
+
!hasClientTools &&
|
|
1044
|
+
opts?.emptyMeansReplace === true &&
|
|
1045
|
+
this.sentNonEmptyClientToolsSessionId === sessionId;
|
|
1046
|
+
|
|
1047
|
+
// `forceFull` flips to true after a 409 cache-miss so the single retry
|
|
1048
|
+
// resends the full list.
|
|
1049
|
+
let forceFull = false;
|
|
1050
|
+
let response: Response;
|
|
1051
|
+
for (let attempt = 0; ; attempt++) {
|
|
1052
|
+
const sendFull = hasClientTools && (forceFull || !unchanged);
|
|
1053
|
+
response = await doFetch({
|
|
1054
|
+
...(sendFull && fullClientTools ? { clientTools: fullClientTools } : {}),
|
|
1055
|
+
...(sendEmptyReplace ? { clientTools: [] } : {}),
|
|
1056
|
+
...(clientToolsFingerprint ? { clientToolsFingerprint } : {}),
|
|
1057
|
+
});
|
|
1058
|
+
|
|
1059
|
+
// Diff-only cache miss: the server has no stored tool set matching our
|
|
1060
|
+
// fingerprint. Retry exactly once with the full list. A second miss
|
|
1061
|
+
// falls through to the caller's normal error handling (no infinite loop).
|
|
1062
|
+
if (response.status === 409 && attempt === 0 && hasClientTools) {
|
|
1063
|
+
const body = (await response
|
|
1064
|
+
.clone()
|
|
1065
|
+
.json()
|
|
1066
|
+
.catch(() => null)) as { error?: string } | null;
|
|
1067
|
+
if (body?.error === 'client_tools_resend_required') {
|
|
1068
|
+
forceFull = true;
|
|
1069
|
+
// Invalidate so future turns also resend until a clean success
|
|
1070
|
+
// commits a fresh fingerprint.
|
|
1071
|
+
this.lastSentClientToolsFingerprint = null;
|
|
1072
|
+
continue;
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
break;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
return {
|
|
1079
|
+
response,
|
|
1080
|
+
commit: () => {
|
|
1081
|
+
this.lastSentClientToolsFingerprint = clientToolsFingerprint ?? null;
|
|
1082
|
+
this.clientToolsFingerprintSessionId = sessionId;
|
|
1083
|
+
if (hasClientTools) {
|
|
1084
|
+
this.sentNonEmptyClientToolsSessionId = sessionId;
|
|
1085
|
+
} else if (sendEmptyReplace) {
|
|
1086
|
+
// The explicit [] replaced the persisted set server-side; the
|
|
1087
|
+
// pending clear is done.
|
|
1088
|
+
this.sentNonEmptyClientToolsSessionId = null;
|
|
1089
|
+
}
|
|
1090
|
+
// Omitted-empty commits (chat with zero tools) leave the flag set:
|
|
1091
|
+
// they don't touch tools persisted for a paused execution.
|
|
1092
|
+
},
|
|
1093
|
+
};
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1023
1096
|
public async resumeFlow(
|
|
1024
1097
|
executionId: string,
|
|
1025
1098
|
toolOutputs: Record<string, unknown>,
|
|
@@ -1060,6 +1133,47 @@ export class AgentWidgetClient {
|
|
|
1060
1133
|
body.sessionId = resumeSessionId;
|
|
1061
1134
|
}
|
|
1062
1135
|
|
|
1136
|
+
if (isClientToken && resumeSessionId) {
|
|
1137
|
+
// Mid-run WebMCP tool refresh (runtypelabs/core#5361): the paused tool
|
|
1138
|
+
// may have navigated the page, so the dispatch-time snapshot the server
|
|
1139
|
+
// persisted can be stale. Re-snapshot the registry — the same built-in +
|
|
1140
|
+
// bridge composition as the payload builders, so fingerprints computed
|
|
1141
|
+
// here and on chat turns describe the same tool space — and ship it via
|
|
1142
|
+
// the shared diff-only protocol. `emptyMeansReplace` sends an explicit
|
|
1143
|
+
// `clientTools: []` when the registry vanished after a non-empty send,
|
|
1144
|
+
// so the server replaces the persisted set instead of keeping it frozen.
|
|
1145
|
+
const fullClientTools = [
|
|
1146
|
+
...builtInClientToolsForDispatch(this.config),
|
|
1147
|
+
...((await this.webMcpBridge?.snapshotForDispatch()) ?? []),
|
|
1148
|
+
];
|
|
1149
|
+
const { response, commit } = await this.sendWithClientToolsDiff(
|
|
1150
|
+
resumeSessionId,
|
|
1151
|
+
fullClientTools,
|
|
1152
|
+
(toolFields) => {
|
|
1153
|
+
const resumeRequest = { ...body, ...toolFields };
|
|
1154
|
+
if (this.debug) {
|
|
1155
|
+
// eslint-disable-next-line no-console
|
|
1156
|
+
console.debug("[AgentWidgetClient] client token resume", resumeRequest);
|
|
1157
|
+
}
|
|
1158
|
+
return fetch(url, {
|
|
1159
|
+
method: 'POST',
|
|
1160
|
+
headers,
|
|
1161
|
+
body: JSON.stringify(resumeRequest),
|
|
1162
|
+
signal: options?.signal,
|
|
1163
|
+
});
|
|
1164
|
+
},
|
|
1165
|
+
{ emptyMeansReplace: true }
|
|
1166
|
+
);
|
|
1167
|
+
// The server stores the refreshed registry before running the
|
|
1168
|
+
// continuation pipeline, so an OK response means it holds this set under
|
|
1169
|
+
// this fingerprint. Mirror chat's commit-on-success discipline: a failed
|
|
1170
|
+
// resume must not record a fingerprint the server never stored.
|
|
1171
|
+
if (response.ok) {
|
|
1172
|
+
commit();
|
|
1173
|
+
}
|
|
1174
|
+
return response;
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1063
1177
|
return fetch(url, {
|
|
1064
1178
|
method: 'POST',
|
|
1065
1179
|
headers,
|
|
@@ -1592,12 +1706,25 @@ export class AgentWidgetClient {
|
|
|
1592
1706
|
|
|
1593
1707
|
// Track tool call IDs for artifact emit tools so we can suppress their UI
|
|
1594
1708
|
const artifactToolCallIds = new Set<string>();
|
|
1595
|
-
// Track artifact reference card
|
|
1709
|
+
// Track artifact block messages (reference card or inline block) so we can
|
|
1710
|
+
// update them on artifact_complete
|
|
1596
1711
|
const artifactCardMessages = new Map<string, AgentWidgetMessage>();
|
|
1597
1712
|
// Track artifact IDs that already have a reference card (from auto-creation or transcript_insert)
|
|
1598
1713
|
const artifactIdsWithCards = new Set<string>();
|
|
1599
|
-
// Accumulate artifact markdown content
|
|
1600
|
-
|
|
1714
|
+
// Accumulate artifact markdown content (and component props) for embedding
|
|
1715
|
+
// in block props on complete. `props` accumulates across `artifact_update`
|
|
1716
|
+
// events so an inline component block hydrates with its real props after a
|
|
1717
|
+
// refresh (the session artifact registry, which also tracks them live, is
|
|
1718
|
+
// not persisted).
|
|
1719
|
+
const artifactContent = new Map<
|
|
1720
|
+
string,
|
|
1721
|
+
{
|
|
1722
|
+
markdown: string;
|
|
1723
|
+
title?: string;
|
|
1724
|
+
file?: PersonaArtifactFileMeta;
|
|
1725
|
+
props?: Record<string, unknown>;
|
|
1726
|
+
}
|
|
1727
|
+
>();
|
|
1601
1728
|
const isArtifactEmitToolName = (name: string | undefined): boolean => {
|
|
1602
1729
|
if (!name) return false;
|
|
1603
1730
|
const normalized = name.replace(/_+/g, "_").replace(/^_|_$/g, "");
|
|
@@ -2996,17 +3123,57 @@ export class AgentWidgetClient {
|
|
|
2996
3123
|
const at = payload.artifactType as PersonaArtifactKind;
|
|
2997
3124
|
const artId = String(payload.id);
|
|
2998
3125
|
const artTitle = typeof payload.title === "string" ? payload.title : undefined;
|
|
3126
|
+
// Additive `file` metadata: validate shape (object with string path +
|
|
3127
|
+
// string mimeType; optional string language). Drop silently if malformed
|
|
3128
|
+
// so old/new backends and unexpected payloads never break the stream.
|
|
3129
|
+
const rawFile = payload.file;
|
|
3130
|
+
let artFile: PersonaArtifactFileMeta | undefined;
|
|
3131
|
+
if (
|
|
3132
|
+
rawFile &&
|
|
3133
|
+
typeof rawFile === "object" &&
|
|
3134
|
+
!Array.isArray(rawFile) &&
|
|
3135
|
+
typeof rawFile.path === "string" &&
|
|
3136
|
+
typeof rawFile.mimeType === "string"
|
|
3137
|
+
) {
|
|
3138
|
+
artFile = {
|
|
3139
|
+
path: rawFile.path,
|
|
3140
|
+
mimeType: rawFile.mimeType,
|
|
3141
|
+
...(typeof rawFile.language === "string" ? { language: rawFile.language } : {}),
|
|
3142
|
+
};
|
|
3143
|
+
}
|
|
2999
3144
|
onEvent({
|
|
3000
3145
|
type: "artifact_start",
|
|
3001
3146
|
id: artId,
|
|
3002
3147
|
artifactType: at,
|
|
3003
3148
|
title: artTitle,
|
|
3004
|
-
component: typeof payload.component === "string" ? payload.component : undefined
|
|
3149
|
+
component: typeof payload.component === "string" ? payload.component : undefined,
|
|
3150
|
+
...(artFile ? { file: artFile } : {})
|
|
3005
3151
|
});
|
|
3006
|
-
|
|
3007
|
-
//
|
|
3152
|
+
// Seed component props from artifact_start when the payload
|
|
3153
|
+
// carries them; artifact_update events accumulate the rest below.
|
|
3154
|
+
const startProps =
|
|
3155
|
+
payload.props && typeof payload.props === "object" && !Array.isArray(payload.props)
|
|
3156
|
+
? { ...(payload.props as Record<string, unknown>) }
|
|
3157
|
+
: undefined;
|
|
3158
|
+
artifactContent.set(artId, {
|
|
3159
|
+
markdown: "",
|
|
3160
|
+
title: artTitle,
|
|
3161
|
+
file: artFile,
|
|
3162
|
+
...(startProps ? { props: startProps } : {})
|
|
3163
|
+
});
|
|
3164
|
+
// Insert the in-thread artifact block (skip if already present from
|
|
3165
|
+
// transcript_insert). The resolved display mode picks the component:
|
|
3166
|
+
// "card"/"panel" inject the reference card; "inline" injects the
|
|
3167
|
+
// inline preview block. Both share the rawContent JSON-component
|
|
3168
|
+
// shape so transcript persistence and hydration work unchanged.
|
|
3008
3169
|
if (!artifactIdsWithCards.has(artId)) {
|
|
3009
3170
|
artifactIdsWithCards.add(artId);
|
|
3171
|
+
const displayMode = resolveArtifactDisplayMode(
|
|
3172
|
+
this.config.features?.artifacts,
|
|
3173
|
+
at
|
|
3174
|
+
);
|
|
3175
|
+
const artComponent =
|
|
3176
|
+
typeof payload.component === "string" ? payload.component : undefined;
|
|
3010
3177
|
const cardMsg: AgentWidgetMessage = {
|
|
3011
3178
|
id: `artifact-ref-${artId}`,
|
|
3012
3179
|
role: "assistant",
|
|
@@ -3014,9 +3181,13 @@ export class AgentWidgetClient {
|
|
|
3014
3181
|
createdAt: new Date().toISOString(),
|
|
3015
3182
|
streaming: true,
|
|
3016
3183
|
sequence: nextSequence(),
|
|
3017
|
-
rawContent:
|
|
3018
|
-
|
|
3019
|
-
|
|
3184
|
+
rawContent: buildArtifactRefRawContent(displayMode, {
|
|
3185
|
+
artifactId: artId,
|
|
3186
|
+
title: artTitle,
|
|
3187
|
+
artifactType: at,
|
|
3188
|
+
status: "streaming",
|
|
3189
|
+
...(artFile ? { file: artFile } : {}),
|
|
3190
|
+
...(artComponent ? { component: artComponent } : {}),
|
|
3020
3191
|
}),
|
|
3021
3192
|
};
|
|
3022
3193
|
artifactCardMessages.set(artId, cardMsg);
|
|
@@ -3043,6 +3214,13 @@ export class AgentWidgetClient {
|
|
|
3043
3214
|
props,
|
|
3044
3215
|
component: typeof payload.component === "string" ? payload.component : undefined
|
|
3045
3216
|
});
|
|
3217
|
+
// Accumulate for hydration: embedded on artifact_complete so an
|
|
3218
|
+
// inline component block re-renders with its real props after a
|
|
3219
|
+
// refresh.
|
|
3220
|
+
const updateAcc = artifactContent.get(String(payload.id));
|
|
3221
|
+
if (updateAcc) {
|
|
3222
|
+
updateAcc.props = { ...(updateAcc.props ?? {}), ...props };
|
|
3223
|
+
}
|
|
3046
3224
|
} else if (payloadType === "artifact_complete") {
|
|
3047
3225
|
const artCompleteId = String(payload.id);
|
|
3048
3226
|
onEvent({ type: "artifact_complete", id: artCompleteId });
|
|
@@ -3059,6 +3237,20 @@ export class AgentWidgetClient {
|
|
|
3059
3237
|
if (acc?.markdown) {
|
|
3060
3238
|
parsed.props.markdown = acc.markdown;
|
|
3061
3239
|
}
|
|
3240
|
+
// Persist file metadata too so the download path unfences correctly after refresh.
|
|
3241
|
+
if (acc?.file) {
|
|
3242
|
+
parsed.props.file = acc.file;
|
|
3243
|
+
}
|
|
3244
|
+
// Embed accumulated component props so an inline component
|
|
3245
|
+
// block hydrates with its real props after a refresh. Only
|
|
3246
|
+
// the inline block reads them; the card never does.
|
|
3247
|
+
if (
|
|
3248
|
+
parsed.component === "PersonaArtifactInline" &&
|
|
3249
|
+
acc?.props &&
|
|
3250
|
+
Object.keys(acc.props).length > 0
|
|
3251
|
+
) {
|
|
3252
|
+
parsed.props.componentProps = acc.props;
|
|
3253
|
+
}
|
|
3062
3254
|
}
|
|
3063
3255
|
refMsg.rawContent = JSON.stringify(parsed);
|
|
3064
3256
|
} catch { /* ignore parse errors */ }
|