@agent-native/core 0.84.13 → 0.84.14

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 (59) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +8 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/agent/durable-background.ts +12 -5
  5. package/corpus/core/src/agent/production-agent.ts +38 -4
  6. package/corpus/core/src/agent/run-manager.ts +39 -2
  7. package/corpus/core/src/agent/run-store.ts +55 -11
  8. package/corpus/core/src/client/agent-chat-adapter.ts +21 -3
  9. package/corpus/core/src/client/blocks/library/diagram.tsx +3 -3
  10. package/corpus/core/src/client/chat/message-components.tsx +17 -0
  11. package/corpus/core/src/observability/traces.ts +17 -3
  12. package/corpus/core/src/server/agent-chat-plugin.ts +11 -1
  13. package/corpus/core/src/styles/blocks.css +44 -1
  14. package/corpus/templates/clips/changelog/2026-07-01-chrome-extension-recordings-stream-uploads-while-recording.md +6 -0
  15. package/corpus/templates/clips/chrome-extension/src/background.ts +8 -0
  16. package/corpus/templates/clips/chrome-extension/src/offscreen.ts +61 -16
  17. package/corpus/templates/plan/app/components/plan/wireframe/html-artboard.css +44 -1
  18. package/corpus/templates/plan/changelog/2026-07-01-visual-plan-and-recap-diagram-labels-now-wrap-inside-their-b.md +6 -0
  19. package/dist/agent/durable-background.d.ts.map +1 -1
  20. package/dist/agent/durable-background.js +14 -3
  21. package/dist/agent/durable-background.js.map +1 -1
  22. package/dist/agent/production-agent.d.ts.map +1 -1
  23. package/dist/agent/production-agent.js +26 -3
  24. package/dist/agent/production-agent.js.map +1 -1
  25. package/dist/agent/run-manager.d.ts +2 -0
  26. package/dist/agent/run-manager.d.ts.map +1 -1
  27. package/dist/agent/run-manager.js +28 -3
  28. package/dist/agent/run-manager.js.map +1 -1
  29. package/dist/agent/run-store.d.ts +9 -0
  30. package/dist/agent/run-store.d.ts.map +1 -1
  31. package/dist/agent/run-store.js +47 -11
  32. package/dist/agent/run-store.js.map +1 -1
  33. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  34. package/dist/client/agent-chat-adapter.js +19 -3
  35. package/dist/client/agent-chat-adapter.js.map +1 -1
  36. package/dist/client/blocks/library/diagram.js +3 -3
  37. package/dist/client/blocks/library/diagram.js.map +1 -1
  38. package/dist/client/chat/message-components.d.ts +3 -1
  39. package/dist/client/chat/message-components.d.ts.map +1 -1
  40. package/dist/client/chat/message-components.js +16 -2
  41. package/dist/client/chat/message-components.js.map +1 -1
  42. package/dist/collab/awareness.d.ts +2 -2
  43. package/dist/collab/awareness.d.ts.map +1 -1
  44. package/dist/collab/routes.d.ts +2 -2
  45. package/dist/notifications/routes.d.ts +3 -3
  46. package/dist/observability/routes.d.ts +3 -3
  47. package/dist/observability/traces.d.ts +5 -0
  48. package/dist/observability/traces.d.ts.map +1 -1
  49. package/dist/observability/traces.js +9 -3
  50. package/dist/observability/traces.js.map +1 -1
  51. package/dist/progress/routes.d.ts +1 -1
  52. package/dist/resources/handlers.d.ts +3 -3
  53. package/dist/server/agent-chat-plugin.d.ts +2 -1
  54. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  55. package/dist/server/agent-chat-plugin.js +7 -2
  56. package/dist/server/agent-chat-plugin.js.map +1 -1
  57. package/dist/server/transcribe-voice.d.ts +1 -1
  58. package/dist/styles/blocks.css +44 -1
  59. package/package.json +1 -1
package/corpus/README.md CHANGED
@@ -28,4 +28,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
28
28
  ## Generated Counts
29
29
 
30
30
  - core files: 2043
31
- - template files: 4917
31
+ - template files: 4919
@@ -1,5 +1,13 @@
1
1
  # @agent-native/core
2
2
 
3
+ ## 0.84.14
4
+
5
+ ### Patch Changes
6
+
7
+ - bbc0a56: Persist terminal reasons for agent runs and classify recoverable run-timeout continuations separately in traces.
8
+ - bbc0a56: Improve agent run diagnostics by recording terminal continuation reasons and adding safe run context to copied recovery debug details.
9
+ - bbc0a56: Keep visual plan and recap diagram labels wrapped inside their boxes instead of overflowing authored diagram shapes.
10
+
3
11
  ## 0.84.13
4
12
 
5
13
  ### Patch Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.84.13",
3
+ "version": "0.84.14",
4
4
  "description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -97,6 +97,17 @@ function resolveWorkspaceBackgroundFunctionUrlPath(): string | null {
97
97
  return `/.netlify/functions/${candidate}-agent-background`;
98
98
  }
99
99
 
100
+ function isNetlifyHostedRuntimeForDispatch(): boolean {
101
+ if (process.env.NETLIFY_LOCAL === "true") return false;
102
+ if (process.env.NETLIFY === "false") return false;
103
+ if (process.env.NETLIFY && process.env.NETLIFY !== "false") return true;
104
+ // Netlify sets AWS Lambda runtime env on deployed Functions, but the build-time
105
+ // NETLIFY flag is not always present in the runtime isolate. Treat Lambda as
106
+ // Netlify here unless Netlify was explicitly disabled above; non-Netlify AWS
107
+ // falls back inline if the /.netlify/functions dispatch fast-fails.
108
+ return Boolean(process.env.AWS_LAMBDA_FUNCTION_NAME);
109
+ }
110
+
100
111
  /**
101
112
  * Resolve the path the foreground POST should self-dispatch the chat background
102
113
  * worker to.
@@ -129,11 +140,7 @@ function resolveWorkspaceBackgroundFunctionUrlPath(): string | null {
129
140
  * to shadow because `/.netlify/*` is already excluded from the `server` catch-all.
130
141
  */
131
142
  export function resolveAgentChatProcessRunDispatchPath(): string {
132
- if (
133
- process.env.NETLIFY &&
134
- process.env.NETLIFY !== "false" &&
135
- process.env.NETLIFY_LOCAL !== "true"
136
- ) {
143
+ if (isNetlifyHostedRuntimeForDispatch()) {
137
144
  return (
138
145
  resolveWorkspaceBackgroundFunctionUrlPath() ??
139
146
  AGENT_BACKGROUND_FUNCTION_URL_PATH
@@ -124,6 +124,7 @@ import {
124
124
  insertRun,
125
125
  updateRunHeartbeat,
126
126
  updateRunStatusIfRunning,
127
+ setRunTerminalReason,
127
128
  claimBackgroundRun,
128
129
  readBackgroundRunClaim,
129
130
  recordRunDiagnostic,
@@ -5089,7 +5090,17 @@ export function createProductionAgentHandler(
5089
5090
  }
5090
5091
  // A background worker owns this run but we cannot subscribe — surface an
5091
5092
  // error rather than risk a double-run by falling through to inline.
5092
- await updateRunStatusIfRunning(runId, "errored").catch(() => {});
5093
+ const terminalReason =
5094
+ backgroundOutcome.action === "stream"
5095
+ ? "background_subscribe_failed"
5096
+ : "background_dispatch_failed";
5097
+ const statusUpdated = await updateRunStatusIfRunning(
5098
+ runId,
5099
+ "errored",
5100
+ ).catch(() => false);
5101
+ if (statusUpdated) {
5102
+ await setRunTerminalReason(runId, terminalReason).catch(() => {});
5103
+ }
5093
5104
  setResponseStatus(event, 500);
5094
5105
  return {
5095
5106
  error:
@@ -5333,9 +5344,16 @@ export function createProductionAgentHandler(
5333
5344
  "[agent-chat] background continuation dispatch failed:",
5334
5345
  chainErr instanceof Error ? chainErr.message : chainErr,
5335
5346
  );
5336
- await updateRunStatusIfRunning(runId, "errored").catch(
5337
- () => {},
5338
- );
5347
+ const statusUpdated = await updateRunStatusIfRunning(
5348
+ runId,
5349
+ "errored",
5350
+ ).catch(() => false);
5351
+ if (statusUpdated) {
5352
+ await setRunTerminalReason(
5353
+ runId,
5354
+ "background_continuation_dispatch_failed",
5355
+ ).catch(() => {});
5356
+ }
5339
5357
  }
5340
5358
  }
5341
5359
  } finally {
@@ -5766,6 +5784,22 @@ export function createProductionAgentHandler(
5766
5784
  threadId: threadId ?? null,
5767
5785
  userId: ownerEmail,
5768
5786
  config: obsConfig,
5787
+ classifyError: () => {
5788
+ if (
5789
+ agentLoopOpts.signal.aborted &&
5790
+ agentLoopOpts.signal.reason === "run_timeout"
5791
+ ) {
5792
+ return {
5793
+ status: "success",
5794
+ errorMessage: null,
5795
+ metadata: {
5796
+ terminalReason: "run_timeout",
5797
+ recoverableContinuation: true,
5798
+ },
5799
+ };
5800
+ }
5801
+ return null;
5802
+ },
5769
5803
  });
5770
5804
  }
5771
5805
  } catch (err) {
@@ -18,6 +18,7 @@ import {
18
18
  reapUnclaimedBackgroundRun,
19
19
  ensureTerminalRunEvent,
20
20
  setRunError,
21
+ setRunTerminalReason,
21
22
  STALE_RUN_ERROR_EVENT,
22
23
  } from "./run-store.js";
23
24
  import type { AgentChatEvent, RunEvent, RunStatus } from "./types.js";
@@ -293,6 +294,26 @@ function isTerminalRunEvent(event: AgentChatEvent): boolean {
293
294
  );
294
295
  }
295
296
 
297
+ function terminalReasonForRun(
298
+ finalStatus: "completed" | "errored" | "aborted",
299
+ terminalEvent: AgentChatEvent | null,
300
+ abortReason: string | undefined,
301
+ completionError: unknown,
302
+ ): string {
303
+ if (terminalEvent?.type === "auto_continue") {
304
+ return terminalEvent.reason || "auto_continue";
305
+ }
306
+ if (terminalEvent?.type === "loop_limit") return "loop_limit";
307
+ if (terminalEvent?.type === "missing_api_key") return "missing_api_key";
308
+ if (terminalEvent?.type === "error") {
309
+ return `error:${terminalEvent.errorCode || "unknown"}`;
310
+ }
311
+ if (finalStatus === "aborted") return `aborted:${abortReason ?? "user"}`;
312
+ if (completionError) return "completion_error";
313
+ if (finalStatus === "errored") return "error:unknown";
314
+ return "done";
315
+ }
316
+
296
317
  function abortInMemoryRun(run: ActiveRun, reason: string = "user") {
297
318
  run.abortReason = reason;
298
319
  run.status = "aborted";
@@ -459,7 +480,7 @@ export function startRun(
459
480
  type: "auto_continue",
460
481
  reason: "run_timeout",
461
482
  });
462
- abort.abort();
483
+ abort.abort("run_timeout");
463
484
  }, softTimeoutMs)
464
485
  : null;
465
486
  let pendingTerminalEvent: RunEvent | null = null;
@@ -634,6 +655,12 @@ export function startRun(
634
655
  : run.status === "errored" || completionError
635
656
  ? "errored"
636
657
  : "completed";
658
+ const terminalReason = terminalReasonForRun(
659
+ finalStatus,
660
+ pendingTerminalEvent?.event ?? null,
661
+ run.abortReason,
662
+ completionError,
663
+ );
637
664
 
638
665
  // 3. Emit the terminal event only after thread_data is durable. Live
639
666
  // SSE clients close on this event and usually fetch thread_data
@@ -706,7 +733,13 @@ export function startRun(
706
733
  try {
707
734
  await insertRunPromise;
708
735
  if (!terminalPersistenceError) {
709
- await updateRunStatusIfRunning(runId, finalStatus);
736
+ const statusUpdated = await updateRunStatusIfRunning(
737
+ runId,
738
+ finalStatus,
739
+ );
740
+ if (statusUpdated) {
741
+ await setRunTerminalReason(runId, terminalReason);
742
+ }
710
743
  }
711
744
  } catch {
712
745
  // Best-effort — reapIfStale will eventually clean this up via
@@ -1077,6 +1110,8 @@ export async function getActiveRunForThreadAsync(threadId: string): Promise<{
1077
1110
  lastProgressAt: number | null;
1078
1111
  /** How the run was dispatched (NULL/foreground, background, background-processing). */
1079
1112
  dispatchMode?: string | null;
1113
+ /** Compact terminal classification, e.g. done, run_timeout, stale_run. */
1114
+ terminalReason?: string | null;
1080
1115
  /**
1081
1116
  * Last reached `_process-run` worker stage as a JSON string
1082
1117
  * `{stage,detail?,at}`. Surfaced so a silent background-worker death is
@@ -1143,6 +1178,7 @@ export async function getActiveRunForThreadAsync(threadId: string): Promise<{
1143
1178
  heartbeatAt: sqlRun.heartbeatAt ?? sqlRun.startedAt,
1144
1179
  lastProgressAt: sqlRun.lastProgressAt,
1145
1180
  dispatchMode: sqlRun.dispatchMode,
1181
+ terminalReason: sqlRun.terminalReason,
1146
1182
  diagStage: sqlRun.diagStage,
1147
1183
  };
1148
1184
  }
@@ -1170,6 +1206,7 @@ export async function getActiveRunForThreadAsync(threadId: string): Promise<{
1170
1206
  heartbeatAt: sqlRun.heartbeatAt ?? sqlRun.startedAt,
1171
1207
  lastProgressAt: sqlRun.lastProgressAt,
1172
1208
  dispatchMode: sqlRun.dispatchMode,
1209
+ terminalReason: sqlRun.terminalReason,
1173
1210
  diagStage: sqlRun.diagStage,
1174
1211
  };
1175
1212
  }
@@ -116,6 +116,7 @@ async function ensureRunTables(): Promise<void> {
116
116
  turn_id TEXT,
117
117
  error_code TEXT,
118
118
  error_detail TEXT,
119
+ terminal_reason TEXT,
119
120
  dispatch_mode TEXT,
120
121
  diag_stage TEXT
121
122
  )
@@ -187,6 +188,7 @@ async function ensureRunTables(): Promise<void> {
187
188
  ["turn_id", "TEXT"],
188
189
  ["error_code", "TEXT"],
189
190
  ["error_detail", "TEXT"],
191
+ ["terminal_reason", "TEXT"],
190
192
  ["dispatch_mode", "TEXT"],
191
193
  ["diag_stage", "TEXT"],
192
194
  ["worker_stage", "TEXT"],
@@ -264,6 +266,7 @@ async function ensureRunTables(): Promise<void> {
264
266
  "turn_id",
265
267
  "error_code",
266
268
  "error_detail",
269
+ "terminal_reason",
267
270
  "dispatch_mode",
268
271
  "diag_stage",
269
272
  "worker_stage",
@@ -621,6 +624,28 @@ export async function setRunError(
621
624
  }
622
625
  }
623
626
 
627
+ /**
628
+ * Record why a run reached its terminal status. Unlike error_code/error_detail,
629
+ * this is set for successful checkpoint boundaries too (for example
630
+ * status='completed' + terminal_reason='run_timeout').
631
+ */
632
+ export async function setRunTerminalReason(
633
+ runId: string,
634
+ terminalReason: string | undefined,
635
+ ): Promise<void> {
636
+ if (!terminalReason) return;
637
+ try {
638
+ await ensureRunTables();
639
+ const client = getDbExec();
640
+ await client.execute({
641
+ sql: `UPDATE agent_runs SET terminal_reason = ? WHERE id = ?`,
642
+ args: [terminalReason.slice(0, 200), runId],
643
+ });
644
+ } catch {
645
+ // Diagnostics are best-effort; never let them break completion.
646
+ }
647
+ }
648
+
624
649
  /**
625
650
  * Diagnostic stage names recorded onto a background run as it moves through the
626
651
  * `_process-run` worker pipeline. Each value is the LAST stage successfully
@@ -765,7 +790,8 @@ export async function reapIfStale(
765
790
  SET status = 'errored',
766
791
  completed_at = ?,
767
792
  error_code = ?,
768
- error_detail = ?
793
+ error_detail = ?,
794
+ terminal_reason = ?
769
795
  WHERE id = ?
770
796
  AND status = 'running'
771
797
  AND ${staleClause}`,
@@ -773,6 +799,7 @@ export async function reapIfStale(
773
799
  completedAt,
774
800
  STALE_RUN_ERROR_EVENT.errorCode,
775
801
  STALE_RUN_ERROR_EVENT.details,
802
+ STALE_RUN_ERROR_EVENT.errorCode,
776
803
  runId,
777
804
  ...staleArgs,
778
805
  ],
@@ -821,7 +848,8 @@ export async function reapUnclaimedBackgroundRun(
821
848
  SET status = 'errored',
822
849
  completed_at = ?,
823
850
  error_code = ?,
824
- error_detail = ?
851
+ error_detail = ?,
852
+ terminal_reason = ?
825
853
  WHERE id = ?
826
854
  AND status = 'running'
827
855
  AND dispatch_mode = 'background'
@@ -830,6 +858,7 @@ export async function reapUnclaimedBackgroundRun(
830
858
  completedAt,
831
859
  UNCLAIMED_BACKGROUND_RUN_ERROR_EVENT.errorCode,
832
860
  UNCLAIMED_BACKGROUND_RUN_ERROR_EVENT.details,
861
+ UNCLAIMED_BACKGROUND_RUN_ERROR_EVENT.errorCode,
833
862
  runId,
834
863
  cutoff,
835
864
  ],
@@ -902,8 +931,8 @@ export async function markRunAborted(
902
931
  await ensureRunTables();
903
932
  const client = getDbExec();
904
933
  await client.execute({
905
- sql: `UPDATE agent_runs SET status = 'aborted', abort_reason = ?, completed_at = ? WHERE id = ?`,
906
- args: [reason ?? "user", Date.now(), runId],
934
+ sql: `UPDATE agent_runs SET status = 'aborted', abort_reason = ?, completed_at = ?, terminal_reason = ? WHERE id = ?`,
935
+ args: [reason ?? "user", Date.now(), `aborted:${reason ?? "user"}`, runId],
907
936
  });
908
937
  await safeAppendTerminalRunEvent(runId, { type: "done" }, "mark-aborted");
909
938
  }
@@ -1005,13 +1034,14 @@ export async function getRunByThread(
1005
1034
  completedAt: number | null;
1006
1035
  lastProgressAt: number | null;
1007
1036
  dispatchMode: string | null;
1037
+ terminalReason: string | null;
1008
1038
  diagStage: string | null;
1009
1039
  } | null> {
1010
1040
  await ensureRunTables();
1011
1041
  const client = getDbExec();
1012
1042
  const sql = options?.includeTerminal
1013
- ? `SELECT id, thread_id, turn_id, status, started_at, heartbeat_at, completed_at, last_progress_at, dispatch_mode, diag_stage FROM agent_runs WHERE thread_id = ? ORDER BY started_at DESC LIMIT 1`
1014
- : `SELECT id, thread_id, turn_id, status, started_at, heartbeat_at, completed_at, last_progress_at, dispatch_mode, diag_stage FROM agent_runs WHERE thread_id = ? AND status = 'running' ORDER BY started_at DESC LIMIT 1`;
1043
+ ? `SELECT id, thread_id, turn_id, status, started_at, heartbeat_at, completed_at, last_progress_at, dispatch_mode, terminal_reason, diag_stage FROM agent_runs WHERE thread_id = ? ORDER BY started_at DESC LIMIT 1`
1044
+ : `SELECT id, thread_id, turn_id, status, started_at, heartbeat_at, completed_at, last_progress_at, dispatch_mode, terminal_reason, diag_stage FROM agent_runs WHERE thread_id = ? AND status = 'running' ORDER BY started_at DESC LIMIT 1`;
1015
1045
  const { rows } = await client.execute({ sql, args: [threadId] });
1016
1046
  if (rows.length === 0) return null;
1017
1047
  const r = rows[0] as {
@@ -1024,6 +1054,7 @@ export async function getRunByThread(
1024
1054
  completed_at: number | string | null;
1025
1055
  last_progress_at: number | string | null;
1026
1056
  dispatch_mode?: string | null;
1057
+ terminal_reason?: string | null;
1027
1058
  diag_stage?: string | null;
1028
1059
  };
1029
1060
  return {
@@ -1037,6 +1068,7 @@ export async function getRunByThread(
1037
1068
  lastProgressAt:
1038
1069
  r.last_progress_at == null ? null : Number(r.last_progress_at),
1039
1070
  dispatchMode: r.dispatch_mode ?? null,
1071
+ terminalReason: r.terminal_reason ?? null,
1040
1072
  diagStage: r.diag_stage ?? null,
1041
1073
  };
1042
1074
  }
@@ -1053,6 +1085,7 @@ export interface AgentRunSummary {
1053
1085
  errorCode: string | null;
1054
1086
  abortReason: string | null;
1055
1087
  dispatchMode: string | null;
1088
+ terminalReason: string | null;
1056
1089
  /** Last reached `_process-run` worker stage (JSON `{stage,detail?,at}`). */
1057
1090
  diagStage: string | null;
1058
1091
  }
@@ -1065,7 +1098,7 @@ export async function listRunsForThread(
1065
1098
  const limit = Math.min(Math.max(options.limit ?? 10, 1), 50);
1066
1099
  const client = getDbExec();
1067
1100
  const { rows } = await client.execute({
1068
- sql: `SELECT id, thread_id, turn_id, status, started_at, heartbeat_at, completed_at, last_progress_at, error_code, abort_reason, dispatch_mode, diag_stage
1101
+ sql: `SELECT id, thread_id, turn_id, status, started_at, heartbeat_at, completed_at, last_progress_at, error_code, abort_reason, dispatch_mode, terminal_reason, diag_stage
1069
1102
  FROM agent_runs
1070
1103
  WHERE thread_id = ?
1071
1104
  ORDER BY started_at DESC
@@ -1085,6 +1118,7 @@ export async function listRunsForThread(
1085
1118
  error_code?: string | null;
1086
1119
  abort_reason?: string | null;
1087
1120
  dispatch_mode?: string | null;
1121
+ terminal_reason?: string | null;
1088
1122
  diag_stage?: string | null;
1089
1123
  };
1090
1124
  return {
@@ -1100,6 +1134,7 @@ export async function listRunsForThread(
1100
1134
  errorCode: row.error_code ?? null,
1101
1135
  abortReason: row.abort_reason ?? null,
1102
1136
  dispatchMode: row.dispatch_mode ?? null,
1137
+ terminalReason: row.terminal_reason ?? null,
1103
1138
  diagStage: row.diag_stage ?? null,
1104
1139
  };
1105
1140
  });
@@ -1183,13 +1218,15 @@ export async function reapAllStaleRuns(): Promise<number> {
1183
1218
  SET status = 'errored',
1184
1219
  completed_at = ?,
1185
1220
  error_code = ?,
1186
- error_detail = ?
1221
+ error_detail = ?,
1222
+ terminal_reason = ?
1187
1223
  WHERE status = 'running'
1188
1224
  AND ${livenessBasisSql()} < ${backgroundAwareStaleCutoffSql()}`,
1189
1225
  args: [
1190
1226
  completedAt,
1191
1227
  STALE_RUN_ERROR_EVENT.errorCode,
1192
1228
  STALE_RUN_ERROR_EVENT.details,
1229
+ STALE_RUN_ERROR_EVENT.errorCode,
1193
1230
  completedAt,
1194
1231
  ],
1195
1232
  });
@@ -1241,12 +1278,14 @@ export async function cleanupOldRuns(
1241
1278
  SET status = 'errored',
1242
1279
  completed_at = ?,
1243
1280
  error_code = ?,
1244
- error_detail = ?
1281
+ error_detail = ?,
1282
+ terminal_reason = ?
1245
1283
  WHERE status = 'running' AND started_at < ?`,
1246
1284
  args: [
1247
1285
  completedAt,
1248
1286
  STALE_RUN_ERROR_EVENT.errorCode,
1249
1287
  STALE_RUN_ERROR_EVENT.details,
1288
+ STALE_RUN_ERROR_EVENT.errorCode,
1250
1289
  cutoff,
1251
1290
  ],
1252
1291
  });
@@ -1257,13 +1296,15 @@ export async function cleanupOldRuns(
1257
1296
  SET status = 'errored',
1258
1297
  completed_at = ?,
1259
1298
  error_code = ?,
1260
- error_detail = ?
1299
+ error_detail = ?,
1300
+ terminal_reason = ?
1261
1301
  WHERE status = 'running'
1262
1302
  AND ${livenessBasisSql()} < ${backgroundAwareStaleCutoffSql()}`,
1263
1303
  args: [
1264
1304
  completedAt,
1265
1305
  STALE_RUN_ERROR_EVENT.errorCode,
1266
1306
  STALE_RUN_ERROR_EVENT.details,
1307
+ STALE_RUN_ERROR_EVENT.errorCode,
1267
1308
  completedAt,
1268
1309
  ],
1269
1310
  });
@@ -1312,6 +1353,7 @@ export async function listErroredRuns(options?: {
1312
1353
  status: string;
1313
1354
  errorCode: string | null;
1314
1355
  errorDetail: string | null;
1356
+ terminalReason: string | null;
1315
1357
  startedAt: number;
1316
1358
  completedAt: number | null;
1317
1359
  durationMs: number | null;
@@ -1323,7 +1365,7 @@ export async function listErroredRuns(options?: {
1323
1365
  const since =
1324
1366
  options?.sinceMs && options.sinceMs > 0 ? Date.now() - options.sinceMs : 0;
1325
1367
  const { rows } = await client.execute({
1326
- sql: `SELECT id, thread_id, turn_id, status, error_code, error_detail, started_at, completed_at
1368
+ sql: `SELECT id, thread_id, turn_id, status, error_code, error_detail, terminal_reason, started_at, completed_at
1327
1369
  FROM agent_runs
1328
1370
  WHERE status IN ('errored', 'aborted')
1329
1371
  AND COALESCE(completed_at, started_at) >= ?
@@ -1339,6 +1381,7 @@ export async function listErroredRuns(options?: {
1339
1381
  status: string;
1340
1382
  error_code: string | null;
1341
1383
  error_detail: string | null;
1384
+ terminal_reason: string | null;
1342
1385
  started_at: number | string;
1343
1386
  completed_at: number | string | null;
1344
1387
  };
@@ -1352,6 +1395,7 @@ export async function listErroredRuns(options?: {
1352
1395
  status: row.status,
1353
1396
  errorCode: row.error_code ?? null,
1354
1397
  errorDetail: row.error_detail ?? null,
1398
+ terminalReason: row.terminal_reason ?? null,
1355
1399
  startedAt,
1356
1400
  completedAt,
1357
1401
  durationMs: completedAt == null ? null : completedAt - startedAt,
@@ -1461,8 +1461,29 @@ export function createAgentChatAdapter(
1461
1461
  const nextContinuationToolCallId = () =>
1462
1462
  `continuation_tc_${++continuationToolCallCounter}`;
1463
1463
 
1464
+ const runDebugContextDetails = (): string => {
1465
+ const pageOrigin =
1466
+ typeof window !== "undefined" && window.location?.origin
1467
+ ? window.location.origin
1468
+ : "";
1469
+ return [
1470
+ `api_url: ${apiUrl}`,
1471
+ pageOrigin ? `page_origin: ${pageOrigin}` : "",
1472
+ tabId ? `tab_id: ${tabId}` : "",
1473
+ threadId ? `thread_id: ${threadId}` : "",
1474
+ `turn_id: ${turnId}`,
1475
+ runId ? `current_run: ${runId}` : "",
1476
+ attemptedRunIds.length > 0
1477
+ ? `attempted_runs: ${attemptedRunIds.join(", ")}`
1478
+ : "",
1479
+ ]
1480
+ .filter(Boolean)
1481
+ .join("\n");
1482
+ };
1483
+
1464
1484
  const connectionRecoveryDetails = (): string => {
1465
1485
  return [
1486
+ runDebugContextDetails(),
1466
1487
  lastAutoContinueReason
1467
1488
  ? `last_auto_continue_reason: ${lastAutoContinueReason}`
1468
1489
  : "",
@@ -1488,9 +1509,6 @@ export function createAgentChatAdapter(
1488
1509
  ? `activity_trail: ${formatActivityTrail(lastActivityTrail)}`
1489
1510
  : "",
1490
1511
  `total_transient_continuations: ${totalTransientContinuationAttempts}`,
1491
- attemptedRunIds.length > 0
1492
- ? `attempted_runs: ${attemptedRunIds.join(", ")}`
1493
- : "",
1494
1512
  ]
1495
1513
  .filter(Boolean)
1496
1514
  .join("\n");
@@ -291,7 +291,7 @@ function PositionedDiagram({
291
291
  return (
292
292
  <span
293
293
  key={`${edge.from}-${edge.to}-${index}-label`}
294
- className="absolute z-10 max-w-[130px] -translate-x-1/2 -translate-y-1/2 rounded-full border border-border bg-background px-2 py-0.5 text-center text-[11px] font-semibold text-muted-foreground shadow-sm"
294
+ className="absolute z-10 max-w-[130px] -translate-x-1/2 -translate-y-1/2 break-words rounded-full border border-border bg-background px-2 py-0.5 text-center text-[11px] font-semibold text-muted-foreground shadow-sm [overflow-wrap:anywhere]"
295
295
  style={{
296
296
  left: `${(from.displayX + to.displayX) / 2}%`,
297
297
  top: `${(from.displayY + to.displayY) / 2}%`,
@@ -305,7 +305,7 @@ function PositionedDiagram({
305
305
  {displayNodesForDirection.map((node, index) => (
306
306
  <article
307
307
  key={node.id}
308
- className="absolute z-20 -translate-x-1/2 -translate-y-1/2 rounded-xl border-2 border-border bg-background p-3 text-foreground shadow-sm"
308
+ className="absolute z-20 -translate-x-1/2 -translate-y-1/2 break-words rounded-xl border-2 border-border bg-background p-3 text-foreground shadow-sm [overflow-wrap:anywhere]"
309
309
  style={{
310
310
  left: `${node.displayX}%`,
311
311
  top: `${node.displayY}%`,
@@ -485,7 +485,7 @@ function SequenceDiagram({
485
485
  {next && (
486
486
  <div className="grid min-w-[72px] justify-items-center gap-1 text-muted-foreground">
487
487
  {edge?.label && (
488
- <span className="max-w-[96px] truncate rounded-full border border-border px-2 py-0.5 text-[11px] font-semibold">
488
+ <span className="max-w-[96px] whitespace-normal break-words rounded-full border border-border px-2 py-0.5 text-center text-[11px] font-semibold [overflow-wrap:anywhere]">
489
489
  {edge.label}
490
490
  </span>
491
491
  )}
@@ -784,19 +784,31 @@ function assistantMessageStatusIsTerminal(message: {
784
784
  return statusType === "complete" || statusType === "incomplete";
785
785
  }
786
786
 
787
+ export function assistantMessageHasUnresolvedTool(content: unknown): boolean {
788
+ if (!Array.isArray(content)) return false;
789
+ return content.some((part): boolean => {
790
+ if (!part || typeof part !== "object") return false;
791
+ const record = part as { type?: unknown; result?: unknown };
792
+ return record.type === "tool-call" && record.result === undefined;
793
+ });
794
+ }
795
+
787
796
  export function shouldShowAssistantMessageFooter({
788
797
  isLast,
789
798
  chatRunning,
790
799
  hasRenderableContent,
791
800
  statusIsTerminal,
801
+ hasUnresolvedTool,
792
802
  }: {
793
803
  isLast: boolean;
794
804
  chatRunning: boolean;
795
805
  hasRenderableContent: boolean;
796
806
  statusIsTerminal: boolean;
807
+ hasUnresolvedTool?: boolean;
797
808
  }): boolean {
798
809
  if (!hasRenderableContent) return false;
799
810
  if (!isLast) return true;
811
+ if (hasUnresolvedTool) return false;
800
812
  return !chatRunning && statusIsTerminal;
801
813
  }
802
814
 
@@ -813,11 +825,13 @@ export function AssistantMessage() {
813
825
  thread.messages.length > 0 &&
814
826
  thread.messages[thread.messages.length - 1].id === msg.id;
815
827
  const hasRenderableContent = assistantMessageHasRenderableContent(msg);
828
+ const hasUnresolvedTool = assistantMessageHasUnresolvedTool(msg.content);
816
829
  const isComplete = shouldShowAssistantMessageFooter({
817
830
  isLast,
818
831
  chatRunning,
819
832
  hasRenderableContent,
820
833
  statusIsTerminal: assistantMessageStatusIsTerminal(msg),
834
+ hasUnresolvedTool,
821
835
  });
822
836
  const cpCtx = React.useContext(CheckpointContext);
823
837
 
@@ -907,6 +921,9 @@ export function AssistantMessage() {
907
921
  {isComplete && hasCodeAgentTools && msgContent && (
908
922
  <FilesChangedSummary parts={msgContent} />
909
923
  )}
924
+ {isLast && hasUnresolvedTool && !chatRunning && (
925
+ <RunningActivityStatus label="Thinking" />
926
+ )}
910
927
  </div>
911
928
  {isComplete && (
912
929
  <div className="mt-1 flex items-center justify-between">
@@ -89,6 +89,14 @@ export async function instrumentAgentLoop(opts: {
89
89
  * reads. */
90
90
  userId: string | null;
91
91
  config: ObservabilityConfig;
92
+ classifyError?: (error: unknown) =>
93
+ | {
94
+ status?: "success" | "error";
95
+ errorMessage?: string | null;
96
+ metadata?: Record<string, unknown> | null;
97
+ }
98
+ | null
99
+ | undefined;
92
100
  }): Promise<AgentLoopUsage> {
93
101
  const { runAgentLoop, loopOpts, runId, threadId, userId, config } = opts;
94
102
  const runStart = Date.now();
@@ -260,11 +268,17 @@ export async function instrumentAgentLoop(opts: {
260
268
  let usage: AgentLoopUsage | undefined;
261
269
  let runStatus: "success" | "error" = "success";
262
270
  let errorMessage: string | null = null;
271
+ let runMetadata: Record<string, unknown> | null = null;
263
272
  try {
264
273
  usage = await runAgentLoop({ ...loopOpts, send: instrumentedSend });
265
274
  } catch (err: any) {
266
- runStatus = "error";
267
- errorMessage = err?.message ?? String(err);
275
+ const classification = opts.classifyError?.(err) ?? null;
276
+ runStatus = classification?.status ?? "error";
277
+ errorMessage =
278
+ classification?.errorMessage === undefined
279
+ ? (err?.message ?? String(err))
280
+ : classification.errorMessage;
281
+ runMetadata = classification?.metadata ?? null;
268
282
  throw err;
269
283
  } finally {
270
284
  const runEnd = Date.now();
@@ -325,7 +339,7 @@ export async function instrumentAgentLoop(opts: {
325
339
  durationMs: totalDurationMs,
326
340
  status: runStatus,
327
341
  errorMessage,
328
- metadata: null,
342
+ metadata: runMetadata,
329
343
  createdAt: runStart,
330
344
  };
331
345
  spans.push(parentSpan);