@runtypelabs/persona 3.15.0 → 3.16.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/dist/index.cjs +46 -46
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.global.js +65 -65
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +46 -46
- package/dist/index.js.map +1 -1
- package/dist/theme-editor.cjs +445 -222
- package/dist/theme-editor.d.cts +11 -0
- package/dist/theme-editor.d.ts +11 -0
- package/dist/theme-editor.js +445 -222
- package/dist/widget.css +11 -8
- package/package.json +1 -1
- package/src/client.test.ts +361 -0
- package/src/client.ts +352 -158
- package/src/components/header-builder.ts +18 -7
- package/src/components/header-layouts.ts +3 -1
- package/src/defaults.ts +6 -0
- package/src/styles/widget.css +11 -8
- package/src/types.ts +11 -0
- package/src/ui.ts +31 -4
- package/src/utils/sequence-buffer.test.ts +256 -0
- package/src/utils/sequence-buffer.ts +130 -0
package/src/client.ts
CHANGED
|
@@ -20,13 +20,14 @@ import {
|
|
|
20
20
|
ClientFeedbackType,
|
|
21
21
|
PersonaArtifactKind
|
|
22
22
|
} from "./types";
|
|
23
|
-
import {
|
|
24
|
-
extractTextFromJson,
|
|
23
|
+
import {
|
|
24
|
+
extractTextFromJson,
|
|
25
25
|
createPlainTextParser,
|
|
26
26
|
createJsonStreamParser,
|
|
27
27
|
createRegexJsonParser,
|
|
28
28
|
createXmlParser
|
|
29
29
|
} from "./utils/formatting";
|
|
30
|
+
import { SequenceReorderBuffer } from "./utils/sequence-buffer";
|
|
30
31
|
// artifactsSidebarEnabled is used in ui.ts to gate the sidebar pane rendering;
|
|
31
32
|
// artifact events are always processed here regardless of config.
|
|
32
33
|
|
|
@@ -1056,6 +1057,16 @@ export class AgentWidgetClient {
|
|
|
1056
1057
|
let didSplitByPartId = false;
|
|
1057
1058
|
const reasoningMessages = new Map<string, AgentWidgetMessage>();
|
|
1058
1059
|
const toolMessages = new Map<string, AgentWidgetMessage>();
|
|
1060
|
+
// Messages produced by steps inside a nested flow executed as a tool.
|
|
1061
|
+
// Keyed by `${parentToolId}::${nestedStepId}::${partId}` so each nested
|
|
1062
|
+
// step (send-stream, prompt) gets its own assistant message, and prompts
|
|
1063
|
+
// with inner tool calls split into one message per text segment — still
|
|
1064
|
+
// attributable to the parent tool call.
|
|
1065
|
+
const nestedStepMessages = new Map<string, AgentWidgetMessage>();
|
|
1066
|
+
// Most-recent partId seen for a given `${toolId}::${stepId}` scope, used
|
|
1067
|
+
// to seal the previous segment when a new partId arrives within the
|
|
1068
|
+
// same nested prompt step.
|
|
1069
|
+
const nestedPartIdByStep = new Map<string, string>();
|
|
1059
1070
|
const reasoningContext = {
|
|
1060
1071
|
lastId: null as string | null,
|
|
1061
1072
|
byStep: new Map<string, string>()
|
|
@@ -1065,6 +1076,49 @@ export class AgentWidgetClient {
|
|
|
1065
1076
|
byCall: new Map<string, string>()
|
|
1066
1077
|
};
|
|
1067
1078
|
|
|
1079
|
+
// Nested message key. partId defaults to "" so steps without segmentation
|
|
1080
|
+
// (e.g. send-stream) still have a deterministic single key.
|
|
1081
|
+
const getNestedStepKey = (
|
|
1082
|
+
toolId: string,
|
|
1083
|
+
stepId: string,
|
|
1084
|
+
partId: string = ""
|
|
1085
|
+
) => `${toolId}::${stepId}::${partId}`;
|
|
1086
|
+
|
|
1087
|
+
// Prefix used to sweep every nested message belonging to a single
|
|
1088
|
+
// (toolId, stepId) scope — needed on step_complete to seal any segments
|
|
1089
|
+
// that are still streaming.
|
|
1090
|
+
const getNestedStepPrefix = (toolId: string, stepId: string) =>
|
|
1091
|
+
`${toolId}::${stepId}::`;
|
|
1092
|
+
|
|
1093
|
+
const ensureNestedStepMessage = (
|
|
1094
|
+
toolId: string,
|
|
1095
|
+
stepId: string,
|
|
1096
|
+
partId: string,
|
|
1097
|
+
executionId?: string
|
|
1098
|
+
): AgentWidgetMessage => {
|
|
1099
|
+
const key = getNestedStepKey(toolId, stepId, partId);
|
|
1100
|
+
const existing = nestedStepMessages.get(key);
|
|
1101
|
+
if (existing) return existing;
|
|
1102
|
+
const idSuffix = partId ? `-${partId}` : "";
|
|
1103
|
+
const message: AgentWidgetMessage = {
|
|
1104
|
+
id: `nested-${toolId}-${stepId}${idSuffix}`,
|
|
1105
|
+
role: "assistant",
|
|
1106
|
+
content: "",
|
|
1107
|
+
createdAt: new Date().toISOString(),
|
|
1108
|
+
streaming: true,
|
|
1109
|
+
sequence: nextSequence(),
|
|
1110
|
+
...(partId ? { partId } : {}),
|
|
1111
|
+
agentMetadata: {
|
|
1112
|
+
executionId,
|
|
1113
|
+
parentToolId: toolId,
|
|
1114
|
+
parentStepId: stepId,
|
|
1115
|
+
},
|
|
1116
|
+
};
|
|
1117
|
+
nestedStepMessages.set(key, message);
|
|
1118
|
+
emitMessage(message);
|
|
1119
|
+
return message;
|
|
1120
|
+
};
|
|
1121
|
+
|
|
1068
1122
|
const normalizeKey = (value: unknown): string | null => {
|
|
1069
1123
|
if (value === null || value === undefined) return null;
|
|
1070
1124
|
try {
|
|
@@ -1295,44 +1349,45 @@ export class AgentWidgetClient {
|
|
|
1295
1349
|
const streamParsers = new Map<string, AgentWidgetStreamParser>();
|
|
1296
1350
|
// Track accumulated raw content for structured formats (JSON, XML, etc.)
|
|
1297
1351
|
const rawContentBuffers = new Map<string, string>();
|
|
1298
|
-
//
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
const
|
|
1352
|
+
// Rebuild incremental text by sequence so late arrivals can repair already-emitted
|
|
1353
|
+
// content after the reorder buffer's gap-timeout flush.
|
|
1354
|
+
const orderedChunkBuffers = new Map<string, Array<{ seq: number; text: string }>>();
|
|
1355
|
+
const assistantMessagesByPartId = new Map<string, AgentWidgetMessage>();
|
|
1356
|
+
// Only the most-recently sealed segment is reconciled with step_complete's
|
|
1357
|
+
// final response. Earlier segments rely on their own async parser microtasks
|
|
1358
|
+
// resolving via the closure-captured `assistant` variable.
|
|
1359
|
+
let lastSealedTextSegment: AgentWidgetMessage | null = null;
|
|
1302
1360
|
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
bufferMap: Map<string, Array<{ seq: number; text: string }>>,
|
|
1309
|
-
key: string,
|
|
1310
|
-
seq: number,
|
|
1311
|
-
text: string
|
|
1312
|
-
): string {
|
|
1313
|
-
let buf = bufferMap.get(key);
|
|
1314
|
-
if (!buf) {
|
|
1315
|
-
buf = [];
|
|
1316
|
-
bufferMap.set(key, buf);
|
|
1361
|
+
const insertOrderedChunk = (key: string, seq: number, text: string): string => {
|
|
1362
|
+
let chunks = orderedChunkBuffers.get(key);
|
|
1363
|
+
if (!chunks) {
|
|
1364
|
+
chunks = [];
|
|
1365
|
+
orderedChunkBuffers.set(key, chunks);
|
|
1317
1366
|
}
|
|
1318
|
-
|
|
1319
|
-
let lo = 0
|
|
1367
|
+
|
|
1368
|
+
let lo = 0;
|
|
1369
|
+
let hi = chunks.length;
|
|
1320
1370
|
while (lo < hi) {
|
|
1321
1371
|
const mid = (lo + hi) >>> 1;
|
|
1322
|
-
if (
|
|
1323
|
-
|
|
1372
|
+
if (chunks[mid].seq < seq) {
|
|
1373
|
+
lo = mid + 1;
|
|
1374
|
+
} else {
|
|
1375
|
+
hi = mid;
|
|
1376
|
+
}
|
|
1324
1377
|
}
|
|
1325
|
-
buf.splice(lo, 0, { seq, text });
|
|
1326
|
-
// Join all chunks in seq order
|
|
1327
|
-
let result = "";
|
|
1328
|
-
for (let i = 0; i < buf.length; i++) result += buf[i].text;
|
|
1329
|
-
return result;
|
|
1330
|
-
}
|
|
1331
1378
|
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1379
|
+
if (chunks[lo]?.seq === seq) {
|
|
1380
|
+
chunks[lo] = { seq, text };
|
|
1381
|
+
} else {
|
|
1382
|
+
chunks.splice(lo, 0, { seq, text });
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
let accumulated = "";
|
|
1386
|
+
for (let index = 0; index < chunks.length; index++) {
|
|
1387
|
+
accumulated += chunks[index].text;
|
|
1388
|
+
}
|
|
1389
|
+
return accumulated;
|
|
1390
|
+
};
|
|
1336
1391
|
|
|
1337
1392
|
/**
|
|
1338
1393
|
* After text_end + didSplitByPartId, merge the authoritative final response into the
|
|
@@ -1426,73 +1481,45 @@ export class AgentWidgetClient {
|
|
|
1426
1481
|
finalizeCleanup();
|
|
1427
1482
|
};
|
|
1428
1483
|
|
|
1484
|
+
// Sequence reorder buffer: SSE events carrying a `seq` (or `sequenceIndex`)
|
|
1485
|
+
// field are held and re-emitted in sequence order so that transport-level
|
|
1486
|
+
// reordering doesn't produce garbled output.
|
|
1487
|
+
const seqReadyQueue: Array<{ payloadType: string; payload: any }> = [];
|
|
1488
|
+
let isDrainScheduled = false;
|
|
1489
|
+
// Declared here so scheduleReadyQueueDrain can reference it; assigned
|
|
1490
|
+
// after all handler-scoped variables are initialised (before the SSE loop).
|
|
1491
|
+
let drainReadyQueue: () => void;
|
|
1492
|
+
// Two drain paths — both are intentional, do not remove either:
|
|
1493
|
+
// 1. Microtask drain (scheduleReadyQueueDrain): required when the
|
|
1494
|
+
// buffer's emitter fires from the gap-timeout setTimeout callback,
|
|
1495
|
+
// because there is no surrounding synchronous drain site there.
|
|
1496
|
+
// 2. Synchronous drain (drainReadyQueue() after each seqBuffer.push):
|
|
1497
|
+
// skips an extra microtask hop on the hot in-order push path.
|
|
1498
|
+
const scheduleReadyQueueDrain = () => {
|
|
1499
|
+
if (isDrainScheduled) return;
|
|
1500
|
+
isDrainScheduled = true;
|
|
1501
|
+
queueMicrotask(() => {
|
|
1502
|
+
isDrainScheduled = false;
|
|
1503
|
+
drainReadyQueue();
|
|
1504
|
+
});
|
|
1505
|
+
};
|
|
1506
|
+
const seqBuffer = new SequenceReorderBuffer((payloadType: string, payload: any) => {
|
|
1507
|
+
seqReadyQueue.push({ payloadType, payload });
|
|
1508
|
+
scheduleReadyQueueDrain();
|
|
1509
|
+
});
|
|
1429
1510
|
// Agent execution state tracking
|
|
1430
1511
|
let agentExecution: AgentExecutionState | null = null;
|
|
1431
1512
|
// Track assistant messages per agent iteration for 'separate' mode
|
|
1432
1513
|
const agentIterationMessages = new Map<number, AgentWidgetMessage>();
|
|
1433
1514
|
const iterationDisplay = this.config.iterationDisplay ?? 'separate';
|
|
1434
1515
|
|
|
1435
|
-
//
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
buffer = events.pop() ?? "";
|
|
1443
|
-
|
|
1444
|
-
for (const event of events) {
|
|
1445
|
-
const lines = event.split("\n");
|
|
1446
|
-
let eventType = "message";
|
|
1447
|
-
let data = "";
|
|
1448
|
-
|
|
1449
|
-
for (const line of lines) {
|
|
1450
|
-
if (line.startsWith("event:")) {
|
|
1451
|
-
eventType = line.replace("event:", "").trim();
|
|
1452
|
-
} else if (line.startsWith("data:")) {
|
|
1453
|
-
data += line.replace("data:", "").trim();
|
|
1454
|
-
}
|
|
1455
|
-
}
|
|
1456
|
-
|
|
1457
|
-
if (!data) continue;
|
|
1458
|
-
let payload: any;
|
|
1459
|
-
try {
|
|
1460
|
-
payload = JSON.parse(data);
|
|
1461
|
-
} catch (error) {
|
|
1462
|
-
onEvent({
|
|
1463
|
-
type: "error",
|
|
1464
|
-
error:
|
|
1465
|
-
error instanceof Error
|
|
1466
|
-
? error
|
|
1467
|
-
: new Error("Failed to parse chat stream payload")
|
|
1468
|
-
});
|
|
1469
|
-
continue;
|
|
1470
|
-
}
|
|
1471
|
-
|
|
1472
|
-
const payloadType =
|
|
1473
|
-
eventType !== "message" ? eventType : payload.type ?? "message";
|
|
1474
|
-
|
|
1475
|
-
// Tap: capture raw SSE event for event stream inspector
|
|
1476
|
-
this.onSSEEvent?.(payloadType, payload);
|
|
1477
|
-
|
|
1478
|
-
// If custom SSE event parser is provided, try it first
|
|
1479
|
-
if (this.parseSSEEvent) {
|
|
1480
|
-
// Keep assistant message ref in sync
|
|
1481
|
-
assistantMessageRef.current = assistantMessage;
|
|
1482
|
-
const handled = await this.handleCustomSSEEvent(
|
|
1483
|
-
payload,
|
|
1484
|
-
onEvent,
|
|
1485
|
-
assistantMessageRef,
|
|
1486
|
-
emitMessage,
|
|
1487
|
-
nextSequence,
|
|
1488
|
-
partIdState
|
|
1489
|
-
);
|
|
1490
|
-
// Update assistantMessage from ref (in case it was created or replaced by partId segmentation)
|
|
1491
|
-
if (assistantMessageRef.current && assistantMessageRef.current !== assistantMessage) {
|
|
1492
|
-
assistantMessage = assistantMessageRef.current;
|
|
1493
|
-
}
|
|
1494
|
-
if (handled) continue; // Skip default handling if custom handler processed it
|
|
1495
|
-
}
|
|
1516
|
+
// Drains reorder-buffered events through the main event handler.
|
|
1517
|
+
// Also invoked after the SSE loop exits so any events buffered at
|
|
1518
|
+
// end-of-stream are processed.
|
|
1519
|
+
drainReadyQueue = () => {
|
|
1520
|
+
for (let i = 0; i < seqReadyQueue.length; i++) {
|
|
1521
|
+
const payloadType = seqReadyQueue[i].payloadType;
|
|
1522
|
+
const payload = seqReadyQueue[i].payload;
|
|
1496
1523
|
|
|
1497
1524
|
if (payloadType === "reason_start") {
|
|
1498
1525
|
const reasoningId =
|
|
@@ -1531,12 +1558,11 @@ export class AgentWidgetClient {
|
|
|
1531
1558
|
payload.delta ??
|
|
1532
1559
|
"";
|
|
1533
1560
|
if (chunk && payload.hidden !== true) {
|
|
1534
|
-
const reasonSeq = typeof payload.sequenceIndex ===
|
|
1561
|
+
const reasonSeq = typeof payload.sequenceIndex === "number" ? payload.sequenceIndex : undefined;
|
|
1535
1562
|
if (reasonSeq !== undefined) {
|
|
1536
|
-
//
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
// We store a single joined string to avoid duplicated / mis-ordered entries.
|
|
1563
|
+
// Rebuild chunks by seq so late arrivals after a gap-timeout flush
|
|
1564
|
+
// are inserted at the correct position rather than appended.
|
|
1565
|
+
const ordered = insertOrderedChunk(reasoningId, reasonSeq, String(chunk));
|
|
1540
1566
|
reasoningMessage.reasoning.chunks = [ordered];
|
|
1541
1567
|
} else {
|
|
1542
1568
|
reasoningMessage.reasoning.chunks.push(String(chunk));
|
|
@@ -1552,7 +1578,7 @@ export class AgentWidgetClient {
|
|
|
1552
1578
|
0,
|
|
1553
1579
|
(reasoningMessage.reasoning.completedAt ?? Date.now()) - start
|
|
1554
1580
|
);
|
|
1555
|
-
|
|
1581
|
+
|
|
1556
1582
|
}
|
|
1557
1583
|
reasoningMessage.streaming = reasoningMessage.reasoning.status !== "complete";
|
|
1558
1584
|
emitMessage(reasoningMessage);
|
|
@@ -1573,7 +1599,7 @@ export class AgentWidgetClient {
|
|
|
1573
1599
|
(reasoningMessage.reasoning.completedAt ?? Date.now()) - start
|
|
1574
1600
|
);
|
|
1575
1601
|
reasoningMessage.streaming = false;
|
|
1576
|
-
|
|
1602
|
+
|
|
1577
1603
|
emitMessage(reasoningMessage);
|
|
1578
1604
|
}
|
|
1579
1605
|
const stepKey = getStepKey(payload);
|
|
@@ -1696,7 +1722,13 @@ export class AgentWidgetClient {
|
|
|
1696
1722
|
toolContext.byCall.delete(callKey);
|
|
1697
1723
|
}
|
|
1698
1724
|
} else if (payloadType === "text_start") {
|
|
1699
|
-
// Lifecycle event: a new text segment is beginning (emitted at tool boundaries)
|
|
1725
|
+
// Lifecycle event: a new text segment is beginning (emitted at tool boundaries).
|
|
1726
|
+
// When toolContext is present this fired inside a nested flow — it must not
|
|
1727
|
+
// seal or rotate the outer assistant message. Nested prompt segmentation is
|
|
1728
|
+
// handled via nestedStepMessages keyed by (toolId, stepId).
|
|
1729
|
+
if ((payload as any).toolContext?.toolId) {
|
|
1730
|
+
continue;
|
|
1731
|
+
}
|
|
1700
1732
|
const incomingPartId = payload.partId;
|
|
1701
1733
|
if (incomingPartId !== undefined && partIdState.current !== null && incomingPartId !== partIdState.current) {
|
|
1702
1734
|
const prev = assistantMessage as AgentWidgetMessage | null;
|
|
@@ -1712,7 +1744,13 @@ export class AgentWidgetClient {
|
|
|
1712
1744
|
partIdState.current = incomingPartId;
|
|
1713
1745
|
}
|
|
1714
1746
|
} else if (payloadType === "text_end") {
|
|
1715
|
-
// Lifecycle event: current text segment ended (tool call about to start)
|
|
1747
|
+
// Lifecycle event: current text segment ended (tool call about to start).
|
|
1748
|
+
// When toolContext is present the boundary belongs to a nested flow — leave
|
|
1749
|
+
// outer assistant state alone so the outer stream is never interrupted by
|
|
1750
|
+
// nested activity.
|
|
1751
|
+
if ((payload as any).toolContext?.toolId) {
|
|
1752
|
+
continue;
|
|
1753
|
+
}
|
|
1716
1754
|
// Seal the current assistant message so the next segment gets a new one
|
|
1717
1755
|
const prev = assistantMessage as AgentWidgetMessage | null;
|
|
1718
1756
|
if (prev) {
|
|
@@ -1731,6 +1769,77 @@ export class AgentWidgetClient {
|
|
|
1731
1769
|
continue;
|
|
1732
1770
|
}
|
|
1733
1771
|
|
|
1772
|
+
// Nested flow routing: when toolContext is present, this step_delta
|
|
1773
|
+
// originated inside a nested flow executed as a tool. Surface it as
|
|
1774
|
+
// its own assistant message keyed by the nested step id, so authors
|
|
1775
|
+
// who add send-stream / prompt steps inside their flow see them as
|
|
1776
|
+
// real messages in the timeline, in order — rather than merging
|
|
1777
|
+
// into the outer assistant bubble or getting buried in the tool
|
|
1778
|
+
// card. Each nested step id gets its own message; the parent tool
|
|
1779
|
+
// bubble continues to represent the invocation via tool_* events.
|
|
1780
|
+
const nestedToolCtx = (payload as any).toolContext as
|
|
1781
|
+
| { toolId?: string; stepId?: string; executionId?: string }
|
|
1782
|
+
| undefined;
|
|
1783
|
+
if (nestedToolCtx?.toolId) {
|
|
1784
|
+
const nestedStepId = String(
|
|
1785
|
+
payload.id ?? nestedToolCtx.stepId ?? `step-${nextSequence()}`
|
|
1786
|
+
);
|
|
1787
|
+
const incomingPartId =
|
|
1788
|
+
payload.partId !== undefined && payload.partId !== null
|
|
1789
|
+
? String(payload.partId)
|
|
1790
|
+
: "";
|
|
1791
|
+
const stepScopeKey = `${nestedToolCtx.toolId}::${nestedStepId}`;
|
|
1792
|
+
const prevPartId = nestedPartIdByStep.get(stepScopeKey);
|
|
1793
|
+
|
|
1794
|
+
// If partId changed within this nested step (prompt with inner
|
|
1795
|
+
// tool call emitting a new text segment), seal the previous
|
|
1796
|
+
// segment's message so each segment renders as its own bubble.
|
|
1797
|
+
if (
|
|
1798
|
+
incomingPartId !== "" &&
|
|
1799
|
+
prevPartId !== undefined &&
|
|
1800
|
+
prevPartId !== "" &&
|
|
1801
|
+
prevPartId !== incomingPartId
|
|
1802
|
+
) {
|
|
1803
|
+
const prev = nestedStepMessages.get(
|
|
1804
|
+
getNestedStepKey(
|
|
1805
|
+
nestedToolCtx.toolId,
|
|
1806
|
+
nestedStepId,
|
|
1807
|
+
prevPartId
|
|
1808
|
+
)
|
|
1809
|
+
);
|
|
1810
|
+
if (prev && prev.streaming !== false) {
|
|
1811
|
+
prev.streaming = false;
|
|
1812
|
+
emitMessage(prev);
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
if (incomingPartId !== "") {
|
|
1816
|
+
nestedPartIdByStep.set(stepScopeKey, incomingPartId);
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1819
|
+
const nestedMsg = ensureNestedStepMessage(
|
|
1820
|
+
nestedToolCtx.toolId,
|
|
1821
|
+
nestedStepId,
|
|
1822
|
+
incomingPartId,
|
|
1823
|
+
nestedToolCtx.executionId
|
|
1824
|
+
);
|
|
1825
|
+
const nestedChunk =
|
|
1826
|
+
payload.text ??
|
|
1827
|
+
payload.delta ??
|
|
1828
|
+
payload.content ??
|
|
1829
|
+
payload.chunk ??
|
|
1830
|
+
"";
|
|
1831
|
+
if (nestedChunk) {
|
|
1832
|
+
nestedMsg.content += String(nestedChunk);
|
|
1833
|
+
nestedMsg.streaming = true;
|
|
1834
|
+
emitMessage(nestedMsg);
|
|
1835
|
+
}
|
|
1836
|
+
if (payload.isComplete) {
|
|
1837
|
+
nestedMsg.streaming = false;
|
|
1838
|
+
emitMessage(nestedMsg);
|
|
1839
|
+
}
|
|
1840
|
+
continue;
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1734
1843
|
// partId-based segmentation: when partId changes, seal current message
|
|
1735
1844
|
// and start a new one so text and tools render in chronological order
|
|
1736
1845
|
const incomingPartId = payload.partId;
|
|
@@ -1748,26 +1857,30 @@ export class AgentWidgetClient {
|
|
|
1748
1857
|
partIdState.current = incomingPartId;
|
|
1749
1858
|
}
|
|
1750
1859
|
|
|
1751
|
-
const assistant =
|
|
1752
|
-
|
|
1753
|
-
|
|
1860
|
+
const assistant =
|
|
1861
|
+
incomingPartId !== undefined
|
|
1862
|
+
? (assistantMessagesByPartId.get(incomingPartId) ?? ensureAssistantMessage())
|
|
1863
|
+
: ensureAssistantMessage();
|
|
1864
|
+
if (incomingPartId !== undefined) {
|
|
1865
|
+
if (!assistant.partId) {
|
|
1866
|
+
assistant.partId = incomingPartId;
|
|
1867
|
+
}
|
|
1868
|
+
assistantMessagesByPartId.set(incomingPartId, assistant);
|
|
1754
1869
|
}
|
|
1755
1870
|
// Support various field names: text, delta, content, chunk (Runtype uses 'chunk')
|
|
1756
1871
|
const chunk = payload.text ?? payload.delta ?? payload.content ?? payload.chunk ?? "";
|
|
1757
1872
|
if (chunk) {
|
|
1758
|
-
// Check if the event carries a sequence number for reordering
|
|
1759
|
-
const chunkSeq = typeof payload.seq === 'number' ? payload.seq : undefined;
|
|
1760
|
-
|
|
1761
1873
|
// Accumulate raw content for structured format parsing.
|
|
1762
|
-
//
|
|
1763
|
-
//
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1874
|
+
// Most out-of-order events are fixed at the dispatch layer, but once the
|
|
1875
|
+
// gap timeout flushes later seqs we can still see genuine late arrivals.
|
|
1876
|
+
// Rebuild chunked content by seq so those events repair prior output
|
|
1877
|
+
// instead of appending in the wrong position.
|
|
1878
|
+
const chunkSeq = typeof payload.seq === "number" ? payload.seq : undefined;
|
|
1879
|
+
const chunkBufferKey = incomingPartId ?? assistant.id;
|
|
1880
|
+
const accumulatedRaw =
|
|
1881
|
+
chunkSeq !== undefined
|
|
1882
|
+
? insertOrderedChunk(chunkBufferKey, chunkSeq, String(chunk))
|
|
1883
|
+
: (rawContentBuffers.get(assistant.id) ?? "") + chunk;
|
|
1771
1884
|
// Store raw content for action parsing, but NEVER set assistant.content to raw JSON
|
|
1772
1885
|
assistant.rawContent = accumulatedRaw;
|
|
1773
1886
|
|
|
@@ -1790,13 +1903,7 @@ export class AgentWidgetClient {
|
|
|
1790
1903
|
|
|
1791
1904
|
// If plain text parser, just append the chunk directly
|
|
1792
1905
|
if (isPlainTextParser) {
|
|
1793
|
-
|
|
1794
|
-
// otherwise fall back to simple append.
|
|
1795
|
-
if (chunkSeq !== undefined) {
|
|
1796
|
-
assistant.content = accumulatedRaw;
|
|
1797
|
-
} else {
|
|
1798
|
-
assistant.content += chunk;
|
|
1799
|
-
}
|
|
1906
|
+
assistant.content = chunkSeq !== undefined ? accumulatedRaw : assistant.content + chunk;
|
|
1800
1907
|
// Clear any raw buffer/parser since we're in plain text mode
|
|
1801
1908
|
rawContentBuffers.delete(assistant.id);
|
|
1802
1909
|
streamParsers.delete(assistant.id);
|
|
@@ -1822,27 +1929,25 @@ export class AgentWidgetClient {
|
|
|
1822
1929
|
} else if (!looksLikeJson && !accumulatedRaw.trim().startsWith('<')) {
|
|
1823
1930
|
// Not a structured format - show as plain text
|
|
1824
1931
|
const currentAssistant = assistantMessage;
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
currentAssistant
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1932
|
+
const targetAssistant =
|
|
1933
|
+
currentAssistant && currentAssistant.id === assistant.id
|
|
1934
|
+
? currentAssistant
|
|
1935
|
+
: assistant;
|
|
1936
|
+
if (targetAssistant.id === assistant.id) {
|
|
1937
|
+
targetAssistant.content =
|
|
1938
|
+
chunkSeq !== undefined ? accumulatedRaw : targetAssistant.content + chunk;
|
|
1939
|
+
rawContentBuffers.delete(targetAssistant.id);
|
|
1940
|
+
streamParsers.delete(targetAssistant.id);
|
|
1941
|
+
targetAssistant.rawContent = undefined;
|
|
1942
|
+
emitMessage(targetAssistant);
|
|
1835
1943
|
}
|
|
1836
1944
|
}
|
|
1837
1945
|
// Otherwise wait for more chunks (incomplete structured format)
|
|
1838
1946
|
// Don't emit message if parser hasn't extracted text yet
|
|
1839
1947
|
}).catch(() => {
|
|
1840
1948
|
// On error, treat as plain text
|
|
1841
|
-
|
|
1842
|
-
assistant.content
|
|
1843
|
-
} else {
|
|
1844
|
-
assistant.content += chunk;
|
|
1845
|
-
}
|
|
1949
|
+
assistant.content =
|
|
1950
|
+
chunkSeq !== undefined ? accumulatedRaw : assistant.content + chunk;
|
|
1846
1951
|
rawContentBuffers.delete(assistant.id);
|
|
1847
1952
|
streamParsers.delete(assistant.id);
|
|
1848
1953
|
assistant.rawContent = undefined;
|
|
@@ -1860,11 +1965,8 @@ export class AgentWidgetClient {
|
|
|
1860
1965
|
emitMessage(assistant);
|
|
1861
1966
|
} else if (!looksLikeJson && !accumulatedRaw.trim().startsWith('<')) {
|
|
1862
1967
|
// Not a structured format - show as plain text
|
|
1863
|
-
|
|
1864
|
-
assistant.content
|
|
1865
|
-
} else {
|
|
1866
|
-
assistant.content += chunk;
|
|
1867
|
-
}
|
|
1968
|
+
assistant.content =
|
|
1969
|
+
chunkSeq !== undefined ? accumulatedRaw : assistant.content + chunk;
|
|
1868
1970
|
// Clear any raw buffer/parser if we were in structured format mode
|
|
1869
1971
|
rawContentBuffers.delete(assistant.id);
|
|
1870
1972
|
streamParsers.delete(assistant.id);
|
|
@@ -1917,7 +2019,6 @@ export class AgentWidgetClient {
|
|
|
1917
2019
|
// Clean up
|
|
1918
2020
|
streamParsers.delete(currentAssistant.id);
|
|
1919
2021
|
rawContentBuffers.delete(currentAssistant.id);
|
|
1920
|
-
seqChunkBuffers.delete(currentAssistant.id);
|
|
1921
2022
|
emitMessage(currentAssistant);
|
|
1922
2023
|
}
|
|
1923
2024
|
}
|
|
@@ -1949,7 +2050,6 @@ export class AgentWidgetClient {
|
|
|
1949
2050
|
streamParsers.delete(assistant.id);
|
|
1950
2051
|
}
|
|
1951
2052
|
rawContentBuffers.delete(assistant.id);
|
|
1952
|
-
seqChunkBuffers.delete(assistant.id);
|
|
1953
2053
|
assistant.streaming = false;
|
|
1954
2054
|
emitMessage(assistant);
|
|
1955
2055
|
}
|
|
@@ -1963,6 +2063,37 @@ export class AgentWidgetClient {
|
|
|
1963
2063
|
// Skip tool-related completions - they're handled by tool_complete
|
|
1964
2064
|
continue;
|
|
1965
2065
|
}
|
|
2066
|
+
|
|
2067
|
+
// Nested flow: seal every segment message produced by this nested
|
|
2068
|
+
// step (a single nested prompt step may have produced multiple
|
|
2069
|
+
// messages, one per partId, when inner tool calls split it). The
|
|
2070
|
+
// outer assistantMessage state is untouched so reconciliation for
|
|
2071
|
+
// the outer flow still works.
|
|
2072
|
+
const nestedCompleteCtx = (payload as any).toolContext as
|
|
2073
|
+
| { toolId?: string; stepId?: string; executionId?: string }
|
|
2074
|
+
| undefined;
|
|
2075
|
+
if (nestedCompleteCtx?.toolId) {
|
|
2076
|
+
const nestedStepId = String(
|
|
2077
|
+
payload.id ?? nestedCompleteCtx.stepId ?? ""
|
|
2078
|
+
);
|
|
2079
|
+
if (nestedStepId) {
|
|
2080
|
+
const prefix = getNestedStepPrefix(
|
|
2081
|
+
nestedCompleteCtx.toolId,
|
|
2082
|
+
nestedStepId
|
|
2083
|
+
);
|
|
2084
|
+
for (const [key, msg] of nestedStepMessages) {
|
|
2085
|
+
if (key.startsWith(prefix) && msg.streaming !== false) {
|
|
2086
|
+
msg.streaming = false;
|
|
2087
|
+
emitMessage(msg);
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
nestedPartIdByStep.delete(
|
|
2091
|
+
`${nestedCompleteCtx.toolId}::${nestedStepId}`
|
|
2092
|
+
);
|
|
2093
|
+
}
|
|
2094
|
+
continue;
|
|
2095
|
+
}
|
|
2096
|
+
|
|
1966
2097
|
if (didSplitByPartId) {
|
|
1967
2098
|
// Sealed segment(s) — do not create a second bubble from step_complete.
|
|
1968
2099
|
// Merge authoritative final response into the last sealed segment (fixes async lag).
|
|
@@ -1970,7 +2101,6 @@ export class AgentWidgetClient {
|
|
|
1970
2101
|
const msg: AgentWidgetMessage = assistantMessage;
|
|
1971
2102
|
streamParsers.delete(msg.id);
|
|
1972
2103
|
rawContentBuffers.delete(msg.id);
|
|
1973
|
-
seqChunkBuffers.delete(msg.id);
|
|
1974
2104
|
if (msg.streaming !== false) {
|
|
1975
2105
|
msg.streaming = false;
|
|
1976
2106
|
emitMessage(msg);
|
|
@@ -2035,7 +2165,6 @@ export class AgentWidgetClient {
|
|
|
2035
2165
|
// Clean up
|
|
2036
2166
|
streamParsers.delete(currentAssistant.id);
|
|
2037
2167
|
rawContentBuffers.delete(currentAssistant.id);
|
|
2038
|
-
seqChunkBuffers.delete(currentAssistant.id);
|
|
2039
2168
|
emitMessage(currentAssistant);
|
|
2040
2169
|
}
|
|
2041
2170
|
} else {
|
|
@@ -2053,7 +2182,6 @@ export class AgentWidgetClient {
|
|
|
2053
2182
|
// Clean up
|
|
2054
2183
|
streamParsers.delete(currentAssistant.id);
|
|
2055
2184
|
rawContentBuffers.delete(currentAssistant.id);
|
|
2056
|
-
seqChunkBuffers.delete(currentAssistant.id);
|
|
2057
2185
|
emitMessage(currentAssistant);
|
|
2058
2186
|
}
|
|
2059
2187
|
}
|
|
@@ -2101,7 +2229,6 @@ export class AgentWidgetClient {
|
|
|
2101
2229
|
}
|
|
2102
2230
|
streamParsers.delete(assistant.id);
|
|
2103
2231
|
rawContentBuffers.delete(assistant.id);
|
|
2104
|
-
seqChunkBuffers.delete(assistant.id);
|
|
2105
2232
|
assistant.streaming = false;
|
|
2106
2233
|
emitMessage(assistant);
|
|
2107
2234
|
}
|
|
@@ -2109,7 +2236,6 @@ export class AgentWidgetClient {
|
|
|
2109
2236
|
// No final content, just mark as complete and clean up
|
|
2110
2237
|
streamParsers.delete(assistant.id);
|
|
2111
2238
|
rawContentBuffers.delete(assistant.id);
|
|
2112
|
-
seqChunkBuffers.delete(assistant.id);
|
|
2113
2239
|
assistant.streaming = false;
|
|
2114
2240
|
emitMessage(assistant);
|
|
2115
2241
|
}
|
|
@@ -2122,7 +2248,6 @@ export class AgentWidgetClient {
|
|
|
2122
2248
|
const msg: AgentWidgetMessage = assistantMessage;
|
|
2123
2249
|
streamParsers.delete(msg.id);
|
|
2124
2250
|
rawContentBuffers.delete(msg.id);
|
|
2125
|
-
seqChunkBuffers.delete(msg.id);
|
|
2126
2251
|
if (msg.streaming !== false) {
|
|
2127
2252
|
msg.streaming = false;
|
|
2128
2253
|
emitMessage(msg);
|
|
@@ -2164,7 +2289,6 @@ export class AgentWidgetClient {
|
|
|
2164
2289
|
// Clean up parser and buffer
|
|
2165
2290
|
streamParsers.delete(assistant.id);
|
|
2166
2291
|
rawContentBuffers.delete(assistant.id);
|
|
2167
|
-
seqChunkBuffers.delete(assistant.id);
|
|
2168
2292
|
|
|
2169
2293
|
// Only emit if something actually changed to avoid flicker
|
|
2170
2294
|
const contentChanged = displayContent !== assistant.content;
|
|
@@ -2187,8 +2311,6 @@ export class AgentWidgetClient {
|
|
|
2187
2311
|
const msg: AgentWidgetMessage = assistantMessage;
|
|
2188
2312
|
streamParsers.delete(msg.id);
|
|
2189
2313
|
rawContentBuffers.delete(msg.id);
|
|
2190
|
-
seqChunkBuffers.delete(msg.id);
|
|
2191
|
-
|
|
2192
2314
|
// Only emit if streaming state changed
|
|
2193
2315
|
if (msg.streaming !== false) {
|
|
2194
2316
|
msg.streaming = false;
|
|
@@ -2591,7 +2713,6 @@ export class AgentWidgetClient {
|
|
|
2591
2713
|
assistantMessageRef.current = null;
|
|
2592
2714
|
streamParsers.delete(id);
|
|
2593
2715
|
rawContentBuffers.delete(id);
|
|
2594
|
-
seqChunkBuffers.delete(id);
|
|
2595
2716
|
} else if (
|
|
2596
2717
|
payloadType === "error" ||
|
|
2597
2718
|
payloadType === "step_error" ||
|
|
@@ -2631,6 +2752,79 @@ export class AgentWidgetClient {
|
|
|
2631
2752
|
}
|
|
2632
2753
|
}
|
|
2633
2754
|
}
|
|
2755
|
+
seqReadyQueue.length = 0;
|
|
2756
|
+
};
|
|
2757
|
+
|
|
2758
|
+
// eslint-disable-next-line no-constant-condition
|
|
2759
|
+
while (true) {
|
|
2760
|
+
const { done, value } = await reader.read();
|
|
2761
|
+
if (done) break;
|
|
2762
|
+
|
|
2763
|
+
buffer += decoder.decode(value, { stream: true });
|
|
2764
|
+
const events = buffer.split("\n\n");
|
|
2765
|
+
buffer = events.pop() ?? "";
|
|
2766
|
+
|
|
2767
|
+
for (const event of events) {
|
|
2768
|
+
const lines = event.split("\n");
|
|
2769
|
+
let eventType = "message";
|
|
2770
|
+
let data = "";
|
|
2771
|
+
|
|
2772
|
+
for (const line of lines) {
|
|
2773
|
+
if (line.startsWith("event:")) {
|
|
2774
|
+
eventType = line.replace("event:", "").trim();
|
|
2775
|
+
} else if (line.startsWith("data:")) {
|
|
2776
|
+
data += line.replace("data:", "").trim();
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2779
|
+
|
|
2780
|
+
if (!data) continue;
|
|
2781
|
+
let payload: any;
|
|
2782
|
+
try {
|
|
2783
|
+
payload = JSON.parse(data);
|
|
2784
|
+
} catch (error) {
|
|
2785
|
+
onEvent({
|
|
2786
|
+
type: "error",
|
|
2787
|
+
error:
|
|
2788
|
+
error instanceof Error
|
|
2789
|
+
? error
|
|
2790
|
+
: new Error("Failed to parse chat stream payload")
|
|
2791
|
+
});
|
|
2792
|
+
continue;
|
|
2793
|
+
}
|
|
2794
|
+
|
|
2795
|
+
const payloadType =
|
|
2796
|
+
eventType !== "message" ? eventType : payload.type ?? "message";
|
|
2797
|
+
|
|
2798
|
+
// Tap: capture raw SSE event for event stream inspector
|
|
2799
|
+
this.onSSEEvent?.(payloadType, payload);
|
|
2800
|
+
|
|
2801
|
+
// If custom SSE event parser is provided, try it first
|
|
2802
|
+
if (this.parseSSEEvent) {
|
|
2803
|
+
// Keep assistant message ref in sync
|
|
2804
|
+
assistantMessageRef.current = assistantMessage;
|
|
2805
|
+
const handled = await this.handleCustomSSEEvent(
|
|
2806
|
+
payload,
|
|
2807
|
+
onEvent,
|
|
2808
|
+
assistantMessageRef,
|
|
2809
|
+
emitMessage,
|
|
2810
|
+
nextSequence,
|
|
2811
|
+
partIdState
|
|
2812
|
+
);
|
|
2813
|
+
// Update assistantMessage from ref (in case it was created or replaced by partId segmentation)
|
|
2814
|
+
if (assistantMessageRef.current && assistantMessageRef.current !== assistantMessage) {
|
|
2815
|
+
assistantMessage = assistantMessageRef.current;
|
|
2816
|
+
}
|
|
2817
|
+
if (handled) continue; // Skip default handling if custom handler processed it
|
|
2818
|
+
}
|
|
2819
|
+
|
|
2820
|
+
// Push through the sequence reorder buffer
|
|
2821
|
+
seqBuffer.push(payloadType, payload);
|
|
2822
|
+
drainReadyQueue();
|
|
2823
|
+
}
|
|
2634
2824
|
}
|
|
2825
|
+
|
|
2826
|
+
seqBuffer.flushPending();
|
|
2827
|
+
drainReadyQueue();
|
|
2828
|
+
seqBuffer.destroy();
|
|
2635
2829
|
}
|
|
2636
2830
|
}
|