@runtypelabs/persona 4.2.1 → 4.3.1

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 +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 +32 -32
  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 +23 -23
  15. package/dist/index.global.js.map +1 -1
  16. package/dist/index.js +32 -32
  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 +27 -27
  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 +29 -29
  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/dist/widget.css +3 -0
  32. package/package.json +1 -1
  33. package/src/client.test.ts +221 -29
  34. package/src/client.ts +71 -24
  35. package/src/defaults.ts +2 -0
  36. package/src/index-core.ts +2 -0
  37. package/src/install.ts +9 -1
  38. package/src/session.ts +6 -3
  39. package/src/session.voice.test.ts +65 -0
  40. package/src/styles/widget.css +3 -0
  41. package/src/types.ts +57 -2
  42. package/src/utils/__fixtures__/unified-translator.oracle.ts +3 -3
  43. package/src/utils/code-generators.ts +10 -0
  44. package/src/utils/target.test.ts +51 -0
  45. package/src/utils/target.ts +90 -0
@@ -469,12 +469,12 @@ describe('AgentWidgetClient - JSON Streaming', () => {
469
469
  'data: {"type":"flow_complete","flowId":"flow_01k9pfnztzfag9tfz4t65c9c5q","success":true,"duration":2968,"completedAt":"2025-11-12T23:47:42.234Z","totalTokensUsed":0}'
470
470
  ];
471
471
 
472
- // Route the legacy step_chunk fixtures through the oracle as the 4.0 unified
473
- // wire (step_chunk → step_delta → text_delta), exercising the structured
474
- // JSON parser on the unified flow path: incremental text extraction, never
472
+ // Route the legacy step_chunk fixtures through the oracle as the 4.0 wire
473
+ // (step_chunk → step_delta → text_delta), exercising the structured
474
+ // JSON parser on the wire flow path: incremental text extraction, never
475
475
  // showing raw JSON, with the assembled response reconciled at step_complete.
476
476
  global.fetch = createRawStreamFetch(
477
- toUnifiedFrames(
477
+ legacyToWireFrames(
478
478
  sseEvents
479
479
  .filter((f) => f.startsWith('data:'))
480
480
  .map((f) => f.replace('"type":"step_chunk"', '"type":"step_delta"') + '\n\n')
@@ -581,13 +581,13 @@ function sseEvent(eventType: string, data: Record<string, unknown>): string {
581
581
 
582
582
  /**
583
583
  * Re-encode legacy `agent_*` / `flow_*` / `step_*` / `tool_*` SSE frames into the
584
- * neutral UNIFIED wire the 4.0 API now emits, using the same encoder the API uses
584
+ * Persona wire the 4.0 API now emits, using the same encoder the API uses
585
585
  * (the vendored `createUnifiedEventWrite` oracle). The 4.0 widget only consumes the
586
- * unified vocabulary, so these handler tests author the rendering intent in the
586
+ * wire vocabulary, so these handler tests author the rendering intent in the
587
587
  * (more readable) legacy frames and inject exactly what the client sees off the
588
588
  * wire — the bridge translates it straight back before the dispatch chain renders.
589
589
  */
590
- function toUnifiedFrames(legacyFrames: string[]): string[] {
590
+ function legacyToWireFrames(legacyFrames: string[]): string[] {
591
591
  const out: string[] = [];
592
592
  const write = createUnifiedEventWrite((chunk) => out.push(chunk));
593
593
  for (const frame of legacyFrames) write(frame);
@@ -595,7 +595,7 @@ function toUnifiedFrames(legacyFrames: string[]): string[] {
595
595
  }
596
596
 
597
597
  /** Stream pre-built SSE frames verbatim (no re-encode) — for fixtures already in
598
- * the unified vocabulary. */
598
+ * the wire vocabulary. */
599
599
  function createRawStreamFetch(frames: string[]) {
600
600
  return vi.fn().mockImplementation(async (_url?: string, _options?: any) => {
601
601
  const encoder = new TextEncoder();
@@ -610,10 +610,10 @@ function createRawStreamFetch(frames: string[]) {
610
610
  }
611
611
 
612
612
  /**
613
- * Mock fetch that streams the given legacy events as the 4.0 unified wire.
613
+ * Mock fetch that streams the given legacy events as the 4.0 wire.
614
614
  */
615
615
  function createAgentStreamFetch(events: string[]) {
616
- return createRawStreamFetch(toUnifiedFrames(events));
616
+ return createRawStreamFetch(legacyToWireFrames(events));
617
617
  }
618
618
 
619
619
  describe('AgentWidgetClient - Agent Mode Detection', () => {
@@ -638,7 +638,199 @@ describe('AgentWidgetClient - Agent Mode Detection', () => {
638
638
  });
639
639
  });
640
640
 
641
+ describe('AgentWidgetClient - target routing', () => {
642
+ const userMessage = (): AgentWidgetMessage[] => [
643
+ { id: 'u1', role: 'user', content: 'hi', createdAt: '2025-01-01T00:00:00.000Z' },
644
+ ];
645
+
646
+ it('routes a Runtype agent TypeID target through agent mode', async () => {
647
+ let captured: any = null;
648
+ global.fetch = vi.fn().mockImplementation(async (_url: string, options: any) => {
649
+ captured = JSON.parse(options.body);
650
+ const encoder = new TextEncoder();
651
+ return {
652
+ ok: true,
653
+ body: new ReadableStream({
654
+ start(controller) {
655
+ controller.enqueue(encoder.encode(sseEvent('agent_complete', {
656
+ executionId: 'exec_1', agentId: 'agent_123', success: true, iterations: 1,
657
+ completedAt: new Date().toISOString(), seq: 1,
658
+ })));
659
+ controller.close();
660
+ },
661
+ }),
662
+ };
663
+ });
664
+
665
+ const client = new AgentWidgetClient({ apiUrl: 'http://localhost:8000', target: 'agent_123' });
666
+ expect(client.isAgentMode()).toBe(true);
667
+ await client.dispatch({ messages: userMessage() }, () => {});
668
+
669
+ expect(captured.agent).toEqual({ agentId: 'agent_123' });
670
+ });
671
+
672
+ it('routes a Runtype flow TypeID target through flow dispatch', async () => {
673
+ let captured: any = null;
674
+ global.fetch = vi.fn().mockImplementation(async (_url: string, options: any) => {
675
+ captured = JSON.parse(options.body);
676
+ const encoder = new TextEncoder();
677
+ return {
678
+ ok: true,
679
+ body: new ReadableStream({
680
+ start(controller) {
681
+ controller.enqueue(encoder.encode('data: {"type":"flow_complete","success":true}\n\n'));
682
+ controller.close();
683
+ },
684
+ }),
685
+ };
686
+ });
687
+
688
+ const client = new AgentWidgetClient({ apiUrl: 'http://localhost:8000', target: 'flow_123' });
689
+ expect(client.isAgentMode()).toBe(false);
690
+ await client.dispatch({ messages: userMessage() }, () => {});
691
+
692
+ expect(captured.flowId).toBe('flow_123');
693
+ expect(captured.agent).toBeUndefined();
694
+ });
695
+
696
+ it('spreads a custom provider target payload into the proxy dispatch body', async () => {
697
+ let captured: any = null;
698
+ global.fetch = vi.fn().mockImplementation(async (_url: string, options: any) => {
699
+ captured = JSON.parse(options.body);
700
+ const encoder = new TextEncoder();
701
+ return {
702
+ ok: true,
703
+ body: new ReadableStream({
704
+ start(controller) {
705
+ controller.enqueue(encoder.encode('data: {"type":"flow_complete","success":true}\n\n'));
706
+ controller.close();
707
+ },
708
+ }),
709
+ };
710
+ });
711
+
712
+ const client = new AgentWidgetClient({
713
+ apiUrl: 'http://localhost:8000',
714
+ target: 'eve:support',
715
+ targetProviders: { eve: (id) => ({ payload: { assistant: id } }) },
716
+ });
717
+ expect(client.isAgentMode()).toBe(false);
718
+ await client.dispatch({ messages: userMessage() }, () => {});
719
+
720
+ expect(captured.assistant).toBe('support');
721
+ expect(Array.isArray(captured.messages)).toBe(true);
722
+ });
723
+
724
+ it('throws when target is combined with agentId', () => {
725
+ expect(
726
+ () => new AgentWidgetClient({ apiUrl: 'http://localhost:8000', target: 'agent_1', agentId: 'agent_2' }),
727
+ ).toThrow(/mutually exclusive/i);
728
+ });
729
+ });
730
+
641
731
  describe('AgentWidgetClient - Agent Payload Building', () => {
732
+ it('should build a saved agent-id payload from top-level agentId', async () => {
733
+ let capturedPayload: any = null;
734
+ global.fetch = vi.fn().mockImplementation(async (_url: string, options: any) => {
735
+ capturedPayload = JSON.parse(options.body);
736
+ const encoder = new TextEncoder();
737
+ const stream = new ReadableStream({
738
+ start(controller) {
739
+ controller.enqueue(encoder.encode(sseEvent('agent_complete', {
740
+ executionId: 'exec_1',
741
+ agentId: 'agent_123',
742
+ success: true,
743
+ iterations: 1,
744
+ completedAt: new Date().toISOString(),
745
+ seq: 1,
746
+ })));
747
+ controller.close();
748
+ }
749
+ });
750
+ return { ok: true, body: stream };
751
+ });
752
+
753
+ const client = new AgentWidgetClient({
754
+ apiUrl: 'http://localhost:8000',
755
+ agentId: 'agent_123',
756
+ });
757
+
758
+ await client.dispatch({
759
+ messages: [{
760
+ id: 'usr_1',
761
+ role: 'user',
762
+ content: 'Hello saved agent',
763
+ createdAt: '2025-01-01T00:00:00.000Z',
764
+ }],
765
+ }, () => {});
766
+
767
+ expect(capturedPayload).toBeDefined();
768
+ expect(capturedPayload.agent).toEqual({ agentId: 'agent_123' });
769
+ expect(capturedPayload.flowId).toBeUndefined();
770
+ expect(capturedPayload.messages).toHaveLength(1);
771
+ expect(capturedPayload.messages[0].content).toBe('Hello saved agent');
772
+ expect(capturedPayload.options.streamResponse).toBe(true);
773
+ expect(capturedPayload.options.recordMode).toBe('virtual');
774
+ });
775
+
776
+ it('uses top-level agentId as the client-token session target', async () => {
777
+ const requests: Array<{ url: string; body: Record<string, unknown> }> = [];
778
+ global.fetch = vi.fn().mockImplementation(async (url: string, options: any) => {
779
+ requests.push({ url, body: JSON.parse(options.body) });
780
+ if (url.endsWith('/v1/client/init')) {
781
+ return {
782
+ ok: true,
783
+ json: async () => ({
784
+ sessionId: 'sess_agent',
785
+ expiresAt: new Date(Date.now() + 5 * 60_000).toISOString(),
786
+ config: {},
787
+ }),
788
+ };
789
+ }
790
+ const encoder = new TextEncoder();
791
+ return {
792
+ ok: true,
793
+ body: new ReadableStream({
794
+ start(controller) {
795
+ controller.enqueue(encoder.encode(sseEvent('agent_complete', {
796
+ executionId: 'exec_1',
797
+ agentId: 'agent_123',
798
+ success: true,
799
+ iterations: 1,
800
+ completedAt: new Date().toISOString(),
801
+ seq: 1,
802
+ })));
803
+ controller.close();
804
+ },
805
+ }),
806
+ };
807
+ });
808
+
809
+ const client = new AgentWidgetClient({
810
+ apiUrl: 'https://api.runtype.com',
811
+ clientToken: 'ct_live_demo',
812
+ agentId: 'agent_123',
813
+ });
814
+
815
+ await client.dispatch({
816
+ messages: [{
817
+ id: 'usr_1',
818
+ role: 'user',
819
+ content: 'Hello agent token',
820
+ createdAt: '2025-01-01T00:00:00.000Z',
821
+ }],
822
+ }, () => {});
823
+
824
+ expect(requests[0]).toMatchObject({
825
+ url: 'https://api.runtype.com/v1/client/init',
826
+ body: { token: 'ct_live_demo', flowId: 'agent_123' },
827
+ });
828
+ expect(requests[1]).toMatchObject({
829
+ url: 'https://api.runtype.com/v1/client/chat',
830
+ body: { sessionId: 'sess_agent' },
831
+ });
832
+ });
833
+
642
834
  it('should build agent payload with agent config', async () => {
643
835
  let capturedPayload: any = null;
644
836
  global.fetch = vi.fn().mockImplementation(async (_url: string, options: any) => {
@@ -1239,7 +1431,7 @@ describe('AgentWidgetClient - Agent Event Streaming', () => {
1239
1431
  }
1240
1432
  }
1241
1433
 
1242
- // Reflection now folds into a loop-scoped reasoning bubble (unified spec):
1434
+ // Reflection now folds into a loop-scoped reasoning bubble (wire spec):
1243
1435
  // `agent_reflection` → reasoning_start{scope:"loop"} + reasoning_complete{text}.
1244
1436
  const reflectionMessages = Array.from(messagesById.values())
1245
1437
  .filter(m => m.variant === 'reasoning' && m.reasoning?.scope === 'loop');
@@ -1296,7 +1488,7 @@ describe('AgentWidgetClient - Agent Event Streaming', () => {
1296
1488
  });
1297
1489
 
1298
1490
  // ============================================================================
1299
- // Unified Event Name Support (chunk → delta, agent_tool_* → tool_* with agentContext)
1491
+ // Wire event name support (chunk → delta, agent_tool_* → tool_* with agentContext)
1300
1492
  // ============================================================================
1301
1493
 
1302
1494
  // ============================================================================
@@ -1590,7 +1782,7 @@ describe('AgentWidgetClient - partId Text/Tool Interleaving', () => {
1590
1782
  expect(assistantTexts.length).toBe(2);
1591
1783
  // First message uses the provided ID
1592
1784
  expect(assistantTexts[0].id).toBe('ast_pre_generated_id');
1593
- // Second message composes baseId + the unified text block id for traceability
1785
+ // Second message composes baseId + the wire text block id for traceability
1594
1786
  expect(assistantTexts[1].id).toBe('ast_pre_generated_id_text_2');
1595
1787
  // Content is correct per segment
1596
1788
  expect(assistantTexts[0].content).toBe('Before tool.');
@@ -1600,7 +1792,7 @@ describe('AgentWidgetClient - partId Text/Tool Interleaving', () => {
1600
1792
  it('should not overwrite last segment content with the full step response (flow)', async () => {
1601
1793
  const events: AgentWidgetEvent[] = [];
1602
1794
 
1603
- // Unified wire: two flow text segments split by a tool, each its own block
1795
+ // Wire: two flow text segments split by a tool, each its own block
1604
1796
  // (sealed by text_end → text_complete). The step's full structured response
1605
1797
  // (`step_complete.result.response`) reconciles rawContent without clobbering
1606
1798
  // either sealed bubble's displayed content.
@@ -1715,10 +1907,10 @@ describe('AgentWidgetClient - partId Text/Tool Interleaving', () => {
1715
1907
  };
1716
1908
  };
1717
1909
 
1718
- // Unified wire (via the oracle): a single flow text block carrying partial
1910
+ // Wire (via the oracle): a single flow text block carrying partial
1719
1911
  // structured JSON, sealed by text_end, then the authoritative final structured
1720
1912
  // response on step_complete. Exercises the async structured-content parser +
1721
- // sealed-segment reconciliation on the unified flow path.
1913
+ // sealed-segment reconciliation on the wire flow path.
1722
1914
  global.fetch = createAgentStreamFetch([
1723
1915
  sseEvent('flow_start', { flowId: 'f1', flowName: 'Test', totalSteps: 1 }),
1724
1916
  sseEvent('tool_start', { toolId: 'tc_1', name: 'test_tool', toolType: 'custom', startedAt: new Date().toISOString() }),
@@ -1914,7 +2106,7 @@ describe('AgentWidgetClient - flow continuation stream without execution_start',
1914
2106
 
1915
2107
  describe('AgentWidgetClient - nested flow-as-tool (parentToolCallId)', () => {
1916
2108
  // PR #4602: a flow running as a tool enriches its streamed text/reasoning with
1917
- // toolContext.toolId; the unified wire surfaces it as text_start/reasoning_start
2109
+ // toolContext.toolId; the wire surfaces it as text_start/reasoning_start
1918
2110
  // .parentToolCallId, and the widget routes that block into the parent tool's row.
1919
2111
  it('routes nested flow text into the parent tool row, not the top-level assistant', async () => {
1920
2112
  const events: AgentWidgetEvent[] = [];
@@ -2462,9 +2654,9 @@ describe('AgentWidgetClient - agent_turn text/tool interleaving', () => {
2462
2654
  // ============================================================================
2463
2655
 
2464
2656
  describe('AgentWidgetClient: step_await parsing', () => {
2465
- // Unified collapses the legacy flow `step_await` (a `local_tool_required` pause)
2657
+ // Wire collapses the legacy flow `step_await` (a `local_tool_required` pause)
2466
2658
  // into the neutral `await` event the native handler consumes.
2467
- const buildUnifiedAwaitStream = (payload: Record<string, unknown>): ReadableStream<Uint8Array> => {
2659
+ const buildAwaitStream = (payload: Record<string, unknown>): ReadableStream<Uint8Array> => {
2468
2660
  const encoder = new TextEncoder();
2469
2661
  const body = `event: await\ndata: ${JSON.stringify({ type: 'await', ...payload })}\n\n`;
2470
2662
  return new ReadableStream({
@@ -2490,7 +2682,7 @@ describe('AgentWidgetClient: step_await parsing', () => {
2490
2682
  it('emits a complete tool message with awaitingLocalTool=true for local_tool_required', async () => {
2491
2683
  global.fetch = vi.fn().mockResolvedValue({
2492
2684
  ok: true,
2493
- body: buildUnifiedAwaitStream({
2685
+ body: buildAwaitStream({
2494
2686
  awaitReason: 'local_tool_required',
2495
2687
  id: 'step-1',
2496
2688
  name: 'Test Step',
@@ -2530,7 +2722,7 @@ describe('AgentWidgetClient: step_await parsing', () => {
2530
2722
  it('emits a running tool message for WebMCP local_tool_required until the browser tool resolves', async () => {
2531
2723
  global.fetch = vi.fn().mockResolvedValue({
2532
2724
  ok: true,
2533
- body: buildUnifiedAwaitStream({
2725
+ body: buildAwaitStream({
2534
2726
  awaitReason: 'local_tool_required',
2535
2727
  id: 'step-1',
2536
2728
  name: 'Test Step',
@@ -2601,7 +2793,7 @@ describe('AgentWidgetClient: step_await parsing', () => {
2601
2793
  // ============================================================================
2602
2794
 
2603
2795
  describe('AgentWidgetClient: agent_await parsing', () => {
2604
- // Unified collapses the legacy `step_await`/`agent_await` pair into one `await`
2796
+ // Wire collapses the legacy `step_await`/`agent_await` pair into one `await`
2605
2797
  // event; the dispatch origin survives as the `origin` field on the payload.
2606
2798
  const buildAgentAwaitStream = (payload: Record<string, unknown>): ReadableStream<Uint8Array> => {
2607
2799
  const encoder = new TextEncoder();
@@ -3027,7 +3219,7 @@ describe('AgentWidgetClient - agent_media events', () => {
3027
3219
  (e) => events.push(e)
3028
3220
  );
3029
3221
 
3030
- // Unified streams each media item as its own block (media_start/complete),
3222
+ // Wire streams each media item as its own block (media_start/complete),
3031
3223
  // so each renders as its own synthetic message with a single content part.
3032
3224
  const mediaMessages = collectMediaMessages(events);
3033
3225
  expect(mediaMessages).toHaveLength(3);
@@ -3788,8 +3980,8 @@ describe('AgentWidgetClient - version header', () => {
3788
3980
  });
3789
3981
  });
3790
3982
 
3791
- describe('AgentWidgetClient - Unified event vocabulary (default in 4.0)', () => {
3792
- const unifiedTextStream = (execId: string) => [
3983
+ describe('AgentWidgetClient - Wire event vocabulary (default in 4.0)', () => {
3984
+ const sampleTextStream = (execId: string) => [
3793
3985
  sseEvent('execution_start', { kind: 'agent', executionId: execId, agentId: 'virtual', agentName: 'Test', maxTurns: 1, startedAt: new Date().toISOString(), seq: 1 }),
3794
3986
  sseEvent('turn_start', { executionId: execId, id: 'turn_1', iteration: 1, role: 'assistant', seq: 2 }),
3795
3987
  sseEvent('text_start', { executionId: execId, id: 'text_1', role: 'assistant', seq: 3 }),
@@ -3800,14 +3992,14 @@ describe('AgentWidgetClient - Unified event vocabulary (default in 4.0)', () =>
3800
3992
  sseEvent('execution_complete', { kind: 'agent', executionId: execId, success: true, completedAt: new Date().toISOString(), seq: 8 }),
3801
3993
  ];
3802
3994
 
3803
- it('does not append an events param to the dispatch URL (unified is the default wire)', async () => {
3995
+ it('does not append an events param to the dispatch URL (the wire is the only format)', async () => {
3804
3996
  let capturedUrl = '';
3805
3997
  global.fetch = vi.fn().mockImplementation(async (url: string) => {
3806
3998
  capturedUrl = url;
3807
3999
  const encoder = new TextEncoder();
3808
4000
  const stream = new ReadableStream({
3809
4001
  start(c) {
3810
- for (const e of unifiedTextStream('exec_u')) c.enqueue(encoder.encode(e));
4002
+ for (const e of sampleTextStream('exec_u')) c.enqueue(encoder.encode(e));
3811
4003
  c.close();
3812
4004
  },
3813
4005
  });
@@ -3825,10 +4017,10 @@ describe('AgentWidgetClient - Unified event vocabulary (default in 4.0)', () =>
3825
4017
  expect(capturedUrl).not.toContain('events=');
3826
4018
  });
3827
4019
 
3828
- it('renders a unified stream through the internal handlers with no config flag', async () => {
4020
+ it('renders a wire stream through the internal handlers with no config flag', async () => {
3829
4021
  const events: AgentWidgetEvent[] = [];
3830
4022
  const execId = 'exec_u1';
3831
- global.fetch = createRawStreamFetch(unifiedTextStream(execId));
4023
+ global.fetch = createRawStreamFetch(sampleTextStream(execId));
3832
4024
 
3833
4025
  const client = new AgentWidgetClient({
3834
4026
  apiUrl: 'http://localhost:8000',
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,7 +2014,7 @@ 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";
1972
2019
  // Whether `executionKind` was set authoritatively by an `execution_start`
1973
2020
  // frame. Continuation streams (e.g. a tool-driven `/resume`) do NOT re-emit
@@ -2498,7 +2545,7 @@ export class AgentWidgetClient {
2498
2545
  }
2499
2546
 
2500
2547
  // A failed step (`success:false`) — including the legacy `step_error`
2501
- // event, which the unified encoder folds into a failed `step_complete`
2548
+ // event, which the wire encoder folds into a failed `step_complete`
2502
2549
  // — surfaces as a terminal error and finalizes the stream.
2503
2550
  if (payload.success === false) {
2504
2551
  const e = payload.error;
@@ -2620,7 +2667,7 @@ export class AgentWidgetClient {
2620
2667
  // Authoritative args are set at tool_start; nothing to render here.
2621
2668
  continue;
2622
2669
  } else if (payloadType === "turn_complete") {
2623
- // Reasoning is sealed by its own reasoning_complete in the unified
2670
+ // Reasoning is sealed by its own reasoning_complete on the wire
2624
2671
  // vocabulary; this only attaches the turn-level stopReason to the
2625
2672
  // assistant message produced by this turn. Falls back to
2626
2673
  // lastAssistantInTurn when the bubble was sealed at a tool boundary
@@ -2640,7 +2687,7 @@ export class AgentWidgetClient {
2640
2687
  }
2641
2688
  if (openTurnId === payload.id) openTurnId = null;
2642
2689
  } else if (payloadType === "media_start") {
2643
- // Open a unified media block; buffer fragments until media_complete.
2690
+ // Open a media block; buffer fragments until media_complete.
2644
2691
  const id = String(payload.id);
2645
2692
  mediaBuffers.set(id, {
2646
2693
  mediaType: typeof payload.mediaType === "string" ? payload.mediaType : undefined,
@@ -2676,7 +2723,7 @@ export class AgentWidgetClient {
2676
2723
  if (completeData) {
2677
2724
  reconstructed = { type: "media", data: completeData, mediaType: completeMediaType };
2678
2725
  } else if (completeUrl) {
2679
- // 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
2680
2727
  // arrives as the bare bucket hint "image" (per the API encoder). Treat
2681
2728
  // that — and any real `image/*` — as a hosted image so we don't misroute
2682
2729
  // generated images into the file bucket.
@@ -2832,7 +2879,7 @@ export class AgentWidgetClient {
2832
2879
 
2833
2880
  onEvent({ type: "status", status: "idle" });
2834
2881
  } else if (payloadType === "execution_error") {
2835
- // Terminal failure. The unified non-terminal `error` is handled
2882
+ // Terminal failure. The non-terminal `error` is handled
2836
2883
  // separately (recoverable → warn).
2837
2884
  const errorMessage = typeof payload.error === 'string'
2838
2885
  ? payload.error
@@ -3047,7 +3094,7 @@ export class AgentWidgetClient {
3047
3094
  // retrying" — and the execution continues, so it must NOT surface as a
3048
3095
  // fatal error or finalize the stream. The API routes terminal failures
3049
3096
  // through `execution_error`. Only an explicit `recoverable: false`
3050
- // promotes a unified `error` to terminal.
3097
+ // promotes an `error` to terminal.
3051
3098
  if (
3052
3099
  payload.recoverable === false &&
3053
3100
  payload.error != null &&
@@ -3165,7 +3212,7 @@ export class AgentWidgetClient {
3165
3212
  if (handled) continue; // Skip default handling if custom handler processed it
3166
3213
  }
3167
3214
 
3168
- // The wire is the neutral unified vocabulary; the handler consumes it
3215
+ // The wire is the wire vocabulary; the handler consumes it
3169
3216
  // natively. The stream is single-connection and in order, so each frame
3170
3217
  // drains straight through.
3171
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,