@runtypelabs/persona 3.36.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +47 -47
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +328 -821
- package/dist/index.d.ts +328 -821
- package/dist/index.global.js +39 -39
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +47 -47
- 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 -181
- package/dist/smart-dom-reader.d.ts +268 -181
- package/dist/theme-editor-preview.cjs +41 -41
- package/dist/theme-editor-preview.d.cts +268 -181
- package/dist/theme-editor-preview.d.ts +268 -181
- package/dist/theme-editor-preview.js +41 -41
- package/dist/theme-editor.d.cts +268 -181
- package/dist/theme-editor.d.ts +268 -181
- package/package.json +1 -1
- package/src/client.test.ts +506 -1003
- package/src/client.ts +676 -909
- package/src/components/tool-bubble.ts +46 -33
- package/src/generated/runtype-openapi-contract.ts +211 -714
- package/src/index-core.ts +2 -3
- package/src/install.test.ts +1 -34
- package/src/install.ts +1 -22
- 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 -2
- package/src/ui.postprocess.test.ts +107 -0
- package/src/ui.ts +40 -5
- package/src/utils/__fixtures__/unified-translator.oracle.ts +717 -0
- 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/client.ts
CHANGED
|
@@ -32,7 +32,6 @@ import {
|
|
|
32
32
|
createRegexJsonParser,
|
|
33
33
|
createXmlParser
|
|
34
34
|
} from "./utils/formatting";
|
|
35
|
-
import { SequenceReorderBuffer } from "./utils/sequence-buffer";
|
|
36
35
|
import { VERSION } from "./version";
|
|
37
36
|
// artifactsSidebarEnabled is used in ui.ts to gate the sidebar pane rendering;
|
|
38
37
|
// artifact events are always processed here regardless of config.
|
|
@@ -1378,21 +1377,26 @@ export class AgentWidgetClient {
|
|
|
1378
1377
|
let lastAssistantInTurn: AgentWidgetMessage | null = null;
|
|
1379
1378
|
// Reference to track assistant message for custom event handler
|
|
1380
1379
|
const assistantMessageRef = { current: null as AgentWidgetMessage | null };
|
|
1381
|
-
//
|
|
1382
|
-
|
|
1383
|
-
|
|
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>();
|
|
1384
1398
|
const reasoningMessages = new Map<string, AgentWidgetMessage>();
|
|
1385
1399
|
const toolMessages = new Map<string, AgentWidgetMessage>();
|
|
1386
|
-
// Messages produced by steps inside a nested flow executed as a tool.
|
|
1387
|
-
// Keyed by `${parentToolId}::${nestedStepId}::${partId}` so each nested
|
|
1388
|
-
// step (send-stream, prompt) gets its own assistant message, and prompts
|
|
1389
|
-
// with inner tool calls split into one message per text segment: still
|
|
1390
|
-
// attributable to the parent tool call.
|
|
1391
|
-
const nestedStepMessages = new Map<string, AgentWidgetMessage>();
|
|
1392
|
-
// Most-recent partId seen for a given `${toolId}::${stepId}` scope, used
|
|
1393
|
-
// to seal the previous segment when a new partId arrives within the
|
|
1394
|
-
// same nested prompt step.
|
|
1395
|
-
const nestedPartIdByStep = new Map<string, string>();
|
|
1396
1400
|
const reasoningContext = {
|
|
1397
1401
|
lastId: null as string | null,
|
|
1398
1402
|
byStep: new Map<string, string>()
|
|
@@ -1402,49 +1406,6 @@ export class AgentWidgetClient {
|
|
|
1402
1406
|
byCall: new Map<string, string>()
|
|
1403
1407
|
};
|
|
1404
1408
|
|
|
1405
|
-
// Nested message key. partId defaults to "" so steps without segmentation
|
|
1406
|
-
// (e.g. send-stream) still have a deterministic single key.
|
|
1407
|
-
const getNestedStepKey = (
|
|
1408
|
-
toolId: string,
|
|
1409
|
-
stepId: string,
|
|
1410
|
-
partId: string = ""
|
|
1411
|
-
) => `${toolId}::${stepId}::${partId}`;
|
|
1412
|
-
|
|
1413
|
-
// Prefix used to sweep every nested message belonging to a single
|
|
1414
|
-
// (toolId, stepId) scope: needed on step_complete to seal any segments
|
|
1415
|
-
// that are still streaming.
|
|
1416
|
-
const getNestedStepPrefix = (toolId: string, stepId: string) =>
|
|
1417
|
-
`${toolId}::${stepId}::`;
|
|
1418
|
-
|
|
1419
|
-
const ensureNestedStepMessage = (
|
|
1420
|
-
toolId: string,
|
|
1421
|
-
stepId: string,
|
|
1422
|
-
partId: string,
|
|
1423
|
-
executionId?: string
|
|
1424
|
-
): AgentWidgetMessage => {
|
|
1425
|
-
const key = getNestedStepKey(toolId, stepId, partId);
|
|
1426
|
-
const existing = nestedStepMessages.get(key);
|
|
1427
|
-
if (existing) return existing;
|
|
1428
|
-
const idSuffix = partId ? `-${partId}` : "";
|
|
1429
|
-
const message: AgentWidgetMessage = {
|
|
1430
|
-
id: `nested-${toolId}-${stepId}${idSuffix}`,
|
|
1431
|
-
role: "assistant",
|
|
1432
|
-
content: "",
|
|
1433
|
-
createdAt: new Date().toISOString(),
|
|
1434
|
-
streaming: true,
|
|
1435
|
-
sequence: nextSequence(),
|
|
1436
|
-
...(partId ? { partId } : {}),
|
|
1437
|
-
agentMetadata: {
|
|
1438
|
-
executionId,
|
|
1439
|
-
parentToolId: toolId,
|
|
1440
|
-
parentStepId: stepId,
|
|
1441
|
-
},
|
|
1442
|
-
};
|
|
1443
|
-
nestedStepMessages.set(key, message);
|
|
1444
|
-
emitMessage(message);
|
|
1445
|
-
return message;
|
|
1446
|
-
};
|
|
1447
|
-
|
|
1448
1409
|
const normalizeKey = (value: unknown): string | null => {
|
|
1449
1410
|
if (value === null || value === undefined) return null;
|
|
1450
1411
|
try {
|
|
@@ -1482,11 +1443,12 @@ export class AgentWidgetClient {
|
|
|
1482
1443
|
const ensureAssistantMessage = () => {
|
|
1483
1444
|
if (assistantMessage) return assistantMessage;
|
|
1484
1445
|
let id: string;
|
|
1446
|
+
const segment = currentTextBlockId;
|
|
1485
1447
|
if (!assistantIdConsumed && baseAssistantId) {
|
|
1486
1448
|
id = baseAssistantId;
|
|
1487
1449
|
assistantIdConsumed = true;
|
|
1488
|
-
} else if (baseAssistantId &&
|
|
1489
|
-
id = `${baseAssistantId}_${
|
|
1450
|
+
} else if (baseAssistantId && segment) {
|
|
1451
|
+
id = `${baseAssistantId}_${segment}`;
|
|
1490
1452
|
} else {
|
|
1491
1453
|
id = `assistant-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
1492
1454
|
}
|
|
@@ -1678,11 +1640,6 @@ export class AgentWidgetClient {
|
|
|
1678
1640
|
// Rebuild incremental text by sequence so late arrivals can repair already-emitted
|
|
1679
1641
|
// content after the reorder buffer's gap-timeout flush.
|
|
1680
1642
|
const orderedChunkBuffers = new Map<string, Array<{ seq: number; text: string }>>();
|
|
1681
|
-
const assistantMessagesByPartId = new Map<string, AgentWidgetMessage>();
|
|
1682
|
-
// Only the most-recently sealed segment is reconciled with step_complete's
|
|
1683
|
-
// final response. Earlier segments rely on their own async parser microtasks
|
|
1684
|
-
// resolving via the closure-captured `assistant` variable.
|
|
1685
|
-
let lastSealedTextSegment: AgentWidgetMessage | null = null;
|
|
1686
1643
|
|
|
1687
1644
|
const insertOrderedChunk = (key: string, seq: number, text: string): string => {
|
|
1688
1645
|
let chunks = orderedChunkBuffers.get(key);
|
|
@@ -1807,47 +1764,241 @@ export class AgentWidgetClient {
|
|
|
1807
1764
|
finalizeCleanup();
|
|
1808
1765
|
};
|
|
1809
1766
|
|
|
1810
|
-
//
|
|
1811
|
-
//
|
|
1812
|
-
//
|
|
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.
|
|
1813
1955
|
const seqReadyQueue: Array<{ payloadType: string; payload: any }> = [];
|
|
1814
|
-
|
|
1815
|
-
//
|
|
1816
|
-
// 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).
|
|
1817
1958
|
let drainReadyQueue: () => void;
|
|
1818
|
-
//
|
|
1819
|
-
//
|
|
1820
|
-
//
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
});
|
|
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;
|
|
1836
1976
|
// Agent execution state tracking
|
|
1837
1977
|
let agentExecution: AgentExecutionState | null = null;
|
|
1838
1978
|
// Track assistant messages per agent iteration for 'separate' mode
|
|
1839
1979
|
const agentIterationMessages = new Map<number, AgentWidgetMessage>();
|
|
1840
1980
|
const iterationDisplay = this.config.iterationDisplay ?? 'separate';
|
|
1841
1981
|
|
|
1842
|
-
// Drains
|
|
1843
|
-
// 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
|
|
1844
1984
|
// end-of-stream are processed.
|
|
1845
1985
|
drainReadyQueue = () => {
|
|
1846
1986
|
for (let i = 0; i < seqReadyQueue.length; i++) {
|
|
1847
1987
|
const payloadType = seqReadyQueue[i].payloadType;
|
|
1848
1988
|
const payload = seqReadyQueue[i].payload;
|
|
1849
1989
|
|
|
1850
|
-
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
|
+
}
|
|
1851
2002
|
const reasoningId =
|
|
1852
2003
|
resolveReasoningId(payload, true) ?? `reason-${nextSequence()}`;
|
|
1853
2004
|
const reasoningMessage = ensureReasoningMessage(reasoningId);
|
|
@@ -1861,10 +2012,29 @@ export class AgentWidgetClient {
|
|
|
1861
2012
|
resolveTimestamp(payload.startedAt ?? payload.timestamp);
|
|
1862
2013
|
reasoningMessage.reasoning.completedAt = undefined;
|
|
1863
2014
|
reasoningMessage.reasoning.durationMs = undefined;
|
|
2015
|
+
if (payload.scope === "loop" || payload.scope === "turn") {
|
|
2016
|
+
reasoningMessage.reasoning.scope = payload.scope;
|
|
2017
|
+
}
|
|
1864
2018
|
reasoningMessage.streaming = true;
|
|
1865
2019
|
reasoningMessage.reasoning.status = "streaming";
|
|
1866
2020
|
emitMessage(reasoningMessage);
|
|
1867
|
-
} 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
|
+
}
|
|
1868
2038
|
const reasoningId =
|
|
1869
2039
|
resolveReasoningId(payload, false) ??
|
|
1870
2040
|
resolveReasoningId(payload, true) ??
|
|
@@ -1908,13 +2078,51 @@ export class AgentWidgetClient {
|
|
|
1908
2078
|
}
|
|
1909
2079
|
reasoningMessage.streaming = reasoningMessage.reasoning.status !== "complete";
|
|
1910
2080
|
emitMessage(reasoningMessage);
|
|
1911
|
-
} 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
|
+
}
|
|
1912
2104
|
const reasoningId =
|
|
1913
2105
|
resolveReasoningId(payload, false) ??
|
|
1914
2106
|
resolveReasoningId(payload, true) ??
|
|
1915
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
|
+
}
|
|
1916
2118
|
const reasoningMessage = reasoningMessages.get(reasoningId);
|
|
1917
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
|
+
}
|
|
1918
2126
|
reasoningMessage.reasoning.status = "complete";
|
|
1919
2127
|
reasoningMessage.reasoning.completedAt = resolveTimestamp(
|
|
1920
2128
|
payload.completedAt ?? payload.timestamp
|
|
@@ -1933,14 +2141,30 @@ export class AgentWidgetClient {
|
|
|
1933
2141
|
reasoningContext.byStep.delete(stepKey);
|
|
1934
2142
|
}
|
|
1935
2143
|
} else if (payloadType === "tool_start") {
|
|
1936
|
-
|
|
1937
|
-
|
|
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()}`;
|
|
1938
2161
|
const toolName = payload.toolName ?? payload.name;
|
|
1939
2162
|
// Suppress tool UI for artifact emit tools: artifacts are handled via artifact_* events
|
|
1940
2163
|
if (isArtifactEmitToolName(toolName)) {
|
|
1941
2164
|
artifactToolCallIds.add(toolId);
|
|
1942
2165
|
continue;
|
|
1943
2166
|
}
|
|
2167
|
+
trackToolId(getToolCallKey(payload), toolId);
|
|
1944
2168
|
const toolMessage = ensureToolMessage(toolId);
|
|
1945
2169
|
const tool = toolMessage.toolCall ?? {
|
|
1946
2170
|
id: toolId,
|
|
@@ -1948,10 +2172,10 @@ export class AgentWidgetClient {
|
|
|
1948
2172
|
};
|
|
1949
2173
|
tool.name = toolName ?? tool.name;
|
|
1950
2174
|
tool.status = "running";
|
|
1951
|
-
if (payload.
|
|
1952
|
-
tool.args = payload.args;
|
|
1953
|
-
} else if (payload.parameters !== undefined) {
|
|
2175
|
+
if (payload.parameters !== undefined) {
|
|
1954
2176
|
tool.args = payload.parameters;
|
|
2177
|
+
} else if (payload.args !== undefined) {
|
|
2178
|
+
tool.args = payload.args;
|
|
1955
2179
|
}
|
|
1956
2180
|
tool.startedAt =
|
|
1957
2181
|
tool.startedAt ??
|
|
@@ -1960,15 +2184,14 @@ export class AgentWidgetClient {
|
|
|
1960
2184
|
tool.durationMs = undefined;
|
|
1961
2185
|
toolMessage.toolCall = tool;
|
|
1962
2186
|
toolMessage.streaming = true;
|
|
1963
|
-
|
|
1964
|
-
if (agentCtx || payload.executionId) {
|
|
2187
|
+
if (payload.executionId) {
|
|
1965
2188
|
toolMessage.agentMetadata = {
|
|
1966
|
-
executionId:
|
|
1967
|
-
iteration:
|
|
2189
|
+
executionId: payload.executionId,
|
|
2190
|
+
iteration: payload.iteration,
|
|
1968
2191
|
};
|
|
1969
2192
|
}
|
|
1970
2193
|
emitMessage(toolMessage);
|
|
1971
|
-
} else if (payloadType === "
|
|
2194
|
+
} else if (payloadType === "tool_output_delta") {
|
|
1972
2195
|
const toolId =
|
|
1973
2196
|
resolveToolId(payload, false) ??
|
|
1974
2197
|
resolveToolId(payload, true) ??
|
|
@@ -2047,11 +2270,17 @@ export class AgentWidgetClient {
|
|
|
2047
2270
|
if (callKey) {
|
|
2048
2271
|
toolContext.byCall.delete(callKey);
|
|
2049
2272
|
}
|
|
2050
|
-
} else if (payloadType === "
|
|
2051
|
-
// LOCAL tool pause.
|
|
2052
|
-
//
|
|
2053
|
-
//
|
|
2054
|
-
//
|
|
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.
|
|
2055
2284
|
//
|
|
2056
2285
|
// Upsert a fully-populated tool-variant message so the existing
|
|
2057
2286
|
// ask_user_question bubble + sheet paths fire. Mark the message with
|
|
@@ -2074,7 +2303,15 @@ export class AgentWidgetClient {
|
|
|
2074
2303
|
const toolId =
|
|
2075
2304
|
toolCallId ?? (payload.toolId as string) ?? `local-${nextSequence()}`;
|
|
2076
2305
|
const toolMessage = ensureToolMessage(toolId);
|
|
2077
|
-
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;
|
|
2078
2315
|
const webMcpTool = isWebMcpToolName(toolName);
|
|
2079
2316
|
const tool = toolMessage.toolCall ?? { id: toolId, status: "pending" as const };
|
|
2080
2317
|
tool.name = toolName;
|
|
@@ -2088,7 +2325,8 @@ export class AgentWidgetClient {
|
|
|
2088
2325
|
tool.status = webMcpTool ? "running" : "complete";
|
|
2089
2326
|
tool.chunks = tool.chunks ?? [];
|
|
2090
2327
|
tool.startedAt =
|
|
2091
|
-
tool.startedAt ??
|
|
2328
|
+
tool.startedAt ??
|
|
2329
|
+
resolveTimestamp(payload.startedAt ?? payload.timestamp ?? payload.awaitedAt);
|
|
2092
2330
|
if (webMcpTool) {
|
|
2093
2331
|
tool.completedAt = undefined;
|
|
2094
2332
|
tool.duration = undefined;
|
|
@@ -2109,339 +2347,123 @@ export class AgentWidgetClient {
|
|
|
2109
2347
|
};
|
|
2110
2348
|
emitMessage(toolMessage);
|
|
2111
2349
|
} else if (payloadType === "text_start") {
|
|
2112
|
-
//
|
|
2113
|
-
//
|
|
2114
|
-
//
|
|
2115
|
-
//
|
|
2116
|
-
|
|
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);
|
|
2117
2361
|
continue;
|
|
2118
2362
|
}
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
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 {
|
|
2123
2375
|
prev.streaming = false;
|
|
2124
2376
|
emitMessage(prev);
|
|
2125
|
-
lastSealedTextSegment = prev;
|
|
2126
|
-
assistantMessage = null;
|
|
2127
|
-
didSplitByPartId = true;
|
|
2128
2377
|
}
|
|
2129
|
-
}
|
|
2130
|
-
if (incomingPartId !== undefined) {
|
|
2131
|
-
partIdState.current = incomingPartId;
|
|
2132
|
-
}
|
|
2133
|
-
} else if (payloadType === "text_end") {
|
|
2134
|
-
// Lifecycle event: current text segment ended (tool call about to start).
|
|
2135
|
-
// When toolContext is present the boundary belongs to a nested flow: leave
|
|
2136
|
-
// outer assistant state alone so the outer stream is never interrupted by
|
|
2137
|
-
// nested activity.
|
|
2138
|
-
if ((payload as any).toolContext?.toolId) {
|
|
2139
|
-
continue;
|
|
2140
|
-
}
|
|
2141
|
-
// Seal the current assistant message so the next segment gets a new one
|
|
2142
|
-
const prev = assistantMessage as AgentWidgetMessage | null;
|
|
2143
|
-
if (prev) {
|
|
2144
|
-
prev.streaming = false;
|
|
2145
|
-
emitMessage(prev);
|
|
2146
|
-
lastSealedTextSegment = prev;
|
|
2147
2378
|
assistantMessage = null;
|
|
2148
|
-
didSplitByPartId = true;
|
|
2149
2379
|
}
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
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);
|
|
2156
2403
|
continue;
|
|
2157
2404
|
}
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
);
|
|
2174
|
-
|
|
2175
|
-
payload.partId !== undefined && payload.partId !== null
|
|
2176
|
-
? String(payload.partId)
|
|
2177
|
-
: "";
|
|
2178
|
-
const stepScopeKey = `${nestedToolCtx.toolId}::${nestedStepId}`;
|
|
2179
|
-
const prevPartId = nestedPartIdByStep.get(stepScopeKey);
|
|
2180
|
-
|
|
2181
|
-
// If partId changed within this nested step (prompt with inner
|
|
2182
|
-
// tool call emitting a new text segment), seal the previous
|
|
2183
|
-
// segment's message so each segment renders as its own bubble.
|
|
2184
|
-
if (
|
|
2185
|
-
incomingPartId !== "" &&
|
|
2186
|
-
prevPartId !== undefined &&
|
|
2187
|
-
prevPartId !== "" &&
|
|
2188
|
-
prevPartId !== incomingPartId
|
|
2189
|
-
) {
|
|
2190
|
-
const prev = nestedStepMessages.get(
|
|
2191
|
-
getNestedStepKey(
|
|
2192
|
-
nestedToolCtx.toolId,
|
|
2193
|
-
nestedStepId,
|
|
2194
|
-
prevPartId
|
|
2195
|
-
)
|
|
2196
|
-
);
|
|
2197
|
-
if (prev && prev.streaming !== false) {
|
|
2198
|
-
prev.streaming = false;
|
|
2199
|
-
emitMessage(prev);
|
|
2200
|
-
}
|
|
2201
|
-
}
|
|
2202
|
-
if (incomingPartId !== "") {
|
|
2203
|
-
nestedPartIdByStep.set(stepScopeKey, incomingPartId);
|
|
2204
|
-
}
|
|
2205
|
-
|
|
2206
|
-
const nestedMsg = ensureNestedStepMessage(
|
|
2207
|
-
nestedToolCtx.toolId,
|
|
2208
|
-
nestedStepId,
|
|
2209
|
-
incomingPartId,
|
|
2210
|
-
nestedToolCtx.executionId
|
|
2211
|
-
);
|
|
2212
|
-
const nestedChunk =
|
|
2213
|
-
payload.text ??
|
|
2214
|
-
payload.delta ??
|
|
2215
|
-
payload.content ??
|
|
2216
|
-
payload.chunk ??
|
|
2217
|
-
"";
|
|
2218
|
-
if (nestedChunk) {
|
|
2219
|
-
nestedMsg.content += String(nestedChunk);
|
|
2220
|
-
nestedMsg.streaming = true;
|
|
2221
|
-
emitMessage(nestedMsg);
|
|
2222
|
-
}
|
|
2223
|
-
if (payload.isComplete) {
|
|
2224
|
-
nestedMsg.streaming = false;
|
|
2225
|
-
emitMessage(nestedMsg);
|
|
2226
|
-
}
|
|
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;
|
|
2227
2422
|
continue;
|
|
2228
2423
|
}
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
? (assistantMessagesByPartId.get(incomingPartId) ?? ensureAssistantMessage())
|
|
2250
|
-
: ensureAssistantMessage();
|
|
2251
|
-
if (incomingPartId !== undefined) {
|
|
2252
|
-
if (!assistant.partId) {
|
|
2253
|
-
assistant.partId = incomingPartId;
|
|
2254
|
-
}
|
|
2255
|
-
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;
|
|
2256
2444
|
}
|
|
2257
|
-
//
|
|
2258
|
-
const
|
|
2259
|
-
if (
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
const chunkSeq = typeof payload.seq === "number" ? payload.seq : undefined;
|
|
2266
|
-
const chunkBufferKey = incomingPartId ?? assistant.id;
|
|
2267
|
-
const accumulatedRaw =
|
|
2268
|
-
chunkSeq !== undefined
|
|
2269
|
-
? insertOrderedChunk(chunkBufferKey, chunkSeq, String(chunk))
|
|
2270
|
-
: (rawContentBuffers.get(assistant.id) ?? "") + chunk;
|
|
2271
|
-
// Store raw content for action parsing, but NEVER set assistant.content to raw JSON
|
|
2272
|
-
assistant.rawContent = accumulatedRaw;
|
|
2273
|
-
|
|
2274
|
-
// Use stream parser to parse
|
|
2275
|
-
if (!streamParsers.has(assistant.id)) {
|
|
2276
|
-
streamParsers.set(assistant.id, this.createStreamParser());
|
|
2277
|
-
}
|
|
2278
|
-
const parser = streamParsers.get(assistant.id)!;
|
|
2279
|
-
|
|
2280
|
-
// Check if content looks like JSON
|
|
2281
|
-
const looksLikeJson = accumulatedRaw.trim().startsWith('{') || accumulatedRaw.trim().startsWith('[');
|
|
2282
|
-
|
|
2283
|
-
// Store raw buffer before processing (needed for step_complete handler)
|
|
2284
|
-
if (looksLikeJson) {
|
|
2285
|
-
rawContentBuffers.set(assistant.id, accumulatedRaw);
|
|
2286
|
-
}
|
|
2287
|
-
|
|
2288
|
-
// Check if this is a plain text parser (marked with __isPlainTextParser)
|
|
2289
|
-
const isPlainTextParser = (parser as any).__isPlainTextParser === true;
|
|
2290
|
-
|
|
2291
|
-
// If plain text parser, just append the chunk directly
|
|
2292
|
-
if (isPlainTextParser) {
|
|
2293
|
-
assistant.content = chunkSeq !== undefined ? accumulatedRaw : assistant.content + chunk;
|
|
2294
|
-
// Clear any raw buffer/parser since we're in plain text mode
|
|
2295
|
-
rawContentBuffers.delete(assistant.id);
|
|
2296
|
-
streamParsers.delete(assistant.id);
|
|
2297
|
-
assistant.rawContent = undefined;
|
|
2298
|
-
emitMessage(assistant);
|
|
2299
|
-
continue;
|
|
2300
|
-
}
|
|
2301
|
-
|
|
2302
|
-
// Try to parse with the parser (for structured parsers)
|
|
2303
|
-
const parsedResult = parser.processChunk(accumulatedRaw);
|
|
2304
|
-
|
|
2305
|
-
// Handle async parser result
|
|
2306
|
-
if (parsedResult instanceof Promise) {
|
|
2307
|
-
parsedResult.then((result) => {
|
|
2308
|
-
// Extract text from result (could be string or object)
|
|
2309
|
-
const text = typeof result === 'string' ? result : result?.text ?? null;
|
|
2310
|
-
|
|
2311
|
-
if (text !== null && text.trim() !== "") {
|
|
2312
|
-
// Parser successfully extracted text: update the chunk's assistant
|
|
2313
|
-
// (not assistantMessage; text_end may have cleared that ref before microtasks run)
|
|
2314
|
-
assistant.content = text;
|
|
2315
|
-
emitMessage(assistant);
|
|
2316
|
-
} else if (!looksLikeJson && !accumulatedRaw.trim().startsWith('<')) {
|
|
2317
|
-
// Not a structured format - show as plain text
|
|
2318
|
-
const currentAssistant = assistantMessage;
|
|
2319
|
-
const targetAssistant =
|
|
2320
|
-
currentAssistant && currentAssistant.id === assistant.id
|
|
2321
|
-
? currentAssistant
|
|
2322
|
-
: assistant;
|
|
2323
|
-
if (targetAssistant.id === assistant.id) {
|
|
2324
|
-
targetAssistant.content =
|
|
2325
|
-
chunkSeq !== undefined ? accumulatedRaw : targetAssistant.content + chunk;
|
|
2326
|
-
rawContentBuffers.delete(targetAssistant.id);
|
|
2327
|
-
streamParsers.delete(targetAssistant.id);
|
|
2328
|
-
targetAssistant.rawContent = undefined;
|
|
2329
|
-
emitMessage(targetAssistant);
|
|
2330
|
-
}
|
|
2331
|
-
}
|
|
2332
|
-
// Otherwise wait for more chunks (incomplete structured format)
|
|
2333
|
-
// Don't emit message if parser hasn't extracted text yet
|
|
2334
|
-
}).catch(() => {
|
|
2335
|
-
// On error, treat as plain text
|
|
2336
|
-
assistant.content =
|
|
2337
|
-
chunkSeq !== undefined ? accumulatedRaw : assistant.content + chunk;
|
|
2338
|
-
rawContentBuffers.delete(assistant.id);
|
|
2339
|
-
streamParsers.delete(assistant.id);
|
|
2340
|
-
assistant.rawContent = undefined;
|
|
2341
|
-
emitMessage(assistant);
|
|
2342
|
-
});
|
|
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;
|
|
2343
2453
|
} else {
|
|
2344
|
-
//
|
|
2345
|
-
//
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
// Parser successfully extracted text
|
|
2350
|
-
// Buffer is already set above
|
|
2351
|
-
assistant.content = text;
|
|
2352
|
-
emitMessage(assistant);
|
|
2353
|
-
} else if (!looksLikeJson && !accumulatedRaw.trim().startsWith('<')) {
|
|
2354
|
-
// Not a structured format - show as plain text
|
|
2355
|
-
assistant.content =
|
|
2356
|
-
chunkSeq !== undefined ? accumulatedRaw : assistant.content + chunk;
|
|
2357
|
-
// Clear any raw buffer/parser if we were in structured format mode
|
|
2358
|
-
rawContentBuffers.delete(assistant.id);
|
|
2359
|
-
streamParsers.delete(assistant.id);
|
|
2360
|
-
assistant.rawContent = undefined;
|
|
2361
|
-
emitMessage(assistant);
|
|
2362
|
-
}
|
|
2363
|
-
// Otherwise wait for more chunks (incomplete structured format)
|
|
2364
|
-
// Don't emit message if parser hasn't extracted text yet
|
|
2365
|
-
}
|
|
2366
|
-
|
|
2367
|
-
// IMPORTANT: Don't call getExtractedText() and emit messages here
|
|
2368
|
-
// This was causing raw JSON to be displayed because getExtractedText()
|
|
2369
|
-
// wasn't extracting the "text" field correctly during streaming
|
|
2370
|
-
}
|
|
2371
|
-
if (payload.isComplete) {
|
|
2372
|
-
const finalContent = payload.result?.response ?? assistant.content;
|
|
2373
|
-
if (finalContent) {
|
|
2374
|
-
// Check if we have raw content buffer that needs final processing
|
|
2375
|
-
const rawBuffer = rawContentBuffers.get(assistant.id);
|
|
2376
|
-
const contentToProcess = rawBuffer ?? ensureStringContent(finalContent);
|
|
2377
|
-
assistant.rawContent = contentToProcess;
|
|
2378
|
-
|
|
2379
|
-
// Try to extract text from final structured content
|
|
2380
|
-
const parser = streamParsers.get(assistant.id);
|
|
2381
|
-
let extractedText: string | null = null;
|
|
2382
|
-
let asyncPending = false;
|
|
2383
|
-
|
|
2384
|
-
if (parser) {
|
|
2385
|
-
// First check if parser already has extracted text
|
|
2386
|
-
extractedText = parser.getExtractedText();
|
|
2387
|
-
|
|
2388
|
-
if (extractedText === null) {
|
|
2389
|
-
// Try extracting with regex
|
|
2390
|
-
extractedText = extractTextFromJson(contentToProcess);
|
|
2391
|
-
}
|
|
2392
|
-
|
|
2393
|
-
if (extractedText === null) {
|
|
2394
|
-
// Try parser.processChunk as last resort
|
|
2395
|
-
const parsedResult = parser.processChunk(contentToProcess);
|
|
2396
|
-
if (parsedResult instanceof Promise) {
|
|
2397
|
-
asyncPending = true;
|
|
2398
|
-
parsedResult.then((result) => {
|
|
2399
|
-
// Extract text from result (could be string or object)
|
|
2400
|
-
const text = typeof result === 'string' ? result : result?.text ?? null;
|
|
2401
|
-
if (text !== null) {
|
|
2402
|
-
const currentAssistant = assistantMessage;
|
|
2403
|
-
if (currentAssistant && currentAssistant.id === assistant.id) {
|
|
2404
|
-
currentAssistant.content = text;
|
|
2405
|
-
currentAssistant.streaming = false;
|
|
2406
|
-
// Clean up
|
|
2407
|
-
streamParsers.delete(currentAssistant.id);
|
|
2408
|
-
rawContentBuffers.delete(currentAssistant.id);
|
|
2409
|
-
emitMessage(currentAssistant);
|
|
2410
|
-
}
|
|
2411
|
-
}
|
|
2412
|
-
});
|
|
2413
|
-
} else {
|
|
2414
|
-
// Extract text from synchronous result
|
|
2415
|
-
extractedText = typeof parsedResult === 'string' ? parsedResult : parsedResult?.text ?? null;
|
|
2416
|
-
}
|
|
2417
|
-
}
|
|
2418
|
-
}
|
|
2419
|
-
|
|
2420
|
-
// Skip sync emit if we're waiting on async parser
|
|
2421
|
-
if (!asyncPending) {
|
|
2422
|
-
// Set content: use extracted text if available, otherwise use raw content
|
|
2423
|
-
if (extractedText !== null && extractedText.trim() !== "") {
|
|
2424
|
-
assistant.content = extractedText;
|
|
2425
|
-
} else if (!rawContentBuffers.has(assistant.id)) {
|
|
2426
|
-
// Only use raw final content if we didn't accumulate chunks
|
|
2427
|
-
assistant.content = ensureStringContent(finalContent);
|
|
2428
|
-
}
|
|
2429
|
-
|
|
2430
|
-
// Clean up parser and buffer
|
|
2431
|
-
const parserToClose = streamParsers.get(assistant.id);
|
|
2432
|
-
if (parserToClose) {
|
|
2433
|
-
const closeResult = parserToClose.close?.();
|
|
2434
|
-
if (closeResult instanceof Promise) {
|
|
2435
|
-
closeResult.catch(() => {});
|
|
2436
|
-
}
|
|
2437
|
-
streamParsers.delete(assistant.id);
|
|
2438
|
-
}
|
|
2439
|
-
rawContentBuffers.delete(assistant.id);
|
|
2440
|
-
assistant.streaming = false;
|
|
2441
|
-
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;
|
|
2442
2459
|
}
|
|
2460
|
+
prev.streaming = false;
|
|
2461
|
+
emitMessage(prev);
|
|
2443
2462
|
}
|
|
2463
|
+
assistantMessage = null;
|
|
2444
2464
|
}
|
|
2465
|
+
currentTextBlockId = null;
|
|
2466
|
+
pendingFlowRaw = "";
|
|
2445
2467
|
} else if (payloadType === "step_complete") {
|
|
2446
2468
|
// Only process completions for prompt steps, not tool/context steps
|
|
2447
2469
|
const stepType = (payload as any).stepType;
|
|
@@ -2451,378 +2473,139 @@ export class AgentWidgetClient {
|
|
|
2451
2473
|
continue;
|
|
2452
2474
|
}
|
|
2453
2475
|
|
|
2454
|
-
//
|
|
2455
|
-
//
|
|
2456
|
-
//
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
);
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
);
|
|
2471
|
-
for (const [key, msg] of nestedStepMessages) {
|
|
2472
|
-
if (key.startsWith(prefix) && msg.streaming !== false) {
|
|
2473
|
-
msg.streaming = false;
|
|
2474
|
-
emitMessage(msg);
|
|
2475
|
-
}
|
|
2476
|
-
}
|
|
2477
|
-
nestedPartIdByStep.delete(
|
|
2478
|
-
`${nestedCompleteCtx.toolId}::${nestedStepId}`
|
|
2479
|
-
);
|
|
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);
|
|
2480
2492
|
}
|
|
2493
|
+
onEvent({ type: "status", status: "idle" });
|
|
2481
2494
|
continue;
|
|
2482
2495
|
}
|
|
2483
2496
|
|
|
2484
|
-
//
|
|
2485
|
-
//
|
|
2486
|
-
//
|
|
2487
|
-
//
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
if (
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
if (
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
const splitFinalContent = payload.result?.response;
|
|
2506
|
-
const sealedForReconcile = lastSealedTextSegment;
|
|
2507
|
-
if (sealedForReconcile) {
|
|
2508
|
-
if (stepStopReason) sealedForReconcile.stopReason = stepStopReason;
|
|
2509
|
-
if (splitFinalContent !== undefined && splitFinalContent !== null) {
|
|
2510
|
-
reconcileSealedAssistantWithFinalResponse(sealedForReconcile, splitFinalContent);
|
|
2511
|
-
} else {
|
|
2512
|
-
streamParsers.delete(sealedForReconcile.id);
|
|
2513
|
-
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);
|
|
2514
2518
|
}
|
|
2515
|
-
}
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
// First check if parser already extracted text during streaming
|
|
2530
|
-
const currentExtractedText = parser.getExtractedText();
|
|
2531
|
-
const rawBuffer = rawContentBuffers.get(assistant.id);
|
|
2532
|
-
const contentToProcess = rawBuffer ?? ensureStringContent(finalContent);
|
|
2533
|
-
|
|
2534
|
-
// Always set rawContent so action parsers can access the raw JSON
|
|
2535
|
-
assistant.rawContent = contentToProcess;
|
|
2536
|
-
|
|
2537
|
-
if (currentExtractedText !== null && currentExtractedText.trim() !== "") {
|
|
2538
|
-
// We already have extracted text from streaming - use it
|
|
2539
|
-
assistant.content = currentExtractedText;
|
|
2540
|
-
hasExtractedText = true;
|
|
2541
|
-
} else {
|
|
2542
|
-
// No extracted text yet - try to extract from final content
|
|
2543
|
-
|
|
2544
|
-
// Try fast path first
|
|
2545
|
-
const extractedText = extractTextFromJson(contentToProcess);
|
|
2546
|
-
if (extractedText !== null) {
|
|
2547
|
-
assistant.content = extractedText;
|
|
2548
|
-
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);
|
|
2549
2533
|
} else {
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
if (parsedResult instanceof Promise) {
|
|
2553
|
-
asyncPending = true;
|
|
2554
|
-
parsedResult.then((result) => {
|
|
2555
|
-
// Extract text from result (could be string or object)
|
|
2556
|
-
const text = typeof result === 'string' ? result : result?.text ?? null;
|
|
2557
|
-
|
|
2558
|
-
if (text !== null && text.trim() !== "") {
|
|
2559
|
-
const currentAssistant = assistantMessage;
|
|
2560
|
-
if (currentAssistant && currentAssistant.id === assistant.id) {
|
|
2561
|
-
currentAssistant.content = text;
|
|
2562
|
-
currentAssistant.streaming = false;
|
|
2563
|
-
// Clean up
|
|
2564
|
-
streamParsers.delete(currentAssistant.id);
|
|
2565
|
-
rawContentBuffers.delete(currentAssistant.id);
|
|
2566
|
-
emitMessage(currentAssistant);
|
|
2567
|
-
}
|
|
2568
|
-
} else {
|
|
2569
|
-
// No extracted text - check if we should show raw content
|
|
2570
|
-
const finalExtractedText = parser.getExtractedText();
|
|
2571
|
-
const currentAssistant = assistantMessage;
|
|
2572
|
-
if (currentAssistant && currentAssistant.id === assistant.id) {
|
|
2573
|
-
if (finalExtractedText !== null && finalExtractedText.trim() !== "") {
|
|
2574
|
-
currentAssistant.content = finalExtractedText;
|
|
2575
|
-
} else if (!rawContentBuffers.has(currentAssistant.id)) {
|
|
2576
|
-
// Only show raw content if we never had any extracted text
|
|
2577
|
-
currentAssistant.content = ensureStringContent(finalContent);
|
|
2578
|
-
}
|
|
2579
|
-
currentAssistant.streaming = false;
|
|
2580
|
-
// Clean up
|
|
2581
|
-
streamParsers.delete(currentAssistant.id);
|
|
2582
|
-
rawContentBuffers.delete(currentAssistant.id);
|
|
2583
|
-
emitMessage(currentAssistant);
|
|
2584
|
-
}
|
|
2585
|
-
}
|
|
2586
|
-
});
|
|
2587
|
-
} else {
|
|
2588
|
-
// Extract text from synchronous result
|
|
2589
|
-
const text = typeof parsedResult === 'string' ? parsedResult : parsedResult?.text ?? null;
|
|
2590
|
-
|
|
2591
|
-
if (text !== null && text.trim() !== "") {
|
|
2592
|
-
assistant.content = text;
|
|
2593
|
-
hasExtractedText = true;
|
|
2594
|
-
} else {
|
|
2595
|
-
// Check stub one more time
|
|
2596
|
-
const finalExtractedText = parser.getExtractedText();
|
|
2597
|
-
if (finalExtractedText !== null && finalExtractedText.trim() !== "") {
|
|
2598
|
-
assistant.content = finalExtractedText;
|
|
2599
|
-
hasExtractedText = true;
|
|
2600
|
-
}
|
|
2601
|
-
}
|
|
2602
|
-
}
|
|
2603
|
-
}
|
|
2604
|
-
}
|
|
2605
|
-
}
|
|
2606
|
-
|
|
2607
|
-
// Skip sync emit if we're waiting on async parser
|
|
2608
|
-
if (!asyncPending) {
|
|
2609
|
-
// Ensure rawContent is set even if there's no parser (for action parsing)
|
|
2610
|
-
if (!assistant.rawContent) {
|
|
2611
|
-
const rawBuffer = rawContentBuffers.get(assistant.id);
|
|
2612
|
-
assistant.rawContent = rawBuffer ?? ensureStringContent(finalContent);
|
|
2613
|
-
}
|
|
2614
|
-
|
|
2615
|
-
// Only show raw content if we never extracted any text and no buffer was used
|
|
2616
|
-
if (!hasExtractedText && !rawContentBuffers.has(assistant.id)) {
|
|
2617
|
-
// No extracted text and no streaming happened - show raw content
|
|
2618
|
-
assistant.content = ensureStringContent(finalContent);
|
|
2619
|
-
}
|
|
2620
|
-
|
|
2621
|
-
// Clean up parser and buffer
|
|
2622
|
-
if (parser) {
|
|
2623
|
-
const closeResult = parser.close?.();
|
|
2624
|
-
if (closeResult instanceof Promise) {
|
|
2625
|
-
closeResult.catch(() => {});
|
|
2626
|
-
}
|
|
2627
|
-
}
|
|
2628
|
-
streamParsers.delete(assistant.id);
|
|
2629
|
-
rawContentBuffers.delete(assistant.id);
|
|
2630
|
-
assistant.streaming = false;
|
|
2631
|
-
emitMessage(assistant);
|
|
2632
|
-
}
|
|
2633
|
-
} else {
|
|
2634
|
-
// No final content, just mark as complete and clean up
|
|
2635
|
-
streamParsers.delete(assistant.id);
|
|
2636
|
-
rawContentBuffers.delete(assistant.id);
|
|
2637
|
-
assistant.streaming = false;
|
|
2638
|
-
emitMessage(assistant);
|
|
2639
|
-
}
|
|
2640
|
-
} else if (payloadType === "flow_complete") {
|
|
2641
|
-
const finalContent = payload.result?.response;
|
|
2642
|
-
if (didSplitByPartId) {
|
|
2643
|
-
// Content was split into multiple assistant messages: the full response
|
|
2644
|
-
// in flow_complete would overwrite the last segment. Just finalize streaming.
|
|
2645
|
-
if (assistantMessage !== null) {
|
|
2646
|
-
const msg: AgentWidgetMessage = assistantMessage;
|
|
2647
|
-
streamParsers.delete(msg.id);
|
|
2648
|
-
rawContentBuffers.delete(msg.id);
|
|
2649
|
-
if (msg.streaming !== false) {
|
|
2650
|
-
msg.streaming = false;
|
|
2651
|
-
emitMessage(msg);
|
|
2652
|
-
}
|
|
2653
|
-
}
|
|
2654
|
-
} else if (finalContent !== undefined && finalContent !== null) {
|
|
2655
|
-
const assistant = ensureAssistantMessage();
|
|
2656
|
-
// Check if we have raw content buffer that needs final processing
|
|
2657
|
-
const rawBuffer = rawContentBuffers.get(assistant.id);
|
|
2658
|
-
const stringContent = rawBuffer ?? ensureStringContent(finalContent);
|
|
2659
|
-
assistant.rawContent = stringContent;
|
|
2660
|
-
// Try to extract text from structured content
|
|
2661
|
-
let displayContent = ensureStringContent(finalContent);
|
|
2662
|
-
const parser = streamParsers.get(assistant.id);
|
|
2663
|
-
if (parser) {
|
|
2664
|
-
const extractedText = extractTextFromJson(stringContent);
|
|
2665
|
-
if (extractedText !== null) {
|
|
2666
|
-
displayContent = extractedText;
|
|
2667
|
-
} else {
|
|
2668
|
-
// Try parser if it exists
|
|
2669
|
-
const parsedResult = parser.processChunk(stringContent);
|
|
2670
|
-
if (parsedResult instanceof Promise) {
|
|
2671
|
-
parsedResult.then((result) => {
|
|
2672
|
-
// Extract text from result (could be string or object)
|
|
2673
|
-
const text = typeof result === 'string' ? result : result?.text ?? null;
|
|
2674
|
-
if (text !== null) {
|
|
2675
|
-
assistant.content = text;
|
|
2676
|
-
assistant.streaming = false;
|
|
2677
|
-
emitMessage(assistant);
|
|
2678
|
-
}
|
|
2679
|
-
});
|
|
2680
|
-
}
|
|
2681
|
-
const currentText = parser.getExtractedText();
|
|
2682
|
-
if (currentText !== null) {
|
|
2683
|
-
displayContent = currentText;
|
|
2534
|
+
assistant.streaming = false;
|
|
2535
|
+
emitMessage(assistant);
|
|
2684
2536
|
}
|
|
2685
2537
|
}
|
|
2686
2538
|
}
|
|
2687
|
-
|
|
2688
|
-
streamParsers.delete(assistant.id);
|
|
2689
|
-
rawContentBuffers.delete(assistant.id);
|
|
2690
|
-
|
|
2691
|
-
// Only emit if something actually changed to avoid flicker
|
|
2692
|
-
const contentChanged = displayContent !== assistant.content;
|
|
2693
|
-
const streamingChanged = assistant.streaming !== false;
|
|
2694
|
-
|
|
2695
|
-
if (contentChanged) {
|
|
2696
|
-
assistant.content = displayContent;
|
|
2697
|
-
}
|
|
2698
|
-
assistant.streaming = false;
|
|
2699
|
-
|
|
2700
|
-
// Only emit if content or streaming state changed
|
|
2701
|
-
if (contentChanged || streamingChanged) {
|
|
2702
|
-
emitMessage(assistant);
|
|
2703
|
-
}
|
|
2704
|
-
} else {
|
|
2705
|
-
// No final content, just mark as complete and clean up
|
|
2706
|
-
if (assistantMessage !== null) {
|
|
2707
|
-
// Clean up any remaining parsers/buffers
|
|
2708
|
-
// TypeScript narrowing issue - assistantMessage is checked for null above
|
|
2709
|
-
const msg: AgentWidgetMessage = assistantMessage;
|
|
2710
|
-
streamParsers.delete(msg.id);
|
|
2711
|
-
rawContentBuffers.delete(msg.id);
|
|
2712
|
-
// Only emit if streaming state changed
|
|
2713
|
-
if (msg.streaming !== false) {
|
|
2714
|
-
msg.streaming = false;
|
|
2715
|
-
emitMessage(msg);
|
|
2716
|
-
}
|
|
2717
|
-
}
|
|
2539
|
+
continue;
|
|
2718
2540
|
}
|
|
2719
|
-
onEvent({ type: "status", status: "idle" });
|
|
2720
2541
|
// ================================================================
|
|
2721
2542
|
// Agent Loop Execution Events
|
|
2722
2543
|
// ================================================================
|
|
2723
|
-
} else if (payloadType === "
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
agentName: payload.agentName ?? '',
|
|
2728
|
-
status: 'running',
|
|
2729
|
-
currentIteration: 0,
|
|
2730
|
-
maxTurns: payload.maxTurns ?? 1,
|
|
2731
|
-
startedAt: resolveTimestamp(payload.startedAt)
|
|
2732
|
-
};
|
|
2733
|
-
} else if (payloadType === "agent_iteration_start") {
|
|
2734
|
-
if (agentExecution) {
|
|
2735
|
-
agentExecution.currentIteration = payload.iteration;
|
|
2736
|
-
}
|
|
2737
|
-
|
|
2738
|
-
// In 'separate' mode, finalize previous iteration's message and create a new one
|
|
2739
|
-
if (iterationDisplay === 'separate' && payload.iteration > 1) {
|
|
2740
|
-
const prevMsg = assistantMessage as AgentWidgetMessage | null;
|
|
2741
|
-
if (prevMsg) {
|
|
2742
|
-
prevMsg.streaming = false;
|
|
2743
|
-
emitMessage(prevMsg);
|
|
2744
|
-
// Store the completed message for this iteration
|
|
2745
|
-
agentIterationMessages.set(payload.iteration - 1, prevMsg);
|
|
2746
|
-
// Reset assistant message so ensureAssistantMessage creates a new one
|
|
2747
|
-
assistantMessage = null;
|
|
2748
|
-
}
|
|
2749
|
-
}
|
|
2750
|
-
} else if (payloadType === "agent_turn_start") {
|
|
2751
|
-
// Reset the per-turn assistant tracker. lastAssistantInTurn is
|
|
2752
|
-
// used by agent_turn_complete to attach stopReason to the final
|
|
2753
|
-
// text segment of the turn even if that segment was sealed by an
|
|
2754
|
-
// intervening tool-call boundary.
|
|
2755
|
-
lastAssistantInTurn = null;
|
|
2756
|
-
} else if (payloadType === "agent_turn_delta") {
|
|
2757
|
-
if (payload.contentType === 'text') {
|
|
2758
|
-
// Stream text to assistant message
|
|
2759
|
-
const assistant = ensureAssistantMessage();
|
|
2760
|
-
assistant.content += payload.delta ?? '';
|
|
2761
|
-
assistant.agentMetadata = {
|
|
2544
|
+
} else if (payloadType === "execution_start") {
|
|
2545
|
+
executionKind = payload.kind === "flow" ? "flow" : "agent";
|
|
2546
|
+
if (executionKind === "agent") {
|
|
2547
|
+
agentExecution = {
|
|
2762
2548
|
executionId: payload.executionId,
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
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)
|
|
2766
2555
|
};
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
emitMessage(reasoningMessage);
|
|
2785
|
-
} else if (payload.contentType === 'tool_input') {
|
|
2786
|
-
// Stream tool input to current tool message
|
|
2787
|
-
const toolId = payload.toolCallId ?? toolContext.lastId;
|
|
2788
|
-
if (toolId) {
|
|
2789
|
-
const toolMessage = toolMessages.get(toolId);
|
|
2790
|
-
if (toolMessage?.toolCall) {
|
|
2791
|
-
toolMessage.toolCall.chunks = toolMessage.toolCall.chunks ?? [];
|
|
2792
|
-
toolMessage.toolCall.chunks.push(payload.delta ?? '');
|
|
2793
|
-
emitMessage(toolMessage);
|
|
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;
|
|
2794
2573
|
}
|
|
2795
2574
|
}
|
|
2575
|
+
lastIterationSeen = iteration;
|
|
2796
2576
|
}
|
|
2797
|
-
|
|
2798
|
-
//
|
|
2799
|
-
|
|
2800
|
-
if
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
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);
|
|
2812
2592
|
}
|
|
2813
2593
|
}
|
|
2814
|
-
|
|
2815
|
-
//
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
// sealed
|
|
2819
|
-
// 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.
|
|
2820
2603
|
const turnStopReason = (payload as any).stopReason as
|
|
2821
2604
|
| StopReasonKind
|
|
2822
2605
|
| undefined;
|
|
2823
2606
|
const stopReasonTarget = assistantMessage ?? lastAssistantInTurn;
|
|
2824
2607
|
if (turnStopReason && stopReasonTarget !== null) {
|
|
2825
|
-
const turnId = payload.
|
|
2608
|
+
const turnId = payload.id;
|
|
2826
2609
|
const matchesTurn =
|
|
2827
2610
|
!turnId || stopReasonTarget.agentMetadata?.turnId === turnId;
|
|
2828
2611
|
if (matchesTurn) {
|
|
@@ -2830,83 +2613,58 @@ export class AgentWidgetClient {
|
|
|
2830
2613
|
emitMessage(stopReasonTarget);
|
|
2831
2614
|
}
|
|
2832
2615
|
}
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
//
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
const
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
result: undefined, duration: undefined, startedAt: undefined,
|
|
2851
|
-
completedAt: undefined, durationMs: undefined
|
|
2852
|
-
};
|
|
2853
|
-
tool.name = payload.toolName ?? payload.name ?? tool.name;
|
|
2854
|
-
tool.status = "running";
|
|
2855
|
-
if (payload.parameters !== undefined) {
|
|
2856
|
-
tool.args = payload.parameters;
|
|
2857
|
-
}
|
|
2858
|
-
tool.startedAt = resolveTimestamp(payload.startedAt ?? payload.timestamp);
|
|
2859
|
-
toolMessage.toolCall = tool;
|
|
2860
|
-
toolMessage.streaming = true;
|
|
2861
|
-
toolMessage.agentMetadata = {
|
|
2862
|
-
executionId: payload.executionId,
|
|
2863
|
-
iteration: payload.iteration
|
|
2864
|
-
};
|
|
2865
|
-
emitMessage(toolMessage);
|
|
2866
|
-
} else if (payloadType === "agent_tool_delta") {
|
|
2867
|
-
const toolId = payload.toolCallId ?? toolContext.lastId;
|
|
2868
|
-
if (toolId) {
|
|
2869
|
-
const toolMessage = toolMessages.get(toolId) ?? ensureToolMessage(toolId);
|
|
2870
|
-
if (toolMessage.toolCall) {
|
|
2871
|
-
toolMessage.toolCall.chunks = toolMessage.toolCall.chunks ?? [];
|
|
2872
|
-
toolMessage.toolCall.chunks.push(payload.delta ?? '');
|
|
2873
|
-
toolMessage.toolCall.status = "running";
|
|
2874
|
-
toolMessage.streaming = true;
|
|
2875
|
-
emitMessage(toolMessage);
|
|
2876
|
-
}
|
|
2877
|
-
}
|
|
2878
|
-
} else if (payloadType === "agent_tool_complete") {
|
|
2879
|
-
const toolId = payload.toolCallId ?? toolContext.lastId;
|
|
2880
|
-
if (toolId) {
|
|
2881
|
-
const toolMessage = toolMessages.get(toolId) ?? ensureToolMessage(toolId);
|
|
2882
|
-
if (toolMessage.toolCall) {
|
|
2883
|
-
toolMessage.toolCall.status = "complete";
|
|
2884
|
-
if (payload.result !== undefined) {
|
|
2885
|
-
toolMessage.toolCall.result = payload.result;
|
|
2886
|
-
}
|
|
2887
|
-
if (typeof payload.executionTime === "number") {
|
|
2888
|
-
toolMessage.toolCall.durationMs = payload.executionTime;
|
|
2889
|
-
}
|
|
2890
|
-
toolMessage.toolCall.completedAt = resolveTimestamp(payload.completedAt ?? payload.timestamp);
|
|
2891
|
-
toolMessage.streaming = false;
|
|
2892
|
-
emitMessage(toolMessage);
|
|
2893
|
-
const callKey = getToolCallKey(payload);
|
|
2894
|
-
if (callKey) {
|
|
2895
|
-
toolContext.byCall.delete(callKey);
|
|
2896
|
-
}
|
|
2897
|
-
}
|
|
2898
|
-
}
|
|
2899
|
-
} else if (payloadType === "agent_media") {
|
|
2900
|
-
// A tool produced media (image / audio / video / file). Render it
|
|
2901
|
-
// as a synthetic assistant message inserted at the point the tool
|
|
2902
|
-
// completed: between the tool bubble and the next text turn.
|
|
2903
|
-
//
|
|
2904
|
-
// Wire format is the AI SDK–aligned `MediaContentPart` from
|
|
2905
|
-
// @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:
|
|
2906
2633
|
// { type: 'media', data, mediaType } // AI SDK v6: base64
|
|
2907
2634
|
// { type: 'image-url', url, mediaType? } // AI SDK v3/v4
|
|
2908
2635
|
// { type: 'file-url', url, mediaType } // AI SDK v3/v4
|
|
2909
|
-
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] : [];
|
|
2910
2668
|
const mediaContentParts: ContentPart[] = [];
|
|
2911
2669
|
for (const part of rawMedia) {
|
|
2912
2670
|
if (!part || typeof part !== "object") continue;
|
|
@@ -2950,7 +2708,9 @@ export class AgentWidgetClient {
|
|
|
2950
2708
|
mediaContentParts.push({
|
|
2951
2709
|
type: "image",
|
|
2952
2710
|
image: src,
|
|
2953
|
-
|
|
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 } : {}),
|
|
2954
2714
|
});
|
|
2955
2715
|
} else if (mediaType.startsWith("audio/")) {
|
|
2956
2716
|
mediaContentParts.push({
|
|
@@ -2981,7 +2741,7 @@ export class AgentWidgetClient {
|
|
|
2981
2741
|
// sharing an id would let `emitMessage` merge them by id and
|
|
2982
2742
|
// overwrite the prior `contentParts`.
|
|
2983
2743
|
const seq = nextSequence();
|
|
2984
|
-
const toolCallIdRaw =
|
|
2744
|
+
const toolCallIdRaw = mediaToolCallId;
|
|
2985
2745
|
const mediaIdSuffix =
|
|
2986
2746
|
typeof toolCallIdRaw === "string" && toolCallIdRaw.length > 0
|
|
2987
2747
|
? `${toolCallIdRaw}-${seq}`
|
|
@@ -2996,7 +2756,12 @@ export class AgentWidgetClient {
|
|
|
2996
2756
|
sequence: seq,
|
|
2997
2757
|
agentMetadata: {
|
|
2998
2758
|
executionId: payload.executionId,
|
|
2999
|
-
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,
|
|
3000
2765
|
},
|
|
3001
2766
|
};
|
|
3002
2767
|
emitMessage(mediaMessage);
|
|
@@ -3014,67 +2779,49 @@ export class AgentWidgetClient {
|
|
|
3014
2779
|
assistantMessage = null;
|
|
3015
2780
|
assistantMessageRef.current = null;
|
|
3016
2781
|
}
|
|
3017
|
-
} else if (payloadType === "
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
} else if (payloadType === "agent_reflection" || payloadType === "agent_reflect") {
|
|
3021
|
-
// Create a reasoning message for reflection content
|
|
3022
|
-
const reflectionId = `agent-reflection-${payload.executionId}-${payload.iteration}`;
|
|
3023
|
-
const reflectionMessage: AgentWidgetMessage = {
|
|
3024
|
-
id: reflectionId,
|
|
3025
|
-
role: "assistant",
|
|
3026
|
-
content: payload.reflection ?? '',
|
|
3027
|
-
createdAt: new Date().toISOString(),
|
|
3028
|
-
streaming: false,
|
|
3029
|
-
variant: "reasoning",
|
|
3030
|
-
sequence: nextSequence(),
|
|
3031
|
-
reasoning: {
|
|
3032
|
-
id: reflectionId,
|
|
3033
|
-
status: "complete",
|
|
3034
|
-
chunks: [payload.reflection ?? '']
|
|
3035
|
-
},
|
|
3036
|
-
agentMetadata: {
|
|
3037
|
-
executionId: payload.executionId,
|
|
3038
|
-
iteration: payload.iteration
|
|
3039
|
-
}
|
|
3040
|
-
};
|
|
3041
|
-
emitMessage(reflectionMessage);
|
|
3042
|
-
} else if (payloadType === "agent_complete") {
|
|
3043
|
-
if (agentExecution) {
|
|
2782
|
+
} else if (payloadType === "execution_complete") {
|
|
2783
|
+
const kind = payload.kind ?? executionKind;
|
|
2784
|
+
if (kind === "agent" && agentExecution) {
|
|
3044
2785
|
agentExecution.status = payload.success ? 'complete' : 'error';
|
|
3045
2786
|
agentExecution.completedAt = resolveTimestamp(payload.completedAt);
|
|
3046
2787
|
agentExecution.stopReason = payload.stopReason;
|
|
3047
2788
|
}
|
|
3048
2789
|
|
|
3049
|
-
// 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.
|
|
3050
2794
|
const finalMsg = assistantMessage as AgentWidgetMessage | null;
|
|
3051
2795
|
if (finalMsg) {
|
|
3052
|
-
finalMsg.streaming
|
|
3053
|
-
|
|
2796
|
+
if (kind === "flow" && finalMsg.streaming !== false) {
|
|
2797
|
+
finalizeFlowTextBlock(finalMsg);
|
|
2798
|
+
} else {
|
|
2799
|
+
finalMsg.streaming = false;
|
|
2800
|
+
emitMessage(finalMsg);
|
|
2801
|
+
}
|
|
2802
|
+
assistantMessage = null;
|
|
3054
2803
|
}
|
|
2804
|
+
currentTextBlockId = null;
|
|
2805
|
+
pendingFlowRaw = "";
|
|
2806
|
+
lastSealedFlowBubble = null;
|
|
3055
2807
|
|
|
3056
2808
|
onEvent({ type: "status", status: "idle" });
|
|
3057
|
-
} else if (payloadType === "
|
|
2809
|
+
} else if (payloadType === "execution_error") {
|
|
2810
|
+
// Terminal failure. The unified non-terminal `error` is handled
|
|
2811
|
+
// separately (recoverable → warn).
|
|
3058
2812
|
const errorMessage = typeof payload.error === 'string'
|
|
3059
2813
|
? payload.error
|
|
3060
|
-
: payload.error?.message ?? '
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
} else {
|
|
3067
|
-
onEvent({
|
|
3068
|
-
type: "error",
|
|
3069
|
-
error: new Error(errorMessage)
|
|
3070
|
-
});
|
|
3071
|
-
}
|
|
3072
|
-
} 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") {
|
|
3073
2820
|
// Keep-alive heartbeat - no action needed
|
|
3074
2821
|
// ================================================================
|
|
3075
2822
|
// Tool Approval Events
|
|
3076
2823
|
// ================================================================
|
|
3077
|
-
} else if (payloadType === "
|
|
2824
|
+
} else if (payloadType === "approval_start") {
|
|
3078
2825
|
const approvalId = payload.approvalId ?? `approval-${nextSequence()}`;
|
|
3079
2826
|
const approvalMessage: AgentWidgetMessage = {
|
|
3080
2827
|
id: `approval-${approvalId}`,
|
|
@@ -3124,7 +2871,7 @@ export class AgentWidgetClient {
|
|
|
3124
2871
|
},
|
|
3125
2872
|
};
|
|
3126
2873
|
emitMessage(approvalMessage);
|
|
3127
|
-
} else if (payloadType === "
|
|
2874
|
+
} else if (payloadType === "approval_complete") {
|
|
3128
2875
|
const approvalId = payload.approvalId;
|
|
3129
2876
|
if (approvalId) {
|
|
3130
2877
|
// Find and update the existing approval message
|
|
@@ -3269,8 +3016,33 @@ export class AgentWidgetClient {
|
|
|
3269
3016
|
assistantMessageRef.current = null;
|
|
3270
3017
|
streamParsers.delete(id);
|
|
3271
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
|
+
}
|
|
3272
3045
|
} else if (
|
|
3273
|
-
payloadType === "error" ||
|
|
3274
3046
|
payloadType === "step_error" ||
|
|
3275
3047
|
payloadType === "dispatch_error" ||
|
|
3276
3048
|
payloadType === "flow_error"
|
|
@@ -3283,18 +3055,13 @@ export class AgentWidgetClient {
|
|
|
3283
3055
|
if (msg != null && msg !== "") {
|
|
3284
3056
|
resolvedError = new Error(String(msg));
|
|
3285
3057
|
}
|
|
3286
|
-
} else
|
|
3287
|
-
payloadType === "step_error" ||
|
|
3288
|
-
payloadType === "flow_error"
|
|
3289
|
-
) {
|
|
3058
|
+
} else {
|
|
3290
3059
|
const e = payload.error;
|
|
3291
3060
|
if (typeof e === "string" && e !== "") {
|
|
3292
3061
|
resolvedError = new Error(e);
|
|
3293
3062
|
} else if (e != null && typeof e === "object" && "message" in e) {
|
|
3294
3063
|
resolvedError = new Error(String((e as { message?: unknown }).message ?? e));
|
|
3295
3064
|
}
|
|
3296
|
-
} else if (payloadType === "error" && payload.error != null && payload.error !== "") {
|
|
3297
|
-
resolvedError = new Error(String(payload.error));
|
|
3298
3065
|
}
|
|
3299
3066
|
|
|
3300
3067
|
if (resolvedError) {
|
|
@@ -3364,7 +3131,7 @@ export class AgentWidgetClient {
|
|
|
3364
3131
|
assistantMessageRef,
|
|
3365
3132
|
emitMessage,
|
|
3366
3133
|
nextSequence,
|
|
3367
|
-
|
|
3134
|
+
customParsePartId
|
|
3368
3135
|
);
|
|
3369
3136
|
// Update assistantMessage from ref (in case it was created or replaced by partId segmentation)
|
|
3370
3137
|
if (assistantMessageRef.current && assistantMessageRef.current !== assistantMessage) {
|
|
@@ -3373,14 +3140,14 @@ export class AgentWidgetClient {
|
|
|
3373
3140
|
if (handled) continue; // Skip default handling if custom handler processed it
|
|
3374
3141
|
}
|
|
3375
3142
|
|
|
3376
|
-
//
|
|
3377
|
-
|
|
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 });
|
|
3378
3147
|
drainReadyQueue();
|
|
3379
3148
|
}
|
|
3380
3149
|
}
|
|
3381
3150
|
|
|
3382
|
-
seqBuffer.flushPending();
|
|
3383
3151
|
drainReadyQueue();
|
|
3384
|
-
seqBuffer.destroy();
|
|
3385
3152
|
}
|
|
3386
3153
|
}
|