@agent-native/core 0.210.1-nightly-20261020124228 → 0.210.2
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/dist/a2a/server.js +2 -1
- package/dist/agent/contain-principal.js +2 -3
- package/dist/agent/engine/ai-sdk-engine.js +29 -20
- package/dist/agent/engine/anthropic-engine.js +10 -1
- package/dist/agent/engine/builder-engine.js +28 -14
- package/dist/agent/engine/types.d.ts +11 -0
- package/dist/agent/engine/types.js +8 -0
- package/dist/agent/execution-fence.d.ts +5 -0
- package/dist/agent/execution-fence.js +15 -0
- package/dist/agent/harness/background.js +28 -7
- package/dist/agent/production-agent.d.ts +4 -3
- package/dist/agent/production-agent.js +20 -3
- package/dist/agent/run-manager.d.ts +8 -1
- package/dist/agent/run-manager.js +273 -34
- package/dist/agent/run-store.d.ts +3 -2
- package/dist/agent/run-store.fencing.worker.js +19 -1
- package/dist/agent/run-store.js +216 -67
- package/dist/client/use-agent-engine-configured.d.ts +2 -2
- package/dist/client/use-agent-engine-configured.js +3 -9
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/mcp/screen-memory-stdio.d.ts +7 -7
- 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/secrets/routes.d.ts +4 -4
- package/dist/server/agent-chat-plugin.js +27 -11
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/agent-engine-default-model-route.d.ts +1 -1
- package/dist/server/agent-engine-ollama-models-route.d.ts +2 -2
- package/dist/server/agent-teams.d.ts +3 -0
- package/dist/server/agent-teams.js +192 -30
- package/package.json +4 -4
|
@@ -11,7 +11,7 @@ import { runErrorTelemetryProperties } from "./engine/error-telemetry.js";
|
|
|
11
11
|
import { getAgentEngineEntry } from "./engine/registry.js";
|
|
12
12
|
import { EngineError } from "./engine/types.js";
|
|
13
13
|
import { currentExecutionFence, MissingExecutionFenceError, StaleExecutionFenceError, withExecutionFence, withExecutionLeaseRequested, } from "./execution-fence.js";
|
|
14
|
-
import { insertRun, insertRunEvent, insertRunEvents, updateRunStatusIfRunning, markRunAborted, markRunAbortedAtSafeBoundary, getRunAbortState, getRunStatus, getRunEventsSince, getCurrentTurnRunEventsForThread, getRunById, getRunByThread, getRunTurnRef, markTurnAborted, cleanupOldRuns, updateRunHeartbeat, bumpRunProgress, setRunInFlightMarker, reapIfStale, reapUnclaimedBackgroundRun, shouldRedispatchUnclaimedBackgroundRun, reconcileTerminalRunFromEvents, ensureTerminalRunEvent, getLastTerminalRunEvent, resolveErroredRunTerminalEvent, setRunError, setRunTerminalReason, persistRunCheckpointEvent, recordRunDiagnostic, RUN_DIAG_STAGE, claimTurnExecutionLease, renewTurnExecutionLease, releaseTurnExecutionLease, TurnExecutionLeaseBusyError, terminalEventForAbortReason, RUN_RECORD_MISSING_ERROR_EVENT, RUN_RECORD_MISSING_GRACE_MS, RUN_TERMINAL_LOOKUP_FAILED_ERROR_EVENT, UNKNOWN_RUN_STATUS_ERROR_EVENT, } from "./run-store.js";
|
|
14
|
+
import { AgentRunEventNotPersistedError, insertRun, insertRunEvent, insertRunEvents, updateRunStatusIfRunning, markRunAborted, markRunAbortedAtSafeBoundary, getRunAbortState, getRunStatus, getRunEventsSince, getCurrentTurnRunEventsForThread, getRunById, getRunByThread, getRunTurnRef, markTurnAborted, cleanupOldRuns, updateRunHeartbeat, bumpRunProgress, setRunInFlightMarker, reapIfStale, reapUnclaimedBackgroundRun, shouldRedispatchUnclaimedBackgroundRun, reconcileTerminalRunFromEvents, ensureTerminalRunEvent, getLastTerminalRunEvent, resolveErroredRunTerminalEvent, setRunError, setRunTerminalReason, persistRunCheckpointEvent, recordRunDiagnostic, RUN_DIAG_STAGE, claimTurnExecutionLease, renewTurnExecutionLease, releaseTurnExecutionLease, TurnExecutionLeaseBusyError, terminalEventForAbortReason, RUN_RECORD_MISSING_ERROR_EVENT, RUN_RECORD_MISSING_GRACE_MS, RUN_TERMINAL_LOOKUP_FAILED_ERROR_EVENT, UNKNOWN_RUN_STATUS_ERROR_EVENT, } from "./run-store.js";
|
|
15
15
|
import { isContinuationTerminalReason } from "./types.js";
|
|
16
16
|
const activeRuns = new Map();
|
|
17
17
|
const threadToRun = new Map();
|
|
@@ -37,11 +37,17 @@ function reportAbortFlushFailure(run, error) {
|
|
|
37
37
|
}
|
|
38
38
|
async function flushRunEventsBeforeAbort(run) {
|
|
39
39
|
try {
|
|
40
|
+
await run?.waitForPersistence?.();
|
|
40
41
|
await run?.flushEvents?.();
|
|
42
|
+
return null;
|
|
41
43
|
}
|
|
42
44
|
catch (error) {
|
|
43
45
|
if (run)
|
|
44
46
|
reportAbortFlushFailure(run, error);
|
|
47
|
+
if (error instanceof AgentRunEventNotPersistedError && error.terminalRun) {
|
|
48
|
+
return error;
|
|
49
|
+
}
|
|
50
|
+
throw error;
|
|
45
51
|
}
|
|
46
52
|
}
|
|
47
53
|
const CLEANUP_DELAY_MS = 5 * 60 * 1000;
|
|
@@ -543,7 +549,7 @@ export function startRun(runId, threadId, runFn, onComplete, options) {
|
|
|
543
549
|
});
|
|
544
550
|
}
|
|
545
551
|
const runControl = {
|
|
546
|
-
flushEvents: () =>
|
|
552
|
+
flushEvents: () => flushEventWrites(true),
|
|
547
553
|
get turnSignal() {
|
|
548
554
|
return abort.signal;
|
|
549
555
|
},
|
|
@@ -678,7 +684,85 @@ export function startRun(runId, threadId, runFn, onComplete, options) {
|
|
|
678
684
|
});
|
|
679
685
|
};
|
|
680
686
|
let persistenceChain = Promise.resolve();
|
|
681
|
-
|
|
687
|
+
const pendingEventWrites = [];
|
|
688
|
+
let persistenceDrain = null;
|
|
689
|
+
let persistenceFailure;
|
|
690
|
+
let persistenceFailureBlocked = false;
|
|
691
|
+
const flushEventWrites = (retryAfterFailure) => {
|
|
692
|
+
if (persistenceDrain) {
|
|
693
|
+
const activeDrain = persistenceDrain;
|
|
694
|
+
const settledDrain = retryAfterFailure
|
|
695
|
+
? activeDrain.catch((error) => {
|
|
696
|
+
if (error instanceof AgentRunEventNotPersistedError &&
|
|
697
|
+
error.terminalRun) {
|
|
698
|
+
throw error;
|
|
699
|
+
}
|
|
700
|
+
})
|
|
701
|
+
: activeDrain;
|
|
702
|
+
const flush = settledDrain.then(() => pendingEventWrites.length > 0
|
|
703
|
+
? flushEventWrites(retryAfterFailure)
|
|
704
|
+
: undefined);
|
|
705
|
+
persistenceChain = flush;
|
|
706
|
+
return flush;
|
|
707
|
+
}
|
|
708
|
+
if (pendingEventWrites.length === 0) {
|
|
709
|
+
persistenceChain = Promise.resolve();
|
|
710
|
+
return persistenceChain;
|
|
711
|
+
}
|
|
712
|
+
if (persistenceFailureBlocked && !retryAfterFailure) {
|
|
713
|
+
const blocked = Promise.reject(persistenceFailure);
|
|
714
|
+
persistenceChain = blocked;
|
|
715
|
+
return blocked;
|
|
716
|
+
}
|
|
717
|
+
if (retryAfterFailure) {
|
|
718
|
+
persistenceFailure = undefined;
|
|
719
|
+
persistenceFailureBlocked = false;
|
|
720
|
+
}
|
|
721
|
+
const drain = Promise.resolve().then(async () => {
|
|
722
|
+
while (pendingEventWrites.length > 0) {
|
|
723
|
+
const pending = pendingEventWrites[0];
|
|
724
|
+
try {
|
|
725
|
+
await pending.write();
|
|
726
|
+
}
|
|
727
|
+
catch (error) {
|
|
728
|
+
const terminalRun = error instanceof AgentRunEventNotPersistedError &&
|
|
729
|
+
error.terminalRun;
|
|
730
|
+
persistenceFailure = terminalRun ? undefined : error;
|
|
731
|
+
persistenceFailureBlocked = !terminalRun;
|
|
732
|
+
for (const queued of pendingEventWrites) {
|
|
733
|
+
for (const publication of queued.publications) {
|
|
734
|
+
if (publication.settled)
|
|
735
|
+
continue;
|
|
736
|
+
publication.settled = true;
|
|
737
|
+
publication.reject(error);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
if (terminalRun) {
|
|
741
|
+
pendingEventWrites.length = 0;
|
|
742
|
+
openEventBatch = null;
|
|
743
|
+
}
|
|
744
|
+
throw error;
|
|
745
|
+
}
|
|
746
|
+
pendingEventWrites.shift();
|
|
747
|
+
for (const publication of pending.publications) {
|
|
748
|
+
publishRunEvent(publication.runEvent);
|
|
749
|
+
if (publication.settled)
|
|
750
|
+
continue;
|
|
751
|
+
publication.settled = true;
|
|
752
|
+
publication.resolve();
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
});
|
|
756
|
+
const activeDrain = drain.finally(() => {
|
|
757
|
+
if (persistenceDrain === activeDrain)
|
|
758
|
+
persistenceDrain = null;
|
|
759
|
+
});
|
|
760
|
+
persistenceDrain = activeDrain;
|
|
761
|
+
persistenceChain = activeDrain;
|
|
762
|
+
return activeDrain;
|
|
763
|
+
};
|
|
764
|
+
run.waitForPersistence = () => insertRunPromise;
|
|
765
|
+
run.flushEvents = () => flushEventWrites(true);
|
|
682
766
|
// Streamed deltas arrive hundreds per tool call, and a write tool waits for
|
|
683
767
|
// every queued event to persist first; one round trip per event put tens of
|
|
684
768
|
// seconds between the model finishing and the tool starting.
|
|
@@ -1189,6 +1273,28 @@ export function startRun(runId, threadId, runFn, onComplete, options) {
|
|
|
1189
1273
|
bumpProgressIfDue();
|
|
1190
1274
|
}
|
|
1191
1275
|
};
|
|
1276
|
+
const queueRunEvent = (pendingWrite, runEvent) => {
|
|
1277
|
+
let resolve;
|
|
1278
|
+
let reject;
|
|
1279
|
+
const publication = new Promise((res, rej) => {
|
|
1280
|
+
resolve = res;
|
|
1281
|
+
reject = rej;
|
|
1282
|
+
});
|
|
1283
|
+
const pendingPublication = {
|
|
1284
|
+
runEvent,
|
|
1285
|
+
resolve,
|
|
1286
|
+
reject,
|
|
1287
|
+
settled: false,
|
|
1288
|
+
};
|
|
1289
|
+
pendingWrite.publications.push(pendingPublication);
|
|
1290
|
+
void flushEventWrites(false).catch((error) => {
|
|
1291
|
+
if (pendingPublication.settled)
|
|
1292
|
+
return;
|
|
1293
|
+
pendingPublication.settled = true;
|
|
1294
|
+
pendingPublication.reject(error);
|
|
1295
|
+
});
|
|
1296
|
+
return publication;
|
|
1297
|
+
};
|
|
1192
1298
|
const emitRunEvent = (runEvent, options) => {
|
|
1193
1299
|
const eventData = JSON.stringify(runEvent.event);
|
|
1194
1300
|
const eventAt = Date.now();
|
|
@@ -1204,40 +1310,47 @@ export function startRun(runId, threadId, runFn, onComplete, options) {
|
|
|
1204
1310
|
eventType: runEvent.event.type,
|
|
1205
1311
|
});
|
|
1206
1312
|
}
|
|
1313
|
+
if (error instanceof AgentRunEventNotPersistedError &&
|
|
1314
|
+
error.terminalRun) {
|
|
1315
|
+
throw error;
|
|
1316
|
+
}
|
|
1207
1317
|
await write();
|
|
1208
1318
|
}
|
|
1209
1319
|
};
|
|
1210
|
-
let
|
|
1320
|
+
let publication;
|
|
1211
1321
|
if (isBatchableRunEvent(runEvent.event) && !persistEventHook) {
|
|
1212
1322
|
if (!openEventBatch) {
|
|
1213
1323
|
const batch = {
|
|
1214
1324
|
events: [],
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1325
|
+
pendingWrite: {
|
|
1326
|
+
publications: [],
|
|
1327
|
+
write: () => {
|
|
1328
|
+
if (openEventBatch === batch)
|
|
1329
|
+
openEventBatch = null;
|
|
1330
|
+
return retryOnce(() => withWorkerFence(() => insertRunEvents(runId, batch.events)));
|
|
1331
|
+
},
|
|
1332
|
+
},
|
|
1220
1333
|
};
|
|
1221
1334
|
openEventBatch = batch;
|
|
1222
|
-
|
|
1335
|
+
pendingEventWrites.push(batch.pendingWrite);
|
|
1223
1336
|
}
|
|
1224
1337
|
openEventBatch.events.push({
|
|
1225
1338
|
seq: runEvent.seq,
|
|
1226
1339
|
eventAt,
|
|
1227
1340
|
eventData,
|
|
1228
1341
|
});
|
|
1229
|
-
|
|
1342
|
+
publication = queueRunEvent(openEventBatch.pendingWrite, runEvent);
|
|
1230
1343
|
}
|
|
1231
1344
|
else {
|
|
1232
1345
|
// Anything emitted after this event must not persist before it.
|
|
1233
1346
|
openEventBatch = null;
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1347
|
+
const pendingWrite = {
|
|
1348
|
+
publications: [],
|
|
1349
|
+
write: () => retryOnce(() => persistRunEvent(runEvent, eventData, eventAt)),
|
|
1350
|
+
};
|
|
1351
|
+
pendingEventWrites.push(pendingWrite);
|
|
1352
|
+
publication = queueRunEvent(pendingWrite, runEvent);
|
|
1239
1353
|
}
|
|
1240
|
-
const publication = persistence.then(() => publishRunEvent(runEvent));
|
|
1241
1354
|
if (!options?.surfacePersistenceError)
|
|
1242
1355
|
publication.catch(() => { });
|
|
1243
1356
|
checkSqlAbort();
|
|
@@ -2207,11 +2320,33 @@ export async function getActiveRunForThreadAsync(threadId) {
|
|
|
2207
2320
|
const isUnclaimedBackgroundDispatch = sqlRun.dispatchMode === "background";
|
|
2208
2321
|
const stillInsideRedispatchBound = shouldRedispatchUnclaimedBackgroundRun({ startedAt: sqlRun.startedAt });
|
|
2209
2322
|
if (isUnclaimedBackgroundDispatch && !stillInsideRedispatchBound) {
|
|
2210
|
-
const recovered = await reapUnclaimedBackgroundRun(sqlRun.id).catch(() =>
|
|
2323
|
+
const recovered = await reapUnclaimedBackgroundRun(sqlRun.id).catch((error) => {
|
|
2324
|
+
captureError(error, {
|
|
2325
|
+
route: "/_agent-native/agent-chat/runs/active",
|
|
2326
|
+
aiTraceId: sqlRun.id,
|
|
2327
|
+
tags: {
|
|
2328
|
+
source: "agent-run-manager",
|
|
2329
|
+
phase: "unclaimed-background-reap",
|
|
2330
|
+
},
|
|
2331
|
+
extra: { runId: sqlRun.id, threadId },
|
|
2332
|
+
});
|
|
2333
|
+
return false;
|
|
2334
|
+
});
|
|
2211
2335
|
if (recovered)
|
|
2212
2336
|
return null;
|
|
2213
2337
|
}
|
|
2214
|
-
const reaped = await reapIfStale(sqlRun.id).catch(() =>
|
|
2338
|
+
const reaped = await reapIfStale(sqlRun.id).catch((error) => {
|
|
2339
|
+
captureError(error, {
|
|
2340
|
+
route: "/_agent-native/agent-chat/runs/active",
|
|
2341
|
+
aiTraceId: sqlRun.id,
|
|
2342
|
+
tags: {
|
|
2343
|
+
source: "agent-run-manager",
|
|
2344
|
+
phase: "stale-run-reap",
|
|
2345
|
+
},
|
|
2346
|
+
extra: { runId: sqlRun.id, threadId },
|
|
2347
|
+
});
|
|
2348
|
+
return false;
|
|
2349
|
+
});
|
|
2215
2350
|
if (reaped)
|
|
2216
2351
|
return null;
|
|
2217
2352
|
return {
|
|
@@ -2299,16 +2434,17 @@ function abortRunInMemory(runId, reason) {
|
|
|
2299
2434
|
export function abortRun(runId, reason = "user") {
|
|
2300
2435
|
const run = activeRuns.get(runId);
|
|
2301
2436
|
const abortedInMemory = abortRunInMemory(runId, reason);
|
|
2437
|
+
const markAborted = () => markRunAborted(runId, reason);
|
|
2302
2438
|
void flushRunEventsBeforeAbort(run)
|
|
2303
|
-
.then(
|
|
2439
|
+
.then(markAborted, markAborted)
|
|
2304
2440
|
.catch((error) => {
|
|
2305
2441
|
captureError(error, {
|
|
2306
2442
|
route: "/_agent-native/agent-chat/runs/:id/abort",
|
|
2307
2443
|
aiTraceId: runId,
|
|
2308
|
-
tags: { source: "agent-run-manager", phase: "abort-run" },
|
|
2444
|
+
tags: { source: "agent-run-manager", phase: "abort-run-status" },
|
|
2309
2445
|
extra: { runId, threadId: run?.threadId, reason },
|
|
2310
2446
|
});
|
|
2311
|
-
console.error("[run-manager] durable abort
|
|
2447
|
+
console.error("[run-manager] durable abort status write failed:", error instanceof Error ? error.message : error);
|
|
2312
2448
|
});
|
|
2313
2449
|
return abortedInMemory;
|
|
2314
2450
|
}
|
|
@@ -2318,12 +2454,27 @@ export function abortRun(runId, reason = "user") {
|
|
|
2318
2454
|
* must use this path; otherwise the recovery POST can race the old row while
|
|
2319
2455
|
* it is still marked running.
|
|
2320
2456
|
*/
|
|
2321
|
-
export async function abortRunDurably(runId, reason = "user") {
|
|
2457
|
+
export async function abortRunDurably(runId, reason = "user", options) {
|
|
2322
2458
|
const run = activeRuns.get(runId);
|
|
2323
2459
|
const abortedInMemory = abortRunInMemory(runId, reason);
|
|
2324
2460
|
try {
|
|
2325
|
-
await flushRunEventsBeforeAbort(run);
|
|
2326
|
-
await markRunAborted(runId, reason);
|
|
2461
|
+
const eventFlushFailure = await flushRunEventsBeforeAbort(run);
|
|
2462
|
+
const abortPersisted = await markRunAborted(runId, reason);
|
|
2463
|
+
if (!abortPersisted || eventFlushFailure) {
|
|
2464
|
+
const persistedStatus = await getRunStatus(runId);
|
|
2465
|
+
if (persistedStatus === null) {
|
|
2466
|
+
// Agent Teams persists Stop intent before run registration.
|
|
2467
|
+
// Workers recheck it before dispatch.
|
|
2468
|
+
if (options?.allowNotStarted && !run && !eventFlushFailure)
|
|
2469
|
+
return false;
|
|
2470
|
+
throw new Error(`Cannot confirm durable abort for missing run ${runId}`);
|
|
2471
|
+
}
|
|
2472
|
+
if (!abortPersisted && persistedStatus === "running") {
|
|
2473
|
+
throw new Error(`Durable abort did not transition run ${runId}`);
|
|
2474
|
+
}
|
|
2475
|
+
}
|
|
2476
|
+
if (eventFlushFailure)
|
|
2477
|
+
throw eventFlushFailure;
|
|
2327
2478
|
}
|
|
2328
2479
|
catch (error) {
|
|
2329
2480
|
captureError(error, {
|
|
@@ -2341,19 +2492,57 @@ export async function abortRunDurably(runId, reason = "user") {
|
|
|
2341
2492
|
},
|
|
2342
2493
|
});
|
|
2343
2494
|
console.error("[run-manager] durable abort persistence failed:", error instanceof Error ? error.message : error);
|
|
2495
|
+
throw error;
|
|
2344
2496
|
}
|
|
2345
2497
|
return abortedInMemory;
|
|
2346
2498
|
}
|
|
2347
|
-
|
|
2348
|
-
export async function abortRunAtSafeBoundaryDurably(runId, reason = "user") {
|
|
2499
|
+
async function abortRunAtSafeBoundaryDurablyResult(runId, reason = "user") {
|
|
2349
2500
|
const run = activeRuns.get(runId);
|
|
2350
|
-
await flushRunEventsBeforeAbort(run);
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2501
|
+
const eventFlushFailure = await flushRunEventsBeforeAbort(run);
|
|
2502
|
+
let aborted;
|
|
2503
|
+
try {
|
|
2504
|
+
aborted = await markRunAbortedAtSafeBoundary(runId, reason);
|
|
2505
|
+
}
|
|
2506
|
+
catch (error) {
|
|
2507
|
+
try {
|
|
2508
|
+
const state = await getRunAbortState(runId);
|
|
2509
|
+
if (state.aborted && run) {
|
|
2510
|
+
abortRunInMemory(runId, state.reason ?? reason);
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2513
|
+
catch (stateError) {
|
|
2514
|
+
captureError(stateError, {
|
|
2515
|
+
route: "/_agent-native/agent-chat/runs/:id/abort",
|
|
2516
|
+
aiTraceId: runId,
|
|
2517
|
+
tags: {
|
|
2518
|
+
source: "agent-run-manager",
|
|
2519
|
+
phase: "safe-boundary-abort-state-read",
|
|
2520
|
+
},
|
|
2521
|
+
extra: {
|
|
2522
|
+
runId,
|
|
2523
|
+
reason,
|
|
2524
|
+
abortError: error instanceof Error ? error.message : String(error),
|
|
2525
|
+
},
|
|
2526
|
+
});
|
|
2527
|
+
}
|
|
2528
|
+
if (eventFlushFailure) {
|
|
2529
|
+
throw new AggregateError([eventFlushFailure, error], "Run abort event flush and marker persistence failed");
|
|
2530
|
+
}
|
|
2531
|
+
throw error;
|
|
2532
|
+
}
|
|
2533
|
+
if (!aborted) {
|
|
2534
|
+
return { aborted: false, eventFlushFailure };
|
|
2535
|
+
}
|
|
2354
2536
|
if (run)
|
|
2355
2537
|
abortInMemoryRun(run, reason);
|
|
2356
|
-
return true;
|
|
2538
|
+
return { aborted: true, eventFlushFailure };
|
|
2539
|
+
}
|
|
2540
|
+
/** Abort after the durable run store confirms no model or tool work is in flight. */
|
|
2541
|
+
export async function abortRunAtSafeBoundaryDurably(runId, reason = "user") {
|
|
2542
|
+
const result = await abortRunAtSafeBoundaryDurablyResult(runId, reason);
|
|
2543
|
+
if (result.eventFlushFailure)
|
|
2544
|
+
throw result.eventFlushFailure;
|
|
2545
|
+
return result.aborted;
|
|
2357
2546
|
}
|
|
2358
2547
|
export async function abortTurnByRefDurably(threadId, turnId, reason = "user") {
|
|
2359
2548
|
const fence = currentExecutionFence();
|
|
@@ -2369,8 +2558,22 @@ export async function abortTurnByRefDurably(threadId, turnId, reason = "user") {
|
|
|
2369
2558
|
runsToFlush.push(run);
|
|
2370
2559
|
}
|
|
2371
2560
|
}
|
|
2372
|
-
await Promise.all(runsToFlush.map((run) => flushRunEventsBeforeAbort(run)));
|
|
2373
|
-
|
|
2561
|
+
const flushFailures = await Promise.all(runsToFlush.map((run) => flushRunEventsBeforeAbort(run)));
|
|
2562
|
+
let result;
|
|
2563
|
+
try {
|
|
2564
|
+
result = await markTurnAborted(threadId, turnId, reason);
|
|
2565
|
+
}
|
|
2566
|
+
catch (error) {
|
|
2567
|
+
const eventFlushFailure = flushFailures.find(Boolean);
|
|
2568
|
+
if (eventFlushFailure) {
|
|
2569
|
+
throw new AggregateError([eventFlushFailure, error], "Turn abort event flush and marker persistence failed");
|
|
2570
|
+
}
|
|
2571
|
+
throw error;
|
|
2572
|
+
}
|
|
2573
|
+
const eventFlushFailure = flushFailures.find(Boolean);
|
|
2574
|
+
if (eventFlushFailure)
|
|
2575
|
+
throw eventFlushFailure;
|
|
2576
|
+
return result;
|
|
2374
2577
|
}
|
|
2375
2578
|
export async function abortTurnDurably(runId, reason = "user") {
|
|
2376
2579
|
const memRun = activeRuns.get(runId);
|
|
@@ -2393,4 +2596,40 @@ export async function abortTurnDurably(runId, reason = "user") {
|
|
|
2393
2596
|
throw error;
|
|
2394
2597
|
}
|
|
2395
2598
|
}
|
|
2599
|
+
/** Persist a Stop's turn marker even when a terminal event flush must be reported. */
|
|
2600
|
+
export async function abortRunAndTurnDurably(runId, reason = "user", options) {
|
|
2601
|
+
let abortedInMemory = false;
|
|
2602
|
+
let terminalFlushFailure;
|
|
2603
|
+
try {
|
|
2604
|
+
if (options?.safeBoundary) {
|
|
2605
|
+
const result = await abortRunAtSafeBoundaryDurablyResult(runId, reason);
|
|
2606
|
+
abortedInMemory = result.aborted;
|
|
2607
|
+
terminalFlushFailure = result.eventFlushFailure ?? undefined;
|
|
2608
|
+
}
|
|
2609
|
+
else {
|
|
2610
|
+
abortedInMemory = await abortRunDurably(runId, reason);
|
|
2611
|
+
}
|
|
2612
|
+
}
|
|
2613
|
+
catch (error) {
|
|
2614
|
+
if (!(error instanceof AgentRunEventNotPersistedError) ||
|
|
2615
|
+
!error.terminalRun) {
|
|
2616
|
+
throw error;
|
|
2617
|
+
}
|
|
2618
|
+
terminalFlushFailure = error;
|
|
2619
|
+
}
|
|
2620
|
+
if (!options?.safeBoundary || abortedInMemory) {
|
|
2621
|
+
try {
|
|
2622
|
+
await abortTurnDurably(runId, reason);
|
|
2623
|
+
}
|
|
2624
|
+
catch (error) {
|
|
2625
|
+
if (terminalFlushFailure) {
|
|
2626
|
+
throw new AggregateError([terminalFlushFailure, error], "Run event flush and turn abort persistence failed");
|
|
2627
|
+
}
|
|
2628
|
+
throw error;
|
|
2629
|
+
}
|
|
2630
|
+
}
|
|
2631
|
+
if (terminalFlushFailure)
|
|
2632
|
+
throw terminalFlushFailure;
|
|
2633
|
+
return abortedInMemory;
|
|
2634
|
+
}
|
|
2396
2635
|
export { getSlotHoldingRunId, tryClaimRunSlot } from "./run-store.js";
|
|
@@ -353,7 +353,7 @@ export declare function failRunBeforeDispatch(runId: string, errorCode: string,
|
|
|
353
353
|
export declare function releaseBackgroundRunBeforeStart(runId: string, errorCode: string, errorDetail: string): Promise<void>;
|
|
354
354
|
export declare function getRunStatus(runId: string): Promise<string | null>;
|
|
355
355
|
export declare function terminalEventForAbortReason(reason: string | undefined): AgentChatEvent;
|
|
356
|
-
export declare function markRunAborted(runId: string, reason?: string): Promise<
|
|
356
|
+
export declare function markRunAborted(runId: string, reason?: string): Promise<boolean>;
|
|
357
357
|
/** Abort only when no model or tool work has crossed its dispatch boundary. */
|
|
358
358
|
export declare function markRunAbortedAtSafeBoundary(runId: string, reason?: string): Promise<boolean>;
|
|
359
359
|
/** Serialize a task lifecycle change with worker lease claims and writes. */
|
|
@@ -372,7 +372,8 @@ export declare function getRunAbortState(runId: string): Promise<{
|
|
|
372
372
|
reason?: string;
|
|
373
373
|
}>;
|
|
374
374
|
export declare class AgentRunEventNotPersistedError extends Error {
|
|
375
|
-
|
|
375
|
+
readonly terminalRun: boolean;
|
|
376
|
+
constructor(runId: string, seq: number | null, terminalRun?: boolean, cause?: unknown);
|
|
376
377
|
}
|
|
377
378
|
export declare function insertRunEvent(runId: string, seq: number, eventData: string, options?: {
|
|
378
379
|
toolInputSource?: "execution";
|
|
@@ -4,6 +4,7 @@ import { ensureChatThreadTables, getThread, markQueuedSubmissionSubmitted, updat
|
|
|
4
4
|
import { toPostgresParams, withDbExec } from "../db/client.js";
|
|
5
5
|
import { withMigrationRuntime } from "../db/migration-runtime.js";
|
|
6
6
|
import { withExecutionFence } from "./execution-fence.js";
|
|
7
|
+
import { abortRunAtSafeBoundaryDurably } from "./run-manager.js";
|
|
7
8
|
import { beginAgentExecutionStep, claimTurnExecutionLease, completeAgentExecutionStep, ensureRunTables, insertRun, insertRunEvent, markAgentExecutionStepDispatched, } from "./run-store.js";
|
|
8
9
|
import { applySubmittedUserMessage, buildUserMessage, extractThreadMeta, } from "./thread-data-builder.js";
|
|
9
10
|
function executor(sql) {
|
|
@@ -38,6 +39,7 @@ function waitForContinue() {
|
|
|
38
39
|
process.stdin.resume();
|
|
39
40
|
});
|
|
40
41
|
}
|
|
42
|
+
let databaseBackendPid;
|
|
41
43
|
async function main() {
|
|
42
44
|
const databaseUrl = process.env.AGENT_RUN_POSTGRES_TEST_URL; // config-ok: isolated integration-test database selector, not app runtime config
|
|
43
45
|
if (!databaseUrl)
|
|
@@ -112,6 +114,17 @@ async function main() {
|
|
|
112
114
|
}));
|
|
113
115
|
return;
|
|
114
116
|
}
|
|
117
|
+
if (mode === "durable-abort") {
|
|
118
|
+
const [{ pid }] = await sql `SELECT pg_backend_pid() AS pid`;
|
|
119
|
+
databaseBackendPid = Number(pid);
|
|
120
|
+
const aborted = await abortRunAtSafeBoundaryDurably(runId, stepId ?? "user");
|
|
121
|
+
process.stdout.write(JSON.stringify({
|
|
122
|
+
aborted,
|
|
123
|
+
processId: process.pid,
|
|
124
|
+
databaseBackendPid,
|
|
125
|
+
}));
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
115
128
|
await insertRun(runId, threadId, turnId);
|
|
116
129
|
const fence = await claimTurnExecutionLease(threadId, turnId, runId);
|
|
117
130
|
if (mode === "claim") {
|
|
@@ -170,6 +183,11 @@ main().catch((error) => {
|
|
|
170
183
|
const code = error && typeof error === "object" && "code" in error
|
|
171
184
|
? String(error.code)
|
|
172
185
|
: undefined;
|
|
173
|
-
process.stdout.write(JSON.stringify({
|
|
186
|
+
process.stdout.write(JSON.stringify({
|
|
187
|
+
error: String(error),
|
|
188
|
+
code,
|
|
189
|
+
processId: process.pid,
|
|
190
|
+
databaseBackendPid,
|
|
191
|
+
}));
|
|
174
192
|
process.exitCode = code === "turn_execution_lease_busy" ? 3 : 1;
|
|
175
193
|
});
|