@agent-native/core 0.84.49 → 0.84.51

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.
@@ -1,5 +1,17 @@
1
1
  # @agent-native/core
2
2
 
3
+ ## 0.84.51
4
+
5
+ ### Patch Changes
6
+
7
+ - 55e4678: Recover durable background chat runs when action input preparation stops making byte progress, and avoid duplicate tool cards when reconnects replay completed tool events.
8
+
9
+ ## 0.84.50
10
+
11
+ ### Patch Changes
12
+
13
+ - 24a6bd2: Retry agent chat startup timeouts before showing an error when no run stream starts.
14
+
3
15
  ## 0.84.49
4
16
 
5
17
  ### Patch Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.84.49",
3
+ "version": "0.84.51",
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": {
@@ -1380,6 +1380,7 @@ export function createAgentChatAdapter(
1380
1380
  const turnId = generateTurnId();
1381
1381
  let runId: string | null = null;
1382
1382
  let lastSeq = -1;
1383
+ const seenRunSeqs = new Map<string, number>();
1383
1384
  let currentRunDispatchMode: string | null = null;
1384
1385
  let currentMessageText = normalizeMentions(
1385
1386
  recoveryMessageText.trim() || userMessageText,
@@ -1548,6 +1549,27 @@ export function createAgentChatAdapter(
1548
1549
  if (mode) currentRunDispatchMode = mode;
1549
1550
  };
1550
1551
 
1552
+ const rememberRunSeq = (seq: number) => {
1553
+ lastSeq = seq;
1554
+ if (runId) {
1555
+ seenRunSeqs.set(runId, seq);
1556
+ }
1557
+ };
1558
+
1559
+ const reconnectCursorForRun = (
1560
+ nextRunId: string,
1561
+ previousRunId: string | null,
1562
+ ) => {
1563
+ const rememberedSeq = seenRunSeqs.get(nextRunId);
1564
+ if (rememberedSeq !== undefined) {
1565
+ lastSeq = rememberedSeq;
1566
+ return;
1567
+ }
1568
+ if (previousRunId !== nextRunId) {
1569
+ lastSeq = -1;
1570
+ }
1571
+ };
1572
+
1551
1573
  const currentSSEOptions = () => ({
1552
1574
  durableBackgroundRun:
1553
1575
  currentRunDispatchMode?.startsWith("background") === true,
@@ -1671,7 +1693,7 @@ export function createAgentChatAdapter(
1671
1693
  toolCallCounter,
1672
1694
  tabId,
1673
1695
  (seq) => {
1674
- lastSeq = seq;
1696
+ rememberRunSeq(seq);
1675
1697
  if (threadId) updateActiveRunSeq(seq);
1676
1698
  },
1677
1699
  runId,
@@ -1765,12 +1787,13 @@ export function createAgentChatAdapter(
1765
1787
  return false;
1766
1788
  }
1767
1789
  const activeRunId = String(active.runId);
1790
+ const previousRunId = runId;
1768
1791
  runId = activeRunId;
1769
1792
  if (!attemptedRunIds.includes(activeRunId)) {
1770
1793
  attemptedRunIds.push(activeRunId);
1771
1794
  }
1772
- lastSeq = -1;
1773
- setActiveRun({ threadId, runId: activeRunId, lastSeq: -1 });
1795
+ reconnectCursorForRun(activeRunId, previousRunId);
1796
+ setActiveRun({ threadId, runId: activeRunId, lastSeq });
1774
1797
  const reconnected = yield* reconnectCurrentRun();
1775
1798
  if (reconnected) return true;
1776
1799
  }
@@ -1844,12 +1867,13 @@ export function createAgentChatAdapter(
1844
1867
  if (activeStatus !== "running" && activeStatus !== "starting") {
1845
1868
  return false;
1846
1869
  }
1870
+ const previousRunId = runId;
1847
1871
  runId = activeRunId;
1848
1872
  if (!attemptedRunIds.includes(activeRunId)) {
1849
1873
  attemptedRunIds.push(activeRunId);
1850
1874
  }
1851
- lastSeq = -1;
1852
- setActiveRun({ threadId, runId: activeRunId, lastSeq: -1 });
1875
+ reconnectCursorForRun(activeRunId, previousRunId);
1876
+ setActiveRun({ threadId, runId: activeRunId, lastSeq });
1853
1877
  const reconnected = yield* reconnectCurrentRun();
1854
1878
  if (reconnected) return true;
1855
1879
  } catch (activeErr: unknown) {
@@ -2290,13 +2314,14 @@ export function createAgentChatAdapter(
2290
2314
  }
2291
2315
  if (activeRunId) {
2292
2316
  try {
2317
+ const previousRunId = runId;
2293
2318
  runId = activeRunId;
2294
2319
  if (!attemptedRunIds.includes(runId)) {
2295
2320
  attemptedRunIds.push(runId);
2296
2321
  }
2297
- lastSeq = -1;
2322
+ reconnectCursorForRun(activeRunId, previousRunId);
2298
2323
  if (threadId) {
2299
- setActiveRun({ threadId, runId, lastSeq: -1 });
2324
+ setActiveRun({ threadId, runId, lastSeq });
2300
2325
  }
2301
2326
  const reconnected = yield* reconnectCurrentRun();
2302
2327
  if (reconnected) return;
@@ -2423,7 +2448,7 @@ export function createAgentChatAdapter(
2423
2448
  toolCallCounter,
2424
2449
  tabId,
2425
2450
  (seq) => {
2426
- lastSeq = seq;
2451
+ rememberRunSeq(seq);
2427
2452
  if (runId && threadId) {
2428
2453
  updateActiveRunSeq(seq);
2429
2454
  }
@@ -2616,10 +2641,16 @@ export function createAgentChatAdapter(
2616
2641
  if (activeReconnected) return;
2617
2642
 
2618
2643
  if (err instanceof AgentStartupTimeoutError) {
2644
+ if (startupRecoveryAttempts < MAX_STARTUP_RECOVERY_ATTEMPTS) {
2645
+ await retryDelay(startupRecoveryAttempts++, abortSignal);
2646
+ if (abortSignal.aborted) return;
2647
+ continue;
2648
+ }
2619
2649
  const message =
2620
- "The agent chat endpoint accepted the request but did not start streaming in time. This usually means prompt setup, the LLM gateway, or the provider is stalled.";
2650
+ "The agent chat endpoint did not start streaming in time after several recovery attempts. This usually means prompt setup, the LLM gateway, or the provider is stalled.";
2621
2651
  captureChatClientError(err, "startup-timeout", {
2622
2652
  timeoutMs: err.timeoutMs,
2653
+ startupRecoveryAttempts,
2623
2654
  });
2624
2655
  const runError = {
2625
2656
  message,
@@ -151,9 +151,10 @@ export const SSE_ACTION_PREPARATION_STALL_TIMEOUT_MS = 90_000;
151
151
  export interface SSEStreamOptions {
152
152
  /**
153
153
  * Durable background runs have their own server-side liveness budget and
154
- * heartbeat. While one is active, keepalive-only periods and zero-byte
155
- * action-preparation activity should keep the client attached instead of
156
- * aborting and starting duplicate continuations.
154
+ * heartbeat. While one is active, generic keepalive-only periods keep the
155
+ * client attached. Tool-input preparation is stricter: real byte progress
156
+ * keeps long payloads alive, but zero-byte/silent preparation still recovers
157
+ * so one stuck action cannot pin the chat forever.
157
158
  */
158
159
  durableBackgroundRun?: boolean;
159
160
  }
@@ -278,6 +279,24 @@ function findPendingToolCallIndex(
278
279
  return -1;
279
280
  }
280
281
 
282
+ function findCompletedToolCallIndex(
283
+ content: ContentPart[],
284
+ toolCallId?: string,
285
+ ): number {
286
+ if (!toolCallId) return -1;
287
+ for (let i = content.length - 1; i >= 0; i--) {
288
+ const part = content[i];
289
+ if (
290
+ part.type === "tool-call" &&
291
+ part.toolCallId === toolCallId &&
292
+ part.result !== undefined
293
+ ) {
294
+ return i;
295
+ }
296
+ }
297
+ return -1;
298
+ }
299
+
281
300
  function appendActivityTrail(
282
301
  trail: ActivityTrailEntry[],
283
302
  next: ActivityTrailEntry,
@@ -359,12 +378,7 @@ function updatePreparingActionState(
359
378
  return undefined;
360
379
  }
361
380
 
362
- function hasStalledPreparingAction(
363
- state: PreparingActionState,
364
- now: number,
365
- options?: SSEStreamOptions,
366
- ) {
367
- if (options?.durableBackgroundRun === true) return false;
381
+ function hasStalledPreparingAction(state: PreparingActionState, now: number) {
368
382
  // Fire only when a tool input has gone SILENT — no further streaming deltas
369
383
  // for the whole window — never merely because a large input has been
370
384
  // streaming for a long time. `lastProgressAt` advances on every delta
@@ -787,6 +801,9 @@ export function processEvent(
787
801
  if (ev.type === "tool_start") {
788
802
  const args = (ev.input ?? {}) as Record<string, string>;
789
803
  const tool = ev.tool ?? "unknown";
804
+ if (findCompletedToolCallIndex(content, ev.id) >= 0) {
805
+ return { action: "continue" };
806
+ }
790
807
  if (typeof window !== "undefined") {
791
808
  window.dispatchEvent(
792
809
  new CustomEvent("agent-native:tool-start", {
@@ -868,6 +885,9 @@ export function processEvent(
868
885
  // so a tool_done frame with an undefined tool name still matches its
869
886
  // pending tool-call entry instead of leaving it forever unresolved.
870
887
  const doneTool = ev.tool ?? "unknown";
888
+ if (findCompletedToolCallIndex(content, ev.id) >= 0) {
889
+ return { action: "continue" };
890
+ }
871
891
  if (typeof window !== "undefined") {
872
892
  window.dispatchEvent(
873
893
  new CustomEvent("agent-native:tool-done", {
@@ -1340,9 +1360,7 @@ export async function* readSSEStream(
1340
1360
  );
1341
1361
 
1342
1362
  if (result) yield withStreamMetadata(result);
1343
- if (
1344
- hasStalledPreparingAction(preparingActionState, Date.now(), options)
1345
- ) {
1363
+ if (hasStalledPreparingAction(preparingActionState, Date.now())) {
1346
1364
  throw new AgentAutoContinueSignal({
1347
1365
  reason: "no_progress",
1348
1366
  activityTrail: [...activityTrail],
@@ -1525,9 +1543,7 @@ export async function readSSEStreamRaw(
1525
1543
  : { reason: "stream_ended", activityTrail: [...activityTrail] },
1526
1544
  );
1527
1545
  }
1528
- if (
1529
- hasStalledPreparingAction(preparingActionState, Date.now(), options)
1530
- ) {
1546
+ if (hasStalledPreparingAction(preparingActionState, Date.now())) {
1531
1547
  onUpdate(contentSnapshot(content));
1532
1548
  throw new AgentAutoContinueSignal({
1533
1549
  reason: "no_progress",
@@ -1 +1 @@
1
- {"version":3,"file":"agent-chat-adapter.d.ts","sourceRoot":"","sources":["../../src/client/agent-chat-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAsB,MAAM,qBAAqB,CAAC;AAOhF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAkBrE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAE7D,MAAM,MAAM,oBAAoB;AAC9B;;;;GAIG;AACD,KAAK;AACP,0EAA0E;GACxE,WAAW,CAAC;AA+pChB;;;;GAIG;AACH;;;GAGG;AACH,MAAM,WAAW,6BAA6B;IAC5C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE;QAAE,OAAO,EAAE,MAAM,GAAG,SAAS,CAAA;KAAE,CAAC;IAC3C,SAAS,CAAC,EAAE;QAAE,OAAO,EAAE,MAAM,GAAG,SAAS,CAAA;KAAE,CAAC;IAC5C,SAAS,CAAC,EAAE;QAAE,OAAO,EAAE,eAAe,GAAG,SAAS,CAAA;KAAE,CAAC;IACrD,WAAW,CAAC,EAAE;QAAE,OAAO,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAAA;KAAE,CAAC;IACxD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE;QAAE,OAAO,EAAE,eAAe,GAAG,IAAI,GAAG,SAAS,CAAA;KAAE,CAAC;IAC3D,OAAO,CAAC,EAAE,oBAAoB,CAAC;CAChC;AAED,wBAAgB,sBAAsB,CACpC,OAAO,CAAC,EAAE,6BAA6B,GACtC,gBAAgB,CAihDlB"}
1
+ {"version":3,"file":"agent-chat-adapter.d.ts","sourceRoot":"","sources":["../../src/client/agent-chat-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAsB,MAAM,qBAAqB,CAAC;AAOhF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAkBrE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAE7D,MAAM,MAAM,oBAAoB;AAC9B;;;;GAIG;AACD,KAAK;AACP,0EAA0E;GACxE,WAAW,CAAC;AA+pChB;;;;GAIG;AACH;;;GAGG;AACH,MAAM,WAAW,6BAA6B;IAC5C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE;QAAE,OAAO,EAAE,MAAM,GAAG,SAAS,CAAA;KAAE,CAAC;IAC3C,SAAS,CAAC,EAAE;QAAE,OAAO,EAAE,MAAM,GAAG,SAAS,CAAA;KAAE,CAAC;IAC5C,SAAS,CAAC,EAAE;QAAE,OAAO,EAAE,eAAe,GAAG,SAAS,CAAA;KAAE,CAAC;IACrD,WAAW,CAAC,EAAE;QAAE,OAAO,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAAA;KAAE,CAAC;IACxD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE;QAAE,OAAO,EAAE,eAAe,GAAG,IAAI,GAAG,SAAS,CAAA;KAAE,CAAC;IAC3D,OAAO,CAAC,EAAE,oBAAoB,CAAC;CAChC;AAED,wBAAgB,sBAAsB,CACpC,OAAO,CAAC,EAAE,6BAA6B,GACtC,gBAAgB,CAgjDlB"}
@@ -1055,6 +1055,7 @@ export function createAgentChatAdapter(options) {
1055
1055
  const turnId = generateTurnId();
1056
1056
  let runId = null;
1057
1057
  let lastSeq = -1;
1058
+ const seenRunSeqs = new Map();
1058
1059
  let currentRunDispatchMode = null;
1059
1060
  let currentMessageText = normalizeMentions(recoveryMessageText.trim() || userMessageText);
1060
1061
  let currentHistory = history;
@@ -1208,6 +1209,22 @@ export function createAgentChatAdapter(options) {
1208
1209
  if (mode)
1209
1210
  currentRunDispatchMode = mode;
1210
1211
  };
1212
+ const rememberRunSeq = (seq) => {
1213
+ lastSeq = seq;
1214
+ if (runId) {
1215
+ seenRunSeqs.set(runId, seq);
1216
+ }
1217
+ };
1218
+ const reconnectCursorForRun = (nextRunId, previousRunId) => {
1219
+ const rememberedSeq = seenRunSeqs.get(nextRunId);
1220
+ if (rememberedSeq !== undefined) {
1221
+ lastSeq = rememberedSeq;
1222
+ return;
1223
+ }
1224
+ if (previousRunId !== nextRunId) {
1225
+ lastSeq = -1;
1226
+ }
1227
+ };
1211
1228
  const currentSSEOptions = () => ({
1212
1229
  durableBackgroundRun: currentRunDispatchMode?.startsWith("background") === true,
1213
1230
  });
@@ -1305,7 +1322,7 @@ export function createAgentChatAdapter(options) {
1305
1322
  }
1306
1323
  updateCurrentRunDispatchMode(reconnectRes.headers.get("X-Dispatch-Mode"));
1307
1324
  for await (const result of readSSEStream(reconnectRes.body, content, toolCallCounter, tabId, (seq) => {
1308
- lastSeq = seq;
1325
+ rememberRunSeq(seq);
1309
1326
  if (threadId)
1310
1327
  updateActiveRunSeq(seq);
1311
1328
  }, runId, currentSSEOptions())) {
@@ -1380,12 +1397,13 @@ export function createAgentChatAdapter(options) {
1380
1397
  return false;
1381
1398
  }
1382
1399
  const activeRunId = String(active.runId);
1400
+ const previousRunId = runId;
1383
1401
  runId = activeRunId;
1384
1402
  if (!attemptedRunIds.includes(activeRunId)) {
1385
1403
  attemptedRunIds.push(activeRunId);
1386
1404
  }
1387
- lastSeq = -1;
1388
- setActiveRun({ threadId, runId: activeRunId, lastSeq: -1 });
1405
+ reconnectCursorForRun(activeRunId, previousRunId);
1406
+ setActiveRun({ threadId, runId: activeRunId, lastSeq });
1389
1407
  const reconnected = yield* reconnectCurrentRun();
1390
1408
  if (reconnected)
1391
1409
  return true;
@@ -1449,12 +1467,13 @@ export function createAgentChatAdapter(options) {
1449
1467
  if (activeStatus !== "running" && activeStatus !== "starting") {
1450
1468
  return false;
1451
1469
  }
1470
+ const previousRunId = runId;
1452
1471
  runId = activeRunId;
1453
1472
  if (!attemptedRunIds.includes(activeRunId)) {
1454
1473
  attemptedRunIds.push(activeRunId);
1455
1474
  }
1456
- lastSeq = -1;
1457
- setActiveRun({ threadId, runId: activeRunId, lastSeq: -1 });
1475
+ reconnectCursorForRun(activeRunId, previousRunId);
1476
+ setActiveRun({ threadId, runId: activeRunId, lastSeq });
1458
1477
  const reconnected = yield* reconnectCurrentRun();
1459
1478
  if (reconnected)
1460
1479
  return true;
@@ -1834,13 +1853,14 @@ export function createAgentChatAdapter(options) {
1834
1853
  }
1835
1854
  if (activeRunId) {
1836
1855
  try {
1856
+ const previousRunId = runId;
1837
1857
  runId = activeRunId;
1838
1858
  if (!attemptedRunIds.includes(runId)) {
1839
1859
  attemptedRunIds.push(runId);
1840
1860
  }
1841
- lastSeq = -1;
1861
+ reconnectCursorForRun(activeRunId, previousRunId);
1842
1862
  if (threadId) {
1843
- setActiveRun({ threadId, runId, lastSeq: -1 });
1863
+ setActiveRun({ threadId, runId, lastSeq });
1844
1864
  }
1845
1865
  const reconnected = yield* reconnectCurrentRun();
1846
1866
  if (reconnected)
@@ -1959,7 +1979,7 @@ export function createAgentChatAdapter(options) {
1959
1979
  setActiveRun({ threadId, runId, lastSeq: -1 });
1960
1980
  }
1961
1981
  for await (const result of readSSEStream(res.body, content, toolCallCounter, tabId, (seq) => {
1962
- lastSeq = seq;
1982
+ rememberRunSeq(seq);
1963
1983
  if (runId && threadId) {
1964
1984
  updateActiveRunSeq(seq);
1965
1985
  }
@@ -2132,9 +2152,16 @@ export function createAgentChatAdapter(options) {
2132
2152
  if (activeReconnected)
2133
2153
  return;
2134
2154
  if (err instanceof AgentStartupTimeoutError) {
2135
- const message = "The agent chat endpoint accepted the request but did not start streaming in time. This usually means prompt setup, the LLM gateway, or the provider is stalled.";
2155
+ if (startupRecoveryAttempts < MAX_STARTUP_RECOVERY_ATTEMPTS) {
2156
+ await retryDelay(startupRecoveryAttempts++, abortSignal);
2157
+ if (abortSignal.aborted)
2158
+ return;
2159
+ continue;
2160
+ }
2161
+ const message = "The agent chat endpoint did not start streaming in time after several recovery attempts. This usually means prompt setup, the LLM gateway, or the provider is stalled.";
2136
2162
  captureChatClientError(err, "startup-timeout", {
2137
2163
  timeoutMs: err.timeoutMs,
2164
+ startupRecoveryAttempts,
2138
2165
  });
2139
2166
  const runError = {
2140
2167
  message,