@elie-laloum/outpost 7.0.0 → 8.0.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/CHANGELOG.md +15 -0
- package/README.md +2 -0
- package/dist/adapters/agents/antigravity-adapter.js +3 -0
- package/dist/adapters/agents/antigravity-adapter.js.map +1 -1
- package/dist/adapters/agents/claude-adapter.js +3 -0
- package/dist/adapters/agents/claude-adapter.js.map +1 -1
- package/dist/adapters/agents/claude-events.js +25 -1
- package/dist/adapters/agents/claude-events.js.map +1 -1
- package/dist/adapters/agents/codex-adapter.js +3 -0
- package/dist/adapters/agents/codex-adapter.js.map +1 -1
- package/dist/adapters/agents/copilot-adapter.js +3 -0
- package/dist/adapters/agents/copilot-adapter.js.map +1 -1
- package/dist/adapters/agents/copilot-events.js +7 -3
- package/dist/adapters/agents/copilot-events.js.map +1 -1
- package/dist/adapters/agents/kimi-adapter.js +3 -0
- package/dist/adapters/agents/kimi-adapter.js.map +1 -1
- package/dist/adapters/agents/quota.constants.d.ts +7 -0
- package/dist/adapters/agents/quota.constants.js +31 -0
- package/dist/adapters/agents/quota.constants.js.map +1 -0
- package/dist/adapters/agents/quota.d.ts +1 -0
- package/dist/adapters/agents/quota.js +4 -0
- package/dist/adapters/agents/quota.js.map +1 -0
- package/dist/adapters/models/http-provider.js +8 -2
- package/dist/adapters/models/http-provider.js.map +1 -1
- package/dist/adapters/models/model.constants.d.ts +2 -0
- package/dist/adapters/models/model.constants.js +6 -0
- package/dist/adapters/models/model.constants.js.map +1 -1
- package/dist/adapters/models/stream-events.js +11 -7
- package/dist/adapters/models/stream-events.js.map +1 -1
- package/dist/application/agent-output.js +10 -1
- package/dist/application/agent-output.js.map +1 -1
- package/dist/application/agent-quota.d.ts +7 -0
- package/dist/application/agent-quota.js +33 -0
- package/dist/application/agent-quota.js.map +1 -0
- package/dist/application/agent-turn.js +25 -9
- package/dist/application/agent-turn.js.map +1 -1
- package/dist/application/dispatch-observation.js +10 -1
- package/dist/application/dispatch-observation.js.map +1 -1
- package/dist/application/execution.types.d.ts +2 -0
- package/dist/application/execution.types.js.map +1 -1
- package/dist/application/interactive-task-execution.d.ts +3 -0
- package/dist/application/interactive-task-execution.js +76 -0
- package/dist/application/interactive-task-execution.js.map +1 -0
- package/dist/application/interactive-task-protocol.d.ts +4 -0
- package/dist/application/interactive-task-protocol.js +56 -0
- package/dist/application/interactive-task-protocol.js.map +1 -0
- package/dist/application/interactive-task.constants.d.ts +4 -0
- package/dist/application/interactive-task.constants.js +7 -0
- package/dist/application/interactive-task.constants.js.map +1 -0
- package/dist/application/interactive-task.d.ts +3 -0
- package/dist/application/interactive-task.js +84 -0
- package/dist/application/interactive-task.js.map +1 -0
- package/dist/application/interactive-task.types.d.ts +44 -0
- package/dist/application/interactive-task.types.js +2 -0
- package/dist/application/interactive-task.types.js.map +1 -0
- package/dist/application/queue-worker.js +19 -1
- package/dist/application/queue-worker.js.map +1 -1
- package/dist/application/queued-task.js +17 -2
- package/dist/application/queued-task.js.map +1 -1
- package/dist/application/quota-resume.constants.d.ts +1 -0
- package/dist/application/quota-resume.constants.js +2 -0
- package/dist/application/quota-resume.constants.js.map +1 -0
- package/dist/application/quota-resume.d.ts +9 -0
- package/dist/application/quota-resume.js +34 -0
- package/dist/application/quota-resume.js.map +1 -0
- package/dist/application/quota-resume.types.d.ts +1 -0
- package/dist/application/quota-resume.types.js +2 -0
- package/dist/application/quota-resume.types.js.map +1 -0
- package/dist/application/recovery-retention-policy.js +1 -1
- package/dist/application/recovery-retention-policy.js.map +1 -1
- package/dist/application/recovery-retention.js +3 -3
- package/dist/application/recovery-retention.js.map +1 -1
- package/dist/application/recovery-retention.types.d.ts +1 -1
- package/dist/application/recovery-retention.types.js.map +1 -1
- package/dist/application/remote-workspace.js +2 -1
- package/dist/application/remote-workspace.js.map +1 -1
- package/dist/application/replay-recording.d.ts +2 -0
- package/dist/application/replay-recording.js +22 -0
- package/dist/application/replay-recording.js.map +1 -0
- package/dist/application/replay-turn.d.ts +4 -0
- package/dist/application/replay-turn.js +47 -0
- package/dist/application/replay-turn.js.map +1 -0
- package/dist/application/replay-workspace.d.ts +4 -0
- package/dist/application/replay-workspace.js +71 -0
- package/dist/application/replay-workspace.js.map +1 -0
- package/dist/application/replay-workspace.types.d.ts +6 -0
- package/dist/application/replay-workspace.types.js +2 -0
- package/dist/application/replay-workspace.types.js.map +1 -0
- package/dist/application/repository-fingerprint.d.ts +1 -0
- package/dist/application/repository-fingerprint.js +18 -0
- package/dist/application/repository-fingerprint.js.map +1 -0
- package/dist/application/sandbox-dispatch.js +2 -0
- package/dist/application/sandbox-dispatch.js.map +1 -1
- package/dist/application/speculation-checkpoint.js +19 -2
- package/dist/application/speculation-checkpoint.js.map +1 -1
- package/dist/application/speculation-checkpoint.types.d.ts +1 -1
- package/dist/application/speculation-checkpoint.types.js.map +1 -1
- package/dist/application/speculation-resume.js +21 -0
- package/dist/application/speculation-resume.js.map +1 -1
- package/dist/application/speculation-run.js +18 -1
- package/dist/application/speculation-run.js.map +1 -1
- package/dist/application/speculation.types.d.ts +7 -2
- package/dist/application/speculation.types.js.map +1 -1
- package/dist/application/tasks.d.ts +2 -2
- package/dist/application/tasks.js +11 -4
- package/dist/application/tasks.js.map +1 -1
- package/dist/application/tasks.types.d.ts +5 -0
- package/dist/application/tasks.types.js.map +1 -1
- package/dist/application/transport-inspection.constants.js +1 -0
- package/dist/application/transport-inspection.constants.js.map +1 -1
- package/dist/application/transport-retention.js +33 -1
- package/dist/application/transport-retention.js.map +1 -1
- package/dist/domain/agent-observation.js +1 -0
- package/dist/domain/agent-observation.js.map +1 -1
- package/dist/domain/agent.types.d.ts +8 -1
- package/dist/domain/agent.types.js.map +1 -1
- package/dist/domain/errors.types.d.ts +1 -1
- package/dist/domain/errors.types.js.map +1 -1
- package/dist/domain/observation.types.d.ts +6 -1
- package/dist/domain/observation.types.js.map +1 -1
- package/dist/domain/quota.constants.d.ts +1 -0
- package/dist/domain/quota.constants.js +2 -0
- package/dist/domain/quota.constants.js.map +1 -0
- package/dist/domain/quota.d.ts +4 -0
- package/dist/domain/quota.js +28 -0
- package/dist/domain/quota.js.map +1 -0
- package/dist/domain/quota.types.d.ts +7 -0
- package/dist/domain/quota.types.js +2 -0
- package/dist/domain/quota.types.js.map +1 -0
- package/dist/domain/replay.constants.d.ts +11 -0
- package/dist/domain/replay.constants.js +42 -0
- package/dist/domain/replay.constants.js.map +1 -0
- package/dist/domain/replay.d.ts +11 -0
- package/dist/domain/replay.js +231 -0
- package/dist/domain/replay.js.map +1 -0
- package/dist/domain/replay.types.d.ts +84 -0
- package/dist/domain/replay.types.js +2 -0
- package/dist/domain/replay.types.js.map +1 -0
- package/dist/domain/task-queue.js +18 -0
- package/dist/domain/task-queue.js.map +1 -1
- package/dist/domain/task-queue.types.d.ts +9 -0
- package/dist/domain/task-queue.types.js.map +1 -1
- package/dist/domain/workflow/checkpoint-validation.js +14 -1
- package/dist/domain/workflow/checkpoint-validation.js.map +1 -1
- package/dist/domain/workflow/checkpoint.js +13 -2
- package/dist/domain/workflow/checkpoint.js.map +1 -1
- package/dist/domain/workflow/gate-validation.js +2 -1
- package/dist/domain/workflow/gate-validation.js.map +1 -1
- package/dist/domain/workflow/input-validation.d.ts +7 -0
- package/dist/domain/workflow/input-validation.js +82 -0
- package/dist/domain/workflow/input-validation.js.map +1 -0
- package/dist/domain/workflow/input.d.ts +10 -0
- package/dist/domain/workflow/input.js +93 -0
- package/dist/domain/workflow/input.js.map +1 -0
- package/dist/domain/workflow/input.types.d.ts +37 -0
- package/dist/domain/workflow/input.types.js +2 -0
- package/dist/domain/workflow/input.types.js.map +1 -0
- package/dist/domain/workflow/loop-runner.d.ts +3 -0
- package/dist/domain/workflow/loop-runner.js +91 -0
- package/dist/domain/workflow/loop-runner.js.map +1 -0
- package/dist/domain/workflow/loop-task.d.ts +10 -0
- package/dist/domain/workflow/loop-task.js +39 -0
- package/dist/domain/workflow/loop-task.js.map +1 -0
- package/dist/domain/workflow/loop-task.types.d.ts +28 -0
- package/dist/domain/workflow/loop-task.types.js +2 -0
- package/dist/domain/workflow/loop-task.types.js.map +1 -0
- package/dist/domain/workflow/loop-validation.d.ts +4 -0
- package/dist/domain/workflow/loop-validation.js +62 -0
- package/dist/domain/workflow/loop-validation.js.map +1 -0
- package/dist/domain/workflow/quota-pause.d.ts +7 -0
- package/dist/domain/workflow/quota-pause.js +83 -0
- package/dist/domain/workflow/quota-pause.js.map +1 -0
- package/dist/domain/workflow/quota-pause.types.d.ts +14 -0
- package/dist/domain/workflow/quota-pause.types.js +2 -0
- package/dist/domain/workflow/quota-pause.types.js.map +1 -0
- package/dist/domain/workflow/quota-validation.d.ts +2 -0
- package/dist/domain/workflow/quota-validation.js +30 -0
- package/dist/domain/workflow/quota-validation.js.map +1 -0
- package/dist/domain/workflow/task-cache-entry.d.ts +3 -0
- package/dist/domain/workflow/task-cache-entry.js +57 -0
- package/dist/domain/workflow/task-cache-entry.js.map +1 -0
- package/dist/domain/workflow/task-cache.constants.d.ts +3 -0
- package/dist/domain/workflow/task-cache.constants.js +3 -0
- package/dist/domain/workflow/task-cache.constants.js.map +1 -0
- package/dist/domain/workflow/task-cache.d.ts +6 -0
- package/dist/domain/workflow/task-cache.js +92 -0
- package/dist/domain/workflow/task-cache.js.map +1 -0
- package/dist/domain/workflow/task-cache.types.d.ts +35 -0
- package/dist/domain/workflow/task-cache.types.js +2 -0
- package/dist/domain/workflow/task-cache.types.js.map +1 -0
- package/dist/domain/workflow/task-runner.js +100 -34
- package/dist/domain/workflow/task-runner.js.map +1 -1
- package/dist/domain/workflow/task.js +21 -0
- package/dist/domain/workflow/task.js.map +1 -1
- package/dist/domain/workflow/validation.js +13 -2
- package/dist/domain/workflow/validation.js.map +1 -1
- package/dist/domain/workflow/workflow-scheduler.js +27 -2
- package/dist/domain/workflow/workflow-scheduler.js.map +1 -1
- package/dist/domain/workflow/workflow-state.js +20 -3
- package/dist/domain/workflow/workflow-state.js.map +1 -1
- package/dist/domain/workflow.types.d.ts +26 -3
- package/dist/domain/workflow.types.js.map +1 -1
- package/dist/index.d.ts +16 -1
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/infrastructure/git/command.d.ts +2 -1
- package/dist/infrastructure/git/command.js +2 -2
- package/dist/infrastructure/git/command.js.map +1 -1
- package/dist/infrastructure/git/replay-commits.constants.d.ts +5 -0
- package/dist/infrastructure/git/replay-commits.constants.js +6 -0
- package/dist/infrastructure/git/replay-commits.constants.js.map +1 -0
- package/dist/infrastructure/git/replay-commits.d.ts +2 -0
- package/dist/infrastructure/git/replay-commits.js +92 -0
- package/dist/infrastructure/git/replay-commits.js.map +1 -0
- package/dist/infrastructure/journal.types.d.ts +1 -0
- package/dist/infrastructure/journal.types.js.map +1 -1
- package/dist/infrastructure/reporter.js +3 -0
- package/dist/infrastructure/reporter.js.map +1 -1
- package/dist/infrastructure/storage-inventory.types.d.ts +1 -1
- package/dist/infrastructure/storage-inventory.types.js.map +1 -1
- package/dist/infrastructure/transport-task-cache.constants.d.ts +2 -0
- package/dist/infrastructure/transport-task-cache.constants.js +3 -0
- package/dist/infrastructure/transport-task-cache.constants.js.map +1 -0
- package/dist/infrastructure/transport-task-cache.d.ts +3 -0
- package/dist/infrastructure/transport-task-cache.js +56 -0
- package/dist/infrastructure/transport-task-cache.js.map +1 -0
- package/dist/infrastructure/transport-task-cache.types.d.ts +4 -0
- package/dist/infrastructure/transport-task-cache.types.js +2 -0
- package/dist/infrastructure/transport-task-cache.types.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"loop-runner.js","sourceRoot":"","sources":["../../../src/domain/workflow/loop-runner.ts"],"names":[],"mappings":"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","sourcesContent":["import { checkpointValue } from \"./checkpoint-value.ts\";\nimport { LoopTaskExhausted } from \"./loop-task.ts\";\nimport { validateLoopCheck } from \"./loop-validation.ts\";\nimport type { Task, WorkflowExecutionState } from \"../workflow.types.ts\";\nimport type {\n LoopDefinition,\n LoopRoundRecord,\n LoopTaskContext,\n} from \"./loop-task.types.ts\";\n\nexport async function runLoopTask(\n item: Task,\n definition: LoopDefinition,\n runtime: WorkflowExecutionState,\n): Promise<unknown> {\n const state = runtime.record(item);\n let rounds = state.rounds ?? Object.freeze([]);\n async function save(entry: LoopRoundRecord): Promise<void> {\n rounds = Object.freeze([\n ...rounds.filter((value) => value.round !== entry.round),\n Object.freeze(entry),\n ]);\n state.rounds = rounds;\n runtime.emit({\n type: \"loop\",\n key: item.key,\n attempt: state.attempts,\n round: entry.round,\n phase: entry.phase,\n });\n await runtime.persist();\n }\n while (true) {\n runtime.signal.throwIfAborted();\n const last = rounds.at(-1);\n if (last?.check?.done)\n return last.output?.kind === \"json\" ? last.output.value : undefined;\n if (last?.phase === \"complete\" && last.round === definition.maxRounds)\n throw new LoopTaskExhausted(\n item.key,\n definition.maxRounds,\n last.check?.done === false ? last.check.feedback : \"\",\n );\n const entry: LoopRoundRecord =\n last && last.phase !== \"complete\"\n ? last\n : { round: rounds.length + 1, phase: \"attempt\" };\n runtime.accounting.admit();\n state.attempts++;\n runtime.emit({ type: \"attempt\", key: item.key, attempt: state.attempts });\n await save(entry);\n const deadline = new AbortController();\n const timer =\n item.timeoutMs === undefined\n ? undefined\n : setTimeout(\n () => deadline.abort(new Error(`${item.key} timed out`)),\n item.timeoutMs,\n );\n const signal = AbortSignal.any([runtime.signal, deadline.signal]);\n const base = runtime.context(item, state.attempts, signal);\n const context = (phase: LoopTaskContext[\"phase\"]): LoopTaskContext => ({\n ...base,\n round: entry.round,\n phase,\n idempotencyKey: `${base.idempotencyKey}:round:${entry.round}:${phase}`,\n });\n try {\n signal.throwIfAborted();\n let result: unknown =\n entry.output?.kind === \"json\"\n ? structuredClone(entry.output.value)\n : undefined;\n if (entry.phase === \"attempt\") {\n const previous = rounds.at(-2)?.check;\n result = await definition.attempt(\n context(\"attempt\"),\n previous?.done === false ? previous.feedback : undefined,\n );\n signal.throwIfAborted();\n const output = runtime.options.checkpoint\n ? checkpointValue(result)\n : undefined;\n await save({\n round: entry.round,\n phase: \"check\",\n ...(output ? { output } : {}),\n });\n }\n signal.throwIfAborted();\n const checked = await definition.check(context(\"check\"), result);\n signal.throwIfAborted();\n validateLoopCheck(checked);\n const check = Object.freeze(\n checked.done\n ? { done: true as const }\n : { done: false as const, feedback: checked.feedback },\n );\n await save({\n ...rounds.at(-1)!,\n phase: \"complete\",\n check,\n ...(runtime.options.checkpoint\n ? { output: checkpointValue(result) }\n : {}),\n });\n signal.throwIfAborted();\n if (check.done) return result;\n } finally {\n runtime.closeAttempt(item);\n clearTimeout(timer);\n }\n }\n}\n"]}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Task } from "../workflow.types.ts";
|
|
2
|
+
import type { LoopDefinition, LoopTaskOptions } from "./loop-task.types.ts";
|
|
3
|
+
export declare class LoopTaskExhausted extends Error {
|
|
4
|
+
readonly key: string;
|
|
5
|
+
readonly maxRounds: number;
|
|
6
|
+
readonly feedback: string;
|
|
7
|
+
constructor(key: string, maxRounds: number, feedback: string);
|
|
8
|
+
}
|
|
9
|
+
export declare function loopTask<T>(options: LoopTaskOptions<T>): Task<T>;
|
|
10
|
+
export declare function loopDefinition(item: Task): LoopDefinition | undefined;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { task } from "./task.js";
|
|
2
|
+
const definitions = new WeakMap();
|
|
3
|
+
export class LoopTaskExhausted extends Error {
|
|
4
|
+
key;
|
|
5
|
+
maxRounds;
|
|
6
|
+
feedback;
|
|
7
|
+
constructor(key, maxRounds, feedback) {
|
|
8
|
+
super(`Loop task ${key} exhausted its ${maxRounds} rounds`);
|
|
9
|
+
this.name = "LoopTaskExhausted";
|
|
10
|
+
this.key = key;
|
|
11
|
+
this.maxRounds = maxRounds;
|
|
12
|
+
this.feedback = feedback;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export function loopTask(options) {
|
|
16
|
+
if (!Number.isSafeInteger(options.maxRounds) || options.maxRounds < 1)
|
|
17
|
+
throw new Error("maxRounds must be a positive safe integer");
|
|
18
|
+
if (typeof options.attempt !== "function" ||
|
|
19
|
+
typeof options.check !== "function")
|
|
20
|
+
throw new Error("loopTask requires attempt and check callbacks");
|
|
21
|
+
const { maxRounds, attempt, check, ...base } = options;
|
|
22
|
+
const item = task({
|
|
23
|
+
...base,
|
|
24
|
+
perform() {
|
|
25
|
+
throw new Error("loopTask must be executed by a workflow");
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
definitions.set(item, Object.freeze({
|
|
29
|
+
maxRounds,
|
|
30
|
+
attempt,
|
|
31
|
+
// The task's paired callbacks share T; checkpoint versions bind restored outputs to that contract.
|
|
32
|
+
check: (context, result) => check(context, result),
|
|
33
|
+
}));
|
|
34
|
+
return item;
|
|
35
|
+
}
|
|
36
|
+
export function loopDefinition(item) {
|
|
37
|
+
return definitions.get(item);
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=loop-task.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"loop-task.js","sourceRoot":"","sources":["../../../src/domain/workflow/loop-task.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAIjC,MAAM,WAAW,GAAG,IAAI,OAAO,EAAwB,CAAC;AAExD,MAAM,OAAO,iBAAkB,SAAQ,KAAK;IACjC,GAAG,CAAS;IACZ,SAAS,CAAS;IAClB,QAAQ,CAAS;IAE1B,YAAY,GAAW,EAAE,SAAiB,EAAE,QAAgB;QAC1D,KAAK,CAAC,aAAa,GAAG,kBAAkB,SAAS,SAAS,CAAC,CAAC;QAC5D,IAAI,CAAC,IAAI,GAAG,mBAAmB,CAAC;QAChC,IAAI,CAAC,GAAG,GAAG,GAAG,CAAC;QACf,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;QAC3B,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;IAC3B,CAAC;CACF;AAED,MAAM,UAAU,QAAQ,CAAI,OAA2B;IACrD,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,OAAO,CAAC,SAAS,CAAC,IAAI,OAAO,CAAC,SAAS,GAAG,CAAC;QACnE,MAAM,IAAI,KAAK,CAAC,2CAA2C,CAAC,CAAC;IAC/D,IACE,OAAO,OAAO,CAAC,OAAO,KAAK,UAAU;QACrC,OAAO,OAAO,CAAC,KAAK,KAAK,UAAU;QAEnC,MAAM,IAAI,KAAK,CAAC,+CAA+C,CAAC,CAAC;IACnE,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,KAAK,EAAE,GAAG,IAAI,EAAE,GAAG,OAAO,CAAC;IACvD,MAAM,IAAI,GAAG,IAAI,CAAI;QACnB,GAAG,IAAI;QACP,OAAO;YACL,MAAM,IAAI,KAAK,CAAC,yCAAyC,CAAC,CAAC;QAC7D,CAAC;KACF,CAAC,CAAC;IACH,WAAW,CAAC,GAAG,CACb,IAAI,EACJ,MAAM,CAAC,MAAM,CAAiB;QAC5B,SAAS;QACT,OAAO;QACP,mGAAmG;QACnG,KAAK,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,CAAC,KAAK,CAAC,OAAO,EAAE,MAAW,CAAC;KACxD,CAAC,CACH,CAAC;IACF,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,IAAU;IACvC,OAAO,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;AAC/B,CAAC","sourcesContent":["import { task } from \"./task.ts\";\nimport type { Task } from \"../workflow.types.ts\";\nimport type { LoopDefinition, LoopTaskOptions } from \"./loop-task.types.ts\";\n\nconst definitions = new WeakMap<Task, LoopDefinition>();\n\nexport class LoopTaskExhausted extends Error {\n readonly key: string;\n readonly maxRounds: number;\n readonly feedback: string;\n\n constructor(key: string, maxRounds: number, feedback: string) {\n super(`Loop task ${key} exhausted its ${maxRounds} rounds`);\n this.name = \"LoopTaskExhausted\";\n this.key = key;\n this.maxRounds = maxRounds;\n this.feedback = feedback;\n }\n}\n\nexport function loopTask<T>(options: LoopTaskOptions<T>): Task<T> {\n if (!Number.isSafeInteger(options.maxRounds) || options.maxRounds < 1)\n throw new Error(\"maxRounds must be a positive safe integer\");\n if (\n typeof options.attempt !== \"function\" ||\n typeof options.check !== \"function\"\n )\n throw new Error(\"loopTask requires attempt and check callbacks\");\n const { maxRounds, attempt, check, ...base } = options;\n const item = task<T>({\n ...base,\n perform() {\n throw new Error(\"loopTask must be executed by a workflow\");\n },\n });\n definitions.set(\n item,\n Object.freeze<LoopDefinition>({\n maxRounds,\n attempt,\n // The task's paired callbacks share T; checkpoint versions bind restored outputs to that contract.\n check: (context, result) => check(context, result as T),\n }),\n );\n return item;\n}\n\nexport function loopDefinition(item: Task): LoopDefinition | undefined {\n return definitions.get(item);\n}\n"]}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { TaskContext, TaskOptions } from "../workflow.types.ts";
|
|
2
|
+
import type { WorkflowCheckpointValue } from "./checkpoint.types.ts";
|
|
3
|
+
export type LoopCheckResult = {
|
|
4
|
+
readonly done: true;
|
|
5
|
+
} | {
|
|
6
|
+
readonly done: false;
|
|
7
|
+
readonly feedback: string;
|
|
8
|
+
};
|
|
9
|
+
export interface LoopTaskContext extends TaskContext {
|
|
10
|
+
readonly round: number;
|
|
11
|
+
readonly phase: "attempt" | "check";
|
|
12
|
+
}
|
|
13
|
+
export interface LoopTaskOptions<T> extends Pick<TaskOptions<T>, "key" | "after" | "condition" | "timeoutMs" | "cache"> {
|
|
14
|
+
readonly maxRounds: number;
|
|
15
|
+
readonly attempt: (context: LoopTaskContext, feedback: string | undefined) => T | Promise<T>;
|
|
16
|
+
readonly check: (context: LoopTaskContext, result: T) => LoopCheckResult | Promise<LoopCheckResult>;
|
|
17
|
+
}
|
|
18
|
+
export interface LoopRoundRecord {
|
|
19
|
+
readonly round: number;
|
|
20
|
+
readonly phase: "attempt" | "check" | "complete";
|
|
21
|
+
readonly output?: WorkflowCheckpointValue;
|
|
22
|
+
readonly check?: LoopCheckResult;
|
|
23
|
+
}
|
|
24
|
+
export interface LoopDefinition {
|
|
25
|
+
readonly maxRounds: number;
|
|
26
|
+
readonly attempt: (context: LoopTaskContext, feedback: string | undefined) => unknown | Promise<unknown>;
|
|
27
|
+
readonly check: (context: LoopTaskContext, result: unknown) => LoopCheckResult | Promise<LoopCheckResult>;
|
|
28
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"loop-task.types.js","sourceRoot":"","sources":["../../../src/domain/workflow/loop-task.types.ts"],"names":[],"mappings":"","sourcesContent":["import type { TaskContext, TaskOptions } from \"../workflow.types.ts\";\nimport type { WorkflowCheckpointValue } from \"./checkpoint.types.ts\";\n\nexport type LoopCheckResult =\n { readonly done: true } | { readonly done: false; readonly feedback: string };\n\nexport interface LoopTaskContext extends TaskContext {\n readonly round: number;\n readonly phase: \"attempt\" | \"check\";\n}\n\nexport interface LoopTaskOptions<T> extends Pick<\n TaskOptions<T>,\n \"key\" | \"after\" | \"condition\" | \"timeoutMs\" | \"cache\"\n> {\n readonly maxRounds: number;\n readonly attempt: (\n context: LoopTaskContext,\n feedback: string | undefined,\n ) => T | Promise<T>;\n readonly check: (\n context: LoopTaskContext,\n result: T,\n ) => LoopCheckResult | Promise<LoopCheckResult>;\n}\n\nexport interface LoopRoundRecord {\n readonly round: number;\n readonly phase: \"attempt\" | \"check\" | \"complete\";\n readonly output?: WorkflowCheckpointValue;\n readonly check?: LoopCheckResult;\n}\n\nexport interface LoopDefinition {\n readonly maxRounds: number;\n readonly attempt: (\n context: LoopTaskContext,\n feedback: string | undefined,\n ) => unknown | Promise<unknown>;\n readonly check: (\n context: LoopTaskContext,\n result: unknown,\n ) => LoopCheckResult | Promise<LoopCheckResult>;\n}\n"]}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { Task } from "../workflow.types.ts";
|
|
2
|
+
import type { LoopCheckResult } from "./loop-task.types.ts";
|
|
3
|
+
export declare function validateLoopCheck(value: unknown): asserts value is LoopCheckResult;
|
|
4
|
+
export declare function validateLoopRecord(record: Record<string, unknown>, item: Task): void;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { checkpointValue } from "./checkpoint-value.js";
|
|
2
|
+
import { loopDefinition } from "./loop-task.js";
|
|
3
|
+
function object(value) {
|
|
4
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5
|
+
}
|
|
6
|
+
export function validateLoopCheck(value) {
|
|
7
|
+
if (!object(value) ||
|
|
8
|
+
typeof value.done !== "boolean" ||
|
|
9
|
+
(!value.done && typeof value.feedback !== "string"))
|
|
10
|
+
throw new Error("Loop check must return { done: true } or { done: false, feedback: string }");
|
|
11
|
+
}
|
|
12
|
+
export function validateLoopRecord(record, item) {
|
|
13
|
+
const definition = loopDefinition(item);
|
|
14
|
+
const invalid = () => new Error("Invalid or incompatible workflow loop checkpoint");
|
|
15
|
+
if (record.rounds === undefined) {
|
|
16
|
+
if (definition && (record.attempts !== 0 || record.status === "done"))
|
|
17
|
+
throw invalid();
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
if (!definition ||
|
|
21
|
+
!Array.isArray(record.rounds) ||
|
|
22
|
+
record.rounds.length === 0 ||
|
|
23
|
+
record.rounds.length > definition.maxRounds ||
|
|
24
|
+
record.rounds.length > Number(record.attempts))
|
|
25
|
+
throw invalid();
|
|
26
|
+
for (const [index, entry] of record.rounds.entries()) {
|
|
27
|
+
if (!object(entry) ||
|
|
28
|
+
entry.round !== index + 1 ||
|
|
29
|
+
!["attempt", "check", "complete"].includes(String(entry.phase)))
|
|
30
|
+
throw invalid();
|
|
31
|
+
const final = index === record.rounds.length - 1;
|
|
32
|
+
if (!final && entry.phase !== "complete")
|
|
33
|
+
throw invalid();
|
|
34
|
+
if (entry.phase === "attempt") {
|
|
35
|
+
if (entry.output !== undefined || entry.check !== undefined)
|
|
36
|
+
throw invalid();
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (!object(entry.output) ||
|
|
40
|
+
!["undefined", "json"].includes(String(entry.output.kind)))
|
|
41
|
+
throw invalid();
|
|
42
|
+
if (entry.output.kind === "json") {
|
|
43
|
+
if (!Object.hasOwn(entry.output, "value") ||
|
|
44
|
+
entry.output.value === undefined)
|
|
45
|
+
throw invalid();
|
|
46
|
+
checkpointValue(entry.output.value);
|
|
47
|
+
}
|
|
48
|
+
if (entry.phase === "check") {
|
|
49
|
+
if (entry.check !== undefined)
|
|
50
|
+
throw invalid();
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
validateLoopCheck(entry.check);
|
|
54
|
+
if (entry.check.done && !final)
|
|
55
|
+
throw invalid();
|
|
56
|
+
}
|
|
57
|
+
const last = record.rounds.at(-1);
|
|
58
|
+
if (record.status === "done" &&
|
|
59
|
+
(!object(last) || !object(last.check) || last.check.done !== true))
|
|
60
|
+
throw invalid();
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=loop-validation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"loop-validation.js","sourceRoot":"","sources":["../../../src/domain/workflow/loop-validation.ts"],"names":[],"mappings":"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","sourcesContent":["import { checkpointValue } from \"./checkpoint-value.ts\";\nimport { loopDefinition } from \"./loop-task.ts\";\nimport type { Task } from \"../workflow.types.ts\";\nimport type { LoopCheckResult } from \"./loop-task.types.ts\";\n\nfunction object(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nexport function validateLoopCheck(\n value: unknown,\n): asserts value is LoopCheckResult {\n if (\n !object(value) ||\n typeof value.done !== \"boolean\" ||\n (!value.done && typeof value.feedback !== \"string\")\n )\n throw new Error(\n \"Loop check must return { done: true } or { done: false, feedback: string }\",\n );\n}\n\nexport function validateLoopRecord(\n record: Record<string, unknown>,\n item: Task,\n): void {\n const definition = loopDefinition(item);\n const invalid = () =>\n new Error(\"Invalid or incompatible workflow loop checkpoint\");\n if (record.rounds === undefined) {\n if (definition && (record.attempts !== 0 || record.status === \"done\"))\n throw invalid();\n return;\n }\n if (\n !definition ||\n !Array.isArray(record.rounds) ||\n record.rounds.length === 0 ||\n record.rounds.length > definition.maxRounds ||\n record.rounds.length > Number(record.attempts)\n )\n throw invalid();\n for (const [index, entry] of record.rounds.entries()) {\n if (\n !object(entry) ||\n entry.round !== index + 1 ||\n ![\"attempt\", \"check\", \"complete\"].includes(String(entry.phase))\n )\n throw invalid();\n const final = index === record.rounds.length - 1;\n if (!final && entry.phase !== \"complete\") throw invalid();\n if (entry.phase === \"attempt\") {\n if (entry.output !== undefined || entry.check !== undefined)\n throw invalid();\n continue;\n }\n if (\n !object(entry.output) ||\n ![\"undefined\", \"json\"].includes(String(entry.output.kind))\n )\n throw invalid();\n if (entry.output.kind === \"json\") {\n if (\n !Object.hasOwn(entry.output, \"value\") ||\n entry.output.value === undefined\n )\n throw invalid();\n checkpointValue(entry.output.value);\n }\n if (entry.phase === \"check\") {\n if (entry.check !== undefined) throw invalid();\n continue;\n }\n validateLoopCheck(entry.check);\n if (entry.check.done && !final) throw invalid();\n }\n const last: unknown = record.rounds.at(-1);\n if (\n record.status === \"done\" &&\n (!object(last) || !object(last.check) || last.check.done !== true)\n )\n throw invalid();\n}\n"]}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Task, WorkflowExecutionState } from "../workflow.types.ts";
|
|
2
|
+
import type { WorkflowQuotaPolicy } from "./quota-pause.types.ts";
|
|
3
|
+
export declare function validateQuotaPolicy(policy: WorkflowQuotaPolicy): void;
|
|
4
|
+
export declare function quotaResumes(runtime: WorkflowExecutionState): Set<Task>;
|
|
5
|
+
export declare function pauseForQuota(item: Task, runtime: WorkflowExecutionState, error: unknown): Promise<boolean>;
|
|
6
|
+
/** Waits for a paused task's reset when allowed; false leaves the task paused. */
|
|
7
|
+
export declare function awaitQuotaReset(item: Task, runtime: WorkflowExecutionState, resuming: boolean): Promise<boolean>;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { recoveryDetails } from "../errors.js";
|
|
2
|
+
import { quotaFault } from "../quota.js";
|
|
3
|
+
import { waitForRetry } from "./retry.js";
|
|
4
|
+
export function validateQuotaPolicy(policy) {
|
|
5
|
+
if (policy.action !== "pause")
|
|
6
|
+
throw new Error("onQuota.action must be pause");
|
|
7
|
+
const wait = policy.maxWaitMs;
|
|
8
|
+
if (wait !== undefined && (!Number.isSafeInteger(wait) || wait < 0))
|
|
9
|
+
throw new Error("onQuota.maxWaitMs must be a nonnegative safe integer");
|
|
10
|
+
}
|
|
11
|
+
/** Milliseconds before a paused task may run again, or undefined to stay paused. */
|
|
12
|
+
function resumeDelay(pause, maxWaitMs, now) {
|
|
13
|
+
if (pause.resetAt === undefined)
|
|
14
|
+
return 0;
|
|
15
|
+
const delay = Math.max(0, Date.parse(pause.resetAt) - now);
|
|
16
|
+
return delay <= maxWaitMs ? delay : undefined;
|
|
17
|
+
}
|
|
18
|
+
export function quotaResumes(runtime) {
|
|
19
|
+
const maxWaitMs = runtime.options.onQuota?.maxWaitMs ?? 0;
|
|
20
|
+
const now = Date.now();
|
|
21
|
+
const resumes = new Set();
|
|
22
|
+
for (const [item, record] of runtime.records)
|
|
23
|
+
if (record.status === "paused" &&
|
|
24
|
+
record.quota &&
|
|
25
|
+
resumeDelay(record.quota, maxWaitMs, now) !== undefined)
|
|
26
|
+
resumes.add(item);
|
|
27
|
+
return resumes;
|
|
28
|
+
}
|
|
29
|
+
export async function pauseForQuota(item, runtime, error) {
|
|
30
|
+
const fault = runtime.options.onQuota ? quotaFault(error) : undefined;
|
|
31
|
+
if (!fault || runtime.signal.aborted)
|
|
32
|
+
return false;
|
|
33
|
+
const state = runtime.record(item);
|
|
34
|
+
const recovery = recoveryDetails(error);
|
|
35
|
+
const captured = typeof recovery?.transcript === "string" ||
|
|
36
|
+
(fault.conversation !== undefined &&
|
|
37
|
+
recovery?.conversation === fault.conversation);
|
|
38
|
+
state.quota = Object.freeze({
|
|
39
|
+
requestedAt: new Date().toISOString(),
|
|
40
|
+
message: fault.message,
|
|
41
|
+
...(fault.resetAt === undefined ? {} : { resetAt: fault.resetAt }),
|
|
42
|
+
...(captured && fault.conversation
|
|
43
|
+
? { conversation: fault.conversation }
|
|
44
|
+
: {}),
|
|
45
|
+
...(typeof recovery?.branch === "string" && recovery.branch
|
|
46
|
+
? { branch: recovery.branch }
|
|
47
|
+
: {}),
|
|
48
|
+
});
|
|
49
|
+
runtime.finish(item, "paused");
|
|
50
|
+
await runtime.persist();
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
/** Waits for a paused task's reset when allowed; false leaves the task paused. */
|
|
54
|
+
export async function awaitQuotaReset(item, runtime, resuming) {
|
|
55
|
+
const state = runtime.record(item);
|
|
56
|
+
const pause = state.quota;
|
|
57
|
+
const maxWaitMs = runtime.options.onQuota?.maxWaitMs ?? 0;
|
|
58
|
+
const now = Date.now();
|
|
59
|
+
const future = pause.resetAt !== undefined && Date.parse(pause.resetAt) > now;
|
|
60
|
+
const delay = resuming || future ? resumeDelay(pause, maxWaitMs, now) : undefined;
|
|
61
|
+
runtime.emit({
|
|
62
|
+
type: "quota",
|
|
63
|
+
key: item.key,
|
|
64
|
+
attempt: state.attempts,
|
|
65
|
+
status: delay === undefined ? "paused" : "waiting",
|
|
66
|
+
...(pause.resetAt === undefined ? {} : { resetAt: pause.resetAt }),
|
|
67
|
+
...(delay ? { delayMs: delay } : {}),
|
|
68
|
+
});
|
|
69
|
+
if (delay === undefined)
|
|
70
|
+
return false;
|
|
71
|
+
try {
|
|
72
|
+
await waitForRetry(delay, runtime.signal);
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
if (runtime.signal.aborted)
|
|
76
|
+
return false;
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
runtime.quotaResumes.set(item, pause);
|
|
80
|
+
delete state.quota;
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=quota-pause.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"quota-pause.js","sourceRoot":"","sources":["../../../src/domain/workflow/quota-pause.ts"],"names":[],"mappings":"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","sourcesContent":["import { recoveryDetails } from \"../errors.ts\";\nimport { quotaFault } from \"../quota.ts\";\nimport { waitForRetry } from \"./retry.ts\";\nimport type { Task, WorkflowExecutionState } from \"../workflow.types.ts\";\nimport type {\n WorkflowQuotaPause,\n WorkflowQuotaPolicy,\n} from \"./quota-pause.types.ts\";\n\nexport function validateQuotaPolicy(policy: WorkflowQuotaPolicy): void {\n if (policy.action !== \"pause\")\n throw new Error(\"onQuota.action must be pause\");\n const wait = policy.maxWaitMs;\n if (wait !== undefined && (!Number.isSafeInteger(wait) || wait < 0))\n throw new Error(\"onQuota.maxWaitMs must be a nonnegative safe integer\");\n}\n\n/** Milliseconds before a paused task may run again, or undefined to stay paused. */\nfunction resumeDelay(\n pause: WorkflowQuotaPause,\n maxWaitMs: number,\n now: number,\n): number | undefined {\n if (pause.resetAt === undefined) return 0;\n const delay = Math.max(0, Date.parse(pause.resetAt) - now);\n return delay <= maxWaitMs ? delay : undefined;\n}\n\nexport function quotaResumes(runtime: WorkflowExecutionState): Set<Task> {\n const maxWaitMs = runtime.options.onQuota?.maxWaitMs ?? 0;\n const now = Date.now();\n const resumes = new Set<Task>();\n for (const [item, record] of runtime.records)\n if (\n record.status === \"paused\" &&\n record.quota &&\n resumeDelay(record.quota, maxWaitMs, now) !== undefined\n )\n resumes.add(item);\n return resumes;\n}\n\nexport async function pauseForQuota(\n item: Task,\n runtime: WorkflowExecutionState,\n error: unknown,\n): Promise<boolean> {\n const fault = runtime.options.onQuota ? quotaFault(error) : undefined;\n if (!fault || runtime.signal.aborted) return false;\n const state = runtime.record(item);\n const recovery = recoveryDetails(error);\n const captured =\n typeof recovery?.transcript === \"string\" ||\n (fault.conversation !== undefined &&\n recovery?.conversation === fault.conversation);\n state.quota = Object.freeze({\n requestedAt: new Date().toISOString(),\n message: fault.message,\n ...(fault.resetAt === undefined ? {} : { resetAt: fault.resetAt }),\n ...(captured && fault.conversation\n ? { conversation: fault.conversation }\n : {}),\n ...(typeof recovery?.branch === \"string\" && recovery.branch\n ? { branch: recovery.branch }\n : {}),\n });\n runtime.finish(item, \"paused\");\n await runtime.persist();\n return true;\n}\n\n/** Waits for a paused task's reset when allowed; false leaves the task paused. */\nexport async function awaitQuotaReset(\n item: Task,\n runtime: WorkflowExecutionState,\n resuming: boolean,\n): Promise<boolean> {\n const state = runtime.record(item);\n const pause = state.quota!;\n const maxWaitMs = runtime.options.onQuota?.maxWaitMs ?? 0;\n const now = Date.now();\n const future = pause.resetAt !== undefined && Date.parse(pause.resetAt) > now;\n const delay =\n resuming || future ? resumeDelay(pause, maxWaitMs, now) : undefined;\n runtime.emit({\n type: \"quota\",\n key: item.key,\n attempt: state.attempts,\n status: delay === undefined ? \"paused\" : \"waiting\",\n ...(pause.resetAt === undefined ? {} : { resetAt: pause.resetAt }),\n ...(delay ? { delayMs: delay } : {}),\n });\n if (delay === undefined) return false;\n try {\n await waitForRetry(delay, runtime.signal);\n } catch (error) {\n if (runtime.signal.aborted) return false;\n throw error;\n }\n runtime.quotaResumes.set(item, pause);\n delete state.quota;\n return true;\n}\n"]}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface WorkflowQuotaPolicy {
|
|
2
|
+
readonly action: "pause";
|
|
3
|
+
/** Longest in-process wait for a known reset; later resets pause durably. */
|
|
4
|
+
readonly maxWaitMs?: number;
|
|
5
|
+
}
|
|
6
|
+
export interface WorkflowQuotaPause {
|
|
7
|
+
readonly requestedAt: string;
|
|
8
|
+
readonly message: string;
|
|
9
|
+
readonly resetAt?: string;
|
|
10
|
+
/** Captured conversation of the interrupted attempt, portable to a new sandbox. */
|
|
11
|
+
readonly conversation?: string;
|
|
12
|
+
/** Retained work branch of the interrupted attempt. */
|
|
13
|
+
readonly branch?: string;
|
|
14
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"quota-pause.types.js","sourceRoot":"","sources":["../../../src/domain/workflow/quota-pause.types.ts"],"names":[],"mappings":"","sourcesContent":["export interface WorkflowQuotaPolicy {\n readonly action: \"pause\";\n /** Longest in-process wait for a known reset; later resets pause durably. */\n readonly maxWaitMs?: number;\n}\n\nexport interface WorkflowQuotaPause {\n readonly requestedAt: string;\n readonly message: string;\n readonly resetAt?: string;\n /** Captured conversation of the interrupted attempt, portable to a new sandbox. */\n readonly conversation?: string;\n /** Retained work branch of the interrupted attempt. */\n readonly branch?: string;\n}\n"]}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
function timestamp(value) {
|
|
2
|
+
return typeof value === "string" && Number.isFinite(Date.parse(value));
|
|
3
|
+
}
|
|
4
|
+
export function validateQuotaRecord(record, item) {
|
|
5
|
+
const pause = record.quota;
|
|
6
|
+
if (pause === undefined)
|
|
7
|
+
return;
|
|
8
|
+
const invalid = () => new Error("Invalid workflow checkpoint quota record");
|
|
9
|
+
if (item.gate ||
|
|
10
|
+
record.status !== "paused" ||
|
|
11
|
+
record.attempts === 0 ||
|
|
12
|
+
pause === null ||
|
|
13
|
+
typeof pause !== "object" ||
|
|
14
|
+
Array.isArray(pause))
|
|
15
|
+
throw invalid();
|
|
16
|
+
const fields = pause;
|
|
17
|
+
if (Object.keys(fields).some((key) => ![
|
|
18
|
+
"requestedAt",
|
|
19
|
+
"message",
|
|
20
|
+
"resetAt",
|
|
21
|
+
"conversation",
|
|
22
|
+
"branch",
|
|
23
|
+
].includes(key)) ||
|
|
24
|
+
!timestamp(fields.requestedAt) ||
|
|
25
|
+
typeof fields.message !== "string" ||
|
|
26
|
+
(fields.resetAt !== undefined && !timestamp(fields.resetAt)) ||
|
|
27
|
+
[fields.conversation, fields.branch].some((value) => value !== undefined && (typeof value !== "string" || !value)))
|
|
28
|
+
throw invalid();
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=quota-validation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"quota-validation.js","sourceRoot":"","sources":["../../../src/domain/workflow/quota-validation.ts"],"names":[],"mappings":"AAEA,SAAS,SAAS,CAAC,KAAc;IAC/B,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC;AACzE,CAAC;AAED,MAAM,UAAU,mBAAmB,CACjC,MAA+B,EAC/B,IAAU;IAEV,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC;IAC3B,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO;IAChC,MAAM,OAAO,GAAG,GAAG,EAAE,CAAC,IAAI,KAAK,CAAC,0CAA0C,CAAC,CAAC;IAC5E,IACE,IAAI,CAAC,IAAI;QACT,MAAM,CAAC,MAAM,KAAK,QAAQ;QAC1B,MAAM,CAAC,QAAQ,KAAK,CAAC;QACrB,KAAK,KAAK,IAAI;QACd,OAAO,KAAK,KAAK,QAAQ;QACzB,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAEpB,MAAM,OAAO,EAAE,CAAC;IAClB,MAAM,MAAM,GAAG,KAAgC,CAAC;IAChD,IACE,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,IAAI,CACtB,CAAC,GAAG,EAAE,EAAE,CACN,CAAC;QACC,aAAa;QACb,SAAS;QACT,SAAS;QACT,cAAc;QACd,QAAQ;KACT,CAAC,QAAQ,CAAC,GAAG,CAAC,CAClB;QACD,CAAC,SAAS,CAAC,MAAM,CAAC,WAAW,CAAC;QAC9B,OAAO,MAAM,CAAC,OAAO,KAAK,QAAQ;QAClC,CAAC,MAAM,CAAC,OAAO,KAAK,SAAS,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QAC5D,CAAC,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CACvC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,KAAK,SAAS,IAAI,CAAC,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,CACxE;QAED,MAAM,OAAO,EAAE,CAAC;AACpB,CAAC","sourcesContent":["import type { Task } from \"../workflow.types.ts\";\n\nfunction timestamp(value: unknown): boolean {\n return typeof value === \"string\" && Number.isFinite(Date.parse(value));\n}\n\nexport function validateQuotaRecord(\n record: Record<string, unknown>,\n item: Task,\n): void {\n const pause = record.quota;\n if (pause === undefined) return;\n const invalid = () => new Error(\"Invalid workflow checkpoint quota record\");\n if (\n item.gate ||\n record.status !== \"paused\" ||\n record.attempts === 0 ||\n pause === null ||\n typeof pause !== \"object\" ||\n Array.isArray(pause)\n )\n throw invalid();\n const fields = pause as Record<string, unknown>;\n if (\n Object.keys(fields).some(\n (key) =>\n ![\n \"requestedAt\",\n \"message\",\n \"resetAt\",\n \"conversation\",\n \"branch\",\n ].includes(key),\n ) ||\n !timestamp(fields.requestedAt) ||\n typeof fields.message !== \"string\" ||\n (fields.resetAt !== undefined && !timestamp(fields.resetAt)) ||\n [fields.conversation, fields.branch].some(\n (value) => value !== undefined && (typeof value !== \"string\" || !value),\n )\n )\n throw invalid();\n}\n"]}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { TaskCacheEntry } from "./task-cache.types.ts";
|
|
2
|
+
export declare function taskCacheFingerprint(workflow: string, task: string, version: string, key: unknown): string;
|
|
3
|
+
export declare function validateTaskCacheEntry(value: unknown, fingerprint: string): asserts value is TaskCacheEntry;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { checkpointValue } from "./checkpoint-value.js";
|
|
3
|
+
import { taskCacheFormat } from "./task-cache.constants.js";
|
|
4
|
+
function object(value) {
|
|
5
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6
|
+
}
|
|
7
|
+
function canonicalJson(value) {
|
|
8
|
+
if (value === null || typeof value !== "object")
|
|
9
|
+
return JSON.stringify(value);
|
|
10
|
+
if (Array.isArray(value))
|
|
11
|
+
return `[${value.map((item) => canonicalJson(item)).join(",")}]`;
|
|
12
|
+
const fields = Object.entries(value);
|
|
13
|
+
return `{${fields
|
|
14
|
+
.sort(([left], [right]) => (left < right ? -1 : 1))
|
|
15
|
+
.map(([key, item]) => `${JSON.stringify(key)}:${canonicalJson(item)}`)
|
|
16
|
+
.join(",")}}`;
|
|
17
|
+
}
|
|
18
|
+
export function taskCacheFingerprint(workflow, task, version, key) {
|
|
19
|
+
let parts;
|
|
20
|
+
try {
|
|
21
|
+
parts = checkpointValue(key);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
throw new Error("Task cache key must be a lossless JSON value");
|
|
25
|
+
}
|
|
26
|
+
if (parts.kind !== "json")
|
|
27
|
+
throw new Error("Task cache key must be a lossless JSON value");
|
|
28
|
+
return createHash("sha256")
|
|
29
|
+
.update(canonicalJson({
|
|
30
|
+
format: taskCacheFormat,
|
|
31
|
+
workflow,
|
|
32
|
+
task,
|
|
33
|
+
version,
|
|
34
|
+
key: parts.value,
|
|
35
|
+
}))
|
|
36
|
+
.digest("hex");
|
|
37
|
+
}
|
|
38
|
+
export function validateTaskCacheEntry(value, fingerprint) {
|
|
39
|
+
const invalid = () => new Error("Invalid task cache entry");
|
|
40
|
+
if (!object(value) ||
|
|
41
|
+
value.format !== taskCacheFormat ||
|
|
42
|
+
value.fingerprint !== fingerprint ||
|
|
43
|
+
typeof value.workflow !== "string" ||
|
|
44
|
+
typeof value.task !== "string" ||
|
|
45
|
+
typeof value.version !== "string" ||
|
|
46
|
+
typeof value.createdAt !== "string" ||
|
|
47
|
+
!Number.isFinite(Date.parse(value.createdAt)) ||
|
|
48
|
+
!object(value.value))
|
|
49
|
+
throw invalid();
|
|
50
|
+
const output = value.value;
|
|
51
|
+
if (output.kind === "undefined" && !Object.hasOwn(output, "value"))
|
|
52
|
+
return;
|
|
53
|
+
if (output.kind !== "json" || output.value === undefined)
|
|
54
|
+
throw invalid();
|
|
55
|
+
checkpointValue(output.value);
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=task-cache-entry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"task-cache-entry.js","sourceRoot":"","sources":["../../../src/domain/workflow/task-cache-entry.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash } from \"node:crypto\";\nimport { checkpointValue } from \"./checkpoint-value.ts\";\nimport { taskCacheFormat } from \"./task-cache.constants.ts\";\nimport type { WorkflowJson } from \"./checkpoint.types.ts\";\nimport type { TaskCacheEntry } from \"./task-cache.types.ts\";\n\nfunction object(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction canonicalJson(value: WorkflowJson): string {\n if (value === null || typeof value !== \"object\") return JSON.stringify(value);\n if (Array.isArray(value))\n return `[${value.map((item: WorkflowJson) => canonicalJson(item)).join(\",\")}]`;\n const fields = Object.entries(value as Record<string, WorkflowJson>);\n return `{${fields\n .sort(([left], [right]) => (left < right ? -1 : 1))\n .map(([key, item]) => `${JSON.stringify(key)}:${canonicalJson(item)}`)\n .join(\",\")}}`;\n}\n\nexport function taskCacheFingerprint(\n workflow: string,\n task: string,\n version: string,\n key: unknown,\n): string {\n let parts;\n try {\n parts = checkpointValue(key);\n } catch {\n throw new Error(\"Task cache key must be a lossless JSON value\");\n }\n if (parts.kind !== \"json\")\n throw new Error(\"Task cache key must be a lossless JSON value\");\n return createHash(\"sha256\")\n .update(\n canonicalJson({\n format: taskCacheFormat,\n workflow,\n task,\n version,\n key: parts.value,\n }),\n )\n .digest(\"hex\");\n}\n\nexport function validateTaskCacheEntry(\n value: unknown,\n fingerprint: string,\n): asserts value is TaskCacheEntry {\n const invalid = () => new Error(\"Invalid task cache entry\");\n if (\n !object(value) ||\n value.format !== taskCacheFormat ||\n value.fingerprint !== fingerprint ||\n typeof value.workflow !== \"string\" ||\n typeof value.task !== \"string\" ||\n typeof value.version !== \"string\" ||\n typeof value.createdAt !== \"string\" ||\n !Number.isFinite(Date.parse(value.createdAt)) ||\n !object(value.value)\n )\n throw invalid();\n const output = value.value;\n if (output.kind === \"undefined\" && !Object.hasOwn(output, \"value\")) return;\n if (output.kind !== \"json\" || output.value === undefined) throw invalid();\n checkpointValue(output.value);\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"task-cache.constants.js","sourceRoot":"","sources":["../../../src/domain/workflow/task-cache.constants.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,CAAC;AACjC,MAAM,CAAC,MAAM,cAAc,GAA6B,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC","sourcesContent":["import type { TaskCacheMode } from \"./task-cache.types.ts\";\n\nexport const taskCacheFormat = 1;\nexport const taskCacheModes: readonly TaskCacheMode[] = [\"reuse\", \"refresh\"];\n"]}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Task, WorkflowExecutionState } from "../workflow.types.ts";
|
|
2
|
+
import type { TaskCacheLookup, TaskCacheOptions } from "./task-cache.types.ts";
|
|
3
|
+
export declare function validateTaskCache(options: TaskCacheOptions): void;
|
|
4
|
+
/** Key errors fail the task; store and entry errors only degrade to a miss. */
|
|
5
|
+
export declare function lookupTaskCache(item: Task, cache: TaskCacheOptions, runtime: WorkflowExecutionState): Promise<TaskCacheLookup>;
|
|
6
|
+
export declare function storeTaskCache(item: Task, cache: TaskCacheOptions, runtime: WorkflowExecutionState, fingerprint: string, value: unknown): Promise<void>;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { checkpointValue } from "./checkpoint-value.js";
|
|
2
|
+
import { taskCacheFormat, taskCacheModes } from "./task-cache.constants.js";
|
|
3
|
+
import { taskCacheFingerprint, validateTaskCacheEntry, } from "./task-cache-entry.js";
|
|
4
|
+
function message(error) {
|
|
5
|
+
return error instanceof Error ? error.message : String(error);
|
|
6
|
+
}
|
|
7
|
+
function notify(item, runtime, cache, error) {
|
|
8
|
+
runtime.emit({
|
|
9
|
+
type: "cache",
|
|
10
|
+
key: item.key,
|
|
11
|
+
cache,
|
|
12
|
+
...(error === undefined ? {} : { error: message(error) }),
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
export function validateTaskCache(options) {
|
|
16
|
+
if (!options ||
|
|
17
|
+
typeof options !== "object" ||
|
|
18
|
+
typeof options.store?.read !== "function" ||
|
|
19
|
+
typeof options.store.write !== "function" ||
|
|
20
|
+
typeof options.key !== "function")
|
|
21
|
+
throw new Error("Task cache requires a store and a key function");
|
|
22
|
+
if (typeof options.version !== "string" || !options.version.trim())
|
|
23
|
+
throw new Error("Task cache version must not be empty");
|
|
24
|
+
if (options.maxAgeMs !== undefined &&
|
|
25
|
+
(!Number.isSafeInteger(options.maxAgeMs) || options.maxAgeMs < 1))
|
|
26
|
+
throw new Error("cache.maxAgeMs must be a positive integer");
|
|
27
|
+
if (options.mode !== undefined && !taskCacheModes.includes(options.mode))
|
|
28
|
+
throw new Error("cache.mode must be reuse or refresh");
|
|
29
|
+
}
|
|
30
|
+
/** Key errors fail the task; store and entry errors only degrade to a miss. */
|
|
31
|
+
export async function lookupTaskCache(item, cache, runtime) {
|
|
32
|
+
const { signal } = runtime;
|
|
33
|
+
const key = await cache.key(runtime.context(item, 0, signal));
|
|
34
|
+
signal.throwIfAborted();
|
|
35
|
+
const fingerprint = taskCacheFingerprint(runtime.workflow, item.key, cache.version, key);
|
|
36
|
+
if (cache.mode === "refresh") {
|
|
37
|
+
notify(item, runtime, "miss");
|
|
38
|
+
return { hit: false, fingerprint };
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
const entry = await cache.store.read(fingerprint, { signal });
|
|
42
|
+
signal.throwIfAborted();
|
|
43
|
+
if (entry === undefined) {
|
|
44
|
+
notify(item, runtime, "miss");
|
|
45
|
+
return { hit: false, fingerprint };
|
|
46
|
+
}
|
|
47
|
+
validateTaskCacheEntry(entry, fingerprint);
|
|
48
|
+
if (cache.maxAgeMs !== undefined &&
|
|
49
|
+
Date.now() - Date.parse(entry.createdAt) > cache.maxAgeMs) {
|
|
50
|
+
notify(item, runtime, "miss");
|
|
51
|
+
return { hit: false, fingerprint };
|
|
52
|
+
}
|
|
53
|
+
notify(item, runtime, "hit");
|
|
54
|
+
return {
|
|
55
|
+
hit: true,
|
|
56
|
+
value: entry.value.kind === "json"
|
|
57
|
+
? structuredClone(entry.value.value)
|
|
58
|
+
: undefined,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
signal.throwIfAborted();
|
|
63
|
+
notify(item, runtime, "failed", error);
|
|
64
|
+
return { hit: false, fingerprint };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
export async function storeTaskCache(item, cache, runtime, fingerprint, value) {
|
|
68
|
+
let output;
|
|
69
|
+
try {
|
|
70
|
+
output = checkpointValue(value);
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
throw new Error(`Cached task results must be lossless JSON values or undefined: ${message(error)}`);
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
await cache.store.write(Object.freeze({
|
|
77
|
+
format: taskCacheFormat,
|
|
78
|
+
fingerprint,
|
|
79
|
+
workflow: runtime.workflow,
|
|
80
|
+
task: item.key,
|
|
81
|
+
version: cache.version,
|
|
82
|
+
createdAt: new Date().toISOString(),
|
|
83
|
+
value: output,
|
|
84
|
+
}), { signal: runtime.signal });
|
|
85
|
+
notify(item, runtime, "stored");
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
runtime.signal.throwIfAborted();
|
|
89
|
+
notify(item, runtime, "failed", error);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=task-cache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"task-cache.js","sourceRoot":"","sources":["../../../src/domain/workflow/task-cache.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAC5E,OAAO,EACL,oBAAoB,EACpB,sBAAsB,GACvB,MAAM,uBAAuB,CAAC;AAQ/B,SAAS,OAAO,CAAC,KAAc;IAC7B,OAAO,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAChE,CAAC;AAED,SAAS,MAAM,CACb,IAAU,EACV,OAA+B,EAC/B,KAAuB,EACvB,KAAe;IAEf,OAAO,CAAC,IAAI,CAAC;QACX,IAAI,EAAE,OAAO;QACb,GAAG,EAAE,IAAI,CAAC,GAAG;QACb,KAAK;QACL,GAAG,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;KAC1D,CAAC,CAAC;AACL,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,OAAyB;IACzD,IACE,CAAC,OAAO;QACR,OAAO,OAAO,KAAK,QAAQ;QAC3B,OAAO,OAAO,CAAC,KAAK,EAAE,IAAI,KAAK,UAAU;QACzC,OAAO,OAAO,CAAC,KAAK,CAAC,KAAK,KAAK,UAAU;QACzC,OAAO,OAAO,CAAC,GAAG,KAAK,UAAU;QAEjC,MAAM,IAAI,KAAK,CAAC,gDAAgD,CAAC,CAAC;IACpE,IAAI,OAAO,OAAO,CAAC,OAAO,KAAK,QAAQ,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,EAAE;QAChE,MAAM,IAAI,KAAK,CAAC,sCAAsC,CAAC,CAAC;IAC1D,IACE,OAAO,CAAC,QAAQ,KAAK,SAAS;QAC9B,CAAC,CAAC,MAAM,CAAC,aAAa,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;QAEjE,MAAM,IAAI,KAAK,CAAC,2CAA2C,CAAC,CAAC;IAC/D,IAAI,OAAO,CAAC,IAAI,KAAK,SAAS,IAAI,CAAC,cAAc,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC;QACtE,MAAM,IAAI,KAAK,CAAC,qCAAqC,CAAC,CAAC;AAC3D,CAAC;AAED,+EAA+E;AAC/E,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,IAAU,EACV,KAAuB,EACvB,OAA+B;IAE/B,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC;IAC3B,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;IAC9D,MAAM,CAAC,cAAc,EAAE,CAAC;IACxB,MAAM,WAAW,GAAG,oBAAoB,CACtC,OAAO,CAAC,QAAQ,EAChB,IAAI,CAAC,GAAG,EACR,KAAK,CAAC,OAAO,EACb,GAAG,CACJ,CAAC;IACF,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QAC7B,MAAM,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC;QAC9B,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,WAAW,EAAE,CAAC;IACrC,CAAC;IACD,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,MAAM,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC;QAC9D,MAAM,CAAC,cAAc,EAAE,CAAC;QACxB,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;YACxB,MAAM,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC;YAC9B,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,WAAW,EAAE,CAAC;QACrC,CAAC;QACD,sBAAsB,CAAC,KAAK,EAAE,WAAW,CAAC,CAAC;QAC3C,IACE,KAAK,CAAC,QAAQ,KAAK,SAAS;YAC5B,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,SAAS,CAAC,GAAG,KAAK,CAAC,QAAQ,EACzD,CAAC;YACD,MAAM,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC;YAC9B,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,WAAW,EAAE,CAAC;QACrC,CAAC;QACD,MAAM,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC;QAC7B,OAAO;YACL,GAAG,EAAE,IAAI;YACT,KAAK,EACH,KAAK,CAAC,KAAK,CAAC,IAAI,KAAK,MAAM;gBACzB,CAAC,CAAC,eAAe,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC;gBACpC,CAAC,CAAC,SAAS;SAChB,CAAC;IACJ,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,CAAC,cAAc,EAAE,CAAC;QACxB,MAAM,CAAC,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAC;QACvC,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,WAAW,EAAE,CAAC;IACrC,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,IAAU,EACV,KAAuB,EACvB,OAA+B,EAC/B,WAAmB,EACnB,KAAc;IAEd,IAAI,MAAM,CAAC;IACX,IAAI,CAAC;QACH,MAAM,GAAG,eAAe,CAAC,KAAK,CAAC,CAAC;IAClC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,IAAI,KAAK,CACb,kEAAkE,OAAO,CAAC,KAAK,CAAC,EAAE,CACnF,CAAC;IACJ,CAAC;IACD,IAAI,CAAC;QACH,MAAM,KAAK,CAAC,KAAK,CAAC,KAAK,CACrB,MAAM,CAAC,MAAM,CAAC;YACZ,MAAM,EAAE,eAAe;YACvB,WAAW;YACX,QAAQ,EAAE,OAAO,CAAC,QAAQ;YAC1B,IAAI,EAAE,IAAI,CAAC,GAAG;YACd,OAAO,EAAE,KAAK,CAAC,OAAO;YACtB,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;YACnC,KAAK,EAAE,MAAM;SACd,CAAC,EACF,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAC3B,CAAC;QACF,MAAM,CAAC,IAAI,EAAE,OAAO,EAAE,QAAQ,CAAC,CAAC;IAClC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,MAAM,CAAC,cAAc,EAAE,CAAC;QAChC,MAAM,CAAC,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAC;IACzC,CAAC;AACH,CAAC","sourcesContent":["import { checkpointValue } from \"./checkpoint-value.ts\";\nimport { taskCacheFormat, taskCacheModes } from \"./task-cache.constants.ts\";\nimport {\n taskCacheFingerprint,\n validateTaskCacheEntry,\n} from \"./task-cache-entry.ts\";\nimport type { Task, WorkflowExecutionState } from \"../workflow.types.ts\";\nimport type {\n TaskCacheLookup,\n TaskCacheOptions,\n TaskCacheOutcome,\n} from \"./task-cache.types.ts\";\n\nfunction message(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\nfunction notify(\n item: Task,\n runtime: WorkflowExecutionState,\n cache: TaskCacheOutcome,\n error?: unknown,\n): void {\n runtime.emit({\n type: \"cache\",\n key: item.key,\n cache,\n ...(error === undefined ? {} : { error: message(error) }),\n });\n}\n\nexport function validateTaskCache(options: TaskCacheOptions): void {\n if (\n !options ||\n typeof options !== \"object\" ||\n typeof options.store?.read !== \"function\" ||\n typeof options.store.write !== \"function\" ||\n typeof options.key !== \"function\"\n )\n throw new Error(\"Task cache requires a store and a key function\");\n if (typeof options.version !== \"string\" || !options.version.trim())\n throw new Error(\"Task cache version must not be empty\");\n if (\n options.maxAgeMs !== undefined &&\n (!Number.isSafeInteger(options.maxAgeMs) || options.maxAgeMs < 1)\n )\n throw new Error(\"cache.maxAgeMs must be a positive integer\");\n if (options.mode !== undefined && !taskCacheModes.includes(options.mode))\n throw new Error(\"cache.mode must be reuse or refresh\");\n}\n\n/** Key errors fail the task; store and entry errors only degrade to a miss. */\nexport async function lookupTaskCache(\n item: Task,\n cache: TaskCacheOptions,\n runtime: WorkflowExecutionState,\n): Promise<TaskCacheLookup> {\n const { signal } = runtime;\n const key = await cache.key(runtime.context(item, 0, signal));\n signal.throwIfAborted();\n const fingerprint = taskCacheFingerprint(\n runtime.workflow,\n item.key,\n cache.version,\n key,\n );\n if (cache.mode === \"refresh\") {\n notify(item, runtime, \"miss\");\n return { hit: false, fingerprint };\n }\n try {\n const entry = await cache.store.read(fingerprint, { signal });\n signal.throwIfAborted();\n if (entry === undefined) {\n notify(item, runtime, \"miss\");\n return { hit: false, fingerprint };\n }\n validateTaskCacheEntry(entry, fingerprint);\n if (\n cache.maxAgeMs !== undefined &&\n Date.now() - Date.parse(entry.createdAt) > cache.maxAgeMs\n ) {\n notify(item, runtime, \"miss\");\n return { hit: false, fingerprint };\n }\n notify(item, runtime, \"hit\");\n return {\n hit: true,\n value:\n entry.value.kind === \"json\"\n ? structuredClone(entry.value.value)\n : undefined,\n };\n } catch (error) {\n signal.throwIfAborted();\n notify(item, runtime, \"failed\", error);\n return { hit: false, fingerprint };\n }\n}\n\nexport async function storeTaskCache(\n item: Task,\n cache: TaskCacheOptions,\n runtime: WorkflowExecutionState,\n fingerprint: string,\n value: unknown,\n): Promise<void> {\n let output;\n try {\n output = checkpointValue(value);\n } catch (error) {\n throw new Error(\n `Cached task results must be lossless JSON values or undefined: ${message(error)}`,\n );\n }\n try {\n await cache.store.write(\n Object.freeze({\n format: taskCacheFormat,\n fingerprint,\n workflow: runtime.workflow,\n task: item.key,\n version: cache.version,\n createdAt: new Date().toISOString(),\n value: output,\n }),\n { signal: runtime.signal },\n );\n notify(item, runtime, \"stored\");\n } catch (error) {\n runtime.signal.throwIfAborted();\n notify(item, runtime, \"failed\", error);\n }\n}\n"]}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { TaskContext } from "../workflow.types.ts";
|
|
2
|
+
import type { WorkflowCheckpointValue, WorkflowJson } from "./checkpoint.types.ts";
|
|
3
|
+
export type TaskCacheMode = "reuse" | "refresh";
|
|
4
|
+
export type TaskCacheOutcome = "hit" | "miss" | "stored" | "failed";
|
|
5
|
+
export interface TaskCacheEntry {
|
|
6
|
+
readonly format: 1;
|
|
7
|
+
readonly fingerprint: string;
|
|
8
|
+
readonly workflow: string;
|
|
9
|
+
readonly task: string;
|
|
10
|
+
readonly version: string;
|
|
11
|
+
readonly createdAt: string;
|
|
12
|
+
readonly value: WorkflowCheckpointValue;
|
|
13
|
+
}
|
|
14
|
+
export interface TaskCacheAccessOptions {
|
|
15
|
+
readonly signal?: AbortSignal;
|
|
16
|
+
}
|
|
17
|
+
export interface TaskCacheStore {
|
|
18
|
+
read(fingerprint: string, options?: TaskCacheAccessOptions): Promise<TaskCacheEntry | undefined>;
|
|
19
|
+
write(entry: TaskCacheEntry, options?: TaskCacheAccessOptions): Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
export interface TaskCacheOptions {
|
|
22
|
+
readonly store: TaskCacheStore;
|
|
23
|
+
/** Change when the task implementation, agent or output contract changes. */
|
|
24
|
+
readonly version: string;
|
|
25
|
+
readonly key: (context: TaskContext) => WorkflowJson | Promise<WorkflowJson>;
|
|
26
|
+
readonly maxAgeMs?: number;
|
|
27
|
+
readonly mode?: TaskCacheMode;
|
|
28
|
+
}
|
|
29
|
+
export type TaskCacheLookup = {
|
|
30
|
+
readonly hit: true;
|
|
31
|
+
readonly value: unknown;
|
|
32
|
+
} | {
|
|
33
|
+
readonly hit: false;
|
|
34
|
+
readonly fingerprint: string;
|
|
35
|
+
};
|