@tangle-network/agent-runtime 0.15.1 → 0.16.1
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/README.md +46 -64
- package/dist/agent.d.ts +1 -1
- package/dist/index.d.ts +75 -1128
- package/dist/index.js +73 -1686
- package/dist/index.js.map +1 -1
- package/dist/{types-CYxfw14J.d.ts → types-DmhXdAhu.d.ts} +1 -1
- package/package.json +2 -8
package/dist/index.js
CHANGED
|
@@ -252,7 +252,9 @@ function mapCommonBackendEvent(event, context) {
|
|
|
252
252
|
const type = String(record.type ?? "");
|
|
253
253
|
const data = record.data && typeof record.data === "object" ? record.data : record;
|
|
254
254
|
if (type === "message.part.updated" || type === "text_delta" || type === "delta") {
|
|
255
|
-
const
|
|
255
|
+
const part = data.part;
|
|
256
|
+
const partText = part !== void 0 && typeof part === "object" && (part.type === "text" || part.type === void 0) ? stringValue(part.text) : void 0;
|
|
257
|
+
const text = stringValue(data.text) ?? stringValue(data.delta) ?? stringValue(record.text) ?? partText;
|
|
256
258
|
return text ? {
|
|
257
259
|
type: "text_delta",
|
|
258
260
|
task: context.task,
|
|
@@ -514,1691 +516,96 @@ function sandboxAsChatTurnTarget(instance, opts) {
|
|
|
514
516
|
};
|
|
515
517
|
}
|
|
516
518
|
|
|
517
|
-
// src/durable/identity.ts
|
|
518
|
-
import { createHash } from "crypto";
|
|
519
|
-
function canonicalHash(value) {
|
|
520
|
-
const json = canonicalJson(value);
|
|
521
|
-
return createHash("sha256").update(json).digest("hex");
|
|
522
|
-
}
|
|
523
|
-
function canonicalJson(value) {
|
|
524
|
-
return JSON.stringify(canonicalize(value));
|
|
525
|
-
}
|
|
526
|
-
function canonicalize(value) {
|
|
527
|
-
if (value === null) return null;
|
|
528
|
-
if (Array.isArray(value)) return value.map(canonicalize);
|
|
529
|
-
const t = typeof value;
|
|
530
|
-
if (t === "string" || t === "boolean") return value;
|
|
531
|
-
if (t === "number") {
|
|
532
|
-
if (!Number.isFinite(value)) {
|
|
533
|
-
throw new TypeError(`canonicalJson: non-finite number ${String(value)} not serializable`);
|
|
534
|
-
}
|
|
535
|
-
return value;
|
|
536
|
-
}
|
|
537
|
-
if (t === "undefined" || t === "function" || t === "symbol") {
|
|
538
|
-
throw new TypeError(`canonicalJson: ${t} is not JSON-serializable`);
|
|
539
|
-
}
|
|
540
|
-
if (t === "bigint") {
|
|
541
|
-
return { __bigint: String(value) };
|
|
542
|
-
}
|
|
543
|
-
if (t === "object") {
|
|
544
|
-
const obj = value;
|
|
545
|
-
const proto = Object.getPrototypeOf(obj);
|
|
546
|
-
if (proto !== null && proto !== Object.prototype) {
|
|
547
|
-
const ctor = obj.constructor?.name ?? "unknown";
|
|
548
|
-
throw new TypeError(
|
|
549
|
-
`canonicalJson: class instance (${ctor}) is not JSON-serializable. Project to plain { ... } at the boundary.`
|
|
550
|
-
);
|
|
551
|
-
}
|
|
552
|
-
const keys = Object.keys(obj).sort();
|
|
553
|
-
const out = {};
|
|
554
|
-
for (const k of keys) out[k] = canonicalize(obj[k]);
|
|
555
|
-
return out;
|
|
556
|
-
}
|
|
557
|
-
throw new TypeError(`canonicalJson: unsupported type ${t}`);
|
|
558
|
-
}
|
|
559
|
-
function manifestHash(manifest) {
|
|
560
|
-
return canonicalHash({
|
|
561
|
-
projectId: manifest.projectId,
|
|
562
|
-
scenarioId: manifest.scenarioId ?? null,
|
|
563
|
-
taskId: manifest.task.id,
|
|
564
|
-
taskIntent: manifest.task.intent,
|
|
565
|
-
taskDomain: manifest.task.domain,
|
|
566
|
-
input: manifest.input
|
|
567
|
-
});
|
|
568
|
-
}
|
|
569
|
-
function stepId(runId, stepIndex, intent) {
|
|
570
|
-
return canonicalHash({ runId, stepIndex, intent });
|
|
571
|
-
}
|
|
572
|
-
var counter = 0;
|
|
573
|
-
function deriveWorkerId() {
|
|
574
|
-
const host = process.env.HOSTNAME ?? "host";
|
|
575
|
-
const pid = process.pid ?? 0;
|
|
576
|
-
const rand = Math.random().toString(36).slice(2, 10);
|
|
577
|
-
counter += 1;
|
|
578
|
-
return `${host}:${pid}:${rand}:${counter}`;
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
// src/durable/turn.ts
|
|
582
|
-
var STEP_INDEX = 0;
|
|
583
|
-
function runDurableTurn(options) {
|
|
584
|
-
const { store, runId, manifest, workerId } = options;
|
|
585
|
-
const leaseMs = options.leaseMs ?? 6e4;
|
|
586
|
-
const intent = options.intent ?? "turn";
|
|
587
|
-
const inputHash = canonicalHash(manifest.input);
|
|
588
|
-
let accumulated = "";
|
|
589
|
-
let didReplay = false;
|
|
590
|
-
let finalRecord;
|
|
591
|
-
async function* stream() {
|
|
592
|
-
const { completedSteps } = await store.startOrResume({
|
|
593
|
-
runId,
|
|
594
|
-
manifest,
|
|
595
|
-
workerId,
|
|
596
|
-
leaseMs
|
|
597
|
-
});
|
|
598
|
-
const prior = completedSteps.find((s) => s.stepIndex === STEP_INDEX);
|
|
599
|
-
if (prior && prior.status === "completed") {
|
|
600
|
-
didReplay = true;
|
|
601
|
-
const cached = prior.result;
|
|
602
|
-
accumulated = cached?.finalText ?? "";
|
|
603
|
-
yield options.replayEvent(accumulated);
|
|
604
|
-
finalRecord = await store.endRun({ runId, workerId, status: "completed" });
|
|
605
|
-
return;
|
|
606
|
-
}
|
|
607
|
-
await store.beginStep({
|
|
608
|
-
runId,
|
|
609
|
-
stepIndex: STEP_INDEX,
|
|
610
|
-
intent,
|
|
611
|
-
kind: "llm",
|
|
612
|
-
inputHash
|
|
613
|
-
});
|
|
614
|
-
try {
|
|
615
|
-
const producer = options.produce();
|
|
616
|
-
for await (const event of producer.stream) {
|
|
617
|
-
if (options.accumulate) {
|
|
618
|
-
const next = options.accumulate(event, accumulated);
|
|
619
|
-
if (typeof next === "string") accumulated = next;
|
|
620
|
-
}
|
|
621
|
-
yield event;
|
|
622
|
-
}
|
|
623
|
-
const producerText = producer.finalText();
|
|
624
|
-
if (producerText) accumulated = producerText;
|
|
625
|
-
await store.completeStep({
|
|
626
|
-
runId,
|
|
627
|
-
stepIndex: STEP_INDEX,
|
|
628
|
-
result: { finalText: accumulated }
|
|
629
|
-
});
|
|
630
|
-
finalRecord = await store.endRun({
|
|
631
|
-
runId,
|
|
632
|
-
workerId,
|
|
633
|
-
status: "completed",
|
|
634
|
-
outcome: { notes: intent, metadata: { chars: accumulated.length } }
|
|
635
|
-
});
|
|
636
|
-
} catch (err) {
|
|
637
|
-
await store.failStep({
|
|
638
|
-
runId,
|
|
639
|
-
stepIndex: STEP_INDEX,
|
|
640
|
-
error: { message: err instanceof Error ? err.message : String(err) }
|
|
641
|
-
});
|
|
642
|
-
finalRecord = await store.endRun({ runId, workerId, status: "failed" });
|
|
643
|
-
throw err;
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
return {
|
|
647
|
-
stream: stream(),
|
|
648
|
-
finalText: () => accumulated,
|
|
649
|
-
replayed: () => didReplay,
|
|
650
|
-
record: () => finalRecord
|
|
651
|
-
};
|
|
652
|
-
}
|
|
653
|
-
|
|
654
519
|
// src/durable/chat-engine.ts
|
|
655
520
|
var encoder = new TextEncoder();
|
|
656
521
|
function encodeLine(event) {
|
|
657
522
|
return encoder.encode(`${JSON.stringify(event)}
|
|
658
523
|
`);
|
|
659
524
|
}
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
metadata: {
|
|
680
|
-
tenantId: identity.tenantId,
|
|
681
|
-
sessionId: identity.sessionId,
|
|
682
|
-
turnIndex: identity.turnIndex
|
|
525
|
+
function defaultLog(message, meta) {
|
|
526
|
+
if (meta) console.error(message, meta);
|
|
527
|
+
else console.error(message);
|
|
528
|
+
}
|
|
529
|
+
function handleChatTurn(input) {
|
|
530
|
+
const log = input.log ?? defaultLog;
|
|
531
|
+
const { identity, hooks } = input;
|
|
532
|
+
const body = new ReadableStream({
|
|
533
|
+
start: async (controller) => {
|
|
534
|
+
const emit2 = async (event) => {
|
|
535
|
+
controller.enqueue(encodeLine(event));
|
|
536
|
+
if (hooks.onEvent) {
|
|
537
|
+
try {
|
|
538
|
+
await hooks.onEvent(event);
|
|
539
|
+
} catch (err) {
|
|
540
|
+
log("[chat-engine] onEvent hook threw", {
|
|
541
|
+
error: err instanceof Error ? err.message : String(err)
|
|
542
|
+
});
|
|
543
|
+
}
|
|
683
544
|
}
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
}
|
|
693
|
-
};
|
|
694
|
-
const body = new ReadableStream({
|
|
695
|
-
start: async (controller) => {
|
|
696
|
-
const emit2 = async (event) => {
|
|
697
|
-
controller.enqueue(encodeLine(event));
|
|
698
|
-
if (input.hooks.onEvent) {
|
|
699
|
-
try {
|
|
700
|
-
await input.hooks.onEvent(event);
|
|
701
|
-
} catch (err) {
|
|
702
|
-
log("[chat-engine] onEvent hook threw", {
|
|
703
|
-
error: err instanceof Error ? err.message : String(err)
|
|
704
|
-
});
|
|
705
|
-
}
|
|
545
|
+
};
|
|
546
|
+
try {
|
|
547
|
+
await emit2({
|
|
548
|
+
type: "session.run.started",
|
|
549
|
+
data: {
|
|
550
|
+
sessionId: identity.sessionId,
|
|
551
|
+
tenantId: identity.tenantId,
|
|
552
|
+
turnIndex: identity.turnIndex
|
|
706
553
|
}
|
|
707
|
-
};
|
|
708
|
-
|
|
554
|
+
});
|
|
555
|
+
const producer = hooks.produce();
|
|
556
|
+
for await (const event of producer.stream) {
|
|
557
|
+
await emit2(event);
|
|
558
|
+
}
|
|
559
|
+
const rawFinal = producer.finalText();
|
|
560
|
+
const finalText = hooks.transformFinalText ? await hooks.transformFinalText(rawFinal) : rawFinal;
|
|
709
561
|
try {
|
|
710
|
-
await
|
|
711
|
-
type: "session.run.started",
|
|
712
|
-
data: {
|
|
713
|
-
sessionId: identity.sessionId,
|
|
714
|
-
tenantId: identity.tenantId,
|
|
715
|
-
turnIndex: identity.turnIndex
|
|
716
|
-
}
|
|
717
|
-
});
|
|
718
|
-
const turn = runDurableTurn({
|
|
719
|
-
store: input.store,
|
|
720
|
-
runId,
|
|
721
|
-
manifest,
|
|
722
|
-
workerId,
|
|
723
|
-
leaseMs: input.leaseMs,
|
|
724
|
-
intent: `chat:turn-${identity.turnIndex}`,
|
|
725
|
-
produce: input.hooks.produce,
|
|
726
|
-
replayEvent: (finalText2) => ({ type: "result", data: { finalText: finalText2 } }),
|
|
727
|
-
accumulate: (event, current) => {
|
|
728
|
-
if (event.type === "message.part.updated") {
|
|
729
|
-
const data = event.data ?? {};
|
|
730
|
-
const delta = typeof data.delta === "string" ? data.delta : "";
|
|
731
|
-
const part = data.part;
|
|
732
|
-
if (delta) return current + delta;
|
|
733
|
-
if (part?.type === "text" && typeof part.text === "string") return part.text;
|
|
734
|
-
return void 0;
|
|
735
|
-
}
|
|
736
|
-
if (event.type === "result") {
|
|
737
|
-
const data = event.data ?? {};
|
|
738
|
-
if (typeof data.finalText === "string") return data.finalText;
|
|
739
|
-
}
|
|
740
|
-
return void 0;
|
|
741
|
-
}
|
|
742
|
-
});
|
|
743
|
-
for await (const event of turn.stream) {
|
|
744
|
-
await emit2(event);
|
|
745
|
-
}
|
|
746
|
-
const rawFinal = turn.finalText();
|
|
747
|
-
const finalText = input.hooks.transformFinalText ? await input.hooks.transformFinalText(rawFinal) : rawFinal;
|
|
748
|
-
if (!turn.replayed()) {
|
|
749
|
-
try {
|
|
750
|
-
await input.hooks.persistAssistantMessage({
|
|
751
|
-
identity,
|
|
752
|
-
finalText,
|
|
753
|
-
record: turn.record()
|
|
754
|
-
});
|
|
755
|
-
} catch (err) {
|
|
756
|
-
log("[chat-engine] persistAssistantMessage threw", {
|
|
757
|
-
error: err instanceof Error ? err.message : String(err)
|
|
758
|
-
});
|
|
759
|
-
}
|
|
760
|
-
if (input.hooks.onTurnComplete) {
|
|
761
|
-
try {
|
|
762
|
-
await input.hooks.onTurnComplete({ identity, finalText });
|
|
763
|
-
} catch (err) {
|
|
764
|
-
log("[chat-engine] onTurnComplete threw", {
|
|
765
|
-
error: err instanceof Error ? err.message : String(err)
|
|
766
|
-
});
|
|
767
|
-
}
|
|
768
|
-
}
|
|
769
|
-
}
|
|
770
|
-
await emit2({
|
|
771
|
-
type: "session.run.completed",
|
|
772
|
-
data: { sessionId: identity.sessionId, replayed: turn.replayed() }
|
|
773
|
-
});
|
|
562
|
+
await hooks.persistAssistantMessage({ identity, finalText });
|
|
774
563
|
} catch (err) {
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
log("[chat-engine] turn failed", { error: message });
|
|
778
|
-
await emit2({ type: "error", data: { message } });
|
|
779
|
-
await emit2({
|
|
780
|
-
type: "session.run.failed",
|
|
781
|
-
data: { sessionId: identity.sessionId, message }
|
|
564
|
+
log("[chat-engine] persistAssistantMessage threw", {
|
|
565
|
+
error: err instanceof Error ? err.message : String(err)
|
|
782
566
|
});
|
|
783
|
-
} finally {
|
|
784
|
-
if (input.hooks.traceFlush) {
|
|
785
|
-
const flush = input.hooks.traceFlush().catch(
|
|
786
|
-
(err) => log("[chat-engine] traceFlush threw", {
|
|
787
|
-
error: err instanceof Error ? err.message : String(err)
|
|
788
|
-
})
|
|
789
|
-
);
|
|
790
|
-
if (input.waitUntil) input.waitUntil(flush);
|
|
791
|
-
else await flush;
|
|
792
|
-
}
|
|
793
|
-
controller.close();
|
|
794
|
-
void turnFailed;
|
|
795
567
|
}
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
var DurableRunError = class extends Error {
|
|
805
|
-
constructor(message, code) {
|
|
806
|
-
super(message);
|
|
807
|
-
this.code = code;
|
|
808
|
-
this.name = this.constructor.name;
|
|
809
|
-
}
|
|
810
|
-
code;
|
|
811
|
-
};
|
|
812
|
-
var DurableRunLeaseHeldError = class extends DurableRunError {
|
|
813
|
-
constructor(message) {
|
|
814
|
-
super(message, "lease_held");
|
|
815
|
-
}
|
|
816
|
-
};
|
|
817
|
-
var DurableRunInputMismatchError = class extends DurableRunError {
|
|
818
|
-
constructor(message) {
|
|
819
|
-
super(message, "manifest_mismatch");
|
|
820
|
-
}
|
|
821
|
-
};
|
|
822
|
-
var DurableRunDivergenceError = class extends DurableRunError {
|
|
823
|
-
constructor(message) {
|
|
824
|
-
super(message, "step_divergence");
|
|
825
|
-
}
|
|
826
|
-
};
|
|
827
|
-
var DurableAwaitEventTimeoutError = class extends DurableRunError {
|
|
828
|
-
constructor(message) {
|
|
829
|
-
super(message, "await_event_timeout");
|
|
830
|
-
}
|
|
831
|
-
};
|
|
832
|
-
|
|
833
|
-
// src/durable/d1-store.ts
|
|
834
|
-
var DEFAULT_LEASE_MS = 3e4;
|
|
835
|
-
var D1DurableRunStore = class {
|
|
836
|
-
constructor(db) {
|
|
837
|
-
this.db = db;
|
|
838
|
-
}
|
|
839
|
-
db;
|
|
840
|
-
/** Override for tests — defaults to Date.now(). */
|
|
841
|
-
now = () => Date.now();
|
|
842
|
-
async startOrResume(input) {
|
|
843
|
-
const leaseMs = input.leaseMs ?? DEFAULT_LEASE_MS;
|
|
844
|
-
const hash = manifestHash(input.manifest);
|
|
845
|
-
const nowMs = this.now();
|
|
846
|
-
const nowIso2 = new Date(nowMs).toISOString();
|
|
847
|
-
const leaseExpiresAt = new Date(nowMs + leaseMs).toISOString();
|
|
848
|
-
const existing = await this.db.prepare("SELECT * FROM durable_runs WHERE run_id = ?").bind(input.runId).first();
|
|
849
|
-
if (!existing) {
|
|
850
|
-
await this.db.prepare(
|
|
851
|
-
`INSERT INTO durable_runs
|
|
852
|
-
(run_id, manifest_hash, project_id, scenario_id, status,
|
|
853
|
-
created_at, updated_at, lease_holder_id, lease_expires_at, step_count)
|
|
854
|
-
VALUES (?, ?, ?, ?, 'running', ?, ?, ?, ?, 0)`
|
|
855
|
-
).bind(
|
|
856
|
-
input.runId,
|
|
857
|
-
hash,
|
|
858
|
-
input.manifest.projectId,
|
|
859
|
-
input.manifest.scenarioId ?? null,
|
|
860
|
-
nowIso2,
|
|
861
|
-
nowIso2,
|
|
862
|
-
input.workerId,
|
|
863
|
-
leaseExpiresAt
|
|
864
|
-
).run();
|
|
865
|
-
const record2 = {
|
|
866
|
-
runId: input.runId,
|
|
867
|
-
manifestHash: hash,
|
|
868
|
-
projectId: input.manifest.projectId,
|
|
869
|
-
scenarioId: input.manifest.scenarioId,
|
|
870
|
-
status: "running",
|
|
871
|
-
createdAt: nowIso2,
|
|
872
|
-
updatedAt: nowIso2,
|
|
873
|
-
leaseHolderId: input.workerId,
|
|
874
|
-
leaseExpiresAt,
|
|
875
|
-
stepCount: 0
|
|
876
|
-
};
|
|
877
|
-
return { run: record2, completedSteps: [], leaseExpiresAt };
|
|
878
|
-
}
|
|
879
|
-
if (existing.manifest_hash !== hash) {
|
|
880
|
-
throw new DurableRunInputMismatchError(
|
|
881
|
-
`runId ${input.runId} exists with a different manifest hash; refuse to corrupt prior steps`
|
|
882
|
-
);
|
|
883
|
-
}
|
|
884
|
-
const claim = await this.db.prepare(
|
|
885
|
-
`UPDATE durable_runs
|
|
886
|
-
SET lease_holder_id = ?,
|
|
887
|
-
lease_expires_at = ?,
|
|
888
|
-
updated_at = ?,
|
|
889
|
-
status = CASE WHEN status IN ('completed','failed') THEN status ELSE 'running' END
|
|
890
|
-
WHERE run_id = ?
|
|
891
|
-
AND (
|
|
892
|
-
lease_holder_id = ? OR
|
|
893
|
-
lease_holder_id IS NULL OR
|
|
894
|
-
lease_expires_at IS NULL OR
|
|
895
|
-
lease_expires_at < ?
|
|
896
|
-
)`
|
|
897
|
-
).bind(input.workerId, leaseExpiresAt, nowIso2, input.runId, input.workerId, nowIso2).run();
|
|
898
|
-
const changes = claim.meta?.changes ?? 0;
|
|
899
|
-
if (changes === 0) {
|
|
900
|
-
throw new DurableRunLeaseHeldError(
|
|
901
|
-
`runId ${input.runId} leased by ${existing.lease_holder_id} until ${existing.lease_expires_at}`
|
|
902
|
-
);
|
|
903
|
-
}
|
|
904
|
-
const completedSteps = await this.readSteps(input.runId, "completed");
|
|
905
|
-
const record = rowToRunRecord({
|
|
906
|
-
...existing,
|
|
907
|
-
lease_holder_id: input.workerId,
|
|
908
|
-
lease_expires_at: leaseExpiresAt,
|
|
909
|
-
updated_at: nowIso2,
|
|
910
|
-
status: existing.status === "completed" || existing.status === "failed" ? existing.status : "running"
|
|
911
|
-
});
|
|
912
|
-
return { run: record, completedSteps, leaseExpiresAt };
|
|
913
|
-
}
|
|
914
|
-
async renewLease(input) {
|
|
915
|
-
const nowMs = this.now();
|
|
916
|
-
const nowIso2 = new Date(nowMs).toISOString();
|
|
917
|
-
const leaseExpiresAt = new Date(nowMs + (input.leaseMs ?? DEFAULT_LEASE_MS)).toISOString();
|
|
918
|
-
const res = await this.db.prepare(
|
|
919
|
-
`UPDATE durable_runs
|
|
920
|
-
SET lease_expires_at = ?, updated_at = ?
|
|
921
|
-
WHERE run_id = ?
|
|
922
|
-
AND (lease_holder_id = ? OR lease_expires_at IS NULL OR lease_expires_at < ?)`
|
|
923
|
-
).bind(leaseExpiresAt, nowIso2, input.runId, input.workerId, nowIso2).run();
|
|
924
|
-
const ok = (res.meta?.changes ?? 0) > 0;
|
|
925
|
-
return ok ? { ok: true, leaseExpiresAt } : { ok: false };
|
|
926
|
-
}
|
|
927
|
-
async loadStep(runId, stepIndex) {
|
|
928
|
-
const row = await this.db.prepare("SELECT * FROM durable_steps WHERE run_id = ? AND step_index = ?").bind(runId, stepIndex).first();
|
|
929
|
-
return row ? rowToStepRecord(row) : void 0;
|
|
930
|
-
}
|
|
931
|
-
async beginStep(input) {
|
|
932
|
-
const nowIso2 = new Date(this.now()).toISOString();
|
|
933
|
-
const prior = await this.loadStep(input.runId, input.stepIndex);
|
|
934
|
-
if (prior) {
|
|
935
|
-
if (prior.intent !== input.intent) {
|
|
936
|
-
throw new DurableRunDivergenceError(
|
|
937
|
-
`step ${input.stepIndex}: intent changed ('${prior.intent}' -> '${input.intent}')`
|
|
938
|
-
);
|
|
939
|
-
}
|
|
940
|
-
await this.db.prepare(
|
|
941
|
-
`UPDATE durable_steps
|
|
942
|
-
SET status='running', attempts = attempts + 1, started_at = ?, error_json = NULL
|
|
943
|
-
WHERE run_id = ? AND step_index = ?`
|
|
944
|
-
).bind(nowIso2, input.runId, input.stepIndex).run();
|
|
945
|
-
await this.bumpUpdated(input.runId, nowIso2);
|
|
946
|
-
return {
|
|
947
|
-
...prior,
|
|
948
|
-
attempts: prior.attempts + 1,
|
|
949
|
-
status: "running",
|
|
950
|
-
startedAt: nowIso2,
|
|
951
|
-
error: void 0
|
|
952
|
-
};
|
|
953
|
-
}
|
|
954
|
-
await this.db.prepare(
|
|
955
|
-
`INSERT INTO durable_steps
|
|
956
|
-
(run_id, step_index, intent, kind, input_hash, status, attempts, started_at)
|
|
957
|
-
VALUES (?, ?, ?, ?, ?, 'running', 1, ?)`
|
|
958
|
-
).bind(input.runId, input.stepIndex, input.intent, input.kind, input.inputHash, nowIso2).run();
|
|
959
|
-
await this.db.prepare(
|
|
960
|
-
`UPDATE durable_runs
|
|
961
|
-
SET step_count = MAX(step_count, ?), updated_at = ?
|
|
962
|
-
WHERE run_id = ?`
|
|
963
|
-
).bind(input.stepIndex + 1, nowIso2, input.runId).run();
|
|
964
|
-
return {
|
|
965
|
-
runId: input.runId,
|
|
966
|
-
stepIndex: input.stepIndex,
|
|
967
|
-
intent: input.intent,
|
|
968
|
-
kind: input.kind,
|
|
969
|
-
inputHash: input.inputHash,
|
|
970
|
-
status: "running",
|
|
971
|
-
attempts: 1,
|
|
972
|
-
startedAt: nowIso2
|
|
973
|
-
};
|
|
974
|
-
}
|
|
975
|
-
async completeStep(input) {
|
|
976
|
-
const nowIso2 = new Date(this.now()).toISOString();
|
|
977
|
-
await this.db.prepare(
|
|
978
|
-
`UPDATE durable_steps
|
|
979
|
-
SET status='completed', result_json = ?, completed_at = ?, error_json = NULL
|
|
980
|
-
WHERE run_id = ? AND step_index = ?`
|
|
981
|
-
).bind(JSON.stringify(input.result ?? null), nowIso2, input.runId, input.stepIndex).run();
|
|
982
|
-
await this.bumpUpdated(input.runId, nowIso2);
|
|
983
|
-
const row = await this.loadStep(input.runId, input.stepIndex);
|
|
984
|
-
if (!row) {
|
|
985
|
-
throw new Error(`durable-runs: completeStep cannot find step ${input.stepIndex}`);
|
|
986
|
-
}
|
|
987
|
-
return row;
|
|
988
|
-
}
|
|
989
|
-
async failStep(input) {
|
|
990
|
-
const nowIso2 = new Date(this.now()).toISOString();
|
|
991
|
-
await this.db.prepare(
|
|
992
|
-
`UPDATE durable_steps
|
|
993
|
-
SET status='failed', error_json = ?, completed_at = ?
|
|
994
|
-
WHERE run_id = ? AND step_index = ?`
|
|
995
|
-
).bind(JSON.stringify(input.error), nowIso2, input.runId, input.stepIndex).run();
|
|
996
|
-
await this.bumpUpdated(input.runId, nowIso2);
|
|
997
|
-
const row = await this.loadStep(input.runId, input.stepIndex);
|
|
998
|
-
if (!row) {
|
|
999
|
-
throw new Error(`durable-runs: failStep cannot find step ${input.stepIndex}`);
|
|
1000
|
-
}
|
|
1001
|
-
return row;
|
|
1002
|
-
}
|
|
1003
|
-
async endRun(input) {
|
|
1004
|
-
const nowIso2 = new Date(this.now()).toISOString();
|
|
1005
|
-
await this.db.prepare(
|
|
1006
|
-
`UPDATE durable_runs
|
|
1007
|
-
SET status = ?, completed_at = ?, updated_at = ?,
|
|
1008
|
-
outcome_json = ?,
|
|
1009
|
-
lease_holder_id = CASE WHEN lease_holder_id = ? THEN NULL ELSE lease_holder_id END,
|
|
1010
|
-
lease_expires_at = CASE WHEN lease_holder_id = ? THEN NULL ELSE lease_expires_at END
|
|
1011
|
-
WHERE run_id = ?`
|
|
1012
|
-
).bind(
|
|
1013
|
-
input.status,
|
|
1014
|
-
nowIso2,
|
|
1015
|
-
nowIso2,
|
|
1016
|
-
input.outcome ? JSON.stringify(input.outcome) : null,
|
|
1017
|
-
input.workerId,
|
|
1018
|
-
input.workerId,
|
|
1019
|
-
input.runId
|
|
1020
|
-
).run();
|
|
1021
|
-
const row = await this.db.prepare("SELECT * FROM durable_runs WHERE run_id = ?").bind(input.runId).first();
|
|
1022
|
-
if (!row) throw new Error(`durable-runs: endRun cannot find run ${input.runId}`);
|
|
1023
|
-
return rowToRunRecord(row);
|
|
1024
|
-
}
|
|
1025
|
-
async emitEvent(input) {
|
|
1026
|
-
const nowIso2 = new Date(this.now()).toISOString();
|
|
1027
|
-
const res = await this.db.prepare(
|
|
1028
|
-
`INSERT OR IGNORE INTO durable_events (run_id, key, payload_json, emitted_at)
|
|
1029
|
-
VALUES (?, ?, ?, ?)`
|
|
1030
|
-
).bind(input.runId, input.key, JSON.stringify(input.payload ?? null), nowIso2).run();
|
|
1031
|
-
const accepted = (res.meta?.changes ?? 0) > 0;
|
|
1032
|
-
const row = await this.db.prepare("SELECT * FROM durable_events WHERE run_id = ? AND key = ?").bind(input.runId, input.key).first();
|
|
1033
|
-
if (!row) throw new Error("durable-runs: emitEvent failed to persist or read back");
|
|
1034
|
-
return {
|
|
1035
|
-
accepted,
|
|
1036
|
-
record: rowToEventRecord(row)
|
|
1037
|
-
};
|
|
1038
|
-
}
|
|
1039
|
-
async loadEvent(runId, key) {
|
|
1040
|
-
const row = await this.db.prepare("SELECT * FROM durable_events WHERE run_id = ? AND key = ?").bind(runId, key).first();
|
|
1041
|
-
return row ? rowToEventRecord(row) : void 0;
|
|
1042
|
-
}
|
|
1043
|
-
async appendStreamEvent(input) {
|
|
1044
|
-
const nowIso2 = new Date(this.now()).toISOString();
|
|
1045
|
-
const res = await this.db.prepare(
|
|
1046
|
-
`INSERT OR IGNORE INTO durable_stream_events (run_id, seq, event_id, payload_json, appended_at)
|
|
1047
|
-
VALUES (
|
|
1048
|
-
?,
|
|
1049
|
-
(SELECT COALESCE(MAX(seq), -1) + 1 FROM durable_stream_events WHERE run_id = ?),
|
|
1050
|
-
?, ?, ?
|
|
1051
|
-
)`
|
|
1052
|
-
).bind(input.runId, input.runId, input.eventId, JSON.stringify(input.payload ?? null), nowIso2).run();
|
|
1053
|
-
const accepted = (res.meta?.changes ?? 0) > 0;
|
|
1054
|
-
const row = await this.db.prepare("SELECT * FROM durable_stream_events WHERE run_id = ? AND event_id = ?").bind(input.runId, input.eventId).first();
|
|
1055
|
-
if (!row) throw new Error("durable-runs: appendStreamEvent failed to persist or read back");
|
|
1056
|
-
return { accepted, record: rowToStreamEventRecord(row) };
|
|
1057
|
-
}
|
|
1058
|
-
async readStreamEvents(runId, afterSeq) {
|
|
1059
|
-
const { results } = await this.db.prepare("SELECT * FROM durable_stream_events WHERE run_id = ? AND seq > ? ORDER BY seq").bind(runId, afterSeq ?? -1).all();
|
|
1060
|
-
return results.map(rowToStreamEventRecord);
|
|
1061
|
-
}
|
|
1062
|
-
async setRunHandle(input) {
|
|
1063
|
-
const nowIso2 = new Date(this.now()).toISOString();
|
|
1064
|
-
await this.db.prepare("UPDATE durable_runs SET handle_json = ?, updated_at = ? WHERE run_id = ?").bind(JSON.stringify(input.handle), nowIso2, input.runId).run();
|
|
1065
|
-
}
|
|
1066
|
-
async close() {
|
|
1067
|
-
}
|
|
1068
|
-
/** Inspect the currently-applied schema version. */
|
|
1069
|
-
async getSchemaVersion() {
|
|
1070
|
-
const row = await this.db.prepare("SELECT MAX(version) AS version FROM durable_schema_info").first();
|
|
1071
|
-
return row?.version ?? void 0;
|
|
1072
|
-
}
|
|
1073
|
-
// ── internals ──────────────────────────────────────────────────────
|
|
1074
|
-
async readSteps(runId, status) {
|
|
1075
|
-
const { results } = await this.db.prepare("SELECT * FROM durable_steps WHERE run_id = ? AND status = ? ORDER BY step_index").bind(runId, status).all();
|
|
1076
|
-
return results.map(rowToStepRecord);
|
|
1077
|
-
}
|
|
1078
|
-
async bumpUpdated(runId, nowIso2) {
|
|
1079
|
-
await this.db.prepare("UPDATE durable_runs SET updated_at = ? WHERE run_id = ?").bind(nowIso2, runId).run();
|
|
1080
|
-
}
|
|
1081
|
-
};
|
|
1082
|
-
function rowToRunRecord(row) {
|
|
1083
|
-
return {
|
|
1084
|
-
runId: row.run_id,
|
|
1085
|
-
manifestHash: row.manifest_hash,
|
|
1086
|
-
projectId: row.project_id,
|
|
1087
|
-
scenarioId: row.scenario_id ?? void 0,
|
|
1088
|
-
status: row.status,
|
|
1089
|
-
createdAt: row.created_at,
|
|
1090
|
-
updatedAt: row.updated_at,
|
|
1091
|
-
completedAt: row.completed_at ?? void 0,
|
|
1092
|
-
leaseHolderId: row.lease_holder_id ?? void 0,
|
|
1093
|
-
leaseExpiresAt: row.lease_expires_at ?? void 0,
|
|
1094
|
-
outcome: row.outcome_json ? JSON.parse(row.outcome_json) : void 0,
|
|
1095
|
-
stepCount: row.step_count,
|
|
1096
|
-
handle: row.handle_json ? JSON.parse(row.handle_json) : void 0
|
|
1097
|
-
};
|
|
1098
|
-
}
|
|
1099
|
-
function rowToStreamEventRecord(row) {
|
|
1100
|
-
return {
|
|
1101
|
-
runId: row.run_id,
|
|
1102
|
-
seq: row.seq,
|
|
1103
|
-
eventId: row.event_id,
|
|
1104
|
-
payload: row.payload_json ? JSON.parse(row.payload_json) : null,
|
|
1105
|
-
appendedAt: row.appended_at
|
|
1106
|
-
};
|
|
1107
|
-
}
|
|
1108
|
-
function rowToStepRecord(row) {
|
|
1109
|
-
return {
|
|
1110
|
-
runId: row.run_id,
|
|
1111
|
-
stepIndex: row.step_index,
|
|
1112
|
-
intent: row.intent,
|
|
1113
|
-
kind: row.kind,
|
|
1114
|
-
inputHash: row.input_hash,
|
|
1115
|
-
status: row.status,
|
|
1116
|
-
attempts: row.attempts,
|
|
1117
|
-
result: row.result_json ? JSON.parse(row.result_json) : void 0,
|
|
1118
|
-
error: row.error_json ? JSON.parse(row.error_json) : void 0,
|
|
1119
|
-
startedAt: row.started_at ?? void 0,
|
|
1120
|
-
completedAt: row.completed_at ?? void 0
|
|
1121
|
-
};
|
|
1122
|
-
}
|
|
1123
|
-
function rowToEventRecord(row) {
|
|
1124
|
-
return {
|
|
1125
|
-
runId: row.run_id,
|
|
1126
|
-
key: row.key,
|
|
1127
|
-
payload: row.payload_json ? JSON.parse(row.payload_json) : null,
|
|
1128
|
-
emittedAt: row.emitted_at
|
|
1129
|
-
};
|
|
1130
|
-
}
|
|
1131
|
-
|
|
1132
|
-
// src/durable/file-system-store.ts
|
|
1133
|
-
import { existsSync, mkdirSync } from "fs";
|
|
1134
|
-
import { appendFile, readdir, readFile, rename, writeFile } from "fs/promises";
|
|
1135
|
-
import { join } from "path";
|
|
1136
|
-
var DEFAULT_LEASE_MS2 = 3e4;
|
|
1137
|
-
var FileSystemDurableRunStore = class {
|
|
1138
|
-
constructor(root) {
|
|
1139
|
-
this.root = root;
|
|
1140
|
-
mkdirSync(root, { recursive: true });
|
|
1141
|
-
}
|
|
1142
|
-
root;
|
|
1143
|
-
/** Override for tests — defaults to Date.now(). */
|
|
1144
|
-
now = () => Date.now();
|
|
1145
|
-
async startOrResume(input) {
|
|
1146
|
-
const leaseMs = input.leaseMs ?? DEFAULT_LEASE_MS2;
|
|
1147
|
-
const hash = manifestHash(input.manifest);
|
|
1148
|
-
const nowMs = this.now();
|
|
1149
|
-
const nowIso2 = new Date(nowMs).toISOString();
|
|
1150
|
-
const leaseExpiresAt = new Date(nowMs + leaseMs).toISOString();
|
|
1151
|
-
const dir = this.runDir(input.runId);
|
|
1152
|
-
if (!existsSync(dir)) {
|
|
1153
|
-
mkdirSync(dir, { recursive: true });
|
|
1154
|
-
const record2 = {
|
|
1155
|
-
runId: input.runId,
|
|
1156
|
-
manifestHash: hash,
|
|
1157
|
-
projectId: input.manifest.projectId,
|
|
1158
|
-
scenarioId: input.manifest.scenarioId,
|
|
1159
|
-
status: "running",
|
|
1160
|
-
createdAt: nowIso2,
|
|
1161
|
-
updatedAt: nowIso2,
|
|
1162
|
-
leaseHolderId: input.workerId,
|
|
1163
|
-
leaseExpiresAt,
|
|
1164
|
-
stepCount: 0
|
|
1165
|
-
};
|
|
1166
|
-
await this.writeRun(record2);
|
|
1167
|
-
await this.writeLease(input.runId, { workerId: input.workerId, leaseExpiresAt });
|
|
1168
|
-
await appendFile(join(dir, "steps.jsonl"), "", "utf8");
|
|
1169
|
-
await appendFile(join(dir, "events.jsonl"), "", "utf8");
|
|
1170
|
-
await appendFile(join(dir, "stream-events.jsonl"), "", "utf8");
|
|
1171
|
-
return { run: record2, completedSteps: [], leaseExpiresAt };
|
|
1172
|
-
}
|
|
1173
|
-
const record = await this.readRun(input.runId);
|
|
1174
|
-
if (record.manifestHash !== hash) {
|
|
1175
|
-
throw new DurableRunInputMismatchError(
|
|
1176
|
-
`runId ${input.runId} exists with a different manifest hash; refuse to corrupt prior steps`
|
|
1177
|
-
);
|
|
1178
|
-
}
|
|
1179
|
-
const lease = await this.readLeaseSafe(input.runId);
|
|
1180
|
-
const leaseStillLive = lease && lease.workerId !== input.workerId && new Date(lease.leaseExpiresAt).getTime() > nowMs;
|
|
1181
|
-
if (leaseStillLive) {
|
|
1182
|
-
throw new DurableRunLeaseHeldError(
|
|
1183
|
-
`runId ${input.runId} leased by ${lease.workerId} until ${lease.leaseExpiresAt}`
|
|
1184
|
-
);
|
|
1185
|
-
}
|
|
1186
|
-
const completedSteps = await this.readSteps(input.runId);
|
|
1187
|
-
const nextRecord = {
|
|
1188
|
-
...record,
|
|
1189
|
-
status: record.status === "completed" || record.status === "failed" ? record.status : "running",
|
|
1190
|
-
updatedAt: nowIso2,
|
|
1191
|
-
leaseHolderId: input.workerId,
|
|
1192
|
-
leaseExpiresAt
|
|
1193
|
-
};
|
|
1194
|
-
await this.writeRun(nextRecord);
|
|
1195
|
-
await this.writeLease(input.runId, { workerId: input.workerId, leaseExpiresAt });
|
|
1196
|
-
return { run: nextRecord, completedSteps, leaseExpiresAt };
|
|
1197
|
-
}
|
|
1198
|
-
async renewLease(input) {
|
|
1199
|
-
const lease = await this.readLeaseSafe(input.runId);
|
|
1200
|
-
const nowMs = this.now();
|
|
1201
|
-
if (lease && lease.workerId !== input.workerId) {
|
|
1202
|
-
if (new Date(lease.leaseExpiresAt).getTime() > nowMs) {
|
|
1203
|
-
return { ok: false };
|
|
1204
|
-
}
|
|
1205
|
-
}
|
|
1206
|
-
const leaseExpiresAt = new Date(nowMs + (input.leaseMs ?? DEFAULT_LEASE_MS2)).toISOString();
|
|
1207
|
-
await this.writeLease(input.runId, { workerId: input.workerId, leaseExpiresAt });
|
|
1208
|
-
return { ok: true, leaseExpiresAt };
|
|
1209
|
-
}
|
|
1210
|
-
async loadStep(runId, stepIndex) {
|
|
1211
|
-
const steps = await this.readSteps(runId, { includeFailed: true, includeRunning: true });
|
|
1212
|
-
return steps.find((s) => s.stepIndex === stepIndex);
|
|
1213
|
-
}
|
|
1214
|
-
async beginStep(input) {
|
|
1215
|
-
const all = await this.readSteps(input.runId, { includeFailed: true, includeRunning: true });
|
|
1216
|
-
const prior = all.find((s) => s.stepIndex === input.stepIndex);
|
|
1217
|
-
const nowIso2 = new Date(this.now()).toISOString();
|
|
1218
|
-
if (prior) {
|
|
1219
|
-
if (prior.intent !== input.intent) {
|
|
1220
|
-
throw new DurableRunDivergenceError(
|
|
1221
|
-
`step ${input.stepIndex}: intent changed ('${prior.intent}' -> '${input.intent}')`
|
|
1222
|
-
);
|
|
1223
|
-
}
|
|
1224
|
-
const rec2 = {
|
|
1225
|
-
...prior,
|
|
1226
|
-
attempts: prior.attempts + 1,
|
|
1227
|
-
status: "running",
|
|
1228
|
-
startedAt: nowIso2,
|
|
1229
|
-
error: void 0
|
|
1230
|
-
};
|
|
1231
|
-
await this.appendStep(input.runId, rec2);
|
|
1232
|
-
await this.bumpRunUpdated(input.runId, nowIso2);
|
|
1233
|
-
return rec2;
|
|
1234
|
-
}
|
|
1235
|
-
const rec = {
|
|
1236
|
-
runId: input.runId,
|
|
1237
|
-
stepIndex: input.stepIndex,
|
|
1238
|
-
intent: input.intent,
|
|
1239
|
-
kind: input.kind,
|
|
1240
|
-
inputHash: input.inputHash,
|
|
1241
|
-
status: "running",
|
|
1242
|
-
attempts: 1,
|
|
1243
|
-
startedAt: nowIso2
|
|
1244
|
-
};
|
|
1245
|
-
await this.appendStep(input.runId, rec);
|
|
1246
|
-
const record = await this.readRun(input.runId);
|
|
1247
|
-
record.stepCount = Math.max(record.stepCount, input.stepIndex + 1);
|
|
1248
|
-
record.updatedAt = nowIso2;
|
|
1249
|
-
await this.writeRun(record);
|
|
1250
|
-
return rec;
|
|
1251
|
-
}
|
|
1252
|
-
async completeStep(input) {
|
|
1253
|
-
const all = await this.readSteps(input.runId, { includeFailed: true, includeRunning: true });
|
|
1254
|
-
const prior = all.find((s) => s.stepIndex === input.stepIndex);
|
|
1255
|
-
if (!prior) {
|
|
1256
|
-
throw new Error(
|
|
1257
|
-
`durable-runs: completeStep called before beginStep (step ${input.stepIndex})`
|
|
1258
|
-
);
|
|
1259
|
-
}
|
|
1260
|
-
const nowIso2 = new Date(this.now()).toISOString();
|
|
1261
|
-
const rec = {
|
|
1262
|
-
...prior,
|
|
1263
|
-
status: "completed",
|
|
1264
|
-
result: input.result,
|
|
1265
|
-
completedAt: nowIso2,
|
|
1266
|
-
error: void 0
|
|
1267
|
-
};
|
|
1268
|
-
await this.appendStep(input.runId, rec);
|
|
1269
|
-
await this.bumpRunUpdated(input.runId, nowIso2);
|
|
1270
|
-
return rec;
|
|
1271
|
-
}
|
|
1272
|
-
async failStep(input) {
|
|
1273
|
-
const all = await this.readSteps(input.runId, { includeFailed: true, includeRunning: true });
|
|
1274
|
-
const prior = all.find((s) => s.stepIndex === input.stepIndex);
|
|
1275
|
-
if (!prior) {
|
|
1276
|
-
throw new Error(`durable-runs: failStep called before beginStep (step ${input.stepIndex})`);
|
|
1277
|
-
}
|
|
1278
|
-
const nowIso2 = new Date(this.now()).toISOString();
|
|
1279
|
-
const rec = {
|
|
1280
|
-
...prior,
|
|
1281
|
-
status: "failed",
|
|
1282
|
-
error: input.error,
|
|
1283
|
-
completedAt: nowIso2
|
|
1284
|
-
};
|
|
1285
|
-
await this.appendStep(input.runId, rec);
|
|
1286
|
-
await this.bumpRunUpdated(input.runId, nowIso2);
|
|
1287
|
-
return rec;
|
|
1288
|
-
}
|
|
1289
|
-
async endRun(input) {
|
|
1290
|
-
const record = await this.readRun(input.runId);
|
|
1291
|
-
const nowIso2 = new Date(this.now()).toISOString();
|
|
1292
|
-
record.status = input.status;
|
|
1293
|
-
record.outcome = input.outcome;
|
|
1294
|
-
record.completedAt = nowIso2;
|
|
1295
|
-
record.updatedAt = nowIso2;
|
|
1296
|
-
const lease = await this.readLeaseSafe(input.runId);
|
|
1297
|
-
if (lease && lease.workerId === input.workerId) {
|
|
1298
|
-
record.leaseHolderId = void 0;
|
|
1299
|
-
record.leaseExpiresAt = void 0;
|
|
1300
|
-
await this.writeLease(input.runId, null);
|
|
1301
|
-
}
|
|
1302
|
-
await this.writeRun(record);
|
|
1303
|
-
return record;
|
|
1304
|
-
}
|
|
1305
|
-
async emitEvent(input) {
|
|
1306
|
-
const existing = await this.loadEvent(input.runId, input.key);
|
|
1307
|
-
if (existing) return { accepted: false, record: existing };
|
|
1308
|
-
const rec = {
|
|
1309
|
-
runId: input.runId,
|
|
1310
|
-
key: input.key,
|
|
1311
|
-
payload: input.payload,
|
|
1312
|
-
emittedAt: new Date(this.now()).toISOString()
|
|
1313
|
-
};
|
|
1314
|
-
await appendFile(
|
|
1315
|
-
join(this.runDir(input.runId), "events.jsonl"),
|
|
1316
|
-
`${JSON.stringify(rec)}
|
|
1317
|
-
`,
|
|
1318
|
-
"utf8"
|
|
1319
|
-
);
|
|
1320
|
-
return { accepted: true, record: rec };
|
|
1321
|
-
}
|
|
1322
|
-
async loadEvent(runId, key) {
|
|
1323
|
-
const path = join(this.runDir(runId), "events.jsonl");
|
|
1324
|
-
if (!existsSync(path)) return void 0;
|
|
1325
|
-
const content = await readFile(path, "utf8");
|
|
1326
|
-
for (const line of content.split("\n").reverse()) {
|
|
1327
|
-
if (!line) continue;
|
|
1328
|
-
const rec = JSON.parse(line);
|
|
1329
|
-
if (rec.key === key) return rec;
|
|
1330
|
-
}
|
|
1331
|
-
return void 0;
|
|
1332
|
-
}
|
|
1333
|
-
async appendStreamEvent(input) {
|
|
1334
|
-
const existing = await this.readStreamEventsRaw(input.runId);
|
|
1335
|
-
const dup = existing.find((e) => e.eventId === input.eventId);
|
|
1336
|
-
if (dup) return { accepted: false, record: dup };
|
|
1337
|
-
const rec = {
|
|
1338
|
-
runId: input.runId,
|
|
1339
|
-
seq: existing.length,
|
|
1340
|
-
eventId: input.eventId,
|
|
1341
|
-
payload: input.payload,
|
|
1342
|
-
appendedAt: new Date(this.now()).toISOString()
|
|
1343
|
-
};
|
|
1344
|
-
await appendFile(
|
|
1345
|
-
join(this.runDir(input.runId), "stream-events.jsonl"),
|
|
1346
|
-
`${JSON.stringify(rec)}
|
|
1347
|
-
`,
|
|
1348
|
-
"utf8"
|
|
1349
|
-
);
|
|
1350
|
-
return { accepted: true, record: rec };
|
|
1351
|
-
}
|
|
1352
|
-
async readStreamEvents(runId, afterSeq) {
|
|
1353
|
-
const cutoff = afterSeq ?? -1;
|
|
1354
|
-
return (await this.readStreamEventsRaw(runId)).filter((e) => e.seq > cutoff);
|
|
1355
|
-
}
|
|
1356
|
-
async setRunHandle(input) {
|
|
1357
|
-
const record = await this.readRun(input.runId);
|
|
1358
|
-
record.handle = input.handle;
|
|
1359
|
-
record.updatedAt = new Date(this.now()).toISOString();
|
|
1360
|
-
await this.writeRun(record);
|
|
1361
|
-
}
|
|
1362
|
-
async close() {
|
|
1363
|
-
}
|
|
1364
|
-
async readStreamEventsRaw(runId) {
|
|
1365
|
-
const path = join(this.runDir(runId), "stream-events.jsonl");
|
|
1366
|
-
if (!existsSync(path)) return [];
|
|
1367
|
-
const content = await readFile(path, "utf8");
|
|
1368
|
-
const out = [];
|
|
1369
|
-
for (const line of content.split("\n")) {
|
|
1370
|
-
if (!line) continue;
|
|
1371
|
-
out.push(JSON.parse(line));
|
|
1372
|
-
}
|
|
1373
|
-
return out.sort((a, b) => a.seq - b.seq);
|
|
1374
|
-
}
|
|
1375
|
-
/** @internal — used by tests to list runs in the store. */
|
|
1376
|
-
async _listRunIds() {
|
|
1377
|
-
if (!existsSync(this.root)) return [];
|
|
1378
|
-
const entries = await readdir(this.root, { withFileTypes: true });
|
|
1379
|
-
return entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
1380
|
-
}
|
|
1381
|
-
// ── internals ──────────────────────────────────────────────────────
|
|
1382
|
-
runDir(runId) {
|
|
1383
|
-
return join(this.root, runId);
|
|
1384
|
-
}
|
|
1385
|
-
async readRun(runId) {
|
|
1386
|
-
const path = join(this.runDir(runId), "run.json");
|
|
1387
|
-
const content = await readFile(path, "utf8");
|
|
1388
|
-
return JSON.parse(content);
|
|
1389
|
-
}
|
|
1390
|
-
async writeRun(record) {
|
|
1391
|
-
const dir = this.runDir(record.runId);
|
|
1392
|
-
const path = join(dir, "run.json");
|
|
1393
|
-
const tmp = `${path}.tmp`;
|
|
1394
|
-
await writeFile(tmp, JSON.stringify(record, null, 2), "utf8");
|
|
1395
|
-
await rename(tmp, path);
|
|
1396
|
-
}
|
|
1397
|
-
async readLeaseSafe(runId) {
|
|
1398
|
-
const path = join(this.runDir(runId), "lease.json");
|
|
1399
|
-
if (!existsSync(path)) return void 0;
|
|
1400
|
-
try {
|
|
1401
|
-
const content = await readFile(path, "utf8");
|
|
1402
|
-
if (!content.trim()) return void 0;
|
|
1403
|
-
return JSON.parse(content);
|
|
1404
|
-
} catch {
|
|
1405
|
-
return void 0;
|
|
1406
|
-
}
|
|
1407
|
-
}
|
|
1408
|
-
async writeLease(runId, lease) {
|
|
1409
|
-
const path = join(this.runDir(runId), "lease.json");
|
|
1410
|
-
const tmp = `${path}.tmp`;
|
|
1411
|
-
await writeFile(tmp, lease ? JSON.stringify(lease) : "", "utf8");
|
|
1412
|
-
await rename(tmp, path);
|
|
1413
|
-
}
|
|
1414
|
-
async readSteps(runId, opts = {}) {
|
|
1415
|
-
const path = join(this.runDir(runId), "steps.jsonl");
|
|
1416
|
-
if (!existsSync(path)) return [];
|
|
1417
|
-
const content = await readFile(path, "utf8");
|
|
1418
|
-
const latest = /* @__PURE__ */ new Map();
|
|
1419
|
-
for (const line of content.split("\n")) {
|
|
1420
|
-
if (!line) continue;
|
|
1421
|
-
const rec = JSON.parse(line);
|
|
1422
|
-
latest.set(rec.stepIndex, rec);
|
|
1423
|
-
}
|
|
1424
|
-
const out = [...latest.values()].sort((a, b) => a.stepIndex - b.stepIndex);
|
|
1425
|
-
return out.filter((s) => {
|
|
1426
|
-
if (s.status === "completed") return true;
|
|
1427
|
-
if (s.status === "failed") return opts.includeFailed ?? false;
|
|
1428
|
-
if (s.status === "running") return opts.includeRunning ?? false;
|
|
1429
|
-
return false;
|
|
1430
|
-
});
|
|
1431
|
-
}
|
|
1432
|
-
async appendStep(runId, rec) {
|
|
1433
|
-
await appendFile(join(this.runDir(runId), "steps.jsonl"), `${JSON.stringify(rec)}
|
|
1434
|
-
`, "utf8");
|
|
1435
|
-
}
|
|
1436
|
-
async bumpRunUpdated(runId, nowIso2) {
|
|
1437
|
-
const record = await this.readRun(runId);
|
|
1438
|
-
record.updatedAt = nowIso2;
|
|
1439
|
-
await this.writeRun(record);
|
|
1440
|
-
}
|
|
1441
|
-
};
|
|
1442
|
-
|
|
1443
|
-
// src/durable/in-memory-store.ts
|
|
1444
|
-
var DEFAULT_LEASE_MS3 = 3e4;
|
|
1445
|
-
var InMemoryDurableRunStore = class {
|
|
1446
|
-
runs = /* @__PURE__ */ new Map();
|
|
1447
|
-
/** Override for tests — defaults to Date.now(). */
|
|
1448
|
-
now = () => Date.now();
|
|
1449
|
-
async startOrResume(input) {
|
|
1450
|
-
const leaseMs = input.leaseMs ?? DEFAULT_LEASE_MS3;
|
|
1451
|
-
const hash = manifestHash(input.manifest);
|
|
1452
|
-
const nowMs = this.now();
|
|
1453
|
-
const nowIso2 = new Date(nowMs).toISOString();
|
|
1454
|
-
const leaseExpiresMs = nowMs + leaseMs;
|
|
1455
|
-
const leaseExpiresAt = new Date(leaseExpiresMs).toISOString();
|
|
1456
|
-
let state = this.runs.get(input.runId);
|
|
1457
|
-
if (!state) {
|
|
1458
|
-
const record = {
|
|
1459
|
-
runId: input.runId,
|
|
1460
|
-
manifestHash: hash,
|
|
1461
|
-
projectId: input.manifest.projectId,
|
|
1462
|
-
scenarioId: input.manifest.scenarioId,
|
|
1463
|
-
status: "running",
|
|
1464
|
-
createdAt: nowIso2,
|
|
1465
|
-
updatedAt: nowIso2,
|
|
1466
|
-
leaseHolderId: input.workerId,
|
|
1467
|
-
leaseExpiresAt,
|
|
1468
|
-
stepCount: 0
|
|
1469
|
-
};
|
|
1470
|
-
state = { record, steps: /* @__PURE__ */ new Map(), events: /* @__PURE__ */ new Map(), streamEvents: [] };
|
|
1471
|
-
this.runs.set(input.runId, state);
|
|
1472
|
-
return { run: { ...record }, completedSteps: [], leaseExpiresAt };
|
|
1473
|
-
}
|
|
1474
|
-
if (state.record.manifestHash !== hash) {
|
|
1475
|
-
throw new DurableRunInputMismatchError(
|
|
1476
|
-
`runId ${input.runId} exists with a different manifest hash; refuse to corrupt prior steps`
|
|
1477
|
-
);
|
|
1478
|
-
}
|
|
1479
|
-
const leaseStillLive = state.record.leaseHolderId !== void 0 && state.record.leaseHolderId !== input.workerId && state.record.leaseExpiresAt !== void 0 && new Date(state.record.leaseExpiresAt).getTime() > nowMs;
|
|
1480
|
-
if (leaseStillLive) {
|
|
1481
|
-
throw new DurableRunLeaseHeldError(
|
|
1482
|
-
`runId ${input.runId} is leased by ${state.record.leaseHolderId} until ${state.record.leaseExpiresAt}`
|
|
1483
|
-
);
|
|
1484
|
-
}
|
|
1485
|
-
state.record.leaseHolderId = input.workerId;
|
|
1486
|
-
state.record.leaseExpiresAt = leaseExpiresAt;
|
|
1487
|
-
state.record.status = state.record.status === "completed" || state.record.status === "failed" ? state.record.status : "running";
|
|
1488
|
-
state.record.updatedAt = nowIso2;
|
|
1489
|
-
const completed = [...state.steps.values()].filter((s) => s.status === "completed").sort((a, b) => a.stepIndex - b.stepIndex);
|
|
1490
|
-
return {
|
|
1491
|
-
run: { ...state.record },
|
|
1492
|
-
completedSteps: completed.map((s) => ({ ...s })),
|
|
1493
|
-
leaseExpiresAt
|
|
1494
|
-
};
|
|
1495
|
-
}
|
|
1496
|
-
async renewLease(input) {
|
|
1497
|
-
const state = this.runs.get(input.runId);
|
|
1498
|
-
if (!state) return { ok: false };
|
|
1499
|
-
const nowMs = this.now();
|
|
1500
|
-
if (state.record.leaseHolderId !== input.workerId) {
|
|
1501
|
-
if (state.record.leaseExpiresAt && new Date(state.record.leaseExpiresAt).getTime() > nowMs) {
|
|
1502
|
-
return { ok: false };
|
|
1503
|
-
}
|
|
1504
|
-
}
|
|
1505
|
-
const leaseExpiresMs = nowMs + (input.leaseMs ?? DEFAULT_LEASE_MS3);
|
|
1506
|
-
const leaseExpiresAt = new Date(leaseExpiresMs).toISOString();
|
|
1507
|
-
state.record.leaseHolderId = input.workerId;
|
|
1508
|
-
state.record.leaseExpiresAt = leaseExpiresAt;
|
|
1509
|
-
state.record.updatedAt = new Date(nowMs).toISOString();
|
|
1510
|
-
return { ok: true, leaseExpiresAt };
|
|
1511
|
-
}
|
|
1512
|
-
async loadStep(runId, stepIndex) {
|
|
1513
|
-
const state = this.runs.get(runId);
|
|
1514
|
-
return state ? cloneStep(state.steps.get(stepIndex)) : void 0;
|
|
1515
|
-
}
|
|
1516
|
-
async beginStep(input) {
|
|
1517
|
-
const state = this.requireRun(input.runId);
|
|
1518
|
-
const nowIso2 = new Date(this.now()).toISOString();
|
|
1519
|
-
const prior = state.steps.get(input.stepIndex);
|
|
1520
|
-
if (prior) {
|
|
1521
|
-
if (prior.intent !== input.intent) {
|
|
1522
|
-
throw new DurableRunDivergenceError(
|
|
1523
|
-
`step ${input.stepIndex}: intent changed across replays ('${prior.intent}' -> '${input.intent}')`
|
|
1524
|
-
);
|
|
1525
|
-
}
|
|
1526
|
-
prior.attempts += 1;
|
|
1527
|
-
prior.status = "running";
|
|
1528
|
-
prior.startedAt = nowIso2;
|
|
1529
|
-
prior.error = void 0;
|
|
1530
|
-
state.record.updatedAt = nowIso2;
|
|
1531
|
-
return cloneStep(prior);
|
|
1532
|
-
}
|
|
1533
|
-
const rec = {
|
|
1534
|
-
runId: input.runId,
|
|
1535
|
-
stepIndex: input.stepIndex,
|
|
1536
|
-
intent: input.intent,
|
|
1537
|
-
kind: input.kind,
|
|
1538
|
-
inputHash: input.inputHash,
|
|
1539
|
-
status: "running",
|
|
1540
|
-
attempts: 1,
|
|
1541
|
-
startedAt: nowIso2
|
|
1542
|
-
};
|
|
1543
|
-
state.steps.set(input.stepIndex, rec);
|
|
1544
|
-
state.record.stepCount = Math.max(state.record.stepCount, input.stepIndex + 1);
|
|
1545
|
-
state.record.updatedAt = nowIso2;
|
|
1546
|
-
return cloneStep(rec);
|
|
1547
|
-
}
|
|
1548
|
-
async completeStep(input) {
|
|
1549
|
-
const state = this.requireRun(input.runId);
|
|
1550
|
-
const rec = state.steps.get(input.stepIndex);
|
|
1551
|
-
if (!rec) {
|
|
1552
|
-
throw new Error(
|
|
1553
|
-
`durable-runs: completeStep called before beginStep (step ${input.stepIndex})`
|
|
1554
|
-
);
|
|
1555
|
-
}
|
|
1556
|
-
const nowIso2 = new Date(this.now()).toISOString();
|
|
1557
|
-
rec.status = "completed";
|
|
1558
|
-
rec.result = input.result;
|
|
1559
|
-
rec.completedAt = nowIso2;
|
|
1560
|
-
rec.error = void 0;
|
|
1561
|
-
state.record.updatedAt = nowIso2;
|
|
1562
|
-
return cloneStep(rec);
|
|
1563
|
-
}
|
|
1564
|
-
async failStep(input) {
|
|
1565
|
-
const state = this.requireRun(input.runId);
|
|
1566
|
-
const rec = state.steps.get(input.stepIndex);
|
|
1567
|
-
if (!rec) {
|
|
1568
|
-
throw new Error(`durable-runs: failStep called before beginStep (step ${input.stepIndex})`);
|
|
1569
|
-
}
|
|
1570
|
-
const nowIso2 = new Date(this.now()).toISOString();
|
|
1571
|
-
rec.status = "failed";
|
|
1572
|
-
rec.error = input.error;
|
|
1573
|
-
rec.completedAt = nowIso2;
|
|
1574
|
-
state.record.updatedAt = nowIso2;
|
|
1575
|
-
return cloneStep(rec);
|
|
1576
|
-
}
|
|
1577
|
-
async endRun(input) {
|
|
1578
|
-
const state = this.requireRun(input.runId);
|
|
1579
|
-
const nowIso2 = new Date(this.now()).toISOString();
|
|
1580
|
-
state.record.status = input.status;
|
|
1581
|
-
state.record.outcome = input.outcome;
|
|
1582
|
-
state.record.completedAt = nowIso2;
|
|
1583
|
-
state.record.updatedAt = nowIso2;
|
|
1584
|
-
if (state.record.leaseHolderId === input.workerId) {
|
|
1585
|
-
state.record.leaseHolderId = void 0;
|
|
1586
|
-
state.record.leaseExpiresAt = void 0;
|
|
1587
|
-
}
|
|
1588
|
-
return { ...state.record };
|
|
1589
|
-
}
|
|
1590
|
-
async emitEvent(input) {
|
|
1591
|
-
const state = this.requireRun(input.runId);
|
|
1592
|
-
const existing = state.events.get(input.key);
|
|
1593
|
-
if (existing) {
|
|
1594
|
-
return { accepted: false, record: { ...existing } };
|
|
1595
|
-
}
|
|
1596
|
-
const rec = {
|
|
1597
|
-
runId: input.runId,
|
|
1598
|
-
key: input.key,
|
|
1599
|
-
payload: input.payload,
|
|
1600
|
-
emittedAt: new Date(this.now()).toISOString()
|
|
1601
|
-
};
|
|
1602
|
-
state.events.set(input.key, rec);
|
|
1603
|
-
return { accepted: true, record: { ...rec } };
|
|
1604
|
-
}
|
|
1605
|
-
async loadEvent(runId, key) {
|
|
1606
|
-
const state = this.runs.get(runId);
|
|
1607
|
-
if (!state) return void 0;
|
|
1608
|
-
const rec = state.events.get(key);
|
|
1609
|
-
return rec ? { ...rec } : void 0;
|
|
1610
|
-
}
|
|
1611
|
-
async appendStreamEvent(input) {
|
|
1612
|
-
const state = this.requireRun(input.runId);
|
|
1613
|
-
const existing = state.streamEvents.find((e) => e.eventId === input.eventId);
|
|
1614
|
-
if (existing) return { accepted: false, record: { ...existing } };
|
|
1615
|
-
const rec = {
|
|
1616
|
-
runId: input.runId,
|
|
1617
|
-
seq: state.streamEvents.length,
|
|
1618
|
-
eventId: input.eventId,
|
|
1619
|
-
payload: input.payload,
|
|
1620
|
-
appendedAt: new Date(this.now()).toISOString()
|
|
1621
|
-
};
|
|
1622
|
-
state.streamEvents.push(rec);
|
|
1623
|
-
return { accepted: true, record: { ...rec } };
|
|
1624
|
-
}
|
|
1625
|
-
async readStreamEvents(runId, afterSeq) {
|
|
1626
|
-
const state = this.runs.get(runId);
|
|
1627
|
-
if (!state) return [];
|
|
1628
|
-
const cutoff = afterSeq ?? -1;
|
|
1629
|
-
return state.streamEvents.filter((e) => e.seq > cutoff).map((e) => ({ ...e }));
|
|
1630
|
-
}
|
|
1631
|
-
async setRunHandle(input) {
|
|
1632
|
-
const state = this.requireRun(input.runId);
|
|
1633
|
-
state.record.handle = { ...input.handle };
|
|
1634
|
-
state.record.updatedAt = new Date(this.now()).toISOString();
|
|
1635
|
-
}
|
|
1636
|
-
async close() {
|
|
1637
|
-
this.runs.clear();
|
|
1638
|
-
}
|
|
1639
|
-
// ── test helpers ───────────────────────────────────────────────────
|
|
1640
|
-
/** @internal — used by tests to inspect lease metadata. */
|
|
1641
|
-
_inspect(runId) {
|
|
1642
|
-
const s = this.runs.get(runId);
|
|
1643
|
-
return s ? { ...s.record } : void 0;
|
|
1644
|
-
}
|
|
1645
|
-
/** @internal — used by tests to simulate lease expiry. */
|
|
1646
|
-
_expireLease(runId) {
|
|
1647
|
-
const s = this.runs.get(runId);
|
|
1648
|
-
if (s) {
|
|
1649
|
-
s.record.leaseHolderId = void 0;
|
|
1650
|
-
s.record.leaseExpiresAt = void 0;
|
|
1651
|
-
}
|
|
1652
|
-
}
|
|
1653
|
-
requireRun(runId) {
|
|
1654
|
-
const s = this.runs.get(runId);
|
|
1655
|
-
if (!s) {
|
|
1656
|
-
throw new Error(`durable-runs: run ${runId} not found (must call startOrResume first)`);
|
|
1657
|
-
}
|
|
1658
|
-
return s;
|
|
1659
|
-
}
|
|
1660
|
-
};
|
|
1661
|
-
function cloneStep(rec) {
|
|
1662
|
-
if (!rec) return void 0;
|
|
1663
|
-
return { ...rec, error: rec.error ? { ...rec.error } : void 0 };
|
|
1664
|
-
}
|
|
1665
|
-
|
|
1666
|
-
// src/durable/runner.ts
|
|
1667
|
-
var DEFAULT_LEASE_MS4 = 3e4;
|
|
1668
|
-
var DEFAULT_AWAIT_POLL_MS = 250;
|
|
1669
|
-
async function runDurable(input) {
|
|
1670
|
-
const workerId = input.workerId ?? deriveWorkerId();
|
|
1671
|
-
const leaseMs = input.leaseMs ?? DEFAULT_LEASE_MS4;
|
|
1672
|
-
const { completedSteps } = await input.store.startOrResume({
|
|
1673
|
-
runId: input.runId,
|
|
1674
|
-
manifest: input.manifest,
|
|
1675
|
-
workerId,
|
|
1676
|
-
leaseMs
|
|
1677
|
-
});
|
|
1678
|
-
const priorByIndex = /* @__PURE__ */ new Map();
|
|
1679
|
-
for (const s of completedSteps) priorByIndex.set(s.stepIndex, s);
|
|
1680
|
-
const collected = [...completedSteps].sort((a, b) => a.stepIndex - b.stepIndex);
|
|
1681
|
-
let leaseLost = false;
|
|
1682
|
-
const heartbeatIntervalMs = Math.max(1e3, Math.floor(leaseMs / 3));
|
|
1683
|
-
const heartbeat = setInterval(() => {
|
|
1684
|
-
void input.store.renewLease({ runId: input.runId, workerId, leaseMs }).then((res) => {
|
|
1685
|
-
if (!res.ok) leaseLost = true;
|
|
1686
|
-
}).catch(() => {
|
|
1687
|
-
leaseLost = true;
|
|
1688
|
-
});
|
|
1689
|
-
}, heartbeatIntervalMs);
|
|
1690
|
-
if (typeof heartbeat.unref === "function") heartbeat.unref();
|
|
1691
|
-
let positionCounter = 0;
|
|
1692
|
-
const ctx = {
|
|
1693
|
-
runId: input.runId,
|
|
1694
|
-
projectId: input.manifest.projectId,
|
|
1695
|
-
scenarioId: input.manifest.scenarioId,
|
|
1696
|
-
async step(intent, fn, opts) {
|
|
1697
|
-
checkAbortAndLease(input.signal, leaseLost);
|
|
1698
|
-
const stepIndex = positionCounter++;
|
|
1699
|
-
const prior = priorByIndex.get(stepIndex);
|
|
1700
|
-
const inputHash = opts?.inputFingerprint !== void 0 ? canonicalHash(opts.inputFingerprint) : "";
|
|
1701
|
-
if (prior && prior.status === "completed") {
|
|
1702
|
-
if (prior.intent !== intent) {
|
|
1703
|
-
throw new DurableRunDivergenceError(
|
|
1704
|
-
`step ${stepIndex}: intent changed across replays ('${prior.intent}' -> '${intent}')`
|
|
1705
|
-
);
|
|
568
|
+
if (hooks.onTurnComplete) {
|
|
569
|
+
try {
|
|
570
|
+
await hooks.onTurnComplete({ identity, finalText });
|
|
571
|
+
} catch (err) {
|
|
572
|
+
log("[chat-engine] onTurnComplete threw", {
|
|
573
|
+
error: err instanceof Error ? err.message : String(err)
|
|
574
|
+
});
|
|
575
|
+
}
|
|
1706
576
|
}
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
runId: input.runId,
|
|
1711
|
-
stepIndex,
|
|
1712
|
-
intent,
|
|
1713
|
-
kind: opts?.kind ?? "logic",
|
|
1714
|
-
inputHash
|
|
1715
|
-
});
|
|
1716
|
-
try {
|
|
1717
|
-
const result = await fn();
|
|
1718
|
-
const completed = await input.store.completeStep({
|
|
1719
|
-
runId: input.runId,
|
|
1720
|
-
stepIndex,
|
|
1721
|
-
result
|
|
577
|
+
await emit2({
|
|
578
|
+
type: "session.run.completed",
|
|
579
|
+
data: { sessionId: identity.sessionId }
|
|
1722
580
|
});
|
|
1723
|
-
upsertCollected(collected, completed);
|
|
1724
|
-
priorByIndex.set(stepIndex, completed);
|
|
1725
|
-
return result;
|
|
1726
581
|
} catch (err) {
|
|
1727
|
-
const
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
582
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
583
|
+
log("[chat-engine] turn failed", { error: message });
|
|
584
|
+
await emit2({ type: "error", data: { message } });
|
|
585
|
+
await emit2({
|
|
586
|
+
type: "session.run.failed",
|
|
587
|
+
data: { sessionId: identity.sessionId, message }
|
|
1731
588
|
});
|
|
1732
|
-
upsertCollected(collected, failed);
|
|
1733
|
-
priorByIndex.set(stepIndex, failed);
|
|
1734
|
-
throw err;
|
|
1735
589
|
} finally {
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
const prior = priorByIndex.get(stepIndex);
|
|
1742
|
-
if (prior && prior.status === "completed") {
|
|
1743
|
-
if (prior.intent !== `event:${key}`) {
|
|
1744
|
-
throw new DurableRunDivergenceError(
|
|
1745
|
-
`step ${stepIndex}: awaitEvent key changed across replays`
|
|
590
|
+
if (hooks.traceFlush) {
|
|
591
|
+
const flush = hooks.traceFlush().catch(
|
|
592
|
+
(err) => log("[chat-engine] traceFlush threw", {
|
|
593
|
+
error: err instanceof Error ? err.message : String(err)
|
|
594
|
+
})
|
|
1746
595
|
);
|
|
596
|
+
if (input.waitUntil) input.waitUntil(flush);
|
|
597
|
+
else await flush;
|
|
1747
598
|
}
|
|
1748
|
-
|
|
1749
|
-
}
|
|
1750
|
-
const beginAt = Date.now();
|
|
1751
|
-
const timeoutMs = opts?.timeoutMs ?? 6e4;
|
|
1752
|
-
const pollMs = opts?.pollMs ?? DEFAULT_AWAIT_POLL_MS;
|
|
1753
|
-
await input.store.beginStep({
|
|
1754
|
-
runId: input.runId,
|
|
1755
|
-
stepIndex,
|
|
1756
|
-
intent: `event:${key}`,
|
|
1757
|
-
kind: "event",
|
|
1758
|
-
inputHash: ""
|
|
1759
|
-
});
|
|
1760
|
-
try {
|
|
1761
|
-
for (; ; ) {
|
|
1762
|
-
checkAbortAndLease(input.signal, leaseLost);
|
|
1763
|
-
const evt = await input.store.loadEvent(input.runId, key);
|
|
1764
|
-
if (evt) {
|
|
1765
|
-
const completed = await input.store.completeStep({
|
|
1766
|
-
runId: input.runId,
|
|
1767
|
-
stepIndex,
|
|
1768
|
-
result: evt.payload
|
|
1769
|
-
});
|
|
1770
|
-
upsertCollected(collected, completed);
|
|
1771
|
-
priorByIndex.set(stepIndex, completed);
|
|
1772
|
-
return evt.payload;
|
|
1773
|
-
}
|
|
1774
|
-
if (Date.now() - beginAt > timeoutMs) {
|
|
1775
|
-
const err = new DurableAwaitEventTimeoutError(
|
|
1776
|
-
`awaitEvent('${key}') timed out after ${timeoutMs}ms`
|
|
1777
|
-
);
|
|
1778
|
-
const failed = await input.store.failStep({
|
|
1779
|
-
runId: input.runId,
|
|
1780
|
-
stepIndex,
|
|
1781
|
-
error: toStepError(err)
|
|
1782
|
-
});
|
|
1783
|
-
upsertCollected(collected, failed);
|
|
1784
|
-
priorByIndex.set(stepIndex, failed);
|
|
1785
|
-
throw err;
|
|
1786
|
-
}
|
|
1787
|
-
await sleep2(pollMs, input.signal);
|
|
1788
|
-
}
|
|
1789
|
-
} catch (err) {
|
|
1790
|
-
if (!(err instanceof DurableAwaitEventTimeoutError)) {
|
|
1791
|
-
const existing = priorByIndex.get(stepIndex);
|
|
1792
|
-
if (!existing || existing.status !== "failed") {
|
|
1793
|
-
const failed = await input.store.failStep({
|
|
1794
|
-
runId: input.runId,
|
|
1795
|
-
stepIndex,
|
|
1796
|
-
error: toStepError(err)
|
|
1797
|
-
});
|
|
1798
|
-
upsertCollected(collected, failed);
|
|
1799
|
-
priorByIndex.set(stepIndex, failed);
|
|
1800
|
-
}
|
|
1801
|
-
}
|
|
1802
|
-
throw err;
|
|
1803
|
-
}
|
|
1804
|
-
},
|
|
1805
|
-
async emitEvent(key, payload) {
|
|
1806
|
-
const res = await input.store.emitEvent({ runId: input.runId, key, payload });
|
|
1807
|
-
return { accepted: res.accepted };
|
|
1808
|
-
},
|
|
1809
|
-
async now() {
|
|
1810
|
-
const v = await this.step(`deterministic:now`, async () => (/* @__PURE__ */ new Date()).toISOString(), {
|
|
1811
|
-
kind: "deterministic"
|
|
1812
|
-
});
|
|
1813
|
-
return new Date(v);
|
|
1814
|
-
},
|
|
1815
|
-
async uuid() {
|
|
1816
|
-
return this.step(`deterministic:uuid`, async () => cryptoRandomUuid(), {
|
|
1817
|
-
kind: "deterministic"
|
|
1818
|
-
});
|
|
1819
|
-
}
|
|
1820
|
-
};
|
|
1821
|
-
try {
|
|
1822
|
-
const result = await input.taskFn(ctx);
|
|
1823
|
-
const finalRecord = await input.store.endRun({
|
|
1824
|
-
runId: input.runId,
|
|
1825
|
-
workerId,
|
|
1826
|
-
status: "completed",
|
|
1827
|
-
outcome: input.defaultOutcome
|
|
1828
|
-
});
|
|
1829
|
-
return { result, record: finalRecord, steps: collected };
|
|
1830
|
-
} catch (err) {
|
|
1831
|
-
const finalRecord = await input.store.endRun({
|
|
1832
|
-
runId: input.runId,
|
|
1833
|
-
workerId,
|
|
1834
|
-
status: "failed",
|
|
1835
|
-
outcome: {
|
|
1836
|
-
...input.defaultOutcome,
|
|
1837
|
-
notes: err instanceof Error ? err.message : String(err)
|
|
599
|
+
controller.close();
|
|
1838
600
|
}
|
|
1839
|
-
});
|
|
1840
|
-
void finalRecord;
|
|
1841
|
-
throw err;
|
|
1842
|
-
} finally {
|
|
1843
|
-
clearInterval(heartbeat);
|
|
1844
|
-
}
|
|
1845
|
-
}
|
|
1846
|
-
function checkAbortAndLease(signal, leaseLost) {
|
|
1847
|
-
if (signal?.aborted) throw signal.reason ?? new Error("aborted");
|
|
1848
|
-
if (leaseLost) throw new Error("durable-runs: lease lost; another worker has taken over this run");
|
|
1849
|
-
}
|
|
1850
|
-
function upsertCollected(list, rec) {
|
|
1851
|
-
const i = list.findIndex((s) => s.stepIndex === rec.stepIndex);
|
|
1852
|
-
if (i === -1) list.push(rec);
|
|
1853
|
-
else list[i] = rec;
|
|
1854
|
-
}
|
|
1855
|
-
function toStepError(err) {
|
|
1856
|
-
if (err instanceof Error) {
|
|
1857
|
-
return {
|
|
1858
|
-
message: err.message,
|
|
1859
|
-
code: err.code,
|
|
1860
|
-
stack: err.stack
|
|
1861
|
-
};
|
|
1862
|
-
}
|
|
1863
|
-
return { message: String(err) };
|
|
1864
|
-
}
|
|
1865
|
-
function sleep2(ms, signal) {
|
|
1866
|
-
return new Promise((resolve, reject) => {
|
|
1867
|
-
if (signal?.aborted) {
|
|
1868
|
-
reject(signal.reason ?? new Error("aborted"));
|
|
1869
|
-
return;
|
|
1870
601
|
}
|
|
1871
|
-
const t = setTimeout(() => {
|
|
1872
|
-
signal?.removeEventListener("abort", onAbort);
|
|
1873
|
-
resolve();
|
|
1874
|
-
}, ms);
|
|
1875
|
-
const onAbort = () => {
|
|
1876
|
-
clearTimeout(t);
|
|
1877
|
-
reject(signal?.reason ?? new Error("aborted"));
|
|
1878
|
-
};
|
|
1879
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1880
602
|
});
|
|
603
|
+
return { body, contentType: "application/x-ndjson" };
|
|
1881
604
|
}
|
|
1882
|
-
function cryptoRandomUuid() {
|
|
1883
|
-
if (typeof globalThis.crypto?.randomUUID === "function") {
|
|
1884
|
-
return globalThis.crypto.randomUUID();
|
|
1885
|
-
}
|
|
1886
|
-
const bytes = new Uint8Array(16);
|
|
1887
|
-
for (let i = 0; i < 16; i++) bytes[i] = Math.floor(Math.random() * 256);
|
|
1888
|
-
bytes[6] = (bytes[6] ?? 0) & 15 | 64;
|
|
1889
|
-
bytes[8] = (bytes[8] ?? 0) & 63 | 128;
|
|
1890
|
-
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
1891
|
-
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
1892
|
-
}
|
|
1893
|
-
|
|
1894
|
-
// src/durable/schema.ts
|
|
1895
|
-
var DURABLE_SCHEMA_VERSION = 2;
|
|
1896
|
-
var DURABLE_SCHEMA_SQL = `-- Durable-run substrate \u2014 versioned schema for D1 / SQLite.
|
|
1897
|
-
--
|
|
1898
|
-
-- Apply once per database. Subsequent migrations append; never rewrite a
|
|
1899
|
-
-- prior version. See \`durable_schema_info\` for the migration trail.
|
|
1900
|
-
--
|
|
1901
|
-
-- Concurrency notes for D1:
|
|
1902
|
-
-- - SQLite supports UNIQUE constraints for first-emit-wins (\`durable_events\`
|
|
1903
|
-
-- PK is (run_id, key) \u2014 duplicate insert raises, caller treats as "already
|
|
1904
|
-
-- emitted").
|
|
1905
|
-
-- - Lease takeover happens via a conditional UPDATE: we only claim the lease
|
|
1906
|
-
-- if (lease_holder_id IS NULL OR lease_expires_at < :now) \u2014 atomic under
|
|
1907
|
-
-- SQLite's row-level locking.
|
|
1908
|
-
-- - All timestamps stored as ISO-8601 TEXT for cross-platform consistency.
|
|
1909
|
-
-- - \`result_json\` / \`error_json\` / \`outcome_json\` / \`payload_json\` are
|
|
1910
|
-
-- JSON-encoded TEXT; the application enforces canonical-JSON discipline at
|
|
1911
|
-
-- the boundary so the store stays type-agnostic.
|
|
1912
|
-
|
|
1913
|
-
CREATE TABLE IF NOT EXISTS durable_schema_info (
|
|
1914
|
-
version INTEGER PRIMARY KEY,
|
|
1915
|
-
applied_at TEXT NOT NULL
|
|
1916
|
-
);
|
|
1917
|
-
|
|
1918
|
-
CREATE TABLE IF NOT EXISTS durable_runs (
|
|
1919
|
-
run_id TEXT PRIMARY KEY,
|
|
1920
|
-
manifest_hash TEXT NOT NULL,
|
|
1921
|
-
project_id TEXT NOT NULL,
|
|
1922
|
-
scenario_id TEXT,
|
|
1923
|
-
status TEXT NOT NULL CHECK (status IN ('pending','running','completed','failed','suspended')),
|
|
1924
|
-
created_at TEXT NOT NULL,
|
|
1925
|
-
updated_at TEXT NOT NULL,
|
|
1926
|
-
completed_at TEXT,
|
|
1927
|
-
lease_holder_id TEXT,
|
|
1928
|
-
lease_expires_at TEXT,
|
|
1929
|
-
outcome_json TEXT,
|
|
1930
|
-
step_count INTEGER NOT NULL DEFAULT 0
|
|
1931
|
-
);
|
|
1932
|
-
|
|
1933
|
-
CREATE INDEX IF NOT EXISTS idx_durable_runs_project_status ON durable_runs(project_id, status);
|
|
1934
|
-
CREATE INDEX IF NOT EXISTS idx_durable_runs_lease_expires ON durable_runs(lease_expires_at);
|
|
1935
|
-
|
|
1936
|
-
CREATE TABLE IF NOT EXISTS durable_steps (
|
|
1937
|
-
run_id TEXT NOT NULL,
|
|
1938
|
-
step_index INTEGER NOT NULL,
|
|
1939
|
-
intent TEXT NOT NULL,
|
|
1940
|
-
kind TEXT NOT NULL,
|
|
1941
|
-
input_hash TEXT NOT NULL DEFAULT '',
|
|
1942
|
-
status TEXT NOT NULL CHECK (status IN ('pending','running','completed','failed')),
|
|
1943
|
-
attempts INTEGER NOT NULL DEFAULT 0,
|
|
1944
|
-
result_json TEXT,
|
|
1945
|
-
error_json TEXT,
|
|
1946
|
-
started_at TEXT,
|
|
1947
|
-
completed_at TEXT,
|
|
1948
|
-
PRIMARY KEY (run_id, step_index)
|
|
1949
|
-
);
|
|
1950
|
-
|
|
1951
|
-
CREATE INDEX IF NOT EXISTS idx_durable_steps_status ON durable_steps(run_id, status);
|
|
1952
|
-
|
|
1953
|
-
CREATE TABLE IF NOT EXISTS durable_events (
|
|
1954
|
-
run_id TEXT NOT NULL,
|
|
1955
|
-
key TEXT NOT NULL,
|
|
1956
|
-
payload_json TEXT,
|
|
1957
|
-
emitted_at TEXT NOT NULL,
|
|
1958
|
-
PRIMARY KEY (run_id, key)
|
|
1959
|
-
);
|
|
1960
|
-
|
|
1961
|
-
INSERT OR IGNORE INTO durable_schema_info (version, applied_at)
|
|
1962
|
-
VALUES (1, strftime('%Y-%m-%dT%H:%M:%fZ', 'now'));
|
|
1963
|
-
|
|
1964
|
-
-- \u2500\u2500 Migration v2 \u2014 durable event-stream log + run handle \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1965
|
-
-- Run once on a database created at v1. \`ALTER TABLE\` is not idempotent; the
|
|
1966
|
-
-- version trail in \`durable_schema_info\` is how migrations are sequenced \u2014
|
|
1967
|
-
-- never by blind re-execution of this block.
|
|
1968
|
-
--
|
|
1969
|
-
-- - \`durable_stream_events\` is the ordered, replayable per-run event log.
|
|
1970
|
-
-- \`seq\` is the store-assigned monotonic cursor; the UNIQUE index on
|
|
1971
|
-
-- (run_id, event_id) makes appends idempotent \u2014 a reconnecting adapter
|
|
1972
|
-
-- that re-yields a boundary event cannot double-log it.
|
|
1973
|
-
-- - \`durable_runs.handle_json\` is the pointer (sandbox + substrate run id +
|
|
1974
|
-
-- cursor) a fresh supervisor re-attaches by.
|
|
1975
|
-
|
|
1976
|
-
ALTER TABLE durable_runs ADD COLUMN handle_json TEXT;
|
|
1977
605
|
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
event_id TEXT NOT NULL,
|
|
1982
|
-
payload_json TEXT,
|
|
1983
|
-
appended_at TEXT NOT NULL,
|
|
1984
|
-
PRIMARY KEY (run_id, seq)
|
|
1985
|
-
);
|
|
1986
|
-
|
|
1987
|
-
CREATE UNIQUE INDEX IF NOT EXISTS idx_durable_stream_events_event_id
|
|
1988
|
-
ON durable_stream_events(run_id, event_id);
|
|
1989
|
-
|
|
1990
|
-
INSERT OR IGNORE INTO durable_schema_info (version, applied_at)
|
|
1991
|
-
VALUES (2, strftime('%Y-%m-%dT%H:%M:%fZ', 'now'));
|
|
1992
|
-
`;
|
|
1993
|
-
|
|
1994
|
-
// src/durable/supervisor.ts
|
|
1995
|
-
var TURN_STEP = 0;
|
|
1996
|
-
function runSupervisedTurn(options) {
|
|
1997
|
-
const { store, runId, manifest, workerId, adapter } = options;
|
|
1998
|
-
const leaseMs = options.leaseMs ?? 6e4;
|
|
1999
|
-
const heartbeatMs = options.heartbeatMs ?? 3e4;
|
|
2000
|
-
const intent = options.intent ?? "turn";
|
|
2001
|
-
const now = options.now ?? (() => Date.now());
|
|
2002
|
-
const inputHash = canonicalHash(manifest.input);
|
|
2003
|
-
let mode = "fresh";
|
|
2004
|
-
let finalRecord;
|
|
2005
|
-
let currentHandle;
|
|
2006
|
-
let leaseLost = false;
|
|
2007
|
-
async function* drain(source) {
|
|
2008
|
-
let lastRenew = now();
|
|
2009
|
-
for await (const event of source) {
|
|
2010
|
-
if (event.handle) {
|
|
2011
|
-
currentHandle = event.handle;
|
|
2012
|
-
await store.setRunHandle({ runId, handle: event.handle });
|
|
2013
|
-
}
|
|
2014
|
-
const { accepted } = await store.appendStreamEvent({
|
|
2015
|
-
runId,
|
|
2016
|
-
eventId: event.eventId,
|
|
2017
|
-
payload: event.payload
|
|
2018
|
-
});
|
|
2019
|
-
if (now() - lastRenew >= heartbeatMs) {
|
|
2020
|
-
const renewed = await store.renewLease({ runId, workerId, leaseMs });
|
|
2021
|
-
if (!renewed.ok) {
|
|
2022
|
-
leaseLost = true;
|
|
2023
|
-
throw new Error(`durable-runs: lease lost on ${runId} \u2014 another supervisor took over`);
|
|
2024
|
-
}
|
|
2025
|
-
lastRenew = now();
|
|
2026
|
-
}
|
|
2027
|
-
if (accepted) yield event.payload;
|
|
2028
|
-
}
|
|
2029
|
-
}
|
|
2030
|
-
async function* stream() {
|
|
2031
|
-
const { run, completedSteps } = await store.startOrResume({
|
|
2032
|
-
runId,
|
|
2033
|
-
manifest,
|
|
2034
|
-
workerId,
|
|
2035
|
-
leaseMs
|
|
2036
|
-
});
|
|
2037
|
-
if (completedSteps.some((s) => s.stepIndex === TURN_STEP && s.status === "completed")) {
|
|
2038
|
-
mode = "replayed";
|
|
2039
|
-
for (const e of await store.readStreamEvents(runId)) yield e.payload;
|
|
2040
|
-
finalRecord = await store.endRun({ runId, workerId, status: "completed" });
|
|
2041
|
-
return;
|
|
2042
|
-
}
|
|
2043
|
-
const logged = await store.readStreamEvents(runId);
|
|
2044
|
-
const priorHandle = run.handle;
|
|
2045
|
-
const resumable = priorHandle !== void 0 && priorHandle.status === "running" && typeof priorHandle.runId === "string";
|
|
2046
|
-
let source;
|
|
2047
|
-
if (resumable && priorHandle) {
|
|
2048
|
-
mode = "resumed";
|
|
2049
|
-
currentHandle = priorHandle;
|
|
2050
|
-
for (const e of logged) yield e.payload;
|
|
2051
|
-
const cursor = logged.length > 0 ? logged[logged.length - 1].eventId : priorHandle.cursor;
|
|
2052
|
-
source = adapter.attach(priorHandle, cursor);
|
|
2053
|
-
} else {
|
|
2054
|
-
mode = "fresh";
|
|
2055
|
-
source = adapter.start();
|
|
2056
|
-
}
|
|
2057
|
-
await store.beginStep({ runId, stepIndex: TURN_STEP, intent, kind: "llm", inputHash });
|
|
2058
|
-
try {
|
|
2059
|
-
yield* drain(source);
|
|
2060
|
-
const eventCount = (await store.readStreamEvents(runId)).length;
|
|
2061
|
-
await store.completeStep({ runId, stepIndex: TURN_STEP, result: { eventCount } });
|
|
2062
|
-
if (currentHandle && currentHandle.status === "running") {
|
|
2063
|
-
await store.setRunHandle({ runId, handle: { ...currentHandle, status: "completed" } });
|
|
2064
|
-
}
|
|
2065
|
-
finalRecord = await store.endRun({
|
|
2066
|
-
runId,
|
|
2067
|
-
workerId,
|
|
2068
|
-
status: "completed",
|
|
2069
|
-
outcome: { notes: intent, metadata: { events: eventCount, mode } }
|
|
2070
|
-
});
|
|
2071
|
-
} catch (err) {
|
|
2072
|
-
if (!leaseLost) {
|
|
2073
|
-
await store.failStep({
|
|
2074
|
-
runId,
|
|
2075
|
-
stepIndex: TURN_STEP,
|
|
2076
|
-
error: { message: err instanceof Error ? err.message : String(err) }
|
|
2077
|
-
});
|
|
2078
|
-
finalRecord = await store.endRun({ runId, workerId, status: "failed" });
|
|
2079
|
-
}
|
|
2080
|
-
throw err;
|
|
2081
|
-
}
|
|
2082
|
-
}
|
|
2083
|
-
return {
|
|
2084
|
-
stream: stream(),
|
|
2085
|
-
mode: () => mode,
|
|
2086
|
-
record: () => finalRecord
|
|
2087
|
-
};
|
|
2088
|
-
}
|
|
2089
|
-
|
|
2090
|
-
// src/durable/session-supervisor-do.ts
|
|
2091
|
-
var ACTIVE_RUN_KEY = "agent-runtime:active-run-id";
|
|
2092
|
-
async function drainHeadless(stream) {
|
|
2093
|
-
let next = await stream.next();
|
|
2094
|
-
while (!next.done) next = await stream.next();
|
|
2095
|
-
}
|
|
2096
|
-
function createSessionSupervisorDO(config) {
|
|
2097
|
-
const orphanCheckMs = config.orphanCheckMs ?? 6e4;
|
|
2098
|
-
const now = config.now ?? (() => Date.now());
|
|
2099
|
-
return class {
|
|
2100
|
-
constructor(state, env) {
|
|
2101
|
-
this.state = state;
|
|
2102
|
-
this.env = env;
|
|
2103
|
-
}
|
|
2104
|
-
state;
|
|
2105
|
-
env;
|
|
2106
|
-
async fetch(request) {
|
|
2107
|
-
const opts = await config.resolveRun(request, this.env, this.state);
|
|
2108
|
-
if (!opts) return new Response("no run for this request", { status: 404 });
|
|
2109
|
-
await this.state.storage.put(ACTIVE_RUN_KEY, opts.runId);
|
|
2110
|
-
await this.state.storage.setAlarm(now() + orphanCheckMs);
|
|
2111
|
-
const supervised = runSupervisedTurn(opts);
|
|
2112
|
-
const storage = this.state.storage;
|
|
2113
|
-
const encoder2 = new TextEncoder();
|
|
2114
|
-
const body = new ReadableStream({
|
|
2115
|
-
async pull(controller) {
|
|
2116
|
-
try {
|
|
2117
|
-
const next = await supervised.stream.next();
|
|
2118
|
-
if (next.done) {
|
|
2119
|
-
await storage.delete(ACTIVE_RUN_KEY);
|
|
2120
|
-
controller.close();
|
|
2121
|
-
return;
|
|
2122
|
-
}
|
|
2123
|
-
controller.enqueue(encoder2.encode(config.encodeEvent(next.value)));
|
|
2124
|
-
} catch (err) {
|
|
2125
|
-
controller.error(err instanceof Error ? err : new Error(String(err)));
|
|
2126
|
-
}
|
|
2127
|
-
}
|
|
2128
|
-
});
|
|
2129
|
-
return new Response(body, {
|
|
2130
|
-
headers: { "content-type": "text/event-stream", "cache-control": "no-cache" }
|
|
2131
|
-
});
|
|
2132
|
-
}
|
|
2133
|
-
async alarm() {
|
|
2134
|
-
const runId = await this.state.storage.get(ACTIVE_RUN_KEY);
|
|
2135
|
-
if (!runId) return;
|
|
2136
|
-
const opts = await config.resolveOrphan(runId, this.env, this.state);
|
|
2137
|
-
if (!opts) {
|
|
2138
|
-
await this.state.storage.delete(ACTIVE_RUN_KEY);
|
|
2139
|
-
return;
|
|
2140
|
-
}
|
|
2141
|
-
try {
|
|
2142
|
-
await drainHeadless(runSupervisedTurn(opts).stream);
|
|
2143
|
-
await this.state.storage.delete(ACTIVE_RUN_KEY);
|
|
2144
|
-
} catch (err) {
|
|
2145
|
-
if (err instanceof DurableRunLeaseHeldError) {
|
|
2146
|
-
await this.state.storage.setAlarm(now() + orphanCheckMs);
|
|
2147
|
-
return;
|
|
2148
|
-
}
|
|
2149
|
-
await this.state.storage.delete(ACTIVE_RUN_KEY);
|
|
2150
|
-
}
|
|
2151
|
-
}
|
|
2152
|
-
};
|
|
2153
|
-
}
|
|
2154
|
-
|
|
2155
|
-
// src/durable/workflows.ts
|
|
2156
|
-
async function runOnWorkflowStep(workflowStep, input) {
|
|
2157
|
-
const stepCfg = input.stepConfig;
|
|
2158
|
-
let counter2 = 0;
|
|
2159
|
-
const stepName = (intent) => `${input.workflowName}/${counter2++}:${intent}`;
|
|
2160
|
-
const ctx = {
|
|
2161
|
-
runId: `cf-workflow:${input.workflowName}`,
|
|
2162
|
-
projectId: input.workflowName,
|
|
2163
|
-
async step(intent, fn) {
|
|
2164
|
-
const name = stepName(intent);
|
|
2165
|
-
if (stepCfg) {
|
|
2166
|
-
return workflowStep.do(name, stepCfg, fn);
|
|
2167
|
-
}
|
|
2168
|
-
return workflowStep.do(name, fn);
|
|
2169
|
-
},
|
|
2170
|
-
async awaitEvent(key, opts) {
|
|
2171
|
-
const timeout = opts?.timeoutMs ? `${Math.ceil(opts.timeoutMs / 1e3)}s` : void 0;
|
|
2172
|
-
const ev = await workflowStep.waitForEvent(stepName(`event:${key}`), {
|
|
2173
|
-
type: key,
|
|
2174
|
-
timeout
|
|
2175
|
-
});
|
|
2176
|
-
return ev.payload;
|
|
2177
|
-
},
|
|
2178
|
-
async emitEvent() {
|
|
2179
|
-
throw new Error(
|
|
2180
|
-
"runOnWorkflowStep: ctx.emitEvent is not available inside a Workflows entrypoint. Emit from the sibling worker that drives the workflow."
|
|
2181
|
-
);
|
|
2182
|
-
},
|
|
2183
|
-
async now() {
|
|
2184
|
-
const iso = await this.step("deterministic:now", async () => (/* @__PURE__ */ new Date()).toISOString());
|
|
2185
|
-
return new Date(iso);
|
|
2186
|
-
},
|
|
2187
|
-
async uuid() {
|
|
2188
|
-
return this.step("deterministic:uuid", async () => {
|
|
2189
|
-
if (typeof globalThis.crypto?.randomUUID === "function") {
|
|
2190
|
-
return globalThis.crypto.randomUUID();
|
|
2191
|
-
}
|
|
2192
|
-
const bytes = new Uint8Array(16);
|
|
2193
|
-
crypto.getRandomValues(bytes);
|
|
2194
|
-
bytes[6] = (bytes[6] ?? 0) & 15 | 64;
|
|
2195
|
-
bytes[8] = (bytes[8] ?? 0) & 63 | 128;
|
|
2196
|
-
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
2197
|
-
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
2198
|
-
});
|
|
2199
|
-
}
|
|
2200
|
-
};
|
|
2201
|
-
return input.taskFn(ctx);
|
|
606
|
+
// src/durable/execution-handle.ts
|
|
607
|
+
function deriveExecutionId(input) {
|
|
608
|
+
return `${input.projectId}:${input.sessionId}:${input.turnIndex}`;
|
|
2202
609
|
}
|
|
2203
610
|
|
|
2204
611
|
// src/intent-router.ts
|
|
@@ -3366,8 +1773,8 @@ function createTraceBridge(options) {
|
|
|
3366
1773
|
if (!options.runId) {
|
|
3367
1774
|
throw new ValidationError("createTraceBridge: runId is required");
|
|
3368
1775
|
}
|
|
3369
|
-
let
|
|
3370
|
-
const newEventId = options.newEventId ?? (() => `evt-${++
|
|
1776
|
+
let counter = 0;
|
|
1777
|
+
const newEventId = options.newEventId ?? (() => `evt-${++counter}`);
|
|
3371
1778
|
const baseSpanId = options.spanId;
|
|
3372
1779
|
const toTraceEvent = (event) => {
|
|
3373
1780
|
const projection = projectToTraceEvent(event);
|
|
@@ -3554,18 +1961,7 @@ export {
|
|
|
3554
1961
|
CaptureIntegrityError,
|
|
3555
1962
|
ChatTurnError,
|
|
3556
1963
|
ConfigError,
|
|
3557
|
-
D1DurableRunStore,
|
|
3558
1964
|
DEFAULT_ROUTER_BASE_URL,
|
|
3559
|
-
DURABLE_SCHEMA_SQL,
|
|
3560
|
-
DURABLE_SCHEMA_VERSION,
|
|
3561
|
-
DurableAwaitEventTimeoutError,
|
|
3562
|
-
DurableChatTurnEngine,
|
|
3563
|
-
DurableRunDivergenceError,
|
|
3564
|
-
DurableRunError,
|
|
3565
|
-
DurableRunInputMismatchError,
|
|
3566
|
-
DurableRunLeaseHeldError,
|
|
3567
|
-
FileSystemDurableRunStore,
|
|
3568
|
-
InMemoryDurableRunStore,
|
|
3569
1965
|
InMemoryRuntimeSessionStore,
|
|
3570
1966
|
JudgeError,
|
|
3571
1967
|
NotFoundError,
|
|
@@ -3575,8 +1971,6 @@ export {
|
|
|
3575
1971
|
ValidationError,
|
|
3576
1972
|
VerificationError,
|
|
3577
1973
|
assertProfileConformance,
|
|
3578
|
-
canonicalHash,
|
|
3579
|
-
canonicalJson,
|
|
3580
1974
|
classifyIntent,
|
|
3581
1975
|
cleanModelId,
|
|
3582
1976
|
composeTurnProfile,
|
|
@@ -3585,31 +1979,24 @@ export {
|
|
|
3585
1979
|
createRuntimeEventCollector,
|
|
3586
1980
|
createRuntimeStreamEventCollector,
|
|
3587
1981
|
createSandboxPromptBackend,
|
|
3588
|
-
createSessionSupervisorDO,
|
|
3589
1982
|
createTraceBridge,
|
|
3590
1983
|
decideKnowledgeReadiness,
|
|
3591
|
-
|
|
3592
|
-
durableChatTurnEngine,
|
|
1984
|
+
deriveExecutionId,
|
|
3593
1985
|
encodeServerSentEvent,
|
|
3594
1986
|
getModels,
|
|
3595
|
-
|
|
1987
|
+
handleChatTurn,
|
|
3596
1988
|
readinessServerSentEvent,
|
|
3597
1989
|
resolveChatModel,
|
|
3598
1990
|
resolveRouterBaseUrl,
|
|
3599
1991
|
runAgentTask,
|
|
3600
1992
|
runAgentTaskStream,
|
|
3601
1993
|
runChatTurn,
|
|
3602
|
-
runDurable,
|
|
3603
|
-
runDurableTurn,
|
|
3604
|
-
runOnWorkflowStep,
|
|
3605
|
-
runSupervisedTurn,
|
|
3606
1994
|
runtimeStreamServerSentEvent,
|
|
3607
1995
|
sandboxAsChatTurnTarget,
|
|
3608
1996
|
sanitizeAgentRuntimeEvent,
|
|
3609
1997
|
sanitizeKnowledgeReadinessReport,
|
|
3610
1998
|
sanitizeRuntimeStreamEvent,
|
|
3611
1999
|
startRuntimeRun,
|
|
3612
|
-
stepId,
|
|
3613
2000
|
summarizeAgentTaskRun,
|
|
3614
2001
|
toAgentEvalTrace,
|
|
3615
2002
|
validateChatModelId,
|