@agent-native/core 0.179.0 → 0.179.1-nightly-20260914212129

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.
@@ -29,6 +29,7 @@ interface BulkActionToolbarProps {
29
29
  allSelected?: boolean;
30
30
  onSelectAll?: () => void;
31
31
  onArchive?: () => void;
32
+ archiveAction?: "archive" | "unarchive";
32
33
  onMove?: (folderId: string | null) => void;
33
34
  onTrash?: () => void;
34
35
  onClear?: () => void;
@@ -42,6 +43,7 @@ export function BulkActionToolbar({
42
43
  allSelected = false,
43
44
  onSelectAll,
44
45
  onArchive,
46
+ archiveAction = "archive",
45
47
  onMove,
46
48
  onTrash,
47
49
  onClear,
@@ -82,7 +84,12 @@ export function BulkActionToolbar({
82
84
  onClick={onArchive}
83
85
  disabled={isPending}
84
86
  >
85
- <IconArchive className="h-3.5 w-3.5" /> {t("libraryGrid.archiveAction")}
87
+ <IconArchive className="h-3.5 w-3.5" />{" "}
88
+ {t(
89
+ archiveAction === "unarchive"
90
+ ? "clipsFinalRaw.unarchive"
91
+ : "libraryGrid.archiveAction",
92
+ )}
86
93
  </Button>
87
94
  {canMove && (
88
95
  <DropdownMenu>
@@ -782,12 +782,17 @@ export function LibraryGrid({
782
782
  allSelected={allSelected}
783
783
  onSelectAll={toggleSelectAll}
784
784
  moveTargets={moveTargets}
785
+ archiveAction={view === "archive" ? "unarchive" : "archive"}
785
786
  onArchive={async () => {
786
787
  setIsBulkPending(true);
787
788
  try {
788
789
  const ids = Array.from(selected);
789
790
  const results = await Promise.allSettled(
790
- ids.map((id) => archiveRecording.mutateAsync({ id })),
791
+ ids.map((id) =>
792
+ view === "archive"
793
+ ? restoreRecording.mutateAsync({ id })
794
+ : archiveRecording.mutateAsync({ id }),
795
+ ),
791
796
  );
792
797
  const succeededIds = ids.filter(
793
798
  (_, i) => results[i].status === "fulfilled",
@@ -795,9 +800,12 @@ export function LibraryGrid({
795
800
  const failed = ids.length - succeededIds.length;
796
801
  if (succeededIds.length > 0) {
797
802
  toast.success(
798
- t("libraryGrid.clipsArchived", {
799
- count: succeededIds.length,
800
- }),
803
+ t(
804
+ view === "archive"
805
+ ? "trashRoute.clipsRestored"
806
+ : "libraryGrid.clipsArchived",
807
+ { count: succeededIds.length },
808
+ ),
801
809
  );
802
810
  setSelected((prev) => {
803
811
  const next = new Set(prev);
@@ -807,7 +815,12 @@ export function LibraryGrid({
807
815
  }
808
816
  if (failed > 0) {
809
817
  toast.error(
810
- t("libraryGrid.clipsArchiveFailed", { count: failed }),
818
+ t(
819
+ view === "archive"
820
+ ? "trashRoute.clipsRestoreFailed"
821
+ : "libraryGrid.clipsArchiveFailed",
822
+ { count: failed },
823
+ ),
811
824
  );
812
825
  }
813
826
  } finally {
@@ -7,6 +7,7 @@ import {
7
7
  WorkspaceAppCard,
8
8
  } from "@agent-native/dispatch/components";
9
9
  import { IconPlus } from "@tabler/icons-react";
10
+ import { forwardRef } from "react";
10
11
 
11
12
  import { Button } from "@/components/ui/button";
12
13
 
@@ -21,14 +22,19 @@ interface WorkspaceAppSummary {
21
22
  statusLabel?: string;
22
23
  }
23
24
 
24
- function CreateAppButton({ label }: { label: string }) {
25
- return (
26
- <Button size="sm">
27
- <IconPlus size={15} />
28
- {label}
29
- </Button>
30
- );
31
- }
25
+ // Radix's PopoverTrigger asChild clones its child and injects onClick/ref
26
+ // directly onto it. A plain function component here would drop those props
27
+ // silently, leaving the trigger button inert. Forward both through to Button.
28
+ const CreateAppTriggerButton = forwardRef<
29
+ HTMLButtonElement,
30
+ React.ComponentPropsWithoutRef<typeof Button> & { label: string }
31
+ >(({ label, ...props }, ref) => (
32
+ <Button ref={ref} size="sm" {...props}>
33
+ <IconPlus size={15} />
34
+ {label}
35
+ </Button>
36
+ ));
37
+ CreateAppTriggerButton.displayName = "CreateAppTriggerButton";
32
38
 
33
39
  function AgenticAppsSection({ t }: { t: ReturnType<typeof useT> }) {
34
40
  const query = useActionQuery<WorkspaceAppSummary[]>("list-workspace-apps", {
@@ -55,7 +61,7 @@ function AgenticAppsSection({ t }: { t: ReturnType<typeof useT> }) {
55
61
  {apps.length > 0 ? (
56
62
  <CreateAppPopover
57
63
  align="end"
58
- trigger={<CreateAppButton label={createAppLabel} />}
64
+ trigger={<CreateAppTriggerButton label={createAppLabel} />}
59
65
  onCreated={() => void query.refetch()}
60
66
  />
61
67
  ) : null}
@@ -86,7 +92,7 @@ function AgenticAppsSection({ t }: { t: ReturnType<typeof useT> }) {
86
92
  </div>
87
93
  <div className="mt-4 flex justify-center">
88
94
  <CreateAppPopover
89
- trigger={<CreateAppButton label={createAppLabel} />}
95
+ trigger={<CreateAppTriggerButton label={createAppLabel} />}
90
96
  onCreated={() => void query.refetch()}
91
97
  />
92
98
  </div>
@@ -47,7 +47,7 @@ import { getDefaultMaxIterations, getDefaultMaxRunInputTokens, MAX_AGENT_MAX_ITE
47
47
  import { getContextWindowForModel, resolveFallbackModel, } from "./model-config.js";
48
48
  import { maybeCompactThread, buildObservationalContext, hasObservationalMemory, serializeObservationalMemoryBlock, } from "./observational-memory/index.js";
49
49
  import { ProcessorChain, TripWire, toolCallsFromContent, } from "./processors.js";
50
- import { startRun, subscribeToRun, replayCompletedTurn, getActiveRunForThread, getActiveRunForThreadAsync, getRun, abortRun, abortRunDurably, abortTurnDurably, tryClaimRunSlot, isHostedRuntime, resolveRunSoftTimeoutMs, resolveRunToolTimeoutCeilingMs, endsAfterCompletedToolWithoutAssistantFinal, endsDuringActionPreparation, } from "./run-manager.js";
50
+ import { startRun, subscribeToRun, replayCompletedTurn, getActiveRunForThread, getActiveRunForThreadAsync, getRun, abortRun, abortRunDurably, abortTurnDurably, tryClaimRunSlot, isHostedRuntime, resolveRunSoftTimeoutMs, resolveRunToolTimeoutCeilingMs, endsAfterToolResultWithoutAssistantFinal, endsDuringActionPreparation, } from "./run-manager.js";
51
51
  import { writeLedgerEntry, readLedgerEntry, clearLedgerForThread, insertRun, insertRunEvent, isTurnAborted, markRunAborted, updateRunHeartbeat, updateRunStatusIfRunning, setRunError, setRunTerminalReason, claimBackgroundRun, readBackgroundRunClaim, recordRunDiagnostic, countRunsForTurn, RUN_DIAG_STAGE, UNCLAIMED_BACKGROUND_RUN_GRACE_MS, turnRunLedgerExhausted, } from "./run-store.js";
52
52
  import { buildCurrentTimeUserContext } from "./runtime-context.js";
53
53
  import { consumeAgentToolApproval, createAgentToolApproval, isAgentToolAlwaysAllowed, resolveAgentToolApprovalTurnId, } from "./tool-approval-store.js";
@@ -5972,7 +5972,7 @@ export function backgroundContinuationReasonForRun(run) {
5972
5972
  providerRetryable: last.providerRetryable,
5973
5973
  }));
5974
5974
  }
5975
- if (endsAfterCompletedToolWithoutAssistantFinal(run) ||
5975
+ if (endsAfterToolResultWithoutAssistantFinal(run) ||
5976
5976
  endsDuringActionPreparation(run)) {
5977
5977
  return "stream_ended";
5978
5978
  }
@@ -6078,7 +6078,7 @@ export async function runAgentLoopWithMainChatInternalContinuations(opts) {
6078
6078
  }
6079
6079
  function endsAtContinuationBoundary(run) {
6080
6080
  return (endsAtInternalContinuationBoundary(run) ||
6081
- endsAfterCompletedToolWithoutAssistantFinal(run) ||
6081
+ endsAfterToolResultWithoutAssistantFinal(run) ||
6082
6082
  endsDuringActionPreparation(run));
6083
6083
  }
6084
6084
  // Defined in `app-config/run-lifecycle-invariants.ts`, the neutral home for
@@ -6089,7 +6089,7 @@ function endsAtContinuationBoundary(run) {
6089
6089
  * Forward progress inside ONE chunk, read from the events it actually emitted:
6090
6090
  * assistant text or tool activity. Same evidence the agent-teams no-progress
6091
6091
  * budget counts (`agent-teams.ts`), and the same events
6092
- * `endsAfterCompletedToolWithoutAssistantFinal` reads to tell an unfinished
6092
+ * `endsAfterToolResultWithoutAssistantFinal` reads to tell an unfinished
6093
6093
  * turn from a finished one.
6094
6094
  */
6095
6095
  function chunkMadeForwardProgress(run) {
@@ -361,12 +361,22 @@ export declare function resolveBackgroundRunHardTimeoutMs(): number;
361
361
  */
362
362
  export declare function resolveBackgroundAutomationSoftTimeoutMs(overrideMs?: number): number;
363
363
  /**
364
- * A completed tool with no later assistant text is an unfinished turn, not a
364
+ * A tool result with no later assistant text is an unfinished turn, not a
365
365
  * successful terminal response. Keep this predicate beside the run-manager's
366
366
  * terminal synthesis so the run-manager and production continuation paths use
367
367
  * the same boundary evidence.
368
+ *
369
+ * A FAILED tool result counts too. Skipping it made the verdict depend on
370
+ * whether some earlier call in the same turn happened to succeed: a turn ending
371
+ * on `resources` ok then `web_request` failed continued, while the same turn
372
+ * with both failing terminated as a plain `done`. The client can only render
373
+ * "stopped after these actions ... without sending a final message" for that,
374
+ * so the tool's real error — an expired handoff URL, a missing credential —
375
+ * never reached the user, and typing "continue" by hand was the only way to see
376
+ * it. The model has not read the error yet at this point, which makes a failed
377
+ * tail strictly more unfinished than a successful one.
368
378
  */
369
- export declare function endsAfterCompletedToolWithoutAssistantFinal(run: ActiveRun): boolean;
379
+ export declare function endsAfterToolResultWithoutAssistantFinal(run: ActiveRun): boolean;
370
380
  /**
371
381
  * A model can emit a lead-in before it starts assembling an action input. If
372
382
  * the run ends in that preparation phase, `done` is not a successful turn.
@@ -457,21 +457,34 @@ function isTerminalRunEvent(event) {
457
457
  event.type === "auto_continue");
458
458
  }
459
459
  /**
460
- * A completed tool with no later assistant text is an unfinished turn, not a
460
+ * A tool result with no later assistant text is an unfinished turn, not a
461
461
  * successful terminal response. Keep this predicate beside the run-manager's
462
462
  * terminal synthesis so the run-manager and production continuation paths use
463
463
  * the same boundary evidence.
464
+ *
465
+ * A FAILED tool result counts too. Skipping it made the verdict depend on
466
+ * whether some earlier call in the same turn happened to succeed: a turn ending
467
+ * on `resources` ok then `web_request` failed continued, while the same turn
468
+ * with both failing terminated as a plain `done`. The client can only render
469
+ * "stopped after these actions ... without sending a final message" for that,
470
+ * so the tool's real error — an expired handoff URL, a missing credential —
471
+ * never reached the user, and typing "continue" by hand was the only way to see
472
+ * it. The model has not read the error yet at this point, which makes a failed
473
+ * tail strictly more unfinished than a successful one.
464
474
  */
465
- export function endsAfterCompletedToolWithoutAssistantFinal(run) {
466
- let completedToolAfterLastAssistantText = false;
475
+ export function endsAfterToolResultWithoutAssistantFinal(run) {
476
+ let toolResultAfterLastAssistantText = false;
467
477
  for (const { event } of run.events) {
468
478
  if (event.type === "text" && event.text.trim().length > 0) {
469
- completedToolAfterLastAssistantText = false;
479
+ toolResultAfterLastAssistantText = false;
470
480
  continue;
471
481
  }
472
- if (event.type === "tool_done" && event.isError !== true) {
473
- completedToolAfterLastAssistantText =
474
- event.chatUI === undefined && event.mcpApp === undefined;
482
+ if (event.type === "tool_done") {
483
+ // Custom UI is the one tool result that is a legitimate final answer on
484
+ // its own, and only when it succeeded.
485
+ toolResultAfterLastAssistantText =
486
+ event.isError === true ||
487
+ (event.chatUI === undefined && event.mcpApp === undefined);
475
488
  continue;
476
489
  }
477
490
  if (event.type === "clear" ||
@@ -479,10 +492,10 @@ export function endsAfterCompletedToolWithoutAssistantFinal(run) {
479
492
  event.type === "missing_api_key" ||
480
493
  event.type === "auto_continue" ||
481
494
  event.type === "loop_limit") {
482
- completedToolAfterLastAssistantText = false;
495
+ toolResultAfterLastAssistantText = false;
483
496
  }
484
497
  }
485
- return completedToolAfterLastAssistantText;
498
+ return toolResultAfterLastAssistantText;
486
499
  }
487
500
  /**
488
501
  * A model can emit a lead-in before it starts assembling an action input. If
@@ -1635,7 +1648,7 @@ export function startRun(runId, threadId, runFn, onComplete, options) {
1635
1648
  // visible through StartedRun/getRun during the cleanup window.
1636
1649
  run.status = finalStatus;
1637
1650
  const shouldAutoContinueAfterUnfinishedTurn = finalStatus === "completed" &&
1638
- (endsAfterCompletedToolWithoutAssistantFinal(run) ||
1651
+ (endsAfterToolResultWithoutAssistantFinal(run) ||
1639
1652
  endsDuringActionPreparation(run)) &&
1640
1653
  (!terminalEventForCompletion ||
1641
1654
  (terminalEventForCompletion.event.type === "done" &&
@@ -186,8 +186,8 @@ export declare function createAutomationCallbackHandler(runtime: AutomationRunti
186
186
  duplicate?: undefined;
187
187
  eventId?: undefined;
188
188
  } | {
189
- error?: undefined;
190
189
  accepted: boolean;
191
190
  duplicate: boolean;
192
191
  eventId: string;
192
+ error?: undefined;
193
193
  }>;
@@ -656,7 +656,10 @@ export function isMissingFinalResponseWarningText(text) {
656
656
  return true;
657
657
  }
658
658
  return (normalized.includes("stopped before sending a final message") ||
659
- normalized.includes("stopped without sending a final message"));
659
+ normalized.includes("stopped without sending a final message") ||
660
+ // "stopped after <action> failed, without sending a final message."
661
+ (normalized.startsWith("The agent stopped after ") &&
662
+ normalized.includes("without sending a final message")));
660
663
  }
661
664
  function finalResponseTextFromContent(content) {
662
665
  if (typeof content === "string")
@@ -2,15 +2,21 @@ import React from "react";
2
2
  import type { BuilderConnectFlow } from "./useBuilderStatus.js";
3
3
  type BuilderConnectTrigger = React.ReactElement<{
4
4
  onClick?: React.MouseEventHandler<HTMLElement>;
5
- "aria-disabled"?: boolean;
5
+ "aria-busy"?: boolean;
6
6
  }>;
7
7
  export interface BuilderConnectPopoverProps {
8
8
  flow: Pick<BuilderConnectFlow, "connecting" | "start"> & {
9
9
  agentNativeProvisioningEnabled?: boolean;
10
10
  accountExists?: boolean;
11
- /** Retry the status request without bypassing provisioning consent. */
12
- retry?: () => void;
11
+ /**
12
+ * Retry the status request without bypassing provisioning consent.
13
+ * Returns true when a read actually started; a retry that cannot report
14
+ * that is treated as "did not start" and never queues a click.
15
+ */
16
+ retry?: () => boolean | void;
13
17
  statusResolved?: boolean;
18
+ /** Bounds a queued click: increments whenever a status read settles. */
19
+ statusReadSettledCount?: number;
14
20
  };
15
21
  children: BuilderConnectTrigger;
16
22
  /** Preserve a surface-specific tracking source or callback when choosing a path. */
@@ -12,6 +12,14 @@ export function BuilderConnectPopover({ flow, children, onConnect, onTriggerClic
12
12
  (capabilityResolved && flow.agentNativeProvisioningEnabled === true);
13
13
  const accountExists = capabilityResolved && flow.accountExists;
14
14
  const initiatedByThisTriggerRef = useRef(false);
15
+ // A click landing before the first status read cannot be answered yet:
16
+ // whether it opens the provisioning consent choice is exactly what that read
17
+ // decides. The trigger renders as an ordinary enabled button for that whole
18
+ // window, which is seconds long on a cold serverless start, so the intent is
19
+ // held rather than discarded. The snapshot bounds the wait: once the read
20
+ // this click asked for has settled without resolving, the intent is dropped.
21
+ const [queuedClick, setQueuedClick] = useState(null);
22
+ const settledCount = flow.statusReadSettledCount ?? 0;
15
23
  useEffect(() => {
16
24
  if (accountExists && initiatedByThisTriggerRef.current) {
17
25
  initiatedByThisTriggerRef.current = false;
@@ -27,8 +35,38 @@ export function BuilderConnectPopover({ flow, children, onConnect, onTriggerClic
27
35
  }
28
36
  flow.start({ provisionAccount });
29
37
  };
38
+ // Only ever replays work that needs no popup. `flow.start` reaches
39
+ // `window.open`, which browsers permit solely inside the click that asked
40
+ // for it; calling it from this effect would trade a dead button for a
41
+ // blocked popup and an "allow popups" message that blames the user for a
42
+ // gesture we dropped. When the resolved capability has no consent choice to
43
+ // show, the intent is released instead, and the now-resolved trigger answers
44
+ // the next click synchronously.
45
+ const openQueuedPopoverRef = useRef(() => { });
46
+ openQueuedPopoverRef.current = () => {
47
+ setQueuedClick(null);
48
+ if (showPopover)
49
+ setOpen(true);
50
+ };
51
+ useEffect(() => {
52
+ if (!queuedClick)
53
+ return;
54
+ if (capabilityResolved) {
55
+ openQueuedPopoverRef.current();
56
+ return;
57
+ }
58
+ // The read this click triggered came back and still did not resolve the
59
+ // capability. Surfaces that render this popover also render `flow.error`,
60
+ // so the user already has the reason; dropping the intent here is what
61
+ // keeps the trigger from sitting busy forever against an unreachable
62
+ // status route. Any movement counts, not just an increment: disabling the
63
+ // flow cancels the pending read and resets the counter, and a snapshot
64
+ // taken above that reset would otherwise never be passed again.
65
+ if (settledCount !== queuedClick.settledAt)
66
+ setQueuedClick(null);
67
+ }, [queuedClick, capabilityResolved, settledCount]);
30
68
  const trigger = React.cloneElement(children, {
31
- "aria-disabled": capabilityResolved || defaultProvisionAccount ? undefined : true,
69
+ "aria-busy": queuedClick ? true : undefined,
32
70
  onClick: (event) => {
33
71
  if (flow.connecting) {
34
72
  event.preventDefault();
@@ -40,9 +78,17 @@ export function BuilderConnectPopover({ flow, children, onConnect, onTriggerClic
40
78
  event.stopPropagation();
41
79
  if (defaultProvisionAccount) {
42
80
  setOpen(true);
81
+ return;
43
82
  }
44
- else {
45
- flow.retry?.();
83
+ // A click is already waiting on a read. Starting a second one would
84
+ // supersede the first in the hook's newest-wins refresh, discarding a
85
+ // success that was about to land.
86
+ if (queuedClick)
87
+ return;
88
+ // Only wait on a read that actually started. A disabled flow never
89
+ // reads, so queuing against it would spin the trigger forever.
90
+ if (flow.retry?.() === true) {
91
+ setQueuedClick({ settledAt: settledCount });
46
92
  }
47
93
  return;
48
94
  }
@@ -102,6 +102,14 @@ export interface BuilderConnectFlow {
102
102
  configured: boolean;
103
103
  /** True after at least one successful Builder connection-status response. */
104
104
  statusResolved: boolean;
105
+ /**
106
+ * Increments every time a `retry`/lifecycle status read settles, whether it
107
+ * resolved or failed. `statusResolved` alone cannot bound a caller waiting
108
+ * on a read, because a second failure leaves it false with no observable
109
+ * change. Consumers that queue work on a read need the settle, not the
110
+ * outcome.
111
+ */
112
+ statusReadSettledCount: number;
105
113
  /**
106
114
  * True when the deploy has BUILDER_PRIVATE_KEY set as a fallback. Connect
107
115
  * is still available so users can override the fallback with their own
@@ -140,8 +148,12 @@ export interface BuilderConnectFlow {
140
148
  hasFetchedStatus: boolean;
141
149
  /** Open the popup and begin polling. Must be called from a user-gesture handler. */
142
150
  start: (options?: BuilderConnectStartOptions) => void;
143
- /** Retry the status request before choosing a connection path. */
144
- retry: () => void;
151
+ /**
152
+ * Retry the status request before choosing a connection path. Returns true
153
+ * when a read actually started. A disabled flow never reads, so a caller
154
+ * that waits on the result must be able to tell the difference.
155
+ */
156
+ retry: () => boolean;
145
157
  }
146
158
  export declare function withBuilderConnectTrackingParams(url: string, options?: {
147
159
  source?: string;
@@ -441,6 +441,7 @@ export function useBuilderConnectFlow(opts = {}) {
441
441
  const [accountExists, setAccountExists] = useState(false);
442
442
  const [hasFetchedStatus, setHasFetchedStatus] = useState(false);
443
443
  const [statusResolved, setStatusResolved] = useState(false);
444
+ const [statusReadSettledCount, setStatusReadSettledCount] = useState(0);
444
445
  const [statusConnectUrl, setStatusConnectUrl] = useState(null);
445
446
  // When statusConnectUrl was last fetched. The server signs the embedded
446
447
  // _an_connect token with a 10-minute TTL; using an older URL fails the
@@ -455,7 +456,7 @@ export function useBuilderConnectFlow(opts = {}) {
455
456
  const activePopupRef = useRef(null);
456
457
  const popupClosedAtRef = useRef(null);
457
458
  const callbackSuccessStartedAtRef = useRef(null);
458
- const retryStatusRef = useRef(() => { });
459
+ const retryStatusRef = useRef(() => false);
459
460
  const statusUnavailableRef = useRef(false);
460
461
  const mountedRef = useRef(true);
461
462
  const notifiedConnectedRef = useRef(false);
@@ -527,10 +528,11 @@ export function useBuilderConnectFlow(opts = {}) {
527
528
  setAccountExists(false);
528
529
  setHasFetchedStatus(false);
529
530
  setStatusResolved(false);
531
+ setStatusReadSettledCount(0);
530
532
  setStatusConnectUrl(null);
531
533
  statusConnectUrlAtRef.current = null;
532
534
  connectAttemptIdRef.current = null;
533
- retryStatusRef.current = () => { };
535
+ retryStatusRef.current = () => false;
534
536
  return;
535
537
  }
536
538
  mountedRef.current = true;
@@ -548,6 +550,7 @@ export function useBuilderConnectFlow(opts = {}) {
548
550
  // "use initial props until the hook has an answer" pattern wants to
549
551
  // stop waiting after we've tried, regardless of network outcome.
550
552
  setHasFetchedStatus(true);
553
+ setStatusReadSettledCount((count) => count + 1);
551
554
  if (!s) {
552
555
  // "Could not read the status" must not render the same as "no status
553
556
  // yet". `statusResolved` only flips on success, so without a visible
@@ -606,7 +609,10 @@ export function useBuilderConnectFlow(opts = {}) {
606
609
  setError(null);
607
610
  }
608
611
  };
609
- retryStatusRef.current = () => void refresh();
612
+ retryStatusRef.current = () => {
613
+ void refresh();
614
+ return true;
615
+ };
610
616
  // Connect-CTA cards render above the fold but their status is not needed
611
617
  // for first paint; defer the initial read. Focus/visibility/event
612
618
  // refreshes below stay immediate.
@@ -636,15 +642,13 @@ export function useBuilderConnectFlow(opts = {}) {
636
642
  cancelled = true;
637
643
  mountedRef.current = false;
638
644
  cancelInitialRefresh();
639
- retryStatusRef.current = () => { };
645
+ retryStatusRef.current = () => false;
640
646
  window.removeEventListener("focus", refreshNow);
641
647
  document.removeEventListener("visibilitychange", onVisible);
642
648
  window.removeEventListener("agent-engine:configured-changed", refreshNow);
643
649
  };
644
650
  }, [enabled, fetchStatus]);
645
- const retry = useCallback(() => {
646
- retryStatusRef.current();
647
- }, []);
651
+ const retry = useCallback(() => retryStatusRef.current(), []);
648
652
  const start = useCallback((startOptions) => {
649
653
  if (!enabled)
650
654
  return;
@@ -1074,6 +1078,7 @@ export function useBuilderConnectFlow(opts = {}) {
1074
1078
  configured,
1075
1079
  codeChangeConfigured,
1076
1080
  statusResolved,
1081
+ statusReadSettledCount,
1077
1082
  envManaged,
1078
1083
  credentialSource,
1079
1084
  canDisconnect,
@@ -260,6 +260,7 @@ export declare function appendMissingFinalResponseWarning(content: ContentPart[]
260
260
  message: string;
261
261
  errorCode: string;
262
262
  recoverable: true;
263
+ failedTools?: string[];
263
264
  } | null;
264
265
  interface ProcessEventState {
265
266
  completedToolsAfterLastAssistantText: Set<string>;
@@ -877,6 +877,50 @@ function completedToolOnlyMessage(toolNames) {
877
877
  const label = formatToolNames(toolNames);
878
878
  return `The agent completed ${label}, but stopped before sending a final message. Review the completed tool card above or ask the agent to continue.`;
879
879
  }
880
+ const MAX_REPORTED_TOOL_ERROR_LENGTH = 300;
881
+ /**
882
+ * The failing tool results the turn ended on, newest first.
883
+ *
884
+ * A turn that stops on a failed tool used to render the same "review the tool
885
+ * card above" note as one that stops on a successful tool, so the reason it
886
+ * stopped — an expired handoff URL, a missing Stripe credential — was one the
887
+ * user had to go hunting for. The error text the tool already returned is the
888
+ * answer, so say it.
889
+ */
890
+ function failedToolResultsAfterLastAssistantText(content) {
891
+ const lastTextIndex = lastAssistantTextIndex(content);
892
+ const failures = [];
893
+ for (let index = content.length - 1; index > lastTextIndex; index--) {
894
+ const part = content[index];
895
+ if (part?.type !== "tool-call" ||
896
+ part.activity === true ||
897
+ part.isError !== true ||
898
+ part.result === undefined) {
899
+ continue;
900
+ }
901
+ failures.push({
902
+ toolName: part.toolName,
903
+ error: typeof part.result === "string" ? part.result.trim() : "",
904
+ });
905
+ }
906
+ return failures;
907
+ }
908
+ function failedToolMessage(failures) {
909
+ const latest = failures[0];
910
+ if (!latest)
911
+ return null;
912
+ const label = formatToolNames(failures.map((failure) => failure.toolName));
913
+ const detail = latest.error
914
+ ? ` ${truncateToolError(latest.error)}`
915
+ : " No error detail was returned.";
916
+ return `The agent stopped after ${label} failed, without sending a final message.${detail} Ask the agent to continue, or fix the underlying failure and retry.`;
917
+ }
918
+ function truncateToolError(error) {
919
+ const singleLine = error.replace(/\s+/g, " ").trim();
920
+ return singleLine.length > MAX_REPORTED_TOOL_ERROR_LENGTH
921
+ ? `${singleLine.slice(0, MAX_REPORTED_TOOL_ERROR_LENGTH)}…`
922
+ : singleLine;
923
+ }
880
924
  function hasCompletedCustomUi(content) {
881
925
  const lastTextIndex = lastAssistantTextIndex(content);
882
926
  let lastCompletedToolIsCustomUi = false;
@@ -904,6 +948,7 @@ export function appendMissingFinalResponseWarning(content, completedToolNames) {
904
948
  ...new Set(completedToolNames ?? completedToolNamesAfterLastAssistantText(content)),
905
949
  ];
906
950
  let lastToolIndex = -1;
951
+ let lastToolResultFailed = false;
907
952
  const materializedToolNames = new Set();
908
953
  for (let index = lastTextIndex + 1; index < content.length; index++) {
909
954
  const part = content[index];
@@ -911,20 +956,28 @@ export function appendMissingFinalResponseWarning(content, completedToolNames) {
911
956
  part.activity !== true &&
912
957
  part.result !== undefined) {
913
958
  lastToolIndex = index;
959
+ lastToolResultFailed = part.isError === true;
914
960
  materializedToolNames.add(part.toolName);
915
961
  }
916
962
  }
917
- if (hasCompletedCustomUi(content))
963
+ // A rendered custom UI is a legitimate final answer only when nothing failed
964
+ // after it. `hasCompletedCustomUi` skips errored results, so without this a
965
+ // widget followed by a failing tool would silently claim the turn finished —
966
+ // the same verdict the run manager makes from the last tool_done.
967
+ if (!lastToolResultFailed && hasCompletedCustomUi(content))
918
968
  return null;
919
969
  if (successfulToolNames.length === 0 && lastTextIndex > lastToolIndex) {
920
970
  return null;
921
971
  }
972
+ // A failure outranks the completed-tool note: it is both the reason the turn
973
+ // stopped and the only part of it the user cannot reconstruct on their own.
974
+ const failures = failedToolResultsAfterLastAssistantText(content);
922
975
  const completedToolMessage = completedToolOnlyMessage(successfulToolNames);
923
- const message = completedToolMessage
924
- ? completedToolMessage
925
- : materializedToolNames.size > 0
976
+ const message = failedToolMessage(failures) ??
977
+ completedToolMessage ??
978
+ (materializedToolNames.size > 0
926
979
  ? `The agent stopped after ${formatToolNames([...materializedToolNames])} without sending a final message. Review the tool card above or ask the agent to continue.`
927
- : "The agent stopped without sending a final message. Ask the agent to continue or retry.";
980
+ : "The agent stopped without sending a final message. Ask the agent to continue or retry.");
928
981
  if (!content.some((part) => part.type === "text" && part.text === message)) {
929
982
  content.push({ type: "text", text: message });
930
983
  }
@@ -934,6 +987,9 @@ export function appendMissingFinalResponseWarning(content, completedToolNames) {
934
987
  ? "final_response_missing_after_tool"
935
988
  : "final_response_missing",
936
989
  recoverable: true,
990
+ ...(failures.length > 0
991
+ ? { failedTools: failures.map((failure) => failure.toolName) }
992
+ : {}),
937
993
  };
938
994
  }
939
995
  function markAssistantText(state) {
@@ -13,8 +13,8 @@
13
13
  * Body: { json: any, fieldName?: string, type?: "map"|"array", requestSource?: string }
14
14
  */
15
15
  export declare const postCollabJson: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
16
- ok?: undefined;
17
16
  error: string;
17
+ ok?: undefined;
18
18
  } | {
19
19
  error?: undefined;
20
20
  ok: boolean;
@@ -28,6 +28,6 @@ export declare function createNotificationsHandler(): import("h3").EventHandlerW
28
28
  } | {
29
29
  count?: undefined;
30
30
  updated?: undefined;
31
- ok: boolean;
32
31
  error?: undefined;
32
+ ok: boolean;
33
33
  }>>;
@@ -41,27 +41,27 @@ export declare function createObservabilityHandler(): import("h3").EventHandlerW
41
41
  thumbsUpRate: number;
42
42
  avgEvalScore: number;
43
43
  } | {
44
+ error?: undefined;
45
+ ok?: undefined;
44
46
  summary: import("./types.js").TraceSummary;
45
47
  spans: import("./types.js").TraceSpan[];
46
48
  id?: undefined;
49
+ } | {
47
50
  error?: undefined;
48
51
  ok?: undefined;
49
- } | {
50
52
  summary?: undefined;
51
53
  spans?: undefined;
52
54
  id: string;
53
- error?: undefined;
54
- ok?: undefined;
55
55
  } | {
56
+ ok?: undefined;
56
57
  summary?: undefined;
57
58
  spans?: undefined;
58
59
  id?: undefined;
59
60
  error: any;
60
- ok?: undefined;
61
61
  } | {
62
+ error?: undefined;
62
63
  summary?: undefined;
63
64
  spans?: undefined;
64
65
  id?: undefined;
65
- error?: undefined;
66
66
  ok: boolean;
67
67
  }>>;
@@ -53,16 +53,16 @@ export declare function createListSecretsHandler(): import("h3").EventHandlerWit
53
53
  /** POST /_agent-native/secrets/:key — write a secret. */
54
54
  export declare function createWriteSecretHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
55
55
  error: string;
56
- ok?: undefined;
57
56
  status?: undefined;
57
+ ok?: undefined;
58
58
  } | {
59
59
  ok: boolean;
60
60
  status: string;
61
61
  error?: undefined;
62
62
  } | {
63
- ok?: undefined;
64
63
  error: string;
65
64
  removed?: undefined;
65
+ ok?: undefined;
66
66
  } | {
67
67
  ok: boolean;
68
68
  removed: boolean;
@@ -73,9 +73,9 @@ export declare function createWriteSecretHandler(): import("h3").EventHandlerWit
73
73
  * or the current stored value without changing anything.
74
74
  */
75
75
  export declare function createTestSecretHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
76
- ok?: undefined;
77
76
  error: string;
78
77
  note?: undefined;
78
+ ok?: undefined;
79
79
  } | {
80
80
  ok: boolean;
81
81
  note?: undefined;
@@ -20,6 +20,6 @@ export declare function createTranscribeVoiceHandler(): import("h3").EventHandle
20
20
  error: string;
21
21
  text?: undefined;
22
22
  } | {
23
- error?: undefined;
24
23
  text: string;
24
+ error?: undefined;
25
25
  }>>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.179.0",
3
+ "version": "0.179.1-nightly-20260914212129",
4
4
  "description": "The agentic application framework for building autonomous agents with intuitive UIs",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -475,9 +475,9 @@
475
475
  "yaml": "^2.9.0",
476
476
  "yjs": "^13.6.32",
477
477
  "zod": "^4.3.6",
478
- "@agent-native/agentkit": "^0.2.0",
479
- "@agent-native/toolkit": "^0.20.0",
480
- "@agent-native/recap-cli": "0.5.30"
478
+ "@agent-native/agentkit": "^0.2.1-nightly-20260914212129",
479
+ "@agent-native/recap-cli": "0.5.30",
480
+ "@agent-native/toolkit": "^0.20.1-nightly-20260914212129"
481
481
  },
482
482
  "devDependencies": {
483
483
  "@ai-sdk/anthropic": "^3.0.71",