@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.
- 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 +44 -44
- 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 +33 -33
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +44 -44
- 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 +35 -35
- package/dist/theme-editor-preview.d.cts +76 -1
- package/dist/theme-editor-preview.d.ts +76 -1
- package/dist/theme-editor-preview.js +37 -37
- 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/package.json +1 -1
- package/src/client.test.ts +349 -29
- package/src/client.ts +96 -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/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() }),
|
|
@@ -1784,9 +1976,137 @@ describe('AgentWidgetClient - partId Text/Tool Interleaving', () => {
|
|
|
1784
1976
|
});
|
|
1785
1977
|
});
|
|
1786
1978
|
|
|
1979
|
+
describe('AgentWidgetClient - flow continuation stream without execution_start', () => {
|
|
1980
|
+
// A tool-driven `/resume` continues a flow on a brand-new stream that does NOT
|
|
1981
|
+
// re-emit `execution_start` (`flow_start`). Each stream resolves its execution
|
|
1982
|
+
// kind independently and defaults to `"agent"`, so the continuation used to
|
|
1983
|
+
// mis-route the final prompt-step finalization: the streamed text block was
|
|
1984
|
+
// sealed via the agent path (which never records it as the sealed flow bubble),
|
|
1985
|
+
// then `step_complete.result.response` re-rendered the same text as a SECOND
|
|
1986
|
+
// bubble. The client now recovers the flow kind from the leading `step_*`
|
|
1987
|
+
// frame (a `stepType` is flow-only), so the finalization reconciles in place.
|
|
1988
|
+
function collectAssistantTexts(events: AgentWidgetEvent[]) {
|
|
1989
|
+
const messagesById = new Map<string, AgentWidgetMessage>();
|
|
1990
|
+
for (const event of events) {
|
|
1991
|
+
if (event.type === 'message') messagesById.set(event.message.id, event.message);
|
|
1992
|
+
}
|
|
1993
|
+
return Array.from(messagesById.values())
|
|
1994
|
+
.filter(m => m.role === 'assistant' && !m.variant)
|
|
1995
|
+
.sort((a, b) => (a.sequence ?? 0) - (b.sequence ?? 0));
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
it('does not duplicate the final message when a flow resumes without execution_start', async () => {
|
|
1999
|
+
const events: AgentWidgetEvent[] = [];
|
|
2000
|
+
|
|
2001
|
+
// No `flow_start`/`execution_start`: this is exactly the wire a tool-driven
|
|
2002
|
+
// `/resume` continuation delivers.
|
|
2003
|
+
global.fetch = createAgentStreamFetch([
|
|
2004
|
+
sseEvent('step_start', { id: 's1', name: 'Prompt', stepType: 'prompt', index: 0, totalSteps: 1 }),
|
|
2005
|
+
sseEvent('text_start', { messageId: 'msg_s1' }),
|
|
2006
|
+
sseEvent('step_delta', { id: 's1', text: 'Done! Added to your cart.' }),
|
|
2007
|
+
sseEvent('text_end', { messageId: 'msg_s1' }),
|
|
2008
|
+
// The authoritative final response mirrors the streamed text.
|
|
2009
|
+
sseEvent('step_complete', { id: 's1', name: 'Prompt', stepType: 'prompt', success: true, result: { response: 'Done! Added to your cart.' }, executionTime: 500 }),
|
|
2010
|
+
sseEvent('flow_complete', { success: true }),
|
|
2011
|
+
]);
|
|
2012
|
+
|
|
2013
|
+
const client = new AgentWidgetClient({ apiUrl: 'http://localhost:8000' });
|
|
2014
|
+
await client.dispatch(
|
|
2015
|
+
{ messages: [{ id: 'usr_1', role: 'user', content: 'add it to my cart', createdAt: new Date().toISOString() }] },
|
|
2016
|
+
(event) => events.push(event)
|
|
2017
|
+
);
|
|
2018
|
+
|
|
2019
|
+
const assistantTexts = collectAssistantTexts(events);
|
|
2020
|
+
|
|
2021
|
+
// Exactly ONE assistant bubble — not duplicated by the step_complete finalization.
|
|
2022
|
+
expect(assistantTexts.length).toBe(1);
|
|
2023
|
+
expect(assistantTexts[0].content).toBe('Done! Added to your cart.');
|
|
2024
|
+
expect(assistantTexts[0].streaming).toBe(false);
|
|
2025
|
+
});
|
|
2026
|
+
|
|
2027
|
+
it('still reconciles in place when execution_start IS present (initial flow stream)', async () => {
|
|
2028
|
+
const events: AgentWidgetEvent[] = [];
|
|
2029
|
+
|
|
2030
|
+
// Same shape but WITH flow_start — the pre-existing, already-correct path.
|
|
2031
|
+
global.fetch = createAgentStreamFetch([
|
|
2032
|
+
sseEvent('flow_start', { flowId: 'f1', flowName: 'Test', totalSteps: 1, executionId: 'exec_f1' }),
|
|
2033
|
+
sseEvent('step_start', { id: 's1', name: 'Prompt', stepType: 'prompt', index: 0, totalSteps: 1 }),
|
|
2034
|
+
sseEvent('text_start', { messageId: 'msg_s1' }),
|
|
2035
|
+
sseEvent('step_delta', { id: 's1', text: 'Done! Added to your cart.' }),
|
|
2036
|
+
sseEvent('text_end', { messageId: 'msg_s1' }),
|
|
2037
|
+
sseEvent('step_complete', { id: 's1', name: 'Prompt', stepType: 'prompt', success: true, result: { response: 'Done! Added to your cart.' }, executionTime: 500 }),
|
|
2038
|
+
sseEvent('flow_complete', { success: true }),
|
|
2039
|
+
]);
|
|
2040
|
+
|
|
2041
|
+
const client = new AgentWidgetClient({ apiUrl: 'http://localhost:8000' });
|
|
2042
|
+
await client.dispatch(
|
|
2043
|
+
{ messages: [{ id: 'usr_1', role: 'user', content: 'add it to my cart', createdAt: new Date().toISOString() }] },
|
|
2044
|
+
(event) => events.push(event)
|
|
2045
|
+
);
|
|
2046
|
+
|
|
2047
|
+
const assistantTexts = collectAssistantTexts(events);
|
|
2048
|
+
expect(assistantTexts.length).toBe(1);
|
|
2049
|
+
expect(assistantTexts[0].content).toBe('Done! Added to your cart.');
|
|
2050
|
+
expect(assistantTexts[0].streaming).toBe(false);
|
|
2051
|
+
});
|
|
2052
|
+
|
|
2053
|
+
it('does not misclassify an agent stream as a flow (no stepType present)', async () => {
|
|
2054
|
+
const events: AgentWidgetEvent[] = [];
|
|
2055
|
+
const execId = 'exec_a1';
|
|
2056
|
+
|
|
2057
|
+
// Agent loops use `agent_*`/turn_* frames and never carry a `stepType`, so the
|
|
2058
|
+
// flow recovery must not engage. A single streamed turn yields exactly one
|
|
2059
|
+
// bubble (regression guard so the recovery can't over-fire on agents).
|
|
2060
|
+
global.fetch = createAgentStreamFetch([
|
|
2061
|
+
sseEvent('agent_start', {
|
|
2062
|
+
executionId: execId, agentId: 'virtual', agentName: 'Test',
|
|
2063
|
+
maxTurns: 1, startedAt: new Date().toISOString(), seq: 1,
|
|
2064
|
+
}),
|
|
2065
|
+
sseEvent('agent_iteration_start', {
|
|
2066
|
+
executionId: execId, iteration: 1, maxTurns: 1,
|
|
2067
|
+
startedAt: new Date().toISOString(), seq: 2,
|
|
2068
|
+
}),
|
|
2069
|
+
sseEvent('agent_turn_start', {
|
|
2070
|
+
executionId: execId, iteration: 1, turnIndex: 0,
|
|
2071
|
+
role: 'assistant', turnId: 'turn_1', seq: 3,
|
|
2072
|
+
}),
|
|
2073
|
+
sseEvent('agent_turn_delta', {
|
|
2074
|
+
executionId: execId, iteration: 1, delta: 'Hi there!',
|
|
2075
|
+
contentType: 'text', turnId: 'turn_1', seq: 4,
|
|
2076
|
+
}),
|
|
2077
|
+
sseEvent('agent_turn_complete', {
|
|
2078
|
+
executionId: execId, iteration: 1, role: 'assistant',
|
|
2079
|
+
turnId: 'turn_1', completedAt: new Date().toISOString(), seq: 5,
|
|
2080
|
+
}),
|
|
2081
|
+
sseEvent('agent_iteration_complete', {
|
|
2082
|
+
executionId: execId, iteration: 1, toolCallsMade: 0,
|
|
2083
|
+
stopConditionMet: false, completedAt: new Date().toISOString(), seq: 6,
|
|
2084
|
+
}),
|
|
2085
|
+
sseEvent('agent_complete', {
|
|
2086
|
+
executionId: execId, agentId: 'virtual', success: true,
|
|
2087
|
+
iterations: 1, stopReason: 'max_iterations',
|
|
2088
|
+
completedAt: new Date().toISOString(), seq: 7,
|
|
2089
|
+
}),
|
|
2090
|
+
]);
|
|
2091
|
+
|
|
2092
|
+
const client = new AgentWidgetClient({
|
|
2093
|
+
apiUrl: 'http://localhost:8000',
|
|
2094
|
+
agent: { name: 'Test', model: 'openai:gpt-4o-mini', systemPrompt: 'test' },
|
|
2095
|
+
});
|
|
2096
|
+
await client.dispatch(
|
|
2097
|
+
{ messages: [{ id: 'usr_1', role: 'user', content: 'hi', createdAt: new Date().toISOString() }] },
|
|
2098
|
+
(event) => events.push(event)
|
|
2099
|
+
);
|
|
2100
|
+
|
|
2101
|
+
const assistantTexts = collectAssistantTexts(events);
|
|
2102
|
+
expect(assistantTexts.length).toBe(1);
|
|
2103
|
+
expect(assistantTexts[0].content).toBe('Hi there!');
|
|
2104
|
+
});
|
|
2105
|
+
});
|
|
2106
|
+
|
|
1787
2107
|
describe('AgentWidgetClient - nested flow-as-tool (parentToolCallId)', () => {
|
|
1788
2108
|
// PR #4602: a flow running as a tool enriches its streamed text/reasoning with
|
|
1789
|
-
// toolContext.toolId; the
|
|
2109
|
+
// toolContext.toolId; the wire surfaces it as text_start/reasoning_start
|
|
1790
2110
|
// .parentToolCallId, and the widget routes that block into the parent tool's row.
|
|
1791
2111
|
it('routes nested flow text into the parent tool row, not the top-level assistant', async () => {
|
|
1792
2112
|
const events: AgentWidgetEvent[] = [];
|
|
@@ -2334,9 +2654,9 @@ describe('AgentWidgetClient - agent_turn text/tool interleaving', () => {
|
|
|
2334
2654
|
// ============================================================================
|
|
2335
2655
|
|
|
2336
2656
|
describe('AgentWidgetClient: step_await parsing', () => {
|
|
2337
|
-
//
|
|
2657
|
+
// Wire collapses the legacy flow `step_await` (a `local_tool_required` pause)
|
|
2338
2658
|
// into the neutral `await` event the native handler consumes.
|
|
2339
|
-
const
|
|
2659
|
+
const buildAwaitStream = (payload: Record<string, unknown>): ReadableStream<Uint8Array> => {
|
|
2340
2660
|
const encoder = new TextEncoder();
|
|
2341
2661
|
const body = `event: await\ndata: ${JSON.stringify({ type: 'await', ...payload })}\n\n`;
|
|
2342
2662
|
return new ReadableStream({
|
|
@@ -2362,7 +2682,7 @@ describe('AgentWidgetClient: step_await parsing', () => {
|
|
|
2362
2682
|
it('emits a complete tool message with awaitingLocalTool=true for local_tool_required', async () => {
|
|
2363
2683
|
global.fetch = vi.fn().mockResolvedValue({
|
|
2364
2684
|
ok: true,
|
|
2365
|
-
body:
|
|
2685
|
+
body: buildAwaitStream({
|
|
2366
2686
|
awaitReason: 'local_tool_required',
|
|
2367
2687
|
id: 'step-1',
|
|
2368
2688
|
name: 'Test Step',
|
|
@@ -2402,7 +2722,7 @@ describe('AgentWidgetClient: step_await parsing', () => {
|
|
|
2402
2722
|
it('emits a running tool message for WebMCP local_tool_required until the browser tool resolves', async () => {
|
|
2403
2723
|
global.fetch = vi.fn().mockResolvedValue({
|
|
2404
2724
|
ok: true,
|
|
2405
|
-
body:
|
|
2725
|
+
body: buildAwaitStream({
|
|
2406
2726
|
awaitReason: 'local_tool_required',
|
|
2407
2727
|
id: 'step-1',
|
|
2408
2728
|
name: 'Test Step',
|
|
@@ -2473,7 +2793,7 @@ describe('AgentWidgetClient: step_await parsing', () => {
|
|
|
2473
2793
|
// ============================================================================
|
|
2474
2794
|
|
|
2475
2795
|
describe('AgentWidgetClient: agent_await parsing', () => {
|
|
2476
|
-
//
|
|
2796
|
+
// Wire collapses the legacy `step_await`/`agent_await` pair into one `await`
|
|
2477
2797
|
// event; the dispatch origin survives as the `origin` field on the payload.
|
|
2478
2798
|
const buildAgentAwaitStream = (payload: Record<string, unknown>): ReadableStream<Uint8Array> => {
|
|
2479
2799
|
const encoder = new TextEncoder();
|
|
@@ -2899,7 +3219,7 @@ describe('AgentWidgetClient - agent_media events', () => {
|
|
|
2899
3219
|
(e) => events.push(e)
|
|
2900
3220
|
);
|
|
2901
3221
|
|
|
2902
|
-
//
|
|
3222
|
+
// Wire streams each media item as its own block (media_start/complete),
|
|
2903
3223
|
// so each renders as its own synthetic message with a single content part.
|
|
2904
3224
|
const mediaMessages = collectMediaMessages(events);
|
|
2905
3225
|
expect(mediaMessages).toHaveLength(3);
|
|
@@ -3660,8 +3980,8 @@ describe('AgentWidgetClient - version header', () => {
|
|
|
3660
3980
|
});
|
|
3661
3981
|
});
|
|
3662
3982
|
|
|
3663
|
-
describe('AgentWidgetClient -
|
|
3664
|
-
const
|
|
3983
|
+
describe('AgentWidgetClient - Wire event vocabulary (default in 4.0)', () => {
|
|
3984
|
+
const sampleTextStream = (execId: string) => [
|
|
3665
3985
|
sseEvent('execution_start', { kind: 'agent', executionId: execId, agentId: 'virtual', agentName: 'Test', maxTurns: 1, startedAt: new Date().toISOString(), seq: 1 }),
|
|
3666
3986
|
sseEvent('turn_start', { executionId: execId, id: 'turn_1', iteration: 1, role: 'assistant', seq: 2 }),
|
|
3667
3987
|
sseEvent('text_start', { executionId: execId, id: 'text_1', role: 'assistant', seq: 3 }),
|
|
@@ -3672,14 +3992,14 @@ describe('AgentWidgetClient - Unified event vocabulary (default in 4.0)', () =>
|
|
|
3672
3992
|
sseEvent('execution_complete', { kind: 'agent', executionId: execId, success: true, completedAt: new Date().toISOString(), seq: 8 }),
|
|
3673
3993
|
];
|
|
3674
3994
|
|
|
3675
|
-
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 () => {
|
|
3676
3996
|
let capturedUrl = '';
|
|
3677
3997
|
global.fetch = vi.fn().mockImplementation(async (url: string) => {
|
|
3678
3998
|
capturedUrl = url;
|
|
3679
3999
|
const encoder = new TextEncoder();
|
|
3680
4000
|
const stream = new ReadableStream({
|
|
3681
4001
|
start(c) {
|
|
3682
|
-
for (const e of
|
|
4002
|
+
for (const e of sampleTextStream('exec_u')) c.enqueue(encoder.encode(e));
|
|
3683
4003
|
c.close();
|
|
3684
4004
|
},
|
|
3685
4005
|
});
|
|
@@ -3697,10 +4017,10 @@ describe('AgentWidgetClient - Unified event vocabulary (default in 4.0)', () =>
|
|
|
3697
4017
|
expect(capturedUrl).not.toContain('events=');
|
|
3698
4018
|
});
|
|
3699
4019
|
|
|
3700
|
-
it('renders a
|
|
4020
|
+
it('renders a wire stream through the internal handlers with no config flag', async () => {
|
|
3701
4021
|
const events: AgentWidgetEvent[] = [];
|
|
3702
4022
|
const execId = 'exec_u1';
|
|
3703
|
-
global.fetch = createRawStreamFetch(
|
|
4023
|
+
global.fetch = createRawStreamFetch(sampleTextStream(execId));
|
|
3704
4024
|
|
|
3705
4025
|
const client = new AgentWidgetClient({
|
|
3706
4026
|
apiUrl: 'http://localhost:8000',
|