@runtypelabs/persona 3.37.0 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +4 -5
  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-DgYsuwXL.d.cts → types-B_xbfvR0.d.cts} +263 -181
  5. package/dist/animations/{types-DgYsuwXL.d.ts → types-B_xbfvR0.d.ts} +263 -181
  6. package/dist/animations/wipe.d.cts +1 -1
  7. package/dist/animations/wipe.d.ts +1 -1
  8. package/dist/codegen.cjs +1 -1
  9. package/dist/codegen.js +1 -1
  10. package/dist/index.cjs +54 -52
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.d.cts +328 -839
  13. package/dist/index.d.ts +328 -839
  14. package/dist/index.global.js +41 -39
  15. package/dist/index.global.js.map +1 -1
  16. package/dist/index.js +54 -52
  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/smart-dom-reader.d.cts +269 -192
  21. package/dist/smart-dom-reader.d.ts +269 -192
  22. package/dist/theme-editor-preview.cjs +55 -53
  23. package/dist/theme-editor-preview.d.cts +269 -192
  24. package/dist/theme-editor-preview.d.ts +269 -192
  25. package/dist/theme-editor-preview.js +57 -55
  26. package/dist/theme-editor.d.cts +269 -192
  27. package/dist/theme-editor.d.ts +269 -192
  28. package/dist/widget.css +14 -0
  29. package/package.json +1 -1
  30. package/src/client.test.ts +446 -1041
  31. package/src/client.ts +684 -967
  32. package/src/components/message-bubble.ts +7 -0
  33. package/src/components/tool-bubble.ts +46 -33
  34. package/src/generated/runtype-openapi-contract.ts +210 -714
  35. package/src/index-core.ts +2 -3
  36. package/src/install.test.ts +1 -34
  37. package/src/install.ts +1 -26
  38. package/src/runtime/init.test.ts +8 -67
  39. package/src/runtime/init.ts +2 -17
  40. package/src/session.test.ts +8 -8
  41. package/src/session.ts +1 -1
  42. package/src/styles/widget.css +14 -0
  43. package/src/types.ts +12 -13
  44. package/src/ui.postprocess.test.ts +107 -0
  45. package/src/ui.ts +45 -5
  46. package/src/utils/__fixtures__/unified-translator.oracle.ts +62 -14
  47. package/src/utils/copy-selection.test.ts +37 -0
  48. package/src/utils/copy-selection.ts +19 -0
  49. package/src/utils/event-stream-capture.test.ts +9 -64
  50. package/src/utils/streaming-table.test.ts +100 -0
  51. package/src/utils/streaming-table.ts +103 -0
  52. package/src/utils/sequence-buffer.test.ts +0 -256
  53. package/src/utils/sequence-buffer.ts +0 -130
  54. package/src/utils/unified-event-bridge.test.ts +0 -263
  55. package/src/utils/unified-event-bridge.ts +0 -431
@@ -1,256 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
- import { SequenceReorderBuffer } from "./sequence-buffer";
3
-
4
- describe("SequenceReorderBuffer", () => {
5
- let emitted: Array<{ payloadType: string; payload: any }>;
6
- let emitter: (payloadType: string, payload: any) => void;
7
-
8
- beforeEach(() => {
9
- vi.useFakeTimers();
10
- emitted = [];
11
- emitter = (payloadType, payload) => {
12
- emitted.push({ payloadType, payload });
13
- };
14
- });
15
-
16
- afterEach(() => {
17
- vi.useRealTimers();
18
- });
19
-
20
- it("passes in-order events through immediately", () => {
21
- const buf = new SequenceReorderBuffer(emitter);
22
- buf.push("step_delta", { seq: 1, text: "a" });
23
- buf.push("step_delta", { seq: 2, text: "b" });
24
- buf.push("step_delta", { seq: 3, text: "c" });
25
-
26
- expect(emitted).toHaveLength(3);
27
- expect(emitted[0].payload.text).toBe("a");
28
- expect(emitted[1].payload.text).toBe("b");
29
- expect(emitted[2].payload.text).toBe("c");
30
- buf.destroy();
31
- });
32
-
33
- it("reorders leading out-of-order events (3, 1, 2 → 1, 2, 3)", () => {
34
- const buf = new SequenceReorderBuffer(emitter);
35
- // seq=3 arrives first: should be buffered (3 > nextExpected=1)
36
- buf.push("step_delta", { seq: 3, text: "c" });
37
- expect(emitted).toHaveLength(0);
38
-
39
- // seq=1 arrives: matches nextExpected, emits, then drains seq=2 (not present), stops
40
- buf.push("step_delta", { seq: 1, text: "a" });
41
- expect(emitted).toHaveLength(1);
42
- expect(emitted[0].payload.text).toBe("a");
43
-
44
- // seq=2 arrives: matches nextExpected=2, emits, drains seq=3 from buffer
45
- buf.push("step_delta", { seq: 2, text: "b" });
46
- expect(emitted).toHaveLength(3);
47
- expect(emitted[1].payload.text).toBe("b");
48
- expect(emitted[2].payload.text).toBe("c");
49
- buf.destroy();
50
- });
51
-
52
- it("reorders mid-stream out-of-order events", () => {
53
- const buf = new SequenceReorderBuffer(emitter);
54
- buf.push("step_delta", { seq: 1, text: "a" });
55
- buf.push("step_delta", { seq: 3, text: "c" }); // buffered
56
- buf.push("step_delta", { seq: 2, text: "b" }); // emits, drains 3
57
-
58
- expect(emitted).toHaveLength(3);
59
- expect(emitted[0].payload.text).toBe("a");
60
- expect(emitted[1].payload.text).toBe("b");
61
- expect(emitted[2].payload.text).toBe("c");
62
- buf.destroy();
63
- });
64
-
65
- it("flushes buffered events after gap timeout when a seq is missing", () => {
66
- const buf = new SequenceReorderBuffer(emitter, 50);
67
- buf.push("step_delta", { seq: 1, text: "a" }); // emits
68
- buf.push("step_delta", { seq: 3, text: "c" }); // buffered (waiting for seq 2)
69
-
70
- expect(emitted).toHaveLength(1);
71
-
72
- // Advance past gap timeout: seq=2 never arrives, flush seq=3 anyway
73
- vi.advanceTimersByTime(60);
74
-
75
- expect(emitted).toHaveLength(2);
76
- expect(emitted[1].payload.text).toBe("c");
77
- buf.destroy();
78
- });
79
-
80
- it("passes no-seq events through immediately (backward compat)", () => {
81
- const buf = new SequenceReorderBuffer(emitter);
82
- buf.push("flow_start", { flowId: "abc" });
83
- buf.push("step_start", { name: "test" });
84
-
85
- expect(emitted).toHaveLength(2);
86
- expect(emitted[0].payload.flowId).toBe("abc");
87
- expect(emitted[1].payload.name).toBe("test");
88
- buf.destroy();
89
- });
90
-
91
- it("emits late/duplicate events (seq < nextExpected)", () => {
92
- const buf = new SequenceReorderBuffer(emitter);
93
- // Process seq 1-3 normally to advance nextExpected to 4
94
- buf.push("step_delta", { seq: 1, text: "a" });
95
- buf.push("step_delta", { seq: 2, text: "b" });
96
- buf.push("step_delta", { seq: 3, text: "c" });
97
- expect(emitted).toHaveLength(3);
98
-
99
- // Now seq=1 arrives again: it's a duplicate (1 < nextExpected=4), still emitted
100
- buf.push("step_delta", { seq: 1, text: "a-dup" });
101
- expect(emitted).toHaveLength(4);
102
- expect(emitted[3].payload.text).toBe("a-dup");
103
- buf.destroy();
104
- });
105
-
106
- it("handles mixed seq and no-seq events", () => {
107
- const buf = new SequenceReorderBuffer(emitter);
108
- buf.push("step_delta", { seq: 1, text: "a" });
109
- buf.push("status", { status: "streaming" }); // no seq
110
- buf.push("step_delta", { seq: 2, text: "b" });
111
- buf.push("error", { error: "oops" }); // no seq
112
-
113
- expect(emitted).toHaveLength(4);
114
- expect(emitted[0].payload.text).toBe("a");
115
- expect(emitted[1].payload.status).toBe("streaming");
116
- expect(emitted[2].payload.text).toBe("b");
117
- expect(emitted[3].payload.error).toBe("oops");
118
- buf.destroy();
119
- });
120
-
121
- it("handles large burst of out-of-order events correctly", () => {
122
- const buf = new SequenceReorderBuffer(emitter);
123
- // Send events in reverse order: 10, 9, 8, ..., 1
124
- // All are buffered until seq=1 arrives (last), then everything drains
125
- const seqs = [10, 9, 8, 7, 6, 5, 4, 3, 2, 1];
126
- for (const seq of seqs) {
127
- buf.push("step_delta", { seq, text: `chunk-${seq}` });
128
- }
129
-
130
- expect(emitted).toHaveLength(10);
131
- const emittedTexts = emitted.map(e => e.payload.text);
132
- expect(emittedTexts).toEqual([
133
- "chunk-1", "chunk-2", "chunk-3", "chunk-4", "chunk-5",
134
- "chunk-6", "chunk-7", "chunk-8", "chunk-9", "chunk-10"
135
- ]);
136
- buf.destroy();
137
- });
138
-
139
- it("handles scrambled arrival order", () => {
140
- const buf = new SequenceReorderBuffer(emitter);
141
- const scrambled = [1, 5, 3, 2, 4, 8, 6, 7, 10, 9];
142
- for (const seq of scrambled) {
143
- buf.push("step_delta", { seq, text: `chunk-${seq}` });
144
- }
145
-
146
- expect(emitted).toHaveLength(10);
147
- const emittedTexts = emitted.map(e => e.payload.text);
148
- expect(emittedTexts).toEqual([
149
- "chunk-1", "chunk-2", "chunk-3", "chunk-4", "chunk-5",
150
- "chunk-6", "chunk-7", "chunk-8", "chunk-9", "chunk-10"
151
- ]);
152
- buf.destroy();
153
- });
154
-
155
- it("no-seq event flushes pending buffer and cancels the gap timer", () => {
156
- const buf = new SequenceReorderBuffer(emitter, 50);
157
- buf.push("step_delta", { seq: 1, text: "a" });
158
- buf.push("step_delta", { seq: 3, text: "c" }); // buffered, starts gap timer
159
- expect(emitted).toHaveLength(1);
160
-
161
- // A no-seq event triggers flushAll, which drains the buffer in seq order
162
- // and cancels the gap timer.
163
- buf.push("flow_complete", { flowId: "done" });
164
- expect(emitted).toHaveLength(3); // a, c, flow_complete
165
-
166
- // The gap timer must no longer fire after the flushAll.
167
- vi.advanceTimersByTime(100);
168
- expect(emitted).toHaveLength(3);
169
- buf.destroy();
170
- });
171
-
172
- it("supports sequenceIndex as an alternative to seq", () => {
173
- const buf = new SequenceReorderBuffer(emitter);
174
- buf.push("reason_delta", { sequenceIndex: 1, text: "a" });
175
- buf.push("reason_delta", { sequenceIndex: 3, text: "c" });
176
- buf.push("reason_delta", { sequenceIndex: 2, text: "b" });
177
-
178
- expect(emitted).toHaveLength(3);
179
- expect(emitted[0].payload.text).toBe("a");
180
- expect(emitted[1].payload.text).toBe("b");
181
- expect(emitted[2].payload.text).toBe("c");
182
- buf.destroy();
183
- });
184
-
185
- it("leading gap flushes via timeout when seq=1 never arrives", () => {
186
- const buf = new SequenceReorderBuffer(emitter, 50);
187
- // Only seq=2 and seq=3 arrive: seq=1 is missing
188
- buf.push("step_delta", { seq: 2, text: "b" });
189
- buf.push("step_delta", { seq: 3, text: "c" });
190
- expect(emitted).toHaveLength(0); // both buffered
191
-
192
- vi.advanceTimersByTime(50);
193
- // Gap timer flushes in seq order
194
- expect(emitted).toHaveLength(2);
195
- expect(emitted[0].payload.text).toBe("b");
196
- expect(emitted[1].payload.text).toBe("c");
197
- buf.destroy();
198
- });
199
-
200
- it("handles a stream whose first seq is > 1 via the gap timeout (no loss)", () => {
201
- // Defensive: if the server's counter ever starts above 1 (e.g. a resumed
202
- // stream), the hardcoded nextExpectedSeq=1 would buffer the first event.
203
- // The gap timer must still flush it so nothing is lost.
204
- const buf = new SequenceReorderBuffer(emitter, 50);
205
- buf.push("step_delta", { seq: 5, text: "first" });
206
- buf.push("step_delta", { seq: 6, text: "second" });
207
- expect(emitted).toHaveLength(0);
208
-
209
- vi.advanceTimersByTime(50);
210
-
211
- expect(emitted).toHaveLength(2);
212
- expect(emitted[0].payload.text).toBe("first");
213
- expect(emitted[1].payload.text).toBe("second");
214
-
215
- // Subsequent in-order events should pass through immediately.
216
- buf.push("step_delta", { seq: 7, text: "third" });
217
- expect(emitted).toHaveLength(3);
218
- expect(emitted[2].payload.text).toBe("third");
219
- buf.destroy();
220
- });
221
-
222
- it("warns and emits both events on seq collision (does not silently drop)", () => {
223
- // Server invariant: seq is unique per stream. If it's ever violated
224
- // (bug, replay, mixed counters), Map.set would silently overwrite. The
225
- // buffer must detect this, warn, and emit the prior event so nothing is
226
- // dropped.
227
- const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
228
- const buf = new SequenceReorderBuffer(emitter, 50);
229
-
230
- buf.push("step_delta", { seq: 1, text: "a" });
231
- // seq=3 buffered, waiting for seq=2
232
- buf.push("step_delta", { seq: 3, text: "first-at-3" });
233
- expect(emitted).toHaveLength(1);
234
-
235
- // Second event with same seq=3: prior one should be emitted out-of-order
236
- buf.push("reason_delta", { seq: 3, text: "second-at-3" });
237
-
238
- expect(warnSpy).toHaveBeenCalledTimes(1);
239
- expect(warnSpy.mock.calls[0][0]).toContain("duplicate seq=3");
240
- expect(warnSpy.mock.calls[0][0]).toContain("step_delta");
241
- expect(warnSpy.mock.calls[0][0]).toContain("reason_delta");
242
-
243
- // Prior event flushed immediately (out of seq order), nothing lost
244
- expect(emitted).toHaveLength(2);
245
- expect(emitted[1].payload.text).toBe("first-at-3");
246
-
247
- // seq=2 arrives: advances nextExpected through the buffered second-at-3
248
- buf.push("step_delta", { seq: 2, text: "b" });
249
- expect(emitted).toHaveLength(4);
250
- expect(emitted[2].payload.text).toBe("b");
251
- expect(emitted[3].payload.text).toBe("second-at-3");
252
-
253
- warnSpy.mockRestore();
254
- buf.destroy();
255
- });
256
- });
@@ -1,130 +0,0 @@
1
- type BufferedEvent = { payloadType: string; payload: any; seq: number };
2
-
3
- export class SequenceReorderBuffer {
4
- private nextExpectedSeq: number | null = null;
5
- private buffer: Map<number, BufferedEvent> = new Map();
6
- private flushTimer: ReturnType<typeof setTimeout> | null = null;
7
- private emitter: (payloadType: string, payload: any) => void;
8
- private gapTimeoutMs: number;
9
-
10
- constructor(emitter: (payloadType: string, payload: any) => void, gapTimeoutMs = 50) {
11
- this.emitter = emitter;
12
- this.gapTimeoutMs = gapTimeoutMs;
13
- }
14
-
15
- push(payloadType: string, payload: any): void {
16
- // All three fields are sourced from the same FlowExecutionEngine.sequenceCounter:
17
- // - `seq`: step_delta, text_start, text_end, agent_* events (top-level)
18
- // - `sequenceIndex`: reason_start, reason_delta, reason_complete, source
19
- // - `agentContext.seq`: tool_start, tool_delta, tool_complete (agent loop)
20
- const seq = payload?.seq ?? payload?.sequenceIndex ?? payload?.agentContext?.seq;
21
-
22
- // No seq field: emit immediately (backward compat).
23
- // If there are buffered events waiting for a gap to fill, flush them
24
- // first: the server sending an unsequenced event means it has moved on
25
- // and the missing seq numbers are not coming.
26
- if (seq === undefined || seq === null) {
27
- if (this.buffer.size > 0) {
28
- this.flushAll();
29
- }
30
- this.emitter(payloadType, payload);
31
- return;
32
- }
33
-
34
- // Server's sequenceCounter resets to 0 on each execution and pre-increments,
35
- // so the first sequenced event in any stream is expected to have seq=1.
36
- // If a server ever starts at a different number (e.g. a resumed stream),
37
- // the 50ms gap timer below is the safety net: the first event gets
38
- // buffered, then flushed after the gap elapses. Correctness is preserved;
39
- // the only cost is a one-time latency on the leading event.
40
- if (this.nextExpectedSeq === null) {
41
- this.nextExpectedSeq = 1;
42
- }
43
-
44
- // If this is the expected event, emit it and drain consecutive buffered events
45
- if (seq === this.nextExpectedSeq) {
46
- this.emitter(payloadType, payload);
47
- this.nextExpectedSeq = (seq as number) + 1;
48
- this.drainConsecutive();
49
- return;
50
- }
51
-
52
- // If seq < nextExpected, it's a duplicate or late arrival: emit anyway (don't drop)
53
- if (seq < this.nextExpectedSeq!) {
54
- this.emitter(payloadType, payload);
55
- return;
56
- }
57
-
58
- // seq > nextExpected: buffer it and start gap timer.
59
- // If another event with the same seq is already buffered, the server
60
- // broke its "seq is unique per stream" invariant. Rather than silently
61
- // overwrite (losing one event) or swallow the new one, emit the prior
62
- // event immediately, out of order, but better than dropping it, and
63
- // warn so the issue is visible.
64
- const existing = this.buffer.get(seq);
65
- if (existing !== undefined) {
66
- if (typeof console !== "undefined" && typeof console.warn === "function") {
67
- console.warn(
68
- `[persona] SequenceReorderBuffer: duplicate seq=${seq} ` +
69
- `(${existing.payloadType} vs ${payloadType}); ` +
70
- `emitting earlier event out-of-order to avoid loss`
71
- );
72
- }
73
- this.emitter(existing.payloadType, existing.payload);
74
- }
75
- this.buffer.set(seq, { payloadType, payload, seq });
76
- this.startGapTimer();
77
- }
78
-
79
- private drainConsecutive(): void {
80
- while (this.buffer.has(this.nextExpectedSeq!)) {
81
- const event = this.buffer.get(this.nextExpectedSeq!)!;
82
- this.buffer.delete(this.nextExpectedSeq!);
83
- this.emitter(event.payloadType, event.payload);
84
- this.nextExpectedSeq!++;
85
- }
86
- // If buffer is empty, clear the gap timer
87
- if (this.buffer.size === 0) {
88
- this.clearGapTimer();
89
- }
90
- }
91
-
92
- private startGapTimer(): void {
93
- if (this.flushTimer !== null) return;
94
- this.flushTimer = setTimeout(() => {
95
- this.flushAll();
96
- }, this.gapTimeoutMs);
97
- }
98
-
99
- private clearGapTimer(): void {
100
- if (this.flushTimer !== null) {
101
- clearTimeout(this.flushTimer);
102
- this.flushTimer = null;
103
- }
104
- }
105
-
106
- private flushAll(): void {
107
- this.clearGapTimer();
108
- if (this.buffer.size === 0) return;
109
-
110
- // Flush all buffered events in seq order
111
- const sorted = [...this.buffer.entries()].sort((a, b) => a[0] - b[0]);
112
- for (const [seq, event] of sorted) {
113
- this.buffer.delete(seq);
114
- this.emitter(event.payloadType, event.payload);
115
- }
116
- // Update nextExpectedSeq to after the last flushed
117
- if (sorted.length > 0) {
118
- this.nextExpectedSeq = sorted[sorted.length - 1][0] + 1;
119
- }
120
- }
121
-
122
- destroy(): void {
123
- this.clearGapTimer();
124
- this.buffer.clear();
125
- }
126
-
127
- flushPending(): void {
128
- this.flushAll();
129
- }
130
- }
@@ -1,263 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { UnifiedToLegacyBridge, isUnifiedLifecycleStart, type LegacyEvent } from "./unified-event-bridge";
3
- import { createUnifiedEventWrite } from "./__fixtures__/unified-translator.oracle";
4
-
5
- type Frame = Record<string, unknown> & { type: string };
6
-
7
- /** legacy frames → api oracle → parsed unified frames */
8
- function legacyToUnified(legacy: Frame[], executionId?: string): Frame[] {
9
- const unified: Frame[] = [];
10
- const sink = (chunk: string) => {
11
- for (const part of chunk.split("\n\n")) {
12
- const dataLine = part.split("\n").find((l) => l.startsWith("data: "));
13
- if (!dataLine) continue;
14
- const json = dataLine.slice(6);
15
- if (json.trim() === "[DONE]") continue;
16
- try {
17
- unified.push(JSON.parse(json) as Frame);
18
- } catch {
19
- /* ignore */
20
- }
21
- }
22
- };
23
- const write = createUnifiedEventWrite(sink, executionId ? { executionId } : undefined);
24
- for (const f of legacy) write(`data: ${JSON.stringify(f)}\n\n`);
25
- return unified;
26
- }
27
-
28
- /** unified frames → widget bridge → legacy' events */
29
- function unifiedToLegacy(unified: Frame[], executionId?: string): LegacyEvent[] {
30
- const bridge = new UnifiedToLegacyBridge(executionId ? { executionId } : undefined);
31
- const out: LegacyEvent[] = [];
32
- for (const f of unified) out.push(...bridge.push(f.type, f));
33
- return out;
34
- }
35
-
36
- /** full round-trip: legacy → unified → legacy' */
37
- function roundTrip(legacy: Frame[], executionId = "exec_test"): { unified: Frame[]; legacy2: LegacyEvent[] } {
38
- const unified = legacyToUnified(legacy, executionId);
39
- return { unified, legacy2: unifiedToLegacy(unified, executionId) };
40
- }
41
-
42
- const byType = (evs: LegacyEvent[], t: string) => evs.filter((e) => e.payloadType === t);
43
- const field = (e: LegacyEvent | undefined, k: string): unknown => e?.payload[k];
44
- const textOf = (evs: LegacyEvent[]) =>
45
- byType(evs, "agent_turn_delta")
46
- .filter((e) => e.payload.contentType === "text")
47
- .map((e) => String(e.payload.delta ?? ""))
48
- .join("");
49
- const thinkingOf = (evs: LegacyEvent[]) =>
50
- byType(evs, "agent_turn_delta")
51
- .filter((e) => e.payload.contentType === "thinking")
52
- .map((e) => String(e.payload.delta ?? ""))
53
- .join("");
54
-
55
- describe("UnifiedToLegacyBridge — round-trip against the api oracle", () => {
56
- it("agent text stream preserves text, turnId, and lifecycle", () => {
57
- const { unified, legacy2 } = roundTrip([
58
- { type: "agent_start", executionId: "exec_test", agentId: "virtual", startedAt: "t0" },
59
- { type: "agent_turn_start", turnId: "turn_1", iteration: 1 },
60
- { type: "agent_turn_delta", executionId: "exec_test", iteration: 1, turnId: "turn_1", contentType: "text", delta: "Hello " },
61
- { type: "agent_turn_delta", executionId: "exec_test", iteration: 1, turnId: "turn_1", contentType: "text", delta: "world" },
62
- { type: "agent_turn_complete", turnId: "turn_1", iteration: 1, stopReason: "end_turn", completedAt: "t1" },
63
- { type: "agent_complete", executionId: "exec_test", success: true, stopReason: "end_turn", completedAt: "t1" },
64
- ]);
65
-
66
- // the oracle really did produce the neutral vocabulary
67
- expect(unified[0].type).toBe("execution_start");
68
- expect(unified.some((f) => f.type === "text_delta")).toBe(true);
69
-
70
- expect(byType(legacy2, "agent_start")).toHaveLength(1);
71
- expect(textOf(legacy2)).toBe("Hello world");
72
- // turnId recovered from the open turn (unified decouples block-id from turn)
73
- expect(field(byType(legacy2, "agent_turn_delta")[0], "turnId")).toBe("turn_1");
74
- expect(field(byType(legacy2, "agent_turn_complete")[0], "stopReason")).toBe("end_turn");
75
- const complete = byType(legacy2, "agent_complete")[0];
76
- expect(field(complete, "success")).toBe(true);
77
- expect(field(complete, "executionId")).toBe("exec_test");
78
- });
79
-
80
- it("agent text WITHOUT an explicit turn still renders (block-id fallback)", () => {
81
- // the verified minimal adapter omits agent_turn_start
82
- const { legacy2 } = roundTrip([
83
- { type: "agent_start", executionId: "exec_test", agentId: "virtual" },
84
- { type: "agent_turn_delta", executionId: "exec_test", iteration: 1, turnId: "turn_x", contentType: "text", delta: "hi" },
85
- { type: "agent_complete", executionId: "exec_test", success: true },
86
- ]);
87
- expect(textOf(legacy2)).toBe("hi");
88
- // no turn_start → bridge falls back to the unified text block id (stable, non-empty)
89
- expect(field(byType(legacy2, "agent_turn_delta")[0], "turnId")).toBeTruthy();
90
- });
91
-
92
- it("agent thinking round-trips through the reasoning channel", () => {
93
- const { unified, legacy2 } = roundTrip([
94
- { type: "agent_start", executionId: "exec_test", agentId: "virtual" },
95
- { type: "agent_turn_start", turnId: "turn_1", iteration: 1 },
96
- { type: "agent_turn_delta", executionId: "exec_test", iteration: 1, turnId: "turn_1", contentType: "thinking", delta: "let me think" },
97
- { type: "agent_turn_complete", turnId: "turn_1", iteration: 1 },
98
- { type: "agent_complete", executionId: "exec_test", success: true },
99
- ]);
100
- expect(unified.some((f) => f.type === "reasoning_delta")).toBe(true);
101
- expect(thinkingOf(legacy2)).toBe("let me think");
102
- });
103
-
104
- it("agent tool call preserves name, args, result, and toolCallId", () => {
105
- const { unified, legacy2 } = roundTrip([
106
- { type: "agent_start", executionId: "exec_test", agentId: "virtual" },
107
- { type: "agent_tool_start", executionId: "exec_test", iteration: 1, toolCallId: "tc1", toolName: "search", parameters: { q: "shoes" } },
108
- { type: "agent_tool_delta", toolCallId: "tc1", delta: "partial" },
109
- { type: "agent_tool_complete", toolCallId: "tc1", result: { hits: 3 }, executionTime: 42 },
110
- { type: "agent_complete", executionId: "exec_test", success: true },
111
- ]);
112
- expect(unified.some((f) => f.type === "tool_start")).toBe(true);
113
-
114
- const start = byType(legacy2, "agent_tool_start")[0];
115
- expect(field(start, "toolCallId")).toBe("tc1");
116
- expect(field(start, "toolName")).toBe("search");
117
- expect(field(start, "parameters")).toEqual({ q: "shoes" });
118
-
119
- expect(field(byType(legacy2, "agent_tool_delta")[0], "delta")).toBe("partial");
120
-
121
- const done = byType(legacy2, "agent_tool_complete")[0];
122
- expect(field(done, "result")).toEqual({ hits: 3 });
123
- expect(field(done, "executionTime")).toBe(42);
124
- });
125
-
126
- it("WebMCP agent_await maps onto the 3.35.0 step_await local-tool path with the webmcp: prefix", () => {
127
- const { unified, legacy2 } = roundTrip([
128
- { type: "agent_start", executionId: "exec_test", agentId: "virtual" },
129
- {
130
- type: "agent_await",
131
- executionId: "exec_test",
132
- toolId: "runtime_add_to_cart_1",
133
- toolName: "add_to_cart", // BARE on the wire
134
- origin: "webmcp",
135
- toolCallId: "toolu_123",
136
- parameters: { sku: "SHOE-003" },
137
- awaitedAt: "t2",
138
- },
139
- ]);
140
- // the oracle collapses agent_await → the neutral `await`
141
- expect(unified.some((f) => f.type === "await")).toBe(true);
142
- expect(unified.some((f) => f.type === "agent_await")).toBe(false);
143
-
144
- const awaitEv = byType(legacy2, "step_await")[0];
145
- expect(awaitEv).toBeDefined();
146
- expect(field(awaitEv, "awaitReason")).toBe("local_tool_required");
147
- expect(field(awaitEv, "toolName")).toBe("webmcp:add_to_cart"); // prefix synthesized by the bridge
148
- expect(field(awaitEv, "toolCallId")).toBe("toolu_123");
149
- expect(field(awaitEv, "parameters")).toEqual({ sku: "SHOE-003" });
150
- expect(field(awaitEv, "executionId")).toBe("exec_test");
151
- });
152
-
153
- it("tool-produced media round-trips to a single agent_media", () => {
154
- const { unified, legacy2 } = roundTrip([
155
- { type: "agent_start", executionId: "exec_test", agentId: "virtual" },
156
- {
157
- type: "agent_media",
158
- executionId: "exec_test",
159
- toolCallId: "tc1",
160
- media: [{ type: "image-url", url: "https://x/img.png", mediaType: "image/png" }],
161
- },
162
- { type: "agent_complete", executionId: "exec_test", success: true },
163
- ]);
164
- // oracle expands to the media triad
165
- expect(unified.filter((f) => f.type.startsWith("media_"))).toHaveLength(3);
166
-
167
- const media = byType(legacy2, "agent_media");
168
- expect(media).toHaveLength(1);
169
- const parts = field(media[0], "media") as Array<Record<string, unknown>>;
170
- expect(parts[0].type).toBe("image-url");
171
- expect(parts[0].url).toBe("https://x/img.png");
172
- expect(parts[0].mediaType).toBe("image/png");
173
- });
174
-
175
- it("approval round-trips", () => {
176
- const { legacy2 } = roundTrip([
177
- { type: "agent_start", executionId: "exec_test", agentId: "virtual" },
178
- { type: "agent_approval_start", approvalId: "ap1", toolName: "delete_file", toolType: "builtin", description: "Delete?", parameters: { path: "/x" } },
179
- { type: "agent_approval_complete", approvalId: "ap1", decision: "approved" },
180
- ]);
181
- const start = byType(legacy2, "agent_approval_start")[0];
182
- expect(field(start, "approvalId")).toBe("ap1");
183
- expect(field(start, "toolName")).toBe("delete_file");
184
- expect(field(byType(legacy2, "agent_approval_complete")[0], "decision")).toBe("approved");
185
- });
186
-
187
- it("artifacts round-trip 1:1", () => {
188
- const { legacy2 } = roundTrip([
189
- { type: "agent_start", executionId: "exec_test", agentId: "virtual" },
190
- { type: "artifact_start", id: "a1", artifactType: "markdown", title: "Doc" },
191
- { type: "artifact_delta", id: "a1", delta: "# Hello" },
192
- { type: "artifact_complete", id: "a1" },
193
- ]);
194
- expect(field(byType(legacy2, "artifact_start")[0], "title")).toBe("Doc");
195
- expect(field(byType(legacy2, "artifact_delta")[0], "delta")).toBe("# Hello");
196
- expect(byType(legacy2, "artifact_complete")).toHaveLength(1);
197
- });
198
- });
199
-
200
- describe("UnifiedToLegacyBridge — unit: kind routing, drops, and error mapping", () => {
201
- it("flow-kind text routes to step_delta (not agent_turn_delta)", () => {
202
- const b = new UnifiedToLegacyBridge();
203
- b.push("execution_start", { kind: "flow", executionId: "exec_test" });
204
- b.push("step_start", { id: "step_1", name: "Generate" });
205
- const evs = b.push("text_delta", { id: "text_1", delta: "flow text" });
206
- expect(evs).toHaveLength(1);
207
- expect(evs[0].payloadType).toBe("step_delta");
208
- expect(evs[0].payload.id).toBe("step_1"); // recovered from the open step
209
- expect(evs[0].payload.text).toBe("flow text");
210
- });
211
-
212
- it("drops events with no legacy renderer", () => {
213
- const b = new UnifiedToLegacyBridge();
214
- b.push("execution_start", { kind: "agent", executionId: "exec_test" });
215
- expect(b.push("source", { url: "https://x" })).toHaveLength(0);
216
- expect(b.push("custom", { name: "runtype.fallback", value: {} })).toHaveLength(0);
217
- expect(b.push("step_skip", { id: "s2" })).toHaveLength(0);
218
- expect(b.push("tool_input_complete", { toolCallId: "tc1", parameters: {} })).toHaveLength(0);
219
- });
220
-
221
- it("unified `error` (recoverable) → agent_error{recoverable:true}, never the terminal `error`", () => {
222
- const b = new UnifiedToLegacyBridge();
223
- b.push("execution_start", { kind: "agent", executionId: "exec_test" });
224
- const evs = b.push("error", { recoverable: true, error: { message: "transient" } });
225
- expect(evs[0].payloadType).toBe("agent_error");
226
- expect(evs[0].payload.recoverable).toBe(true);
227
- });
228
-
229
- it("execution_error{kind:agent} → terminal agent_error{recoverable:false}", () => {
230
- const b = new UnifiedToLegacyBridge();
231
- b.push("execution_start", { kind: "agent", executionId: "exec_test" });
232
- const evs = b.push("execution_error", { kind: "agent", error: { message: "boom" } });
233
- expect(evs[0].payloadType).toBe("agent_error");
234
- expect(evs[0].payload.recoverable).toBe(false);
235
- });
236
-
237
- it("loop-scoped reasoning_complete folds into agent_reflection", () => {
238
- const b = new UnifiedToLegacyBridge();
239
- b.push("execution_start", { kind: "agent", executionId: "exec_test" });
240
- b.push("reasoning_start", { id: "reason_1", scope: "loop" });
241
- const evs = b.push("reasoning_complete", { id: "reason_1", text: "I should retry", scope: "loop" });
242
- expect(evs[0].payloadType).toBe("agent_reflection");
243
- expect(evs[0].payload.reflection).toBe("I should retry");
244
- });
245
-
246
- it("ask_user_question (non-webmcp local tool) await keeps its bare name", () => {
247
- const b = new UnifiedToLegacyBridge();
248
- b.push("execution_start", { kind: "agent", executionId: "exec_test" });
249
- const evs = b.push("await", { toolName: "ask_user_question", toolCallId: "tc9", parameters: {} });
250
- expect(evs[0].payloadType).toBe("step_await");
251
- expect(evs[0].payload.awaitReason).toBe("local_tool_required");
252
- expect(evs[0].payload.toolName).toBe("ask_user_question"); // no webmcp prefix
253
- });
254
- });
255
-
256
- describe("isUnifiedLifecycleStart", () => {
257
- it("identifies the unified vocabulary from the first lifecycle frame", () => {
258
- expect(isUnifiedLifecycleStart("execution_start")).toBe(true);
259
- expect(isUnifiedLifecycleStart("agent_start")).toBe(false);
260
- expect(isUnifiedLifecycleStart("flow_start")).toBe(false);
261
- expect(isUnifiedLifecycleStart("step_start")).toBe(false);
262
- });
263
- });