@runtypelabs/persona 3.36.0 → 3.37.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.
@@ -4891,6 +4891,16 @@ type AgentWidgetConfig = {
4891
4891
  * ```
4892
4892
  */
4893
4893
  parseSSEEvent?: AgentWidgetSSEEventParser;
4894
+ /**
4895
+ * Wire vocabulary requested from the dispatch endpoint.
4896
+ * - `'legacy'` (default): the current `agent_*` / `flow_*` SSE events.
4897
+ * - `'unified'`: opt into the API's neutral unified event vocabulary by sending
4898
+ * `?events=unified`; incoming frames are transparently bridged back to the same
4899
+ * handlers, so rendering is unchanged. Requires an upstream that honors the
4900
+ * param — the widget detects the actual wire mode from the first stream frame
4901
+ * and falls back to legacy automatically if the param was ignored.
4902
+ */
4903
+ events?: "legacy" | "unified";
4894
4904
  /**
4895
4905
  * Called for every parsed SSE frame (after JSON parse), before native handling.
4896
4906
  * Use for lightweight side effects (e.g. telemetry). Does not replace native
@@ -4891,6 +4891,16 @@ type AgentWidgetConfig = {
4891
4891
  * ```
4892
4892
  */
4893
4893
  parseSSEEvent?: AgentWidgetSSEEventParser;
4894
+ /**
4895
+ * Wire vocabulary requested from the dispatch endpoint.
4896
+ * - `'legacy'` (default): the current `agent_*` / `flow_*` SSE events.
4897
+ * - `'unified'`: opt into the API's neutral unified event vocabulary by sending
4898
+ * `?events=unified`; incoming frames are transparently bridged back to the same
4899
+ * handlers, so rendering is unchanged. Requires an upstream that honors the
4900
+ * param — the widget detects the actual wire mode from the first stream frame
4901
+ * and falls back to legacy automatically if the param was ignored.
4902
+ */
4903
+ events?: "legacy" | "unified";
4894
4904
  /**
4895
4905
  * Called for every parsed SSE frame (after JSON parse), before native handling.
4896
4906
  * Use for lightweight side effects (e.g. telemetry). Does not replace native
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runtypelabs/persona",
3
- "version": "3.36.0",
3
+ "version": "3.37.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",
@@ -4220,3 +4220,101 @@ describe('AgentWidgetClient - version header', () => {
4220
4220
  expect(headers[0]['X-Persona-Version']).toBe('override');
4221
4221
  });
4222
4222
  });
4223
+
4224
+ describe('AgentWidgetClient - Unified event vocabulary (events: "unified")', () => {
4225
+ const unifiedTextStream = (execId: string) => [
4226
+ sseEvent('execution_start', { kind: 'agent', executionId: execId, agentId: 'virtual', agentName: 'Test', maxTurns: 1, startedAt: new Date().toISOString(), seq: 1 }),
4227
+ sseEvent('turn_start', { executionId: execId, id: 'turn_1', iteration: 1, role: 'assistant', seq: 2 }),
4228
+ sseEvent('text_start', { executionId: execId, id: 'text_1', role: 'assistant', seq: 3 }),
4229
+ sseEvent('text_delta', { executionId: execId, id: 'text_1', delta: 'Hello', seq: 4 }),
4230
+ sseEvent('text_delta', { executionId: execId, id: 'text_1', delta: ' World', seq: 5 }),
4231
+ sseEvent('text_complete', { executionId: execId, id: 'text_1', seq: 6 }),
4232
+ sseEvent('turn_complete', { executionId: execId, id: 'turn_1', iteration: 1, role: 'assistant', completedAt: new Date().toISOString(), seq: 7 }),
4233
+ sseEvent('execution_complete', { kind: 'agent', executionId: execId, success: true, completedAt: new Date().toISOString(), seq: 8 }),
4234
+ ];
4235
+
4236
+ it('appends ?events=unified to the dispatch URL when opted in', async () => {
4237
+ let capturedUrl = '';
4238
+ global.fetch = vi.fn().mockImplementation(async (url: string) => {
4239
+ capturedUrl = url;
4240
+ const encoder = new TextEncoder();
4241
+ const stream = new ReadableStream({
4242
+ start(c) {
4243
+ for (const e of unifiedTextStream('exec_u')) c.enqueue(encoder.encode(e));
4244
+ c.close();
4245
+ },
4246
+ });
4247
+ return { ok: true, body: stream };
4248
+ });
4249
+
4250
+ const client = new AgentWidgetClient({
4251
+ apiUrl: 'http://localhost:8000',
4252
+ events: 'unified',
4253
+ agent: { name: 'Test', model: 'openai:gpt-4o-mini', systemPrompt: 'test' },
4254
+ });
4255
+ await client.dispatch(
4256
+ { messages: [{ id: 'u1', role: 'user', content: 'Hi', createdAt: new Date().toISOString() }] },
4257
+ () => {}
4258
+ );
4259
+ expect(capturedUrl).toContain('events=unified');
4260
+ });
4261
+
4262
+ it('renders a unified stream by bridging it back to the legacy handlers', async () => {
4263
+ const events: AgentWidgetEvent[] = [];
4264
+ const execId = 'exec_u1';
4265
+ global.fetch = createAgentStreamFetch(unifiedTextStream(execId));
4266
+
4267
+ const client = new AgentWidgetClient({
4268
+ apiUrl: 'http://localhost:8000',
4269
+ events: 'unified',
4270
+ agent: { name: 'Test', model: 'openai:gpt-4o-mini', systemPrompt: 'test' },
4271
+ });
4272
+ await client.dispatch(
4273
+ { messages: [{ id: 'u1', role: 'user', content: 'Hi', createdAt: new Date().toISOString() }] },
4274
+ (e) => events.push(e)
4275
+ );
4276
+
4277
+ const messageEvents = events.filter((e) => e.type === 'message');
4278
+ expect(messageEvents.length).toBeGreaterThan(0);
4279
+ const last = messageEvents[messageEvents.length - 1];
4280
+ expect(last.type).toBe('message');
4281
+ if (last.type === 'message') {
4282
+ expect(last.message.content).toBe('Hello World');
4283
+ expect(last.message.streaming).toBe(false);
4284
+ expect(last.message.role).toBe('assistant');
4285
+ expect(last.message.agentMetadata?.executionId).toBe(execId);
4286
+ }
4287
+ });
4288
+
4289
+ it('auto-falls-back to legacy when "unified" was requested but the API streamed legacy frames', async () => {
4290
+ const events: AgentWidgetEvent[] = [];
4291
+ const execId = 'exec_legacy_fallback';
4292
+ // Same content, LEGACY vocabulary — the API ignored ?events=unified.
4293
+ global.fetch = createAgentStreamFetch([
4294
+ sseEvent('agent_start', { executionId: execId, agentId: 'virtual', agentName: 'Test', maxTurns: 1, startedAt: new Date().toISOString(), seq: 1 }),
4295
+ sseEvent('agent_turn_start', { executionId: execId, iteration: 1, turnId: 'turn_1', role: 'assistant', seq: 2 }),
4296
+ sseEvent('agent_turn_delta', { executionId: execId, iteration: 1, delta: 'Hello', contentType: 'text', turnId: 'turn_1', seq: 3 }),
4297
+ sseEvent('agent_turn_delta', { executionId: execId, iteration: 1, delta: ' World', contentType: 'text', turnId: 'turn_1', seq: 4 }),
4298
+ sseEvent('agent_turn_complete', { executionId: execId, iteration: 1, turnId: 'turn_1', completedAt: new Date().toISOString(), seq: 5 }),
4299
+ sseEvent('agent_complete', { executionId: execId, success: true, stopReason: 'max_iterations', completedAt: new Date().toISOString(), seq: 6 }),
4300
+ ]);
4301
+
4302
+ const client = new AgentWidgetClient({
4303
+ apiUrl: 'http://localhost:8000',
4304
+ events: 'unified',
4305
+ agent: { name: 'Test', model: 'openai:gpt-4o-mini', systemPrompt: 'test' },
4306
+ });
4307
+ await client.dispatch(
4308
+ { messages: [{ id: 'u1', role: 'user', content: 'Hi', createdAt: new Date().toISOString() }] },
4309
+ (e) => events.push(e)
4310
+ );
4311
+
4312
+ const messageEvents = events.filter((e) => e.type === 'message');
4313
+ const last = messageEvents[messageEvents.length - 1];
4314
+ expect(last.type).toBe('message');
4315
+ if (last.type === 'message') {
4316
+ expect(last.message.content).toBe('Hello World');
4317
+ expect(last.message.streaming).toBe(false);
4318
+ }
4319
+ });
4320
+ });
package/src/client.ts CHANGED
@@ -33,6 +33,7 @@ import {
33
33
  createXmlParser
34
34
  } from "./utils/formatting";
35
35
  import { SequenceReorderBuffer } from "./utils/sequence-buffer";
36
+ import { UnifiedToLegacyBridge, isUnifiedLifecycleStart } from "./utils/unified-event-bridge";
36
37
  import { VERSION } from "./version";
37
38
  // artifactsSidebarEnabled is used in ui.ts to gate the sidebar pane rendering;
38
39
  // artifact events are always processed here regardless of config.
@@ -292,6 +293,17 @@ export class AgentWidgetClient {
292
293
  return !!this.config.agent;
293
294
  }
294
295
 
296
+ /**
297
+ * Append `?events=unified` when the widget opted into the unified vocabulary,
298
+ * preserving any existing query string. No-op for the default legacy mode.
299
+ * The server only switches vocabularies when it sees the param; the widget
300
+ * still auto-detects the actual wire mode from the first stream frame.
301
+ */
302
+ private withEventsParam(url: string): string {
303
+ if (this.config.events !== "unified") return url;
304
+ return url + (url.includes("?") ? "&" : "?") + "events=unified";
305
+ }
306
+
295
307
  /**
296
308
  * Get the appropriate API URL based on mode
297
309
  */
@@ -682,7 +694,7 @@ export class AgentWidgetClient {
682
694
  console.debug("[AgentWidgetClient] client token dispatch", chatRequest);
683
695
  }
684
696
 
685
- response = await fetch(this.getClientApiUrl('chat'), {
697
+ response = await fetch(this.withEventsParam(this.getClientApiUrl('chat')), {
686
698
  method: 'POST',
687
699
  headers: {
688
700
  'Content-Type': 'application/json',
@@ -802,7 +814,7 @@ export class AgentWidgetClient {
802
814
  if (this.customFetch) {
803
815
  try {
804
816
  response = await this.customFetch(
805
- this.apiUrl,
817
+ this.withEventsParam(this.apiUrl),
806
818
  {
807
819
  method: "POST",
808
820
  headers,
@@ -817,7 +829,7 @@ export class AgentWidgetClient {
817
829
  throw err;
818
830
  }
819
831
  } else {
820
- response = await fetch(this.apiUrl, {
832
+ response = await fetch(this.withEventsParam(this.apiUrl), {
821
833
  method: "POST",
822
834
  headers,
823
835
  body: JSON.stringify(payload),
@@ -878,7 +890,7 @@ export class AgentWidgetClient {
878
890
  if (this.customFetch) {
879
891
  try {
880
892
  response = await this.customFetch(
881
- this.apiUrl,
893
+ this.withEventsParam(this.apiUrl),
882
894
  {
883
895
  method: "POST",
884
896
  headers,
@@ -893,7 +905,7 @@ export class AgentWidgetClient {
893
905
  throw err;
894
906
  }
895
907
  } else {
896
- response = await fetch(this.apiUrl, {
908
+ response = await fetch(this.withEventsParam(this.apiUrl), {
897
909
  method: "POST",
898
910
  headers,
899
911
  body: JSON.stringify(payload),
@@ -996,9 +1008,11 @@ export class AgentWidgetClient {
996
1008
  options?: { streamResponse?: boolean; signal?: AbortSignal }
997
1009
  ): Promise<Response> {
998
1010
  const isClientToken = this.isClientTokenMode();
999
- const url = isClientToken
1000
- ? this.getClientApiUrl('resume')
1001
- : `${this.config.apiUrl?.replace(/\/+$/, '') || DEFAULT_CLIENT_API_BASE}/resume`;
1011
+ const url = this.withEventsParam(
1012
+ isClientToken
1013
+ ? this.getClientApiUrl('resume')
1014
+ : `${this.config.apiUrl?.replace(/\/+$/, '') || DEFAULT_CLIENT_API_BASE}/resume`
1015
+ );
1002
1016
 
1003
1017
  // The client-token resume route authenticates the session, not a Bearer
1004
1018
  // key. A WebMCP approval can sit awaiting user input for a long time, so by
@@ -1833,6 +1847,17 @@ export class AgentWidgetClient {
1833
1847
  seqReadyQueue.push({ payloadType, payload });
1834
1848
  scheduleReadyQueueDrain();
1835
1849
  });
1850
+ // Unified-event consumer (opt-in; see utils/unified-event-bridge.ts). When the
1851
+ // API streams the neutral unified vocabulary, each frame is transduced back to
1852
+ // the legacy events the dispatch chain below already renders. Seed the mode from
1853
+ // the requested vocabulary so /resume (whose stream continues mid-run with no
1854
+ // `execution_start`) bridges correctly; a leading lifecycle frame is then
1855
+ // authoritative and can flip it (e.g. an upstream that ignored `?events=unified`
1856
+ // streams `agent_start`). The unified stream is single-connection and in order,
1857
+ // so bridged events bypass the reorder buffer.
1858
+ const unifiedBridge = new UnifiedToLegacyBridge();
1859
+ let unifiedMode = this.config.events === "unified";
1860
+ let wireModeResolved = false;
1836
1861
  // Agent execution state tracking
1837
1862
  let agentExecution: AgentExecutionState | null = null;
1838
1863
  // Track assistant messages per agent iteration for 'separate' mode
@@ -3373,6 +3398,31 @@ export class AgentWidgetClient {
3373
3398
  if (handled) continue; // Skip default handling if custom handler processed it
3374
3399
  }
3375
3400
 
3401
+ // Unified vocabulary (opt-in): resolve the wire mode from the first
3402
+ // lifecycle frame (the flag only requests the param; the frame is
3403
+ // authoritative), then transduce unified → legacy, bypassing the reorder
3404
+ // buffer (the unified stream is single-connection and already in order).
3405
+ if (!wireModeResolved) {
3406
+ if (isUnifiedLifecycleStart(payloadType)) {
3407
+ unifiedMode = true;
3408
+ wireModeResolved = true;
3409
+ } else if (
3410
+ payloadType === "agent_start" ||
3411
+ payloadType === "flow_start" ||
3412
+ payloadType === "step_start"
3413
+ ) {
3414
+ unifiedMode = false;
3415
+ wireModeResolved = true;
3416
+ }
3417
+ }
3418
+ if (unifiedMode) {
3419
+ for (const legacyEvent of unifiedBridge.push(payloadType, payload)) {
3420
+ seqReadyQueue.push(legacyEvent);
3421
+ }
3422
+ drainReadyQueue();
3423
+ continue;
3424
+ }
3425
+
3376
3426
  // Push through the sequence reorder buffer
3377
3427
  seqBuffer.push(payloadType, payload);
3378
3428
  drainReadyQueue();
@@ -1190,6 +1190,7 @@ export type RuntypeClientChatRequest = {
1190
1190
  type: "object";
1191
1191
  [key: string]: unknown;
1192
1192
  };
1193
+ untrustedContentHint?: boolean;
1193
1194
  }>;
1194
1195
  clientToolsFingerprint?: string;
1195
1196
  inputs?: Record<string, unknown>;
package/src/install.ts CHANGED
@@ -18,6 +18,9 @@ interface SiteAgentInstallConfig {
18
18
  clientToken?: string;
19
19
  flowId?: string;
20
20
  apiUrl?: string;
21
+ /** Wire vocabulary requested from the dispatch endpoint; 'unified' opts into the
22
+ * API's neutral event schema (transparently bridged back to the same rendering). */
23
+ events?: "legacy" | "unified";
21
24
  // Optional query param key that gates widget installation in preview mode
22
25
  previewQueryParam?: string;
23
26
  // Shadow DOM option (defaults to false for better CSS compatibility)
@@ -377,6 +380,7 @@ declare global {
377
380
  if (config.apiUrl && !widgetConfig.apiUrl) widgetConfig.apiUrl = config.apiUrl;
378
381
  if (config.clientToken && !widgetConfig.clientToken) widgetConfig.clientToken = config.clientToken;
379
382
  if (config.flowId && !widgetConfig.flowId) widgetConfig.flowId = config.flowId;
383
+ if (config.events && !widgetConfig.events) widgetConfig.events = config.events;
380
384
 
381
385
  const hasApiConfig = !!(widgetConfig.apiUrl || widgetConfig.clientToken);
382
386
  return { target, widgetConfig, hasApiConfig };
package/src/types.ts CHANGED
@@ -4056,6 +4056,16 @@ export type AgentWidgetConfig = {
4056
4056
  * ```
4057
4057
  */
4058
4058
  parseSSEEvent?: AgentWidgetSSEEventParser;
4059
+ /**
4060
+ * Wire vocabulary requested from the dispatch endpoint.
4061
+ * - `'legacy'` (default): the current `agent_*` / `flow_*` SSE events.
4062
+ * - `'unified'`: opt into the API's neutral unified event vocabulary by sending
4063
+ * `?events=unified`; incoming frames are transparently bridged back to the same
4064
+ * handlers, so rendering is unchanged. Requires an upstream that honors the
4065
+ * param — the widget detects the actual wire mode from the first stream frame
4066
+ * and falls back to legacy automatically if the param was ignored.
4067
+ */
4068
+ events?: "legacy" | "unified";
4059
4069
  /**
4060
4070
  * Called for every parsed SSE frame (after JSON parse), before native handling.
4061
4071
  * Use for lightweight side effects (e.g. telemetry). Does not replace native