@noetaris/harness 0.12.0 → 0.13.0

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/index.js CHANGED
@@ -564,6 +564,9 @@ function composeObservers(observers, onObserverError) {
564
564
  onStepSettled: (ctx, event) => {
565
565
  for (const o of observers) safeInvoke("onStepSettled", () => o.onStepSettled?.(ctx, event), onObserverError);
566
566
  },
567
+ onCheckpoint: (ctx, event) => {
568
+ for (const o of observers) safeInvoke("onCheckpoint", () => o.onCheckpoint?.(ctx, event), onObserverError);
569
+ },
567
570
  // Each observer isolated on its own; later keys win.
568
571
  carry: (ctx) => {
569
572
  let merged;
@@ -766,8 +769,9 @@ async function runLoop(graph, state, ctx, schema, shouldStop, startCursor, callb
766
769
  ctx["within"] = createWithinFn(obs, () => stepCtxRef.current, onObserverError);
767
770
  const telemetryObserver = obs !== void 0 ? composeObservers([obs], onObserverError) : NOOP_OBSERVER;
768
771
  let cursor = startCursor ?? graph.entryStep;
772
+ let first = true;
769
773
  try {
770
- while (true) {
774
+ for (; ; first = false) {
771
775
  if (shouldStop?.()) {
772
776
  endRun("$stopped");
773
777
  return withCarrier({ state, signal: null, cursor, paused: true });
@@ -790,6 +794,14 @@ async function runLoop(graph, state, ctx, schema, shouldStop, startCursor, callb
790
794
  return withCarrier({ state, signal: null, cursor, paused: true });
791
795
  }
792
796
  }
797
+ if (!first && callbacks?.checkpoint !== void 0 && typeof cursor === "string") {
798
+ const started = Date.now();
799
+ const saved = await callbacks.checkpoint(cursor, state, carrier);
800
+ if (saved !== void 0) {
801
+ const event = { step: cursor, version: saved.version, durationMs: Date.now() - started };
802
+ safeInvoke("onCheckpoint", () => obs?.onCheckpoint?.(runCtx, event), onObserverError);
803
+ }
804
+ }
793
805
  if (isForkCursor(cursor)) {
794
806
  const forkCursor = cursor;
795
807
  const forkNode = graph.steps.find((s) => s.name === forkCursor.fork);
@@ -976,7 +988,7 @@ async function dispatchFork(forkNode, priorBranches, outerState, outerCtx, schem
976
988
  const branchSignals = forkNode.branches.map(
977
989
  (_, i) => outerSignal !== void 0 ? AbortSignal.any([outerSignal, localAborts[i].signal]) : localAborts[i].signal
978
990
  );
979
- const { onBeforeStep: _outerOnBeforeStep, onAfterStep: _outerOnAfterStep, onComplete: _outerOnComplete, ...branchCallbacksRest } = callbacks ?? {};
991
+ const { onBeforeStep: _outerOnBeforeStep, onAfterStep: _outerOnAfterStep, onComplete: _outerOnComplete, checkpoint: _outerCheckpoint, ...branchCallbacksRest } = callbacks ?? {};
980
992
  const branchCallbacksBase = callbacks !== void 0 ? branchCallbacksRest : void 0;
981
993
  const settled = await Promise.allSettled(
982
994
  forkNode.branches.map(async (branch, i) => {
@@ -1109,6 +1121,13 @@ function defaultClaimTtlMs() {
1109
1121
  const ttlMs = Number(process.env["HARNESS_CLAIM_TTL_MS"]);
1110
1122
  return isValidTtlMs(ttlMs) ? ttlMs : 3e4;
1111
1123
  }
1124
+ function isCheckpointMode(value) {
1125
+ return value === "off" || value === "step";
1126
+ }
1127
+ async function saveRun(store, agentId, sessionId, run, lease) {
1128
+ if (lease !== null && lease !== void 0) await store.save(agentId, sessionId, run, { lease });
1129
+ else await store.save(agentId, sessionId, run);
1130
+ }
1112
1131
  function collectPendingInterrupts(cursor, state) {
1113
1132
  if (cursor !== void 0 && isForkCursor(cursor)) {
1114
1133
  const result = [];
@@ -1130,14 +1149,23 @@ function storedSessionToPhase(loaded) {
1130
1149
  if (loaded === null) {
1131
1150
  return { phase: "fresh" };
1132
1151
  }
1152
+ const step = loaded.step === void 0 ? "" : isForkCursor(loaded.step) ? loaded.step.fork : loaded.step;
1133
1153
  if (loaded.phase === "paused") {
1134
- const step = loaded.step === void 0 ? "" : isForkCursor(loaded.step) ? loaded.step.fork : loaded.step;
1135
1154
  const pendingInterrupts = collectPendingInterrupts(loaded.step, loaded.finalState);
1136
1155
  if (loaded.signal !== void 0) {
1137
1156
  return { phase: "paused", step, signal: loaded.signal, pendingInterrupts };
1138
1157
  }
1139
1158
  return { phase: "paused", step, pendingInterrupts };
1140
1159
  }
1160
+ if (loaded.phase === "running") {
1161
+ const instanceId = loaded.metadata?.instanceId;
1162
+ return {
1163
+ phase: "running",
1164
+ step,
1165
+ ...instanceId !== void 0 ? { instanceId } : {},
1166
+ checkpointedAt: loaded.settledAt
1167
+ };
1168
+ }
1141
1169
  if (loaded.signal !== void 0) {
1142
1170
  return { phase: "completed", signal: loaded.signal };
1143
1171
  }
@@ -1238,12 +1266,19 @@ var SessionBusyError = class extends Error {
1238
1266
  var LeaseExpiredError = class extends Error {
1239
1267
  /** The session whose lease expired. */
1240
1268
  sessionId;
1241
- constructor(sessionId) {
1269
+ constructor(sessionId, cause) {
1242
1270
  super(`lease for session "${sessionId}" has expired \u2014 run stopped without saving`);
1243
1271
  this.name = "LeaseExpiredError";
1244
1272
  this.sessionId = sessionId;
1273
+ if (cause !== void 0) this.cause = cause;
1245
1274
  }
1246
1275
  };
1276
+ function isLeaseLost(error) {
1277
+ return error instanceof Error && error.name === "LeaseLostError";
1278
+ }
1279
+ function isConflict(error) {
1280
+ return error instanceof Error && error.name === "ConcurrentModificationError";
1281
+ }
1247
1282
 
1248
1283
  // src/agent/ctx-keep-alive.ts
1249
1284
  function createKeepAliveFn(leaseRef, store, originalTtlMs) {
@@ -1353,7 +1388,7 @@ async function runWithSession(store, agentId, sessionId, runId, graph, initialSt
1353
1388
  }
1354
1389
  return false;
1355
1390
  };
1356
- const { mode = "run", requestId: callerRequestId, ...loopOptions } = options ?? {};
1391
+ const { mode = "run", requestId: callerRequestId, checkpoint: checkpointMode = "off", ...loopOptions } = options ?? {};
1357
1392
  if (store === void 0) {
1358
1393
  const state2 = initializeState(null, initialStateArg, schema);
1359
1394
  const result2 = await runLoop(graph, state2, ctx, schema, composedShouldStop, void 0, {
@@ -1394,7 +1429,7 @@ async function runWithSession(store, agentId, sessionId, runId, graph, initialSt
1394
1429
  state: row.finalState,
1395
1430
  signal: row.signal ?? null,
1396
1431
  cursor: row.step ?? null,
1397
- paused: row.phase === "paused"
1432
+ paused: row.phase !== "completed"
1398
1433
  };
1399
1434
  }
1400
1435
  const segmentRequestId = mode === "run" ? requestId : loaded?.requestId;
@@ -1403,8 +1438,47 @@ async function runWithSession(store, agentId, sessionId, runId, graph, initialSt
1403
1438
  const state = initializeState(base, input, schema);
1404
1439
  const initialStateSnapshot = { ...state };
1405
1440
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
1441
+ const metadata = options?.instanceId !== void 0 ? { instanceId: options.instanceId } : void 0;
1442
+ let nextVersion = (loaded?.version ?? -1) + 1;
1443
+ const baseRow = (carrier) => ({
1444
+ agentId,
1445
+ runId,
1446
+ sessionId,
1447
+ version: nextVersion,
1448
+ startedAt,
1449
+ settledAt: (/* @__PURE__ */ new Date()).toISOString(),
1450
+ initialState: initialStateSnapshot,
1451
+ ...metadata !== void 0 ? { metadata } : {},
1452
+ ...carrier !== void 0 ? { carrier } : {},
1453
+ ...segmentRequestId !== void 0 ? { requestId: segmentRequestId } : {}
1454
+ });
1455
+ const checkpoint = async (next, live, carrier) => {
1456
+ let finalState;
1457
+ try {
1458
+ finalState = structuredClone(live);
1459
+ } catch (error) {
1460
+ options?.onStoreError?.(error, "checkpoint");
1461
+ return void 0;
1462
+ }
1463
+ const row = { ...baseRow(carrier), phase: "running", step: next, finalState };
1464
+ try {
1465
+ await saveRun(store, agentId, sessionId, row, leaseRef.current);
1466
+ } catch (error) {
1467
+ if (isLeaseLost(error) || isConflict(error)) {
1468
+ const expiredError = new LeaseExpiredError(sessionId, error);
1469
+ options?.onStoreError?.(expiredError, "claim");
1470
+ throw expiredError;
1471
+ }
1472
+ options?.onStoreError?.(error, "checkpoint");
1473
+ return void 0;
1474
+ }
1475
+ nextVersion++;
1476
+ await extendLease(store, leaseRef, options?.claimTtlMs ?? defaultClaimTtlMs());
1477
+ return { version: row.version };
1478
+ };
1406
1479
  const result = await runLoop(graph, state, ctx, schema, composedShouldStop, loaded?.step, {
1407
1480
  ...loopOptions,
1481
+ ...checkpointMode === "step" ? { checkpoint } : {},
1408
1482
  runId,
1409
1483
  ...options?.parentRunId !== void 0 ? { parentRunId: options.parentRunId } : {},
1410
1484
  ...segmentRequestId !== void 0 ? { requestId: segmentRequestId } : {},
@@ -1415,53 +1489,39 @@ async function runWithSession(store, agentId, sessionId, runId, graph, initialSt
1415
1489
  options?.onStoreError?.(expiredError, "claim");
1416
1490
  throw expiredError;
1417
1491
  }
1418
- const settledAt = (/* @__PURE__ */ new Date()).toISOString();
1419
- const metadata = options?.instanceId !== void 0 ? { instanceId: options.instanceId } : void 0;
1492
+ const saved = {
1493
+ ...baseRow(result.carrier),
1494
+ phase: result.paused ? "paused" : "completed",
1495
+ finalState: result.state,
1496
+ ...result.paused ? { step: result.cursor } : {},
1497
+ ...result.signal !== null ? { signal: result.signal } : {}
1498
+ };
1420
1499
  try {
1421
- const version = (loaded?.version ?? -1) + 1;
1422
- if (result.paused) {
1423
- const saved = {
1424
- agentId,
1425
- runId,
1426
- sessionId,
1427
- version,
1428
- startedAt,
1429
- settledAt,
1430
- phase: "paused",
1431
- initialState: initialStateSnapshot,
1432
- finalState: result.state,
1433
- step: result.cursor,
1434
- ...result.signal !== null ? { signal: result.signal } : {},
1435
- ...metadata !== void 0 ? { metadata } : {},
1436
- ...result.carrier !== void 0 ? { carrier: result.carrier } : {},
1437
- ...segmentRequestId !== void 0 ? { requestId: segmentRequestId } : {}
1438
- };
1439
- await store.save(agentId, sessionId, saved);
1440
- } else {
1441
- const saved = {
1442
- agentId,
1443
- runId,
1444
- sessionId,
1445
- version,
1446
- startedAt,
1447
- settledAt,
1448
- phase: "completed",
1449
- initialState: initialStateSnapshot,
1450
- finalState: result.state,
1451
- ...result.signal !== null ? { signal: result.signal } : {},
1452
- ...metadata !== void 0 ? { metadata } : {},
1453
- ...result.carrier !== void 0 ? { carrier: result.carrier } : {},
1454
- ...segmentRequestId !== void 0 ? { requestId: segmentRequestId } : {}
1455
- };
1456
- await store.save(agentId, sessionId, saved);
1457
- }
1500
+ await saveRun(store, agentId, sessionId, saved, leaseRef.current);
1458
1501
  } catch (error) {
1502
+ if (isLeaseLost(error)) {
1503
+ const expiredError = new LeaseExpiredError(sessionId, error);
1504
+ options?.onStoreError?.(expiredError, "claim");
1505
+ throw expiredError;
1506
+ }
1459
1507
  options?.onStoreError?.(error, "persist");
1460
1508
  }
1461
1509
  return result;
1462
1510
  }
1511
+ async function extendLease(store, leaseRef, ttlMs) {
1512
+ const held = leaseRef.current;
1513
+ if (held === null || typeof store.extendClaim !== "function") return;
1514
+ try {
1515
+ leaseRef.current = await store.extendClaim(held, { ttlMs });
1516
+ } catch {
1517
+ }
1518
+ }
1463
1519
 
1464
1520
  // src/agent/claim-scope.ts
1521
+ function heldLease(options) {
1522
+ const lease = options.leaseRef?.current ?? null;
1523
+ return lease !== null ? { lease } : {};
1524
+ }
1465
1525
  function failOutcome(failState, error) {
1466
1526
  const state = failState();
1467
1527
  state["$error"] = error;
@@ -1555,7 +1615,7 @@ function injectInterruptResponseIntoState(cursor, state, interruptId, response)
1555
1615
  found: true
1556
1616
  };
1557
1617
  }
1558
- async function injectInterruptResponse(store, agentId, sessionId, interruptId, response) {
1618
+ async function injectInterruptResponse(store, agentId, sessionId, interruptId, response, lease) {
1559
1619
  const loaded = await store.load(agentId, sessionId);
1560
1620
  if (loaded === null) throw new NoInterruptError();
1561
1621
  if (loaded.phase !== "paused") throw new NoInterruptError();
@@ -1578,14 +1638,19 @@ async function injectInterruptResponse(store, agentId, sessionId, interruptId, r
1578
1638
  ...loaded.branchedFrom !== void 0 ? { branchedFrom: loaded.branchedFrom } : {},
1579
1639
  ...loaded.requestId !== void 0 ? { requestId: loaded.requestId } : {}
1580
1640
  };
1581
- await store.save(agentId, sessionId, updated);
1641
+ await saveRun(store, agentId, sessionId, updated, lease);
1582
1642
  }
1583
1643
  async function injectThenRun(args) {
1584
- const { store, agentId, sessionId, interruptId, response, onStoreError, failState, run } = args;
1644
+ const { store, agentId, sessionId, interruptId, response, lease, onStoreError, failState, run } = args;
1585
1645
  try {
1586
- await injectInterruptResponse(store, agentId, sessionId, interruptId, response);
1646
+ await injectInterruptResponse(store, agentId, sessionId, interruptId, response, lease);
1587
1647
  } catch (injectError) {
1588
1648
  if (injectError instanceof NoInterruptError) throw injectError;
1649
+ if (isLeaseLost(injectError)) {
1650
+ const expiredError = new LeaseExpiredError(sessionId, injectError);
1651
+ onStoreError?.(expiredError, "claim");
1652
+ return failOutcome(failState, expiredError);
1653
+ }
1589
1654
  const storeError = new StoreLoadError(injectError);
1590
1655
  onStoreError?.(storeError, "claim");
1591
1656
  return failOutcome(failState, storeError);
@@ -2329,13 +2394,14 @@ var InvalidClaimOptionsError = class extends Error {
2329
2394
  this.ttlMs = ttlMs;
2330
2395
  }
2331
2396
  };
2397
+ var AGENT_OPTION_EXPECTED = { observer: "an object", onObserverError: "a function", checkpoint: "'off' or 'step'" };
2332
2398
  var InvalidAgentOptionError = class extends Error {
2333
2399
  /** The option that was rejected. */
2334
2400
  key;
2335
2401
  /** The rejected value, as given. */
2336
2402
  value;
2337
2403
  constructor(key, value) {
2338
- super(`${key} must be ${key === "observer" ? "an object" : "a function"} \u2014 got ${describeValue(value)} in createAgent options`);
2404
+ super(`${key} must be ${AGENT_OPTION_EXPECTED[key]} \u2014 got ${describeValue(value)} in createAgent options`);
2339
2405
  this.name = "InvalidAgentOptionError";
2340
2406
  this.key = key;
2341
2407
  this.value = value;
@@ -2347,7 +2413,7 @@ var InvalidRunResourceError = class extends Error {
2347
2413
  /** The rejected value, as given. */
2348
2414
  value;
2349
2415
  constructor(key, value) {
2350
- super(`${key} must be a non-empty string \u2014 got ${describeValue(value)} in agent.run() resources`);
2416
+ super(key === "checkpoint" ? `checkpoint must be 'off' or 'step' \u2014 got ${describeValue(value)} in the call's resources` : `${key} must be a non-empty string \u2014 got ${describeValue(value)} in agent.run() resources`);
2351
2417
  this.name = "InvalidRunResourceError";
2352
2418
  this.key = key;
2353
2419
  this.value = value;
@@ -2359,6 +2425,12 @@ function readIdResource(resources, key) {
2359
2425
  if (typeof value !== "string" || value.length === 0) throw new InvalidRunResourceError(key, value);
2360
2426
  return value;
2361
2427
  }
2428
+ function readCheckpointResource(resources) {
2429
+ const value = resources["checkpoint"];
2430
+ if (value === void 0) return void 0;
2431
+ if (!isCheckpointMode(value)) throw new InvalidRunResourceError("checkpoint", value);
2432
+ return value;
2433
+ }
2362
2434
  var _agentInternals = /* @__PURE__ */ Symbol("_agentInternals");
2363
2435
  function createAgent(id, h, slots, agentOptions) {
2364
2436
  const internals = getInternals(h);
@@ -2413,6 +2485,10 @@ function createAgent(id, h, slots, agentOptions) {
2413
2485
  ...onObserverError !== void 0 ? { onObserverError } : {}
2414
2486
  };
2415
2487
  };
2488
+ const agentCheckpoint = agentOptions?.checkpoint;
2489
+ if (agentCheckpoint !== void 0 && !isCheckpointMode(agentCheckpoint)) {
2490
+ throw new InvalidAgentOptionError("checkpoint", agentCheckpoint);
2491
+ }
2416
2492
  const resolvedProviders = /* @__PURE__ */ new Map();
2417
2493
  const storeEntries = [];
2418
2494
  const runtimeKeys = /* @__PURE__ */ new Set();
@@ -2462,13 +2538,13 @@ function createAgent(id, h, slots, agentOptions) {
2462
2538
  runtimeKeys,
2463
2539
  requiredKeys
2464
2540
  };
2465
- const reservedRunKeys = /* @__PURE__ */ new Set(["sessionId", "requestId", "signal", "events", "listeners", "observer", "onObserverError", "claimOptions", "parentRunId"]);
2541
+ const reservedRunKeys = /* @__PURE__ */ new Set(["sessionId", "requestId", "signal", "events", "listeners", "observer", "onObserverError", "claimOptions", "parentRunId", "checkpoint"]);
2466
2542
  const inFlightSessions = /* @__PURE__ */ new Set();
2467
2543
  const debugSessions = /* @__PURE__ */ new Set();
2468
2544
  const interruptPendingSessions = /* @__PURE__ */ new Set();
2469
2545
  const resumeFailState = () => initializeState(null, {}, agentInternals.stateSchema);
2470
2546
  const makeAgentResumeHandle = (resp, sId, iId, resumeResources) => {
2471
- const { onStoreError, observer, onObserverError, telemetrySlots, parentRunId, claimOptions } = resumeResources;
2547
+ const { onStoreError, observer, onObserverError, telemetrySlots, parentRunId, claimOptions, checkpoint } = resumeResources;
2472
2548
  let _stopped = false;
2473
2549
  const abortController = new AbortController();
2474
2550
  const flag = {
@@ -2501,6 +2577,7 @@ function createAgent(id, h, slots, agentOptions) {
2501
2577
  sessionId: sId,
2502
2578
  interruptId: iId,
2503
2579
  response: resp,
2580
+ ...heldLease(lease),
2504
2581
  ...onStoreError !== void 0 ? { onStoreError } : {},
2505
2582
  failState: resumeFailState,
2506
2583
  run: async () => {
@@ -2523,6 +2600,7 @@ function createAgent(id, h, slots, agentOptions) {
2523
2600
  agentCtx,
2524
2601
  {
2525
2602
  mode: "resume",
2603
+ checkpoint,
2526
2604
  shouldStop: () => flag.stopped,
2527
2605
  onBeforeStep: (n) => {
2528
2606
  ref.current = n;
@@ -2574,6 +2652,7 @@ function createAgent(id, h, slots, agentOptions) {
2574
2652
  }
2575
2653
  const sessionId = readIdResource(resources, "sessionId") ?? randomUUID();
2576
2654
  const requestId = readIdResource(resources, "requestId");
2655
+ const checkpoint = readCheckpointResource(resources) ?? agentCheckpoint ?? "off";
2577
2656
  const runId = randomUUID();
2578
2657
  if (debugSessions.has(sessionId)) throw new SessionInDebugError(sessionId);
2579
2658
  if (inFlightSessions.has(sessionId)) throw new SessionInFlightError(sessionId);
@@ -2628,6 +2707,7 @@ function createAgent(id, h, slots, agentOptions) {
2628
2707
  const claimOptions = parseClaimOptions(resources["claimOptions"], fallbackClaimOptions());
2629
2708
  const options = {
2630
2709
  mode: "run",
2710
+ checkpoint,
2631
2711
  ...requestId !== void 0 ? { requestId } : {},
2632
2712
  shouldStop: () => stopFlag.stopped,
2633
2713
  onBeforeStep: (name, state) => {
@@ -2723,6 +2803,7 @@ function createAgent(id, h, slots, agentOptions) {
2723
2803
  sessionId,
2724
2804
  interruptId,
2725
2805
  response,
2806
+ ...heldLease(lease),
2726
2807
  ...onStoreError !== void 0 ? { onStoreError } : {},
2727
2808
  failState: resumeFailState,
2728
2809
  run: async () => {
@@ -2738,6 +2819,7 @@ function createAgent(id, h, slots, agentOptions) {
2738
2819
  resumeCtx,
2739
2820
  {
2740
2821
  mode: "resume",
2822
+ checkpoint,
2741
2823
  shouldStop: () => resumeStopFlag.stopped,
2742
2824
  onBeforeStep: (n, s) => {
2743
2825
  resumeStepRef.current = n;
@@ -2813,6 +2895,7 @@ function createAgent(id, h, slots, agentOptions) {
2813
2895
  resume: (response, sessionId, interruptId, resources) => {
2814
2896
  if (debugSessions.has(sessionId)) throw new SessionInDebugError(sessionId);
2815
2897
  if (inFlightSessions.has(sessionId)) throw new SessionInFlightError(sessionId);
2898
+ const checkpoint = readCheckpointResource(resources ?? {}) ?? agentCheckpoint ?? "off";
2816
2899
  interruptPendingSessions.delete(sessionId);
2817
2900
  inFlightSessions.add(sessionId);
2818
2901
  const events = extractRunEvents(resources ?? {});
@@ -2822,6 +2905,7 @@ function createAgent(id, h, slots, agentOptions) {
2822
2905
  const claimOptions = parseClaimOptions(resources?.["claimOptions"], fallbackClaimOptions());
2823
2906
  return makeAgentResumeHandle(response, sessionId, interruptId, {
2824
2907
  claimOptions,
2908
+ checkpoint,
2825
2909
  ...events.onStoreError !== void 0 ? { onStoreError: events.onStoreError } : {},
2826
2910
  ...observer !== void 0 ? { observer } : {},
2827
2911
  ...onObserverError !== void 0 ? { onObserverError } : {},