@tangle-network/agent-runtime 0.12.1 → 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/agent.d.ts +1 -1
- package/dist/index.d.ts +677 -3
- package/dist/index.js +1122 -2
- package/dist/index.js.map +1 -1
- package/dist/{types-afLuHk1G.d.ts → types-jr_EFhrD.d.ts} +1 -1
- package/package.json +3 -1
package/dist/index.js
CHANGED
|
@@ -433,6 +433,1111 @@ function sandboxAsChatTurnTarget(instance, opts) {
|
|
|
433
433
|
};
|
|
434
434
|
}
|
|
435
435
|
|
|
436
|
+
// src/durable/identity.ts
|
|
437
|
+
import { createHash } from "crypto";
|
|
438
|
+
function canonicalHash(value) {
|
|
439
|
+
const json = canonicalJson(value);
|
|
440
|
+
return createHash("sha256").update(json).digest("hex");
|
|
441
|
+
}
|
|
442
|
+
function canonicalJson(value) {
|
|
443
|
+
return JSON.stringify(canonicalize(value));
|
|
444
|
+
}
|
|
445
|
+
function canonicalize(value) {
|
|
446
|
+
if (value === null) return null;
|
|
447
|
+
if (Array.isArray(value)) return value.map(canonicalize);
|
|
448
|
+
const t = typeof value;
|
|
449
|
+
if (t === "string" || t === "boolean") return value;
|
|
450
|
+
if (t === "number") {
|
|
451
|
+
if (!Number.isFinite(value)) {
|
|
452
|
+
throw new TypeError(`canonicalJson: non-finite number ${String(value)} not serializable`);
|
|
453
|
+
}
|
|
454
|
+
return value;
|
|
455
|
+
}
|
|
456
|
+
if (t === "undefined" || t === "function" || t === "symbol") {
|
|
457
|
+
throw new TypeError(`canonicalJson: ${t} is not JSON-serializable`);
|
|
458
|
+
}
|
|
459
|
+
if (t === "bigint") {
|
|
460
|
+
return { __bigint: String(value) };
|
|
461
|
+
}
|
|
462
|
+
if (t === "object") {
|
|
463
|
+
const obj = value;
|
|
464
|
+
const proto = Object.getPrototypeOf(obj);
|
|
465
|
+
if (proto !== null && proto !== Object.prototype) {
|
|
466
|
+
const ctor = obj.constructor?.name ?? "unknown";
|
|
467
|
+
throw new TypeError(
|
|
468
|
+
`canonicalJson: class instance (${ctor}) is not JSON-serializable. Project to plain { ... } at the boundary.`
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
const keys = Object.keys(obj).sort();
|
|
472
|
+
const out = {};
|
|
473
|
+
for (const k of keys) out[k] = canonicalize(obj[k]);
|
|
474
|
+
return out;
|
|
475
|
+
}
|
|
476
|
+
throw new TypeError(`canonicalJson: unsupported type ${t}`);
|
|
477
|
+
}
|
|
478
|
+
function manifestHash(manifest) {
|
|
479
|
+
return canonicalHash({
|
|
480
|
+
projectId: manifest.projectId,
|
|
481
|
+
scenarioId: manifest.scenarioId ?? null,
|
|
482
|
+
taskId: manifest.task.id,
|
|
483
|
+
taskIntent: manifest.task.intent,
|
|
484
|
+
taskDomain: manifest.task.domain,
|
|
485
|
+
input: manifest.input
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
function stepId(runId, stepIndex, intent) {
|
|
489
|
+
return canonicalHash({ runId, stepIndex, intent });
|
|
490
|
+
}
|
|
491
|
+
var counter = 0;
|
|
492
|
+
function deriveWorkerId() {
|
|
493
|
+
const host = process.env.HOSTNAME ?? "host";
|
|
494
|
+
const pid = process.pid ?? 0;
|
|
495
|
+
const rand = Math.random().toString(36).slice(2, 10);
|
|
496
|
+
counter += 1;
|
|
497
|
+
return `${host}:${pid}:${rand}:${counter}`;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// src/durable/types.ts
|
|
501
|
+
var DurableRunError = class extends Error {
|
|
502
|
+
constructor(message, code) {
|
|
503
|
+
super(message);
|
|
504
|
+
this.code = code;
|
|
505
|
+
this.name = this.constructor.name;
|
|
506
|
+
}
|
|
507
|
+
code;
|
|
508
|
+
};
|
|
509
|
+
var DurableRunLeaseHeldError = class extends DurableRunError {
|
|
510
|
+
constructor(message) {
|
|
511
|
+
super(message, "lease_held");
|
|
512
|
+
}
|
|
513
|
+
};
|
|
514
|
+
var DurableRunInputMismatchError = class extends DurableRunError {
|
|
515
|
+
constructor(message) {
|
|
516
|
+
super(message, "manifest_mismatch");
|
|
517
|
+
}
|
|
518
|
+
};
|
|
519
|
+
var DurableRunDivergenceError = class extends DurableRunError {
|
|
520
|
+
constructor(message) {
|
|
521
|
+
super(message, "step_divergence");
|
|
522
|
+
}
|
|
523
|
+
};
|
|
524
|
+
var DurableAwaitEventTimeoutError = class extends DurableRunError {
|
|
525
|
+
constructor(message) {
|
|
526
|
+
super(message, "await_event_timeout");
|
|
527
|
+
}
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
// src/durable/d1-store.ts
|
|
531
|
+
var DEFAULT_LEASE_MS = 3e4;
|
|
532
|
+
var D1DurableRunStore = class {
|
|
533
|
+
constructor(db) {
|
|
534
|
+
this.db = db;
|
|
535
|
+
}
|
|
536
|
+
db;
|
|
537
|
+
/** Override for tests — defaults to Date.now(). */
|
|
538
|
+
now = () => Date.now();
|
|
539
|
+
async startOrResume(input) {
|
|
540
|
+
const leaseMs = input.leaseMs ?? DEFAULT_LEASE_MS;
|
|
541
|
+
const hash = manifestHash(input.manifest);
|
|
542
|
+
const nowMs = this.now();
|
|
543
|
+
const nowIso2 = new Date(nowMs).toISOString();
|
|
544
|
+
const leaseExpiresAt = new Date(nowMs + leaseMs).toISOString();
|
|
545
|
+
const existing = await this.db.prepare("SELECT * FROM durable_runs WHERE run_id = ?").bind(input.runId).first();
|
|
546
|
+
if (!existing) {
|
|
547
|
+
await this.db.prepare(
|
|
548
|
+
`INSERT INTO durable_runs
|
|
549
|
+
(run_id, manifest_hash, project_id, scenario_id, status,
|
|
550
|
+
created_at, updated_at, lease_holder_id, lease_expires_at, step_count)
|
|
551
|
+
VALUES (?, ?, ?, ?, 'running', ?, ?, ?, ?, 0)`
|
|
552
|
+
).bind(
|
|
553
|
+
input.runId,
|
|
554
|
+
hash,
|
|
555
|
+
input.manifest.projectId,
|
|
556
|
+
input.manifest.scenarioId ?? null,
|
|
557
|
+
nowIso2,
|
|
558
|
+
nowIso2,
|
|
559
|
+
input.workerId,
|
|
560
|
+
leaseExpiresAt
|
|
561
|
+
).run();
|
|
562
|
+
const record2 = {
|
|
563
|
+
runId: input.runId,
|
|
564
|
+
manifestHash: hash,
|
|
565
|
+
projectId: input.manifest.projectId,
|
|
566
|
+
scenarioId: input.manifest.scenarioId,
|
|
567
|
+
status: "running",
|
|
568
|
+
createdAt: nowIso2,
|
|
569
|
+
updatedAt: nowIso2,
|
|
570
|
+
leaseHolderId: input.workerId,
|
|
571
|
+
leaseExpiresAt,
|
|
572
|
+
stepCount: 0
|
|
573
|
+
};
|
|
574
|
+
return { run: record2, completedSteps: [], leaseExpiresAt };
|
|
575
|
+
}
|
|
576
|
+
if (existing.manifest_hash !== hash) {
|
|
577
|
+
throw new DurableRunInputMismatchError(
|
|
578
|
+
`runId ${input.runId} exists with a different manifest hash; refuse to corrupt prior steps`
|
|
579
|
+
);
|
|
580
|
+
}
|
|
581
|
+
const claim = await this.db.prepare(
|
|
582
|
+
`UPDATE durable_runs
|
|
583
|
+
SET lease_holder_id = ?,
|
|
584
|
+
lease_expires_at = ?,
|
|
585
|
+
updated_at = ?,
|
|
586
|
+
status = CASE WHEN status IN ('completed','failed') THEN status ELSE 'running' END
|
|
587
|
+
WHERE run_id = ?
|
|
588
|
+
AND (
|
|
589
|
+
lease_holder_id = ? OR
|
|
590
|
+
lease_holder_id IS NULL OR
|
|
591
|
+
lease_expires_at IS NULL OR
|
|
592
|
+
lease_expires_at < ?
|
|
593
|
+
)`
|
|
594
|
+
).bind(input.workerId, leaseExpiresAt, nowIso2, input.runId, input.workerId, nowIso2).run();
|
|
595
|
+
const changes = claim.meta?.changes ?? 0;
|
|
596
|
+
if (changes === 0) {
|
|
597
|
+
throw new DurableRunLeaseHeldError(
|
|
598
|
+
`runId ${input.runId} leased by ${existing.lease_holder_id} until ${existing.lease_expires_at}`
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
const completedSteps = await this.readSteps(input.runId, "completed");
|
|
602
|
+
const record = rowToRunRecord({
|
|
603
|
+
...existing,
|
|
604
|
+
lease_holder_id: input.workerId,
|
|
605
|
+
lease_expires_at: leaseExpiresAt,
|
|
606
|
+
updated_at: nowIso2,
|
|
607
|
+
status: existing.status === "completed" || existing.status === "failed" ? existing.status : "running"
|
|
608
|
+
});
|
|
609
|
+
return { run: record, completedSteps, leaseExpiresAt };
|
|
610
|
+
}
|
|
611
|
+
async renewLease(input) {
|
|
612
|
+
const nowMs = this.now();
|
|
613
|
+
const nowIso2 = new Date(nowMs).toISOString();
|
|
614
|
+
const leaseExpiresAt = new Date(nowMs + (input.leaseMs ?? DEFAULT_LEASE_MS)).toISOString();
|
|
615
|
+
const res = await this.db.prepare(
|
|
616
|
+
`UPDATE durable_runs
|
|
617
|
+
SET lease_expires_at = ?, updated_at = ?
|
|
618
|
+
WHERE run_id = ?
|
|
619
|
+
AND (lease_holder_id = ? OR lease_expires_at IS NULL OR lease_expires_at < ?)`
|
|
620
|
+
).bind(leaseExpiresAt, nowIso2, input.runId, input.workerId, nowIso2).run();
|
|
621
|
+
const ok = (res.meta?.changes ?? 0) > 0;
|
|
622
|
+
return ok ? { ok: true, leaseExpiresAt } : { ok: false };
|
|
623
|
+
}
|
|
624
|
+
async loadStep(runId, stepIndex) {
|
|
625
|
+
const row = await this.db.prepare("SELECT * FROM durable_steps WHERE run_id = ? AND step_index = ?").bind(runId, stepIndex).first();
|
|
626
|
+
return row ? rowToStepRecord(row) : void 0;
|
|
627
|
+
}
|
|
628
|
+
async beginStep(input) {
|
|
629
|
+
const nowIso2 = new Date(this.now()).toISOString();
|
|
630
|
+
const prior = await this.loadStep(input.runId, input.stepIndex);
|
|
631
|
+
if (prior) {
|
|
632
|
+
if (prior.intent !== input.intent) {
|
|
633
|
+
throw new DurableRunDivergenceError(
|
|
634
|
+
`step ${input.stepIndex}: intent changed ('${prior.intent}' -> '${input.intent}')`
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
await this.db.prepare(
|
|
638
|
+
`UPDATE durable_steps
|
|
639
|
+
SET status='running', attempts = attempts + 1, started_at = ?, error_json = NULL
|
|
640
|
+
WHERE run_id = ? AND step_index = ?`
|
|
641
|
+
).bind(nowIso2, input.runId, input.stepIndex).run();
|
|
642
|
+
await this.bumpUpdated(input.runId, nowIso2);
|
|
643
|
+
return {
|
|
644
|
+
...prior,
|
|
645
|
+
attempts: prior.attempts + 1,
|
|
646
|
+
status: "running",
|
|
647
|
+
startedAt: nowIso2,
|
|
648
|
+
error: void 0
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
await this.db.prepare(
|
|
652
|
+
`INSERT INTO durable_steps
|
|
653
|
+
(run_id, step_index, intent, kind, input_hash, status, attempts, started_at)
|
|
654
|
+
VALUES (?, ?, ?, ?, ?, 'running', 1, ?)`
|
|
655
|
+
).bind(input.runId, input.stepIndex, input.intent, input.kind, input.inputHash, nowIso2).run();
|
|
656
|
+
await this.db.prepare(
|
|
657
|
+
`UPDATE durable_runs
|
|
658
|
+
SET step_count = MAX(step_count, ?), updated_at = ?
|
|
659
|
+
WHERE run_id = ?`
|
|
660
|
+
).bind(input.stepIndex + 1, nowIso2, input.runId).run();
|
|
661
|
+
return {
|
|
662
|
+
runId: input.runId,
|
|
663
|
+
stepIndex: input.stepIndex,
|
|
664
|
+
intent: input.intent,
|
|
665
|
+
kind: input.kind,
|
|
666
|
+
inputHash: input.inputHash,
|
|
667
|
+
status: "running",
|
|
668
|
+
attempts: 1,
|
|
669
|
+
startedAt: nowIso2
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
async completeStep(input) {
|
|
673
|
+
const nowIso2 = new Date(this.now()).toISOString();
|
|
674
|
+
await this.db.prepare(
|
|
675
|
+
`UPDATE durable_steps
|
|
676
|
+
SET status='completed', result_json = ?, completed_at = ?, error_json = NULL
|
|
677
|
+
WHERE run_id = ? AND step_index = ?`
|
|
678
|
+
).bind(JSON.stringify(input.result ?? null), nowIso2, input.runId, input.stepIndex).run();
|
|
679
|
+
await this.bumpUpdated(input.runId, nowIso2);
|
|
680
|
+
const row = await this.loadStep(input.runId, input.stepIndex);
|
|
681
|
+
if (!row) {
|
|
682
|
+
throw new Error(`durable-runs: completeStep cannot find step ${input.stepIndex}`);
|
|
683
|
+
}
|
|
684
|
+
return row;
|
|
685
|
+
}
|
|
686
|
+
async failStep(input) {
|
|
687
|
+
const nowIso2 = new Date(this.now()).toISOString();
|
|
688
|
+
await this.db.prepare(
|
|
689
|
+
`UPDATE durable_steps
|
|
690
|
+
SET status='failed', error_json = ?, completed_at = ?
|
|
691
|
+
WHERE run_id = ? AND step_index = ?`
|
|
692
|
+
).bind(JSON.stringify(input.error), nowIso2, input.runId, input.stepIndex).run();
|
|
693
|
+
await this.bumpUpdated(input.runId, nowIso2);
|
|
694
|
+
const row = await this.loadStep(input.runId, input.stepIndex);
|
|
695
|
+
if (!row) {
|
|
696
|
+
throw new Error(`durable-runs: failStep cannot find step ${input.stepIndex}`);
|
|
697
|
+
}
|
|
698
|
+
return row;
|
|
699
|
+
}
|
|
700
|
+
async endRun(input) {
|
|
701
|
+
const nowIso2 = new Date(this.now()).toISOString();
|
|
702
|
+
await this.db.prepare(
|
|
703
|
+
`UPDATE durable_runs
|
|
704
|
+
SET status = ?, completed_at = ?, updated_at = ?,
|
|
705
|
+
outcome_json = ?,
|
|
706
|
+
lease_holder_id = CASE WHEN lease_holder_id = ? THEN NULL ELSE lease_holder_id END,
|
|
707
|
+
lease_expires_at = CASE WHEN lease_holder_id = ? THEN NULL ELSE lease_expires_at END
|
|
708
|
+
WHERE run_id = ?`
|
|
709
|
+
).bind(
|
|
710
|
+
input.status,
|
|
711
|
+
nowIso2,
|
|
712
|
+
nowIso2,
|
|
713
|
+
input.outcome ? JSON.stringify(input.outcome) : null,
|
|
714
|
+
input.workerId,
|
|
715
|
+
input.workerId,
|
|
716
|
+
input.runId
|
|
717
|
+
).run();
|
|
718
|
+
const row = await this.db.prepare("SELECT * FROM durable_runs WHERE run_id = ?").bind(input.runId).first();
|
|
719
|
+
if (!row) throw new Error(`durable-runs: endRun cannot find run ${input.runId}`);
|
|
720
|
+
return rowToRunRecord(row);
|
|
721
|
+
}
|
|
722
|
+
async emitEvent(input) {
|
|
723
|
+
const nowIso2 = new Date(this.now()).toISOString();
|
|
724
|
+
const res = await this.db.prepare(
|
|
725
|
+
`INSERT OR IGNORE INTO durable_events (run_id, key, payload_json, emitted_at)
|
|
726
|
+
VALUES (?, ?, ?, ?)`
|
|
727
|
+
).bind(input.runId, input.key, JSON.stringify(input.payload ?? null), nowIso2).run();
|
|
728
|
+
const accepted = (res.meta?.changes ?? 0) > 0;
|
|
729
|
+
const row = await this.db.prepare("SELECT * FROM durable_events WHERE run_id = ? AND key = ?").bind(input.runId, input.key).first();
|
|
730
|
+
if (!row) throw new Error("durable-runs: emitEvent failed to persist or read back");
|
|
731
|
+
return {
|
|
732
|
+
accepted,
|
|
733
|
+
record: rowToEventRecord(row)
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
async loadEvent(runId, key) {
|
|
737
|
+
const row = await this.db.prepare("SELECT * FROM durable_events WHERE run_id = ? AND key = ?").bind(runId, key).first();
|
|
738
|
+
return row ? rowToEventRecord(row) : void 0;
|
|
739
|
+
}
|
|
740
|
+
async close() {
|
|
741
|
+
}
|
|
742
|
+
/** Inspect the currently-applied schema version. */
|
|
743
|
+
async getSchemaVersion() {
|
|
744
|
+
const row = await this.db.prepare("SELECT MAX(version) AS version FROM durable_schema_info").first();
|
|
745
|
+
return row?.version ?? void 0;
|
|
746
|
+
}
|
|
747
|
+
// ── internals ──────────────────────────────────────────────────────
|
|
748
|
+
async readSteps(runId, status) {
|
|
749
|
+
const { results } = await this.db.prepare("SELECT * FROM durable_steps WHERE run_id = ? AND status = ? ORDER BY step_index").bind(runId, status).all();
|
|
750
|
+
return results.map(rowToStepRecord);
|
|
751
|
+
}
|
|
752
|
+
async bumpUpdated(runId, nowIso2) {
|
|
753
|
+
await this.db.prepare("UPDATE durable_runs SET updated_at = ? WHERE run_id = ?").bind(nowIso2, runId).run();
|
|
754
|
+
}
|
|
755
|
+
};
|
|
756
|
+
function rowToRunRecord(row) {
|
|
757
|
+
return {
|
|
758
|
+
runId: row.run_id,
|
|
759
|
+
manifestHash: row.manifest_hash,
|
|
760
|
+
projectId: row.project_id,
|
|
761
|
+
scenarioId: row.scenario_id ?? void 0,
|
|
762
|
+
status: row.status,
|
|
763
|
+
createdAt: row.created_at,
|
|
764
|
+
updatedAt: row.updated_at,
|
|
765
|
+
completedAt: row.completed_at ?? void 0,
|
|
766
|
+
leaseHolderId: row.lease_holder_id ?? void 0,
|
|
767
|
+
leaseExpiresAt: row.lease_expires_at ?? void 0,
|
|
768
|
+
outcome: row.outcome_json ? JSON.parse(row.outcome_json) : void 0,
|
|
769
|
+
stepCount: row.step_count
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
function rowToStepRecord(row) {
|
|
773
|
+
return {
|
|
774
|
+
runId: row.run_id,
|
|
775
|
+
stepIndex: row.step_index,
|
|
776
|
+
intent: row.intent,
|
|
777
|
+
kind: row.kind,
|
|
778
|
+
inputHash: row.input_hash,
|
|
779
|
+
status: row.status,
|
|
780
|
+
attempts: row.attempts,
|
|
781
|
+
result: row.result_json ? JSON.parse(row.result_json) : void 0,
|
|
782
|
+
error: row.error_json ? JSON.parse(row.error_json) : void 0,
|
|
783
|
+
startedAt: row.started_at ?? void 0,
|
|
784
|
+
completedAt: row.completed_at ?? void 0
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
function rowToEventRecord(row) {
|
|
788
|
+
return {
|
|
789
|
+
runId: row.run_id,
|
|
790
|
+
key: row.key,
|
|
791
|
+
payload: row.payload_json ? JSON.parse(row.payload_json) : null,
|
|
792
|
+
emittedAt: row.emitted_at
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// src/durable/file-system-store.ts
|
|
797
|
+
import { existsSync, mkdirSync } from "fs";
|
|
798
|
+
import { appendFile, readdir, readFile, rename, writeFile } from "fs/promises";
|
|
799
|
+
import { join } from "path";
|
|
800
|
+
var DEFAULT_LEASE_MS2 = 3e4;
|
|
801
|
+
var FileSystemDurableRunStore = class {
|
|
802
|
+
constructor(root) {
|
|
803
|
+
this.root = root;
|
|
804
|
+
mkdirSync(root, { recursive: true });
|
|
805
|
+
}
|
|
806
|
+
root;
|
|
807
|
+
/** Override for tests — defaults to Date.now(). */
|
|
808
|
+
now = () => Date.now();
|
|
809
|
+
async startOrResume(input) {
|
|
810
|
+
const leaseMs = input.leaseMs ?? DEFAULT_LEASE_MS2;
|
|
811
|
+
const hash = manifestHash(input.manifest);
|
|
812
|
+
const nowMs = this.now();
|
|
813
|
+
const nowIso2 = new Date(nowMs).toISOString();
|
|
814
|
+
const leaseExpiresAt = new Date(nowMs + leaseMs).toISOString();
|
|
815
|
+
const dir = this.runDir(input.runId);
|
|
816
|
+
if (!existsSync(dir)) {
|
|
817
|
+
mkdirSync(dir, { recursive: true });
|
|
818
|
+
const record2 = {
|
|
819
|
+
runId: input.runId,
|
|
820
|
+
manifestHash: hash,
|
|
821
|
+
projectId: input.manifest.projectId,
|
|
822
|
+
scenarioId: input.manifest.scenarioId,
|
|
823
|
+
status: "running",
|
|
824
|
+
createdAt: nowIso2,
|
|
825
|
+
updatedAt: nowIso2,
|
|
826
|
+
leaseHolderId: input.workerId,
|
|
827
|
+
leaseExpiresAt,
|
|
828
|
+
stepCount: 0
|
|
829
|
+
};
|
|
830
|
+
await this.writeRun(record2);
|
|
831
|
+
await this.writeLease(input.runId, { workerId: input.workerId, leaseExpiresAt });
|
|
832
|
+
await appendFile(join(dir, "steps.jsonl"), "", "utf8");
|
|
833
|
+
await appendFile(join(dir, "events.jsonl"), "", "utf8");
|
|
834
|
+
return { run: record2, completedSteps: [], leaseExpiresAt };
|
|
835
|
+
}
|
|
836
|
+
const record = await this.readRun(input.runId);
|
|
837
|
+
if (record.manifestHash !== hash) {
|
|
838
|
+
throw new DurableRunInputMismatchError(
|
|
839
|
+
`runId ${input.runId} exists with a different manifest hash; refuse to corrupt prior steps`
|
|
840
|
+
);
|
|
841
|
+
}
|
|
842
|
+
const lease = await this.readLeaseSafe(input.runId);
|
|
843
|
+
const leaseStillLive = lease && lease.workerId !== input.workerId && new Date(lease.leaseExpiresAt).getTime() > nowMs;
|
|
844
|
+
if (leaseStillLive) {
|
|
845
|
+
throw new DurableRunLeaseHeldError(
|
|
846
|
+
`runId ${input.runId} leased by ${lease.workerId} until ${lease.leaseExpiresAt}`
|
|
847
|
+
);
|
|
848
|
+
}
|
|
849
|
+
const completedSteps = await this.readSteps(input.runId);
|
|
850
|
+
const nextRecord = {
|
|
851
|
+
...record,
|
|
852
|
+
status: record.status === "completed" || record.status === "failed" ? record.status : "running",
|
|
853
|
+
updatedAt: nowIso2,
|
|
854
|
+
leaseHolderId: input.workerId,
|
|
855
|
+
leaseExpiresAt
|
|
856
|
+
};
|
|
857
|
+
await this.writeRun(nextRecord);
|
|
858
|
+
await this.writeLease(input.runId, { workerId: input.workerId, leaseExpiresAt });
|
|
859
|
+
return { run: nextRecord, completedSteps, leaseExpiresAt };
|
|
860
|
+
}
|
|
861
|
+
async renewLease(input) {
|
|
862
|
+
const lease = await this.readLeaseSafe(input.runId);
|
|
863
|
+
const nowMs = this.now();
|
|
864
|
+
if (lease && lease.workerId !== input.workerId) {
|
|
865
|
+
if (new Date(lease.leaseExpiresAt).getTime() > nowMs) {
|
|
866
|
+
return { ok: false };
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
const leaseExpiresAt = new Date(nowMs + (input.leaseMs ?? DEFAULT_LEASE_MS2)).toISOString();
|
|
870
|
+
await this.writeLease(input.runId, { workerId: input.workerId, leaseExpiresAt });
|
|
871
|
+
return { ok: true, leaseExpiresAt };
|
|
872
|
+
}
|
|
873
|
+
async loadStep(runId, stepIndex) {
|
|
874
|
+
const steps = await this.readSteps(runId, { includeFailed: true, includeRunning: true });
|
|
875
|
+
return steps.find((s) => s.stepIndex === stepIndex);
|
|
876
|
+
}
|
|
877
|
+
async beginStep(input) {
|
|
878
|
+
const all = await this.readSteps(input.runId, { includeFailed: true, includeRunning: true });
|
|
879
|
+
const prior = all.find((s) => s.stepIndex === input.stepIndex);
|
|
880
|
+
const nowIso2 = new Date(this.now()).toISOString();
|
|
881
|
+
if (prior) {
|
|
882
|
+
if (prior.intent !== input.intent) {
|
|
883
|
+
throw new DurableRunDivergenceError(
|
|
884
|
+
`step ${input.stepIndex}: intent changed ('${prior.intent}' -> '${input.intent}')`
|
|
885
|
+
);
|
|
886
|
+
}
|
|
887
|
+
const rec2 = {
|
|
888
|
+
...prior,
|
|
889
|
+
attempts: prior.attempts + 1,
|
|
890
|
+
status: "running",
|
|
891
|
+
startedAt: nowIso2,
|
|
892
|
+
error: void 0
|
|
893
|
+
};
|
|
894
|
+
await this.appendStep(input.runId, rec2);
|
|
895
|
+
await this.bumpRunUpdated(input.runId, nowIso2);
|
|
896
|
+
return rec2;
|
|
897
|
+
}
|
|
898
|
+
const rec = {
|
|
899
|
+
runId: input.runId,
|
|
900
|
+
stepIndex: input.stepIndex,
|
|
901
|
+
intent: input.intent,
|
|
902
|
+
kind: input.kind,
|
|
903
|
+
inputHash: input.inputHash,
|
|
904
|
+
status: "running",
|
|
905
|
+
attempts: 1,
|
|
906
|
+
startedAt: nowIso2
|
|
907
|
+
};
|
|
908
|
+
await this.appendStep(input.runId, rec);
|
|
909
|
+
const record = await this.readRun(input.runId);
|
|
910
|
+
record.stepCount = Math.max(record.stepCount, input.stepIndex + 1);
|
|
911
|
+
record.updatedAt = nowIso2;
|
|
912
|
+
await this.writeRun(record);
|
|
913
|
+
return rec;
|
|
914
|
+
}
|
|
915
|
+
async completeStep(input) {
|
|
916
|
+
const all = await this.readSteps(input.runId, { includeFailed: true, includeRunning: true });
|
|
917
|
+
const prior = all.find((s) => s.stepIndex === input.stepIndex);
|
|
918
|
+
if (!prior) {
|
|
919
|
+
throw new Error(
|
|
920
|
+
`durable-runs: completeStep called before beginStep (step ${input.stepIndex})`
|
|
921
|
+
);
|
|
922
|
+
}
|
|
923
|
+
const nowIso2 = new Date(this.now()).toISOString();
|
|
924
|
+
const rec = {
|
|
925
|
+
...prior,
|
|
926
|
+
status: "completed",
|
|
927
|
+
result: input.result,
|
|
928
|
+
completedAt: nowIso2,
|
|
929
|
+
error: void 0
|
|
930
|
+
};
|
|
931
|
+
await this.appendStep(input.runId, rec);
|
|
932
|
+
await this.bumpRunUpdated(input.runId, nowIso2);
|
|
933
|
+
return rec;
|
|
934
|
+
}
|
|
935
|
+
async failStep(input) {
|
|
936
|
+
const all = await this.readSteps(input.runId, { includeFailed: true, includeRunning: true });
|
|
937
|
+
const prior = all.find((s) => s.stepIndex === input.stepIndex);
|
|
938
|
+
if (!prior) {
|
|
939
|
+
throw new Error(`durable-runs: failStep called before beginStep (step ${input.stepIndex})`);
|
|
940
|
+
}
|
|
941
|
+
const nowIso2 = new Date(this.now()).toISOString();
|
|
942
|
+
const rec = {
|
|
943
|
+
...prior,
|
|
944
|
+
status: "failed",
|
|
945
|
+
error: input.error,
|
|
946
|
+
completedAt: nowIso2
|
|
947
|
+
};
|
|
948
|
+
await this.appendStep(input.runId, rec);
|
|
949
|
+
await this.bumpRunUpdated(input.runId, nowIso2);
|
|
950
|
+
return rec;
|
|
951
|
+
}
|
|
952
|
+
async endRun(input) {
|
|
953
|
+
const record = await this.readRun(input.runId);
|
|
954
|
+
const nowIso2 = new Date(this.now()).toISOString();
|
|
955
|
+
record.status = input.status;
|
|
956
|
+
record.outcome = input.outcome;
|
|
957
|
+
record.completedAt = nowIso2;
|
|
958
|
+
record.updatedAt = nowIso2;
|
|
959
|
+
const lease = await this.readLeaseSafe(input.runId);
|
|
960
|
+
if (lease && lease.workerId === input.workerId) {
|
|
961
|
+
record.leaseHolderId = void 0;
|
|
962
|
+
record.leaseExpiresAt = void 0;
|
|
963
|
+
await this.writeLease(input.runId, null);
|
|
964
|
+
}
|
|
965
|
+
await this.writeRun(record);
|
|
966
|
+
return record;
|
|
967
|
+
}
|
|
968
|
+
async emitEvent(input) {
|
|
969
|
+
const existing = await this.loadEvent(input.runId, input.key);
|
|
970
|
+
if (existing) return { accepted: false, record: existing };
|
|
971
|
+
const rec = {
|
|
972
|
+
runId: input.runId,
|
|
973
|
+
key: input.key,
|
|
974
|
+
payload: input.payload,
|
|
975
|
+
emittedAt: new Date(this.now()).toISOString()
|
|
976
|
+
};
|
|
977
|
+
await appendFile(
|
|
978
|
+
join(this.runDir(input.runId), "events.jsonl"),
|
|
979
|
+
`${JSON.stringify(rec)}
|
|
980
|
+
`,
|
|
981
|
+
"utf8"
|
|
982
|
+
);
|
|
983
|
+
return { accepted: true, record: rec };
|
|
984
|
+
}
|
|
985
|
+
async loadEvent(runId, key) {
|
|
986
|
+
const path = join(this.runDir(runId), "events.jsonl");
|
|
987
|
+
if (!existsSync(path)) return void 0;
|
|
988
|
+
const content = await readFile(path, "utf8");
|
|
989
|
+
for (const line of content.split("\n").reverse()) {
|
|
990
|
+
if (!line) continue;
|
|
991
|
+
const rec = JSON.parse(line);
|
|
992
|
+
if (rec.key === key) return rec;
|
|
993
|
+
}
|
|
994
|
+
return void 0;
|
|
995
|
+
}
|
|
996
|
+
async close() {
|
|
997
|
+
}
|
|
998
|
+
/** @internal — used by tests to list runs in the store. */
|
|
999
|
+
async _listRunIds() {
|
|
1000
|
+
if (!existsSync(this.root)) return [];
|
|
1001
|
+
const entries = await readdir(this.root, { withFileTypes: true });
|
|
1002
|
+
return entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
1003
|
+
}
|
|
1004
|
+
// ── internals ──────────────────────────────────────────────────────
|
|
1005
|
+
runDir(runId) {
|
|
1006
|
+
return join(this.root, runId);
|
|
1007
|
+
}
|
|
1008
|
+
async readRun(runId) {
|
|
1009
|
+
const path = join(this.runDir(runId), "run.json");
|
|
1010
|
+
const content = await readFile(path, "utf8");
|
|
1011
|
+
return JSON.parse(content);
|
|
1012
|
+
}
|
|
1013
|
+
async writeRun(record) {
|
|
1014
|
+
const dir = this.runDir(record.runId);
|
|
1015
|
+
const path = join(dir, "run.json");
|
|
1016
|
+
const tmp = `${path}.tmp`;
|
|
1017
|
+
await writeFile(tmp, JSON.stringify(record, null, 2), "utf8");
|
|
1018
|
+
await rename(tmp, path);
|
|
1019
|
+
}
|
|
1020
|
+
async readLeaseSafe(runId) {
|
|
1021
|
+
const path = join(this.runDir(runId), "lease.json");
|
|
1022
|
+
if (!existsSync(path)) return void 0;
|
|
1023
|
+
try {
|
|
1024
|
+
const content = await readFile(path, "utf8");
|
|
1025
|
+
if (!content.trim()) return void 0;
|
|
1026
|
+
return JSON.parse(content);
|
|
1027
|
+
} catch {
|
|
1028
|
+
return void 0;
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
async writeLease(runId, lease) {
|
|
1032
|
+
const path = join(this.runDir(runId), "lease.json");
|
|
1033
|
+
const tmp = `${path}.tmp`;
|
|
1034
|
+
await writeFile(tmp, lease ? JSON.stringify(lease) : "", "utf8");
|
|
1035
|
+
await rename(tmp, path);
|
|
1036
|
+
}
|
|
1037
|
+
async readSteps(runId, opts = {}) {
|
|
1038
|
+
const path = join(this.runDir(runId), "steps.jsonl");
|
|
1039
|
+
if (!existsSync(path)) return [];
|
|
1040
|
+
const content = await readFile(path, "utf8");
|
|
1041
|
+
const latest = /* @__PURE__ */ new Map();
|
|
1042
|
+
for (const line of content.split("\n")) {
|
|
1043
|
+
if (!line) continue;
|
|
1044
|
+
const rec = JSON.parse(line);
|
|
1045
|
+
latest.set(rec.stepIndex, rec);
|
|
1046
|
+
}
|
|
1047
|
+
const out = [...latest.values()].sort((a, b) => a.stepIndex - b.stepIndex);
|
|
1048
|
+
return out.filter((s) => {
|
|
1049
|
+
if (s.status === "completed") return true;
|
|
1050
|
+
if (s.status === "failed") return opts.includeFailed ?? false;
|
|
1051
|
+
if (s.status === "running") return opts.includeRunning ?? false;
|
|
1052
|
+
return false;
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1055
|
+
async appendStep(runId, rec) {
|
|
1056
|
+
await appendFile(join(this.runDir(runId), "steps.jsonl"), `${JSON.stringify(rec)}
|
|
1057
|
+
`, "utf8");
|
|
1058
|
+
}
|
|
1059
|
+
async bumpRunUpdated(runId, nowIso2) {
|
|
1060
|
+
const record = await this.readRun(runId);
|
|
1061
|
+
record.updatedAt = nowIso2;
|
|
1062
|
+
await this.writeRun(record);
|
|
1063
|
+
}
|
|
1064
|
+
};
|
|
1065
|
+
|
|
1066
|
+
// src/durable/in-memory-store.ts
|
|
1067
|
+
var DEFAULT_LEASE_MS3 = 3e4;
|
|
1068
|
+
var InMemoryDurableRunStore = class {
|
|
1069
|
+
runs = /* @__PURE__ */ new Map();
|
|
1070
|
+
/** Override for tests — defaults to Date.now(). */
|
|
1071
|
+
now = () => Date.now();
|
|
1072
|
+
async startOrResume(input) {
|
|
1073
|
+
const leaseMs = input.leaseMs ?? DEFAULT_LEASE_MS3;
|
|
1074
|
+
const hash = manifestHash(input.manifest);
|
|
1075
|
+
const nowMs = this.now();
|
|
1076
|
+
const nowIso2 = new Date(nowMs).toISOString();
|
|
1077
|
+
const leaseExpiresMs = nowMs + leaseMs;
|
|
1078
|
+
const leaseExpiresAt = new Date(leaseExpiresMs).toISOString();
|
|
1079
|
+
let state = this.runs.get(input.runId);
|
|
1080
|
+
if (!state) {
|
|
1081
|
+
const record = {
|
|
1082
|
+
runId: input.runId,
|
|
1083
|
+
manifestHash: hash,
|
|
1084
|
+
projectId: input.manifest.projectId,
|
|
1085
|
+
scenarioId: input.manifest.scenarioId,
|
|
1086
|
+
status: "running",
|
|
1087
|
+
createdAt: nowIso2,
|
|
1088
|
+
updatedAt: nowIso2,
|
|
1089
|
+
leaseHolderId: input.workerId,
|
|
1090
|
+
leaseExpiresAt,
|
|
1091
|
+
stepCount: 0
|
|
1092
|
+
};
|
|
1093
|
+
state = { record, steps: /* @__PURE__ */ new Map(), events: /* @__PURE__ */ new Map() };
|
|
1094
|
+
this.runs.set(input.runId, state);
|
|
1095
|
+
return { run: { ...record }, completedSteps: [], leaseExpiresAt };
|
|
1096
|
+
}
|
|
1097
|
+
if (state.record.manifestHash !== hash) {
|
|
1098
|
+
throw new DurableRunInputMismatchError(
|
|
1099
|
+
`runId ${input.runId} exists with a different manifest hash; refuse to corrupt prior steps`
|
|
1100
|
+
);
|
|
1101
|
+
}
|
|
1102
|
+
const leaseStillLive = state.record.leaseHolderId !== void 0 && state.record.leaseHolderId !== input.workerId && state.record.leaseExpiresAt !== void 0 && new Date(state.record.leaseExpiresAt).getTime() > nowMs;
|
|
1103
|
+
if (leaseStillLive) {
|
|
1104
|
+
throw new DurableRunLeaseHeldError(
|
|
1105
|
+
`runId ${input.runId} is leased by ${state.record.leaseHolderId} until ${state.record.leaseExpiresAt}`
|
|
1106
|
+
);
|
|
1107
|
+
}
|
|
1108
|
+
state.record.leaseHolderId = input.workerId;
|
|
1109
|
+
state.record.leaseExpiresAt = leaseExpiresAt;
|
|
1110
|
+
state.record.status = state.record.status === "completed" || state.record.status === "failed" ? state.record.status : "running";
|
|
1111
|
+
state.record.updatedAt = nowIso2;
|
|
1112
|
+
const completed = [...state.steps.values()].filter((s) => s.status === "completed").sort((a, b) => a.stepIndex - b.stepIndex);
|
|
1113
|
+
return {
|
|
1114
|
+
run: { ...state.record },
|
|
1115
|
+
completedSteps: completed.map((s) => ({ ...s })),
|
|
1116
|
+
leaseExpiresAt
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
async renewLease(input) {
|
|
1120
|
+
const state = this.runs.get(input.runId);
|
|
1121
|
+
if (!state) return { ok: false };
|
|
1122
|
+
const nowMs = this.now();
|
|
1123
|
+
if (state.record.leaseHolderId !== input.workerId) {
|
|
1124
|
+
if (state.record.leaseExpiresAt && new Date(state.record.leaseExpiresAt).getTime() > nowMs) {
|
|
1125
|
+
return { ok: false };
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
const leaseExpiresMs = nowMs + (input.leaseMs ?? DEFAULT_LEASE_MS3);
|
|
1129
|
+
const leaseExpiresAt = new Date(leaseExpiresMs).toISOString();
|
|
1130
|
+
state.record.leaseHolderId = input.workerId;
|
|
1131
|
+
state.record.leaseExpiresAt = leaseExpiresAt;
|
|
1132
|
+
state.record.updatedAt = new Date(nowMs).toISOString();
|
|
1133
|
+
return { ok: true, leaseExpiresAt };
|
|
1134
|
+
}
|
|
1135
|
+
async loadStep(runId, stepIndex) {
|
|
1136
|
+
const state = this.runs.get(runId);
|
|
1137
|
+
return state ? cloneStep(state.steps.get(stepIndex)) : void 0;
|
|
1138
|
+
}
|
|
1139
|
+
async beginStep(input) {
|
|
1140
|
+
const state = this.requireRun(input.runId);
|
|
1141
|
+
const nowIso2 = new Date(this.now()).toISOString();
|
|
1142
|
+
const prior = state.steps.get(input.stepIndex);
|
|
1143
|
+
if (prior) {
|
|
1144
|
+
if (prior.intent !== input.intent) {
|
|
1145
|
+
throw new DurableRunDivergenceError(
|
|
1146
|
+
`step ${input.stepIndex}: intent changed across replays ('${prior.intent}' -> '${input.intent}')`
|
|
1147
|
+
);
|
|
1148
|
+
}
|
|
1149
|
+
prior.attempts += 1;
|
|
1150
|
+
prior.status = "running";
|
|
1151
|
+
prior.startedAt = nowIso2;
|
|
1152
|
+
prior.error = void 0;
|
|
1153
|
+
state.record.updatedAt = nowIso2;
|
|
1154
|
+
return cloneStep(prior);
|
|
1155
|
+
}
|
|
1156
|
+
const rec = {
|
|
1157
|
+
runId: input.runId,
|
|
1158
|
+
stepIndex: input.stepIndex,
|
|
1159
|
+
intent: input.intent,
|
|
1160
|
+
kind: input.kind,
|
|
1161
|
+
inputHash: input.inputHash,
|
|
1162
|
+
status: "running",
|
|
1163
|
+
attempts: 1,
|
|
1164
|
+
startedAt: nowIso2
|
|
1165
|
+
};
|
|
1166
|
+
state.steps.set(input.stepIndex, rec);
|
|
1167
|
+
state.record.stepCount = Math.max(state.record.stepCount, input.stepIndex + 1);
|
|
1168
|
+
state.record.updatedAt = nowIso2;
|
|
1169
|
+
return cloneStep(rec);
|
|
1170
|
+
}
|
|
1171
|
+
async completeStep(input) {
|
|
1172
|
+
const state = this.requireRun(input.runId);
|
|
1173
|
+
const rec = state.steps.get(input.stepIndex);
|
|
1174
|
+
if (!rec) {
|
|
1175
|
+
throw new Error(
|
|
1176
|
+
`durable-runs: completeStep called before beginStep (step ${input.stepIndex})`
|
|
1177
|
+
);
|
|
1178
|
+
}
|
|
1179
|
+
const nowIso2 = new Date(this.now()).toISOString();
|
|
1180
|
+
rec.status = "completed";
|
|
1181
|
+
rec.result = input.result;
|
|
1182
|
+
rec.completedAt = nowIso2;
|
|
1183
|
+
rec.error = void 0;
|
|
1184
|
+
state.record.updatedAt = nowIso2;
|
|
1185
|
+
return cloneStep(rec);
|
|
1186
|
+
}
|
|
1187
|
+
async failStep(input) {
|
|
1188
|
+
const state = this.requireRun(input.runId);
|
|
1189
|
+
const rec = state.steps.get(input.stepIndex);
|
|
1190
|
+
if (!rec) {
|
|
1191
|
+
throw new Error(`durable-runs: failStep called before beginStep (step ${input.stepIndex})`);
|
|
1192
|
+
}
|
|
1193
|
+
const nowIso2 = new Date(this.now()).toISOString();
|
|
1194
|
+
rec.status = "failed";
|
|
1195
|
+
rec.error = input.error;
|
|
1196
|
+
rec.completedAt = nowIso2;
|
|
1197
|
+
state.record.updatedAt = nowIso2;
|
|
1198
|
+
return cloneStep(rec);
|
|
1199
|
+
}
|
|
1200
|
+
async endRun(input) {
|
|
1201
|
+
const state = this.requireRun(input.runId);
|
|
1202
|
+
const nowIso2 = new Date(this.now()).toISOString();
|
|
1203
|
+
state.record.status = input.status;
|
|
1204
|
+
state.record.outcome = input.outcome;
|
|
1205
|
+
state.record.completedAt = nowIso2;
|
|
1206
|
+
state.record.updatedAt = nowIso2;
|
|
1207
|
+
if (state.record.leaseHolderId === input.workerId) {
|
|
1208
|
+
state.record.leaseHolderId = void 0;
|
|
1209
|
+
state.record.leaseExpiresAt = void 0;
|
|
1210
|
+
}
|
|
1211
|
+
return { ...state.record };
|
|
1212
|
+
}
|
|
1213
|
+
async emitEvent(input) {
|
|
1214
|
+
const state = this.requireRun(input.runId);
|
|
1215
|
+
const existing = state.events.get(input.key);
|
|
1216
|
+
if (existing) {
|
|
1217
|
+
return { accepted: false, record: { ...existing } };
|
|
1218
|
+
}
|
|
1219
|
+
const rec = {
|
|
1220
|
+
runId: input.runId,
|
|
1221
|
+
key: input.key,
|
|
1222
|
+
payload: input.payload,
|
|
1223
|
+
emittedAt: new Date(this.now()).toISOString()
|
|
1224
|
+
};
|
|
1225
|
+
state.events.set(input.key, rec);
|
|
1226
|
+
return { accepted: true, record: { ...rec } };
|
|
1227
|
+
}
|
|
1228
|
+
async loadEvent(runId, key) {
|
|
1229
|
+
const state = this.runs.get(runId);
|
|
1230
|
+
if (!state) return void 0;
|
|
1231
|
+
const rec = state.events.get(key);
|
|
1232
|
+
return rec ? { ...rec } : void 0;
|
|
1233
|
+
}
|
|
1234
|
+
async close() {
|
|
1235
|
+
this.runs.clear();
|
|
1236
|
+
}
|
|
1237
|
+
// ── test helpers ───────────────────────────────────────────────────
|
|
1238
|
+
/** @internal — used by tests to inspect lease metadata. */
|
|
1239
|
+
_inspect(runId) {
|
|
1240
|
+
const s = this.runs.get(runId);
|
|
1241
|
+
return s ? { ...s.record } : void 0;
|
|
1242
|
+
}
|
|
1243
|
+
/** @internal — used by tests to simulate lease expiry. */
|
|
1244
|
+
_expireLease(runId) {
|
|
1245
|
+
const s = this.runs.get(runId);
|
|
1246
|
+
if (s) {
|
|
1247
|
+
s.record.leaseHolderId = void 0;
|
|
1248
|
+
s.record.leaseExpiresAt = void 0;
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
requireRun(runId) {
|
|
1252
|
+
const s = this.runs.get(runId);
|
|
1253
|
+
if (!s) {
|
|
1254
|
+
throw new Error(`durable-runs: run ${runId} not found (must call startOrResume first)`);
|
|
1255
|
+
}
|
|
1256
|
+
return s;
|
|
1257
|
+
}
|
|
1258
|
+
};
|
|
1259
|
+
function cloneStep(rec) {
|
|
1260
|
+
if (!rec) return void 0;
|
|
1261
|
+
return { ...rec, error: rec.error ? { ...rec.error } : void 0 };
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
// src/durable/runner.ts
|
|
1265
|
+
var DEFAULT_LEASE_MS4 = 3e4;
|
|
1266
|
+
var DEFAULT_AWAIT_POLL_MS = 250;
|
|
1267
|
+
async function runDurable(input) {
|
|
1268
|
+
const workerId = input.workerId ?? deriveWorkerId();
|
|
1269
|
+
const leaseMs = input.leaseMs ?? DEFAULT_LEASE_MS4;
|
|
1270
|
+
const { completedSteps } = await input.store.startOrResume({
|
|
1271
|
+
runId: input.runId,
|
|
1272
|
+
manifest: input.manifest,
|
|
1273
|
+
workerId,
|
|
1274
|
+
leaseMs
|
|
1275
|
+
});
|
|
1276
|
+
const priorByIndex = /* @__PURE__ */ new Map();
|
|
1277
|
+
for (const s of completedSteps) priorByIndex.set(s.stepIndex, s);
|
|
1278
|
+
const collected = [...completedSteps].sort((a, b) => a.stepIndex - b.stepIndex);
|
|
1279
|
+
let leaseLost = false;
|
|
1280
|
+
const heartbeatIntervalMs = Math.max(1e3, Math.floor(leaseMs / 3));
|
|
1281
|
+
const heartbeat = setInterval(() => {
|
|
1282
|
+
void input.store.renewLease({ runId: input.runId, workerId, leaseMs }).then((res) => {
|
|
1283
|
+
if (!res.ok) leaseLost = true;
|
|
1284
|
+
}).catch(() => {
|
|
1285
|
+
leaseLost = true;
|
|
1286
|
+
});
|
|
1287
|
+
}, heartbeatIntervalMs);
|
|
1288
|
+
if (typeof heartbeat.unref === "function") heartbeat.unref();
|
|
1289
|
+
let positionCounter = 0;
|
|
1290
|
+
const ctx = {
|
|
1291
|
+
runId: input.runId,
|
|
1292
|
+
projectId: input.manifest.projectId,
|
|
1293
|
+
scenarioId: input.manifest.scenarioId,
|
|
1294
|
+
async step(intent, fn, opts) {
|
|
1295
|
+
checkAbortAndLease(input.signal, leaseLost);
|
|
1296
|
+
const stepIndex = positionCounter++;
|
|
1297
|
+
const prior = priorByIndex.get(stepIndex);
|
|
1298
|
+
const inputHash = opts?.inputFingerprint !== void 0 ? canonicalHash(opts.inputFingerprint) : "";
|
|
1299
|
+
if (prior && prior.status === "completed") {
|
|
1300
|
+
if (prior.intent !== intent) {
|
|
1301
|
+
throw new DurableRunDivergenceError(
|
|
1302
|
+
`step ${stepIndex}: intent changed across replays ('${prior.intent}' -> '${intent}')`
|
|
1303
|
+
);
|
|
1304
|
+
}
|
|
1305
|
+
return prior.result;
|
|
1306
|
+
}
|
|
1307
|
+
const begun = await input.store.beginStep({
|
|
1308
|
+
runId: input.runId,
|
|
1309
|
+
stepIndex,
|
|
1310
|
+
intent,
|
|
1311
|
+
kind: opts?.kind ?? "logic",
|
|
1312
|
+
inputHash
|
|
1313
|
+
});
|
|
1314
|
+
try {
|
|
1315
|
+
const result = await fn();
|
|
1316
|
+
const completed = await input.store.completeStep({
|
|
1317
|
+
runId: input.runId,
|
|
1318
|
+
stepIndex,
|
|
1319
|
+
result
|
|
1320
|
+
});
|
|
1321
|
+
upsertCollected(collected, completed);
|
|
1322
|
+
priorByIndex.set(stepIndex, completed);
|
|
1323
|
+
return result;
|
|
1324
|
+
} catch (err) {
|
|
1325
|
+
const failed = await input.store.failStep({
|
|
1326
|
+
runId: input.runId,
|
|
1327
|
+
stepIndex,
|
|
1328
|
+
error: toStepError(err)
|
|
1329
|
+
});
|
|
1330
|
+
upsertCollected(collected, failed);
|
|
1331
|
+
priorByIndex.set(stepIndex, failed);
|
|
1332
|
+
throw err;
|
|
1333
|
+
} finally {
|
|
1334
|
+
void begun;
|
|
1335
|
+
}
|
|
1336
|
+
},
|
|
1337
|
+
async awaitEvent(key, opts) {
|
|
1338
|
+
const stepIndex = positionCounter++;
|
|
1339
|
+
const prior = priorByIndex.get(stepIndex);
|
|
1340
|
+
if (prior && prior.status === "completed") {
|
|
1341
|
+
if (prior.intent !== `event:${key}`) {
|
|
1342
|
+
throw new DurableRunDivergenceError(
|
|
1343
|
+
`step ${stepIndex}: awaitEvent key changed across replays`
|
|
1344
|
+
);
|
|
1345
|
+
}
|
|
1346
|
+
return prior.result;
|
|
1347
|
+
}
|
|
1348
|
+
const beginAt = Date.now();
|
|
1349
|
+
const timeoutMs = opts?.timeoutMs ?? 6e4;
|
|
1350
|
+
const pollMs = opts?.pollMs ?? DEFAULT_AWAIT_POLL_MS;
|
|
1351
|
+
await input.store.beginStep({
|
|
1352
|
+
runId: input.runId,
|
|
1353
|
+
stepIndex,
|
|
1354
|
+
intent: `event:${key}`,
|
|
1355
|
+
kind: "event",
|
|
1356
|
+
inputHash: ""
|
|
1357
|
+
});
|
|
1358
|
+
try {
|
|
1359
|
+
for (; ; ) {
|
|
1360
|
+
checkAbortAndLease(input.signal, leaseLost);
|
|
1361
|
+
const evt = await input.store.loadEvent(input.runId, key);
|
|
1362
|
+
if (evt) {
|
|
1363
|
+
const completed = await input.store.completeStep({
|
|
1364
|
+
runId: input.runId,
|
|
1365
|
+
stepIndex,
|
|
1366
|
+
result: evt.payload
|
|
1367
|
+
});
|
|
1368
|
+
upsertCollected(collected, completed);
|
|
1369
|
+
priorByIndex.set(stepIndex, completed);
|
|
1370
|
+
return evt.payload;
|
|
1371
|
+
}
|
|
1372
|
+
if (Date.now() - beginAt > timeoutMs) {
|
|
1373
|
+
const err = new DurableAwaitEventTimeoutError(
|
|
1374
|
+
`awaitEvent('${key}') timed out after ${timeoutMs}ms`
|
|
1375
|
+
);
|
|
1376
|
+
const failed = await input.store.failStep({
|
|
1377
|
+
runId: input.runId,
|
|
1378
|
+
stepIndex,
|
|
1379
|
+
error: toStepError(err)
|
|
1380
|
+
});
|
|
1381
|
+
upsertCollected(collected, failed);
|
|
1382
|
+
priorByIndex.set(stepIndex, failed);
|
|
1383
|
+
throw err;
|
|
1384
|
+
}
|
|
1385
|
+
await sleep2(pollMs, input.signal);
|
|
1386
|
+
}
|
|
1387
|
+
} catch (err) {
|
|
1388
|
+
if (!(err instanceof DurableAwaitEventTimeoutError)) {
|
|
1389
|
+
const existing = priorByIndex.get(stepIndex);
|
|
1390
|
+
if (!existing || existing.status !== "failed") {
|
|
1391
|
+
const failed = await input.store.failStep({
|
|
1392
|
+
runId: input.runId,
|
|
1393
|
+
stepIndex,
|
|
1394
|
+
error: toStepError(err)
|
|
1395
|
+
});
|
|
1396
|
+
upsertCollected(collected, failed);
|
|
1397
|
+
priorByIndex.set(stepIndex, failed);
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
throw err;
|
|
1401
|
+
}
|
|
1402
|
+
},
|
|
1403
|
+
async emitEvent(key, payload) {
|
|
1404
|
+
const res = await input.store.emitEvent({ runId: input.runId, key, payload });
|
|
1405
|
+
return { accepted: res.accepted };
|
|
1406
|
+
},
|
|
1407
|
+
async now() {
|
|
1408
|
+
const v = await this.step(`deterministic:now`, async () => (/* @__PURE__ */ new Date()).toISOString(), {
|
|
1409
|
+
kind: "deterministic"
|
|
1410
|
+
});
|
|
1411
|
+
return new Date(v);
|
|
1412
|
+
},
|
|
1413
|
+
async uuid() {
|
|
1414
|
+
return this.step(`deterministic:uuid`, async () => cryptoRandomUuid(), {
|
|
1415
|
+
kind: "deterministic"
|
|
1416
|
+
});
|
|
1417
|
+
}
|
|
1418
|
+
};
|
|
1419
|
+
try {
|
|
1420
|
+
const result = await input.taskFn(ctx);
|
|
1421
|
+
const finalRecord = await input.store.endRun({
|
|
1422
|
+
runId: input.runId,
|
|
1423
|
+
workerId,
|
|
1424
|
+
status: "completed",
|
|
1425
|
+
outcome: input.defaultOutcome
|
|
1426
|
+
});
|
|
1427
|
+
return { result, record: finalRecord, steps: collected };
|
|
1428
|
+
} catch (err) {
|
|
1429
|
+
const finalRecord = await input.store.endRun({
|
|
1430
|
+
runId: input.runId,
|
|
1431
|
+
workerId,
|
|
1432
|
+
status: "failed",
|
|
1433
|
+
outcome: {
|
|
1434
|
+
...input.defaultOutcome,
|
|
1435
|
+
notes: err instanceof Error ? err.message : String(err)
|
|
1436
|
+
}
|
|
1437
|
+
});
|
|
1438
|
+
void finalRecord;
|
|
1439
|
+
throw err;
|
|
1440
|
+
} finally {
|
|
1441
|
+
clearInterval(heartbeat);
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
function checkAbortAndLease(signal, leaseLost) {
|
|
1445
|
+
if (signal?.aborted) throw signal.reason ?? new Error("aborted");
|
|
1446
|
+
if (leaseLost) throw new Error("durable-runs: lease lost; another worker has taken over this run");
|
|
1447
|
+
}
|
|
1448
|
+
function upsertCollected(list, rec) {
|
|
1449
|
+
const i = list.findIndex((s) => s.stepIndex === rec.stepIndex);
|
|
1450
|
+
if (i === -1) list.push(rec);
|
|
1451
|
+
else list[i] = rec;
|
|
1452
|
+
}
|
|
1453
|
+
function toStepError(err) {
|
|
1454
|
+
if (err instanceof Error) {
|
|
1455
|
+
return {
|
|
1456
|
+
message: err.message,
|
|
1457
|
+
code: err.code,
|
|
1458
|
+
stack: err.stack
|
|
1459
|
+
};
|
|
1460
|
+
}
|
|
1461
|
+
return { message: String(err) };
|
|
1462
|
+
}
|
|
1463
|
+
function sleep2(ms, signal) {
|
|
1464
|
+
return new Promise((resolve, reject) => {
|
|
1465
|
+
if (signal?.aborted) {
|
|
1466
|
+
reject(signal.reason ?? new Error("aborted"));
|
|
1467
|
+
return;
|
|
1468
|
+
}
|
|
1469
|
+
const t = setTimeout(() => {
|
|
1470
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1471
|
+
resolve();
|
|
1472
|
+
}, ms);
|
|
1473
|
+
const onAbort = () => {
|
|
1474
|
+
clearTimeout(t);
|
|
1475
|
+
reject(signal?.reason ?? new Error("aborted"));
|
|
1476
|
+
};
|
|
1477
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1478
|
+
});
|
|
1479
|
+
}
|
|
1480
|
+
function cryptoRandomUuid() {
|
|
1481
|
+
if (typeof globalThis.crypto?.randomUUID === "function") {
|
|
1482
|
+
return globalThis.crypto.randomUUID();
|
|
1483
|
+
}
|
|
1484
|
+
const bytes = new Uint8Array(16);
|
|
1485
|
+
for (let i = 0; i < 16; i++) bytes[i] = Math.floor(Math.random() * 256);
|
|
1486
|
+
bytes[6] = (bytes[6] ?? 0) & 15 | 64;
|
|
1487
|
+
bytes[8] = (bytes[8] ?? 0) & 63 | 128;
|
|
1488
|
+
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
1489
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
// src/durable/workflows.ts
|
|
1493
|
+
async function runOnWorkflowStep(workflowStep, input) {
|
|
1494
|
+
const stepCfg = input.stepConfig;
|
|
1495
|
+
let counter2 = 0;
|
|
1496
|
+
const stepName = (intent) => `${input.workflowName}/${counter2++}:${intent}`;
|
|
1497
|
+
const ctx = {
|
|
1498
|
+
runId: `cf-workflow:${input.workflowName}`,
|
|
1499
|
+
projectId: input.workflowName,
|
|
1500
|
+
async step(intent, fn) {
|
|
1501
|
+
const name = stepName(intent);
|
|
1502
|
+
if (stepCfg) {
|
|
1503
|
+
return workflowStep.do(name, stepCfg, fn);
|
|
1504
|
+
}
|
|
1505
|
+
return workflowStep.do(name, fn);
|
|
1506
|
+
},
|
|
1507
|
+
async awaitEvent(key, opts) {
|
|
1508
|
+
const timeout = opts?.timeoutMs ? `${Math.ceil(opts.timeoutMs / 1e3)}s` : void 0;
|
|
1509
|
+
const ev = await workflowStep.waitForEvent(stepName(`event:${key}`), {
|
|
1510
|
+
type: key,
|
|
1511
|
+
timeout
|
|
1512
|
+
});
|
|
1513
|
+
return ev.payload;
|
|
1514
|
+
},
|
|
1515
|
+
async emitEvent() {
|
|
1516
|
+
throw new Error(
|
|
1517
|
+
"runOnWorkflowStep: ctx.emitEvent is not available inside a Workflows entrypoint. Emit from the sibling worker that drives the workflow."
|
|
1518
|
+
);
|
|
1519
|
+
},
|
|
1520
|
+
async now() {
|
|
1521
|
+
const iso = await this.step("deterministic:now", async () => (/* @__PURE__ */ new Date()).toISOString());
|
|
1522
|
+
return new Date(iso);
|
|
1523
|
+
},
|
|
1524
|
+
async uuid() {
|
|
1525
|
+
return this.step("deterministic:uuid", async () => {
|
|
1526
|
+
if (typeof globalThis.crypto?.randomUUID === "function") {
|
|
1527
|
+
return globalThis.crypto.randomUUID();
|
|
1528
|
+
}
|
|
1529
|
+
const bytes = new Uint8Array(16);
|
|
1530
|
+
crypto.getRandomValues(bytes);
|
|
1531
|
+
bytes[6] = (bytes[6] ?? 0) & 15 | 64;
|
|
1532
|
+
bytes[8] = (bytes[8] ?? 0) & 63 | 128;
|
|
1533
|
+
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
1534
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
1535
|
+
});
|
|
1536
|
+
}
|
|
1537
|
+
};
|
|
1538
|
+
return input.taskFn(ctx);
|
|
1539
|
+
}
|
|
1540
|
+
|
|
436
1541
|
// src/intent-router.ts
|
|
437
1542
|
var DEFAULT_PATTERN_WEIGHT = 1.5;
|
|
438
1543
|
var DEFAULT_MIN_SCORE = 1;
|
|
@@ -1507,8 +2612,8 @@ function createTraceBridge(options) {
|
|
|
1507
2612
|
if (!options.runId) {
|
|
1508
2613
|
throw new ValidationError("createTraceBridge: runId is required");
|
|
1509
2614
|
}
|
|
1510
|
-
let
|
|
1511
|
-
const newEventId = options.newEventId ?? (() => `evt-${++
|
|
2615
|
+
let counter2 = 0;
|
|
2616
|
+
const newEventId = options.newEventId ?? (() => `evt-${++counter2}`);
|
|
1512
2617
|
const baseSpanId = options.spanId;
|
|
1513
2618
|
const toTraceEvent = (event) => {
|
|
1514
2619
|
const projection = projectToTraceEvent(event);
|
|
@@ -1685,6 +2790,14 @@ export {
|
|
|
1685
2790
|
CaptureIntegrityError,
|
|
1686
2791
|
ChatTurnError,
|
|
1687
2792
|
ConfigError,
|
|
2793
|
+
D1DurableRunStore,
|
|
2794
|
+
DurableAwaitEventTimeoutError,
|
|
2795
|
+
DurableRunDivergenceError,
|
|
2796
|
+
DurableRunError,
|
|
2797
|
+
DurableRunInputMismatchError,
|
|
2798
|
+
DurableRunLeaseHeldError,
|
|
2799
|
+
FileSystemDurableRunStore,
|
|
2800
|
+
InMemoryDurableRunStore,
|
|
1688
2801
|
InMemoryRuntimeSessionStore,
|
|
1689
2802
|
JudgeError,
|
|
1690
2803
|
NotFoundError,
|
|
@@ -1694,6 +2807,8 @@ export {
|
|
|
1694
2807
|
ValidationError,
|
|
1695
2808
|
VerificationError,
|
|
1696
2809
|
assertProfileConformance,
|
|
2810
|
+
canonicalHash,
|
|
2811
|
+
canonicalJson,
|
|
1697
2812
|
classifyIntent,
|
|
1698
2813
|
composeTurnProfile,
|
|
1699
2814
|
createIterableBackend,
|
|
@@ -1703,17 +2818,22 @@ export {
|
|
|
1703
2818
|
createSandboxPromptBackend,
|
|
1704
2819
|
createTraceBridge,
|
|
1705
2820
|
decideKnowledgeReadiness,
|
|
2821
|
+
deriveWorkerId,
|
|
1706
2822
|
encodeServerSentEvent,
|
|
2823
|
+
manifestHash,
|
|
1707
2824
|
readinessServerSentEvent,
|
|
1708
2825
|
runAgentTask,
|
|
1709
2826
|
runAgentTaskStream,
|
|
1710
2827
|
runChatTurn,
|
|
2828
|
+
runDurable,
|
|
2829
|
+
runOnWorkflowStep,
|
|
1711
2830
|
runtimeStreamServerSentEvent,
|
|
1712
2831
|
sandboxAsChatTurnTarget,
|
|
1713
2832
|
sanitizeAgentRuntimeEvent,
|
|
1714
2833
|
sanitizeKnowledgeReadinessReport,
|
|
1715
2834
|
sanitizeRuntimeStreamEvent,
|
|
1716
2835
|
startRuntimeRun,
|
|
2836
|
+
stepId,
|
|
1717
2837
|
summarizeAgentTaskRun,
|
|
1718
2838
|
toAgentEvalTrace
|
|
1719
2839
|
};
|