@runtypelabs/persona 4.2.0 → 4.3.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 (43) hide show
  1. package/README.md +22 -6
  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-B_xbfvR0.d.cts → types-C6tFDxKy.d.cts} +1 -1
  5. package/dist/animations/{types-B_xbfvR0.d.ts → types-C6tFDxKy.d.ts} +1 -1
  6. package/dist/animations/wipe.d.cts +1 -1
  7. package/dist/animations/wipe.d.ts +1 -1
  8. package/dist/codegen.cjs +6 -6
  9. package/dist/codegen.js +9 -9
  10. package/dist/index.cjs +44 -44
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.d.cts +101 -3
  13. package/dist/index.d.ts +101 -3
  14. package/dist/index.global.js +33 -33
  15. package/dist/index.global.js.map +1 -1
  16. package/dist/index.js +44 -44
  17. package/dist/index.js.map +1 -1
  18. package/dist/install.global.js +1 -1
  19. package/dist/install.global.js.map +1 -1
  20. package/dist/launcher.global.js.map +1 -1
  21. package/dist/smart-dom-reader.d.cts +76 -1
  22. package/dist/smart-dom-reader.d.ts +76 -1
  23. package/dist/theme-editor-preview.cjs +35 -35
  24. package/dist/theme-editor-preview.d.cts +76 -1
  25. package/dist/theme-editor-preview.d.ts +76 -1
  26. package/dist/theme-editor-preview.js +37 -37
  27. package/dist/theme-editor.cjs +1 -1
  28. package/dist/theme-editor.d.cts +76 -1
  29. package/dist/theme-editor.d.ts +76 -1
  30. package/dist/theme-editor.js +1 -1
  31. package/package.json +1 -1
  32. package/src/client.test.ts +349 -29
  33. package/src/client.ts +96 -24
  34. package/src/defaults.ts +2 -0
  35. package/src/index-core.ts +2 -0
  36. package/src/install.ts +9 -1
  37. package/src/session.ts +6 -3
  38. package/src/session.voice.test.ts +65 -0
  39. package/src/types.ts +57 -2
  40. package/src/utils/__fixtures__/unified-translator.oracle.ts +3 -3
  41. package/src/utils/code-generators.ts +10 -0
  42. package/src/utils/target.test.ts +51 -0
  43. package/src/utils/target.ts +90 -0
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 { resolveTarget } from "./utils/target";
27
28
  import { builtInClientToolsForDispatch } from "./ask-user-question-tool";
28
29
  import {
29
30
  extractTextFromJson,
@@ -185,6 +186,11 @@ export class AgentWidgetClient {
185
186
  private readonly webMcpBridge: WebMcpBridge | null;
186
187
 
187
188
  constructor(private config: AgentWidgetConfig = {}) {
189
+ if (config.target && (config.agentId || config.flowId || config.agent)) {
190
+ throw new Error(
191
+ "[Persona] `target` is mutually exclusive with `agentId`, `flowId`, and `agent`. Set only one routing field.",
192
+ );
193
+ }
188
194
  this.apiUrl = config.apiUrl ?? DEFAULT_ENDPOINT;
189
195
  this.headers = {
190
196
  "Content-Type": "application/json",
@@ -284,11 +290,33 @@ export class AgentWidgetClient {
284
290
  return !!this.config.clientToken;
285
291
  }
286
292
 
293
+ /**
294
+ * Resolve the effective backend routing for the current config. Combines the
295
+ * explicit `agentId`/`flowId` fields with the normalized `target` string
296
+ * (resolved via `resolveTarget`). Computed on demand so it stays correct
297
+ * across `update()`; the `target`/explicit-field conflict is rejected in the
298
+ * constructor, so at most one source is set here.
299
+ */
300
+ private routing(): {
301
+ agentId?: string;
302
+ flowId?: string;
303
+ targetPayload?: Record<string, unknown>;
304
+ } {
305
+ const { agentId, flowId, target, targetProviders } = this.config;
306
+ if (!target) {
307
+ return { agentId, flowId };
308
+ }
309
+ const resolved = resolveTarget(target, targetProviders);
310
+ if (resolved.kind === "agentId") return { agentId: resolved.agentId };
311
+ if (resolved.kind === "flowId") return { flowId: resolved.flowId };
312
+ return { targetPayload: resolved.payload };
313
+ }
314
+
287
315
  /**
288
316
  * Check if operating in agent execution mode
289
317
  */
290
318
  public isAgentMode(): boolean {
291
- return !!this.config.agent;
319
+ return !!(this.config.agent || this.routing().agentId);
292
320
  }
293
321
 
294
322
  /**
@@ -346,9 +374,11 @@ export class AgentWidgetClient {
346
374
  // Get stored session_id if available (for session resumption)
347
375
  const storedSessionId = this.config.getStoredSessionId?.() || null;
348
376
 
377
+ const routed = this.routing();
378
+ const sessionTargetId = routed.agentId ?? routed.flowId;
349
379
  const requestBody: Record<string, unknown> = {
350
380
  token: this.config.clientToken,
351
- ...(this.config.flowId && { flowId: this.config.flowId }),
381
+ ...(sessionTargetId && { flowId: sessionTargetId }),
352
382
  ...(storedSessionId && { sessionId: storedSessionId }),
353
383
  };
354
384
 
@@ -584,12 +614,12 @@ export class AgentWidgetClient {
584
614
  * Send a message - handles both proxy and client token modes
585
615
  */
586
616
  public async dispatch(options: DispatchOptions, onEvent: SSEHandler) {
587
- if (this.isAgentMode()) {
588
- return this.dispatchAgent(options, onEvent);
589
- }
590
617
  if (this.isClientTokenMode()) {
591
618
  return this.dispatchClientToken(options, onEvent);
592
619
  }
620
+ if (this.isAgentMode()) {
621
+ return this.dispatchAgent(options, onEvent);
622
+ }
593
623
  return this.dispatchProxy(options, onEvent);
594
624
  }
595
625
 
@@ -1040,7 +1070,8 @@ export class AgentWidgetClient {
1040
1070
  private async buildAgentPayload(
1041
1071
  messages: AgentWidgetMessage[]
1042
1072
  ): Promise<AgentWidgetAgentRequestPayload> {
1043
- if (!this.config.agent) {
1073
+ const routedAgentId = this.routing().agentId;
1074
+ if (!this.config.agent && !routedAgentId) {
1044
1075
  throw new Error('Agent configuration required for agent mode');
1045
1076
  }
1046
1077
 
@@ -1062,7 +1093,7 @@ export class AgentWidgetClient {
1062
1093
  }));
1063
1094
 
1064
1095
  const payload: AgentWidgetAgentRequestPayload = {
1065
- agent: this.config.agent,
1096
+ agent: this.config.agent ?? { agentId: routedAgentId! },
1066
1097
  messages: normalizedMessages,
1067
1098
  options: {
1068
1099
  streamResponse: true,
@@ -1134,11 +1165,27 @@ export class AgentWidgetClient {
1134
1165
  createdAt: message.createdAt
1135
1166
  }));
1136
1167
 
1168
+ const routed = this.routing();
1137
1169
  const payload: AgentWidgetRequestPayload = {
1138
1170
  messages: normalizedMessages,
1139
- ...(this.config.flowId && { flowId: this.config.flowId })
1171
+ ...(routed.agentId
1172
+ ? { agent: { agentId: routed.agentId } }
1173
+ : routed.flowId
1174
+ ? { flowId: routed.flowId }
1175
+ : {})
1140
1176
  };
1141
1177
 
1178
+ // Custom-provider targets (e.g. `eve:support`) resolve to a payload
1179
+ // fragment that is merged into the dispatch body so a BYO backend can read
1180
+ // whatever routing keys its resolver chose. `messages` is authoritative and
1181
+ // can never be overridden by a resolver.
1182
+ if (routed.targetPayload) {
1183
+ for (const [key, value] of Object.entries(routed.targetPayload)) {
1184
+ if (key === "messages") continue;
1185
+ (payload as Record<string, unknown>)[key] = value;
1186
+ }
1187
+ }
1188
+
1142
1189
  // Client tools: same built-in + WebMCP merge as buildAgentPayload
1143
1190
  // (flow-dispatch path).
1144
1191
  const clientTools = [
@@ -1378,10 +1425,10 @@ export class AgentWidgetClient {
1378
1425
  // Reference to track assistant message for custom event handler
1379
1426
  const assistantMessageRef = { current: null as AgentWidgetMessage | null };
1380
1427
  // Segmentation state for the `parseSSEEvent` extensibility callback (the
1381
- // consumer's own `partId` field) — independent of the unified wire.
1428
+ // consumer's own `partId` field) — independent of the wire.
1382
1429
  const customParsePartId = { current: null as string | null };
1383
1430
  // Unified text-channel block id (from `text_start`/`text_delta` `id`). Drives
1384
- // bubble-id segmentation on the unified wire in place of the legacy `partId`:
1431
+ // bubble-id segmentation on the wire in place of the legacy `partId`:
1385
1432
  // a new block id means a new bubble, sealed at `text_complete`/tool boundaries.
1386
1433
  let currentTextBlockId: string | null = null;
1387
1434
  // Raw text accumulated for the open flow block before its bubble is
@@ -1389,7 +1436,7 @@ export class AgentWidgetClient {
1389
1436
  let pendingFlowRaw = "";
1390
1437
  // Nested flow-as-tool attribution (PR #4602): a text/reasoning block whose
1391
1438
  // `parentToolCallId` matches a `tool_start.toolCallId` belongs to a flow
1392
- // running as that tool. Keyed by the unified block id, these route the block's
1439
+ // running as that tool. Keyed by the wire block id, these route the block's
1393
1440
  // deltas into a message tagged `agentMetadata.parentToolId` (the parent tool's
1394
1441
  // row) instead of the top-level assistant/reasoning channel.
1395
1442
  const nestedBlockParent = new Map<string, string>();
@@ -1769,7 +1816,7 @@ export class AgentWidgetClient {
1769
1816
  // `text_start`/`text_complete`) and can be structured JSON, so each block
1770
1817
  // runs through the per-bubble structured-content parser — agent text stays
1771
1818
  // plain. This is the legacy step_delta parser core, re-keyed from `partId`
1772
- // to the unified block-id bubble. The caller materializes the bubble lazily
1819
+ // to the wire block-id bubble. The caller materializes the bubble lazily
1773
1820
  // (whitespace-only blocks around tool boundaries never leave a stray bubble)
1774
1821
  // and `step_complete.result.response` reconciles the authoritative final.
1775
1822
  let lastSealedFlowBubble: AgentWidgetMessage | null = null;
@@ -1947,16 +1994,16 @@ export class AgentWidgetClient {
1947
1994
  return message;
1948
1995
  };
1949
1996
 
1950
- // Ready queue of parsed unified frames awaiting a drain. The API streams the
1951
- // neutral 33-event unified vocabulary; each frame is parsed in the SSE loop
1997
+ // Ready queue of parsed wire frames awaiting a drain. The API streams the
1998
+ // 33-event wire vocabulary; each frame is parsed in the SSE loop
1952
1999
  // below and rendered directly by the handler (no translation bridge), then
1953
- // pushed here. The unified stream is a single, in-order SSE connection, so
2000
+ // pushed here. The wire stream is a single, in-order SSE connection, so
1954
2001
  // frames drain straight through with no reordering.
1955
2002
  const seqReadyQueue: Array<{ payloadType: string; payload: any }> = [];
1956
2003
  // Declared here so later closures can reference it; assigned after all
1957
2004
  // handler-scoped variables are initialised (before the SSE loop).
1958
2005
  let drainReadyQueue: () => void;
1959
- // Per-stream media-block buffer: the unified media triad
2006
+ // Per-stream media-block buffer: the media triad
1960
2007
  // (media_start/media_delta/media_complete) is reassembled here into a single
1961
2008
  // synthetic message at media_complete, keyed by the block id.
1962
2009
  const mediaBuffers = new Map<
@@ -1967,8 +2014,17 @@ export class AgentWidgetClient {
1967
2014
  // `turn_start` advancing the iteration rotates the bubble in 'separate' mode.
1968
2015
  let lastIterationSeen = 0;
1969
2016
  // Execution kind, resolved from the leading `execution_start` frame. Drives
1970
- // the agent-vs-flow branches that the single unified vocabulary collapses.
2017
+ // the agent-vs-flow branches that the single wire vocabulary collapses.
1971
2018
  let executionKind: "agent" | "flow" = "agent";
2019
+ // Whether `executionKind` was set authoritatively by an `execution_start`
2020
+ // frame. Continuation streams (e.g. a tool-driven `/resume`) do NOT re-emit
2021
+ // `execution_start`, so a fresh `streamResponse` for the continuation starts
2022
+ // with the default `"agent"`. For a flow that mis-routes the final
2023
+ // prompt-step finalization and duplicates the last message (the streamed
2024
+ // text block is sealed, then `step_complete.result.response` re-renders it as
2025
+ // a second bubble). When `execution_start` is absent we recover the flow kind
2026
+ // from the first flow `step_*` frame below.
2027
+ let executionKindResolved = false;
1972
2028
  // Open turn id (from `turn_start`). Unified text/reasoning deltas carry their
1973
2029
  // own block id, not the turn id, so the turn id is threaded onto agentMetadata
1974
2030
  // from here.
@@ -1987,6 +2043,21 @@ export class AgentWidgetClient {
1987
2043
  const payloadType = seqReadyQueue[i].payloadType;
1988
2044
  const payload = seqReadyQueue[i].payload;
1989
2045
 
2046
+ // Recover the execution kind on continuation streams that omit
2047
+ // `execution_start` (e.g. a tool-driven `/resume`). Flow `step_*` frames
2048
+ // carry a `stepType`; agent loops never do (they use `turn_*`). Without
2049
+ // this, the continuation defaults to `"agent"` and a flow's final
2050
+ // prompt-step finalization is duplicated. We only infer when no
2051
+ // `execution_start` resolved the kind, so an explicit `agent` is never
2052
+ // overridden.
2053
+ if (
2054
+ !executionKindResolved &&
2055
+ executionKind !== "flow" &&
2056
+ typeof (payload as { stepType?: unknown }).stepType === "string"
2057
+ ) {
2058
+ executionKind = "flow";
2059
+ }
2060
+
1990
2061
  if (payloadType === "reasoning_start") {
1991
2062
  // Nested flow-as-tool thinking (PR #4602): route to the parent tool's row.
1992
2063
  const rStartBlockId = typeof payload.id === "string" ? payload.id : null;
@@ -2474,7 +2545,7 @@ export class AgentWidgetClient {
2474
2545
  }
2475
2546
 
2476
2547
  // A failed step (`success:false`) — including the legacy `step_error`
2477
- // event, which the unified encoder folds into a failed `step_complete`
2548
+ // event, which the wire encoder folds into a failed `step_complete`
2478
2549
  // — surfaces as a terminal error and finalizes the stream.
2479
2550
  if (payload.success === false) {
2480
2551
  const e = payload.error;
@@ -2543,6 +2614,7 @@ export class AgentWidgetClient {
2543
2614
  // ================================================================
2544
2615
  } else if (payloadType === "execution_start") {
2545
2616
  executionKind = payload.kind === "flow" ? "flow" : "agent";
2617
+ executionKindResolved = true;
2546
2618
  if (executionKind === "agent") {
2547
2619
  agentExecution = {
2548
2620
  executionId: payload.executionId,
@@ -2595,7 +2667,7 @@ export class AgentWidgetClient {
2595
2667
  // Authoritative args are set at tool_start; nothing to render here.
2596
2668
  continue;
2597
2669
  } else if (payloadType === "turn_complete") {
2598
- // Reasoning is sealed by its own reasoning_complete in the unified
2670
+ // Reasoning is sealed by its own reasoning_complete on the wire
2599
2671
  // vocabulary; this only attaches the turn-level stopReason to the
2600
2672
  // assistant message produced by this turn. Falls back to
2601
2673
  // lastAssistantInTurn when the bubble was sealed at a tool boundary
@@ -2615,7 +2687,7 @@ export class AgentWidgetClient {
2615
2687
  }
2616
2688
  if (openTurnId === payload.id) openTurnId = null;
2617
2689
  } else if (payloadType === "media_start") {
2618
- // Open a unified media block; buffer fragments until media_complete.
2690
+ // Open a media block; buffer fragments until media_complete.
2619
2691
  const id = String(payload.id);
2620
2692
  mediaBuffers.set(id, {
2621
2693
  mediaType: typeof payload.mediaType === "string" ? payload.mediaType : undefined,
@@ -2651,7 +2723,7 @@ export class AgentWidgetClient {
2651
2723
  if (completeData) {
2652
2724
  reconstructed = { type: "media", data: completeData, mediaType: completeMediaType };
2653
2725
  } else if (completeUrl) {
2654
- // The unified wire is mediaType-only; a URL part with no declared MIME
2726
+ // The wire is mediaType-only; a URL part with no declared MIME
2655
2727
  // arrives as the bare bucket hint "image" (per the API encoder). Treat
2656
2728
  // that — and any real `image/*` — as a hosted image so we don't misroute
2657
2729
  // generated images into the file bucket.
@@ -2807,7 +2879,7 @@ export class AgentWidgetClient {
2807
2879
 
2808
2880
  onEvent({ type: "status", status: "idle" });
2809
2881
  } else if (payloadType === "execution_error") {
2810
- // Terminal failure. The unified non-terminal `error` is handled
2882
+ // Terminal failure. The non-terminal `error` is handled
2811
2883
  // separately (recoverable → warn).
2812
2884
  const errorMessage = typeof payload.error === 'string'
2813
2885
  ? payload.error
@@ -3022,7 +3094,7 @@ export class AgentWidgetClient {
3022
3094
  // retrying" — and the execution continues, so it must NOT surface as a
3023
3095
  // fatal error or finalize the stream. The API routes terminal failures
3024
3096
  // through `execution_error`. Only an explicit `recoverable: false`
3025
- // promotes a unified `error` to terminal.
3097
+ // promotes an `error` to terminal.
3026
3098
  if (
3027
3099
  payload.recoverable === false &&
3028
3100
  payload.error != null &&
@@ -3140,7 +3212,7 @@ export class AgentWidgetClient {
3140
3212
  if (handled) continue; // Skip default handling if custom handler processed it
3141
3213
  }
3142
3214
 
3143
- // The wire is the neutral unified vocabulary; the handler consumes it
3215
+ // The wire is the wire vocabulary; the handler consumes it
3144
3216
  // natively. The stream is single-connection and in order, so each frame
3145
3217
  // drains straight through.
3146
3218
  seqReadyQueue.push({ payloadType, payload });
package/src/defaults.ts CHANGED
@@ -72,6 +72,8 @@ export const DEFAULT_WIDGET_CONFIG: Partial<AgentWidgetConfig> = {
72
72
  apiUrl: "https://api.runtype.com/api/chat/dispatch",
73
73
  // Client token mode defaults (optional, only used when clientToken is set)
74
74
  clientToken: undefined,
75
+ agentId: undefined,
76
+ target: undefined,
75
77
  theme: undefined,
76
78
  darkTheme: undefined,
77
79
  colorScheme: "light",
package/src/index-core.ts CHANGED
@@ -5,6 +5,8 @@ import {
5
5
 
6
6
  export type {
7
7
  AgentWidgetConfig,
8
+ TargetResolver,
9
+ ResolvedTarget,
8
10
  AgentWidgetFeatureFlags,
9
11
  AgentWidgetArtifactsFeature,
10
12
  AgentWidgetArtifactsLayoutConfig,
package/src/install.ts CHANGED
@@ -17,6 +17,7 @@ interface SiteAgentInstallConfig {
17
17
  // Client token mode options (can also be set via data attributes)
18
18
  clientToken?: string;
19
19
  flowId?: string;
20
+ agentId?: string;
20
21
  apiUrl?: string;
21
22
  // Optional query param key that gates widget installation in preview mode
22
23
  previewQueryParam?: string;
@@ -69,7 +70,7 @@ declare global {
69
70
 
70
71
  /**
71
72
  * Read configuration from data attributes on the current script tag.
72
- * Supports: data-config (JSON), data-runtype-token, data-flow-id, data-api-url
73
+ * Supports: data-config (JSON), data-runtype-token, data-flow-id, data-agent-id, data-api-url
73
74
  */
74
75
  const getConfigFromScript = (): Partial<SiteAgentInstallConfig> => {
75
76
  // Try to get the current script element
@@ -112,6 +113,12 @@ declare global {
112
113
  scriptConfig.flowId = flowId;
113
114
  }
114
115
 
116
+ // Optional agent ID
117
+ const agentId = script.getAttribute('data-agent-id');
118
+ if (agentId) {
119
+ scriptConfig.agentId = agentId;
120
+ }
121
+
115
122
  // Optional API URL override
116
123
  const apiUrl = script.getAttribute('data-api-url');
117
124
  if (apiUrl) {
@@ -357,6 +364,7 @@ declare global {
357
364
  if (config.apiUrl && !widgetConfig.apiUrl) widgetConfig.apiUrl = config.apiUrl;
358
365
  if (config.clientToken && !widgetConfig.clientToken) widgetConfig.clientToken = config.clientToken;
359
366
  if (config.flowId && !widgetConfig.flowId) widgetConfig.flowId = config.flowId;
367
+ if (config.agentId && !widgetConfig.agentId) widgetConfig.agentId = config.agentId;
360
368
 
361
369
  const hasApiConfig = !!(widgetConfig.apiUrl || widgetConfig.clientToken);
362
370
  return { target, widgetConfig, hasApiConfig };
package/src/session.ts CHANGED
@@ -70,6 +70,9 @@ const CONNECTION_CONFIG_KEYS = [
70
70
  "apiUrl",
71
71
  "clientToken",
72
72
  "flowId",
73
+ "agentId",
74
+ "target",
75
+ "targetProviders",
73
76
  "agent",
74
77
  "agentOptions",
75
78
  "headers",
@@ -286,7 +289,7 @@ export class AgentWidgetSession {
286
289
  // clientToken) falls back to the browser voice, so the fetch would be wasted.
287
290
  const host = tts.host ?? this.config.apiUrl;
288
291
  const agentId =
289
- tts.agentId ?? this.config.voiceRecognition?.provider?.runtype?.agentId;
292
+ tts.agentId ?? this.config.voiceRecognition?.provider?.runtype?.agentId ?? this.config.agentId;
290
293
  if (!host || !agentId || !this.config.clientToken) return;
291
294
  void loadRuntypeTts().catch(() => {});
292
295
  }
@@ -415,7 +418,7 @@ export class AgentWidgetSession {
415
418
  if (tts?.provider === "runtype") {
416
419
  const host = tts.host ?? this.config.apiUrl;
417
420
  const agentId =
418
- tts.agentId ?? this.config.voiceRecognition?.provider?.runtype?.agentId;
421
+ tts.agentId ?? this.config.voiceRecognition?.provider?.runtype?.agentId ?? this.config.agentId;
419
422
  const clientToken = this.config.clientToken;
420
423
  const wantFallback = tts.browserFallback !== false;
421
424
 
@@ -659,7 +662,7 @@ export class AgentWidgetSession {
659
662
  return {
660
663
  type: 'runtype',
661
664
  runtype: {
662
- agentId: providerConfig.runtype?.agentId || '',
665
+ agentId: providerConfig.runtype?.agentId ?? this.config.agentId ?? '',
663
666
  // Default credentials/endpoint from the widget config so the minimum
664
667
  // voice config collapses to just `agentId`.
665
668
  clientToken: providerConfig.runtype?.clientToken ?? this.config.clientToken,
@@ -49,6 +49,7 @@ vi.mock('./voice', async (importOriginal) => {
49
49
  });
50
50
 
51
51
  import { AgentWidgetSession } from './session';
52
+ import { setRuntypeTtsLoader } from './voice/runtype-tts-loader';
52
53
 
53
54
  describe('AgentWidgetSession - realtime voice onTranscript (Option B)', () => {
54
55
  let session: AgentWidgetSession;
@@ -144,3 +145,67 @@ describe('AgentWidgetSession - realtime voice onTranscript (Option B)', () => {
144
145
  expect(metricsSeen).toEqual([{ llmMs: 100, totalMs: 250 }]);
145
146
  });
146
147
  });
148
+
149
+ describe('AgentWidgetSession - Runtype TTS config', () => {
150
+ it('uses top-level agentId as the default Runtype TTS agent', async () => {
151
+ let capturedOptions: { agentId?: string; clientToken?: string; host?: string } | null = null;
152
+
153
+ class FakeRuntypeSpeechEngine {
154
+ readonly id = 'runtype';
155
+ readonly supportsPause = false;
156
+
157
+ constructor(options: { agentId?: string; clientToken?: string; host?: string }) {
158
+ capturedOptions = options;
159
+ }
160
+
161
+ speak() {}
162
+ pause() {}
163
+ resume() {}
164
+ stop() {}
165
+ }
166
+
167
+ class FakeFallbackSpeechEngine {}
168
+
169
+ setRuntypeTtsLoader(async () => ({
170
+ RuntypeSpeechEngine: FakeRuntypeSpeechEngine as any,
171
+ FallbackSpeechEngine: FakeFallbackSpeechEngine as any,
172
+ }));
173
+
174
+ try {
175
+ const session = new AgentWidgetSession(
176
+ {
177
+ apiUrl: 'https://api.runtype.com',
178
+ clientToken: 'ct_live_demo',
179
+ agentId: 'agent_top_level',
180
+ textToSpeech: { enabled: true, provider: 'runtype', browserFallback: false },
181
+ initialMessages: [
182
+ {
183
+ id: 'assistant-1',
184
+ role: 'assistant',
185
+ content: 'Read this',
186
+ createdAt: new Date().toISOString(),
187
+ },
188
+ ],
189
+ },
190
+ {
191
+ onMessagesChanged: () => {},
192
+ onStatusChanged: () => {},
193
+ onStreamingChanged: () => {},
194
+ onError: () => {},
195
+ },
196
+ );
197
+
198
+ session.toggleReadAloud('assistant-1');
199
+ await Promise.resolve();
200
+ await Promise.resolve();
201
+
202
+ expect(capturedOptions).toMatchObject({
203
+ agentId: 'agent_top_level',
204
+ clientToken: 'ct_live_demo',
205
+ host: 'https://api.runtype.com',
206
+ });
207
+ } finally {
208
+ setRuntypeTtsLoader(null);
209
+ }
210
+ });
211
+ });
package/src/types.ts CHANGED
@@ -5,6 +5,9 @@ import type {
5
5
  RuntypeClientFeedbackRequest,
6
6
  RuntypeStopReasonKind,
7
7
  } from "./generated/runtype-openapi-contract";
8
+ import type { TargetResolver } from "./utils/target";
9
+
10
+ export type { TargetResolver, ResolvedTarget } from "./utils/target";
8
11
 
9
12
  // ============================================================================
10
13
  // Multi-Modal Content Types
@@ -100,6 +103,7 @@ export type AgentWidgetRequestPayloadMessage = {
100
103
  export type AgentWidgetRequestPayload = {
101
104
  messages: AgentWidgetRequestPayloadMessage[];
102
105
  flowId?: string;
106
+ agent?: { agentId: string };
103
107
  context?: Record<string, unknown>;
104
108
  metadata?: Record<string, unknown>;
105
109
  /** Per-turn template variables for /v1/client/chat (merged as root-level {{var}} in Runtype). */
@@ -212,6 +216,11 @@ export type AgentConfig = {
212
216
  loopConfig?: AgentLoopConfig;
213
217
  };
214
218
 
219
+ export type SavedAgentConfig = {
220
+ /** Persisted Runtype agent ID to execute. */
221
+ agentId: string;
222
+ };
223
+
215
224
  /**
216
225
  * Options for agent execution requests.
217
226
  */
@@ -230,7 +239,7 @@ export type AgentRequestOptions = {
230
239
  * Request payload for agent execution mode.
231
240
  */
232
241
  export type AgentWidgetAgentRequestPayload = {
233
- agent: AgentConfig;
242
+ agent: AgentConfig | SavedAgentConfig;
234
243
  messages: AgentWidgetRequestPayloadMessage[];
235
244
  options: AgentRequestOptions;
236
245
  context?: Record<string, unknown>;
@@ -3573,6 +3582,52 @@ export type AgentWidgetLoadingIndicatorConfig = {
3573
3582
  export type AgentWidgetConfig = {
3574
3583
  apiUrl?: string;
3575
3584
  flowId?: string;
3585
+ /**
3586
+ * Persisted Runtype agent ID to execute.
3587
+ *
3588
+ * This is the primary Runtype entry point for simple agent-backed widgets.
3589
+ * It is mutually exclusive with `flowId` and with inline `agent` configs.
3590
+ * Voice and Runtype TTS providers inherit this value unless they specify their
3591
+ * own feature-scoped agent ID.
3592
+ */
3593
+ agentId?: string;
3594
+ /**
3595
+ * Normalized, backend-neutral routing target. A single string that selects
3596
+ * the backend resource to talk to, optimized for portability across
3597
+ * frameworks (it is always serializable: no live objects).
3598
+ *
3599
+ * Shapes:
3600
+ * - Runtype TypeID (no prefix): `"agent_…"` / `"flow_…"` route to the
3601
+ * Runtype agent/flow paths (the prefix is self-describing).
3602
+ * - Provider-prefixed: `"<provider>:<id>"` is resolved by
3603
+ * `targetProviders[provider]` (e.g. `"eve:support"`). `"runtype:…"` is a
3604
+ * built-in that re-detects a TypeID.
3605
+ * - Bare name: `"support"` requires a `targetProviders.default` resolver.
3606
+ *
3607
+ * Mutually exclusive with `agentId`, `flowId`, and inline `agent`. Prefer
3608
+ * `target` for backend-agnostic apps; use `agentId`/`flowId` for the explicit
3609
+ * Runtype path.
3610
+ *
3611
+ * @example
3612
+ * ```typescript
3613
+ * config: { target: "agent_01k..." } // Runtype agent
3614
+ * config: { target: "flow_01k..." } // Runtype flow
3615
+ * config: { // custom backend
3616
+ * apiUrl: "/api/chat",
3617
+ * target: "eve:support",
3618
+ * targetProviders: { eve: (id) => ({ payload: { assistant: id } }) },
3619
+ * }
3620
+ * ```
3621
+ */
3622
+ target?: string;
3623
+ /**
3624
+ * Prefix-keyed resolvers for `target` strings of the form
3625
+ * `"<provider>:<id>"`. Each resolver maps the id to the dispatch-payload
3626
+ * fragment its backend expects. Register a `default` resolver to also accept
3627
+ * bare (unprefixed, non-TypeID) target names. The built-in `runtype` provider
3628
+ * is always available and does not need to be registered.
3629
+ */
3630
+ targetProviders?: Record<string, TargetResolver>;
3576
3631
  /**
3577
3632
  * Override the assistant-bubble copy shown when a dispatch fails before any
3578
3633
  * response streams back (connection refused, CORS, 4xx/5xx, malformed
@@ -4265,7 +4320,7 @@ export type AgentWidgetReasoning = {
4265
4320
  status: "pending" | "streaming" | "complete";
4266
4321
  chunks: string[];
4267
4322
  /**
4268
- * Reasoning channel scope (unified spec). `"turn"` is ordinary per-turn
4323
+ * Reasoning channel scope (wire spec). `"turn"` is ordinary per-turn
4269
4324
  * thinking; `"loop"` is a cross-iteration agent reflection (the fold that
4270
4325
  * replaced the legacy `agent_reflection` event). Absent for legacy streams.
4271
4326
  */
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * TEST ORACLE — vendored copy of the runtype-core `createUnifiedEventWrite`
3
- * translator (the api-side legacy → unified 33-event encoder).
3
+ * translator (the api-side legacy → wire 33-event encoder).
4
4
  *
5
- * NOT shipped. Used only by `unified-event-bridge.test.ts` as the inverse
6
- * reference: `legacy frames → THIS oracle → unified frames → UnifiedToLegacyBridge
5
+ * NOT shipped. Used only by legacy-wire handler tests as the inverse
6
+ * reference: `legacy frames → THIS oracle → wire frames → client handlers
7
7
  * → legacy frames'` must be semantically equal to the original. Keeping a real
8
8
  * copy here means the round-trip test fails loudly if the api mapping and the
9
9
  * widget bridge ever drift.
@@ -605,6 +605,8 @@ function generateESMCode(config: any, options?: CodeGeneratorOptions): string {
605
605
 
606
606
  if (config.apiUrl) lines.push(` apiUrl: "${config.apiUrl}",`);
607
607
  if (config.clientToken) lines.push(` clientToken: "${config.clientToken}",`);
608
+ if (config.agentId) lines.push(` agentId: "${config.agentId}",`);
609
+ if (config.target) lines.push(` target: "${config.target}",`);
608
610
  if (config.flowId) lines.push(` flowId: "${config.flowId}",`);
609
611
  if (shouldEmitParserType) lines.push(` parserType: "${parserType}",`);
610
612
 
@@ -751,6 +753,8 @@ function generateReactComponentCode(config: any, options?: CodeGeneratorOptions)
751
753
 
752
754
  if (config.apiUrl) lines.push(` apiUrl: "${config.apiUrl}",`);
753
755
  if (config.clientToken) lines.push(` clientToken: "${config.clientToken}",`);
756
+ if (config.agentId) lines.push(` agentId: "${config.agentId}",`);
757
+ if (config.target) lines.push(` target: "${config.target}",`);
754
758
  if (config.flowId) lines.push(` flowId: "${config.flowId}",`);
755
759
  if (shouldEmitParserType) lines.push(` parserType: "${parserType}",`);
756
760
 
@@ -1015,6 +1019,8 @@ function generateReactAdvancedCode(config: any, options?: CodeGeneratorOptions):
1015
1019
 
1016
1020
  if (config.apiUrl) lines.push(` apiUrl: "${config.apiUrl}",`);
1017
1021
  if (config.clientToken) lines.push(` clientToken: "${config.clientToken}",`);
1022
+ if (config.agentId) lines.push(` agentId: "${config.agentId}",`);
1023
+ if (config.target) lines.push(` target: "${config.target}",`);
1018
1024
  if (config.flowId) lines.push(` flowId: "${config.flowId}",`);
1019
1025
 
1020
1026
  if (config.theme && typeof config.theme === "object" && Object.keys(config.theme).length > 0) {
@@ -1270,6 +1276,8 @@ function buildSerializableConfig(config: any): Record<string, any> {
1270
1276
 
1271
1277
  if (config.apiUrl) serializableConfig.apiUrl = config.apiUrl;
1272
1278
  if (config.clientToken) serializableConfig.clientToken = config.clientToken;
1279
+ if (config.agentId) serializableConfig.agentId = config.agentId;
1280
+ if (config.target) serializableConfig.target = config.target;
1273
1281
  if (config.flowId) serializableConfig.flowId = config.flowId;
1274
1282
  if (shouldEmitParserType) serializableConfig.parserType = parserType;
1275
1283
  if (config.theme) serializableConfig.theme = config.theme;
@@ -1419,6 +1427,8 @@ function generateScriptManualCode(config: any, options?: CodeGeneratorOptions):
1419
1427
 
1420
1428
  if (config.apiUrl) lines.push(` apiUrl: "${config.apiUrl}",`);
1421
1429
  if (config.clientToken) lines.push(` clientToken: "${config.clientToken}",`);
1430
+ if (config.agentId) lines.push(` agentId: "${config.agentId}",`);
1431
+ if (config.target) lines.push(` target: "${config.target}",`);
1422
1432
  if (config.flowId) lines.push(` flowId: "${config.flowId}",`);
1423
1433
  if (shouldEmitParserType) lines.push(` parserType: "${parserType}",`);
1424
1434
 
@@ -0,0 +1,51 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { resolveTarget } from "./target";
3
+
4
+ describe("resolveTarget", () => {
5
+ it("resolves a Runtype agent TypeID to agentId routing", () => {
6
+ expect(resolveTarget("agent_01k")).toEqual({
7
+ kind: "agentId",
8
+ agentId: "agent_01k",
9
+ });
10
+ });
11
+
12
+ it("resolves a Runtype flow TypeID to flowId routing", () => {
13
+ expect(resolveTarget("flow_01k")).toEqual({
14
+ kind: "flowId",
15
+ flowId: "flow_01k",
16
+ });
17
+ });
18
+
19
+ it("accepts an explicit runtype: prefix on a TypeID", () => {
20
+ expect(resolveTarget("runtype:agent_01k")).toEqual({
21
+ kind: "agentId",
22
+ agentId: "agent_01k",
23
+ });
24
+ });
25
+
26
+ it("routes a provider-prefixed target through its registered resolver", () => {
27
+ const resolved = resolveTarget("eve:support", {
28
+ eve: (id) => ({ payload: { assistant: id } }),
29
+ });
30
+ expect(resolved).toEqual({ kind: "payload", payload: { assistant: "support" } });
31
+ });
32
+
33
+ it("uses a default resolver for a bare name when registered", () => {
34
+ const resolved = resolveTarget("support", {
35
+ default: (id) => ({ payload: { target: id } }),
36
+ });
37
+ expect(resolved).toEqual({ kind: "payload", payload: { target: "support" } });
38
+ });
39
+
40
+ it("throws for an unknown provider prefix", () => {
41
+ expect(() => resolveTarget("eve:support")).toThrow(/no target provider/i);
42
+ });
43
+
44
+ it("throws for a bare name with no TypeID and no default resolver", () => {
45
+ expect(() => resolveTarget("support")).toThrow(/support/);
46
+ });
47
+
48
+ it("throws for an empty target", () => {
49
+ expect(() => resolveTarget(" ")).toThrow(/empty/i);
50
+ });
51
+ });