@runtypelabs/persona 3.37.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.
- package/README.md +0 -1
- package/dist/animations/glyph-cycle.d.cts +1 -1
- package/dist/animations/glyph-cycle.d.ts +1 -1
- package/dist/animations/{types-DgYsuwXL.d.cts → types-B_xbfvR0.d.cts} +263 -181
- package/dist/animations/{types-DgYsuwXL.d.ts → types-B_xbfvR0.d.ts} +263 -181
- package/dist/animations/wipe.d.cts +1 -1
- package/dist/animations/wipe.d.ts +1 -1
- package/dist/codegen.cjs +1 -1
- package/dist/codegen.js +1 -1
- package/dist/index.cjs +52 -52
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +327 -838
- package/dist/index.d.ts +327 -838
- package/dist/index.global.js +39 -39
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +52 -52
- package/dist/index.js.map +1 -1
- package/dist/install.global.js +1 -1
- package/dist/install.global.js.map +1 -1
- package/dist/smart-dom-reader.d.cts +268 -191
- package/dist/smart-dom-reader.d.ts +268 -191
- package/dist/theme-editor-preview.cjs +53 -53
- package/dist/theme-editor-preview.d.cts +268 -191
- package/dist/theme-editor-preview.d.ts +268 -191
- package/dist/theme-editor-preview.js +55 -55
- package/dist/theme-editor.d.cts +268 -191
- package/dist/theme-editor.d.ts +268 -191
- package/package.json +1 -1
- package/src/client.test.ts +446 -1041
- package/src/client.ts +684 -967
- package/src/components/tool-bubble.ts +46 -33
- package/src/generated/runtype-openapi-contract.ts +210 -714
- package/src/index-core.ts +2 -3
- package/src/install.test.ts +1 -34
- package/src/install.ts +1 -26
- package/src/runtime/init.test.ts +8 -67
- package/src/runtime/init.ts +2 -17
- package/src/session.test.ts +8 -8
- package/src/session.ts +1 -1
- package/src/types.ts +11 -12
- package/src/ui.postprocess.test.ts +107 -0
- package/src/ui.ts +40 -5
- package/src/utils/__fixtures__/unified-translator.oracle.ts +62 -14
- package/src/utils/copy-selection.test.ts +37 -0
- package/src/utils/copy-selection.ts +19 -0
- package/src/utils/event-stream-capture.test.ts +9 -64
- package/src/utils/sequence-buffer.test.ts +0 -256
- package/src/utils/sequence-buffer.ts +0 -130
- package/src/utils/unified-event-bridge.test.ts +0 -263
- package/src/utils/unified-event-bridge.ts +0 -431
package/src/client.ts
CHANGED
|
@@ -32,8 +32,6 @@ import {
|
|
|
32
32
|
createRegexJsonParser,
|
|
33
33
|
createXmlParser
|
|
34
34
|
} from "./utils/formatting";
|
|
35
|
-
import { SequenceReorderBuffer } from "./utils/sequence-buffer";
|
|
36
|
-
import { UnifiedToLegacyBridge, isUnifiedLifecycleStart } from "./utils/unified-event-bridge";
|
|
37
35
|
import { VERSION } from "./version";
|
|
38
36
|
// artifactsSidebarEnabled is used in ui.ts to gate the sidebar pane rendering;
|
|
39
37
|
// artifact events are always processed here regardless of config.
|
|
@@ -293,17 +291,6 @@ export class AgentWidgetClient {
|
|
|
293
291
|
return !!this.config.agent;
|
|
294
292
|
}
|
|
295
293
|
|
|
296
|
-
/**
|
|
297
|
-
* Append `?events=unified` when the widget opted into the unified vocabulary,
|
|
298
|
-
* preserving any existing query string. No-op for the default legacy mode.
|
|
299
|
-
* The server only switches vocabularies when it sees the param; the widget
|
|
300
|
-
* still auto-detects the actual wire mode from the first stream frame.
|
|
301
|
-
*/
|
|
302
|
-
private withEventsParam(url: string): string {
|
|
303
|
-
if (this.config.events !== "unified") return url;
|
|
304
|
-
return url + (url.includes("?") ? "&" : "?") + "events=unified";
|
|
305
|
-
}
|
|
306
|
-
|
|
307
294
|
/**
|
|
308
295
|
* Get the appropriate API URL based on mode
|
|
309
296
|
*/
|
|
@@ -694,7 +681,7 @@ export class AgentWidgetClient {
|
|
|
694
681
|
console.debug("[AgentWidgetClient] client token dispatch", chatRequest);
|
|
695
682
|
}
|
|
696
683
|
|
|
697
|
-
response = await fetch(this.
|
|
684
|
+
response = await fetch(this.getClientApiUrl('chat'), {
|
|
698
685
|
method: 'POST',
|
|
699
686
|
headers: {
|
|
700
687
|
'Content-Type': 'application/json',
|
|
@@ -814,7 +801,7 @@ export class AgentWidgetClient {
|
|
|
814
801
|
if (this.customFetch) {
|
|
815
802
|
try {
|
|
816
803
|
response = await this.customFetch(
|
|
817
|
-
this.
|
|
804
|
+
this.apiUrl,
|
|
818
805
|
{
|
|
819
806
|
method: "POST",
|
|
820
807
|
headers,
|
|
@@ -829,7 +816,7 @@ export class AgentWidgetClient {
|
|
|
829
816
|
throw err;
|
|
830
817
|
}
|
|
831
818
|
} else {
|
|
832
|
-
response = await fetch(this.
|
|
819
|
+
response = await fetch(this.apiUrl, {
|
|
833
820
|
method: "POST",
|
|
834
821
|
headers,
|
|
835
822
|
body: JSON.stringify(payload),
|
|
@@ -890,7 +877,7 @@ export class AgentWidgetClient {
|
|
|
890
877
|
if (this.customFetch) {
|
|
891
878
|
try {
|
|
892
879
|
response = await this.customFetch(
|
|
893
|
-
this.
|
|
880
|
+
this.apiUrl,
|
|
894
881
|
{
|
|
895
882
|
method: "POST",
|
|
896
883
|
headers,
|
|
@@ -905,7 +892,7 @@ export class AgentWidgetClient {
|
|
|
905
892
|
throw err;
|
|
906
893
|
}
|
|
907
894
|
} else {
|
|
908
|
-
response = await fetch(this.
|
|
895
|
+
response = await fetch(this.apiUrl, {
|
|
909
896
|
method: "POST",
|
|
910
897
|
headers,
|
|
911
898
|
body: JSON.stringify(payload),
|
|
@@ -1008,11 +995,9 @@ export class AgentWidgetClient {
|
|
|
1008
995
|
options?: { streamResponse?: boolean; signal?: AbortSignal }
|
|
1009
996
|
): Promise<Response> {
|
|
1010
997
|
const isClientToken = this.isClientTokenMode();
|
|
1011
|
-
const url =
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
: `${this.config.apiUrl?.replace(/\/+$/, '') || DEFAULT_CLIENT_API_BASE}/resume`
|
|
1015
|
-
);
|
|
998
|
+
const url = isClientToken
|
|
999
|
+
? this.getClientApiUrl('resume')
|
|
1000
|
+
: `${this.config.apiUrl?.replace(/\/+$/, '') || DEFAULT_CLIENT_API_BASE}/resume`;
|
|
1016
1001
|
|
|
1017
1002
|
// The client-token resume route authenticates the session, not a Bearer
|
|
1018
1003
|
// key. A WebMCP approval can sit awaiting user input for a long time, so by
|
|
@@ -1392,21 +1377,26 @@ export class AgentWidgetClient {
|
|
|
1392
1377
|
let lastAssistantInTurn: AgentWidgetMessage | null = null;
|
|
1393
1378
|
// Reference to track assistant message for custom event handler
|
|
1394
1379
|
const assistantMessageRef = { current: null as AgentWidgetMessage | null };
|
|
1395
|
-
//
|
|
1396
|
-
|
|
1397
|
-
|
|
1380
|
+
// Segmentation state for the `parseSSEEvent` extensibility callback (the
|
|
1381
|
+
// consumer's own `partId` field) — independent of the unified wire.
|
|
1382
|
+
const customParsePartId = { current: null as string | null };
|
|
1383
|
+
// Unified text-channel block id (from `text_start`/`text_delta` `id`). Drives
|
|
1384
|
+
// bubble-id segmentation on the unified wire in place of the legacy `partId`:
|
|
1385
|
+
// a new block id means a new bubble, sealed at `text_complete`/tool boundaries.
|
|
1386
|
+
let currentTextBlockId: string | null = null;
|
|
1387
|
+
// Raw text accumulated for the open flow block before its bubble is
|
|
1388
|
+
// materialized — lets a whitespace-only block resolve without a stray bubble.
|
|
1389
|
+
let pendingFlowRaw = "";
|
|
1390
|
+
// Nested flow-as-tool attribution (PR #4602): a text/reasoning block whose
|
|
1391
|
+
// `parentToolCallId` matches a `tool_start.toolCallId` belongs to a flow
|
|
1392
|
+
// running as that tool. Keyed by the unified block id, these route the block's
|
|
1393
|
+
// deltas into a message tagged `agentMetadata.parentToolId` (the parent tool's
|
|
1394
|
+
// row) instead of the top-level assistant/reasoning channel.
|
|
1395
|
+
const nestedBlockParent = new Map<string, string>();
|
|
1396
|
+
const nestedBlockMessages = new Map<string, AgentWidgetMessage>();
|
|
1397
|
+
const nestedBlockRaw = new Map<string, string>();
|
|
1398
1398
|
const reasoningMessages = new Map<string, AgentWidgetMessage>();
|
|
1399
1399
|
const toolMessages = new Map<string, AgentWidgetMessage>();
|
|
1400
|
-
// Messages produced by steps inside a nested flow executed as a tool.
|
|
1401
|
-
// Keyed by `${parentToolId}::${nestedStepId}::${partId}` so each nested
|
|
1402
|
-
// step (send-stream, prompt) gets its own assistant message, and prompts
|
|
1403
|
-
// with inner tool calls split into one message per text segment: still
|
|
1404
|
-
// attributable to the parent tool call.
|
|
1405
|
-
const nestedStepMessages = new Map<string, AgentWidgetMessage>();
|
|
1406
|
-
// Most-recent partId seen for a given `${toolId}::${stepId}` scope, used
|
|
1407
|
-
// to seal the previous segment when a new partId arrives within the
|
|
1408
|
-
// same nested prompt step.
|
|
1409
|
-
const nestedPartIdByStep = new Map<string, string>();
|
|
1410
1400
|
const reasoningContext = {
|
|
1411
1401
|
lastId: null as string | null,
|
|
1412
1402
|
byStep: new Map<string, string>()
|
|
@@ -1416,49 +1406,6 @@ export class AgentWidgetClient {
|
|
|
1416
1406
|
byCall: new Map<string, string>()
|
|
1417
1407
|
};
|
|
1418
1408
|
|
|
1419
|
-
// Nested message key. partId defaults to "" so steps without segmentation
|
|
1420
|
-
// (e.g. send-stream) still have a deterministic single key.
|
|
1421
|
-
const getNestedStepKey = (
|
|
1422
|
-
toolId: string,
|
|
1423
|
-
stepId: string,
|
|
1424
|
-
partId: string = ""
|
|
1425
|
-
) => `${toolId}::${stepId}::${partId}`;
|
|
1426
|
-
|
|
1427
|
-
// Prefix used to sweep every nested message belonging to a single
|
|
1428
|
-
// (toolId, stepId) scope: needed on step_complete to seal any segments
|
|
1429
|
-
// that are still streaming.
|
|
1430
|
-
const getNestedStepPrefix = (toolId: string, stepId: string) =>
|
|
1431
|
-
`${toolId}::${stepId}::`;
|
|
1432
|
-
|
|
1433
|
-
const ensureNestedStepMessage = (
|
|
1434
|
-
toolId: string,
|
|
1435
|
-
stepId: string,
|
|
1436
|
-
partId: string,
|
|
1437
|
-
executionId?: string
|
|
1438
|
-
): AgentWidgetMessage => {
|
|
1439
|
-
const key = getNestedStepKey(toolId, stepId, partId);
|
|
1440
|
-
const existing = nestedStepMessages.get(key);
|
|
1441
|
-
if (existing) return existing;
|
|
1442
|
-
const idSuffix = partId ? `-${partId}` : "";
|
|
1443
|
-
const message: AgentWidgetMessage = {
|
|
1444
|
-
id: `nested-${toolId}-${stepId}${idSuffix}`,
|
|
1445
|
-
role: "assistant",
|
|
1446
|
-
content: "",
|
|
1447
|
-
createdAt: new Date().toISOString(),
|
|
1448
|
-
streaming: true,
|
|
1449
|
-
sequence: nextSequence(),
|
|
1450
|
-
...(partId ? { partId } : {}),
|
|
1451
|
-
agentMetadata: {
|
|
1452
|
-
executionId,
|
|
1453
|
-
parentToolId: toolId,
|
|
1454
|
-
parentStepId: stepId,
|
|
1455
|
-
},
|
|
1456
|
-
};
|
|
1457
|
-
nestedStepMessages.set(key, message);
|
|
1458
|
-
emitMessage(message);
|
|
1459
|
-
return message;
|
|
1460
|
-
};
|
|
1461
|
-
|
|
1462
1409
|
const normalizeKey = (value: unknown): string | null => {
|
|
1463
1410
|
if (value === null || value === undefined) return null;
|
|
1464
1411
|
try {
|
|
@@ -1496,11 +1443,12 @@ export class AgentWidgetClient {
|
|
|
1496
1443
|
const ensureAssistantMessage = () => {
|
|
1497
1444
|
if (assistantMessage) return assistantMessage;
|
|
1498
1445
|
let id: string;
|
|
1446
|
+
const segment = currentTextBlockId;
|
|
1499
1447
|
if (!assistantIdConsumed && baseAssistantId) {
|
|
1500
1448
|
id = baseAssistantId;
|
|
1501
1449
|
assistantIdConsumed = true;
|
|
1502
|
-
} else if (baseAssistantId &&
|
|
1503
|
-
id = `${baseAssistantId}_${
|
|
1450
|
+
} else if (baseAssistantId && segment) {
|
|
1451
|
+
id = `${baseAssistantId}_${segment}`;
|
|
1504
1452
|
} else {
|
|
1505
1453
|
id = `assistant-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
1506
1454
|
}
|
|
@@ -1692,11 +1640,6 @@ export class AgentWidgetClient {
|
|
|
1692
1640
|
// Rebuild incremental text by sequence so late arrivals can repair already-emitted
|
|
1693
1641
|
// content after the reorder buffer's gap-timeout flush.
|
|
1694
1642
|
const orderedChunkBuffers = new Map<string, Array<{ seq: number; text: string }>>();
|
|
1695
|
-
const assistantMessagesByPartId = new Map<string, AgentWidgetMessage>();
|
|
1696
|
-
// Only the most-recently sealed segment is reconciled with step_complete's
|
|
1697
|
-
// final response. Earlier segments rely on their own async parser microtasks
|
|
1698
|
-
// resolving via the closure-captured `assistant` variable.
|
|
1699
|
-
let lastSealedTextSegment: AgentWidgetMessage | null = null;
|
|
1700
1643
|
|
|
1701
1644
|
const insertOrderedChunk = (key: string, seq: number, text: string): string => {
|
|
1702
1645
|
let chunks = orderedChunkBuffers.get(key);
|
|
@@ -1821,58 +1764,241 @@ export class AgentWidgetClient {
|
|
|
1821
1764
|
finalizeCleanup();
|
|
1822
1765
|
};
|
|
1823
1766
|
|
|
1824
|
-
//
|
|
1825
|
-
//
|
|
1826
|
-
//
|
|
1767
|
+
// === Unified flow text channel ===
|
|
1768
|
+
// Flow prompt-step text streams as `text_delta` blocks (segmented by
|
|
1769
|
+
// `text_start`/`text_complete`) and can be structured JSON, so each block
|
|
1770
|
+
// runs through the per-bubble structured-content parser — agent text stays
|
|
1771
|
+
// plain. This is the legacy step_delta parser core, re-keyed from `partId`
|
|
1772
|
+
// to the unified block-id bubble. The caller materializes the bubble lazily
|
|
1773
|
+
// (whitespace-only blocks around tool boundaries never leave a stray bubble)
|
|
1774
|
+
// and `step_complete.result.response` reconciles the authoritative final.
|
|
1775
|
+
let lastSealedFlowBubble: AgentWidgetMessage | null = null;
|
|
1776
|
+
|
|
1777
|
+
// Stream one accumulated chunk of flow block text through the parser, setting
|
|
1778
|
+
// display `content` (extracted) + `rawContent` (raw) and emitting. Mirrors the
|
|
1779
|
+
// legacy step_delta chunk path; plain text bypasses the structured parser.
|
|
1780
|
+
const applyFlowTextChunk = (
|
|
1781
|
+
assistant: AgentWidgetMessage,
|
|
1782
|
+
accumulatedRaw: string,
|
|
1783
|
+
chunk: string,
|
|
1784
|
+
chunkSeq: number | undefined
|
|
1785
|
+
) => {
|
|
1786
|
+
assistant.rawContent = accumulatedRaw;
|
|
1787
|
+
if (!streamParsers.has(assistant.id)) {
|
|
1788
|
+
streamParsers.set(assistant.id, this.createStreamParser());
|
|
1789
|
+
}
|
|
1790
|
+
const parser = streamParsers.get(assistant.id)!;
|
|
1791
|
+
const looksLikeJson =
|
|
1792
|
+
accumulatedRaw.trim().startsWith("{") || accumulatedRaw.trim().startsWith("[");
|
|
1793
|
+
if (looksLikeJson) {
|
|
1794
|
+
rawContentBuffers.set(assistant.id, accumulatedRaw);
|
|
1795
|
+
}
|
|
1796
|
+
const isPlainTextParser = (parser as any).__isPlainTextParser === true;
|
|
1797
|
+
if (isPlainTextParser) {
|
|
1798
|
+
assistant.content =
|
|
1799
|
+
chunkSeq !== undefined ? accumulatedRaw : assistant.content + chunk;
|
|
1800
|
+
rawContentBuffers.delete(assistant.id);
|
|
1801
|
+
streamParsers.delete(assistant.id);
|
|
1802
|
+
assistant.rawContent = undefined;
|
|
1803
|
+
emitMessage(assistant);
|
|
1804
|
+
return;
|
|
1805
|
+
}
|
|
1806
|
+
const parsedResult = parser.processChunk(accumulatedRaw);
|
|
1807
|
+
if (parsedResult instanceof Promise) {
|
|
1808
|
+
parsedResult
|
|
1809
|
+
.then((result) => {
|
|
1810
|
+
const text = typeof result === "string" ? result : result?.text ?? null;
|
|
1811
|
+
if (text !== null && text.trim() !== "") {
|
|
1812
|
+
assistant.content = text;
|
|
1813
|
+
emitMessage(assistant);
|
|
1814
|
+
} else if (!looksLikeJson && !accumulatedRaw.trim().startsWith("<")) {
|
|
1815
|
+
assistant.content =
|
|
1816
|
+
chunkSeq !== undefined ? accumulatedRaw : assistant.content + chunk;
|
|
1817
|
+
rawContentBuffers.delete(assistant.id);
|
|
1818
|
+
streamParsers.delete(assistant.id);
|
|
1819
|
+
assistant.rawContent = undefined;
|
|
1820
|
+
emitMessage(assistant);
|
|
1821
|
+
}
|
|
1822
|
+
})
|
|
1823
|
+
.catch(() => {
|
|
1824
|
+
assistant.content =
|
|
1825
|
+
chunkSeq !== undefined ? accumulatedRaw : assistant.content + chunk;
|
|
1826
|
+
rawContentBuffers.delete(assistant.id);
|
|
1827
|
+
streamParsers.delete(assistant.id);
|
|
1828
|
+
assistant.rawContent = undefined;
|
|
1829
|
+
emitMessage(assistant);
|
|
1830
|
+
});
|
|
1831
|
+
} else {
|
|
1832
|
+
const text =
|
|
1833
|
+
typeof parsedResult === "string" ? parsedResult : parsedResult?.text ?? null;
|
|
1834
|
+
if (text !== null && text.trim() !== "") {
|
|
1835
|
+
assistant.content = text;
|
|
1836
|
+
emitMessage(assistant);
|
|
1837
|
+
} else if (!looksLikeJson && !accumulatedRaw.trim().startsWith("<")) {
|
|
1838
|
+
assistant.content =
|
|
1839
|
+
chunkSeq !== undefined ? accumulatedRaw : assistant.content + chunk;
|
|
1840
|
+
rawContentBuffers.delete(assistant.id);
|
|
1841
|
+
streamParsers.delete(assistant.id);
|
|
1842
|
+
assistant.rawContent = undefined;
|
|
1843
|
+
emitMessage(assistant);
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
};
|
|
1847
|
+
|
|
1848
|
+
// Seal a flow text block at `text_complete`: run final structured extraction
|
|
1849
|
+
// off the accumulated raw buffer (U2: `text_complete.text` mirrors that raw
|
|
1850
|
+
// buffer, so we never double-count), then finalize the bubble. The structured
|
|
1851
|
+
// `step_complete.result.response` reconciles afterward.
|
|
1852
|
+
const finalizeFlowTextBlock = (
|
|
1853
|
+
assistant: AgentWidgetMessage,
|
|
1854
|
+
finalContent?: unknown
|
|
1855
|
+
) => {
|
|
1856
|
+
const effectiveFinal =
|
|
1857
|
+
finalContent !== undefined && finalContent !== null
|
|
1858
|
+
? finalContent
|
|
1859
|
+
: assistant.content;
|
|
1860
|
+
if (
|
|
1861
|
+
effectiveFinal === undefined ||
|
|
1862
|
+
effectiveFinal === null ||
|
|
1863
|
+
effectiveFinal === ""
|
|
1864
|
+
) {
|
|
1865
|
+
assistant.streaming = false;
|
|
1866
|
+
emitMessage(assistant);
|
|
1867
|
+
return;
|
|
1868
|
+
}
|
|
1869
|
+
const rawBuffer = rawContentBuffers.get(assistant.id);
|
|
1870
|
+
const contentToProcess = rawBuffer ?? ensureStringContent(effectiveFinal);
|
|
1871
|
+
assistant.rawContent = contentToProcess;
|
|
1872
|
+
const parser = streamParsers.get(assistant.id);
|
|
1873
|
+
let extractedText: string | null = null;
|
|
1874
|
+
let asyncPending = false;
|
|
1875
|
+
if (parser) {
|
|
1876
|
+
extractedText = parser.getExtractedText();
|
|
1877
|
+
if (extractedText === null) {
|
|
1878
|
+
extractedText = extractTextFromJson(contentToProcess);
|
|
1879
|
+
}
|
|
1880
|
+
if (extractedText === null) {
|
|
1881
|
+
const parsedResult = parser.processChunk(contentToProcess);
|
|
1882
|
+
if (parsedResult instanceof Promise) {
|
|
1883
|
+
asyncPending = true;
|
|
1884
|
+
parsedResult
|
|
1885
|
+
.then((result) => {
|
|
1886
|
+
const text =
|
|
1887
|
+
typeof result === "string" ? result : result?.text ?? null;
|
|
1888
|
+
if (text !== null) {
|
|
1889
|
+
assistant.content = text;
|
|
1890
|
+
assistant.streaming = false;
|
|
1891
|
+
streamParsers.delete(assistant.id);
|
|
1892
|
+
rawContentBuffers.delete(assistant.id);
|
|
1893
|
+
emitMessage(assistant);
|
|
1894
|
+
}
|
|
1895
|
+
})
|
|
1896
|
+
.catch(() => {});
|
|
1897
|
+
} else {
|
|
1898
|
+
extractedText =
|
|
1899
|
+
typeof parsedResult === "string"
|
|
1900
|
+
? parsedResult
|
|
1901
|
+
: parsedResult?.text ?? null;
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
if (!asyncPending) {
|
|
1906
|
+
if (extractedText !== null && extractedText.trim() !== "") {
|
|
1907
|
+
assistant.content = extractedText;
|
|
1908
|
+
} else if (!rawContentBuffers.has(assistant.id)) {
|
|
1909
|
+
assistant.content = ensureStringContent(effectiveFinal);
|
|
1910
|
+
}
|
|
1911
|
+
const parserToClose = streamParsers.get(assistant.id);
|
|
1912
|
+
if (parserToClose) {
|
|
1913
|
+
const closeResult = parserToClose.close?.();
|
|
1914
|
+
if (closeResult instanceof Promise) closeResult.catch(() => {});
|
|
1915
|
+
streamParsers.delete(assistant.id);
|
|
1916
|
+
}
|
|
1917
|
+
rawContentBuffers.delete(assistant.id);
|
|
1918
|
+
assistant.streaming = false;
|
|
1919
|
+
emitMessage(assistant);
|
|
1920
|
+
}
|
|
1921
|
+
};
|
|
1922
|
+
|
|
1923
|
+
// Materialize (lazily) the message for a nested flow-as-tool block, tagged
|
|
1924
|
+
// with the parent tool-call id so the UI renders it in the parent tool's row.
|
|
1925
|
+
const ensureNestedBlockMessage = (
|
|
1926
|
+
blockId: string,
|
|
1927
|
+
parentToolCallId: string,
|
|
1928
|
+
variant?: "reasoning"
|
|
1929
|
+
): AgentWidgetMessage => {
|
|
1930
|
+
const existing = nestedBlockMessages.get(blockId);
|
|
1931
|
+
if (existing) return existing;
|
|
1932
|
+
const message: AgentWidgetMessage = {
|
|
1933
|
+
id: `nested-${parentToolCallId}-${blockId}`,
|
|
1934
|
+
role: "assistant",
|
|
1935
|
+
content: "",
|
|
1936
|
+
createdAt: new Date().toISOString(),
|
|
1937
|
+
streaming: true,
|
|
1938
|
+
sequence: nextSequence(),
|
|
1939
|
+
...(variant ? { variant } : {}),
|
|
1940
|
+
...(variant === "reasoning"
|
|
1941
|
+
? { reasoning: { id: blockId, status: "streaming", chunks: [] } }
|
|
1942
|
+
: {}),
|
|
1943
|
+
agentMetadata: { parentToolId: parentToolCallId },
|
|
1944
|
+
};
|
|
1945
|
+
nestedBlockMessages.set(blockId, message);
|
|
1946
|
+
emitMessage(message);
|
|
1947
|
+
return message;
|
|
1948
|
+
};
|
|
1949
|
+
|
|
1950
|
+
// Ready queue of parsed unified frames awaiting a drain. The API streams the
|
|
1951
|
+
// neutral 33-event unified vocabulary; each frame is parsed in the SSE loop
|
|
1952
|
+
// below and rendered directly by the handler (no translation bridge), then
|
|
1953
|
+
// pushed here. The unified stream is a single, in-order SSE connection, so
|
|
1954
|
+
// frames drain straight through with no reordering.
|
|
1827
1955
|
const seqReadyQueue: Array<{ payloadType: string; payload: any }> = [];
|
|
1828
|
-
|
|
1829
|
-
//
|
|
1830
|
-
// after all handler-scoped variables are initialised (before the SSE loop).
|
|
1956
|
+
// Declared here so later closures can reference it; assigned after all
|
|
1957
|
+
// handler-scoped variables are initialised (before the SSE loop).
|
|
1831
1958
|
let drainReadyQueue: () => void;
|
|
1832
|
-
//
|
|
1833
|
-
//
|
|
1834
|
-
//
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
});
|
|
1850
|
-
// Unified-event consumer (opt-in; see utils/unified-event-bridge.ts). When the
|
|
1851
|
-
// API streams the neutral unified vocabulary, each frame is transduced back to
|
|
1852
|
-
// the legacy events the dispatch chain below already renders. Seed the mode from
|
|
1853
|
-
// the requested vocabulary so /resume (whose stream continues mid-run with no
|
|
1854
|
-
// `execution_start`) bridges correctly; a leading lifecycle frame is then
|
|
1855
|
-
// authoritative and can flip it (e.g. an upstream that ignored `?events=unified`
|
|
1856
|
-
// streams `agent_start`). The unified stream is single-connection and in order,
|
|
1857
|
-
// so bridged events bypass the reorder buffer.
|
|
1858
|
-
const unifiedBridge = new UnifiedToLegacyBridge();
|
|
1859
|
-
let unifiedMode = this.config.events === "unified";
|
|
1860
|
-
let wireModeResolved = false;
|
|
1959
|
+
// Per-stream media-block buffer: the unified media triad
|
|
1960
|
+
// (media_start/media_delta/media_complete) is reassembled here into a single
|
|
1961
|
+
// synthetic message at media_complete, keyed by the block id.
|
|
1962
|
+
const mediaBuffers = new Map<
|
|
1963
|
+
string,
|
|
1964
|
+
{ mediaType?: string; role?: string; toolCallId?: unknown; parts: string[] }
|
|
1965
|
+
>();
|
|
1966
|
+
// Tracks the last iteration surfaced as a per-iteration message boundary, so
|
|
1967
|
+
// `turn_start` advancing the iteration rotates the bubble in 'separate' mode.
|
|
1968
|
+
let lastIterationSeen = 0;
|
|
1969
|
+
// Execution kind, resolved from the leading `execution_start` frame. Drives
|
|
1970
|
+
// the agent-vs-flow branches that the single unified vocabulary collapses.
|
|
1971
|
+
let executionKind: "agent" | "flow" = "agent";
|
|
1972
|
+
// Open turn id (from `turn_start`). Unified text/reasoning deltas carry their
|
|
1973
|
+
// own block id, not the turn id, so the turn id is threaded onto agentMetadata
|
|
1974
|
+
// from here.
|
|
1975
|
+
let openTurnId: string | null = null;
|
|
1861
1976
|
// Agent execution state tracking
|
|
1862
1977
|
let agentExecution: AgentExecutionState | null = null;
|
|
1863
1978
|
// Track assistant messages per agent iteration for 'separate' mode
|
|
1864
1979
|
const agentIterationMessages = new Map<number, AgentWidgetMessage>();
|
|
1865
1980
|
const iterationDisplay = this.config.iterationDisplay ?? 'separate';
|
|
1866
1981
|
|
|
1867
|
-
// Drains
|
|
1868
|
-
// Also invoked after the SSE loop exits so any events
|
|
1982
|
+
// Drains the queued transduced events through the main event handler.
|
|
1983
|
+
// Also invoked after the SSE loop exits so any events queued at
|
|
1869
1984
|
// end-of-stream are processed.
|
|
1870
1985
|
drainReadyQueue = () => {
|
|
1871
1986
|
for (let i = 0; i < seqReadyQueue.length; i++) {
|
|
1872
1987
|
const payloadType = seqReadyQueue[i].payloadType;
|
|
1873
1988
|
const payload = seqReadyQueue[i].payload;
|
|
1874
1989
|
|
|
1875
|
-
if (payloadType === "
|
|
1990
|
+
if (payloadType === "reasoning_start") {
|
|
1991
|
+
// Nested flow-as-tool thinking (PR #4602): route to the parent tool's row.
|
|
1992
|
+
const rStartBlockId = typeof payload.id === "string" ? payload.id : null;
|
|
1993
|
+
const rStartParent =
|
|
1994
|
+
typeof payload.parentToolCallId === "string" && payload.parentToolCallId
|
|
1995
|
+
? payload.parentToolCallId
|
|
1996
|
+
: null;
|
|
1997
|
+
if (rStartBlockId && rStartParent) {
|
|
1998
|
+
nestedBlockParent.set(rStartBlockId, rStartParent);
|
|
1999
|
+
ensureNestedBlockMessage(rStartBlockId, rStartParent, "reasoning");
|
|
2000
|
+
continue;
|
|
2001
|
+
}
|
|
1876
2002
|
const reasoningId =
|
|
1877
2003
|
resolveReasoningId(payload, true) ?? `reason-${nextSequence()}`;
|
|
1878
2004
|
const reasoningMessage = ensureReasoningMessage(reasoningId);
|
|
@@ -1886,10 +2012,29 @@ export class AgentWidgetClient {
|
|
|
1886
2012
|
resolveTimestamp(payload.startedAt ?? payload.timestamp);
|
|
1887
2013
|
reasoningMessage.reasoning.completedAt = undefined;
|
|
1888
2014
|
reasoningMessage.reasoning.durationMs = undefined;
|
|
2015
|
+
if (payload.scope === "loop" || payload.scope === "turn") {
|
|
2016
|
+
reasoningMessage.reasoning.scope = payload.scope;
|
|
2017
|
+
}
|
|
1889
2018
|
reasoningMessage.streaming = true;
|
|
1890
2019
|
reasoningMessage.reasoning.status = "streaming";
|
|
1891
2020
|
emitMessage(reasoningMessage);
|
|
1892
|
-
} else if (payloadType === "
|
|
2021
|
+
} else if (payloadType === "reasoning_delta") {
|
|
2022
|
+
// Nested flow-as-tool thinking: append to the parent-tool-row message.
|
|
2023
|
+
const rDeltaBlockId = typeof payload.id === "string" ? payload.id : null;
|
|
2024
|
+
if (
|
|
2025
|
+
rDeltaBlockId &&
|
|
2026
|
+
nestedBlockParent.has(rDeltaBlockId) &&
|
|
2027
|
+
nestedBlockMessages.has(rDeltaBlockId)
|
|
2028
|
+
) {
|
|
2029
|
+
const nested = nestedBlockMessages.get(rDeltaBlockId)!;
|
|
2030
|
+
const nestedChunk =
|
|
2031
|
+
payload.reasoningText ?? payload.text ?? payload.delta ?? "";
|
|
2032
|
+
if (nestedChunk && payload.hidden !== true && nested.reasoning) {
|
|
2033
|
+
nested.reasoning.chunks.push(String(nestedChunk));
|
|
2034
|
+
emitMessage(nested);
|
|
2035
|
+
}
|
|
2036
|
+
continue;
|
|
2037
|
+
}
|
|
1893
2038
|
const reasoningId =
|
|
1894
2039
|
resolveReasoningId(payload, false) ??
|
|
1895
2040
|
resolveReasoningId(payload, true) ??
|
|
@@ -1933,13 +2078,51 @@ export class AgentWidgetClient {
|
|
|
1933
2078
|
}
|
|
1934
2079
|
reasoningMessage.streaming = reasoningMessage.reasoning.status !== "complete";
|
|
1935
2080
|
emitMessage(reasoningMessage);
|
|
1936
|
-
} else if (payloadType === "
|
|
2081
|
+
} else if (payloadType === "reasoning_complete") {
|
|
2082
|
+
// Nested flow-as-tool thinking close: seal the parent-tool-row message.
|
|
2083
|
+
const rCompleteBlockId = typeof payload.id === "string" ? payload.id : null;
|
|
2084
|
+
if (
|
|
2085
|
+
rCompleteBlockId &&
|
|
2086
|
+
nestedBlockParent.has(rCompleteBlockId) &&
|
|
2087
|
+
nestedBlockMessages.has(rCompleteBlockId)
|
|
2088
|
+
) {
|
|
2089
|
+
const nested = nestedBlockMessages.get(rCompleteBlockId)!;
|
|
2090
|
+
if (nested.reasoning) {
|
|
2091
|
+
const nestedReflection =
|
|
2092
|
+
typeof payload.text === "string" ? payload.text : "";
|
|
2093
|
+
if (nestedReflection && nested.reasoning.chunks.length === 0) {
|
|
2094
|
+
nested.reasoning.chunks.push(nestedReflection);
|
|
2095
|
+
}
|
|
2096
|
+
nested.reasoning.status = "complete";
|
|
2097
|
+
nested.streaming = false;
|
|
2098
|
+
emitMessage(nested);
|
|
2099
|
+
}
|
|
2100
|
+
nestedBlockParent.delete(rCompleteBlockId);
|
|
2101
|
+
nestedBlockMessages.delete(rCompleteBlockId);
|
|
2102
|
+
continue;
|
|
2103
|
+
}
|
|
1937
2104
|
const reasoningId =
|
|
1938
2105
|
resolveReasoningId(payload, false) ??
|
|
1939
2106
|
resolveReasoningId(payload, true) ??
|
|
1940
2107
|
`reason-${nextSequence()}`;
|
|
2108
|
+
// A close carrying text (or scope:"loop") is a cross-iteration
|
|
2109
|
+
// reflection fold (merged spec §4 E3): the API streams nothing for the
|
|
2110
|
+
// block, then delivers the whole reflection as `text` on the close.
|
|
2111
|
+
// Materialize a reasoning bubble even if no reasoning_start/delta opened
|
|
2112
|
+
// one, and adopt the close text when the block streamed no chunks (the
|
|
2113
|
+
// common reflection case, where reasoning_start opened an empty bubble).
|
|
2114
|
+
const reflectionText = typeof payload.text === "string" ? payload.text : "";
|
|
2115
|
+
if (!reasoningMessages.get(reasoningId) && (reflectionText || payload.scope === "loop")) {
|
|
2116
|
+
ensureReasoningMessage(reasoningId);
|
|
2117
|
+
}
|
|
1941
2118
|
const reasoningMessage = reasoningMessages.get(reasoningId);
|
|
1942
2119
|
if (reasoningMessage?.reasoning) {
|
|
2120
|
+
if (payload.scope === "loop" || payload.scope === "turn") {
|
|
2121
|
+
reasoningMessage.reasoning.scope = payload.scope;
|
|
2122
|
+
}
|
|
2123
|
+
if (reflectionText && reasoningMessage.reasoning.chunks.length === 0) {
|
|
2124
|
+
reasoningMessage.reasoning.chunks.push(reflectionText);
|
|
2125
|
+
}
|
|
1943
2126
|
reasoningMessage.reasoning.status = "complete";
|
|
1944
2127
|
reasoningMessage.reasoning.completedAt = resolveTimestamp(
|
|
1945
2128
|
payload.completedAt ?? payload.timestamp
|
|
@@ -1958,14 +2141,30 @@ export class AgentWidgetClient {
|
|
|
1958
2141
|
reasoningContext.byStep.delete(stepKey);
|
|
1959
2142
|
}
|
|
1960
2143
|
} else if (payloadType === "tool_start") {
|
|
1961
|
-
|
|
1962
|
-
|
|
2144
|
+
// Unified tool family (agent + flow). Seal any open assistant bubble so
|
|
2145
|
+
// text→tool→text interleaves chronologically (the API also emits a
|
|
2146
|
+
// text_complete here, so this is usually a no-op — kept for safety).
|
|
2147
|
+
if (assistantMessage) {
|
|
2148
|
+
(assistantMessage as AgentWidgetMessage).streaming = false;
|
|
2149
|
+
emitMessage(assistantMessage as AgentWidgetMessage);
|
|
2150
|
+
assistantMessage = null;
|
|
2151
|
+
}
|
|
2152
|
+
// Unified denormalizes `iteration` onto tool frames too (merged spec §2).
|
|
2153
|
+
// Track it so media/reflection blocks — which carry no iteration of their
|
|
2154
|
+
// own — can be stamped with the enclosing iteration even on tool-only
|
|
2155
|
+
// turns that never emit a `turn_start`.
|
|
2156
|
+
if (typeof payload.iteration === "number") lastIterationSeen = payload.iteration;
|
|
2157
|
+
const toolId: string =
|
|
2158
|
+
(typeof payload.toolCallId === "string" ? payload.toolCallId : undefined) ??
|
|
2159
|
+
resolveToolId(payload, true) ??
|
|
2160
|
+
`tool-${nextSequence()}`;
|
|
1963
2161
|
const toolName = payload.toolName ?? payload.name;
|
|
1964
2162
|
// Suppress tool UI for artifact emit tools: artifacts are handled via artifact_* events
|
|
1965
2163
|
if (isArtifactEmitToolName(toolName)) {
|
|
1966
2164
|
artifactToolCallIds.add(toolId);
|
|
1967
2165
|
continue;
|
|
1968
2166
|
}
|
|
2167
|
+
trackToolId(getToolCallKey(payload), toolId);
|
|
1969
2168
|
const toolMessage = ensureToolMessage(toolId);
|
|
1970
2169
|
const tool = toolMessage.toolCall ?? {
|
|
1971
2170
|
id: toolId,
|
|
@@ -1973,10 +2172,10 @@ export class AgentWidgetClient {
|
|
|
1973
2172
|
};
|
|
1974
2173
|
tool.name = toolName ?? tool.name;
|
|
1975
2174
|
tool.status = "running";
|
|
1976
|
-
if (payload.
|
|
1977
|
-
tool.args = payload.args;
|
|
1978
|
-
} else if (payload.parameters !== undefined) {
|
|
2175
|
+
if (payload.parameters !== undefined) {
|
|
1979
2176
|
tool.args = payload.parameters;
|
|
2177
|
+
} else if (payload.args !== undefined) {
|
|
2178
|
+
tool.args = payload.args;
|
|
1980
2179
|
}
|
|
1981
2180
|
tool.startedAt =
|
|
1982
2181
|
tool.startedAt ??
|
|
@@ -1985,15 +2184,14 @@ export class AgentWidgetClient {
|
|
|
1985
2184
|
tool.durationMs = undefined;
|
|
1986
2185
|
toolMessage.toolCall = tool;
|
|
1987
2186
|
toolMessage.streaming = true;
|
|
1988
|
-
|
|
1989
|
-
if (agentCtx || payload.executionId) {
|
|
2187
|
+
if (payload.executionId) {
|
|
1990
2188
|
toolMessage.agentMetadata = {
|
|
1991
|
-
executionId:
|
|
1992
|
-
iteration:
|
|
2189
|
+
executionId: payload.executionId,
|
|
2190
|
+
iteration: payload.iteration,
|
|
1993
2191
|
};
|
|
1994
2192
|
}
|
|
1995
2193
|
emitMessage(toolMessage);
|
|
1996
|
-
} else if (payloadType === "
|
|
2194
|
+
} else if (payloadType === "tool_output_delta") {
|
|
1997
2195
|
const toolId =
|
|
1998
2196
|
resolveToolId(payload, false) ??
|
|
1999
2197
|
resolveToolId(payload, true) ??
|
|
@@ -2072,11 +2270,17 @@ export class AgentWidgetClient {
|
|
|
2072
2270
|
if (callKey) {
|
|
2073
2271
|
toolContext.byCall.delete(callKey);
|
|
2074
2272
|
}
|
|
2075
|
-
} else if (payloadType === "
|
|
2076
|
-
// LOCAL tool pause.
|
|
2077
|
-
//
|
|
2078
|
-
//
|
|
2079
|
-
//
|
|
2273
|
+
} else if (payloadType === "await" && payload.toolName) {
|
|
2274
|
+
// LOCAL tool pause. Two wire shapes resolve here, by dispatch target:
|
|
2275
|
+
// - FLOW dispatch → `step_await` + `awaitReason: "local_tool_required"`
|
|
2276
|
+
// (Runtype's prompt step throws LocalToolRequiredError when the model
|
|
2277
|
+
// calls a `toolType: "local"` tool).
|
|
2278
|
+
// - AGENT dispatch → `agent_await` (the agent runtime's native pause).
|
|
2279
|
+
// Either way the server emits the tool name, params, and execution id;
|
|
2280
|
+
// the execution pauses until the client POSTs /resume with toolOutputs.
|
|
2281
|
+
// `agent_await` carries a BARE tool name plus an `origin`; page tools
|
|
2282
|
+
// (origin "webmcp") are normalized to the `webmcp:`-prefixed form below
|
|
2283
|
+
// so the bridge + session.ts `/resume` keying are identical for both.
|
|
2080
2284
|
//
|
|
2081
2285
|
// Upsert a fully-populated tool-variant message so the existing
|
|
2082
2286
|
// ask_user_question bubble + sheet paths fire. Mark the message with
|
|
@@ -2099,7 +2303,15 @@ export class AgentWidgetClient {
|
|
|
2099
2303
|
const toolId =
|
|
2100
2304
|
toolCallId ?? (payload.toolId as string) ?? `local-${nextSequence()}`;
|
|
2101
2305
|
const toolMessage = ensureToolMessage(toolId);
|
|
2102
|
-
const
|
|
2306
|
+
const rawToolName = payload.toolName as string;
|
|
2307
|
+
// `agent_await` page tools arrive with a bare name; synthesize the
|
|
2308
|
+
// `webmcp:` prefix so isWebMcpToolName (and the bridge's prefix-strip on
|
|
2309
|
+
// resume) treat them identically to a flow `step_await`.
|
|
2310
|
+
const toolName =
|
|
2311
|
+
payload.origin === "webmcp" &&
|
|
2312
|
+
!isWebMcpToolName(rawToolName)
|
|
2313
|
+
? `webmcp:${rawToolName}`
|
|
2314
|
+
: rawToolName;
|
|
2103
2315
|
const webMcpTool = isWebMcpToolName(toolName);
|
|
2104
2316
|
const tool = toolMessage.toolCall ?? { id: toolId, status: "pending" as const };
|
|
2105
2317
|
tool.name = toolName;
|
|
@@ -2113,7 +2325,8 @@ export class AgentWidgetClient {
|
|
|
2113
2325
|
tool.status = webMcpTool ? "running" : "complete";
|
|
2114
2326
|
tool.chunks = tool.chunks ?? [];
|
|
2115
2327
|
tool.startedAt =
|
|
2116
|
-
tool.startedAt ??
|
|
2328
|
+
tool.startedAt ??
|
|
2329
|
+
resolveTimestamp(payload.startedAt ?? payload.timestamp ?? payload.awaitedAt);
|
|
2117
2330
|
if (webMcpTool) {
|
|
2118
2331
|
tool.completedAt = undefined;
|
|
2119
2332
|
tool.duration = undefined;
|
|
@@ -2134,339 +2347,123 @@ export class AgentWidgetClient {
|
|
|
2134
2347
|
};
|
|
2135
2348
|
emitMessage(toolMessage);
|
|
2136
2349
|
} else if (payloadType === "text_start") {
|
|
2137
|
-
//
|
|
2138
|
-
//
|
|
2139
|
-
//
|
|
2140
|
-
//
|
|
2141
|
-
|
|
2350
|
+
// Nested flow-as-tool text (PR #4602): a `parentToolCallId` means this
|
|
2351
|
+
// block belongs to a flow running as that tool — record the mapping and
|
|
2352
|
+
// leave the top-level assistant bubble untouched (the nested deltas route
|
|
2353
|
+
// into the parent tool's row).
|
|
2354
|
+
const startBlockId = typeof payload.id === "string" ? payload.id : null;
|
|
2355
|
+
const startParent =
|
|
2356
|
+
typeof payload.parentToolCallId === "string" && payload.parentToolCallId
|
|
2357
|
+
? payload.parentToolCallId
|
|
2358
|
+
: null;
|
|
2359
|
+
if (startBlockId && startParent) {
|
|
2360
|
+
nestedBlockParent.set(startBlockId, startParent);
|
|
2142
2361
|
continue;
|
|
2143
2362
|
}
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2363
|
+
// Unified text-channel block open. A new block id means a new bubble, so
|
|
2364
|
+
// seal any open assistant bubble; the next text_delta creates a fresh one
|
|
2365
|
+
// (lazily). The API emits a fresh block at every tool/media/approval/await
|
|
2366
|
+
// boundary, so block-id keying drives segmentation — no partId.
|
|
2367
|
+
const prev = assistantMessage as AgentWidgetMessage | null;
|
|
2368
|
+
if (prev) {
|
|
2369
|
+
// Normally text_complete already sealed the prior block; this is the
|
|
2370
|
+
// defensive path if a producer opens a new block without closing.
|
|
2371
|
+
if (executionKind === "flow") {
|
|
2372
|
+
finalizeFlowTextBlock(prev);
|
|
2373
|
+
lastSealedFlowBubble = prev;
|
|
2374
|
+
} else {
|
|
2148
2375
|
prev.streaming = false;
|
|
2149
2376
|
emitMessage(prev);
|
|
2150
|
-
lastSealedTextSegment = prev;
|
|
2151
|
-
assistantMessage = null;
|
|
2152
|
-
didSplitByPartId = true;
|
|
2153
2377
|
}
|
|
2154
|
-
}
|
|
2155
|
-
if (incomingPartId !== undefined) {
|
|
2156
|
-
partIdState.current = incomingPartId;
|
|
2157
|
-
}
|
|
2158
|
-
} else if (payloadType === "text_end") {
|
|
2159
|
-
// Lifecycle event: current text segment ended (tool call about to start).
|
|
2160
|
-
// When toolContext is present the boundary belongs to a nested flow: leave
|
|
2161
|
-
// outer assistant state alone so the outer stream is never interrupted by
|
|
2162
|
-
// nested activity.
|
|
2163
|
-
if ((payload as any).toolContext?.toolId) {
|
|
2164
|
-
continue;
|
|
2165
|
-
}
|
|
2166
|
-
// Seal the current assistant message so the next segment gets a new one
|
|
2167
|
-
const prev = assistantMessage as AgentWidgetMessage | null;
|
|
2168
|
-
if (prev) {
|
|
2169
|
-
prev.streaming = false;
|
|
2170
|
-
emitMessage(prev);
|
|
2171
|
-
lastSealedTextSegment = prev;
|
|
2172
2378
|
assistantMessage = null;
|
|
2173
|
-
didSplitByPartId = true;
|
|
2174
2379
|
}
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2380
|
+
currentTextBlockId =
|
|
2381
|
+
typeof payload.id === "string" ? payload.id : currentTextBlockId;
|
|
2382
|
+
pendingFlowRaw = "";
|
|
2383
|
+
} else if (payloadType === "text_delta") {
|
|
2384
|
+
// Nested flow-as-tool text: route to the parent tool's row, through the
|
|
2385
|
+
// same structured-content parser, never the top-level assistant channel.
|
|
2386
|
+
const deltaBlockId = typeof payload.id === "string" ? payload.id : null;
|
|
2387
|
+
const nestedParent = deltaBlockId
|
|
2388
|
+
? nestedBlockParent.get(deltaBlockId)
|
|
2389
|
+
: undefined;
|
|
2390
|
+
if (deltaBlockId && nestedParent) {
|
|
2391
|
+
const nestedDelta =
|
|
2392
|
+
typeof payload.delta === "string" ? payload.delta : "";
|
|
2393
|
+
const nestedRaw = (nestedBlockRaw.get(deltaBlockId) ?? "") + nestedDelta;
|
|
2394
|
+
nestedBlockRaw.set(deltaBlockId, nestedRaw);
|
|
2395
|
+
if (nestedRaw.trim() === "") continue;
|
|
2396
|
+
const nested = ensureNestedBlockMessage(deltaBlockId, nestedParent);
|
|
2397
|
+
nested.agentMetadata = {
|
|
2398
|
+
...nested.agentMetadata,
|
|
2399
|
+
executionId: payload.executionId,
|
|
2400
|
+
parentToolId: nestedParent,
|
|
2401
|
+
};
|
|
2402
|
+
applyFlowTextChunk(nested, nestedRaw, nestedDelta, undefined);
|
|
2181
2403
|
continue;
|
|
2182
2404
|
}
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
);
|
|
2199
|
-
|
|
2200
|
-
payload.partId !== undefined && payload.partId !== null
|
|
2201
|
-
? String(payload.partId)
|
|
2202
|
-
: "";
|
|
2203
|
-
const stepScopeKey = `${nestedToolCtx.toolId}::${nestedStepId}`;
|
|
2204
|
-
const prevPartId = nestedPartIdByStep.get(stepScopeKey);
|
|
2205
|
-
|
|
2206
|
-
// If partId changed within this nested step (prompt with inner
|
|
2207
|
-
// tool call emitting a new text segment), seal the previous
|
|
2208
|
-
// segment's message so each segment renders as its own bubble.
|
|
2209
|
-
if (
|
|
2210
|
-
incomingPartId !== "" &&
|
|
2211
|
-
prevPartId !== undefined &&
|
|
2212
|
-
prevPartId !== "" &&
|
|
2213
|
-
prevPartId !== incomingPartId
|
|
2214
|
-
) {
|
|
2215
|
-
const prev = nestedStepMessages.get(
|
|
2216
|
-
getNestedStepKey(
|
|
2217
|
-
nestedToolCtx.toolId,
|
|
2218
|
-
nestedStepId,
|
|
2219
|
-
prevPartId
|
|
2220
|
-
)
|
|
2221
|
-
);
|
|
2222
|
-
if (prev && prev.streaming !== false) {
|
|
2223
|
-
prev.streaming = false;
|
|
2224
|
-
emitMessage(prev);
|
|
2225
|
-
}
|
|
2226
|
-
}
|
|
2227
|
-
if (incomingPartId !== "") {
|
|
2228
|
-
nestedPartIdByStep.set(stepScopeKey, incomingPartId);
|
|
2229
|
-
}
|
|
2230
|
-
|
|
2231
|
-
const nestedMsg = ensureNestedStepMessage(
|
|
2232
|
-
nestedToolCtx.toolId,
|
|
2233
|
-
nestedStepId,
|
|
2234
|
-
incomingPartId,
|
|
2235
|
-
nestedToolCtx.executionId
|
|
2236
|
-
);
|
|
2237
|
-
const nestedChunk =
|
|
2238
|
-
payload.text ??
|
|
2239
|
-
payload.delta ??
|
|
2240
|
-
payload.content ??
|
|
2241
|
-
payload.chunk ??
|
|
2242
|
-
"";
|
|
2243
|
-
if (nestedChunk) {
|
|
2244
|
-
nestedMsg.content += String(nestedChunk);
|
|
2245
|
-
nestedMsg.streaming = true;
|
|
2246
|
-
emitMessage(nestedMsg);
|
|
2247
|
-
}
|
|
2248
|
-
if (payload.isComplete) {
|
|
2249
|
-
nestedMsg.streaming = false;
|
|
2250
|
-
emitMessage(nestedMsg);
|
|
2251
|
-
}
|
|
2405
|
+
currentTextBlockId =
|
|
2406
|
+
typeof payload.id === "string" ? payload.id : currentTextBlockId;
|
|
2407
|
+
if (executionKind === "flow") {
|
|
2408
|
+
// Flow prompt-step text can be structured JSON: accumulate the raw
|
|
2409
|
+
// block and run it through the structured-content parser, keyed by the
|
|
2410
|
+
// block-id bubble. Materialize lazily so a whitespace-only block
|
|
2411
|
+
// (newlines around a tool boundary) never leaves a stray bubble.
|
|
2412
|
+
const delta = typeof payload.delta === "string" ? payload.delta : "";
|
|
2413
|
+
pendingFlowRaw += delta;
|
|
2414
|
+
if (pendingFlowRaw.trim() === "") continue;
|
|
2415
|
+
const assistant = ensureAssistantMessage();
|
|
2416
|
+
assistant.agentMetadata = {
|
|
2417
|
+
executionId: payload.executionId,
|
|
2418
|
+
iteration: payload.iteration,
|
|
2419
|
+
};
|
|
2420
|
+
applyFlowTextChunk(assistant, pendingFlowRaw, delta, undefined);
|
|
2421
|
+
lastAssistantInTurn = assistant;
|
|
2252
2422
|
continue;
|
|
2253
2423
|
}
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
? (assistantMessagesByPartId.get(incomingPartId) ?? ensureAssistantMessage())
|
|
2275
|
-
: ensureAssistantMessage();
|
|
2276
|
-
if (incomingPartId !== undefined) {
|
|
2277
|
-
if (!assistant.partId) {
|
|
2278
|
-
assistant.partId = incomingPartId;
|
|
2279
|
-
}
|
|
2280
|
-
assistantMessagesByPartId.set(incomingPartId, assistant);
|
|
2424
|
+
const assistant = ensureAssistantMessage();
|
|
2425
|
+
assistant.content += payload.delta ?? '';
|
|
2426
|
+
assistant.agentMetadata = {
|
|
2427
|
+
executionId: payload.executionId,
|
|
2428
|
+
iteration: payload.iteration,
|
|
2429
|
+
turnId: openTurnId ?? undefined,
|
|
2430
|
+
agentName: agentExecution?.agentName
|
|
2431
|
+
};
|
|
2432
|
+
lastAssistantInTurn = assistant;
|
|
2433
|
+
emitMessage(assistant);
|
|
2434
|
+
} else if (payloadType === "text_complete") {
|
|
2435
|
+
// Nested flow-as-tool text block close: seal its parent-tool-row message.
|
|
2436
|
+
const completeBlockId = typeof payload.id === "string" ? payload.id : null;
|
|
2437
|
+
if (completeBlockId && nestedBlockParent.has(completeBlockId)) {
|
|
2438
|
+
const nested = nestedBlockMessages.get(completeBlockId);
|
|
2439
|
+
if (nested) finalizeFlowTextBlock(nested);
|
|
2440
|
+
nestedBlockParent.delete(completeBlockId);
|
|
2441
|
+
nestedBlockRaw.delete(completeBlockId);
|
|
2442
|
+
nestedBlockMessages.delete(completeBlockId);
|
|
2443
|
+
continue;
|
|
2281
2444
|
}
|
|
2282
|
-
//
|
|
2283
|
-
const
|
|
2284
|
-
if (
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
const chunkSeq = typeof payload.seq === "number" ? payload.seq : undefined;
|
|
2291
|
-
const chunkBufferKey = incomingPartId ?? assistant.id;
|
|
2292
|
-
const accumulatedRaw =
|
|
2293
|
-
chunkSeq !== undefined
|
|
2294
|
-
? insertOrderedChunk(chunkBufferKey, chunkSeq, String(chunk))
|
|
2295
|
-
: (rawContentBuffers.get(assistant.id) ?? "") + chunk;
|
|
2296
|
-
// Store raw content for action parsing, but NEVER set assistant.content to raw JSON
|
|
2297
|
-
assistant.rawContent = accumulatedRaw;
|
|
2298
|
-
|
|
2299
|
-
// Use stream parser to parse
|
|
2300
|
-
if (!streamParsers.has(assistant.id)) {
|
|
2301
|
-
streamParsers.set(assistant.id, this.createStreamParser());
|
|
2302
|
-
}
|
|
2303
|
-
const parser = streamParsers.get(assistant.id)!;
|
|
2304
|
-
|
|
2305
|
-
// Check if content looks like JSON
|
|
2306
|
-
const looksLikeJson = accumulatedRaw.trim().startsWith('{') || accumulatedRaw.trim().startsWith('[');
|
|
2307
|
-
|
|
2308
|
-
// Store raw buffer before processing (needed for step_complete handler)
|
|
2309
|
-
if (looksLikeJson) {
|
|
2310
|
-
rawContentBuffers.set(assistant.id, accumulatedRaw);
|
|
2311
|
-
}
|
|
2312
|
-
|
|
2313
|
-
// Check if this is a plain text parser (marked with __isPlainTextParser)
|
|
2314
|
-
const isPlainTextParser = (parser as any).__isPlainTextParser === true;
|
|
2315
|
-
|
|
2316
|
-
// If plain text parser, just append the chunk directly
|
|
2317
|
-
if (isPlainTextParser) {
|
|
2318
|
-
assistant.content = chunkSeq !== undefined ? accumulatedRaw : assistant.content + chunk;
|
|
2319
|
-
// Clear any raw buffer/parser since we're in plain text mode
|
|
2320
|
-
rawContentBuffers.delete(assistant.id);
|
|
2321
|
-
streamParsers.delete(assistant.id);
|
|
2322
|
-
assistant.rawContent = undefined;
|
|
2323
|
-
emitMessage(assistant);
|
|
2324
|
-
continue;
|
|
2325
|
-
}
|
|
2326
|
-
|
|
2327
|
-
// Try to parse with the parser (for structured parsers)
|
|
2328
|
-
const parsedResult = parser.processChunk(accumulatedRaw);
|
|
2329
|
-
|
|
2330
|
-
// Handle async parser result
|
|
2331
|
-
if (parsedResult instanceof Promise) {
|
|
2332
|
-
parsedResult.then((result) => {
|
|
2333
|
-
// Extract text from result (could be string or object)
|
|
2334
|
-
const text = typeof result === 'string' ? result : result?.text ?? null;
|
|
2335
|
-
|
|
2336
|
-
if (text !== null && text.trim() !== "") {
|
|
2337
|
-
// Parser successfully extracted text: update the chunk's assistant
|
|
2338
|
-
// (not assistantMessage; text_end may have cleared that ref before microtasks run)
|
|
2339
|
-
assistant.content = text;
|
|
2340
|
-
emitMessage(assistant);
|
|
2341
|
-
} else if (!looksLikeJson && !accumulatedRaw.trim().startsWith('<')) {
|
|
2342
|
-
// Not a structured format - show as plain text
|
|
2343
|
-
const currentAssistant = assistantMessage;
|
|
2344
|
-
const targetAssistant =
|
|
2345
|
-
currentAssistant && currentAssistant.id === assistant.id
|
|
2346
|
-
? currentAssistant
|
|
2347
|
-
: assistant;
|
|
2348
|
-
if (targetAssistant.id === assistant.id) {
|
|
2349
|
-
targetAssistant.content =
|
|
2350
|
-
chunkSeq !== undefined ? accumulatedRaw : targetAssistant.content + chunk;
|
|
2351
|
-
rawContentBuffers.delete(targetAssistant.id);
|
|
2352
|
-
streamParsers.delete(targetAssistant.id);
|
|
2353
|
-
targetAssistant.rawContent = undefined;
|
|
2354
|
-
emitMessage(targetAssistant);
|
|
2355
|
-
}
|
|
2356
|
-
}
|
|
2357
|
-
// Otherwise wait for more chunks (incomplete structured format)
|
|
2358
|
-
// Don't emit message if parser hasn't extracted text yet
|
|
2359
|
-
}).catch(() => {
|
|
2360
|
-
// On error, treat as plain text
|
|
2361
|
-
assistant.content =
|
|
2362
|
-
chunkSeq !== undefined ? accumulatedRaw : assistant.content + chunk;
|
|
2363
|
-
rawContentBuffers.delete(assistant.id);
|
|
2364
|
-
streamParsers.delete(assistant.id);
|
|
2365
|
-
assistant.rawContent = undefined;
|
|
2366
|
-
emitMessage(assistant);
|
|
2367
|
-
});
|
|
2445
|
+
// Seal the current text block's bubble.
|
|
2446
|
+
const prev = assistantMessage as AgentWidgetMessage | null;
|
|
2447
|
+
if (prev) {
|
|
2448
|
+
if (executionKind === "flow") {
|
|
2449
|
+
// Final structured extraction off the accumulated raw buffer; the
|
|
2450
|
+
// authoritative step_complete.result.response reconciles next.
|
|
2451
|
+
finalizeFlowTextBlock(prev);
|
|
2452
|
+
lastSealedFlowBubble = prev;
|
|
2368
2453
|
} else {
|
|
2369
|
-
//
|
|
2370
|
-
//
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
// Parser successfully extracted text
|
|
2375
|
-
// Buffer is already set above
|
|
2376
|
-
assistant.content = text;
|
|
2377
|
-
emitMessage(assistant);
|
|
2378
|
-
} else if (!looksLikeJson && !accumulatedRaw.trim().startsWith('<')) {
|
|
2379
|
-
// Not a structured format - show as plain text
|
|
2380
|
-
assistant.content =
|
|
2381
|
-
chunkSeq !== undefined ? accumulatedRaw : assistant.content + chunk;
|
|
2382
|
-
// Clear any raw buffer/parser if we were in structured format mode
|
|
2383
|
-
rawContentBuffers.delete(assistant.id);
|
|
2384
|
-
streamParsers.delete(assistant.id);
|
|
2385
|
-
assistant.rawContent = undefined;
|
|
2386
|
-
emitMessage(assistant);
|
|
2387
|
-
}
|
|
2388
|
-
// Otherwise wait for more chunks (incomplete structured format)
|
|
2389
|
-
// Don't emit message if parser hasn't extracted text yet
|
|
2390
|
-
}
|
|
2391
|
-
|
|
2392
|
-
// IMPORTANT: Don't call getExtractedText() and emit messages here
|
|
2393
|
-
// This was causing raw JSON to be displayed because getExtractedText()
|
|
2394
|
-
// wasn't extracting the "text" field correctly during streaming
|
|
2395
|
-
}
|
|
2396
|
-
if (payload.isComplete) {
|
|
2397
|
-
const finalContent = payload.result?.response ?? assistant.content;
|
|
2398
|
-
if (finalContent) {
|
|
2399
|
-
// Check if we have raw content buffer that needs final processing
|
|
2400
|
-
const rawBuffer = rawContentBuffers.get(assistant.id);
|
|
2401
|
-
const contentToProcess = rawBuffer ?? ensureStringContent(finalContent);
|
|
2402
|
-
assistant.rawContent = contentToProcess;
|
|
2403
|
-
|
|
2404
|
-
// Try to extract text from final structured content
|
|
2405
|
-
const parser = streamParsers.get(assistant.id);
|
|
2406
|
-
let extractedText: string | null = null;
|
|
2407
|
-
let asyncPending = false;
|
|
2408
|
-
|
|
2409
|
-
if (parser) {
|
|
2410
|
-
// First check if parser already has extracted text
|
|
2411
|
-
extractedText = parser.getExtractedText();
|
|
2412
|
-
|
|
2413
|
-
if (extractedText === null) {
|
|
2414
|
-
// Try extracting with regex
|
|
2415
|
-
extractedText = extractTextFromJson(contentToProcess);
|
|
2416
|
-
}
|
|
2417
|
-
|
|
2418
|
-
if (extractedText === null) {
|
|
2419
|
-
// Try parser.processChunk as last resort
|
|
2420
|
-
const parsedResult = parser.processChunk(contentToProcess);
|
|
2421
|
-
if (parsedResult instanceof Promise) {
|
|
2422
|
-
asyncPending = true;
|
|
2423
|
-
parsedResult.then((result) => {
|
|
2424
|
-
// Extract text from result (could be string or object)
|
|
2425
|
-
const text = typeof result === 'string' ? result : result?.text ?? null;
|
|
2426
|
-
if (text !== null) {
|
|
2427
|
-
const currentAssistant = assistantMessage;
|
|
2428
|
-
if (currentAssistant && currentAssistant.id === assistant.id) {
|
|
2429
|
-
currentAssistant.content = text;
|
|
2430
|
-
currentAssistant.streaming = false;
|
|
2431
|
-
// Clean up
|
|
2432
|
-
streamParsers.delete(currentAssistant.id);
|
|
2433
|
-
rawContentBuffers.delete(currentAssistant.id);
|
|
2434
|
-
emitMessage(currentAssistant);
|
|
2435
|
-
}
|
|
2436
|
-
}
|
|
2437
|
-
});
|
|
2438
|
-
} else {
|
|
2439
|
-
// Extract text from synchronous result
|
|
2440
|
-
extractedText = typeof parsedResult === 'string' ? parsedResult : parsedResult?.text ?? null;
|
|
2441
|
-
}
|
|
2442
|
-
}
|
|
2443
|
-
}
|
|
2444
|
-
|
|
2445
|
-
// Skip sync emit if we're waiting on async parser
|
|
2446
|
-
if (!asyncPending) {
|
|
2447
|
-
// Set content: use extracted text if available, otherwise use raw content
|
|
2448
|
-
if (extractedText !== null && extractedText.trim() !== "") {
|
|
2449
|
-
assistant.content = extractedText;
|
|
2450
|
-
} else if (!rawContentBuffers.has(assistant.id)) {
|
|
2451
|
-
// Only use raw final content if we didn't accumulate chunks
|
|
2452
|
-
assistant.content = ensureStringContent(finalContent);
|
|
2453
|
-
}
|
|
2454
|
-
|
|
2455
|
-
// Clean up parser and buffer
|
|
2456
|
-
const parserToClose = streamParsers.get(assistant.id);
|
|
2457
|
-
if (parserToClose) {
|
|
2458
|
-
const closeResult = parserToClose.close?.();
|
|
2459
|
-
if (closeResult instanceof Promise) {
|
|
2460
|
-
closeResult.catch(() => {});
|
|
2461
|
-
}
|
|
2462
|
-
streamParsers.delete(assistant.id);
|
|
2463
|
-
}
|
|
2464
|
-
rawContentBuffers.delete(assistant.id);
|
|
2465
|
-
assistant.streaming = false;
|
|
2466
|
-
emitMessage(assistant);
|
|
2454
|
+
// U2: text_complete carries the assembled text, but the bubble already
|
|
2455
|
+
// holds it from the deltas — only fall back to payload.text when no
|
|
2456
|
+
// delta content was seen, never double-count.
|
|
2457
|
+
if ((prev.content ?? "") === "" && typeof payload.text === "string") {
|
|
2458
|
+
prev.content = payload.text;
|
|
2467
2459
|
}
|
|
2460
|
+
prev.streaming = false;
|
|
2461
|
+
emitMessage(prev);
|
|
2468
2462
|
}
|
|
2463
|
+
assistantMessage = null;
|
|
2469
2464
|
}
|
|
2465
|
+
currentTextBlockId = null;
|
|
2466
|
+
pendingFlowRaw = "";
|
|
2470
2467
|
} else if (payloadType === "step_complete") {
|
|
2471
2468
|
// Only process completions for prompt steps, not tool/context steps
|
|
2472
2469
|
const stepType = (payload as any).stepType;
|
|
@@ -2476,378 +2473,139 @@ export class AgentWidgetClient {
|
|
|
2476
2473
|
continue;
|
|
2477
2474
|
}
|
|
2478
2475
|
|
|
2479
|
-
//
|
|
2480
|
-
//
|
|
2481
|
-
//
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
);
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
);
|
|
2496
|
-
for (const [key, msg] of nestedStepMessages) {
|
|
2497
|
-
if (key.startsWith(prefix) && msg.streaming !== false) {
|
|
2498
|
-
msg.streaming = false;
|
|
2499
|
-
emitMessage(msg);
|
|
2500
|
-
}
|
|
2501
|
-
}
|
|
2502
|
-
nestedPartIdByStep.delete(
|
|
2503
|
-
`${nestedCompleteCtx.toolId}::${nestedStepId}`
|
|
2504
|
-
);
|
|
2476
|
+
// A failed step (`success:false`) — including the legacy `step_error`
|
|
2477
|
+
// event, which the unified encoder folds into a failed `step_complete`
|
|
2478
|
+
// — surfaces as a terminal error and finalizes the stream.
|
|
2479
|
+
if (payload.success === false) {
|
|
2480
|
+
const e = payload.error;
|
|
2481
|
+
const message =
|
|
2482
|
+
typeof e === "string" && e !== ""
|
|
2483
|
+
? e
|
|
2484
|
+
: e != null && typeof e === "object" && "message" in e
|
|
2485
|
+
? String((e as { message?: unknown }).message ?? "Step failed")
|
|
2486
|
+
: "Step failed";
|
|
2487
|
+
onEvent({ type: "error", error: new Error(message) });
|
|
2488
|
+
const finalMsg = assistantMessage as AgentWidgetMessage | null;
|
|
2489
|
+
if (finalMsg && finalMsg.streaming) {
|
|
2490
|
+
finalMsg.streaming = false;
|
|
2491
|
+
emitMessage(finalMsg);
|
|
2505
2492
|
}
|
|
2493
|
+
onEvent({ type: "status", status: "idle" });
|
|
2506
2494
|
continue;
|
|
2507
2495
|
}
|
|
2508
2496
|
|
|
2509
|
-
//
|
|
2510
|
-
//
|
|
2511
|
-
//
|
|
2512
|
-
//
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
if (
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
if (
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
const splitFinalContent = payload.result?.response;
|
|
2531
|
-
const sealedForReconcile = lastSealedTextSegment;
|
|
2532
|
-
if (sealedForReconcile) {
|
|
2533
|
-
if (stepStopReason) sealedForReconcile.stopReason = stepStopReason;
|
|
2534
|
-
if (splitFinalContent !== undefined && splitFinalContent !== null) {
|
|
2535
|
-
reconcileSealedAssistantWithFinalResponse(sealedForReconcile, splitFinalContent);
|
|
2536
|
-
} else {
|
|
2537
|
-
streamParsers.delete(sealedForReconcile.id);
|
|
2538
|
-
rawContentBuffers.delete(sealedForReconcile.id);
|
|
2497
|
+
// Unified flow: reconcile the just-sealed text block with the
|
|
2498
|
+
// authoritative structured final (`result.response`). Displayed content
|
|
2499
|
+
// stays as streamed — a multi-segment step keeps each bubble's own text;
|
|
2500
|
+
// reconcile only fills/repairs the last sealed block and sets rawContent.
|
|
2501
|
+
// A pure-tool / text-less step (no sealed flow bubble) completes silently.
|
|
2502
|
+
{
|
|
2503
|
+
const sealed = lastSealedFlowBubble;
|
|
2504
|
+
lastSealedFlowBubble = null;
|
|
2505
|
+
const flowStopReason = (payload as any).stopReason as
|
|
2506
|
+
| StopReasonKind
|
|
2507
|
+
| undefined;
|
|
2508
|
+
const finalResponse = payload.result?.response;
|
|
2509
|
+
if (sealed) {
|
|
2510
|
+
if (flowStopReason) sealed.stopReason = flowStopReason;
|
|
2511
|
+
if (finalResponse !== undefined && finalResponse !== null) {
|
|
2512
|
+
reconcileSealedAssistantWithFinalResponse(sealed, finalResponse);
|
|
2513
|
+
} else if (sealed.streaming !== false) {
|
|
2514
|
+
streamParsers.delete(sealed.id);
|
|
2515
|
+
rawContentBuffers.delete(sealed.id);
|
|
2516
|
+
sealed.streaming = false;
|
|
2517
|
+
emitMessage(sealed);
|
|
2539
2518
|
}
|
|
2540
|
-
}
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
// First check if parser already extracted text during streaming
|
|
2555
|
-
const currentExtractedText = parser.getExtractedText();
|
|
2556
|
-
const rawBuffer = rawContentBuffers.get(assistant.id);
|
|
2557
|
-
const contentToProcess = rawBuffer ?? ensureStringContent(finalContent);
|
|
2558
|
-
|
|
2559
|
-
// Always set rawContent so action parsers can access the raw JSON
|
|
2560
|
-
assistant.rawContent = contentToProcess;
|
|
2561
|
-
|
|
2562
|
-
if (currentExtractedText !== null && currentExtractedText.trim() !== "") {
|
|
2563
|
-
// We already have extracted text from streaming - use it
|
|
2564
|
-
assistant.content = currentExtractedText;
|
|
2565
|
-
hasExtractedText = true;
|
|
2566
|
-
} else {
|
|
2567
|
-
// No extracted text yet - try to extract from final content
|
|
2568
|
-
|
|
2569
|
-
// Try fast path first
|
|
2570
|
-
const extractedText = extractTextFromJson(contentToProcess);
|
|
2571
|
-
if (extractedText !== null) {
|
|
2572
|
-
assistant.content = extractedText;
|
|
2573
|
-
hasExtractedText = true;
|
|
2519
|
+
} else {
|
|
2520
|
+
// Buffered / dispatch-mode step: no streamed text block, but the step
|
|
2521
|
+
// carries the final response (and/or a stopReason) — render it as the
|
|
2522
|
+
// assistant message. An empty response + stopReason still surfaces a
|
|
2523
|
+
// sealed bubble so the UI can show an affordance.
|
|
2524
|
+
const hasResponse =
|
|
2525
|
+
finalResponse !== undefined &&
|
|
2526
|
+
finalResponse !== null &&
|
|
2527
|
+
finalResponse !== "";
|
|
2528
|
+
if (hasResponse || flowStopReason) {
|
|
2529
|
+
const assistant = ensureAssistantMessage();
|
|
2530
|
+
if (flowStopReason) assistant.stopReason = flowStopReason;
|
|
2531
|
+
if (hasResponse) {
|
|
2532
|
+
finalizeFlowTextBlock(assistant, finalResponse);
|
|
2574
2533
|
} else {
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
if (parsedResult instanceof Promise) {
|
|
2578
|
-
asyncPending = true;
|
|
2579
|
-
parsedResult.then((result) => {
|
|
2580
|
-
// Extract text from result (could be string or object)
|
|
2581
|
-
const text = typeof result === 'string' ? result : result?.text ?? null;
|
|
2582
|
-
|
|
2583
|
-
if (text !== null && text.trim() !== "") {
|
|
2584
|
-
const currentAssistant = assistantMessage;
|
|
2585
|
-
if (currentAssistant && currentAssistant.id === assistant.id) {
|
|
2586
|
-
currentAssistant.content = text;
|
|
2587
|
-
currentAssistant.streaming = false;
|
|
2588
|
-
// Clean up
|
|
2589
|
-
streamParsers.delete(currentAssistant.id);
|
|
2590
|
-
rawContentBuffers.delete(currentAssistant.id);
|
|
2591
|
-
emitMessage(currentAssistant);
|
|
2592
|
-
}
|
|
2593
|
-
} else {
|
|
2594
|
-
// No extracted text - check if we should show raw content
|
|
2595
|
-
const finalExtractedText = parser.getExtractedText();
|
|
2596
|
-
const currentAssistant = assistantMessage;
|
|
2597
|
-
if (currentAssistant && currentAssistant.id === assistant.id) {
|
|
2598
|
-
if (finalExtractedText !== null && finalExtractedText.trim() !== "") {
|
|
2599
|
-
currentAssistant.content = finalExtractedText;
|
|
2600
|
-
} else if (!rawContentBuffers.has(currentAssistant.id)) {
|
|
2601
|
-
// Only show raw content if we never had any extracted text
|
|
2602
|
-
currentAssistant.content = ensureStringContent(finalContent);
|
|
2603
|
-
}
|
|
2604
|
-
currentAssistant.streaming = false;
|
|
2605
|
-
// Clean up
|
|
2606
|
-
streamParsers.delete(currentAssistant.id);
|
|
2607
|
-
rawContentBuffers.delete(currentAssistant.id);
|
|
2608
|
-
emitMessage(currentAssistant);
|
|
2609
|
-
}
|
|
2610
|
-
}
|
|
2611
|
-
});
|
|
2612
|
-
} else {
|
|
2613
|
-
// Extract text from synchronous result
|
|
2614
|
-
const text = typeof parsedResult === 'string' ? parsedResult : parsedResult?.text ?? null;
|
|
2615
|
-
|
|
2616
|
-
if (text !== null && text.trim() !== "") {
|
|
2617
|
-
assistant.content = text;
|
|
2618
|
-
hasExtractedText = true;
|
|
2619
|
-
} else {
|
|
2620
|
-
// Check stub one more time
|
|
2621
|
-
const finalExtractedText = parser.getExtractedText();
|
|
2622
|
-
if (finalExtractedText !== null && finalExtractedText.trim() !== "") {
|
|
2623
|
-
assistant.content = finalExtractedText;
|
|
2624
|
-
hasExtractedText = true;
|
|
2625
|
-
}
|
|
2626
|
-
}
|
|
2627
|
-
}
|
|
2628
|
-
}
|
|
2629
|
-
}
|
|
2630
|
-
}
|
|
2631
|
-
|
|
2632
|
-
// Skip sync emit if we're waiting on async parser
|
|
2633
|
-
if (!asyncPending) {
|
|
2634
|
-
// Ensure rawContent is set even if there's no parser (for action parsing)
|
|
2635
|
-
if (!assistant.rawContent) {
|
|
2636
|
-
const rawBuffer = rawContentBuffers.get(assistant.id);
|
|
2637
|
-
assistant.rawContent = rawBuffer ?? ensureStringContent(finalContent);
|
|
2638
|
-
}
|
|
2639
|
-
|
|
2640
|
-
// Only show raw content if we never extracted any text and no buffer was used
|
|
2641
|
-
if (!hasExtractedText && !rawContentBuffers.has(assistant.id)) {
|
|
2642
|
-
// No extracted text and no streaming happened - show raw content
|
|
2643
|
-
assistant.content = ensureStringContent(finalContent);
|
|
2644
|
-
}
|
|
2645
|
-
|
|
2646
|
-
// Clean up parser and buffer
|
|
2647
|
-
if (parser) {
|
|
2648
|
-
const closeResult = parser.close?.();
|
|
2649
|
-
if (closeResult instanceof Promise) {
|
|
2650
|
-
closeResult.catch(() => {});
|
|
2651
|
-
}
|
|
2652
|
-
}
|
|
2653
|
-
streamParsers.delete(assistant.id);
|
|
2654
|
-
rawContentBuffers.delete(assistant.id);
|
|
2655
|
-
assistant.streaming = false;
|
|
2656
|
-
emitMessage(assistant);
|
|
2657
|
-
}
|
|
2658
|
-
} else {
|
|
2659
|
-
// No final content, just mark as complete and clean up
|
|
2660
|
-
streamParsers.delete(assistant.id);
|
|
2661
|
-
rawContentBuffers.delete(assistant.id);
|
|
2662
|
-
assistant.streaming = false;
|
|
2663
|
-
emitMessage(assistant);
|
|
2664
|
-
}
|
|
2665
|
-
} else if (payloadType === "flow_complete") {
|
|
2666
|
-
const finalContent = payload.result?.response;
|
|
2667
|
-
if (didSplitByPartId) {
|
|
2668
|
-
// Content was split into multiple assistant messages: the full response
|
|
2669
|
-
// in flow_complete would overwrite the last segment. Just finalize streaming.
|
|
2670
|
-
if (assistantMessage !== null) {
|
|
2671
|
-
const msg: AgentWidgetMessage = assistantMessage;
|
|
2672
|
-
streamParsers.delete(msg.id);
|
|
2673
|
-
rawContentBuffers.delete(msg.id);
|
|
2674
|
-
if (msg.streaming !== false) {
|
|
2675
|
-
msg.streaming = false;
|
|
2676
|
-
emitMessage(msg);
|
|
2677
|
-
}
|
|
2678
|
-
}
|
|
2679
|
-
} else if (finalContent !== undefined && finalContent !== null) {
|
|
2680
|
-
const assistant = ensureAssistantMessage();
|
|
2681
|
-
// Check if we have raw content buffer that needs final processing
|
|
2682
|
-
const rawBuffer = rawContentBuffers.get(assistant.id);
|
|
2683
|
-
const stringContent = rawBuffer ?? ensureStringContent(finalContent);
|
|
2684
|
-
assistant.rawContent = stringContent;
|
|
2685
|
-
// Try to extract text from structured content
|
|
2686
|
-
let displayContent = ensureStringContent(finalContent);
|
|
2687
|
-
const parser = streamParsers.get(assistant.id);
|
|
2688
|
-
if (parser) {
|
|
2689
|
-
const extractedText = extractTextFromJson(stringContent);
|
|
2690
|
-
if (extractedText !== null) {
|
|
2691
|
-
displayContent = extractedText;
|
|
2692
|
-
} else {
|
|
2693
|
-
// Try parser if it exists
|
|
2694
|
-
const parsedResult = parser.processChunk(stringContent);
|
|
2695
|
-
if (parsedResult instanceof Promise) {
|
|
2696
|
-
parsedResult.then((result) => {
|
|
2697
|
-
// Extract text from result (could be string or object)
|
|
2698
|
-
const text = typeof result === 'string' ? result : result?.text ?? null;
|
|
2699
|
-
if (text !== null) {
|
|
2700
|
-
assistant.content = text;
|
|
2701
|
-
assistant.streaming = false;
|
|
2702
|
-
emitMessage(assistant);
|
|
2703
|
-
}
|
|
2704
|
-
});
|
|
2705
|
-
}
|
|
2706
|
-
const currentText = parser.getExtractedText();
|
|
2707
|
-
if (currentText !== null) {
|
|
2708
|
-
displayContent = currentText;
|
|
2534
|
+
assistant.streaming = false;
|
|
2535
|
+
emitMessage(assistant);
|
|
2709
2536
|
}
|
|
2710
2537
|
}
|
|
2711
2538
|
}
|
|
2712
|
-
|
|
2713
|
-
streamParsers.delete(assistant.id);
|
|
2714
|
-
rawContentBuffers.delete(assistant.id);
|
|
2715
|
-
|
|
2716
|
-
// Only emit if something actually changed to avoid flicker
|
|
2717
|
-
const contentChanged = displayContent !== assistant.content;
|
|
2718
|
-
const streamingChanged = assistant.streaming !== false;
|
|
2719
|
-
|
|
2720
|
-
if (contentChanged) {
|
|
2721
|
-
assistant.content = displayContent;
|
|
2722
|
-
}
|
|
2723
|
-
assistant.streaming = false;
|
|
2724
|
-
|
|
2725
|
-
// Only emit if content or streaming state changed
|
|
2726
|
-
if (contentChanged || streamingChanged) {
|
|
2727
|
-
emitMessage(assistant);
|
|
2728
|
-
}
|
|
2729
|
-
} else {
|
|
2730
|
-
// No final content, just mark as complete and clean up
|
|
2731
|
-
if (assistantMessage !== null) {
|
|
2732
|
-
// Clean up any remaining parsers/buffers
|
|
2733
|
-
// TypeScript narrowing issue - assistantMessage is checked for null above
|
|
2734
|
-
const msg: AgentWidgetMessage = assistantMessage;
|
|
2735
|
-
streamParsers.delete(msg.id);
|
|
2736
|
-
rawContentBuffers.delete(msg.id);
|
|
2737
|
-
// Only emit if streaming state changed
|
|
2738
|
-
if (msg.streaming !== false) {
|
|
2739
|
-
msg.streaming = false;
|
|
2740
|
-
emitMessage(msg);
|
|
2741
|
-
}
|
|
2742
|
-
}
|
|
2539
|
+
continue;
|
|
2743
2540
|
}
|
|
2744
|
-
onEvent({ type: "status", status: "idle" });
|
|
2745
2541
|
// ================================================================
|
|
2746
2542
|
// Agent Loop Execution Events
|
|
2747
2543
|
// ================================================================
|
|
2748
|
-
} else if (payloadType === "
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
agentName: payload.agentName ?? '',
|
|
2753
|
-
status: 'running',
|
|
2754
|
-
currentIteration: 0,
|
|
2755
|
-
maxTurns: payload.maxTurns ?? 1,
|
|
2756
|
-
startedAt: resolveTimestamp(payload.startedAt)
|
|
2757
|
-
};
|
|
2758
|
-
} else if (payloadType === "agent_iteration_start") {
|
|
2759
|
-
if (agentExecution) {
|
|
2760
|
-
agentExecution.currentIteration = payload.iteration;
|
|
2761
|
-
}
|
|
2762
|
-
|
|
2763
|
-
// In 'separate' mode, finalize previous iteration's message and create a new one
|
|
2764
|
-
if (iterationDisplay === 'separate' && payload.iteration > 1) {
|
|
2765
|
-
const prevMsg = assistantMessage as AgentWidgetMessage | null;
|
|
2766
|
-
if (prevMsg) {
|
|
2767
|
-
prevMsg.streaming = false;
|
|
2768
|
-
emitMessage(prevMsg);
|
|
2769
|
-
// Store the completed message for this iteration
|
|
2770
|
-
agentIterationMessages.set(payload.iteration - 1, prevMsg);
|
|
2771
|
-
// Reset assistant message so ensureAssistantMessage creates a new one
|
|
2772
|
-
assistantMessage = null;
|
|
2773
|
-
}
|
|
2774
|
-
}
|
|
2775
|
-
} else if (payloadType === "agent_turn_start") {
|
|
2776
|
-
// Reset the per-turn assistant tracker. lastAssistantInTurn is
|
|
2777
|
-
// used by agent_turn_complete to attach stopReason to the final
|
|
2778
|
-
// text segment of the turn even if that segment was sealed by an
|
|
2779
|
-
// intervening tool-call boundary.
|
|
2780
|
-
lastAssistantInTurn = null;
|
|
2781
|
-
} else if (payloadType === "agent_turn_delta") {
|
|
2782
|
-
if (payload.contentType === 'text') {
|
|
2783
|
-
// Stream text to assistant message
|
|
2784
|
-
const assistant = ensureAssistantMessage();
|
|
2785
|
-
assistant.content += payload.delta ?? '';
|
|
2786
|
-
assistant.agentMetadata = {
|
|
2787
|
-
executionId: payload.executionId,
|
|
2788
|
-
iteration: payload.iteration,
|
|
2789
|
-
turnId: payload.turnId,
|
|
2790
|
-
agentName: agentExecution?.agentName
|
|
2791
|
-
};
|
|
2792
|
-
lastAssistantInTurn = assistant;
|
|
2793
|
-
emitMessage(assistant);
|
|
2794
|
-
} else if (payload.contentType === 'thinking') {
|
|
2795
|
-
// Stream thinking content to a reasoning message
|
|
2796
|
-
const reasoningId = payload.turnId ?? `agent-think-${payload.iteration}`;
|
|
2797
|
-
const reasoningMessage = ensureReasoningMessage(reasoningId);
|
|
2798
|
-
reasoningMessage.reasoning = reasoningMessage.reasoning ?? {
|
|
2799
|
-
id: reasoningId,
|
|
2800
|
-
status: "streaming",
|
|
2801
|
-
chunks: []
|
|
2802
|
-
};
|
|
2803
|
-
reasoningMessage.reasoning.chunks.push(payload.delta ?? '');
|
|
2804
|
-
reasoningMessage.agentMetadata = {
|
|
2544
|
+
} else if (payloadType === "execution_start") {
|
|
2545
|
+
executionKind = payload.kind === "flow" ? "flow" : "agent";
|
|
2546
|
+
if (executionKind === "agent") {
|
|
2547
|
+
agentExecution = {
|
|
2805
2548
|
executionId: payload.executionId,
|
|
2806
|
-
|
|
2807
|
-
|
|
2549
|
+
agentId: payload.agentId ?? 'virtual',
|
|
2550
|
+
agentName: payload.agentName ?? '',
|
|
2551
|
+
status: 'running',
|
|
2552
|
+
currentIteration: 0,
|
|
2553
|
+
maxTurns: payload.maxTurns ?? 1,
|
|
2554
|
+
startedAt: resolveTimestamp(payload.startedAt)
|
|
2808
2555
|
};
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2556
|
+
}
|
|
2557
|
+
} else if (payloadType === "turn_start") {
|
|
2558
|
+
// Unified collapsed `agent_iteration_*` into a denormalized `iteration`
|
|
2559
|
+
// field on the turn (merged spec §2). Reconstruct the per-iteration
|
|
2560
|
+
// message boundary the 'separate' renderer keys off: when the iteration
|
|
2561
|
+
// advances, seal the previous iteration's bubble and rotate to a new one.
|
|
2562
|
+
const iteration =
|
|
2563
|
+
typeof payload.iteration === "number" ? payload.iteration : lastIterationSeen;
|
|
2564
|
+
if (iteration !== lastIterationSeen) {
|
|
2565
|
+
if (agentExecution) agentExecution.currentIteration = iteration;
|
|
2566
|
+
if (iterationDisplay === 'separate' && iteration > 1) {
|
|
2567
|
+
const prevMsg = assistantMessage as AgentWidgetMessage | null;
|
|
2568
|
+
if (prevMsg) {
|
|
2569
|
+
prevMsg.streaming = false;
|
|
2570
|
+
emitMessage(prevMsg);
|
|
2571
|
+
agentIterationMessages.set(iteration - 1, prevMsg);
|
|
2572
|
+
assistantMessage = null;
|
|
2819
2573
|
}
|
|
2820
2574
|
}
|
|
2575
|
+
lastIterationSeen = iteration;
|
|
2821
2576
|
}
|
|
2822
|
-
|
|
2823
|
-
//
|
|
2824
|
-
|
|
2825
|
-
if
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
emitMessage(
|
|
2577
|
+
openTurnId = typeof payload.id === "string" ? payload.id : null;
|
|
2578
|
+
// Reset the per-turn assistant tracker. lastAssistantInTurn is used by
|
|
2579
|
+
// turn_complete to attach stopReason to the final text segment of the
|
|
2580
|
+
// turn even if that segment was sealed by an intervening tool boundary.
|
|
2581
|
+
lastAssistantInTurn = null;
|
|
2582
|
+
} else if (payloadType === "tool_input_delta") {
|
|
2583
|
+
// Streamed tool arguments (display-only; authoritative args ride
|
|
2584
|
+
// tool_input_complete / tool_start).
|
|
2585
|
+
const toolId = payload.toolCallId ?? toolContext.lastId;
|
|
2586
|
+
if (toolId) {
|
|
2587
|
+
const toolMessage = toolMessages.get(toolId);
|
|
2588
|
+
if (toolMessage?.toolCall) {
|
|
2589
|
+
toolMessage.toolCall.chunks = toolMessage.toolCall.chunks ?? [];
|
|
2590
|
+
toolMessage.toolCall.chunks.push(payload.delta ?? '');
|
|
2591
|
+
emitMessage(toolMessage);
|
|
2837
2592
|
}
|
|
2838
2593
|
}
|
|
2839
|
-
|
|
2840
|
-
//
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
// sealed
|
|
2844
|
-
// attaches
|
|
2594
|
+
} else if (payloadType === "tool_input_complete") {
|
|
2595
|
+
// Authoritative args are set at tool_start; nothing to render here.
|
|
2596
|
+
continue;
|
|
2597
|
+
} else if (payloadType === "turn_complete") {
|
|
2598
|
+
// Reasoning is sealed by its own reasoning_complete in the unified
|
|
2599
|
+
// vocabulary; this only attaches the turn-level stopReason to the
|
|
2600
|
+
// assistant message produced by this turn. Falls back to
|
|
2601
|
+
// lastAssistantInTurn when the bubble was sealed at a tool boundary
|
|
2602
|
+
// mid-turn, so the notice still attaches to the final visible segment.
|
|
2845
2603
|
const turnStopReason = (payload as any).stopReason as
|
|
2846
2604
|
| StopReasonKind
|
|
2847
2605
|
| undefined;
|
|
2848
2606
|
const stopReasonTarget = assistantMessage ?? lastAssistantInTurn;
|
|
2849
2607
|
if (turnStopReason && stopReasonTarget !== null) {
|
|
2850
|
-
const turnId = payload.
|
|
2608
|
+
const turnId = payload.id;
|
|
2851
2609
|
const matchesTurn =
|
|
2852
2610
|
!turnId || stopReasonTarget.agentMetadata?.turnId === turnId;
|
|
2853
2611
|
if (matchesTurn) {
|
|
@@ -2855,83 +2613,58 @@ export class AgentWidgetClient {
|
|
|
2855
2613
|
emitMessage(stopReasonTarget);
|
|
2856
2614
|
}
|
|
2857
2615
|
}
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
//
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
const
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
result: undefined, duration: undefined, startedAt: undefined,
|
|
2876
|
-
completedAt: undefined, durationMs: undefined
|
|
2877
|
-
};
|
|
2878
|
-
tool.name = payload.toolName ?? payload.name ?? tool.name;
|
|
2879
|
-
tool.status = "running";
|
|
2880
|
-
if (payload.parameters !== undefined) {
|
|
2881
|
-
tool.args = payload.parameters;
|
|
2882
|
-
}
|
|
2883
|
-
tool.startedAt = resolveTimestamp(payload.startedAt ?? payload.timestamp);
|
|
2884
|
-
toolMessage.toolCall = tool;
|
|
2885
|
-
toolMessage.streaming = true;
|
|
2886
|
-
toolMessage.agentMetadata = {
|
|
2887
|
-
executionId: payload.executionId,
|
|
2888
|
-
iteration: payload.iteration
|
|
2889
|
-
};
|
|
2890
|
-
emitMessage(toolMessage);
|
|
2891
|
-
} else if (payloadType === "agent_tool_delta") {
|
|
2892
|
-
const toolId = payload.toolCallId ?? toolContext.lastId;
|
|
2893
|
-
if (toolId) {
|
|
2894
|
-
const toolMessage = toolMessages.get(toolId) ?? ensureToolMessage(toolId);
|
|
2895
|
-
if (toolMessage.toolCall) {
|
|
2896
|
-
toolMessage.toolCall.chunks = toolMessage.toolCall.chunks ?? [];
|
|
2897
|
-
toolMessage.toolCall.chunks.push(payload.delta ?? '');
|
|
2898
|
-
toolMessage.toolCall.status = "running";
|
|
2899
|
-
toolMessage.streaming = true;
|
|
2900
|
-
emitMessage(toolMessage);
|
|
2901
|
-
}
|
|
2902
|
-
}
|
|
2903
|
-
} else if (payloadType === "agent_tool_complete") {
|
|
2904
|
-
const toolId = payload.toolCallId ?? toolContext.lastId;
|
|
2905
|
-
if (toolId) {
|
|
2906
|
-
const toolMessage = toolMessages.get(toolId) ?? ensureToolMessage(toolId);
|
|
2907
|
-
if (toolMessage.toolCall) {
|
|
2908
|
-
toolMessage.toolCall.status = "complete";
|
|
2909
|
-
if (payload.result !== undefined) {
|
|
2910
|
-
toolMessage.toolCall.result = payload.result;
|
|
2911
|
-
}
|
|
2912
|
-
if (typeof payload.executionTime === "number") {
|
|
2913
|
-
toolMessage.toolCall.durationMs = payload.executionTime;
|
|
2914
|
-
}
|
|
2915
|
-
toolMessage.toolCall.completedAt = resolveTimestamp(payload.completedAt ?? payload.timestamp);
|
|
2916
|
-
toolMessage.streaming = false;
|
|
2917
|
-
emitMessage(toolMessage);
|
|
2918
|
-
const callKey = getToolCallKey(payload);
|
|
2919
|
-
if (callKey) {
|
|
2920
|
-
toolContext.byCall.delete(callKey);
|
|
2921
|
-
}
|
|
2922
|
-
}
|
|
2923
|
-
}
|
|
2924
|
-
} else if (payloadType === "agent_media") {
|
|
2925
|
-
// A tool produced media (image / audio / video / file). Render it
|
|
2926
|
-
// as a synthetic assistant message inserted at the point the tool
|
|
2927
|
-
// completed: between the tool bubble and the next text turn.
|
|
2928
|
-
//
|
|
2929
|
-
// Wire format is the AI SDK–aligned `MediaContentPart` from
|
|
2930
|
-
// @runtypelabs/shared:
|
|
2616
|
+
if (openTurnId === payload.id) openTurnId = null;
|
|
2617
|
+
} else if (payloadType === "media_start") {
|
|
2618
|
+
// Open a unified media block; buffer fragments until media_complete.
|
|
2619
|
+
const id = String(payload.id);
|
|
2620
|
+
mediaBuffers.set(id, {
|
|
2621
|
+
mediaType: typeof payload.mediaType === "string" ? payload.mediaType : undefined,
|
|
2622
|
+
role: typeof payload.role === "string" ? payload.role : undefined,
|
|
2623
|
+
toolCallId: payload.toolCallId,
|
|
2624
|
+
parts: [],
|
|
2625
|
+
});
|
|
2626
|
+
} else if (payloadType === "media_delta") {
|
|
2627
|
+
const buf = mediaBuffers.get(String(payload.id));
|
|
2628
|
+
if (buf && typeof payload.delta === "string") buf.parts.push(payload.delta);
|
|
2629
|
+
} else if (payloadType === "media_complete") {
|
|
2630
|
+
// Reassemble the buffered media triad into a single AI SDK–aligned
|
|
2631
|
+
// `MediaContentPart`, then render it as a synthetic assistant message
|
|
2632
|
+
// inserted between the tool bubble and the next text turn:
|
|
2931
2633
|
// { type: 'media', data, mediaType } // AI SDK v6: base64
|
|
2932
2634
|
// { type: 'image-url', url, mediaType? } // AI SDK v3/v4
|
|
2933
2635
|
// { type: 'file-url', url, mediaType } // AI SDK v3/v4
|
|
2934
|
-
const
|
|
2636
|
+
const mediaBlockId = String(payload.id);
|
|
2637
|
+
const buf = mediaBuffers.get(mediaBlockId);
|
|
2638
|
+
mediaBuffers.delete(mediaBlockId);
|
|
2639
|
+
const completeMediaType =
|
|
2640
|
+
(typeof payload.mediaType === "string" ? payload.mediaType : undefined) ??
|
|
2641
|
+
buf?.mediaType ??
|
|
2642
|
+
"application/octet-stream";
|
|
2643
|
+
const completeData = typeof payload.data === "string" ? payload.data : undefined;
|
|
2644
|
+
const completeUrl =
|
|
2645
|
+
typeof payload.url === "string"
|
|
2646
|
+
? payload.url
|
|
2647
|
+
: buf && buf.parts.length > 0
|
|
2648
|
+
? buf.parts.join("")
|
|
2649
|
+
: undefined;
|
|
2650
|
+
let reconstructed: Record<string, unknown> | null = null;
|
|
2651
|
+
if (completeData) {
|
|
2652
|
+
reconstructed = { type: "media", data: completeData, mediaType: completeMediaType };
|
|
2653
|
+
} else if (completeUrl) {
|
|
2654
|
+
// The unified wire is mediaType-only; a URL part with no declared MIME
|
|
2655
|
+
// arrives as the bare bucket hint "image" (per the API encoder). Treat
|
|
2656
|
+
// that — and any real `image/*` — as a hosted image so we don't misroute
|
|
2657
|
+
// generated images into the file bucket.
|
|
2658
|
+
const lower = completeMediaType.toLowerCase();
|
|
2659
|
+
const isImage = lower === "image" || lower.startsWith("image/");
|
|
2660
|
+
reconstructed = {
|
|
2661
|
+
type: isImage ? "image-url" : "file-url",
|
|
2662
|
+
url: completeUrl,
|
|
2663
|
+
mediaType: completeMediaType,
|
|
2664
|
+
};
|
|
2665
|
+
}
|
|
2666
|
+
const mediaToolCallId = payload.toolCallId ?? buf?.toolCallId;
|
|
2667
|
+
const rawMedia = reconstructed ? [reconstructed] : [];
|
|
2935
2668
|
const mediaContentParts: ContentPart[] = [];
|
|
2936
2669
|
for (const part of rawMedia) {
|
|
2937
2670
|
if (!part || typeof part !== "object") continue;
|
|
@@ -2975,7 +2708,9 @@ export class AgentWidgetClient {
|
|
|
2975
2708
|
mediaContentParts.push({
|
|
2976
2709
|
type: "image",
|
|
2977
2710
|
image: src,
|
|
2978
|
-
|
|
2711
|
+
// Only a real MIME (`image/png`) is a usable mimeType; the bare
|
|
2712
|
+
// bucket hint "image" (a hosted URL with no declared type) is not.
|
|
2713
|
+
...(mediaType.includes("/") ? { mimeType: mediaType } : {}),
|
|
2979
2714
|
});
|
|
2980
2715
|
} else if (mediaType.startsWith("audio/")) {
|
|
2981
2716
|
mediaContentParts.push({
|
|
@@ -3006,7 +2741,7 @@ export class AgentWidgetClient {
|
|
|
3006
2741
|
// sharing an id would let `emitMessage` merge them by id and
|
|
3007
2742
|
// overwrite the prior `contentParts`.
|
|
3008
2743
|
const seq = nextSequence();
|
|
3009
|
-
const toolCallIdRaw =
|
|
2744
|
+
const toolCallIdRaw = mediaToolCallId;
|
|
3010
2745
|
const mediaIdSuffix =
|
|
3011
2746
|
typeof toolCallIdRaw === "string" && toolCallIdRaw.length > 0
|
|
3012
2747
|
? `${toolCallIdRaw}-${seq}`
|
|
@@ -3021,7 +2756,12 @@ export class AgentWidgetClient {
|
|
|
3021
2756
|
sequence: seq,
|
|
3022
2757
|
agentMetadata: {
|
|
3023
2758
|
executionId: payload.executionId,
|
|
3024
|
-
iteration
|
|
2759
|
+
// Media blocks carry no iteration of their own; stamp the
|
|
2760
|
+
// enclosing iteration tracked from turn/tool frames.
|
|
2761
|
+
iteration:
|
|
2762
|
+
typeof payload.iteration === "number"
|
|
2763
|
+
? payload.iteration
|
|
2764
|
+
: lastIterationSeen,
|
|
3025
2765
|
},
|
|
3026
2766
|
};
|
|
3027
2767
|
emitMessage(mediaMessage);
|
|
@@ -3039,67 +2779,49 @@ export class AgentWidgetClient {
|
|
|
3039
2779
|
assistantMessage = null;
|
|
3040
2780
|
assistantMessageRef.current = null;
|
|
3041
2781
|
}
|
|
3042
|
-
} else if (payloadType === "
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
} else if (payloadType === "agent_reflection" || payloadType === "agent_reflect") {
|
|
3046
|
-
// Create a reasoning message for reflection content
|
|
3047
|
-
const reflectionId = `agent-reflection-${payload.executionId}-${payload.iteration}`;
|
|
3048
|
-
const reflectionMessage: AgentWidgetMessage = {
|
|
3049
|
-
id: reflectionId,
|
|
3050
|
-
role: "assistant",
|
|
3051
|
-
content: payload.reflection ?? '',
|
|
3052
|
-
createdAt: new Date().toISOString(),
|
|
3053
|
-
streaming: false,
|
|
3054
|
-
variant: "reasoning",
|
|
3055
|
-
sequence: nextSequence(),
|
|
3056
|
-
reasoning: {
|
|
3057
|
-
id: reflectionId,
|
|
3058
|
-
status: "complete",
|
|
3059
|
-
chunks: [payload.reflection ?? '']
|
|
3060
|
-
},
|
|
3061
|
-
agentMetadata: {
|
|
3062
|
-
executionId: payload.executionId,
|
|
3063
|
-
iteration: payload.iteration
|
|
3064
|
-
}
|
|
3065
|
-
};
|
|
3066
|
-
emitMessage(reflectionMessage);
|
|
3067
|
-
} else if (payloadType === "agent_complete") {
|
|
3068
|
-
if (agentExecution) {
|
|
2782
|
+
} else if (payloadType === "execution_complete") {
|
|
2783
|
+
const kind = payload.kind ?? executionKind;
|
|
2784
|
+
if (kind === "agent" && agentExecution) {
|
|
3069
2785
|
agentExecution.status = payload.success ? 'complete' : 'error';
|
|
3070
2786
|
agentExecution.completedAt = resolveTimestamp(payload.completedAt);
|
|
3071
2787
|
agentExecution.stopReason = payload.stopReason;
|
|
3072
2788
|
}
|
|
3073
2789
|
|
|
3074
|
-
// Finalize
|
|
2790
|
+
// Finalize any still-open assistant message. Per-step reconciliation
|
|
2791
|
+
// (step_complete.result.response) normally sealed the flow blocks
|
|
2792
|
+
// already; this is the defensive close for an unterminated block, and
|
|
2793
|
+
// for flow it runs the final structured extraction off the raw buffer.
|
|
3075
2794
|
const finalMsg = assistantMessage as AgentWidgetMessage | null;
|
|
3076
2795
|
if (finalMsg) {
|
|
3077
|
-
finalMsg.streaming
|
|
3078
|
-
|
|
2796
|
+
if (kind === "flow" && finalMsg.streaming !== false) {
|
|
2797
|
+
finalizeFlowTextBlock(finalMsg);
|
|
2798
|
+
} else {
|
|
2799
|
+
finalMsg.streaming = false;
|
|
2800
|
+
emitMessage(finalMsg);
|
|
2801
|
+
}
|
|
2802
|
+
assistantMessage = null;
|
|
3079
2803
|
}
|
|
2804
|
+
currentTextBlockId = null;
|
|
2805
|
+
pendingFlowRaw = "";
|
|
2806
|
+
lastSealedFlowBubble = null;
|
|
3080
2807
|
|
|
3081
2808
|
onEvent({ type: "status", status: "idle" });
|
|
3082
|
-
} else if (payloadType === "
|
|
2809
|
+
} else if (payloadType === "execution_error") {
|
|
2810
|
+
// Terminal failure. The unified non-terminal `error` is handled
|
|
2811
|
+
// separately (recoverable → warn).
|
|
3083
2812
|
const errorMessage = typeof payload.error === 'string'
|
|
3084
2813
|
? payload.error
|
|
3085
|
-
: payload.error?.message ?? '
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
} else {
|
|
3092
|
-
onEvent({
|
|
3093
|
-
type: "error",
|
|
3094
|
-
error: new Error(errorMessage)
|
|
3095
|
-
});
|
|
3096
|
-
}
|
|
3097
|
-
} else if (payloadType === "agent_ping") {
|
|
2814
|
+
: payload.error?.message ?? 'Execution error';
|
|
2815
|
+
onEvent({
|
|
2816
|
+
type: "error",
|
|
2817
|
+
error: new Error(errorMessage)
|
|
2818
|
+
});
|
|
2819
|
+
} else if (payloadType === "ping") {
|
|
3098
2820
|
// Keep-alive heartbeat - no action needed
|
|
3099
2821
|
// ================================================================
|
|
3100
2822
|
// Tool Approval Events
|
|
3101
2823
|
// ================================================================
|
|
3102
|
-
} else if (payloadType === "
|
|
2824
|
+
} else if (payloadType === "approval_start") {
|
|
3103
2825
|
const approvalId = payload.approvalId ?? `approval-${nextSequence()}`;
|
|
3104
2826
|
const approvalMessage: AgentWidgetMessage = {
|
|
3105
2827
|
id: `approval-${approvalId}`,
|
|
@@ -3149,7 +2871,7 @@ export class AgentWidgetClient {
|
|
|
3149
2871
|
},
|
|
3150
2872
|
};
|
|
3151
2873
|
emitMessage(approvalMessage);
|
|
3152
|
-
} else if (payloadType === "
|
|
2874
|
+
} else if (payloadType === "approval_complete") {
|
|
3153
2875
|
const approvalId = payload.approvalId;
|
|
3154
2876
|
if (approvalId) {
|
|
3155
2877
|
// Find and update the existing approval message
|
|
@@ -3294,8 +3016,33 @@ export class AgentWidgetClient {
|
|
|
3294
3016
|
assistantMessageRef.current = null;
|
|
3295
3017
|
streamParsers.delete(id);
|
|
3296
3018
|
rawContentBuffers.delete(id);
|
|
3019
|
+
} else if (payloadType === "error") {
|
|
3020
|
+
// Unified non-terminal error (merged spec). A bare `error` is
|
|
3021
|
+
// recoverable by default — a transient notice such as "rate limited,
|
|
3022
|
+
// retrying" — and the execution continues, so it must NOT surface as a
|
|
3023
|
+
// fatal error or finalize the stream. The API routes terminal failures
|
|
3024
|
+
// through `execution_error`. Only an explicit `recoverable: false`
|
|
3025
|
+
// promotes a unified `error` to terminal.
|
|
3026
|
+
if (
|
|
3027
|
+
payload.recoverable === false &&
|
|
3028
|
+
payload.error != null &&
|
|
3029
|
+
payload.error !== ""
|
|
3030
|
+
) {
|
|
3031
|
+
const errorMessage =
|
|
3032
|
+
typeof payload.error === "string"
|
|
3033
|
+
? payload.error
|
|
3034
|
+
: (payload.error as { message?: unknown })?.message != null
|
|
3035
|
+
? String((payload.error as { message?: unknown }).message)
|
|
3036
|
+
: "Execution error";
|
|
3037
|
+
onEvent({ type: "error", error: new Error(errorMessage) });
|
|
3038
|
+
const finalMsg = assistantMessage as AgentWidgetMessage | null;
|
|
3039
|
+
if (finalMsg && finalMsg.streaming) {
|
|
3040
|
+
finalMsg.streaming = false;
|
|
3041
|
+
emitMessage(finalMsg);
|
|
3042
|
+
}
|
|
3043
|
+
onEvent({ type: "status", status: "idle" });
|
|
3044
|
+
}
|
|
3297
3045
|
} else if (
|
|
3298
|
-
payloadType === "error" ||
|
|
3299
3046
|
payloadType === "step_error" ||
|
|
3300
3047
|
payloadType === "dispatch_error" ||
|
|
3301
3048
|
payloadType === "flow_error"
|
|
@@ -3308,18 +3055,13 @@ export class AgentWidgetClient {
|
|
|
3308
3055
|
if (msg != null && msg !== "") {
|
|
3309
3056
|
resolvedError = new Error(String(msg));
|
|
3310
3057
|
}
|
|
3311
|
-
} else
|
|
3312
|
-
payloadType === "step_error" ||
|
|
3313
|
-
payloadType === "flow_error"
|
|
3314
|
-
) {
|
|
3058
|
+
} else {
|
|
3315
3059
|
const e = payload.error;
|
|
3316
3060
|
if (typeof e === "string" && e !== "") {
|
|
3317
3061
|
resolvedError = new Error(e);
|
|
3318
3062
|
} else if (e != null && typeof e === "object" && "message" in e) {
|
|
3319
3063
|
resolvedError = new Error(String((e as { message?: unknown }).message ?? e));
|
|
3320
3064
|
}
|
|
3321
|
-
} else if (payloadType === "error" && payload.error != null && payload.error !== "") {
|
|
3322
|
-
resolvedError = new Error(String(payload.error));
|
|
3323
3065
|
}
|
|
3324
3066
|
|
|
3325
3067
|
if (resolvedError) {
|
|
@@ -3389,7 +3131,7 @@ export class AgentWidgetClient {
|
|
|
3389
3131
|
assistantMessageRef,
|
|
3390
3132
|
emitMessage,
|
|
3391
3133
|
nextSequence,
|
|
3392
|
-
|
|
3134
|
+
customParsePartId
|
|
3393
3135
|
);
|
|
3394
3136
|
// Update assistantMessage from ref (in case it was created or replaced by partId segmentation)
|
|
3395
3137
|
if (assistantMessageRef.current && assistantMessageRef.current !== assistantMessage) {
|
|
@@ -3398,39 +3140,14 @@ export class AgentWidgetClient {
|
|
|
3398
3140
|
if (handled) continue; // Skip default handling if custom handler processed it
|
|
3399
3141
|
}
|
|
3400
3142
|
|
|
3401
|
-
//
|
|
3402
|
-
//
|
|
3403
|
-
//
|
|
3404
|
-
|
|
3405
|
-
if (!wireModeResolved) {
|
|
3406
|
-
if (isUnifiedLifecycleStart(payloadType)) {
|
|
3407
|
-
unifiedMode = true;
|
|
3408
|
-
wireModeResolved = true;
|
|
3409
|
-
} else if (
|
|
3410
|
-
payloadType === "agent_start" ||
|
|
3411
|
-
payloadType === "flow_start" ||
|
|
3412
|
-
payloadType === "step_start"
|
|
3413
|
-
) {
|
|
3414
|
-
unifiedMode = false;
|
|
3415
|
-
wireModeResolved = true;
|
|
3416
|
-
}
|
|
3417
|
-
}
|
|
3418
|
-
if (unifiedMode) {
|
|
3419
|
-
for (const legacyEvent of unifiedBridge.push(payloadType, payload)) {
|
|
3420
|
-
seqReadyQueue.push(legacyEvent);
|
|
3421
|
-
}
|
|
3422
|
-
drainReadyQueue();
|
|
3423
|
-
continue;
|
|
3424
|
-
}
|
|
3425
|
-
|
|
3426
|
-
// Push through the sequence reorder buffer
|
|
3427
|
-
seqBuffer.push(payloadType, payload);
|
|
3143
|
+
// The wire is the neutral unified vocabulary; the handler consumes it
|
|
3144
|
+
// natively. The stream is single-connection and in order, so each frame
|
|
3145
|
+
// drains straight through.
|
|
3146
|
+
seqReadyQueue.push({ payloadType, payload });
|
|
3428
3147
|
drainReadyQueue();
|
|
3429
3148
|
}
|
|
3430
3149
|
}
|
|
3431
3150
|
|
|
3432
|
-
seqBuffer.flushPending();
|
|
3433
3151
|
drainReadyQueue();
|
|
3434
|
-
seqBuffer.destroy();
|
|
3435
3152
|
}
|
|
3436
3153
|
}
|