@agent-native/core 0.179.0 → 0.179.1-nightly-20260914215457
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/templates/clips/app/components/library/bulk-action-toolbar.tsx +8 -1
- package/corpus/templates/clips/app/components/library/library-grid.tsx +18 -5
- package/corpus/templates/factory/app/components/factory/FactoryAgentsView.tsx +16 -10
- package/dist/agent/production-agent.js +4 -4
- package/dist/agent/run-manager.d.ts +12 -2
- package/dist/agent/run-manager.js +23 -10
- package/dist/automation/index.d.ts +1 -1
- package/dist/client/chat/message-components.js +4 -1
- package/dist/client/settings/BuilderConnectPopover.d.ts +9 -3
- package/dist/client/settings/BuilderConnectPopover.js +49 -3
- package/dist/client/settings/useBuilderStatus.d.ts +14 -2
- package/dist/client/settings/useBuilderStatus.js +12 -7
- package/dist/client/sse-event-processor.d.ts +1 -0
- package/dist/client/sse-event-processor.js +61 -5
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +5 -5
- package/dist/progress/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +3 -3
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/package.json +3 -3
|
@@ -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" />
|
|
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) =>
|
|
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(
|
|
799
|
-
|
|
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(
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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={<
|
|
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={<
|
|
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,
|
|
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 (
|
|
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
|
-
|
|
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
|
-
* `
|
|
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
|
|
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
|
|
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
|
|
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
|
|
466
|
-
let
|
|
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
|
-
|
|
479
|
+
toolResultAfterLastAssistantText = false;
|
|
470
480
|
continue;
|
|
471
481
|
}
|
|
472
|
-
if (event.type === "tool_done"
|
|
473
|
-
|
|
474
|
-
|
|
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
|
-
|
|
495
|
+
toolResultAfterLastAssistantText = false;
|
|
483
496
|
}
|
|
484
497
|
}
|
|
485
|
-
return
|
|
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
|
-
(
|
|
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-
|
|
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
|
-
/**
|
|
12
|
-
|
|
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-
|
|
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
|
-
|
|
45
|
-
|
|
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
|
-
/**
|
|
144
|
-
|
|
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 = () =>
|
|
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
|
-
|
|
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 =
|
|
924
|
-
|
|
925
|
-
|
|
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;
|
|
@@ -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
|
}>>;
|
|
@@ -48,8 +48,8 @@ export declare function handleUpdateResource(event: any): Promise<import("./stor
|
|
|
48
48
|
}>;
|
|
49
49
|
/** DELETE /_agent-native/resources/:id — delete a resource */
|
|
50
50
|
export declare function handleDeleteResource(event: any): Promise<{
|
|
51
|
-
error: string;
|
|
52
51
|
ok?: undefined;
|
|
52
|
+
error: string;
|
|
53
53
|
} | {
|
|
54
54
|
error?: undefined;
|
|
55
55
|
ok: boolean;
|
package/dist/secrets/routes.d.ts
CHANGED
|
@@ -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;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-native/core",
|
|
3
|
-
"version": "0.179.
|
|
3
|
+
"version": "0.179.1-nightly-20260914215457",
|
|
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,8 +475,8 @@
|
|
|
475
475
|
"yaml": "^2.9.0",
|
|
476
476
|
"yjs": "^13.6.32",
|
|
477
477
|
"zod": "^4.3.6",
|
|
478
|
-
"@agent-native/
|
|
479
|
-
"@agent-native/
|
|
478
|
+
"@agent-native/toolkit": "^0.20.1-nightly-20260914215457",
|
|
479
|
+
"@agent-native/agentkit": "^0.2.1-nightly-20260914215457",
|
|
480
480
|
"@agent-native/recap-cli": "0.5.30"
|
|
481
481
|
},
|
|
482
482
|
"devDependencies": {
|