@runtypelabs/persona 3.36.0 → 4.0.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 (48) hide show
  1. package/README.md +0 -1
  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 +47 -47
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.d.cts +328 -821
  13. package/dist/index.d.ts +328 -821
  14. package/dist/index.global.js +39 -39
  15. package/dist/index.global.js.map +1 -1
  16. package/dist/index.js +47 -47
  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 +268 -181
  21. package/dist/smart-dom-reader.d.ts +268 -181
  22. package/dist/theme-editor-preview.cjs +41 -41
  23. package/dist/theme-editor-preview.d.cts +268 -181
  24. package/dist/theme-editor-preview.d.ts +268 -181
  25. package/dist/theme-editor-preview.js +41 -41
  26. package/dist/theme-editor.d.cts +268 -181
  27. package/dist/theme-editor.d.ts +268 -181
  28. package/package.json +1 -1
  29. package/src/client.test.ts +506 -1003
  30. package/src/client.ts +676 -909
  31. package/src/components/tool-bubble.ts +46 -33
  32. package/src/generated/runtype-openapi-contract.ts +211 -714
  33. package/src/index-core.ts +2 -3
  34. package/src/install.test.ts +1 -34
  35. package/src/install.ts +1 -22
  36. package/src/runtime/init.test.ts +8 -67
  37. package/src/runtime/init.ts +2 -17
  38. package/src/session.test.ts +8 -8
  39. package/src/session.ts +1 -1
  40. package/src/types.ts +11 -2
  41. package/src/ui.postprocess.test.ts +107 -0
  42. package/src/ui.ts +40 -5
  43. package/src/utils/__fixtures__/unified-translator.oracle.ts +717 -0
  44. package/src/utils/copy-selection.test.ts +37 -0
  45. package/src/utils/copy-selection.ts +19 -0
  46. package/src/utils/event-stream-capture.test.ts +9 -64
  47. package/src/utils/sequence-buffer.test.ts +0 -256
  48. package/src/utils/sequence-buffer.ts +0 -130
@@ -0,0 +1,717 @@
1
+ /**
2
+ * TEST ORACLE — vendored copy of the runtype-core `createUnifiedEventWrite`
3
+ * translator (the api-side legacy → unified 33-event encoder).
4
+ *
5
+ * NOT shipped. Used only by `unified-event-bridge.test.ts` as the inverse
6
+ * reference: `legacy frames → THIS oracle → unified frames → UnifiedToLegacyBridge
7
+ * → legacy frames'` must be semantically equal to the original. Keeping a real
8
+ * copy here means the round-trip test fails loudly if the api mapping and the
9
+ * widget bridge ever drift.
10
+ *
11
+ * The only edit vs the api source is the removed `getLogger` import (the
12
+ * per-frame error log is a no-op here).
13
+ */
14
+
15
+ type Json = Record<string, unknown>;
16
+
17
+ type ExecutionKind = "agent" | "flow";
18
+
19
+ interface UnifiedEventWriteOptions {
20
+ executionId?: string;
21
+ }
22
+
23
+ const ENVELOPE_KEYS = new Set(["type", "executionId", "seq"]);
24
+
25
+ function omitEnvelope(data: Json): Json {
26
+ const out: Json = {};
27
+ for (const [k, v] of Object.entries(data)) {
28
+ if (!ENVELOPE_KEYS.has(k)) out[k] = v;
29
+ }
30
+ return out;
31
+ }
32
+
33
+ class UnifiedEventTranslator {
34
+ private executionId: string;
35
+ private seq = 0;
36
+ private kind: ExecutionKind = "flow";
37
+ private blockCounter = 0;
38
+ private openText: string | null = null;
39
+ private openTextBuffer = "";
40
+ private openReasoning: string | null = null;
41
+ private openReasoningBuffer = "";
42
+
43
+ constructor(
44
+ private readonly sink: (chunk: string) => void,
45
+ options?: UnifiedEventWriteOptions
46
+ ) {
47
+ this.executionId = options?.executionId ?? "";
48
+ }
49
+
50
+ private mint(prefix: string): string {
51
+ this.blockCounter += 1;
52
+ return `${prefix}_${this.blockCounter}`;
53
+ }
54
+
55
+ private out(type: string, payload: Json): void {
56
+ const frame: Json = { type, executionId: this.executionId, seq: this.seq, ...payload };
57
+ this.seq += 1;
58
+ this.sink(`event: ${type}\ndata: ${JSON.stringify(frame)}\n\n`);
59
+ }
60
+
61
+ /**
62
+ * Parent model tool-call id stamped by the `tool_nested` FilteredStream as
63
+ * `toolContext.toolId` (a flow running as a tool enriches its frames this way).
64
+ * Surfaced on the text/reasoning channel as `parentToolCallId` so consumers can
65
+ * route nested streamed output into the parent tool's row (PR #4602). Undefined
66
+ * for top-level output.
67
+ */
68
+ private parentToolCallId(data: Json): string | undefined {
69
+ const toolContext = data.toolContext;
70
+ if (toolContext && typeof toolContext === "object") {
71
+ const toolId = (toolContext as Json).toolId;
72
+ if (typeof toolId === "string" && toolId) return toolId;
73
+ }
74
+ return undefined;
75
+ }
76
+
77
+ private emitTextDelta(delta: unknown, parentToolCallId?: string): void {
78
+ if (delta == null || delta === "") return;
79
+ if (!this.openText) {
80
+ this.openText = this.mint("text");
81
+ this.openTextBuffer = "";
82
+ this.out("text_start", {
83
+ id: this.openText,
84
+ ...(this.kind === "agent" ? { role: "assistant" } : {}),
85
+ ...(parentToolCallId ? { parentToolCallId } : {}),
86
+ });
87
+ }
88
+ const text = String(delta);
89
+ this.openTextBuffer += text;
90
+ this.out("text_delta", { id: this.openText, delta: text });
91
+ }
92
+
93
+ private closeText(): void {
94
+ if (!this.openText) return;
95
+ // U2: carry the assembled text so a non-streaming consumer can read the
96
+ // finished message off `text_complete` instead of re-concatenating deltas.
97
+ this.out("text_complete", {
98
+ id: this.openText,
99
+ ...(this.openTextBuffer ? { text: this.openTextBuffer } : {}),
100
+ });
101
+ this.openText = null;
102
+ this.openTextBuffer = "";
103
+ }
104
+
105
+ private ensureReasoningOpen(scope?: "turn" | "loop", parentToolCallId?: string): void {
106
+ if (this.openReasoning) return;
107
+ this.openReasoning = this.mint("reason");
108
+ this.openReasoningBuffer = "";
109
+ this.out("reasoning_start", {
110
+ id: this.openReasoning,
111
+ ...(scope ? { scope } : {}),
112
+ ...(parentToolCallId ? { parentToolCallId } : {}),
113
+ });
114
+ }
115
+
116
+ private emitReasoningDelta(delta: unknown, parentToolCallId?: string): void {
117
+ if (delta == null || delta === "") return;
118
+ this.ensureReasoningOpen(undefined, parentToolCallId);
119
+ const text = String(delta);
120
+ this.openReasoningBuffer += text;
121
+ this.out("reasoning_delta", { id: this.openReasoning, delta: text });
122
+ }
123
+
124
+ private closeReasoning(): void {
125
+ if (!this.openReasoning) return;
126
+ // U2: carry the assembled reasoning text (parity with text_complete).
127
+ this.out("reasoning_complete", {
128
+ id: this.openReasoning,
129
+ ...(this.openReasoningBuffer ? { text: this.openReasoningBuffer } : {}),
130
+ });
131
+ this.openReasoning = null;
132
+ this.openReasoningBuffer = "";
133
+ }
134
+
135
+ private closeChannels(): void {
136
+ this.closeText();
137
+ this.closeReasoning();
138
+ }
139
+
140
+ private fallbackInfo(data: Json): Json | undefined {
141
+ const used = data.usedFallback === true;
142
+ const executions = Array.isArray(data.fallbackExecutions) ? data.fallbackExecutions : [];
143
+ if (!used && executions.length === 0) return undefined;
144
+ const attempts = executions.map((entry: unknown, index: number) => {
145
+ const e = (entry ?? {}) as Json;
146
+ return {
147
+ attempt: typeof e.attempt === "number" ? e.attempt : index + 1,
148
+ type: typeof e.fallbackType === "string" ? e.fallbackType : "model",
149
+ ...(e.fallbackModel != null ? { model: e.fallbackModel } : {}),
150
+ success: true,
151
+ };
152
+ });
153
+ return {
154
+ fallback: {
155
+ used,
156
+ ...(data.modelUsed != null
157
+ ? { model: data.modelUsed }
158
+ : data.fallbackModel != null
159
+ ? { model: data.fallbackModel }
160
+ : {}),
161
+ attempts,
162
+ exhausted: false,
163
+ },
164
+ };
165
+ }
166
+
167
+ private handle(type: string, data: Json): void {
168
+ switch (type) {
169
+ case "agent_start":
170
+ this.kind = "agent";
171
+ if (typeof data.executionId === "string") this.executionId = data.executionId;
172
+ this.out("execution_start", {
173
+ kind: "agent",
174
+ startedAt: data.startedAt ?? new Date().toISOString(),
175
+ agentId: data.agentId,
176
+ agentName: data.agentName,
177
+ maxTurns: data.maxTurns,
178
+ config: data.config,
179
+ });
180
+ break;
181
+ case "agent_iteration_start":
182
+ case "agent_iteration_complete":
183
+ break;
184
+ case "agent_turn_start":
185
+ this.closeChannels();
186
+ this.out("turn_start", {
187
+ id: data.turnId,
188
+ iteration: data.iteration,
189
+ turnIndex: data.turnIndex,
190
+ role: data.role ?? "assistant",
191
+ });
192
+ break;
193
+ case "agent_turn_delta": {
194
+ const contentType = data.contentType;
195
+ if (contentType === "thinking") this.emitReasoningDelta(data.delta);
196
+ else if (contentType === "tool_input")
197
+ this.out("tool_input_delta", {
198
+ toolCallId: data.turnId ?? this.mint("toolinput"),
199
+ delta: String(data.delta ?? ""),
200
+ });
201
+ else this.emitTextDelta(data.delta);
202
+ break;
203
+ }
204
+ case "agent_turn_complete":
205
+ this.closeChannels();
206
+ this.out("turn_complete", {
207
+ id: data.turnId,
208
+ iteration: data.iteration,
209
+ role: data.role ?? "assistant",
210
+ content: data.content,
211
+ tokens: data.tokens,
212
+ cost: data.cost,
213
+ stopReason: data.stopReason,
214
+ completedAt: data.completedAt,
215
+ });
216
+ break;
217
+ case "agent_tool_start":
218
+ this.out("tool_start", {
219
+ toolCallId: data.toolCallId,
220
+ toolName: data.toolName,
221
+ toolType: data.toolType ?? "builtin",
222
+ iteration: data.iteration,
223
+ parameters: data.parameters,
224
+ hiddenParameterNames: data.hiddenParameterNames,
225
+ origin: data.origin,
226
+ pageOrigin: data.pageOrigin,
227
+ });
228
+ break;
229
+ case "agent_tool_delta":
230
+ this.out("tool_output_delta", {
231
+ toolCallId: data.toolCallId,
232
+ delta: String(data.delta ?? ""),
233
+ });
234
+ break;
235
+ case "agent_tool_input_delta":
236
+ this.out("tool_input_delta", {
237
+ toolCallId: data.toolCallId,
238
+ delta: String(data.delta ?? ""),
239
+ });
240
+ break;
241
+ case "agent_tool_input_complete":
242
+ this.out("tool_input_complete", {
243
+ toolCallId: data.toolCallId,
244
+ toolName: data.toolName,
245
+ parameters: (data.parameters as Json) ?? {},
246
+ hiddenParameterNames: data.hiddenParameterNames,
247
+ });
248
+ break;
249
+ case "agent_tool_complete":
250
+ this.out("tool_complete", {
251
+ toolCallId: data.toolCallId,
252
+ toolName: data.toolName,
253
+ success: data.success ?? true,
254
+ result: data.result,
255
+ executionTime: data.executionTime,
256
+ iteration: data.iteration,
257
+ });
258
+ break;
259
+ case "agent_media":
260
+ this.emitMedia(data);
261
+ break;
262
+ case "agent_approval_start":
263
+ this.out("approval_start", {
264
+ approvalId: data.approvalId,
265
+ toolCallId: data.toolCallId,
266
+ toolName: data.toolName,
267
+ toolType: data.toolType,
268
+ description: data.description,
269
+ reason: data.reason,
270
+ parameters: data.parameters,
271
+ timeout: data.timeout,
272
+ startedAt: data.startedAt,
273
+ iteration: data.iteration,
274
+ });
275
+ break;
276
+ case "agent_approval_complete":
277
+ this.out("approval_complete", {
278
+ approvalId: data.approvalId,
279
+ decision: data.decision,
280
+ completedAt: data.completedAt,
281
+ resolvedBy: data.resolvedBy,
282
+ });
283
+ break;
284
+ case "agent_await":
285
+ this.out("await", {
286
+ toolId: data.toolId,
287
+ toolCallId: data.toolCallId,
288
+ toolName: data.toolName,
289
+ parameters: data.parameters,
290
+ awaitedAt: data.awaitedAt,
291
+ origin: data.origin,
292
+ pageOrigin: data.pageOrigin,
293
+ });
294
+ break;
295
+ case "agent_reflection": {
296
+ const id = this.mint("reason");
297
+ this.out("reasoning_start", { id, scope: "loop" });
298
+ this.out("reasoning_complete", { id, text: data.reflection, scope: "loop" });
299
+ break;
300
+ }
301
+ case "agent_skill_loaded": {
302
+ const toolCallId = (data.toolCallId as string) ?? this.mint("skill");
303
+ const toolName = `skill:${String(data.skill ?? "unknown")}`;
304
+ this.out("tool_start", {
305
+ toolCallId,
306
+ toolName,
307
+ toolType: "builtin",
308
+ iteration: data.iteration,
309
+ });
310
+ this.out("tool_complete", {
311
+ toolCallId,
312
+ toolName,
313
+ success: true,
314
+ result: {
315
+ kind: "skill_loaded",
316
+ skill: data.skill,
317
+ activatedCapabilities: data.activatedCapabilities ?? [],
318
+ },
319
+ iteration: data.iteration,
320
+ });
321
+ break;
322
+ }
323
+ case "agent_skill_proposed": {
324
+ const toolCallId = (data.toolCallId as string) ?? this.mint("propose");
325
+ this.out("tool_start", {
326
+ toolCallId,
327
+ toolName: "propose_skill",
328
+ toolType: "builtin",
329
+ iteration: data.iteration,
330
+ });
331
+ this.out("tool_complete", {
332
+ toolCallId,
333
+ toolName: "propose_skill",
334
+ success: true,
335
+ result: {
336
+ kind: "skill_proposed",
337
+ skill: data.skill,
338
+ outcome: data.outcome,
339
+ proposalId: data.proposalId,
340
+ },
341
+ iteration: data.iteration,
342
+ });
343
+ break;
344
+ }
345
+ case "agent_complete":
346
+ this.closeChannels();
347
+ if (data.success === false || data.stopReason === "error") {
348
+ this.out("execution_error", {
349
+ kind: "agent",
350
+ error: this.toErrorPayload(data.error ?? "Agent execution failed"),
351
+ completedAt: data.completedAt,
352
+ });
353
+ } else {
354
+ this.out("execution_complete", {
355
+ kind: "agent",
356
+ success: data.success ?? true,
357
+ iterations: data.iterations,
358
+ stopReason: data.stopReason,
359
+ completedAt: data.completedAt,
360
+ durationMs: data.duration,
361
+ finalOutput: data.finalOutput,
362
+ totalCost: data.totalCost,
363
+ totalTokens: data.totalTokens,
364
+ });
365
+ }
366
+ break;
367
+ case "agent_error":
368
+ if (data.recoverable)
369
+ this.out("error", { error: this.toErrorPayload(data.error), recoverable: true });
370
+ else this.out("execution_error", { kind: this.kind, error: this.toErrorPayload(data.error) });
371
+ break;
372
+ case "agent_ping":
373
+ this.out("ping", { timestamp: data.timestamp ?? new Date().toISOString() });
374
+ break;
375
+
376
+ case "flow_start":
377
+ this.kind = "flow";
378
+ if (typeof data.executionId === "string") this.executionId = data.executionId;
379
+ this.out("execution_start", {
380
+ kind: "flow",
381
+ startedAt: data.startedAt ?? new Date().toISOString(),
382
+ flowId: data.flowId,
383
+ flowName: data.flowName,
384
+ totalSteps: data.totalSteps,
385
+ source: data.source,
386
+ });
387
+ break;
388
+ case "flow_complete":
389
+ this.closeChannels();
390
+ this.out("execution_complete", {
391
+ kind: "flow",
392
+ success: data.success ?? true,
393
+ completedAt: data.completedAt,
394
+ durationMs: data.duration ?? data.executionTime,
395
+ finalOutput: data.finalOutput,
396
+ totalSteps: data.totalSteps,
397
+ successfulSteps: data.successfulSteps,
398
+ failedSteps: data.failedSteps,
399
+ });
400
+ break;
401
+ case "flow_error":
402
+ this.closeChannels();
403
+ this.out("execution_error", {
404
+ kind: "flow",
405
+ error: this.toErrorPayload(data.error),
406
+ code: data.code,
407
+ upgradeUrl: data.upgradeUrl,
408
+ });
409
+ break;
410
+ case "flow_await":
411
+ this.out("await", {
412
+ toolId: data.toolId,
413
+ toolCallId: data.toolCallId,
414
+ toolName: data.toolName,
415
+ parameters: data.parameters,
416
+ awaitedAt: data.awaitedAt,
417
+ origin: data.origin,
418
+ pageOrigin: data.pageOrigin,
419
+ });
420
+ break;
421
+ case "step_start":
422
+ this.closeChannels();
423
+ this.out("step_start", {
424
+ id: data.id ?? data.stepId,
425
+ name: data.name ?? data.stepName,
426
+ stepType: data.stepType,
427
+ index: data.index,
428
+ totalSteps: data.totalSteps,
429
+ startedAt: data.startedAt,
430
+ outputVariable: data.outputVariable,
431
+ });
432
+ break;
433
+ case "step_delta":
434
+ this.emitTextDelta(data.text ?? data.delta, this.parentToolCallId(data));
435
+ break;
436
+ case "step_complete":
437
+ this.closeChannels();
438
+ this.out("step_complete", {
439
+ id: data.id ?? data.stepId,
440
+ name: data.name ?? data.stepName,
441
+ stepType: data.stepType,
442
+ success: data.success ?? true,
443
+ durationMs: data.duration ?? data.durationMs ?? data.executionTime,
444
+ result: data.result ?? data.output,
445
+ stopReason: data.stopReason,
446
+ completedAt: data.completedAt,
447
+ unresolvedVariables: data.unresolvedVariables,
448
+ ...(this.fallbackInfo(data) ?? {}),
449
+ });
450
+ break;
451
+ case "step_error":
452
+ this.closeChannels();
453
+ this.out("step_complete", {
454
+ id: data.id ?? data.stepId,
455
+ name: data.name,
456
+ stepType: data.stepType,
457
+ success: false,
458
+ error: data.error,
459
+ durationMs: data.executionTime,
460
+ });
461
+ break;
462
+ case "step_skip":
463
+ this.out("step_skip", {
464
+ id: data.id,
465
+ name: data.name,
466
+ stepType: data.stepType,
467
+ index: data.index,
468
+ totalSteps: data.totalSteps,
469
+ when: data.when,
470
+ skippedAt: data.skippedAt,
471
+ });
472
+ break;
473
+ case "step_await":
474
+ this.out("await", {
475
+ toolId: data.toolId,
476
+ toolCallId: data.toolCallId,
477
+ toolName: data.toolName,
478
+ parameters: data.parameters,
479
+ awaitedAt: data.awaitedAt,
480
+ });
481
+ break;
482
+
483
+ case "tool_start":
484
+ this.out("tool_start", {
485
+ toolCallId: data.toolCallId ?? data.toolId,
486
+ toolName: data.toolName ?? data.name,
487
+ toolType: data.toolType ?? "builtin",
488
+ stepId: data.stepId,
489
+ parameters: data.parameters,
490
+ hiddenParameterNames: data.hiddenParameterNames,
491
+ startedAt: data.startedAt,
492
+ });
493
+ break;
494
+ case "tool_delta":
495
+ this.out("tool_output_delta", {
496
+ toolCallId: data.toolId ?? data.toolCallId,
497
+ delta: String(data.delta ?? ""),
498
+ });
499
+ break;
500
+ case "tool_input_delta":
501
+ this.out("tool_input_delta", {
502
+ toolCallId: data.toolCallId ?? data.toolId,
503
+ delta: String(data.delta ?? ""),
504
+ });
505
+ break;
506
+ case "tool_input_complete":
507
+ this.out("tool_input_complete", {
508
+ toolCallId: data.toolCallId ?? data.toolId,
509
+ toolName: data.toolName,
510
+ parameters: (data.parameters as Json) ?? {},
511
+ hiddenParameterNames: data.hiddenParameterNames,
512
+ });
513
+ break;
514
+ case "tool_complete":
515
+ this.out("tool_complete", {
516
+ toolCallId: data.toolCallId ?? data.toolId,
517
+ toolName: data.toolName ?? data.name,
518
+ success: data.success ?? true,
519
+ result: data.result,
520
+ error: data.error,
521
+ executionTime: data.executionTime,
522
+ stepId: data.stepId,
523
+ });
524
+ break;
525
+ case "tool_error":
526
+ this.out("tool_complete", {
527
+ toolCallId: data.toolId ?? data.toolCallId,
528
+ toolName: data.name,
529
+ success: false,
530
+ error: data.error,
531
+ executionTime: data.executionTime,
532
+ });
533
+ break;
534
+ case "chunk":
535
+ this.emitTextDelta(data.text, this.parentToolCallId(data));
536
+ break;
537
+
538
+ case "text_start":
539
+ if (!this.openText) {
540
+ this.openText = (data.id as string) ?? this.mint("text");
541
+ this.openTextBuffer = "";
542
+ const parentToolCallId = this.parentToolCallId(data);
543
+ this.out("text_start", {
544
+ id: this.openText,
545
+ ...(parentToolCallId ? { parentToolCallId } : {}),
546
+ });
547
+ }
548
+ break;
549
+ case "text_end":
550
+ this.closeText();
551
+ break;
552
+ case "reason_start":
553
+ this.ensureReasoningOpen(undefined, this.parentToolCallId(data));
554
+ break;
555
+ case "reason_delta":
556
+ this.emitReasoningDelta(
557
+ data.reasoningText ?? data.delta ?? data.text,
558
+ this.parentToolCallId(data)
559
+ );
560
+ break;
561
+ case "reason_complete":
562
+ this.closeReasoning();
563
+ break;
564
+ case "source":
565
+ this.out("source", omitEnvelope(data));
566
+ break;
567
+
568
+ case "fallback_start":
569
+ this.out("custom", {
570
+ name: "runtype.fallback",
571
+ value: { phase: "start", ...omitEnvelope(data) },
572
+ });
573
+ break;
574
+ case "fallback_complete":
575
+ this.out("custom", {
576
+ name: "runtype.fallback",
577
+ value: { phase: "complete", ...omitEnvelope(data) },
578
+ });
579
+ break;
580
+ case "fallback_exhausted":
581
+ this.out("custom", {
582
+ name: "runtype.fallback",
583
+ value: { phase: "exhausted", ...omitEnvelope(data) },
584
+ });
585
+ break;
586
+
587
+ case "artifact_start":
588
+ this.out("artifact_start", {
589
+ id: data.id,
590
+ artifactType: data.artifactType,
591
+ title: data.title,
592
+ component: data.component,
593
+ });
594
+ break;
595
+ case "artifact_delta":
596
+ this.out("artifact_delta", { id: data.id, delta: data.delta });
597
+ break;
598
+ case "artifact_update":
599
+ this.out("artifact_update", { id: data.id, component: data.component, props: data.props });
600
+ break;
601
+ case "artifact_complete":
602
+ this.out("artifact_complete", { id: data.id });
603
+ break;
604
+ case "artifact":
605
+ this.out("artifact_start", {
606
+ id: data.id,
607
+ artifactType: data.artifactType ?? "markdown",
608
+ title: data.title,
609
+ component: data.component,
610
+ });
611
+ this.out("artifact_complete", { id: data.id });
612
+ break;
613
+
614
+ case "dispatch_error":
615
+ this.closeChannels();
616
+ this.out("execution_error", {
617
+ kind: this.kind,
618
+ error: this.toErrorPayload(data),
619
+ code: data.code,
620
+ upgradeUrl: data.upgradeUrl,
621
+ });
622
+ break;
623
+
624
+ default:
625
+ break;
626
+ }
627
+ }
628
+
629
+ private emitMedia(data: Json): void {
630
+ const media = Array.isArray(data.media) ? data.media : [];
631
+ for (const raw of media) {
632
+ const item = (raw ?? {}) as Json;
633
+ const id = this.mint("media");
634
+ const mediaType =
635
+ typeof item.mediaType === "string"
636
+ ? item.mediaType
637
+ : item.type === "image-url"
638
+ ? "image"
639
+ : "application/octet-stream";
640
+ this.out("media_start", {
641
+ id,
642
+ mediaType,
643
+ role: "assistant",
644
+ toolCallId: data.toolCallId,
645
+ });
646
+ const fragment =
647
+ typeof item.url === "string" ? item.url : typeof item.data === "string" ? item.data : "";
648
+ if (fragment) this.out("media_delta", { id, delta: fragment });
649
+ this.out("media_complete", {
650
+ id,
651
+ mediaType,
652
+ url: item.url,
653
+ data: item.data,
654
+ toolCallId: data.toolCallId,
655
+ });
656
+ }
657
+ }
658
+
659
+ private toErrorPayload(error: unknown): Json | string {
660
+ if (typeof error === "string") return error;
661
+ if (error && typeof error === "object") {
662
+ const e = error as Json;
663
+ if (typeof e.code === "string" && typeof e.message === "string") {
664
+ return {
665
+ code: e.code,
666
+ message: e.message,
667
+ ...(e.details ? { details: e.details } : {}),
668
+ };
669
+ }
670
+ if (typeof e.error === "string") return e.error;
671
+ if (typeof e.message === "string")
672
+ return { code: typeof e.code === "string" ? e.code : "error", message: e.message };
673
+ }
674
+ return { code: "error", message: "Execution failed" };
675
+ }
676
+
677
+ write(chunk: string): void {
678
+ const parts = chunk.split("\n\n");
679
+ for (const part of parts) {
680
+ if (!part.trim()) continue;
681
+ let dataStr: string | null = null;
682
+ for (const line of part.split("\n")) {
683
+ if (line.startsWith("data: ")) dataStr = line.slice(6);
684
+ }
685
+ if (dataStr === null) {
686
+ this.sink(`${part}\n\n`);
687
+ continue;
688
+ }
689
+ if (dataStr.trim() === "[DONE]") {
690
+ this.sink("data: [DONE]\n\n");
691
+ continue;
692
+ }
693
+ let data: Json;
694
+ try {
695
+ data = JSON.parse(dataStr) as Json;
696
+ } catch {
697
+ this.sink(`${part}\n\n`);
698
+ continue;
699
+ }
700
+ const type = data?.type;
701
+ if (typeof type !== "string") continue;
702
+ try {
703
+ this.handle(type, data);
704
+ } catch {
705
+ // oracle fixture: swallow per-frame translation errors (prod logs via getLogger)
706
+ }
707
+ }
708
+ }
709
+ }
710
+
711
+ export function createUnifiedEventWrite(
712
+ sink: (chunk: string) => void,
713
+ options?: UnifiedEventWriteOptions
714
+ ): (chunk: string) => void {
715
+ const translator = new UnifiedEventTranslator(sink, options);
716
+ return (chunk: string) => translator.write(chunk);
717
+ }