@runtypelabs/persona 3.31.1 → 3.33.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.
Files changed (45) hide show
  1. package/README.md +2 -0
  2. package/dist/animations/glyph-cycle.d.cts +1 -1
  3. package/dist/animations/glyph-cycle.d.ts +1 -1
  4. package/dist/animations/{types-quh7NmYD.d.cts → types-CthJFfNx.d.cts} +7 -0
  5. package/dist/animations/{types-quh7NmYD.d.ts → types-CthJFfNx.d.ts} +7 -0
  6. package/dist/animations/wipe.d.cts +1 -1
  7. package/dist/animations/wipe.d.ts +1 -1
  8. package/dist/codegen.cjs +1 -1
  9. package/dist/codegen.js +1 -1
  10. package/dist/index.cjs +43 -43
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.d.cts +288 -8
  13. package/dist/index.d.ts +288 -8
  14. package/dist/index.global.js +51 -51
  15. package/dist/index.global.js.map +1 -1
  16. package/dist/index.js +43 -43
  17. package/dist/index.js.map +1 -1
  18. package/dist/launcher.global.js.map +1 -1
  19. package/dist/smart-dom-reader.d.cts +78 -2
  20. package/dist/smart-dom-reader.d.ts +78 -2
  21. package/dist/theme-editor.cjs +30 -30
  22. package/dist/theme-editor.d.cts +92 -5
  23. package/dist/theme-editor.d.ts +92 -5
  24. package/dist/theme-editor.js +30 -30
  25. package/package.json +1 -1
  26. package/src/ask-user-question-tool.test.ts +148 -0
  27. package/src/ask-user-question-tool.ts +138 -0
  28. package/src/client.ts +43 -9
  29. package/src/components/messages.ts +10 -0
  30. package/src/components/suggestions.ts +49 -6
  31. package/src/index-core.ts +15 -0
  32. package/src/runtime/host-layout.test.ts +158 -3
  33. package/src/runtime/host-layout.ts +95 -1
  34. package/src/session.ts +188 -32
  35. package/src/session.webmcp.test.ts +48 -0
  36. package/src/suggest-replies-tool.test.ts +445 -0
  37. package/src/suggest-replies-tool.ts +152 -0
  38. package/src/theme-editor/index.ts +2 -0
  39. package/src/theme-editor/webmcp/index.ts +2 -0
  40. package/src/theme-editor/webmcp/types.ts +16 -3
  41. package/src/types.ts +79 -2
  42. package/src/ui.suggest-replies.test.ts +237 -0
  43. package/src/ui.ts +57 -13
  44. package/src/utils/dock.test.ts +23 -1
  45. package/src/utils/dock.ts +2 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runtypelabs/persona",
3
- "version": "3.31.1",
3
+ "version": "3.33.0",
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",
@@ -0,0 +1,148 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import {
3
+ ASK_USER_QUESTION_CLIENT_TOOL,
4
+ ASK_USER_QUESTION_PARAMETERS_SCHEMA,
5
+ builtInClientToolsForDispatch,
6
+ } from "./ask-user-question-tool";
7
+ import {
8
+ ASK_USER_QUESTION_MAX,
9
+ ASK_USER_QUESTION_TOOL_NAME,
10
+ } from "./components/ask-user-question-bubble";
11
+ import { AgentWidgetClient } from "./client";
12
+ import { computeClientToolsFingerprint } from "./webmcp-bridge";
13
+ import type { AgentWidgetConfig } from "./types";
14
+
15
+ describe("ASK_USER_QUESTION_CLIENT_TOOL definition", () => {
16
+ it("matches the renderer's tool name and origin/annotation contract", () => {
17
+ expect(ASK_USER_QUESTION_CLIENT_TOOL.name).toBe(ASK_USER_QUESTION_TOOL_NAME);
18
+ // `'sdk'` keeps the bare name on the wire (the server only prefixes
19
+ // `origin: 'webmcp'` tools) so the step_await routes to the answer sheet,
20
+ // not the WebMCP bridge. `'local'` is NOT a valid server-side origin.
21
+ expect(ASK_USER_QUESTION_CLIENT_TOOL.origin).toBe("sdk");
22
+ expect(ASK_USER_QUESTION_CLIENT_TOOL.annotations?.readOnlyHint).toBe(true);
23
+ });
24
+
25
+ it("bounds questions to the renderer cap and options to 2-4", () => {
26
+ const questions = ASK_USER_QUESTION_PARAMETERS_SCHEMA.properties.questions;
27
+ expect(questions.minItems).toBe(1);
28
+ expect(questions.maxItems).toBe(ASK_USER_QUESTION_MAX);
29
+ const options = questions.items.properties.options;
30
+ expect(options.minItems).toBe(2);
31
+ expect(options.maxItems).toBe(4);
32
+ expect(questions.items.required).toEqual(["question", "options"]);
33
+ });
34
+ });
35
+
36
+ describe("builtInClientToolsForDispatch", () => {
37
+ it("returns nothing by default (expose is opt-in)", () => {
38
+ expect(builtInClientToolsForDispatch(undefined)).toEqual([]);
39
+ expect(builtInClientToolsForDispatch({} as AgentWidgetConfig)).toEqual([]);
40
+ expect(
41
+ builtInClientToolsForDispatch({
42
+ features: { askUserQuestion: {} },
43
+ } as AgentWidgetConfig)
44
+ ).toEqual([]);
45
+ });
46
+
47
+ it("returns the tool when expose is true", () => {
48
+ expect(
49
+ builtInClientToolsForDispatch({
50
+ features: { askUserQuestion: { expose: true } },
51
+ } as AgentWidgetConfig)
52
+ ).toEqual([ASK_USER_QUESTION_CLIENT_TOOL]);
53
+ });
54
+
55
+ it("ignores expose when the answer sheet is disabled", () => {
56
+ // Offering the agent a question tool the widget can't render an answer
57
+ // UI for would park the execution on a generic tool bubble forever.
58
+ expect(
59
+ builtInClientToolsForDispatch({
60
+ features: { askUserQuestion: { expose: true, enabled: false } },
61
+ } as AgentWidgetConfig)
62
+ ).toEqual([]);
63
+ });
64
+ });
65
+
66
+ describe("AgentWidgetClient - built-in ask_user_question exposure", () => {
67
+ const captureDispatchBody = () => {
68
+ const captured: { body: string | null } = { body: null };
69
+ global.fetch = vi
70
+ .fn()
71
+ .mockImplementation(async (_url: string, options: { body: string }) => {
72
+ captured.body = options.body;
73
+ const encoder = new TextEncoder();
74
+ const stream = new ReadableStream({
75
+ start(controller) {
76
+ controller.enqueue(encoder.encode('data: {"type":"done"}\n\n'));
77
+ controller.close();
78
+ },
79
+ });
80
+ return new Response(stream, {
81
+ headers: { "Content-Type": "text/event-stream" },
82
+ });
83
+ });
84
+ return captured;
85
+ };
86
+
87
+ const userMessage = () => ({
88
+ id: "u1",
89
+ role: "user" as const,
90
+ content: "hi",
91
+ createdAt: new Date().toISOString(),
92
+ });
93
+
94
+ it("ships ask_user_question on clientTools when expose is on (no WebMCP)", async () => {
95
+ const captured = captureDispatchBody();
96
+ const client = new AgentWidgetClient({
97
+ apiUrl: "http://localhost:8000",
98
+ features: { askUserQuestion: { expose: true } },
99
+ });
100
+ await client.dispatch({ messages: [userMessage()] }, () => undefined);
101
+ const parsed = JSON.parse(captured.body!);
102
+ expect(parsed.clientTools).toEqual([ASK_USER_QUESTION_CLIENT_TOOL]);
103
+ });
104
+
105
+ it("omits clientTools entirely when expose is off and no WebMCP tools exist", async () => {
106
+ const captured = captureDispatchBody();
107
+ const client = new AgentWidgetClient({
108
+ apiUrl: "http://localhost:8000",
109
+ });
110
+ await client.dispatch({ messages: [userMessage()] }, () => undefined);
111
+ const parsed = JSON.parse(captured.body!);
112
+ expect(parsed.clientTools).toBeUndefined();
113
+ });
114
+
115
+ it("leads the WebMCP snapshot when both are present", async () => {
116
+ const captured = captureDispatchBody();
117
+ const client = new AgentWidgetClient({
118
+ apiUrl: "http://localhost:8000",
119
+ features: { askUserQuestion: { expose: true } },
120
+ });
121
+ (
122
+ client as unknown as {
123
+ webMcpBridge: { snapshotForDispatch: () => unknown[] } | null;
124
+ }
125
+ ).webMcpBridge = {
126
+ snapshotForDispatch: () => [
127
+ { name: "search", description: "s", origin: "webmcp" },
128
+ ],
129
+ };
130
+ await client.dispatch({ messages: [userMessage()] }, () => undefined);
131
+ const parsed = JSON.parse(captured.body!);
132
+ expect(parsed.clientTools).toEqual([
133
+ ASK_USER_QUESTION_CLIENT_TOOL,
134
+ { name: "search", description: "s", origin: "webmcp" },
135
+ ]);
136
+ });
137
+
138
+ it("changes the clientTools fingerprint when toggled (diff-only resend)", () => {
139
+ // In client-token mode the widget only resends the full clientTools[]
140
+ // when the fingerprint changes — toggling expose must change it so the
141
+ // server actually learns about (or forgets) the built-in tool.
142
+ const webMcpOnly = [{ name: "search", description: "s" }];
143
+ const withBuiltIn = [ASK_USER_QUESTION_CLIENT_TOOL, ...webMcpOnly];
144
+ expect(computeClientToolsFingerprint(withBuiltIn)).not.toBe(
145
+ computeClientToolsFingerprint(webMcpOnly)
146
+ );
147
+ });
148
+ });
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Built-in `ask_user_question` client tool.
3
+ *
4
+ * The widget can advertise this tool to the agent on every dispatch via
5
+ * `clientTools[]` (set `features.askUserQuestion.expose: true`) — the same
6
+ * wire surface WebMCP page tools ride. The server registers it as a LOCAL
7
+ * tool under its bare name (`origin: 'sdk'` tools are not `webmcp:`-prefixed),
8
+ * so when the model calls it the execution pauses with a `step_await`
9
+ * (`awaitReason: "local_tool_required"`), the widget's answer-pill sheet
10
+ * renders, and `session.resolveAskUserQuestion()` resumes the execution with
11
+ * the structured answers.
12
+ *
13
+ * This replaces the previous integrator burden of hand-declaring the tool in
14
+ * a flow's `runtimeTools` and keeping that schema in sync with the renderer.
15
+ * Flows that already declare `ask_user_question` server-side should leave
16
+ * `expose` off — the model would otherwise see the tool twice.
17
+ */
18
+
19
+ import {
20
+ ASK_USER_QUESTION_MAX,
21
+ ASK_USER_QUESTION_TOOL_NAME,
22
+ } from "./components/ask-user-question-bubble";
23
+ import {
24
+ SUGGEST_REPLIES_CLIENT_TOOL,
25
+ shouldExposeSuggestReplies,
26
+ } from "./suggest-replies-tool";
27
+ import type { AgentWidgetConfig, ClientToolDefinition } from "./types";
28
+
29
+ /**
30
+ * JSON Schema for the tool's parameters. Mirrors {@link AskUserQuestionPayload}
31
+ * — the shape `parseAskUserQuestionPayload` hydrates the answer sheet from —
32
+ * so the schema the model is held to and the schema the renderer expects can
33
+ * never drift.
34
+ */
35
+ export const ASK_USER_QUESTION_PARAMETERS_SCHEMA = {
36
+ type: "object",
37
+ properties: {
38
+ questions: {
39
+ type: "array",
40
+ minItems: 1,
41
+ maxItems: ASK_USER_QUESTION_MAX,
42
+ description: "Questions to ask the user. Prefer a single question.",
43
+ items: {
44
+ type: "object",
45
+ properties: {
46
+ question: {
47
+ type: "string",
48
+ description: "The complete question, ending with a question mark.",
49
+ },
50
+ header: {
51
+ type: "string",
52
+ maxLength: 12,
53
+ description: 'Short topic label, e.g. "Auth method".',
54
+ },
55
+ options: {
56
+ type: "array",
57
+ minItems: 2,
58
+ maxItems: 4,
59
+ description:
60
+ '2-4 distinct choices. Do NOT add an "Other" option — free text is automatic.',
61
+ items: {
62
+ type: "object",
63
+ properties: {
64
+ label: {
65
+ type: "string",
66
+ description: "Concise choice text (1-5 words).",
67
+ },
68
+ description: {
69
+ type: "string",
70
+ description: "What the option means or implies.",
71
+ },
72
+ },
73
+ required: ["label"],
74
+ additionalProperties: false,
75
+ },
76
+ },
77
+ multiSelect: {
78
+ type: "boolean",
79
+ description: "Allow selecting multiple options. Default false.",
80
+ },
81
+ allowFreeText: {
82
+ type: "boolean",
83
+ description: "Show a free-text input. Default true.",
84
+ },
85
+ },
86
+ required: ["question", "options"],
87
+ additionalProperties: false,
88
+ },
89
+ },
90
+ },
91
+ required: ["questions"],
92
+ additionalProperties: false,
93
+ } as const;
94
+
95
+ /**
96
+ * The `ClientToolDefinition` shipped on `dispatch.clientTools[]` when
97
+ * `features.askUserQuestion.expose` is on. Exported so integrators who prefer
98
+ * declaring the tool server-side (a flow's `runtimeTools`) can reuse the same
99
+ * description and schema instead of hand-writing them.
100
+ */
101
+ export const ASK_USER_QUESTION_CLIENT_TOOL: ClientToolDefinition = {
102
+ name: ASK_USER_QUESTION_TOOL_NAME,
103
+ description:
104
+ "Ask the user multiple-choice questions and wait for their answers. Use " +
105
+ "only when blocked on a decision that is the user's to make — a " +
106
+ "preference, a choice between valid approaches, or information you " +
107
+ "cannot infer. Each question offers 2-4 options plus an automatic " +
108
+ "free-text input. The result maps each question to its answer (an array " +
109
+ "when multiSelect); a question absent from the result was skipped.",
110
+ parametersSchema: ASK_USER_QUESTION_PARAMETERS_SCHEMA,
111
+ origin: "sdk",
112
+ annotations: { readOnlyHint: true },
113
+ };
114
+
115
+ /**
116
+ * Built-in client tools to append to a dispatch's `clientTools[]` for the
117
+ * given widget config: `ask_user_question` and `suggest_replies`; future
118
+ * built-in local tools join here.
119
+ *
120
+ * Each tool is gated on BOTH of its feature flags: `expose` opts the tool
121
+ * into the agent's catalog, and `enabled !== false` guarantees the widget can
122
+ * actually render UI (and, for fire-and-forget tools, auto-resume) for it —
123
+ * exposing a tool with its feature disabled would park the execution on a
124
+ * generic tool bubble with no way to advance.
125
+ */
126
+ export const builtInClientToolsForDispatch = (
127
+ config: AgentWidgetConfig | undefined,
128
+ ): ClientToolDefinition[] => {
129
+ const tools: ClientToolDefinition[] = [];
130
+ const ask = config?.features?.askUserQuestion;
131
+ if (ask?.expose === true && ask.enabled !== false) {
132
+ tools.push(ASK_USER_QUESTION_CLIENT_TOOL);
133
+ }
134
+ if (shouldExposeSuggestReplies(config)) {
135
+ tools.push(SUGGEST_REPLIES_CLIENT_TOOL);
136
+ }
137
+ return tools;
138
+ };
package/src/client.ts CHANGED
@@ -24,6 +24,7 @@ import {
24
24
  WebMcpConfirmHandler
25
25
  } from "./types";
26
26
  import { WebMcpBridge, computeClientToolsFingerprint, isWebMcpToolName } from "./webmcp-bridge";
27
+ import { builtInClientToolsForDispatch } from "./ask-user-question-tool";
27
28
  import {
28
29
  extractTextFromJson,
29
30
  createPlainTextParser,
@@ -201,6 +202,28 @@ export class AgentWidgetClient {
201
202
  config.webmcp?.enabled === true ? new WebMcpBridge(config.webmcp) : null;
202
203
  }
203
204
 
205
+ /**
206
+ * Refresh config in place WITHOUT tearing down the live connection or the
207
+ * WebMCP bridge. `AgentWidgetSession.updateConfig` calls this when only
208
+ * connection-irrelevant fields changed (theme, copy, layout, suggestions, …),
209
+ * so a UI update that lands mid-turn — e.g. a `webmcp:*` tool restyling the
210
+ * widget while the agent's turn is still streaming — doesn't abandon the
211
+ * in-flight stream/resume. Connection or request-shaping changes (apiUrl,
212
+ * clientToken, webmcp, headers, parser, …) take the full client rebuild path
213
+ * in the session instead, which is the only place the bridge is recreated.
214
+ *
215
+ * Only the live-read `config` is refreshed (e.g. `iterationDisplay`); the
216
+ * constructor-derived request-shaping fields (apiUrl, headers, parser,
217
+ * contextProviders, middleware, …) are left untouched because the session
218
+ * routes any change to those down the full-rebuild path instead — so they are
219
+ * guaranteed unchanged here. The `webMcpBridge` instance and its
220
+ * installed-polyfill memo are deliberately preserved, which keeps any
221
+ * in-flight resolve alive.
222
+ */
223
+ public updateConfig(next: AgentWidgetConfig): void {
224
+ this.config = next;
225
+ }
226
+
204
227
  /**
205
228
  * Set callback for capturing raw SSE events
206
229
  */
@@ -1035,11 +1058,18 @@ export class AgentWidgetClient {
1035
1058
  }
1036
1059
  };
1037
1060
 
1038
- // WebMCP: snapshot the page tool registry per turn and ship as
1039
- // `clientTools[]`. The server merges these under the `webmcp:` namespace.
1040
- const webMcpSnapshot = await this.webMcpBridge?.snapshotForDispatch();
1041
- if (webMcpSnapshot && webMcpSnapshot.length > 0) {
1042
- payload.clientTools = webMcpSnapshot;
1061
+ // Client tools: built-in widget tools (ask_user_question, when exposed)
1062
+ // plus the per-turn WebMCP page-registry snapshot. Name collisions are
1063
+ // impossible WebMCP entries are `webmcp:`-prefixed server-side while
1064
+ // `sdk`-origin built-ins keep bare names. Both kinds ride the same
1065
+ // diff-only fingerprint path in client-token mode. Kept to a single await
1066
+ // so dispatch microtask timing is unchanged.
1067
+ const clientTools = [
1068
+ ...builtInClientToolsForDispatch(this.config),
1069
+ ...((await this.webMcpBridge?.snapshotForDispatch()) ?? []),
1070
+ ];
1071
+ if (clientTools.length > 0) {
1072
+ payload.clientTools = clientTools;
1043
1073
  }
1044
1074
 
1045
1075
  // Add context from providers
@@ -1096,10 +1126,14 @@ export class AgentWidgetClient {
1096
1126
  ...(this.config.flowId && { flowId: this.config.flowId })
1097
1127
  };
1098
1128
 
1099
- // WebMCP: same per-turn snapshot as buildAgentPayload (flow-dispatch path).
1100
- const webMcpSnapshot = await this.webMcpBridge?.snapshotForDispatch();
1101
- if (webMcpSnapshot && webMcpSnapshot.length > 0) {
1102
- payload.clientTools = webMcpSnapshot;
1129
+ // Client tools: same built-in + WebMCP merge as buildAgentPayload
1130
+ // (flow-dispatch path).
1131
+ const clientTools = [
1132
+ ...builtInClientToolsForDispatch(this.config),
1133
+ ...((await this.webMcpBridge?.snapshotForDispatch()) ?? []),
1134
+ ];
1135
+ if (clientTools.length > 0) {
1136
+ payload.clientTools = clientTools;
1103
1137
  }
1104
1138
 
1105
1139
  if (this.contextProviders.length) {
@@ -9,6 +9,7 @@ import {
9
9
  isAskUserQuestionMessage,
10
10
  removeAskUserQuestionSheet,
11
11
  } from "./ask-user-question-bubble";
12
+ import { isSuggestRepliesMessage } from "../suggest-replies-tool";
12
13
 
13
14
  export const renderMessages = (
14
15
  container: HTMLElement,
@@ -40,6 +41,15 @@ export const renderMessages = (
40
41
  ensureAskUserQuestionSheet(message, config, composerOverlay ?? null);
41
42
  }
42
43
  return;
44
+ } else if (
45
+ isSuggestRepliesMessage(message) &&
46
+ config?.features?.suggestReplies?.enabled !== false
47
+ ) {
48
+ // No transcript bubble — the chips above the composer are the only UI.
49
+ // When the feature is disabled the message falls through to the generic
50
+ // tool bubble below (and is never auto-resumed), making the parked
51
+ // execution visible instead of silently swallowed.
52
+ return;
43
53
  } else if (message.variant === "tool" && message.toolCall) {
44
54
  if (!showToolCalls) return;
45
55
  bubble = createToolBubble(message, config);
@@ -9,29 +9,50 @@ export interface SuggestionButtons {
9
9
  session: AgentWidgetSession,
10
10
  textarea: HTMLTextAreaElement,
11
11
  messages?: AgentWidgetMessage[],
12
- config?: AgentWidgetSuggestionChipsConfig
12
+ config?: AgentWidgetSuggestionChipsConfig,
13
+ opts?: SuggestionRenderOptions
13
14
  ) => void;
14
15
  }
15
16
 
17
+ export interface SuggestionRenderOptions {
18
+ /**
19
+ * Chips pushed by the agent's `suggest_replies` tool rather than the
20
+ * static `suggestionChips` config. Skips the before-first-user-message
21
+ * gate (the caller already applied the latest-turn visibility rule) and
22
+ * dispatches `persona:suggestReplies:*` DOM events.
23
+ */
24
+ agentPushed?: boolean;
25
+ }
26
+
16
27
  export const createSuggestions = (container: HTMLElement): SuggestionButtons => {
17
28
  const suggestionButtons: HTMLButtonElement[] = [];
29
+ // render() runs on every message change; only announce agent-pushed chips
30
+ // when the visible set actually changes, not on each re-render pass.
31
+ let lastAgentShownKey: string | null = null;
18
32
 
19
33
  const render = (
20
34
  chips: string[] | undefined,
21
35
  session: AgentWidgetSession,
22
36
  textarea: HTMLTextAreaElement,
23
37
  messages?: AgentWidgetMessage[],
24
- chipsConfig?: AgentWidgetSuggestionChipsConfig
38
+ chipsConfig?: AgentWidgetSuggestionChipsConfig,
39
+ opts?: SuggestionRenderOptions
25
40
  ) => {
26
41
  container.innerHTML = "";
27
42
  suggestionButtons.length = 0;
43
+ const agentPushed = opts?.agentPushed === true;
44
+ if (!agentPushed) lastAgentShownKey = null;
28
45
  if (!chips || !chips.length) return;
29
46
 
30
- // Hide suggestions after the first user message is sent
47
+ // Hide config suggestions after the first user message is sent.
48
+ // Agent-pushed chips skip this gate — their visibility is the caller's
49
+ // latest-turn rule (last suggest_replies call with no user message after).
31
50
  // Use provided messages or get from session
32
- const messagesToCheck = messages ?? (session ? session.getMessages() : []);
33
- const hasUserMessage = messagesToCheck.some((msg) => msg.role === "user");
34
- if (hasUserMessage) return;
51
+ if (!agentPushed) {
52
+ const messagesToCheck = messages ?? (session ? session.getMessages() : []);
53
+ const hasUserMessage = messagesToCheck.some((msg) => msg.role === "user");
54
+ if (hasUserMessage) return;
55
+ }
35
56
 
36
57
  const fragment = document.createDocumentFragment();
37
58
  const streaming = session ? session.isStreaming() : false;
@@ -79,12 +100,34 @@ export const createSuggestions = (container: HTMLElement): SuggestionButtons =>
79
100
  btn.addEventListener("click", () => {
80
101
  if (!session || session.isStreaming()) return;
81
102
  textarea.value = "";
103
+ if (agentPushed) {
104
+ container.dispatchEvent(
105
+ new CustomEvent("persona:suggestReplies:selected", {
106
+ detail: { suggestion: chip },
107
+ bubbles: true,
108
+ composed: true,
109
+ })
110
+ );
111
+ }
82
112
  session.sendMessage(chip);
83
113
  });
84
114
  fragment.appendChild(btn);
85
115
  suggestionButtons.push(btn);
86
116
  });
87
117
  container.appendChild(fragment);
118
+ if (agentPushed) {
119
+ const shownKey = JSON.stringify(chips);
120
+ if (shownKey !== lastAgentShownKey) {
121
+ lastAgentShownKey = shownKey;
122
+ container.dispatchEvent(
123
+ new CustomEvent("persona:suggestReplies:shown", {
124
+ detail: { suggestions: [...chips] },
125
+ bubbles: true,
126
+ composed: true,
127
+ })
128
+ );
129
+ }
130
+ }
88
131
  };
89
132
 
90
133
  return {
package/src/index-core.ts CHANGED
@@ -134,6 +134,21 @@ export {
134
134
  parseAskUserQuestionPayload
135
135
  } from "./components/ask-user-question-bubble";
136
136
 
137
+ export {
138
+ ASK_USER_QUESTION_CLIENT_TOOL,
139
+ ASK_USER_QUESTION_PARAMETERS_SCHEMA,
140
+ builtInClientToolsForDispatch
141
+ } from "./ask-user-question-tool";
142
+
143
+ export {
144
+ SUGGEST_REPLIES_TOOL_NAME,
145
+ SUGGEST_REPLIES_CLIENT_TOOL,
146
+ SUGGEST_REPLIES_PARAMETERS_SCHEMA,
147
+ isSuggestRepliesMessage,
148
+ parseSuggestRepliesPayload,
149
+ latestAgentSuggestions
150
+ } from "./suggest-replies-tool";
151
+
137
152
  export { initAgentWidgetFn as initAgentWidget };
138
153
  export {
139
154
  createWidgetHostLayout,