@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.
- package/README.md +22 -6
- package/dist/animations/glyph-cycle.d.cts +1 -1
- package/dist/animations/glyph-cycle.d.ts +1 -1
- package/dist/animations/{types-B_xbfvR0.d.cts → types-C6tFDxKy.d.cts} +1 -1
- package/dist/animations/{types-B_xbfvR0.d.ts → types-C6tFDxKy.d.ts} +1 -1
- package/dist/animations/wipe.d.cts +1 -1
- package/dist/animations/wipe.d.ts +1 -1
- package/dist/codegen.cjs +6 -6
- package/dist/codegen.js +9 -9
- package/dist/index.cjs +32 -32
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +101 -3
- package/dist/index.d.ts +101 -3
- package/dist/index.global.js +23 -23
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +32 -32
- package/dist/index.js.map +1 -1
- package/dist/install.global.js +1 -1
- package/dist/install.global.js.map +1 -1
- package/dist/launcher.global.js.map +1 -1
- package/dist/smart-dom-reader.d.cts +76 -1
- package/dist/smart-dom-reader.d.ts +76 -1
- package/dist/theme-editor-preview.cjs +27 -27
- package/dist/theme-editor-preview.d.cts +76 -1
- package/dist/theme-editor-preview.d.ts +76 -1
- package/dist/theme-editor-preview.js +29 -29
- package/dist/theme-editor.cjs +1 -1
- package/dist/theme-editor.d.cts +76 -1
- package/dist/theme-editor.d.ts +76 -1
- package/dist/theme-editor.js +1 -1
- package/dist/widget.css +3 -0
- package/package.json +1 -1
- package/src/client.test.ts +221 -29
- package/src/client.ts +71 -24
- package/src/defaults.ts +2 -0
- package/src/index-core.ts +2 -0
- package/src/install.ts +9 -1
- package/src/session.ts +6 -3
- package/src/session.voice.test.ts +65 -0
- package/src/styles/widget.css +3 -0
- package/src/types.ts +57 -2
- package/src/utils/__fixtures__/unified-translator.oracle.ts +3 -3
- package/src/utils/code-generators.ts +10 -0
- package/src/utils/target.test.ts +51 -0
- package/src/utils/target.ts +90 -0
package/src/client.test.ts
CHANGED
|
@@ -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
|
|
473
|
-
//
|
|
474
|
-
// JSON parser on the
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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 (
|
|
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
|
-
//
|
|
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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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
|
-
//
|
|
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
|
|
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:
|
|
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:
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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 -
|
|
3792
|
-
const
|
|
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 (
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
...(
|
|
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
|
-
|
|
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
|
-
...(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1951
|
-
//
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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",
|