@agent-native/core 0.76.4 → 0.76.5
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/core/CHANGELOG.md +19 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/agent/production-agent.ts +147 -56
- package/corpus/core/src/agent/run-store.ts +36 -0
- package/corpus/core/src/deploy/build.ts +31 -14
- package/dist/agent/production-agent.d.ts +47 -0
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +112 -56
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/agent/run-store.d.ts +21 -0
- package/dist/agent/run-store.d.ts.map +1 -1
- package/dist/agent/run-store.js +32 -0
- package/dist/agent/run-store.js.map +1 -1
- package/dist/deploy/build.d.ts.map +1 -1
- package/dist/deploy/build.js +31 -14
- package/dist/deploy/build.js.map +1 -1
- package/package.json +1 -1
|
@@ -24,7 +24,7 @@ import { createToolSearchEntry, TOOL_SEARCH_ACTION_NAME, } from "./tool-search.j
|
|
|
24
24
|
import { getDefaultMaxIterations, normalizeMaxIterations, readAgentLoopSettings, } from "./loop-settings.js";
|
|
25
25
|
import { isReasoningEffort, normalizeReasoningEffortForModel, } from "../shared/reasoning-effort.js";
|
|
26
26
|
import { isAgentActionStopError } from "../action.js";
|
|
27
|
-
import { writeLedgerEntry, readLedgerEntry, clearLedgerForThread, getCurrentTurnEventsForThread, insertRun, updateRunHeartbeat, updateRunStatusIfRunning, claimBackgroundRun, recordRunDiagnostic, RUN_DIAG_STAGE, } from "./run-store.js";
|
|
27
|
+
import { writeLedgerEntry, readLedgerEntry, clearLedgerForThread, getCurrentTurnEventsForThread, insertRun, updateRunHeartbeat, updateRunStatusIfRunning, claimBackgroundRun, readBackgroundRunClaim, recordRunDiagnostic, RUN_DIAG_STAGE, } from "./run-store.js";
|
|
28
28
|
import { classifyToolCallJournal, findCompletedJournalEntry, } from "./tool-call-journal.js";
|
|
29
29
|
import { preUploadAttachments } from "../file-upload/pre-upload-attachments.js";
|
|
30
30
|
import { extensionIdFromPathname } from "../extensions/path.js";
|
|
@@ -38,6 +38,71 @@ import { maybeCompactThread, buildObservationalContext, hasObservationalMemory,
|
|
|
38
38
|
// Register built-in engines on first import
|
|
39
39
|
registerBuiltinEngines();
|
|
40
40
|
export { PROVIDER_TO_ENV };
|
|
41
|
+
/**
|
|
42
|
+
* Grace window + poll interval for the foreground circuit-breaker that confirms
|
|
43
|
+
* a background worker actually CLAIMED a 202-dispatched run before recovering
|
|
44
|
+
* inline. The grace is long enough for a cold-start worker to win the claim
|
|
45
|
+
* (~1-2s typical) and short enough to recover quickly within the foreground's
|
|
46
|
+
* ~40s soft-timeout.
|
|
47
|
+
*/
|
|
48
|
+
export const BACKGROUND_CLAIM_GRACE_MS = 8_000;
|
|
49
|
+
export const BACKGROUND_CLAIM_POLL_MS = 400;
|
|
50
|
+
/**
|
|
51
|
+
* Decide what the foreground should do after attempting a durable background
|
|
52
|
+
* dispatch. A Netlify async background function returns 202 the instant it
|
|
53
|
+
* ENQUEUES the invocation — that is NOT proof the worker executed. If the
|
|
54
|
+
* generated wrapper fails to import/hand off to the route, the worker never
|
|
55
|
+
* reaches `claimBackgroundRun` and the run is reaped as "worker never claimed".
|
|
56
|
+
*
|
|
57
|
+
* So after a successful dispatch we poll briefly for the worker to CLAIM the run:
|
|
58
|
+
* - claimed within grace → "stream" (subscribe to the worker)
|
|
59
|
+
* - dispatch failed OR no claim → recover inline by atomically claiming the
|
|
60
|
+
* run ourselves: if we win → "inline"; if a (delayed) worker already won
|
|
61
|
+
* it → "subscribe" (never double-run).
|
|
62
|
+
*
|
|
63
|
+
* Pure except for the injected `readClaim`/`claim`/`now`/`sleep` deps, so each
|
|
64
|
+
* branch is unit-testable.
|
|
65
|
+
*/
|
|
66
|
+
export async function resolveBackgroundDispatchOutcome(opts) {
|
|
67
|
+
const now = opts.now ?? (() => Date.now());
|
|
68
|
+
const sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
69
|
+
if (opts.dispatched) {
|
|
70
|
+
const deadline = now() + opts.graceMs;
|
|
71
|
+
for (;;) {
|
|
72
|
+
const claim = await opts.readClaim(opts.runId).catch(() => null);
|
|
73
|
+
if (claim &&
|
|
74
|
+
((claim.dispatchMode && claim.dispatchMode !== "background") ||
|
|
75
|
+
(claim.status && claim.status !== "running"))) {
|
|
76
|
+
return { action: "stream" };
|
|
77
|
+
}
|
|
78
|
+
if (now() >= deadline)
|
|
79
|
+
break;
|
|
80
|
+
await sleep(opts.pollIntervalMs);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// Dispatch fast-failed OR no worker claimed within grace → recover inline.
|
|
84
|
+
if (!opts.backgroundRowInserted) {
|
|
85
|
+
// No row to reconcile (insert failed / non-duplicate) — run a fresh inline
|
|
86
|
+
// turn; `startRun` inserts the row.
|
|
87
|
+
return { action: "inline", reason: "no-row" };
|
|
88
|
+
}
|
|
89
|
+
let claimedInline = false;
|
|
90
|
+
try {
|
|
91
|
+
claimedInline = await opts.claim(opts.runId);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
claimedInline = false;
|
|
95
|
+
}
|
|
96
|
+
if (claimedInline) {
|
|
97
|
+
return {
|
|
98
|
+
action: "inline",
|
|
99
|
+
reason: opts.dispatched ? "worker-never-claimed" : "dispatch-failed",
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
// The atomic claim was lost: a (delayed) background worker already owns the
|
|
103
|
+
// run — subscribe to it, never run a second copy.
|
|
104
|
+
return { action: "subscribe" };
|
|
105
|
+
}
|
|
41
106
|
const SAFE_BROWSER_TAB_ID_RE = /^[A-Za-z0-9_-]{1,96}$/;
|
|
42
107
|
function normalizeBrowserTabId(value) {
|
|
43
108
|
if (typeof value !== "string")
|
|
@@ -3375,7 +3440,30 @@ export function createProductionAgentHandler(options) {
|
|
|
3375
3440
|
catch (err) {
|
|
3376
3441
|
console.error("[agent-chat] background dispatch failed; falling back to inline:", err instanceof Error ? err.message : err);
|
|
3377
3442
|
}
|
|
3378
|
-
|
|
3443
|
+
// ─── Circuit-breaker: a 202 only ENQUEUES the background invocation ─────
|
|
3444
|
+
// It is NOT proof the worker executed. If the generated background-function
|
|
3445
|
+
// wrapper fails to import `./main.mjs` or hand off to the Nitro
|
|
3446
|
+
// `_process-run` route, the worker never reaches `claimBackgroundRun`: the
|
|
3447
|
+
// row sits at `dispatch_mode='background'` until the reaper errors it
|
|
3448
|
+
// ("worker never claimed the run"). `resolveBackgroundDispatchOutcome`
|
|
3449
|
+
// polls briefly for the claim and decides:
|
|
3450
|
+
// - "stream": a worker claimed the run → subscribe to it.
|
|
3451
|
+
// - "subscribe": a (delayed) worker already owns it → subscribe, NEVER
|
|
3452
|
+
// run a second copy.
|
|
3453
|
+
// - "inline": dispatch failed OR no worker claimed within grace → we
|
|
3454
|
+
// atomically own the run; recover by running it inline so a
|
|
3455
|
+
// dead worker degrades to a working synchronous turn.
|
|
3456
|
+
const backgroundOutcome = await resolveBackgroundDispatchOutcome({
|
|
3457
|
+
dispatched,
|
|
3458
|
+
backgroundRowInserted,
|
|
3459
|
+
runId,
|
|
3460
|
+
graceMs: BACKGROUND_CLAIM_GRACE_MS,
|
|
3461
|
+
pollIntervalMs: BACKGROUND_CLAIM_POLL_MS,
|
|
3462
|
+
readClaim: readBackgroundRunClaim,
|
|
3463
|
+
claim: claimBackgroundRun,
|
|
3464
|
+
});
|
|
3465
|
+
if (backgroundOutcome.action === "stream" ||
|
|
3466
|
+
backgroundOutcome.action === "subscribe") {
|
|
3379
3467
|
const stream = subscribeToRun(runId, 0);
|
|
3380
3468
|
if (stream) {
|
|
3381
3469
|
setResponseHeader(event, "Content-Type", "text/event-stream");
|
|
@@ -3384,62 +3472,30 @@ export function createProductionAgentHandler(options) {
|
|
|
3384
3472
|
setResponseHeader(event, "X-Run-Id", runId);
|
|
3385
3473
|
return stream;
|
|
3386
3474
|
}
|
|
3387
|
-
//
|
|
3388
|
-
// rather than
|
|
3389
|
-
|
|
3475
|
+
// A background worker owns this run but we cannot subscribe — surface an
|
|
3476
|
+
// error rather than risk a double-run by falling through to inline.
|
|
3477
|
+
await updateRunStatusIfRunning(runId, "errored").catch(() => { });
|
|
3390
3478
|
setResponseStatus(event, 500);
|
|
3391
|
-
return {
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
//
|
|
3398
|
-
//
|
|
3399
|
-
// double-execute.
|
|
3400
|
-
//
|
|
3401
|
-
//
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
// The SQL atomic claim is the single source of truth for ownership, so at
|
|
3410
|
-
// most one of {inline fallback, background worker} ever executes this run.
|
|
3411
|
-
if (backgroundRowInserted) {
|
|
3412
|
-
let claimedInline = false;
|
|
3413
|
-
try {
|
|
3414
|
-
claimedInline = await claimBackgroundRun(runId);
|
|
3415
|
-
}
|
|
3416
|
-
catch (err) {
|
|
3417
|
-
console.error("[agent-chat] inline-fallback claim failed:", err instanceof Error ? err.message : err);
|
|
3418
|
-
}
|
|
3419
|
-
if (!claimedInline) {
|
|
3420
|
-
// A background worker already owns this run (a delayed delivery landed
|
|
3421
|
-
// after our fast throw). Stream its events instead of running a second
|
|
3422
|
-
// copy. If we somehow can't subscribe, surface an error rather than
|
|
3423
|
-
// risk a double-run.
|
|
3424
|
-
const stream = subscribeToRun(runId, 0);
|
|
3425
|
-
if (stream) {
|
|
3426
|
-
setResponseHeader(event, "Content-Type", "text/event-stream");
|
|
3427
|
-
setResponseHeader(event, "Cache-Control", "no-cache");
|
|
3428
|
-
setResponseHeader(event, "Connection", "keep-alive");
|
|
3429
|
-
setResponseHeader(event, "X-Run-Id", runId);
|
|
3430
|
-
return stream;
|
|
3431
|
-
}
|
|
3432
|
-
await updateRunStatusIfRunning(runId, "errored").catch(() => { });
|
|
3433
|
-
setResponseStatus(event, 500);
|
|
3434
|
-
return { error: "Failed to dispatch background run" };
|
|
3435
|
-
}
|
|
3436
|
-
// We own the run. `startRun` (below) calls `insertRun` again, but its
|
|
3437
|
-
// duplicate-PK collision is swallowed (`insertRun(...).catch(() => {})`),
|
|
3438
|
-
// so the existing `background-processing` row is reused — no double row.
|
|
3479
|
+
return {
|
|
3480
|
+
error: backgroundOutcome.action === "stream"
|
|
3481
|
+
? "Failed to subscribe to background run"
|
|
3482
|
+
: "Failed to dispatch background run",
|
|
3483
|
+
};
|
|
3484
|
+
}
|
|
3485
|
+
// backgroundOutcome.action === "inline": we atomically own the run (or
|
|
3486
|
+
// there was no row to reconcile), so falling through to the inline
|
|
3487
|
+
// `startRun` path below cannot double-execute. `startRun` calls `insertRun`
|
|
3488
|
+
// again, but its duplicate-PK collision is swallowed, so an existing
|
|
3489
|
+
// `background-processing` row is reused — no double row.
|
|
3490
|
+
if (backgroundOutcome.reason === "worker-never-claimed") {
|
|
3491
|
+
// The async 202 landed but no worker claimed within grace — the generated
|
|
3492
|
+
// wrapper never reached the route. Record it so the failure is visible on
|
|
3493
|
+
// the run even though the user still gets a working (inline) turn.
|
|
3494
|
+
console.error("[agent-chat] background worker did not claim the 202-dispatched run " +
|
|
3495
|
+
"within grace; recovering inline:", runId);
|
|
3496
|
+
await recordRunDiagnostic(runId, RUN_DIAG_STAGE.foregroundInlineRecovery, "202 dispatched but no worker claimed within the foreground grace window").catch(() => { });
|
|
3439
3497
|
}
|
|
3440
|
-
// Fall through to the inline `startRun` path below
|
|
3441
|
-
// flag-off branch uses). If the row was never inserted, `startRun` inserts
|
|
3442
|
-
// it fresh; if it was, the claim above made us the sole owner.
|
|
3498
|
+
// Fall through to the inline `startRun` path below.
|
|
3443
3499
|
}
|
|
3444
3500
|
const trackedProgressOwner = trackInRunsTray === true && ownerEmail ? ownerEmail : null;
|
|
3445
3501
|
const trackedProgressRunId = trackedProgressOwner
|