@runtypelabs/persona 4.23.1 → 4.24.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.d.cts +1 -1
- package/dist/animations/glyph-cycle.d.ts +1 -1
- package/dist/animations/{types-9Vxm-PPz.d.cts → types-Db61aI9J.d.cts} +25 -2
- package/dist/animations/{types-9Vxm-PPz.d.ts → types-Db61aI9J.d.ts} +25 -2
- package/dist/animations/wipe.d.cts +1 -1
- package/dist/animations/wipe.d.ts +1 -1
- package/dist/animations-extra.d.cts +25 -2
- package/dist/animations-extra.d.ts +25 -2
- package/dist/approval-ui-3NMYNXBD.js +1 -0
- package/dist/approval-ui.cjs +2 -2
- package/dist/approval-ui.d.cts +261 -40
- package/dist/approval-ui.d.ts +261 -40
- package/dist/approval-ui.js +2 -2
- package/dist/{artifacts-ui-F4N6VMKF.js → artifacts-ui-IGB5YKLO.js} +4 -4
- package/dist/artifacts-ui.d.cts +256 -28
- package/dist/artifacts-ui.d.ts +256 -28
- package/dist/{chunk-NIKYKSNZ.js → chunk-7CO4T5KB.js} +1 -1
- package/dist/codegen.cjs +11 -11
- package/dist/codegen.js +13 -13
- package/dist/context-mentions.d.cts +256 -28
- package/dist/context-mentions.d.ts +256 -28
- package/dist/event-stream-view.d.cts +256 -28
- package/dist/event-stream-view.d.ts +256 -28
- package/dist/forms-ui.d.cts +291 -30
- package/dist/forms-ui.d.ts +291 -30
- package/dist/history-view.d.cts +25 -2
- package/dist/history-view.d.ts +25 -2
- package/dist/index.cjs +70 -69
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +337 -51
- package/dist/index.d.ts +337 -51
- package/dist/index.global.js +57 -54
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +70 -69
- 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 +2 -2
- package/dist/launcher.global.js.map +1 -1
- package/dist/session-reconnect.d.cts +256 -28
- package/dist/session-reconnect.d.ts +256 -28
- package/dist/smart-dom-reader.d.cts +256 -28
- package/dist/smart-dom-reader.d.ts +256 -28
- package/dist/theme-editor-preview.cjs +74 -73
- package/dist/theme-editor-preview.d.cts +263 -28
- package/dist/theme-editor-preview.d.ts +263 -28
- package/dist/theme-editor-preview.js +51 -50
- package/dist/theme-editor.cjs +10 -10
- package/dist/theme-editor.d.cts +276 -30
- package/dist/theme-editor.d.ts +276 -30
- package/dist/theme-editor.js +10 -10
- package/dist/theme-reference.cjs +1 -1
- package/dist/theme-reference.d.cts +1522 -11
- package/dist/theme-reference.d.ts +1522 -11
- package/dist/theme-reference.js +1 -1
- package/dist/widget.css +1 -1
- package/package.json +3 -2
- package/src/__fixtures__/defaults-parity.v4.json +776 -0
- package/src/approval-ui-entry.ts +0 -1
- package/src/approval-ui-loader.ts +5 -4
- package/src/approval-ui.ts +0 -1
- package/src/client-identity-proof.test.ts +193 -0
- package/src/client-session-warm.test.ts +216 -0
- package/src/client-visitor-history.test.ts +6 -0
- package/src/client.tool-pair-replay.test.ts +325 -0
- package/src/client.ts +154 -61
- package/src/codegen.test.ts +24 -0
- package/src/components/activity-row.test.ts +100 -0
- package/src/components/activity-row.ts +136 -0
- package/src/components/approval-actions.test.ts +8 -5
- package/src/components/approval-actions.ts +19 -6
- package/src/components/approval-bubble.test.ts +56 -9
- package/src/components/approval-bubble.ts +36 -41
- package/src/components/composer-builder.ts +5 -2
- package/src/components/composer-parts.ts +10 -2
- package/src/components/header-builder.test.ts +32 -0
- package/src/components/header-builder.ts +7 -4
- package/src/components/header-layouts.test.ts +12 -0
- package/src/components/header-layouts.ts +3 -2
- package/src/components/launcher.ts +11 -2
- package/src/components/message-bubble.test.ts +22 -0
- package/src/components/message-bubble.ts +7 -3
- package/src/components/panel.test.ts +52 -1
- package/src/components/panel.ts +47 -8
- package/src/components/pill-composer-builder.test.ts +2 -0
- package/src/components/pill-composer-builder.ts +0 -1
- package/src/components/reasoning-bubble.ts +29 -17
- package/src/components/suggestions.test.ts +1 -0
- package/src/components/suggestions.ts +0 -16
- package/src/components/tool-bubble.ts +131 -114
- package/src/components/tool-details.test.ts +87 -0
- package/src/components/tool-details.ts +102 -0
- package/src/components/welcome.test.ts +8 -0
- package/src/components/welcome.ts +4 -2
- package/src/defaults-parity.test.ts +158 -0
- package/src/defaults.test.ts +69 -0
- package/src/defaults.ts +128 -49
- package/src/generated/runtype-openapi-contract.ts +28 -13
- package/src/index-core.ts +2 -0
- package/src/install.test.ts +24 -0
- package/src/install.ts +3 -1
- package/src/launcher-global.ts +2 -1
- package/src/launcher-variants.test.ts +52 -0
- package/src/runtime/init-update-reset.test.ts +21 -0
- package/src/session.ts +61 -9
- package/src/styles/widget-styles.test.ts +19 -0
- package/src/styles/widget.css +295 -23
- package/src/theme-editor/index.ts +2 -0
- package/src/theme-editor/preview-utils.test.ts +8 -0
- package/src/theme-editor/preview-utils.ts +7 -3
- package/src/theme-editor/sections.test.ts +31 -0
- package/src/theme-editor/sections.ts +55 -1
- package/src/theme-editor/state-preferences.test.ts +49 -0
- package/src/theme-editor/state.ts +73 -15
- package/src/theme-reference.test.ts +20 -0
- package/src/theme-reference.ts +11 -5
- package/src/tool-call-display-defaults.test.ts +33 -1
- package/src/types/theme.ts +80 -3
- package/src/types.ts +179 -23
- package/src/ui.activity-row.test.ts +91 -0
- package/src/ui.approval-plugin.test.ts +63 -0
- package/src/ui.composer-compact.test.ts +24 -2
- package/src/ui.composer-placement.test.ts +23 -0
- package/src/ui.detached-panel.test.ts +33 -6
- package/src/ui.docked.test.ts +19 -0
- package/src/ui.header-update-stability.test.ts +25 -0
- package/src/ui.identity-proof.test.ts +58 -0
- package/src/ui.launcher-teaser.test.ts +25 -0
- package/src/ui.message-width.test.ts +17 -2
- package/src/ui.reasoning-display.test.ts +92 -0
- package/src/ui.rebuild-composer.test.ts +20 -3
- package/src/ui.scroll.test.ts +32 -0
- package/src/ui.session-init.test.ts +312 -0
- package/src/ui.status-indicator.test.ts +82 -0
- package/src/ui.suggest-replies.test.ts +11 -0
- package/src/ui.ts +363 -74
- package/src/ui.v5-defaults.test.ts +144 -0
- package/src/ui.welcome.test.ts +92 -3
- package/src/utils/__snapshots__/tokens-parity.test.ts.snap +811 -140
- package/src/utils/code-generators.ts +14 -0
- package/src/utils/composer-compact.test.ts +0 -2
- package/src/utils/composer-compact.ts +2 -4
- package/src/utils/composer-placement.ts +2 -3
- package/src/utils/config-merge.test.ts +81 -5
- package/src/utils/config-merge.ts +13 -3
- package/src/utils/defaults-provenance.ts +67 -0
- package/src/utils/dock.ts +7 -1
- package/src/utils/launcher-variant.ts +16 -0
- package/src/utils/panel-config.ts +97 -0
- package/src/utils/status-indicator.ts +39 -0
- package/src/utils/theme.test.ts +182 -0
- package/src/utils/theme.ts +131 -18
- package/src/utils/tokens.test.ts +122 -1
- package/src/utils/tokens.ts +177 -17
- package/src/utils/tool-pair-replay.ts +100 -0
- package/src/welcome.test.ts +23 -0
- package/src/welcome.ts +16 -9
- package/dist/approval-ui-K7R5Q5OC.js +0 -1
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { AgentWidgetClient } from "./client";
|
|
4
|
+
import { AgentWidgetSession } from "./session";
|
|
5
|
+
import type { AgentWidgetMessage, ClientToolDefinition } from "./types";
|
|
6
|
+
import { modelFacingToolName } from "./utils/tool-pair-replay";
|
|
7
|
+
|
|
8
|
+
const SEARCH_TOOL: ClientToolDefinition = {
|
|
9
|
+
name: "search_catalog",
|
|
10
|
+
description: "Search the catalog",
|
|
11
|
+
origin: "webmcp",
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const RESULT = { content: [{ type: "text", text: "SKU-42 costs $19" }] };
|
|
15
|
+
|
|
16
|
+
const user = (id: string, content: string, at: number): AgentWidgetMessage => ({
|
|
17
|
+
id,
|
|
18
|
+
role: "user",
|
|
19
|
+
content,
|
|
20
|
+
createdAt: new Date(at).toISOString(),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const assistant = (id: string, content: string, at: number): AgentWidgetMessage => ({
|
|
24
|
+
id,
|
|
25
|
+
role: "assistant",
|
|
26
|
+
content,
|
|
27
|
+
createdAt: new Date(at).toISOString(),
|
|
28
|
+
variant: "assistant",
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const answeredTool = (
|
|
32
|
+
toolCallId: string,
|
|
33
|
+
at: number,
|
|
34
|
+
overrides: Partial<NonNullable<AgentWidgetMessage["agentMetadata"]>> = {},
|
|
35
|
+
): AgentWidgetMessage => ({
|
|
36
|
+
id: `tool-${toolCallId}`,
|
|
37
|
+
role: "assistant",
|
|
38
|
+
content: "",
|
|
39
|
+
createdAt: new Date(at).toISOString(),
|
|
40
|
+
variant: "tool",
|
|
41
|
+
toolCall: {
|
|
42
|
+
id: toolCallId,
|
|
43
|
+
name: "webmcp:search_catalog",
|
|
44
|
+
status: "complete",
|
|
45
|
+
args: { q: "sku 42" },
|
|
46
|
+
result: RESULT,
|
|
47
|
+
},
|
|
48
|
+
agentMetadata: {
|
|
49
|
+
executionId: "exec-1",
|
|
50
|
+
awaitingLocalTool: false,
|
|
51
|
+
webMcpToolCallId: toolCallId,
|
|
52
|
+
clientToolAnswer: {
|
|
53
|
+
toolCallId,
|
|
54
|
+
toolName: "webmcp:search_catalog",
|
|
55
|
+
args: { q: "sku 42" },
|
|
56
|
+
result: RESULT,
|
|
57
|
+
},
|
|
58
|
+
...overrides,
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
/** Turn 1 (with its answered tool) followed by turn 2's user message. */
|
|
63
|
+
const twoTurnTranscript = (): AgentWidgetMessage[] => [
|
|
64
|
+
user("u1", "What does SKU 42 cost?", 1_000),
|
|
65
|
+
answeredTool("toolu_01", 2_000),
|
|
66
|
+
assistant("a1", "SKU 42 costs $19.", 3_000),
|
|
67
|
+
user("u2", "And what was the SKU again?", 4_000),
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
function sse(): Response {
|
|
71
|
+
const encoder = new TextEncoder();
|
|
72
|
+
const stream = new ReadableStream({
|
|
73
|
+
start(c) {
|
|
74
|
+
c.enqueue(encoder.encode('data: {"type":"done"}\n\n'));
|
|
75
|
+
c.close();
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
return new Response(stream, { headers: { "Content-Type": "text/event-stream" } });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let bodies: Array<Record<string, unknown>>;
|
|
82
|
+
beforeEach(() => {
|
|
83
|
+
bodies = [];
|
|
84
|
+
global.fetch = vi.fn().mockImplementation(async (_url: string, init: { body: string }) => {
|
|
85
|
+
bodies.push(JSON.parse(init.body));
|
|
86
|
+
return sse();
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const withTools = (client: AgentWidgetClient, tools: ClientToolDefinition[]) => {
|
|
91
|
+
(client as unknown as { webMcpBridge: { snapshotForDispatch: () => unknown[] } }).webMcpBridge = {
|
|
92
|
+
snapshotForDispatch: () => tools,
|
|
93
|
+
};
|
|
94
|
+
return client;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const EXPECTED_TURN_2 = [
|
|
98
|
+
{ role: "user", content: "What does SKU 42 cost?" },
|
|
99
|
+
{
|
|
100
|
+
role: "assistant",
|
|
101
|
+
content: "",
|
|
102
|
+
toolCalls: [{ toolCallId: "toolu_01", toolName: "webmcp_search_catalog", args: { q: "sku 42" } }],
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
role: "tool",
|
|
106
|
+
content: "",
|
|
107
|
+
toolResults: [{ toolCallId: "toolu_01", toolName: "webmcp_search_catalog", result: RESULT }],
|
|
108
|
+
},
|
|
109
|
+
{ role: "assistant", content: "SKU 42 costs $19." },
|
|
110
|
+
{ role: "user", content: "And what was the SKU again?" },
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
const stripCreatedAt = (messages: unknown) =>
|
|
114
|
+
(messages as Array<Record<string, unknown>>).map(({ createdAt: _createdAt, ...rest }) => rest);
|
|
115
|
+
|
|
116
|
+
describe("tool-pair replay: proxy mode", () => {
|
|
117
|
+
it("sends turn 1's answered call and result, paired and in order, on turn 2", async () => {
|
|
118
|
+
const client = withTools(new AgentWidgetClient({ apiUrl: "http://proxy.test/chat" }), [SEARCH_TOOL]);
|
|
119
|
+
await client.dispatch({ messages: twoTurnTranscript() }, () => undefined);
|
|
120
|
+
expect(stripCreatedAt(bodies[0]!.messages)).toEqual(EXPECTED_TURN_2);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("keeps the pair after the page stops offering the tool", async () => {
|
|
124
|
+
const client = withTools(new AgentWidgetClient({ apiUrl: "http://proxy.test/chat" }), []);
|
|
125
|
+
await client.dispatch({ messages: twoTurnTranscript() }, () => undefined);
|
|
126
|
+
expect(stripCreatedAt(bodies[0]!.messages)).toEqual(EXPECTED_TURN_2);
|
|
127
|
+
expect(bodies[0]!.clientTools).toBeUndefined();
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe("tool-pair replay: agent mode", () => {
|
|
132
|
+
it("sends turn 1's answered call and result, paired and in order, on turn 2", async () => {
|
|
133
|
+
const client = withTools(
|
|
134
|
+
new AgentWidgetClient({
|
|
135
|
+
apiUrl: "http://proxy.test/agents",
|
|
136
|
+
agent: { name: "Test", model: "openai:gpt-4o-mini", systemPrompt: "test" },
|
|
137
|
+
}),
|
|
138
|
+
[SEARCH_TOOL],
|
|
139
|
+
);
|
|
140
|
+
await client.dispatch({ messages: twoTurnTranscript() }, () => undefined);
|
|
141
|
+
expect(stripCreatedAt(bodies[0]!.messages)).toEqual(EXPECTED_TURN_2);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe("tool-pair replay: bubbles that must not replay", () => {
|
|
146
|
+
it("a tool bubble with no accepted answer emits nothing", async () => {
|
|
147
|
+
const pending: AgentWidgetMessage = {
|
|
148
|
+
...answeredTool("toolu_02", 2_000),
|
|
149
|
+
toolCall: { id: "toolu_02", name: "webmcp:search_catalog", status: "running", args: {} },
|
|
150
|
+
agentMetadata: { executionId: "exec-1", awaitingLocalTool: true, webMcpToolCallId: "toolu_02" },
|
|
151
|
+
};
|
|
152
|
+
const client = withTools(new AgentWidgetClient({ apiUrl: "http://proxy.test/chat" }), [SEARCH_TOOL]);
|
|
153
|
+
await client.dispatch(
|
|
154
|
+
{ messages: [user("u1", "hi", 1_000), pending, user("u2", "again", 3_000)] },
|
|
155
|
+
() => undefined,
|
|
156
|
+
);
|
|
157
|
+
const messages = bodies[0]!.messages as Array<Record<string, unknown>>;
|
|
158
|
+
expect(messages.map((m) => m.role)).toEqual(["user", "user"]);
|
|
159
|
+
expect(messages.some((m) => "toolCalls" in m || "toolResults" in m)).toBe(false);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("a server-side tool's result stays out (only browser-answered calls replay)", async () => {
|
|
163
|
+
const serverTool: AgentWidgetMessage = {
|
|
164
|
+
...answeredTool("toolu_03", 2_000),
|
|
165
|
+
agentMetadata: { executionId: "exec-1" },
|
|
166
|
+
};
|
|
167
|
+
const client = withTools(new AgentWidgetClient({ apiUrl: "http://proxy.test/chat" }), [SEARCH_TOOL]);
|
|
168
|
+
await client.dispatch(
|
|
169
|
+
{ messages: [user("u1", "hi", 1_000), serverTool, user("u2", "again", 3_000)] },
|
|
170
|
+
() => undefined,
|
|
171
|
+
);
|
|
172
|
+
expect((bodies[0]!.messages as unknown[]).length).toBe(2);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
describe("tool-pair replay: parallel and chained calls", () => {
|
|
177
|
+
type Sent = Array<{
|
|
178
|
+
role: string;
|
|
179
|
+
toolCalls?: Array<{ toolCallId: string }>;
|
|
180
|
+
toolResults?: Array<{ toolCallId: string }>;
|
|
181
|
+
}>;
|
|
182
|
+
const inBatch = (toolCallId: string, at: number, batch: string) => {
|
|
183
|
+
const message = answeredTool(toolCallId, at);
|
|
184
|
+
message.agentMetadata!.clientToolAnswer!.batch = batch;
|
|
185
|
+
return message;
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
it("groups answers from one /resume batch into one call message and one result message", async () => {
|
|
189
|
+
const client = withTools(new AgentWidgetClient({ apiUrl: "http://proxy.test/chat" }), [SEARCH_TOOL]);
|
|
190
|
+
await client.dispatch(
|
|
191
|
+
{
|
|
192
|
+
messages: [
|
|
193
|
+
user("u1", "compare", 1_000),
|
|
194
|
+
inBatch("toolu_a", 2_000, "b1"),
|
|
195
|
+
inBatch("toolu_b", 2_001, "b1"),
|
|
196
|
+
assistant("a1", "done", 3_000),
|
|
197
|
+
],
|
|
198
|
+
},
|
|
199
|
+
() => undefined,
|
|
200
|
+
);
|
|
201
|
+
const messages = bodies[0]!.messages as Sent;
|
|
202
|
+
expect(messages.map((m) => m.role)).toEqual(["user", "assistant", "tool", "assistant"]);
|
|
203
|
+
expect(messages[1]!.toolCalls!.map((c) => c.toolCallId)).toEqual(["toolu_a", "toolu_b"]);
|
|
204
|
+
expect(messages[2]!.toolResults!.map((r) => r.toolCallId)).toEqual(["toolu_a", "toolu_b"]);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("keeps a chained call (a later /resume batch, no text between) as its own pair", async () => {
|
|
208
|
+
const client = withTools(new AgentWidgetClient({ apiUrl: "http://proxy.test/chat" }), [SEARCH_TOOL]);
|
|
209
|
+
await client.dispatch(
|
|
210
|
+
{
|
|
211
|
+
messages: [
|
|
212
|
+
user("u1", "look it up, then look up the related item", 1_000),
|
|
213
|
+
inBatch("toolu_a", 2_000, "b1"),
|
|
214
|
+
inBatch("toolu_b", 2_500, "b2"),
|
|
215
|
+
assistant("a1", "done", 3_000),
|
|
216
|
+
],
|
|
217
|
+
},
|
|
218
|
+
() => undefined,
|
|
219
|
+
);
|
|
220
|
+
const messages = bodies[0]!.messages as Sent;
|
|
221
|
+
expect(messages.map((m) => m.role)).toEqual(["user", "assistant", "tool", "assistant", "tool", "assistant"]);
|
|
222
|
+
expect(messages[1]!.toolCalls!.map((c) => c.toolCallId)).toEqual(["toolu_a"]);
|
|
223
|
+
expect(messages[3]!.toolCalls!.map((c) => c.toolCallId)).toEqual(["toolu_b"]);
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
describe("tool-pair replay: requestMiddleware", () => {
|
|
228
|
+
it("keeps replayed pairs when the middleware removes the tools", async () => {
|
|
229
|
+
const client = withTools(
|
|
230
|
+
new AgentWidgetClient({
|
|
231
|
+
apiUrl: "http://proxy.test/chat",
|
|
232
|
+
requestMiddleware: ({ payload }) => ({ ...payload, clientTools: undefined }),
|
|
233
|
+
}),
|
|
234
|
+
[SEARCH_TOOL],
|
|
235
|
+
);
|
|
236
|
+
await client.dispatch({ messages: twoTurnTranscript() }, () => undefined);
|
|
237
|
+
expect(stripCreatedAt(bodies[0]!.messages)).toEqual(EXPECTED_TURN_2);
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
describe("tool-pair replay: client-token mode is server-owned", () => {
|
|
242
|
+
it("sends no toolCalls, toolResults or tool role on /client/chat", async () => {
|
|
243
|
+
const client = withTools(
|
|
244
|
+
new AgentWidgetClient({ clientToken: "ct_live_demo", apiUrl: "https://api.runtype.com" }),
|
|
245
|
+
[SEARCH_TOOL],
|
|
246
|
+
);
|
|
247
|
+
(client as unknown as { clientSession: { sessionId: string; expiresAt: Date } }).clientSession = {
|
|
248
|
+
sessionId: "cs_1",
|
|
249
|
+
expiresAt: new Date(Date.now() + 600_000),
|
|
250
|
+
};
|
|
251
|
+
await client.dispatch({ messages: twoTurnTranscript(), assistantMessageId: "a2" }, () => undefined);
|
|
252
|
+
const messages = bodies[0]!.messages as Array<Record<string, unknown>>;
|
|
253
|
+
expect(messages.map((m) => m.role)).toEqual(["user", "assistant", "user"]);
|
|
254
|
+
expect(messages.some((m) => "toolCalls" in m || "toolResults" in m)).toBe(false);
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
describe("modelFacingToolName", () => {
|
|
259
|
+
it("matches core's AI-SDK sanitizer", () => {
|
|
260
|
+
expect(modelFacingToolName("webmcp:get_schema")).toBe("webmcp_get_schema");
|
|
261
|
+
expect(modelFacingToolName("webmcp:Get-Slide")).toBe("webmcp_get_slide");
|
|
262
|
+
expect(modelFacingToolName("suggest_replies")).toBe("suggest_replies");
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
describe("session records the accepted client-tool answer", () => {
|
|
267
|
+
const makeSession = (resumeStatus = 200) => {
|
|
268
|
+
const session = new AgentWidgetSession(
|
|
269
|
+
{ apiUrl: "http://test", webmcp: { enabled: true } },
|
|
270
|
+
{
|
|
271
|
+
onMessagesChanged: () => undefined,
|
|
272
|
+
onStatusChanged: () => undefined,
|
|
273
|
+
onStreamingChanged: () => undefined,
|
|
274
|
+
},
|
|
275
|
+
);
|
|
276
|
+
const client = (session as unknown as { client: Record<string, unknown> }).client;
|
|
277
|
+
client.executeWebMcpToolCall = vi.fn(async () => RESULT);
|
|
278
|
+
client.isWebMcpOperational = vi.fn(() => true);
|
|
279
|
+
client.resumeFlow = vi.fn(async () => new Response(new Blob([""]), { status: resumeStatus }));
|
|
280
|
+
(session as unknown as { connectStream: () => Promise<void> }).connectStream = vi.fn(async () => undefined);
|
|
281
|
+
return session;
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
const awaiting = (): AgentWidgetMessage => ({
|
|
285
|
+
id: "tool-toolu_01",
|
|
286
|
+
role: "assistant",
|
|
287
|
+
content: "",
|
|
288
|
+
createdAt: new Date().toISOString(),
|
|
289
|
+
variant: "tool",
|
|
290
|
+
agentMetadata: { executionId: "exec-1", awaitingLocalTool: true, webMcpToolCallId: "toolu_01" },
|
|
291
|
+
toolCall: { id: "toolu_01", name: "webmcp:search_catalog", status: "running", args: { q: "sku 42" } },
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
const stored = (session: AgentWidgetSession) =>
|
|
295
|
+
(session as unknown as { messages: AgentWidgetMessage[] }).messages.find((m) => m.id === "tool-toolu_01");
|
|
296
|
+
|
|
297
|
+
it("stores the answer after /resume succeeds and keeps it across a tool_complete re-emit", async () => {
|
|
298
|
+
const session = makeSession();
|
|
299
|
+
await session.resolveWebMcpToolCall(awaiting());
|
|
300
|
+
expect(stored(session)?.agentMetadata?.clientToolAnswer).toEqual({
|
|
301
|
+
toolCallId: "toolu_01",
|
|
302
|
+
toolName: "webmcp:search_catalog",
|
|
303
|
+
args: { q: "sku 42" },
|
|
304
|
+
result: RESULT,
|
|
305
|
+
batch: expect.any(String),
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
(session as unknown as { upsertMessage: (m: AgentWidgetMessage) => void }).upsertMessage({
|
|
309
|
+
id: "tool-toolu_01",
|
|
310
|
+
role: "assistant",
|
|
311
|
+
content: "",
|
|
312
|
+
createdAt: new Date().toISOString(),
|
|
313
|
+
variant: "tool",
|
|
314
|
+
agentMetadata: { executionId: "exec-1" },
|
|
315
|
+
toolCall: { id: "toolu_01", name: "webmcp:search_catalog", status: "complete", result: "server copy" },
|
|
316
|
+
});
|
|
317
|
+
expect(stored(session)?.agentMetadata?.clientToolAnswer?.result).toEqual(RESULT);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it("stores nothing when /resume fails", async () => {
|
|
321
|
+
const session = makeSession(500);
|
|
322
|
+
await session.resolveWebMcpToolCall(awaiting());
|
|
323
|
+
expect(stored(session)?.agentMetadata?.clientToolAnswer).toBeUndefined();
|
|
324
|
+
});
|
|
325
|
+
});
|
package/src/client.ts
CHANGED
|
@@ -49,6 +49,7 @@ import {
|
|
|
49
49
|
import { resolveTarget } from "./utils/target";
|
|
50
50
|
import { generateTurnId } from "./utils/message-id";
|
|
51
51
|
import { builtInClientToolsForDispatch } from "./ask-user-question-tool";
|
|
52
|
+
import { serializeWithToolPairs } from "./utils/tool-pair-replay";
|
|
52
53
|
import {
|
|
53
54
|
extractTextFromJson,
|
|
54
55
|
createPlainTextParser,
|
|
@@ -221,6 +222,18 @@ const hasValidContent = (message: AgentWidgetMessage): boolean => {
|
|
|
221
222
|
return false;
|
|
222
223
|
};
|
|
223
224
|
|
|
225
|
+
const sortByCreatedAt = (messages: AgentWidgetMessage[]): AgentWidgetMessage[] =>
|
|
226
|
+
messages
|
|
227
|
+
.slice()
|
|
228
|
+
.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
|
|
229
|
+
|
|
230
|
+
// Priority: contentParts (multi-modal) > llmContent (explicit LLM content) > rawContent (structured parsers) > content (display)
|
|
231
|
+
const toPayloadMessage = (message: AgentWidgetMessage) => ({
|
|
232
|
+
role: message.role,
|
|
233
|
+
content: message.contentParts ?? message.llmContent ?? message.rawContent ?? message.content,
|
|
234
|
+
createdAt: message.createdAt
|
|
235
|
+
});
|
|
236
|
+
|
|
224
237
|
/**
|
|
225
238
|
* Maps parserType string to the corresponding parser factory function
|
|
226
239
|
*/
|
|
@@ -294,6 +307,14 @@ export class AgentWidgetClient {
|
|
|
294
307
|
private currentClientTurnId: string | null = null;
|
|
295
308
|
private clientSession: ClientSession | null = null;
|
|
296
309
|
private sessionInitPromise: Promise<ClientSession> | null = null;
|
|
310
|
+
/**
|
|
311
|
+
* Early-init latch for `warmSession()`. `false` = armed. Otherwise it holds
|
|
312
|
+
* the session value (`null` or an expired session) the one early attempt was
|
|
313
|
+
* made against, so a failed attempt is not retried on every keystroke. It is
|
|
314
|
+
* re-armed when a session is installed or cleared, so each session lifetime
|
|
315
|
+
* gets one early init.
|
|
316
|
+
*/
|
|
317
|
+
private sessionWarmLatch: ClientSession | null | false = false;
|
|
297
318
|
|
|
298
319
|
// Diff-only / send-once WebMCP tool dispatch (client-token mode ONLY).
|
|
299
320
|
// Fingerprint of the clientTools[] last *sent in full* and confirmed by a
|
|
@@ -608,9 +629,41 @@ export class AgentWidgetClient {
|
|
|
608
629
|
}
|
|
609
630
|
}
|
|
610
631
|
|
|
632
|
+
/**
|
|
633
|
+
* Fire-and-forget early `initSession()` for client-token mode, driven by the
|
|
634
|
+
* widget's `sessionInit` trigger (or a host calling it directly, e.g. on
|
|
635
|
+
* hover). It fires at most once per session lifetime and is a no-op while a
|
|
636
|
+
* session is live or an init is already in flight. Errors are swallowed, with
|
|
637
|
+
* no `onSessionExpired` and no UI: a send calls `initSession()` again and
|
|
638
|
+
* reuses the in-flight promise, so any failure resurfaces there exactly as it
|
|
639
|
+
* would without the early init. Sends nothing new on the wire. Outside
|
|
640
|
+
* client-token mode this does nothing.
|
|
641
|
+
*
|
|
642
|
+
* Resolves with the session this call initialized, or `null` when it was
|
|
643
|
+
* skipped or failed. It never rejects.
|
|
644
|
+
*/
|
|
645
|
+
public async warmSession(): Promise<ClientSession | null> {
|
|
646
|
+
const current = this.clientSession;
|
|
647
|
+
if (
|
|
648
|
+
!this.isClientTokenMode() ||
|
|
649
|
+
this.sessionInitPromise ||
|
|
650
|
+
(current && new Date() < current.expiresAt) ||
|
|
651
|
+
this.sessionWarmLatch === current
|
|
652
|
+
) {
|
|
653
|
+
return null;
|
|
654
|
+
}
|
|
655
|
+
this.sessionWarmLatch = current;
|
|
656
|
+
// Swallowed on purpose: the send's own initSession() reports failures.
|
|
657
|
+
return this.initSession().catch(() => null);
|
|
658
|
+
}
|
|
659
|
+
|
|
611
660
|
/**
|
|
612
661
|
* Initialize session for client token mode.
|
|
613
|
-
* Called
|
|
662
|
+
* Called by the send if no live session exists. The widget normally calls
|
|
663
|
+
* it earlier, when the visitor shows intent to send (see the `sessionInit`
|
|
664
|
+
* config option and `warmSession()`), and concurrent calls share one
|
|
665
|
+
* in-flight request, so a send racing an early init issues a single
|
|
666
|
+
* `/v1/client/init`.
|
|
614
667
|
*/
|
|
615
668
|
public async initSession(): Promise<ClientSession> {
|
|
616
669
|
if (!this.isClientTokenMode()) {
|
|
@@ -627,21 +680,28 @@ export class AgentWidgetClient {
|
|
|
627
680
|
return this.sessionInitPromise;
|
|
628
681
|
}
|
|
629
682
|
|
|
630
|
-
this
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
683
|
+
// Callers that dedupe share this promise, so the stale check below covers
|
|
684
|
+
// them too (the send reusing an early init included).
|
|
685
|
+
const pending: Promise<ClientSession> = (this.sessionInitPromise = this._doInitSession()
|
|
686
|
+
.then((session) => {
|
|
687
|
+
// A clear or replacement (credential change, start-new, proof re-init)
|
|
688
|
+
// while this was in flight makes the result stale: never install it.
|
|
689
|
+
if (this.sessionInitPromise !== pending) return this.initSession();
|
|
690
|
+
this.clientSession = session;
|
|
691
|
+
this.sessionWarmLatch = false;
|
|
692
|
+
// A freshly-minted session must resend the full WebMCP tool list on its
|
|
693
|
+
// next turn: drop any diff-only fingerprint cached under a prior session,
|
|
694
|
+
// so we never claim "unchanged" against a session the server didn't store
|
|
695
|
+
// the set under. (Belt-and-suspenders with the sessionId comparison in the
|
|
696
|
+
// send decision and the server's 409 resend signal.)
|
|
697
|
+
this.resetClientToolsFingerprint();
|
|
698
|
+
this.config.onSessionInit?.(session);
|
|
699
|
+
return session;
|
|
700
|
+
})
|
|
701
|
+
.finally(() => {
|
|
702
|
+
if (this.sessionInitPromise === pending) this.sessionInitPromise = null;
|
|
703
|
+
}));
|
|
704
|
+
return pending;
|
|
645
705
|
}
|
|
646
706
|
|
|
647
707
|
/** Visitor history rides on client-token init only, and latches off after a 403 degrade. */
|
|
@@ -953,6 +1013,7 @@ export class AgentWidgetClient {
|
|
|
953
1013
|
const installed = this.finishInit(session, previousConversationId, false);
|
|
954
1014
|
this.clientSession = installed;
|
|
955
1015
|
this.sessionInitPromise = null;
|
|
1016
|
+
this.sessionWarmLatch = false;
|
|
956
1017
|
this.resetClientToolsFingerprint();
|
|
957
1018
|
return installed;
|
|
958
1019
|
}
|
|
@@ -1019,6 +1080,7 @@ export class AgentWidgetClient {
|
|
|
1019
1080
|
if (settled) return;
|
|
1020
1081
|
settled = true;
|
|
1021
1082
|
this.clientSession = session;
|
|
1083
|
+
this.sessionWarmLatch = false;
|
|
1022
1084
|
this.resetClientToolsFingerprint();
|
|
1023
1085
|
},
|
|
1024
1086
|
discard: () => {
|
|
@@ -1089,6 +1151,7 @@ export class AgentWidgetClient {
|
|
|
1089
1151
|
public clearClientSession(): void {
|
|
1090
1152
|
this.clientSession = null;
|
|
1091
1153
|
this.sessionInitPromise = null;
|
|
1154
|
+
this.sessionWarmLatch = false;
|
|
1092
1155
|
this.resetClientToolsFingerprint();
|
|
1093
1156
|
}
|
|
1094
1157
|
|
|
@@ -1199,6 +1262,7 @@ export class AgentWidgetClient {
|
|
|
1199
1262
|
|
|
1200
1263
|
if (response.status === 401) {
|
|
1201
1264
|
this.clientSession = null;
|
|
1265
|
+
this.sessionWarmLatch = false;
|
|
1202
1266
|
this.config.onSessionExpired?.();
|
|
1203
1267
|
throw new Error('Session expired. Please refresh to continue.');
|
|
1204
1268
|
}
|
|
@@ -1218,10 +1282,9 @@ export class AgentWidgetClient {
|
|
|
1218
1282
|
messageId: string,
|
|
1219
1283
|
type: 'upvote' | 'downvote' | 'copy'
|
|
1220
1284
|
): Promise<void> {
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
}
|
|
1285
|
+
// Feedback on a restored transcript can precede any init (`sessionInit`
|
|
1286
|
+
// defers it to intent); the feedback click is intent enough.
|
|
1287
|
+
const session = this.getClientSession() ?? (await this.initSession());
|
|
1225
1288
|
|
|
1226
1289
|
return this.sendFeedback({
|
|
1227
1290
|
sessionId: session.sessionId,
|
|
@@ -1238,10 +1301,7 @@ export class AgentWidgetClient {
|
|
|
1238
1301
|
* @param comment - Optional comment
|
|
1239
1302
|
*/
|
|
1240
1303
|
public async submitCSATFeedback(rating: number, comment?: string): Promise<void> {
|
|
1241
|
-
const session = this.getClientSession();
|
|
1242
|
-
if (!session) {
|
|
1243
|
-
throw new Error('No active session. Please initialize session first.');
|
|
1244
|
-
}
|
|
1304
|
+
const session = this.getClientSession() ?? (await this.initSession());
|
|
1245
1305
|
|
|
1246
1306
|
return this.sendFeedback({
|
|
1247
1307
|
sessionId: session.sessionId,
|
|
@@ -1259,10 +1319,7 @@ export class AgentWidgetClient {
|
|
|
1259
1319
|
* @param comment - Optional comment
|
|
1260
1320
|
*/
|
|
1261
1321
|
public async submitNPSFeedback(rating: number, comment?: string): Promise<void> {
|
|
1262
|
-
const session = this.getClientSession();
|
|
1263
|
-
if (!session) {
|
|
1264
|
-
throw new Error('No active session. Please initialize session first.');
|
|
1265
|
-
}
|
|
1322
|
+
const session = this.getClientSession() ?? (await this.initSession());
|
|
1266
1323
|
|
|
1267
1324
|
return this.sendFeedback({
|
|
1268
1325
|
sessionId: session.sessionId,
|
|
@@ -1298,6 +1355,40 @@ export class AgentWidgetClient {
|
|
|
1298
1355
|
return this.config.getIdentityProof ? 'verified-user' : 'browser';
|
|
1299
1356
|
}
|
|
1300
1357
|
|
|
1358
|
+
private async resolveChatIdentityProof(signal?: AbortSignal): Promise<ClientChatRequest['identityProof']> {
|
|
1359
|
+
signal?.throwIfAborted();
|
|
1360
|
+
const provider = this.config.identityProvider;
|
|
1361
|
+
if (provider === undefined) return undefined;
|
|
1362
|
+
const getIdentityProof = this.config.getIdentityProof;
|
|
1363
|
+
if (!provider.trim() || !getIdentityProof) {
|
|
1364
|
+
throw new Error('Chat identity requires identityProvider and getIdentityProof.');
|
|
1365
|
+
}
|
|
1366
|
+
let token: string | null;
|
|
1367
|
+
let onAbort: (() => void) | undefined;
|
|
1368
|
+
try {
|
|
1369
|
+
const proof = Promise.resolve().then(() => getIdentityProof());
|
|
1370
|
+
token = signal
|
|
1371
|
+
? await Promise.race([
|
|
1372
|
+
proof,
|
|
1373
|
+
new Promise<never>((_, reject) => {
|
|
1374
|
+
onAbort = () => reject(signal.reason);
|
|
1375
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
1376
|
+
if (signal.aborted) onAbort();
|
|
1377
|
+
}),
|
|
1378
|
+
])
|
|
1379
|
+
: await proof;
|
|
1380
|
+
} catch {
|
|
1381
|
+
signal?.throwIfAborted();
|
|
1382
|
+
throw new Error('The identity proof provider failed.');
|
|
1383
|
+
} finally {
|
|
1384
|
+
if (onAbort) signal?.removeEventListener('abort', onAbort);
|
|
1385
|
+
}
|
|
1386
|
+
if (typeof token !== 'string' || !token.trim()) {
|
|
1387
|
+
throw new Error('A fresh identity proof is required to send this message.');
|
|
1388
|
+
}
|
|
1389
|
+
return { provider, token };
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1301
1392
|
/**
|
|
1302
1393
|
* Resolve the proof for one logical request. `null` is an intentional
|
|
1303
1394
|
* browser-scope fallback only while the visitor has never been bound.
|
|
@@ -1837,6 +1928,7 @@ export class AgentWidgetClient {
|
|
|
1837
1928
|
}
|
|
1838
1929
|
session = this.finishInit(renewed, previous.conversationId ?? null, false);
|
|
1839
1930
|
this.clientSession = session;
|
|
1931
|
+
this.sessionWarmLatch = false;
|
|
1840
1932
|
this.resetClientToolsFingerprint();
|
|
1841
1933
|
this.config.onSessionInit?.(session);
|
|
1842
1934
|
};
|
|
@@ -1896,16 +1988,23 @@ export class AgentWidgetClient {
|
|
|
1896
1988
|
const recoveryVisitorToken =
|
|
1897
1989
|
session.durableRecovery?.enabled === true ? await this.readVisitorToken() : null;
|
|
1898
1990
|
assertCurrentTurn();
|
|
1899
|
-
return this.sendWithClientToolsDiff(session.sessionId, basePayload.clientTools, (toolFields) => {
|
|
1991
|
+
return this.sendWithClientToolsDiff(session.sessionId, basePayload.clientTools, async (toolFields) => {
|
|
1992
|
+
assertCurrentTurn();
|
|
1993
|
+
const identityProof = await this.resolveChatIdentityProof(options.signal);
|
|
1994
|
+
assertCurrentTurn();
|
|
1900
1995
|
const chatRequest: ClientChatRequest = {
|
|
1901
1996
|
...baseChatRequest,
|
|
1902
1997
|
sessionId: session.sessionId,
|
|
1903
1998
|
...toolFields,
|
|
1999
|
+
...(identityProof && { identityProof }),
|
|
1904
2000
|
};
|
|
1905
2001
|
|
|
1906
2002
|
if (this.debug) {
|
|
1907
2003
|
// eslint-disable-next-line no-console
|
|
1908
|
-
console.debug("[AgentWidgetClient] client token dispatch",
|
|
2004
|
+
console.debug("[AgentWidgetClient] client token dispatch", {
|
|
2005
|
+
...chatRequest,
|
|
2006
|
+
...(identityProof && { identityProof: { provider: identityProof.provider, token: '[REDACTED]' } }),
|
|
2007
|
+
});
|
|
1909
2008
|
}
|
|
1910
2009
|
|
|
1911
2010
|
return fetch(this.getClientApiUrl('chat'), {
|
|
@@ -1938,6 +2037,12 @@ export class AgentWidgetClient {
|
|
|
1938
2037
|
if (!response.ok) {
|
|
1939
2038
|
const data = await response.json().catch(() => ({ error: 'Chat request failed' }));
|
|
1940
2039
|
|
|
2040
|
+
if (response.status === 401 && data.error === 'invalid_identity_proof') {
|
|
2041
|
+
const error = new HistoryClientError('invalid_identity_proof', PROOF_REJECTED_MESSAGE);
|
|
2042
|
+
forward({ type: 'error', error });
|
|
2043
|
+
throw error;
|
|
2044
|
+
}
|
|
2045
|
+
|
|
1941
2046
|
if (response.status === 401) {
|
|
1942
2047
|
// Session expired
|
|
1943
2048
|
this.clearClientSession();
|
|
@@ -2533,22 +2638,17 @@ export class AgentWidgetClient {
|
|
|
2533
2638
|
throw new Error('Agent configuration required for agent mode');
|
|
2534
2639
|
}
|
|
2535
2640
|
|
|
2536
|
-
// Filter out messages with empty content and normalize
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
.map((message) => ({
|
|
2548
|
-
role: message.role,
|
|
2549
|
-
content: message.contentParts ?? message.llmContent ?? message.rawContent ?? message.content,
|
|
2550
|
-
createdAt: message.createdAt
|
|
2551
|
-
}));
|
|
2641
|
+
// Filter out messages with empty content and normalize; answered
|
|
2642
|
+
// client-tool calls replay as paired toolCalls/toolResults messages.
|
|
2643
|
+
const normalizedMessages = serializeWithToolPairs(
|
|
2644
|
+
sortByCreatedAt(messages),
|
|
2645
|
+
(message) =>
|
|
2646
|
+
hasValidContent(message) &&
|
|
2647
|
+
(message.role === "user" || message.role === "assistant" || message.role === "system") &&
|
|
2648
|
+
(!message.variant || message.variant === "assistant")
|
|
2649
|
+
? toPayloadMessage(message)
|
|
2650
|
+
: null
|
|
2651
|
+
);
|
|
2552
2652
|
|
|
2553
2653
|
const composer = this.normalizeComposerOptions(composerOptions);
|
|
2554
2654
|
const payload: AgentWidgetAgentRequestPayload = {
|
|
@@ -2590,21 +2690,14 @@ export class AgentWidgetClient {
|
|
|
2590
2690
|
messages: AgentWidgetMessage[],
|
|
2591
2691
|
composerOptions?: ComposerOptionsPayload
|
|
2592
2692
|
): Promise<AgentWidgetRequestPayload> {
|
|
2593
|
-
// Filter out messages with empty content to prevent validation errors
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
})
|
|
2602
|
-
.map((message) => ({
|
|
2603
|
-
role: message.role,
|
|
2604
|
-
// Priority: contentParts (multi-modal) > llmContent (explicit LLM content) > rawContent (structured parsers) > content (display)
|
|
2605
|
-
content: message.contentParts ?? message.llmContent ?? message.rawContent ?? message.content,
|
|
2606
|
-
createdAt: message.createdAt
|
|
2607
|
-
}));
|
|
2693
|
+
// Filter out messages with empty content to prevent validation errors;
|
|
2694
|
+
// answered client-tool calls replay as paired toolCalls/toolResults
|
|
2695
|
+
// messages. Client-token mode maps `options.messages` itself and never
|
|
2696
|
+
// reads these, so the server stays the replay owner there.
|
|
2697
|
+
const normalizedMessages = serializeWithToolPairs(
|
|
2698
|
+
sortByCreatedAt(messages),
|
|
2699
|
+
(message) => (hasValidContent(message) ? toPayloadMessage(message) : null)
|
|
2700
|
+
);
|
|
2608
2701
|
|
|
2609
2702
|
const routed = this.routing();
|
|
2610
2703
|
const payload: AgentWidgetRequestPayload = {
|