@runtypelabs/persona 4.5.0 → 4.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/animations/glyph-cycle.d.cts +1 -1
  2. package/dist/animations/glyph-cycle.d.ts +1 -1
  3. package/dist/animations/{types-C6tFDxKy.d.cts → types-CSmiKRVa.d.cts} +11 -0
  4. package/dist/animations/{types-C6tFDxKy.d.ts → types-CSmiKRVa.d.ts} +11 -0
  5. package/dist/animations/wipe.d.cts +1 -1
  6. package/dist/animations/wipe.d.ts +1 -1
  7. package/dist/chunk-DFBSCFYN.js +1 -0
  8. package/dist/codegen.cjs +4 -4
  9. package/dist/codegen.js +4 -4
  10. package/dist/index.cjs +51 -51
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.d.cts +238 -3
  13. package/dist/index.d.ts +238 -3
  14. package/dist/index.global.js +37 -37
  15. package/dist/index.global.js.map +1 -1
  16. package/dist/index.js +51 -51
  17. package/dist/index.js.map +1 -1
  18. package/dist/launcher.global.js.map +1 -1
  19. package/dist/markdown-parsers-entry-NVFT3TE6.js +1 -0
  20. package/dist/runtype-tts-entry-HFUV2UF7.js +1 -0
  21. package/dist/session-reconnect-U77QFUR7.js +1 -0
  22. package/dist/smart-dom-reader.d.cts +95 -0
  23. package/dist/smart-dom-reader.d.ts +95 -0
  24. package/dist/theme-editor-preview.cjs +48 -48
  25. package/dist/theme-editor-preview.d.cts +135 -1
  26. package/dist/theme-editor-preview.d.ts +135 -1
  27. package/dist/theme-editor-preview.js +48 -48
  28. package/dist/theme-editor.d.cts +95 -0
  29. package/dist/theme-editor.d.ts +95 -0
  30. package/package.json +5 -3
  31. package/src/client.ts +55 -9
  32. package/src/generated/runtype-openapi-contract.ts +16 -1
  33. package/src/reconnect-wake.test.ts +162 -0
  34. package/src/reconnect.test.ts +430 -0
  35. package/src/session-reconnect.ts +282 -0
  36. package/src/session.ts +408 -5
  37. package/src/types.ts +107 -1
  38. package/src/ui.stream-animation-update.test.ts +99 -0
  39. package/src/ui.ts +71 -1
  40. package/src/utils/constants.ts +3 -1
  41. package/src/utils/morph.test.ts +47 -0
  42. package/src/utils/morph.ts +18 -0
@@ -783,6 +783,7 @@ type RuntypeExecutionStreamEvent = ({
783
783
  };
784
784
  type: "execution_complete";
785
785
  }) | ({
786
+ blockReason?: string;
786
787
  code?: string;
787
788
  completedAt?: string;
788
789
  error: string | {
@@ -1037,6 +1038,7 @@ type RuntypeExecutionStreamEvent = ({
1037
1038
  startedAt?: string;
1038
1039
  subagent?: {
1039
1040
  agentName?: string;
1041
+ parentToolCallId?: string;
1040
1042
  toolName: string;
1041
1043
  };
1042
1044
  timeout?: number;
@@ -1053,12 +1055,15 @@ type RuntypeExecutionStreamEvent = ({
1053
1055
  seq: number;
1054
1056
  type: "approval_complete";
1055
1057
  }) | ({
1058
+ awaitReason?: string;
1056
1059
  awaitedAt?: string;
1060
+ crawlId?: string;
1057
1061
  executionId: string;
1058
1062
  origin?: "webmcp" | "sdk";
1059
1063
  pageOrigin?: string;
1060
1064
  parameters?: Record<string, unknown>;
1061
1065
  seq: number;
1066
+ stepId?: string;
1062
1067
  toolCallId?: string;
1063
1068
  toolId?: string;
1064
1069
  toolName?: string;
@@ -1127,6 +1132,7 @@ type RuntypeFlowSSEEvent = {
1127
1132
  totalTokensUsed?: number;
1128
1133
  type: "flow_complete";
1129
1134
  } | ({
1135
+ blockReason?: string;
1130
1136
  code?: string;
1131
1137
  error: string | {
1132
1138
  code: string;
@@ -1147,13 +1153,18 @@ type RuntypeFlowSSEEvent = {
1147
1153
  type: "flow_error";
1148
1154
  upgradeUrl?: string;
1149
1155
  }) | ({
1156
+ approvalId?: string;
1157
+ awaitReason?: string;
1150
1158
  awaitedAt: string;
1159
+ crawlId?: string;
1151
1160
  executionId?: string;
1152
1161
  flowId: string;
1153
1162
  origin?: "webmcp" | "sdk";
1154
1163
  pageOrigin?: string;
1155
1164
  parameters?: Record<string, unknown>;
1156
1165
  seq?: number;
1166
+ stepId?: string;
1167
+ timeout?: number;
1157
1168
  toolCallId?: string;
1158
1169
  toolId?: string;
1159
1170
  toolName?: string;
@@ -1693,6 +1704,27 @@ type AgentWidgetWebMcpConfig = {
1693
1704
  */
1694
1705
  onConfirm?: WebMcpConfirmHandler;
1695
1706
  };
1707
+ /**
1708
+ * The coordinates needed to resume a durable agent turn after the SSE
1709
+ * connection drops (any resumable, server-persisted execution, e.g. Claude
1710
+ * Managed agents or async/background runs). Held on the session while a
1711
+ * resumable stream is in flight
1712
+ * and surfaced to the host via {@link AgentWidgetConfig.onExecutionState} so it
1713
+ * can persist `{ executionId, lastEventId }` next to its own `conversationId`
1714
+ * for the tab-reload path.
1715
+ *
1716
+ * `conversationId` is intentionally absent: it is host-owned (it lives in the
1717
+ * host's `reconnectStream`/`customFetch` closure; the widget never sees it).
1718
+ */
1719
+ type ResumableHandle = {
1720
+ /** The durable turn to reconnect to (from `agentMetadata.executionId`). */
1721
+ executionId: string;
1722
+ /** Highest SSE `id:` seq applied: the `?after=` reconnect cursor. */
1723
+ lastEventId: string;
1724
+ /** The open assistant message being streamed into, kept open on resume. */
1725
+ assistantMessageId: string;
1726
+ status: 'running';
1727
+ };
1696
1728
  /**
1697
1729
  * Metadata attached to messages created during agent execution.
1698
1730
  */
@@ -3009,6 +3041,10 @@ type AgentWidgetStatusIndicatorConfig = {
3009
3041
  connectingText?: string;
3010
3042
  connectedText?: string;
3011
3043
  errorText?: string;
3044
+ /** Status text while a dropped durable stream is awaiting reconnect. */
3045
+ pausedText?: string;
3046
+ /** Status text while a reconnect attempt is in flight. */
3047
+ resumingText?: string;
3012
3048
  };
3013
3049
  type AgentWidgetVoiceRecognitionConfig = {
3014
3050
  enabled?: boolean;
@@ -5090,6 +5126,65 @@ type AgentWidgetConfig = {
5090
5126
  * ```
5091
5127
  */
5092
5128
  customFetch?: AgentWidgetCustomFetch;
5129
+ /**
5130
+ * Durable-session reconnect transport (host-owned, symmetric to
5131
+ * {@link customFetch}). When a durable agent stream drops mid-turn (any
5132
+ * resumable, server-persisted execution, e.g. Claude Managed agents or
5133
+ * async/background runs), the widget calls this to fetch the read-only
5134
+ * reconnect stream and pipes its body through the normal event pipeline to
5135
+ * resume from where it left off.
5136
+ *
5137
+ * The host closure owns `agentId` / `conversationId` / base URL / auth and
5138
+ * builds the events request, e.g.:
5139
+ *
5140
+ * ```typescript
5141
+ * reconnectStream: ({ executionId, after, signal }) =>
5142
+ * fetch(
5143
+ * `${baseUrl}/v1/agents/${agentId}/executions/${executionId}` +
5144
+ * `/events?conversationId=${conversationId}&after=${after}`,
5145
+ * { headers: { Authorization: `Bearer ${token}`, "X-Persona-Version": ver }, signal }
5146
+ * )
5147
+ * ```
5148
+ *
5149
+ * Resolve with a `text/event-stream` `Response`. Throw or resolve non-ok to
5150
+ * signal "this attempt failed" (the widget backs off and retries, then gives
5151
+ * up after the bounded attempts). Reconnect only ever arms on the durable
5152
+ * lane (streams carrying SSE `id:` lines); without this hook a drop finalizes
5153
+ * exactly as before.
5154
+ */
5155
+ reconnectStream?: (ctx: {
5156
+ executionId: string;
5157
+ after: string;
5158
+ signal: AbortSignal;
5159
+ }) => Promise<Response>;
5160
+ /**
5161
+ * Tuning for the auto-reconnect backoff. Defaults to ~5 attempts over ~30s
5162
+ * (`backoffMs: [1000, 2000, 4000, 8000, 8000]`).
5163
+ */
5164
+ reconnect?: {
5165
+ /** Max reconnect attempts before giving up and finalizing. @default backoffMs.length */
5166
+ maxAttempts?: number;
5167
+ /** Per-attempt delay (ms) before each retry. @default [1000, 2000, 4000, 8000, 8000] */
5168
+ backoffMs?: number[];
5169
+ };
5170
+ /**
5171
+ * Called whenever the durable resume handle changes: created when a durable
5172
+ * turn starts streaming, updated as the cursor advances (throttled), and
5173
+ * `null` when the turn finishes, errors, or is torn down. Persist
5174
+ * `{ executionId, lastEventId }` next to your `conversationId` and pass it
5175
+ * back via {@link resume} on the next mount to survive a tab reload.
5176
+ */
5177
+ onExecutionState?: (handle: ResumableHandle | null) => void;
5178
+ /**
5179
+ * Resume a durable turn on boot (tab-reload path). When present alongside
5180
+ * {@link reconnectStream}, the widget enters `resuming` immediately on mount
5181
+ * and reconnects from `after`, replaying everything past the cursor into the
5182
+ * restored conversation.
5183
+ */
5184
+ resume?: {
5185
+ executionId: string;
5186
+ after: string;
5187
+ };
5093
5188
  /**
5094
5189
  * Custom SSE event parser for non-standard streaming response formats.
5095
5190
  *
@@ -783,6 +783,7 @@ type RuntypeExecutionStreamEvent = ({
783
783
  };
784
784
  type: "execution_complete";
785
785
  }) | ({
786
+ blockReason?: string;
786
787
  code?: string;
787
788
  completedAt?: string;
788
789
  error: string | {
@@ -1037,6 +1038,7 @@ type RuntypeExecutionStreamEvent = ({
1037
1038
  startedAt?: string;
1038
1039
  subagent?: {
1039
1040
  agentName?: string;
1041
+ parentToolCallId?: string;
1040
1042
  toolName: string;
1041
1043
  };
1042
1044
  timeout?: number;
@@ -1053,12 +1055,15 @@ type RuntypeExecutionStreamEvent = ({
1053
1055
  seq: number;
1054
1056
  type: "approval_complete";
1055
1057
  }) | ({
1058
+ awaitReason?: string;
1056
1059
  awaitedAt?: string;
1060
+ crawlId?: string;
1057
1061
  executionId: string;
1058
1062
  origin?: "webmcp" | "sdk";
1059
1063
  pageOrigin?: string;
1060
1064
  parameters?: Record<string, unknown>;
1061
1065
  seq: number;
1066
+ stepId?: string;
1062
1067
  toolCallId?: string;
1063
1068
  toolId?: string;
1064
1069
  toolName?: string;
@@ -1127,6 +1132,7 @@ type RuntypeFlowSSEEvent = {
1127
1132
  totalTokensUsed?: number;
1128
1133
  type: "flow_complete";
1129
1134
  } | ({
1135
+ blockReason?: string;
1130
1136
  code?: string;
1131
1137
  error: string | {
1132
1138
  code: string;
@@ -1147,13 +1153,18 @@ type RuntypeFlowSSEEvent = {
1147
1153
  type: "flow_error";
1148
1154
  upgradeUrl?: string;
1149
1155
  }) | ({
1156
+ approvalId?: string;
1157
+ awaitReason?: string;
1150
1158
  awaitedAt: string;
1159
+ crawlId?: string;
1151
1160
  executionId?: string;
1152
1161
  flowId: string;
1153
1162
  origin?: "webmcp" | "sdk";
1154
1163
  pageOrigin?: string;
1155
1164
  parameters?: Record<string, unknown>;
1156
1165
  seq?: number;
1166
+ stepId?: string;
1167
+ timeout?: number;
1157
1168
  toolCallId?: string;
1158
1169
  toolId?: string;
1159
1170
  toolName?: string;
@@ -1693,6 +1704,27 @@ type AgentWidgetWebMcpConfig = {
1693
1704
  */
1694
1705
  onConfirm?: WebMcpConfirmHandler;
1695
1706
  };
1707
+ /**
1708
+ * The coordinates needed to resume a durable agent turn after the SSE
1709
+ * connection drops (any resumable, server-persisted execution, e.g. Claude
1710
+ * Managed agents or async/background runs). Held on the session while a
1711
+ * resumable stream is in flight
1712
+ * and surfaced to the host via {@link AgentWidgetConfig.onExecutionState} so it
1713
+ * can persist `{ executionId, lastEventId }` next to its own `conversationId`
1714
+ * for the tab-reload path.
1715
+ *
1716
+ * `conversationId` is intentionally absent: it is host-owned (it lives in the
1717
+ * host's `reconnectStream`/`customFetch` closure; the widget never sees it).
1718
+ */
1719
+ type ResumableHandle = {
1720
+ /** The durable turn to reconnect to (from `agentMetadata.executionId`). */
1721
+ executionId: string;
1722
+ /** Highest SSE `id:` seq applied: the `?after=` reconnect cursor. */
1723
+ lastEventId: string;
1724
+ /** The open assistant message being streamed into, kept open on resume. */
1725
+ assistantMessageId: string;
1726
+ status: 'running';
1727
+ };
1696
1728
  /**
1697
1729
  * Metadata attached to messages created during agent execution.
1698
1730
  */
@@ -3009,6 +3041,10 @@ type AgentWidgetStatusIndicatorConfig = {
3009
3041
  connectingText?: string;
3010
3042
  connectedText?: string;
3011
3043
  errorText?: string;
3044
+ /** Status text while a dropped durable stream is awaiting reconnect. */
3045
+ pausedText?: string;
3046
+ /** Status text while a reconnect attempt is in flight. */
3047
+ resumingText?: string;
3012
3048
  };
3013
3049
  type AgentWidgetVoiceRecognitionConfig = {
3014
3050
  enabled?: boolean;
@@ -5090,6 +5126,65 @@ type AgentWidgetConfig = {
5090
5126
  * ```
5091
5127
  */
5092
5128
  customFetch?: AgentWidgetCustomFetch;
5129
+ /**
5130
+ * Durable-session reconnect transport (host-owned, symmetric to
5131
+ * {@link customFetch}). When a durable agent stream drops mid-turn (any
5132
+ * resumable, server-persisted execution, e.g. Claude Managed agents or
5133
+ * async/background runs), the widget calls this to fetch the read-only
5134
+ * reconnect stream and pipes its body through the normal event pipeline to
5135
+ * resume from where it left off.
5136
+ *
5137
+ * The host closure owns `agentId` / `conversationId` / base URL / auth and
5138
+ * builds the events request, e.g.:
5139
+ *
5140
+ * ```typescript
5141
+ * reconnectStream: ({ executionId, after, signal }) =>
5142
+ * fetch(
5143
+ * `${baseUrl}/v1/agents/${agentId}/executions/${executionId}` +
5144
+ * `/events?conversationId=${conversationId}&after=${after}`,
5145
+ * { headers: { Authorization: `Bearer ${token}`, "X-Persona-Version": ver }, signal }
5146
+ * )
5147
+ * ```
5148
+ *
5149
+ * Resolve with a `text/event-stream` `Response`. Throw or resolve non-ok to
5150
+ * signal "this attempt failed" (the widget backs off and retries, then gives
5151
+ * up after the bounded attempts). Reconnect only ever arms on the durable
5152
+ * lane (streams carrying SSE `id:` lines); without this hook a drop finalizes
5153
+ * exactly as before.
5154
+ */
5155
+ reconnectStream?: (ctx: {
5156
+ executionId: string;
5157
+ after: string;
5158
+ signal: AbortSignal;
5159
+ }) => Promise<Response>;
5160
+ /**
5161
+ * Tuning for the auto-reconnect backoff. Defaults to ~5 attempts over ~30s
5162
+ * (`backoffMs: [1000, 2000, 4000, 8000, 8000]`).
5163
+ */
5164
+ reconnect?: {
5165
+ /** Max reconnect attempts before giving up and finalizing. @default backoffMs.length */
5166
+ maxAttempts?: number;
5167
+ /** Per-attempt delay (ms) before each retry. @default [1000, 2000, 4000, 8000, 8000] */
5168
+ backoffMs?: number[];
5169
+ };
5170
+ /**
5171
+ * Called whenever the durable resume handle changes: created when a durable
5172
+ * turn starts streaming, updated as the cursor advances (throttled), and
5173
+ * `null` when the turn finishes, errors, or is torn down. Persist
5174
+ * `{ executionId, lastEventId }` next to your `conversationId` and pass it
5175
+ * back via {@link resume} on the next mount to survive a tab reload.
5176
+ */
5177
+ onExecutionState?: (handle: ResumableHandle | null) => void;
5178
+ /**
5179
+ * Resume a durable turn on boot (tab-reload path). When present alongside
5180
+ * {@link reconnectStream}, the widget enters `resuming` immediately on mount
5181
+ * and reconnects from `after`, replaying everything past the cursor into the
5182
+ * restored conversation.
5183
+ */
5184
+ resume?: {
5185
+ executionId: string;
5186
+ after: string;
5187
+ };
5093
5188
  /**
5094
5189
  * Custom SSE event parser for non-standard streaming response formats.
5095
5190
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runtypelabs/persona",
3
- "version": "4.5.0",
3
+ "version": "4.6.1",
4
4
  "description": "Themeable, pluggable streaming agent widget for websites, in plain JS with support for voice input and reasoning / tool output.",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -79,6 +79,7 @@
79
79
  },
80
80
  "devDependencies": {
81
81
  "@size-limit/file": "^12.1.0",
82
+ "acorn": "^8.15.0",
82
83
  "@types/node": "^20.12.7",
83
84
  "@typescript-eslint/eslint-plugin": "^7.0.0",
84
85
  "@typescript-eslint/parser": "^7.0.0",
@@ -120,11 +121,12 @@
120
121
  "access": "public"
121
122
  },
122
123
  "scripts": {
123
- "build": "rimraf dist && pnpm run build:styles && pnpm run build:markdown-parsers && pnpm run build:client && pnpm run build:installer && pnpm run build:launcher && pnpm run build:webmcp-polyfill && pnpm run build:runtype-tts && pnpm run build:theme-ref && pnpm run build:codegen && pnpm run build:theme-editor && pnpm run build:theme-editor-preview && pnpm run build:testing && pnpm run build:smart-dom-reader && pnpm run build:voice-worklet-player && pnpm run build:plugin-kit && pnpm run build:animations",
124
+ "build": "rimraf dist && pnpm run build:styles && pnpm run build:markdown-parsers && pnpm run build:client && pnpm run build:installer && pnpm run build:launcher && pnpm run build:webmcp-polyfill && pnpm run build:runtype-tts && pnpm run build:theme-ref && pnpm run build:codegen && pnpm run build:theme-editor && pnpm run build:theme-editor-preview && pnpm run build:testing && pnpm run build:smart-dom-reader && pnpm run build:voice-worklet-player && pnpm run build:plugin-kit && pnpm run build:animations && pnpm run check:dist-vite8",
125
+ "check:dist-vite8": "node scripts/check-dist-no-in-for-init.mjs",
124
126
  "build:markdown-parsers": "tsup --config tsup.markdown-parsers.config.ts",
125
127
  "build:plugin-kit": "tsup src/plugin-kit.ts --format esm,cjs --minify --dts --out-dir dist --no-splitting",
126
128
  "build:theme-editor": "tsup src/theme-editor.ts --format esm,cjs --minify --dts --out-dir dist --no-splitting",
127
- "build:theme-editor-preview": "tsup src/theme-editor-preview.ts --format esm,cjs --minify --dts --out-dir dist --no-splitting",
129
+ "build:theme-editor-preview": "tsup src/theme-editor-preview.ts --format esm,cjs --minify --dts --out-dir dist",
128
130
  "build:testing": "tsup src/testing.ts --format esm,cjs --minify --dts --out-dir dist --no-splitting",
129
131
  "build:smart-dom-reader": "tsup src/smart-dom-reader.ts --format esm,cjs --minify --dts --out-dir dist --no-splitting",
130
132
  "build:voice-worklet-player": "tsup src/voice-worklet-player.ts --format esm,cjs --minify --dts --out-dir dist --no-splitting",
package/src/client.ts CHANGED
@@ -954,11 +954,12 @@ export class AgentWidgetClient {
954
954
  public async processStream(
955
955
  body: ReadableStream<Uint8Array>,
956
956
  onEvent: SSEHandler,
957
- assistantMessageId?: string
957
+ assistantMessageId?: string,
958
+ seedContent?: string
958
959
  ): Promise<void> {
959
960
  onEvent({ type: "status", status: "connected" });
960
961
  try {
961
- await this.streamResponse(body, onEvent, assistantMessageId);
962
+ await this.streamResponse(body, onEvent, assistantMessageId, seedContent);
962
963
  } finally {
963
964
  onEvent({ type: "status", status: "idle" });
964
965
  }
@@ -1353,7 +1354,12 @@ export class AgentWidgetClient {
1353
1354
  private async streamResponse(
1354
1355
  body: ReadableStream<Uint8Array>,
1355
1356
  onEvent: SSEHandler,
1356
- assistantMessageId?: string
1357
+ assistantMessageId?: string,
1358
+ // Durable reconnect: seed the assistant accumulator with the text already
1359
+ // shown before the drop, so replayed post-cursor deltas APPEND to it
1360
+ // instead of a fresh stream clobbering it (the replay carries only
1361
+ // `seq > after`, i.e. the new deltas, not the full text).
1362
+ seedContent?: string
1357
1363
  ) {
1358
1364
  const reader = body.getReader();
1359
1365
  const decoder = new TextDecoder();
@@ -1490,10 +1496,14 @@ export class AgentWidgetClient {
1490
1496
  const ensureAssistantMessage = () => {
1491
1497
  if (assistantMessage) return assistantMessage;
1492
1498
  let id: string;
1499
+ let initialContent = "";
1493
1500
  const segment = currentTextBlockId;
1494
1501
  if (!assistantIdConsumed && baseAssistantId) {
1495
1502
  id = baseAssistantId;
1496
1503
  assistantIdConsumed = true;
1504
+ // First (and only) time we reuse the caller-supplied id: this is the
1505
+ // bubble a durable reconnect resumes into, so continue its text.
1506
+ initialContent = seedContent ?? "";
1497
1507
  } else if (baseAssistantId && segment) {
1498
1508
  id = `${baseAssistantId}_${segment}`;
1499
1509
  } else {
@@ -1502,7 +1512,7 @@ export class AgentWidgetClient {
1502
1512
  assistantMessage = {
1503
1513
  id,
1504
1514
  role: "assistant",
1505
- content: "",
1515
+ content: initialContent,
1506
1516
  createdAt: new Date().toISOString(),
1507
1517
  streaming: true,
1508
1518
  sequence: nextSequence()
@@ -2552,7 +2562,11 @@ export class AgentWidgetClient {
2552
2562
  const message =
2553
2563
  typeof e === "string" && e !== ""
2554
2564
  ? e
2555
- : e != null && typeof e === "object" && "message" in e
2565
+ : // Reflect.has, not `in`: the `in` operator inside an arrow body can
2566
+ // be minified into a `for(init;;)` head, which Oxc mis-parses and
2567
+ // Rolldown (Vite 8) silently emits as an empty chunk. Same
2568
+ // [[HasProperty]] semantics. Enforced by scripts/check-dist-no-in-for-init.mjs.
2569
+ e != null && typeof e === "object" && Reflect.has(e, "message")
2556
2570
  ? String((e as { message?: unknown }).message ?? "Step failed")
2557
2571
  : "Step failed";
2558
2572
  onEvent({ type: "error", error: new Error(message) });
@@ -2877,7 +2891,11 @@ export class AgentWidgetClient {
2877
2891
  pendingFlowRaw = "";
2878
2892
  lastSealedFlowBubble = null;
2879
2893
 
2880
- onEvent({ type: "status", status: "idle" });
2894
+ // `terminal: true` marks this as a graceful finish (not a drop). The
2895
+ // session uses it to distinguish the real end-of-turn from the plain
2896
+ // `idle` the dispatch wrappers emit in their `finally` when a durable
2897
+ // connection drops mid-stream (durable-reconnect drop detection).
2898
+ onEvent({ type: "status", status: "idle", terminal: true });
2881
2899
  } else if (payloadType === "execution_error") {
2882
2900
  // Terminal failure. The non-terminal `error` is handled
2883
2901
  // separately (recoverable → warn).
@@ -3131,7 +3149,8 @@ export class AgentWidgetClient {
3131
3149
  const e = payload.error;
3132
3150
  if (typeof e === "string" && e !== "") {
3133
3151
  resolvedError = new Error(e);
3134
- } else if (e != null && typeof e === "object" && "message" in e) {
3152
+ } else if (e != null && typeof e === "object" && Reflect.has(e, "message")) {
3153
+ // Reflect.has, not `in` — see the note on the equivalent guard above.
3135
3154
  resolvedError = new Error(String((e as { message?: unknown }).message ?? e));
3136
3155
  }
3137
3156
  }
@@ -3163,20 +3182,43 @@ export class AgentWidgetClient {
3163
3182
  const lines = event.split("\n");
3164
3183
  let eventType = "message";
3165
3184
  let data = "";
3185
+ // Durable-reconnect cursor: the SSE `id:` line (the durable row seq).
3186
+ // Only durable, resumable agent executions stamp these (e.g. Claude
3187
+ // Managed agents, or any async/background run the backend persists and
3188
+ // can replay); other streams carry no cursor. We emit a `cursor`
3189
+ // event AFTER the frame is fully parsed and dispatched, so the session's
3190
+ // `lastEventId` only advances past frames it has actually applied, so the
3191
+ // happy path has no dupes against the server's `seq > after` replay.
3192
+ let frameId: string | null = null;
3166
3193
 
3167
3194
  for (const line of lines) {
3168
3195
  if (line.startsWith("event:")) {
3169
3196
  eventType = line.replace("event:", "").trim();
3170
3197
  } else if (line.startsWith("data:")) {
3171
3198
  data += line.replace("data:", "").trim();
3199
+ } else if (line.startsWith("id:")) {
3200
+ frameId = line.slice(3).trim();
3172
3201
  }
3173
3202
  }
3174
3203
 
3175
- if (!data) continue;
3204
+ const advanceCursor = () => {
3205
+ if (frameId !== null && frameId !== "") {
3206
+ onEvent({ type: "cursor", id: frameId });
3207
+ }
3208
+ };
3209
+
3210
+ // A frame with an `id:` but no `data:` (e.g. a bare keepalive line) is
3211
+ // still a received durable row, so advance the cursor past it.
3212
+ if (!data) {
3213
+ advanceCursor();
3214
+ continue;
3215
+ }
3176
3216
  let payload: any;
3177
3217
  try {
3178
3218
  payload = JSON.parse(data);
3179
3219
  } catch (error) {
3220
+ // Parse failure: the frame was NOT applied. Do NOT advance the cursor
3221
+ // so a reconnect re-fetches this row.
3180
3222
  onEvent({
3181
3223
  type: "error",
3182
3224
  error:
@@ -3209,7 +3251,10 @@ export class AgentWidgetClient {
3209
3251
  if (assistantMessageRef.current && assistantMessageRef.current !== assistantMessage) {
3210
3252
  assistantMessage = assistantMessageRef.current;
3211
3253
  }
3212
- if (handled) continue; // Skip default handling if custom handler processed it
3254
+ if (handled) {
3255
+ advanceCursor();
3256
+ continue; // Skip default handling if custom handler processed it
3257
+ }
3213
3258
  }
3214
3259
 
3215
3260
  // The wire is the wire vocabulary; the handler consumes it
@@ -3217,6 +3262,7 @@ export class AgentWidgetClient {
3217
3262
  // drains straight through.
3218
3263
  seqReadyQueue.push({ payloadType, payload });
3219
3264
  drainReadyQueue();
3265
+ advanceCursor();
3220
3266
  }
3221
3267
  }
3222
3268
 
@@ -41,6 +41,7 @@ export type RuntypeExecutionStreamEvent = ({
41
41
  };
42
42
  type: "execution_complete";
43
43
  }) | ({
44
+ blockReason?: string;
44
45
  code?: string;
45
46
  completedAt?: string;
46
47
  error: string | {
@@ -295,6 +296,7 @@ export type RuntypeExecutionStreamEvent = ({
295
296
  startedAt?: string;
296
297
  subagent?: {
297
298
  agentName?: string;
299
+ parentToolCallId?: string;
298
300
  toolName: string;
299
301
  };
300
302
  timeout?: number;
@@ -311,12 +313,15 @@ export type RuntypeExecutionStreamEvent = ({
311
313
  seq: number;
312
314
  type: "approval_complete";
313
315
  }) | ({
316
+ awaitReason?: string;
314
317
  awaitedAt?: string;
318
+ crawlId?: string;
315
319
  executionId: string;
316
320
  origin?: "webmcp" | "sdk";
317
321
  pageOrigin?: string;
318
322
  parameters?: Record<string, unknown>;
319
323
  seq: number;
324
+ stepId?: string;
320
325
  toolCallId?: string;
321
326
  toolId?: string;
322
327
  toolName?: string;
@@ -386,6 +391,7 @@ export type RuntypeFlowSSEEvent = {
386
391
  totalTokensUsed?: number;
387
392
  type: "flow_complete";
388
393
  } | ({
394
+ blockReason?: string;
389
395
  code?: string;
390
396
  error: string | {
391
397
  code: string;
@@ -406,13 +412,18 @@ export type RuntypeFlowSSEEvent = {
406
412
  type: "flow_error";
407
413
  upgradeUrl?: string;
408
414
  }) | ({
415
+ approvalId?: string;
416
+ awaitReason?: string;
409
417
  awaitedAt: string;
418
+ crawlId?: string;
410
419
  executionId?: string;
411
420
  flowId: string;
412
421
  origin?: "webmcp" | "sdk";
413
422
  pageOrigin?: string;
414
423
  parameters?: Record<string, unknown>;
415
424
  seq?: number;
425
+ stepId?: string;
426
+ timeout?: number;
416
427
  toolCallId?: string;
417
428
  toolId?: string;
418
429
  toolName?: string;
@@ -667,7 +678,7 @@ export type RuntypeClientInitResponse = {
667
678
  welcomeMessage?: string | null;
668
679
  };
669
680
  expiresAt: string;
670
- flow: {
681
+ flow?: {
671
682
  description?: string | null;
672
683
  id: string;
673
684
  name?: string | null;
@@ -689,6 +700,10 @@ export type RuntypeClientChatRequest = {
689
700
  untrustedContentHint?: boolean;
690
701
  }>;
691
702
  clientToolsFingerprint?: string;
703
+ identityProof?: {
704
+ provider: string;
705
+ token: string;
706
+ };
692
707
  inputs?: Record<string, unknown>;
693
708
  messages: Array<{
694
709
  content: string | (Array<{