agents
Version:
A home for your AI agents
1,656 lines • 70.5 kB
JavaScript
import { i as _classPrivateFieldInitSpec, n as _classPrivateFieldSet2, r as _assertClassBrand, t as _classPrivateFieldGet2 } from "./classPrivateFieldGet2-DZBYAB34.js";
import { t as LifecycleCapability } from "./capability-B4WbF81e.js";
import { t as _classPrivateMethodInitSpec } from "./classPrivateMethodInitSpec-qMjJ6sHQ.js";
import { isDurableObjectCodeUpdateReset, isDurableObjectMemoryLimitReset, isPlatformFailure } from "./retries.js";
import { SqlError } from "./sql-error.js";
import { nanoid } from "nanoid";
//#region src/tasks/errors.ts
/**
* Error classes for the Tasks capability. Each carries a stable `name` so
* hosts and tests can classify failures without depending on message text.
*/
/**
* Thrown by a step callback to fail its run immediately, skipping any
* remaining retry attempts.
*
* Errors named `"NonRetryableError"` from other sources (for example
* `cloudflare:workflows`) are honored the same way.
*
* @experimental The API surface may change before stabilizing.
*/
var NonRetryableError = class extends Error {
constructor(message, options) {
super(message, options);
this.name = "NonRetryableError";
}
};
/** True when an error should skip remaining step retry attempts. */
function isNonRetryableError(error) {
return error instanceof NonRetryableError || error instanceof Error && error.name === "NonRetryableError";
}
/**
* Thrown before executing user code when one replay uses the same step name
* twice. Step names are durable journal keys and must be unique within a run.
*
* @experimental The API surface may change before stabilizing.
*/
var DuplicateTaskStepError = class extends Error {
constructor(stepName) {
super(`Step name "${stepName}" was already used in this run. Step names are durable journal keys; suffix loop steps with a stable index, e.g. "${stepName}:0".`);
this.name = "DuplicateTaskStepError";
this.stepName = stepName;
}
};
/**
* Thrown when a replay observes a journal that this handler code cannot have
* written — a known step under a different kind, for example. The run fails
* rather than guessing; changing a definition's step layout for in-flight
* runs requires versioning the definition name.
*
* @experimental The API surface may change before stabilizing.
*/
var TaskReplayDivergedError = class extends Error {
constructor(stepName, detail) {
super(`Replay diverged from the journal at step "${stepName}": ${detail}. Version the definition name (e.g. "name@v2") instead of changing the step layout of in-flight runs.`);
this.name = "TaskReplayDivergedError";
this.stepName = stepName;
}
};
/**
* Recorded against a run whose persisted definition name is no longer
* registered after a deployment. The run fails visibly; it is never silently
* deleted and never replayed against a different handler.
*
* @experimental The API surface may change before stabilizing.
*/
var MissingTaskDefinitionError = class extends Error {
constructor(definition) {
super(`No Task definition named "${definition}" is registered. A deployment removed or renamed it while this run was active. Re-register the definition (or a versioned successor with the same name) to let the run finish.`);
this.name = "MissingTaskDefinitionError";
this.definition = definition;
}
};
/**
* Thrown when a Task input, step result, metadata value, or final result is
* not JSON-serializable or exceeds the serialized size limit.
*
* @experimental The API surface may change before stabilizing.
*/
var TaskSerializationError = class extends Error {
constructor(context, detail) {
super(`Cannot serialize ${context}: ${detail}`);
this.name = "TaskSerializationError";
}
};
//#endregion
//#region src/tasks/serialization.ts
/**
* Value serialization for the Tasks capability.
*
* Task inputs, step results, metadata, and final results persist as JSON
* text in SQLite. `undefined` (and a `void` handler result) is represented
* as SQL `NULL` rather than a JSON envelope, so the JSON column space stays
* plain: `"null"` is JSON `null`, column `NULL` is `undefined`.
*/
/** Default ceiling for one serialized value (1 MiB). */
const MAX_SERIALIZED_BYTES = 1048576;
const utf8 = new TextEncoder();
/**
* Serialize one Task value for storage.
*
* @param value - The value to persist.
* @param context - What is being serialized, for error messages
* (e.g. `input for definition "report"`, `result of step "fetch"`).
* @returns JSON text, or `null` when the value is `undefined`.
* @throws TaskSerializationError when the value is not JSON-serializable or
* its serialized form exceeds {@link MAX_SERIALIZED_BYTES}.
*/
function serializeTaskValue(value, context) {
if (value === void 0) return null;
let json;
try {
json = JSON.stringify(value);
} catch (error) {
throw new TaskSerializationError(context, error instanceof Error ? error.message : String(error));
}
if (json === void 0) throw new TaskSerializationError(context, `value of type ${typeof value} has no JSON representation`);
const bytes = utf8.encode(json).byteLength;
if (bytes > 1048576) throw new TaskSerializationError(context, `serialized size ${bytes} bytes exceeds the ${MAX_SERIALIZED_BYTES}-byte limit`);
return json;
}
/**
* Restore a value serialized by {@link serializeTaskValue}.
*
* @param stored - The stored column value.
* @returns The original value; column `NULL` restores `undefined`.
*/
function deserializeTaskValue(stored) {
if (stored === null) return void 0;
return JSON.parse(stored);
}
//#endregion
//#region src/tasks/store.ts
/**
* Storage layer for the Tasks capability: owns the `cf_agents_task_runs` and
* `cf_agents_task_steps` tables — DDL, row access, generation-fenced writes,
* and the snapshot projection. The engine in `tasks.ts` holds the state
* machine; every byte that touches SQLite goes through here.
*/
var _storage = /* @__PURE__ */ new WeakMap();
/** @internal SQL-backed store for one Tasks capability instance. */
var TaskStore = class {
constructor(storage) {
_classPrivateFieldInitSpec(this, _storage, void 0);
_classPrivateFieldSet2(_storage, this, storage);
}
sql(strings, ...values) {
const query = strings.reduce((result, part, index) => result + part + (index < values.length ? "?" : ""), "");
try {
return [..._classPrivateFieldGet2(_storage, this).sql.exec(query, ...values)];
} catch (cause) {
throw new SqlError(query, cause);
}
}
write(query, params) {
try {
return _classPrivateFieldGet2(_storage, this).sql.exec(query, ...params).rowsWritten;
} catch (cause) {
throw new SqlError(query, cause);
}
}
/**
* Run one generation-fenced run mutation. Returns false when the fence
* rejected it because another attempt superseded this one.
*/
fencedWrite(runId, generation, query, leadingParams) {
try {
return _classPrivateFieldGet2(_storage, this).sql.exec(query, ...leadingParams, runId, generation).rowsWritten > 0;
} catch (cause) {
throw new SqlError(query, cause);
}
}
getRun(runId) {
return this.sql`
SELECT * FROM cf_agents_task_runs WHERE run_id = ${runId}
`[0];
}
getRunByKey(idempotencyKey) {
return this.sql`
SELECT * FROM cf_agents_task_runs WHERE idempotency_key = ${idempotencyKey}
`[0];
}
deleteRun(runId) {
this.sql`DELETE FROM cf_agents_task_steps WHERE run_id = ${runId}`;
this.sql`DELETE FROM cf_agents_task_runs WHERE run_id = ${runId}`;
}
ensureTables() {
const rawSql = (query) => {
try {
_classPrivateFieldGet2(_storage, this).sql.exec(query);
} catch (cause) {
throw new SqlError(query, cause);
}
};
rawSql(`
CREATE TABLE IF NOT EXISTS cf_agents_task_runs (
run_id TEXT PRIMARY KEY,
definition TEXT NOT NULL,
input TEXT,
state TEXT NOT NULL CHECK (state IN (
'pending', 'running', 'waiting',
'completed', 'failed', 'cancelled'
)),
result TEXT,
error_name TEXT,
error_message TEXT,
status_message TEXT,
metadata TEXT,
idempotency_key TEXT UNIQUE,
retain INTEGER NOT NULL DEFAULT 1,
attempt INTEGER NOT NULL DEFAULT 0,
generation TEXT,
next_at INTEGER,
wait_reason TEXT,
cancel_requested INTEGER NOT NULL DEFAULT 0,
cancel_reason TEXT,
created_at INTEGER NOT NULL,
started_at INTEGER,
updated_at INTEGER NOT NULL,
settled_at INTEGER
) WITHOUT ROWID`);
rawSql(`
CREATE INDEX IF NOT EXISTS cf_agents_task_runs_definition
ON cf_agents_task_runs (definition, created_at)
`);
rawSql(`
CREATE TABLE IF NOT EXISTS cf_agents_task_steps (
run_id TEXT NOT NULL,
step_name TEXT NOT NULL,
kind TEXT NOT NULL CHECK (kind IN ('do', 'sleep')),
state TEXT NOT NULL CHECK (state IN (
'running', 'waiting', 'completed', 'failed'
)),
result TEXT,
error_name TEXT,
error_message TEXT,
attempt INTEGER NOT NULL DEFAULT 0,
next_at INTEGER,
created_at INTEGER NOT NULL,
started_at INTEGER,
updated_at INTEGER NOT NULL,
completed_at INTEGER,
PRIMARY KEY (run_id, step_name)
) WITHOUT ROWID`);
}
rowToSnapshot(row) {
const metadata = row.metadata !== null ? JSON.parse(row.metadata) : void 0;
const base = {
runId: row.run_id,
definition: row.definition,
createdAt: row.created_at,
...metadata !== void 0 ? { metadata } : {}
};
switch (row.state) {
case "pending": return {
...base,
state: "pending"
};
case "running": return {
...base,
state: "running",
attempt: row.attempt,
startedAt: row.started_at ?? row.created_at,
...row.status_message !== null ? { statusMessage: row.status_message } : {}
};
case "waiting": return {
...base,
state: "waiting",
reason: row.wait_reason ?? "sleep",
wakeAt: row.next_at ?? row.updated_at,
...row.status_message !== null ? { statusMessage: row.status_message } : {}
};
case "completed": return {
...base,
state: "completed",
result: deserializeTaskValue(row.result),
settledAt: row.settled_at ?? row.updated_at
};
case "failed": return {
...base,
state: "failed",
error: {
name: row.error_name ?? "Error",
message: row.error_message ?? "Task run failed"
},
settledAt: row.settled_at ?? row.updated_at
};
case "cancelled": return {
...base,
state: "cancelled",
...row.cancel_reason !== null ? { reason: row.cancel_reason } : {},
settledAt: row.settled_at ?? row.updated_at
};
}
}
};
//#endregion
//#region src/tasks/duration.ts
const UNIT_MILLISECONDS = {
second: 1e3,
minute: 60 * 1e3,
hour: 3600 * 1e3,
day: 1440 * 60 * 1e3,
week: 10080 * 60 * 1e3
};
const DURATION_PATTERN = /^(\d+(?:\.\d+)?)\s+(second|minute|hour|day|week)s?$/;
/**
* Parse a duration into whole milliseconds.
*
* @param duration - Milliseconds, or a duration string such as `"10 seconds"`.
* @param context - Name of the option being parsed, used in error messages.
* @returns The duration in milliseconds, floored to an integer.
* @throws Error when the duration is negative, not finite, or unparseable.
*/
function parseTaskDuration(duration, context) {
if (typeof duration === "number") {
if (!Number.isFinite(duration) || duration < 0) throw new Error(`Invalid ${context}: expected a non-negative number of milliseconds, got ${duration}`);
return Math.floor(duration);
}
const match = DURATION_PATTERN.exec(duration.trim());
if (!match) throw new Error(`Invalid ${context}: expected milliseconds or a duration like "10 seconds", got ${JSON.stringify(duration)}`);
const amount = Number(match[1]);
const unit = match[2];
return Math.floor(amount * UNIT_MILLISECONDS[unit]);
}
//#endregion
//#region src/tasks/replay.ts
/**
* Replay step engine for the Tasks capability.
*
* `ReplayStep` implements the `TaskStep` surface one handler attempt
* receives. It owns replay semantics — journal hits, journal misses, retry
* policy, durable sleeps, the status live gate, and duplicate/divergence
* detection — while all SQL stays behind the narrow {@link TaskStepEngine}
* port implemented by the `Tasks` capability, the single owner of the
* schema.
*/
/** Longest computed retry delay: backoff growth never exceeds one day. */
const MAX_RETRY_DELAY_MS = 1440 * 60 * 1e3;
/** Steps per run ceiling; crossing it fails the run instead of degrading. */
const MAX_STEPS_PER_RUN = 1e4;
/**
* Thrown by the engine to end one execution attempt while its run waits for
* a durable deadline (sleep or retry). Not an `Error` subclass so a step
* callback's `catch (error)` around unrelated work is less likely to swallow
* it; the capability re-checks with {@link isTaskSuspension}.
*/
var TaskSuspension = class {
constructor(wakeAt, reason) {
this.wakeAt = wakeAt;
this.reason = reason;
}
};
/** True when a thrown value is the engine's suspension signal. */
function isTaskSuspension(value) {
return value instanceof TaskSuspension;
}
/**
* Thrown by the engine when a step boundary observes the run's cancellation
* request. The capability settles the run as cancelled.
*/
var TaskCancellation = class {
constructor(reason) {
this.reason = reason;
}
};
/** True when a thrown value is the engine's cancellation signal. */
function isTaskCancellation(value) {
return value instanceof TaskCancellation;
}
/**
* Thrown by engine writes when another execution attempt has superseded this
* one. The stale attempt unwinds without settling anything; every durable
* write it might still try is generation-fenced.
*/
var AttemptSupersededError = class extends Error {
constructor(runId) {
super(`Task attempt superseded: run "${runId}" is no longer claimed by this execution attempt`);
this.name = "AttemptSupersededError";
}
};
/** Compute the delay before the next attempt after `failedAttempt` failed. */
function computeRetryDelayMs(policy, failedAttempt) {
const base = policy.retryDelayMs;
let delay;
switch (policy.backoff) {
case "constant":
delay = base;
break;
case "linear":
delay = base * failedAttempt;
break;
case "exponential":
delay = base * 2 ** (failedAttempt - 1);
break;
}
return Math.min(delay, MAX_RETRY_DELAY_MS);
}
/** Resolve one `step.do()` config against the capability defaults. */
function resolveStepPolicy(defaults, config) {
const limit = config?.retries?.limit ?? defaults.retryLimit;
if (!Number.isInteger(limit) || limit < 1) throw new Error(`Invalid step retries.limit: expected an integer >= 1, got ${limit}`);
return {
retryLimit: limit,
retryDelayMs: config?.retries?.delay !== void 0 ? parseTaskDuration(config.retries.delay, "step retries.delay") : defaults.retryDelayMs,
backoff: config?.retries?.backoff ?? defaults.backoff,
timeoutMs: config?.timeout !== void 0 ? parseTaskDuration(config.timeout, "step timeout") : defaults.timeoutMs
};
}
var _engine = /* @__PURE__ */ new WeakMap();
var _usedNames = /* @__PURE__ */ new WeakMap();
var _live = /* @__PURE__ */ new WeakMap();
var _ReplayStep_brand = /* @__PURE__ */ new WeakSet();
/**
* The `TaskStep` implementation for one execution attempt.
*
* Attempt 1 starts live. A later attempt starts silent and becomes live at
* the frontier of new ground — the first journal miss, or a step still
* waiting or running — so replayed `status()` calls from completed ground
* are suppressed instead of re-published as new progress.
*/
var ReplayStep = class {
constructor(engine, options) {
_classPrivateMethodInitSpec(this, _ReplayStep_brand);
_classPrivateFieldInitSpec(this, _engine, void 0);
_classPrivateFieldInitSpec(this, _usedNames, /* @__PURE__ */ new Set());
_classPrivateFieldInitSpec(this, _live, void 0);
_classPrivateFieldSet2(_engine, this, engine);
_classPrivateFieldSet2(_live, this, options.startsLive);
this.interrupted = options.interrupted ?? null;
}
async do(name, configOrCallback, maybeCallback) {
const config = typeof configOrCallback === "function" ? void 0 : configOrCallback;
const callback = typeof configOrCallback === "function" ? configOrCallback : maybeCallback;
if (typeof callback !== "function") throw new Error(`step.do("${name}") requires a callback`);
const policy = resolveStepPolicy(_classPrivateFieldGet2(_engine, this).defaults, config);
_assertClassBrand(_ReplayStep_brand, this, _enterStep).call(this, name);
const row = _classPrivateFieldGet2(_engine, this).readStep(name);
if (row === void 0) {
_classPrivateFieldSet2(_live, this, true);
if (_classPrivateFieldGet2(_engine, this).countSteps() >= 1e4) throw new Error(`Run exceeded ${MAX_STEPS_PER_RUN} steps; split the work across multiple Task runs`);
_classPrivateFieldGet2(_engine, this).insertStep(name, "do", null);
return _assertClassBrand(_ReplayStep_brand, this, _executeAttempt).call(this, name, 1, policy, callback);
}
if (row.kind !== "do") throw new TaskReplayDivergedError(name, `journaled as a ${row.kind} step but replayed as a do step`);
switch (row.state) {
case "completed": return deserializeTaskValue(row.result);
case "failed": throw restoreStepError(row);
case "waiting": {
_classPrivateFieldSet2(_live, this, true);
const wakeAt = row.next_at ?? Date.now();
if (Date.now() < wakeAt) throw new TaskSuspension(wakeAt, "retry");
const attempt = _classPrivateFieldGet2(_engine, this).claimStepAttempt(name);
_classPrivateFieldGet2(_engine, this).emit("task:step:retry", {
step: name,
attempt
});
return _assertClassBrand(_ReplayStep_brand, this, _executeAttempt).call(this, name, attempt, policy, callback);
}
case "running": {
_classPrivateFieldSet2(_live, this, true);
const attempt = _classPrivateFieldGet2(_engine, this).claimStepAttempt(name);
return _assertClassBrand(_ReplayStep_brand, this, _executeAttempt).call(this, name, attempt, policy, callback);
}
}
}
async sleep(name, duration) {
const durationMs = parseTaskDuration(duration, "sleep duration");
return _assertClassBrand(_ReplayStep_brand, this, _sleepAt).call(this, name, () => Date.now() + durationMs);
}
async sleepUntil(name, when) {
const wakeAt = when instanceof Date ? when.getTime() : when;
if (!Number.isFinite(wakeAt)) throw new Error(`Invalid sleepUntil time for step "${name}": ${String(when)}`);
return _assertClassBrand(_ReplayStep_brand, this, _sleepAt).call(this, name, () => wakeAt);
}
async status(message) {
if (!_classPrivateFieldGet2(_live, this)) return;
_classPrivateFieldGet2(_engine, this).writeStatus(String(message));
}
idempotencyKey(name) {
return _classPrivateFieldGet2(_engine, this).stepIdempotencyKey(name);
}
};
/** Validate a step boundary: name rules, duplicates, cancellation. */
function _enterStep(name) {
if (typeof name !== "string" || name.length === 0) throw new Error("Step names must be non-empty strings");
if (name.length > 256) throw new Error(`Step name exceeds 256 characters: "${name.slice(0, 40)}…"`);
if (name.startsWith("__cf")) throw new Error(`Step names must not use the reserved "__cf" prefix`);
if (_classPrivateFieldGet2(_usedNames, this).has(name)) throw new DuplicateTaskStepError(name);
_classPrivateFieldGet2(_usedNames, this).add(name);
const cancellation = _classPrivateFieldGet2(_engine, this).cancellationRequested();
if (cancellation) throw new TaskCancellation(cancellation.reason);
}
/** First persist wins: the recorded wake time is authoritative. */
async function _sleepAt(name, wakeTime) {
_assertClassBrand(_ReplayStep_brand, this, _enterStep).call(this, name);
const row = _classPrivateFieldGet2(_engine, this).readStep(name);
if (row === void 0) {
_classPrivateFieldSet2(_live, this, true);
const wakeAt = wakeTime();
if (wakeAt <= Date.now()) {
_classPrivateFieldGet2(_engine, this).insertCompletedSleep(name);
return;
}
_classPrivateFieldGet2(_engine, this).insertStep(name, "sleep", wakeAt);
throw new TaskSuspension(wakeAt, "sleep");
}
if (row.kind !== "sleep") throw new TaskReplayDivergedError(name, `journaled as a ${row.kind} step but replayed as a sleep step`);
if (row.state === "completed") return;
_classPrivateFieldSet2(_live, this, true);
const wakeAt = row.next_at ?? 0;
if (Date.now() < wakeAt) throw new TaskSuspension(wakeAt, "sleep");
_classPrivateFieldGet2(_engine, this).completeStep(name, void 0);
}
/** Execute one claimed attempt of a `do` step under timeout and retries. */
async function _executeAttempt(name, attempt, policy, callback) {
_classPrivateFieldGet2(_engine, this).refreshClaim();
_classPrivateFieldGet2(_engine, this).emit("task:step:started", {
step: name,
attempt
});
const timeout = new AbortController();
const onRunAbort = () => timeout.abort(_classPrivateFieldGet2(_engine, this).attemptSignal.reason);
_classPrivateFieldGet2(_engine, this).attemptSignal.addEventListener("abort", onRunAbort, { once: true });
const timer = setTimeout(() => {
timeout.abort(/* @__PURE__ */ new Error(`Step "${name}" attempt ${attempt} timed out after ${policy.timeoutMs}ms`));
}, policy.timeoutMs);
try {
const result = await _assertClassBrand(_ReplayStep_brand, this, _raceTimeout).call(this, Promise.resolve(callback({
attempt,
idempotencyKey: _classPrivateFieldGet2(_engine, this).stepIdempotencyKey(name),
signal: timeout.signal
})), timeout.signal);
_classPrivateFieldGet2(_engine, this).completeStep(name, result);
_classPrivateFieldGet2(_engine, this).emit("task:step:completed", {
step: name,
attempt
});
return result;
} catch (error) {
if (error instanceof AttemptSupersededError) throw error;
const cancellation = _classPrivateFieldGet2(_engine, this).cancellationRequested();
if (cancellation) throw new TaskCancellation(cancellation.reason);
if (isDurableObjectCodeUpdateReset(error) || isDurableObjectMemoryLimitReset(error)) throw error;
if (isPlatformFailure(error) && attempt >= policy.retryLimit) throw error;
if (isNonRetryableError(error) || error instanceof TaskSerializationError || attempt >= policy.retryLimit) {
_classPrivateFieldGet2(_engine, this).failStep(name, toErrorSummary(error));
throw error;
}
const wakeAt = Date.now() + computeRetryDelayMs(policy, attempt);
_classPrivateFieldGet2(_engine, this).waitStep(name, wakeAt);
throw new TaskSuspension(wakeAt, "retry");
} finally {
clearTimeout(timer);
_classPrivateFieldGet2(_engine, this).attemptSignal.removeEventListener("abort", onRunAbort);
}
}
/**
* Settle with the callback or its timeout, whichever finishes first. A
* callback that ignores its abort signal cannot wedge the attempt; its
* late settlement is discarded and generation fencing rejects late writes.
*/
function _raceTimeout(pending, signal) {
if (signal.aborted) return Promise.reject(signal.reason);
return new Promise((resolve, reject) => {
const onAbort = () => reject(signal.reason);
signal.addEventListener("abort", onAbort, { once: true });
pending.then((value) => {
signal.removeEventListener("abort", onAbort);
resolve(value);
}, (error) => {
signal.removeEventListener("abort", onAbort);
reject(error);
});
});
}
/** Rebuild a persisted terminal step error for rethrow. */
function restoreStepError(row) {
const error = new Error(row.error_message ?? "Step failed");
error.name = row.error_name ?? "Error";
return error;
}
/** Safe name/message projection of an arbitrary thrown value. */
function toErrorSummary(error) {
if (error instanceof Error) return {
name: error.name,
message: error.message
};
return {
name: "Error",
message: String(error)
};
}
//#endregion
//#region src/tasks/engine-port.ts
/**
* The step-engine port: the storage-side operations `ReplayStep` drives —
* journal reads and writes, generation fencing, claim refresh, progress —
* bound to one claimed attempt. `tasks.ts` owns the state machine; this
* module owns nothing but the port's construction.
*/
/** @internal Build the engine port for one claimed attempt. */
function createTaskStepEngine(deps) {
const { runId, generation } = deps;
let lastClaimWriteAt = deps.claimedAtMs;
let lastStatusMessage;
const assertCurrent = () => {
const row = deps.store.getRun(runId);
if (!row || row.generation !== generation) throw new AttemptSupersededError(runId);
};
return {
readStep: (name) => {
return deps.store.sql`
SELECT * FROM cf_agents_task_steps
WHERE run_id = ${runId} AND step_name = ${name}
`[0];
},
countSteps: () => {
return deps.store.sql`
SELECT COUNT(*) AS count FROM cf_agents_task_steps WHERE run_id = ${runId}
`[0]?.count ?? 0;
},
insertStep: (name, kind, wakeAt) => {
assertCurrent();
const now = Date.now();
deps.store.sql`
INSERT INTO cf_agents_task_steps
(run_id, step_name, kind, state, attempt, next_at, created_at,
started_at, updated_at)
VALUES
(${runId}, ${name}, ${kind},
${kind === "do" ? "running" : wakeAt === null ? "running" : "waiting"},
${kind === "do" ? 1 : 0}, ${wakeAt},
${now}, ${kind === "do" ? now : null}, ${now})
`;
},
insertCompletedSleep: (name) => {
assertCurrent();
const now = Date.now();
deps.store.sql`
INSERT INTO cf_agents_task_steps
(run_id, step_name, kind, state, attempt, next_at, created_at,
completed_at, updated_at)
VALUES
(${runId}, ${name}, 'sleep', 'completed', 0, NULL, ${now},
${now}, ${now})
`;
},
claimStepAttempt: (name) => {
assertCurrent();
const now = Date.now();
deps.store.sql`
UPDATE cf_agents_task_steps
SET state = 'running', attempt = attempt + 1, next_at = NULL,
started_at = ${now}, updated_at = ${now}
WHERE run_id = ${runId} AND step_name = ${name}
`;
return deps.store.sql`
SELECT attempt FROM cf_agents_task_steps
WHERE run_id = ${runId} AND step_name = ${name}
`[0]?.attempt ?? 1;
},
completeStep: (name, result) => {
assertCurrent();
const resultJson = serializeTaskValue(result, `result of step "${name}" in run "${runId}"`);
const now = Date.now();
deps.store.sql`
UPDATE cf_agents_task_steps
SET state = 'completed', result = ${resultJson}, next_at = NULL,
completed_at = ${now}, updated_at = ${now}
WHERE run_id = ${runId} AND step_name = ${name}
`;
},
failStep: (name, error) => {
assertCurrent();
const now = Date.now();
deps.store.sql`
UPDATE cf_agents_task_steps
SET state = 'failed', error_name = ${error.name},
error_message = ${error.message}, next_at = NULL, updated_at = ${now}
WHERE run_id = ${runId} AND step_name = ${name}
`;
},
waitStep: (name, wakeAt) => {
assertCurrent();
const now = Date.now();
deps.store.sql`
UPDATE cf_agents_task_steps
SET state = 'waiting', next_at = ${wakeAt}, updated_at = ${now}
WHERE run_id = ${runId} AND step_name = ${name}
`;
},
refreshClaim: () => {
const now = Date.now();
if (now - lastClaimWriteAt < deps.claimRefreshAfterMs) return;
if (deps.store.fencedWrite(runId, generation, `UPDATE cf_agents_task_runs SET next_at = ?, updated_at = ?
WHERE run_id = ? AND generation = ? AND state = 'running'`, [now + deps.claimTimeoutMs(), now])) lastClaimWriteAt = now;
},
writeStatus: (message) => {
if (message === lastStatusMessage) return;
if (deps.store.fencedWrite(runId, generation, `UPDATE cf_agents_task_runs SET status_message = ?, updated_at = ?
WHERE run_id = ? AND generation = ? AND state = 'running'`, [message, Date.now()])) lastStatusMessage = message;
},
cancellationRequested: () => {
const row = deps.store.getRun(runId);
if (!row || row.cancel_requested !== 1) return null;
return { reason: row.cancel_reason ?? void 0 };
},
attemptSignal: deps.signal,
emit: deps.emit,
stepIdempotencyKey: (name) => `${runId}:${name}`,
defaults: deps.defaults
};
}
//#endregion
//#region src/tasks/tasks.ts
/**
* Durable replayable execution for Lifecycle Objects. `Tasks` owns the
* `cf_agents_task_runs` and `cf_agents_task_steps` tables, the definitions registry, run
* acceptance, generation-fenced claiming, and due-run processing.
*
* Tasks consumes only the standard capability services: storage, the job
* queue, the host invocation boundary, events, and routing. A run's storage
* and step journal always live where it was accepted; only its deadline
* mirrors as one Lifecycle queue job, routed to the root Lifecycle when
* accepted on a routed sub-agent, since only the root owns the physical
* alarm. Definition handlers run through Lifecycle's host invocation
* boundary. Interrupted work replays: completed steps return journaled
* results and handlers resume from durable evidence.
*/
const taskDefinitionResolvers = /* @__PURE__ */ new WeakMap();
/**
* @internal Supply a composition-root fallback for definition names outside
* the declared map. Frameworks use this to attach internal definitions (for
* example a future Agent compatibility layer) without occupying the host's
* constructor map; resolved handlers still run inside the Lifecycle host
* boundary. The resolver must return the same definition for a name on
* every Durable Object wake, or that name's in-flight runs cannot resume.
*/
function setTaskDefinitionResolver(tasks, resolver) {
taskDefinitionResolvers.set(tasks, resolver);
}
const taskRoutedMemoryLimitHandlers = /* @__PURE__ */ new WeakMap();
/**
* @internal Supply a composition-root bridge from a routed run's sealed
* strike to the owning host's own `onAlarmMemoryLimit` hook. A root's own
* local runs already reach that hook through Lifecycle's alarm dispatch on
* the same Durable Object; a routed run's owner is a different instance,
* whose Lifecycle never observes the root's alarm directly.
*/
function setTaskRoutedMemoryLimitHandler(tasks, handler) {
taskRoutedMemoryLimitHandlers.set(tasks, handler);
}
const FIBER_SCHEMA_VERSION_KEY = "cf_agents:tasks_schema_version";
const CURRENT_FIBER_SCHEMA_VERSION = 1;
const DEFAULT_STEP_POLICY = {
retryLimit: 5,
retryDelayMs: 1e3,
backoff: "exponential",
timeoutMs: 300 * 1e3
};
/**
* Slack added to the default step timeout to form the claim deadline — the
* durable recovery backstop that wakes the object when a claimed attempt's
* isolate disappears.
*/
const CLAIM_SLACK_MS = 3e4;
const DEFAULT_LIST_LIMIT = 100;
const MAX_DEFINITION_NAME_LENGTH = 256;
/**
* Queue-job id prefix for run wakes. Run IDs are caller-selectable, so the
* job id namespaces them instead of exposing them verbatim to the shared
* job id space.
*/
const WAKE_JOB_PREFIX = "task:";
/** Normal Task deadline dispatch. */
const WAKE_JOB_FN = "wake";
/**
* A platform failure that escapes an attempt (ReplayStep rethrows once the
* step's own retry budget is spent) leaves the run claimed with a future
* `next_at`. An in-driver retry would not re-run the step — `#executeRun`
* returns at its claim guard — it would only read the claim back as a clean
* `{ rescheduleAt }`, hiding the failure from the alarm boundary. One
* attempt keeps JobDriver's platform-failure contract: the wake rejects, the
* job row is preserved, and the platform re-runs the alarm on a fresh
* invocation while the claim deadline stays the durable wake.
*/
const WAKE_JOB_RETRY = { maxAttempts: 1 };
/**
* How long one queue-driven attempt may hold the serial dispatch loop
* before detaching. Correctness never depends on the inline await — the
* claim backstop owns the durable wake — so this only trades a prompt
* inline settle for queue liveness.
*/
const DISPATCH_BUDGET_MS = 5e3;
const TERMINAL_STATES = /* @__PURE__ */ new Set([
"completed",
"failed",
"cancelled"
]);
function isTaskWakeJobPayload(value) {
return typeof value === "object" && value !== null && typeof value.runId === "string";
}
var _definitions = /* @__PURE__ */ new WeakMap();
var _registered = /* @__PURE__ */ new WeakMap();
var _active = /* @__PURE__ */ new WeakMap();
var _storeInstance = /* @__PURE__ */ new WeakMap();
var _stepDefaults = /* @__PURE__ */ new WeakMap();
var _onError = /* @__PURE__ */ new WeakMap();
var _Tasks_brand = /* @__PURE__ */ new WeakSet();
/**
* Durable replayable execution for a Lifecycle Object.
*
* Declare named definitions in the constructor and install the instance with
* `Lifecycle.use()`. The constructor map is the registry: it is rebuilt on
* every Durable Object wake, so in-flight runs always resolve their
* persisted definition names. Each definition's handler replays from the
* beginning on every execution attempt; completed steps return journaled
* results, sleeps consult persisted deadlines, and interrupted work
* continues from the first unfinished step after process loss.
*
* @experimental The API surface may change before stabilizing.
*/
var Tasks = class extends LifecycleCapability {
/**
* Create a Tasks capability.
*
* @param options - Named definitions plus default step retry/timeout
* policy and alarm batching. Declaring `definitions` types {@link run} and
* {@link handle} against the map — names and inputs are checked where the
* handlers are declared and where runs start. Names outside the map are
* rejected unless a composition-root resolver supplies them.
*/
constructor(options = {}) {
super("tasks");
_classPrivateMethodInitSpec(this, _Tasks_brand);
_classPrivateFieldInitSpec(this, _definitions, void 0);
_classPrivateFieldInitSpec(this, _registered, /* @__PURE__ */ new Map());
_classPrivateFieldInitSpec(this, _active, /* @__PURE__ */ new Map());
_classPrivateFieldInitSpec(this, _storeInstance, void 0);
_classPrivateFieldInitSpec(this, _stepDefaults, void 0);
_classPrivateFieldInitSpec(this, _onError, void 0);
_classPrivateFieldSet2(_definitions, this, options.definitions ?? {});
_classPrivateFieldSet2(_stepDefaults, this, {
retryLimit: options.retries?.limit ?? DEFAULT_STEP_POLICY.retryLimit,
retryDelayMs: options.retries?.delay !== void 0 ? parseTaskDuration(options.retries.delay, "retries.delay") : DEFAULT_STEP_POLICY.retryDelayMs,
backoff: options.retries?.backoff ?? DEFAULT_STEP_POLICY.backoff,
timeoutMs: options.stepTimeout !== void 0 ? parseTaskDuration(options.stepTimeout, "stepTimeout") : DEFAULT_STEP_POLICY.timeoutMs
});
_classPrivateFieldSet2(_onError, this, options.onError);
}
/**
* @internal Framework aperture: register one reserved (`__cf`-prefixed)
* Task definition directly on this instance, bypassing the constructor's
* `definitions` map so a host's own subclass layers can each declare their
* own `definitions` / `taskDefinitions` field without colliding with — or
* being silently clobbered by — a framework's internal names. Call once per
* name from the owning host's own constructor, unconditionally, so the
* definition is rebuilt identically on every Durable Object wake: an
* in-flight run resolves the same handler for its persisted definition name
* every time, or it cannot resume.
*
* Throws if `name` does not carry the reserved `__cf` prefix — this is not
* a general-purpose registration path; declare ordinary definitions in the
* constructor's `definitions` map instead — or if `name` is already
* registered, which is always a real conflict: this method runs exactly
* once per name per Tasks construction.
*/
register(name, definition) {
if (typeof name !== "string" || name.length === 0) throw new Error("Task definition names must be non-empty strings");
if (name.length > MAX_DEFINITION_NAME_LENGTH) throw new Error(`Task definition name exceeds ${MAX_DEFINITION_NAME_LENGTH} characters`);
if (!name.startsWith("__cf")) throw new Error(`register() requires a "__cf"-prefixed reserved definition name, got "${name}"`);
if (Object.hasOwn(_classPrivateFieldGet2(_definitions, this), name) || _classPrivateFieldGet2(_registered, this).has(name)) throw new Error(`Task definition "${name}" is already registered on this Tasks capability`);
_classPrivateFieldGet2(_registered, this).set(name, definition);
}
/**
* Durably accept one run of a declared definition and return a receipt
* without waiting for terminal state. The same `idempotencyKey` or `runId`
* joins the existing run (`accepted: false`) instead of creating a second.
*/
async run(definition, input, options) {
_assertClassBrand(_Tasks_brand, this, _validateDefinitionName).call(this, definition);
return _assertClassBrand(_Tasks_brand, this, _accept).call(this, definition, input, options);
}
/**
* A typed handle scoped to one declared definition: its `run`, `get`,
* `getByIdempotencyKey`, and `cancel` see only that definition's runs. The
* handle is a pure lens over this capability — it holds no state and may
* be created at any time.
*/
handle(definition) {
_assertClassBrand(_Tasks_brand, this, _validateDefinitionName).call(this, definition);
return {
name: definition,
run: (input, options) => this.run(definition, input, options),
get: (runId) => _assertClassBrand(_Tasks_brand, this, _snapshot).call(this, runId, definition),
getByIdempotencyKey: (idempotencyKey) => _assertClassBrand(_Tasks_brand, this, _snapshotByKey).call(this, idempotencyKey, definition),
cancel: (runId, reason) => _assertClassBrand(_Tasks_brand, this, _cancelScoped).call(this, runId, definition, reason)
};
}
/** Migrate storage and reconcile run deadlines during Lifecycle startup. */
async onStart() {
const storage = this.lifecycle.storage;
if ((await storage.get(FIBER_SCHEMA_VERSION_KEY) ?? 0) < CURRENT_FIBER_SCHEMA_VERSION) {
_get_store.call(_assertClassBrand(_Tasks_brand, this)).ensureTables();
await storage.put(FIBER_SCHEMA_VERSION_KEY, CURRENT_FIBER_SCHEMA_VERSION);
}
_assertClassBrand(_Tasks_brand, this, _reconcile).call(this);
await _assertClassBrand(_Tasks_brand, this, _syncAllWakes).call(this);
}
/** Drive one due run's wake dispatched by the Lifecycle event loop. */
async onJob(context) {
const timing = isTaskWakeJobPayload(context.job.payload) ? context.job.payload : void 0;
const runId = timing?.runId ?? context.job.id.slice(5);
if (timing?.owner_path) {
const target = {
key: timing.owner_path_key ?? timing.owner_path,
data: timing.owner_path
};
const call = this.lifecycle.routes.to(target, {
type: "dispatch",
runId
});
let budgetTimer;
const budget = new Promise((resolve) => {
budgetTimer = setTimeout(() => resolve("budget"), DISPATCH_BUDGET_MS);
});
try {
const winner = await Promise.race([call.then((outcome) => ({ outcome })), budget]);
if (winner === "budget") {
this.lifecycle.trackAlarmWork(call);
return;
}
return winner.outcome;
} catch (error) {
if (isPlatformFailure(error)) throw error;
console.error(`error dispatching routed Task run "${runId}"`, error);
return "yield";
} finally {
clearTimeout(budgetTimer);
}
}
return _assertClassBrand(_Tasks_brand, this, _dispatchRun).call(this, runId);
}
/**
* Alarm memory-limit breaker policy (#1825) for the run whose wake struck.
*
* The run row is the durable source of truth: startup reconciliation
* re-derives due-now wakes from it, so the breaker's queue-row backoff
* and purge alone cannot contain a run whose attempt deterministically
* exhausts memory — a fresh isolate would resurrect it immediately. On a
* strike the run's claim is stripped and its deadline pushed to the
* backoff wake: the row keeps its state, so a struck `running` row still
* reads as an interrupted attempt (`step.interrupted`) when it is
* reclaimed, while reconciliation leaves the claimless row alone instead
* of flooring its deadline to now. When the breaker seals, the run
* terminally fails with an observable `task:failed` outcome.
*/
async onMemoryLimit(context) {
const job = context.executing;
if (job?.capability !== this.capabilityId) return;
const timing = isTaskWakeJobPayload(job.payload) ? job.payload : void 0;
const runId = timing?.runId ?? job.id.slice(5);
if (timing?.owner_path) {
try {
await this.lifecycle.routes.to({
key: timing.owner_path_key ?? timing.owner_path,
data: timing.owner_path
}, {
type: "memoryLimit",
runId,
context
});
} catch (error) {
console.error(`Failed to route memory-limit policy for Task run "${runId}"`, error);
}
return;
}
await _assertClassBrand(_Tasks_brand, this, _applyMemoryLimit).call(this, runId, context);
}
/**
* @internal Framework aperture: durably accept one run — reserved
* (`__cf`-prefixed) definition names included, which the public `run()`
* refuses so users cannot start framework runs — and drive its first
* attempt in the caller's invocation, resolving when that attempt reaches
* its next durable boundary. The receipt's run may already be terminal
* when this resolves; callers that need the outcome read it from their own
* channel (the run handler settles it) or from the snapshot.
*/
async __DO_NOT_USE_WILL_BREAK__runAttached(definition, input, options) {
const receipt = await _assertClassBrand(_Tasks_brand, this, _acceptReserved).call(this, definition, input, options, "attached");
if (receipt.accepted) await _assertClassBrand(_Tasks_brand, this, _executeRun).call(this, receipt.runId);
return receipt;
}
/**
* @internal Framework aperture: durably accept one run — reserved names
* included — and leave its first attempt to the durable queue wake instead
* of warm-starting it in the caller's invocation. Chat recovery uses this
* so a continuation always runs under an alarm, where `trackAlarmWork`
* keeps its model turn inside the memory-limit breaker domain.
*/
async __DO_NOT_USE_WILL_BREAK__enqueue(definition, input, options) {
return _assertClassBrand(_Tasks_brand, this, _acceptReserved).call(this, definition, input, options, "queued");
}
/** Handle Tasks protocol messages routed by another Lifecycle. */
async onRoute(context) {
const message = context.payload;
switch (message.type) {
case "syncWake": {
const owner = context.source;
if (!owner) throw new Error("Routed Tasks message missing source");
return _assertClassBrand(_Tasks_brand, this, _syncRoutedWake).call(this, owner, message.runId, message.next);
}
case "dispatch": return _assertClassBrand(_Tasks_brand, this, _dispatchRoutedRun).call(this, message.runId);
case "memoryLimit": {
await _assertClassBrand(_Tasks_brand, this, _applyMemoryLimit).call(this, message.runId, message.context);
const handler = taskRoutedMemoryLimitHandlers.get(this);
if (handler) await this.lifecycle.runInHostContext(() => handler(message.context));
return true;
}
default: throw new Error("Unknown routed Tasks message");
}
}
/**
* @internal Framework aperture: bulk-cancel this root's routed wake
* mirrors for every run owned by a deleted facet subtree. The runs and
* their step journals live on the deleted facets' own storage and are
* wiped with them; only this root's mirror job needs an explicit cancel,
* or it stays due forever, retrying a dispatch to a facet that is gone.
*/
async __DO_NOT_USE_WILL_BREAK__cleanupRoutePrefix(prefix) {
for (const job of this.lifecycle.jobs.list()) {
const timing = isTaskWakeJobPayload(job.payload) ? job.payload : void 0;
const ownerKey = timing?.owner_path_key ?? timing?.owner_path;
if (!timing?.owner_path || ownerKey === null || ownerKey === void 0) continue;
if (ownerKey !== prefix && !ownerKey.startsWith(`${prefix}/`)) continue;
await this.lifecycle.jobs.cancel(job.id);
}
}
/** Read one run by ID across all definitions. */
async get(runId) {
return _assertClassBrand(_Tasks_brand, this, _snapshot).call(this, runId);
}
/** Read one run by idempotency key across all definitions. */
async getByIdempotencyKey(idempotencyKey) {
return _assertClassBrand(_Tasks_brand, this, _snapshotByKey).call(this, idempotencyKey);
}
/** List runs, newest first. */
async list(options = {}) {
await this.lifecycle.ready();
let query = "SELECT * FROM cf_agents_task_runs WHERE 1 = 1";
const params = [];
if (options.definition !== void 0) {
query += " AND definition = ?";
params.push(options.definition);
}
const states = Array.isArray(options.status) ? options.status : options.status !== void 0 ? [options.status] : [];
if (states.length > 0) {
query += ` AND state IN (${states.map(() => "?").join(", ")})`;
params.push(...states);
}
query += " ORDER BY created_at DESC, run_id DESC LIMIT ?";
params.push(options.limit ?? DEFAULT_LIST_LIMIT);
let rows;
try {
rows = this.lifecycle.storage.sql.exec(query, ...params).toArray();
} catch (cause) {
throw new SqlError(query, cause);
}
return rows.map((row) => _get_store.call(_assertClassBrand(_Tasks_brand, this)).rowToSnapshot(row));
}
/**
* Request cooperative cancellation of one run.
*
* A live attempt is aborted and settles as cancelled at its next step
* boundary; a parked run settles immediately.
*
* @returns True when a non-terminal run accepted the request.
*/
async cancel(runId, reason) {
await this.lifecycle.ready();
const row = _get_store.call(_assertClassBrand(_Tasks_brand, this)).getRun(runId);
if (!row || TERMINAL_STATES.has(row.state)) return false;
const active = _classPrivateFieldGet2(_active, this).get(runId);
if (active) {
const now = Date.now();
_get_store.call(_assertClassBrand(_Tasks_brand, this)).sql`
UPDATE cf_agents_task_runs
SET cancel_requested = 1, cancel_reason = ${reason ?? null},
next_at = ${now}, updated_at = ${now}
WHERE run_id = ${runId}
`;
active.controller.abort(new TaskCancellation(reason));
await _assertClassBrand(_Tasks_brand, this, _syncWake).call(this, runId);
return true;
}
await _assertClassBrand(_Tasks_brand, this, _settleCancelled).call(this, runId, null, reason);
return true;
}
/**
* Delete retained terminal runs and their step journals.
*
* @returns The number of runs deleted.
*/
async delete(options = {}) {
await this.lifecycle.ready();
const states = options.status ?? [
"completed",
"failed",
"cancelled"
];
if (states.length === 0) return 0;
let query = `SELECT run_id, definition FROM cf_agents_task_runs WHERE state IN (${states.map(() => "?").join(", ")})`;
const params = [...states];
if (options.settledBefore) {
query += " AND settled_at < ?";
params.push(options.settledBefore.getTime());
}
query += " ORDER BY settled_at ASC LIMIT ?";
params.push(options.limit ?? DEFAULT_LIST_LIMIT);
let rows;
try {
rows = this.lifecycle.storage.sql.exec(query, ...params).toArray();
} catch (cause) {
throw new SqlError(query, cause);
}
for (const row of rows) {
_get_store.call(_assertClassBrand(_Tasks_brand, this)).deleteRun(row.run_id);
_assertClassBrand(_Tasks_brand, this, _emit).call(this, "task:deleted", {
runId: row.run_id,
definition: row.definition
});
}
return rows.length;
}
};
function _claimTimeoutMs() {
return _classPrivateFieldGet2(_stepDefaults, this).timeoutMs + CLAIM_SLACK_MS;
}
/** The SQL store over this Lifecycle's storage (see `store.ts`). */
function _get_store() {
_classPrivateFieldGet2(_storeInstance, this) ?? _classPrivateFieldSet2(_storeInstance, this, new TaskStore(this.lifecycle.storage));
return _classPrivateFieldGet2(_storeInstance, this);
}
/** Resolve a name to its declared or composition-root-supplied handler. */
function _resolveDefinition(name) {
return _classPrivateFieldGet2(_definitions, this)[name] ?? _classPrivateFieldGet2(_registered, this).get(name) ?? taskDefinitionResolvers.get(this)?.(name);
}
/** True when a name resolves to a runnable definition. */
function _hasDefinition(name) {
return _assertClassBrand(_Tasks_brand, this, _resolveDefinition).call(this, name) !== void 0;
}
function _validateDefinitionName(name) {
if (typeof name !== "string" || name.length === 0) throw new Error("Task definition names must be non-empty strings");
if (name.length > MAX_DEFINITION_NAME_LENGTH) throw new Error(`Task definition name exceeds ${MAX_DEFINITION_NAME_LENGTH} characters`);
if (name.startsWith("__cf")) throw new Error(`Task definition names must not use the reserved "__cf" prefix`);
if (!_assertClassBrand(_Tasks_brand, this, _hasDefinition).call(this, name)) throw new Error(`Unknown Task definition "${name}": not declared on this Tasks`);
}
/** Cancel through a handle: another definition's run is not visible. */
async function _cancelScoped(runId, definition, reason) {
await this.lifecycle.ready();
const row = _get_store.call(_assertClassBrand(_Tasks_brand, this)).getRun(runId);
if (!row || row.definition !== definition) return false;
return this.cancel(runId, reason);
}
/** Push a live attempt's durable claim deadline forward one claim window. */
function _refreshClaim(runId) {
_get_store.call(_assertClassBrand(_Tasks_brand, this)).sql`
UPDATE cf_agents_task_runs
SET next_at = ${Date.now() + _assertClassBrand(_Tasks_brand, this, _claimTimeoutMs).call(this)}, updated_at = ${Date.now()}
WHERE run_id = ${runId} AND state = 'running'
`;
}
/**
* Drive one local due run to its next durable boundary, bounded by the
* dispatch budget, and return the wake outcome for this capability's own
* queue job.
*/
async function _dispatchRun(runId) {
const active = _classPrivateFieldGet2(_active, this).get(runId);
if (active) {
_assertClassBrand(_Tasks_brand, this, _refreshClaim).call(this, runId);
this.lifecycle.trackAlarmWork(active.promise);
return _assertClassBrand(_Tasks_brand, this, _wakeOutcome).call(this, runId);
}
let budgetTimer;
const budget = new Promise((resolve) => {
budgetTimer = setTimeout(() => resolve("budget"), DISPATCH_BUDGET_MS);
});
const runAttempt = _assertClassBrand(_Tasks_brand, this, _executeRun).call(this, runId);
const attempt = runAttempt.then(() => "settled");
try {
if (await Promise.race([attempt, budget]) === "budget") {
this.lifecycle.trackAlarmWork(_classPrivateFieldGet2(_active, this).get(runId)?.promise ?? runAttempt);
return _assertClassBrand(_Tasks_brand, this, _wakeOutcome).call(this, runId);
}
} finally {
clearTimeout(budgetTimer);
}
return _assertClassBrand(_Tasks_brand, this, _wakeOutcome).call(this, runId);
}
/**
* Drive one routed dispatch to completion on this owning facet. There is
* no local budget to race here: the root that sent this message races
* its own await of the call instead, so a full await is safe regardless
* of how long the attempt takes — the call keeps running on this facet
* either way. An already-active attempt only needs its claim refreshed:
* it is already tracked against whichever alarm's breaker domain
* originally dispatched it (a root's pending routed call, or this
* facet's own local alarm).
*/
async function _dispatchRoutedRun(runId) {
if (_classPrivateFieldGet2(_active, this).get(runId)) {
_assertClassBrand(_Tasks_brand, this, _refreshClaim).call(this, runId);
return _assertClassBrand(_Tasks_brand, this, _wakeOutcome).call(this, runId);
}
await _assertClassBrand(_Tasks_brand, this, _executeRun).call(this, runId);
return _assertClassBrand(_Tasks_brand, this, _wakeOutcome).call(this, runId);
}
/**
* Apply the alarm memory-limit breaker policy (#1825) to one run, local to
* whichever Lifecycle owns its storage — the root for an unrouted run, or
* the owning facet when {@link onMemoryLimit} forwarded a routed strike.
*/
async function _applyMemoryLimit(runId, context) {
if (context.sealed) {
await _assertClassBrand(_Tasks_brand, this, _settleFailed).call(this, runId, null, {
name: "TaskMemoryLimitSealed",
message: "Sealed by the alarm memory-limit circuit breaker (#1825) after consecutive Durable Object memory-limit resets."
});
return;
}
if (context.nextTime === void 0) return;
const now = Date.now();
_get_store.call(_assertClassBrand(_Tasks_brand, this)).write(`UPDATE cf_agents_task_runs
SET generation = NULL,
next_at = CASE
WHEN next_at IS NULL OR next_at < ? THEN ?
ELSE next_at
END,
updated_at = ?
WHERE run_id = ?
AND state IN ('pending', 'waiting', 'running')`, [
context.nextTime,
context.nextTime,
now,
runId
]);
await _assertClassBrand(_Tasks_brand, this, _syncWake).call(this, runId);
}
/** Accept a run of any resolvable definition, reserved names included. */
async function _acceptReserved(definition, input, options, startMode) {
if (!_assertClassBrand(_Tasks_brand, this, _hasDefinition).call(this, definition)) throw new Error(`Unknown Task definition "${definition}": not declared on this Tasks`);
return _assertClassBrand(_Tasks_brand, this, _accept).call(this, definition, input, options, startMode);
}
/**
* The queue outcome for one run's wake job, derived from the run row's
* authoritative `next_at` after dispatch. A same-id `#syncWake` push made
* mid-drive supersedes this return at the queue (newer pushes win over
* drive results), but both are computed from the same row, so the row is
* the single source of truth for whether — and when — the run wakes
* again either way.
*/
function _wakeOutcome(runId) {
const next = _get_store.call(_assertClassBrand(_Tasks_brand, this)).sql`
SELECT next_at FROM cf_agents_task_runs
WHERE run_id = ${runId}
AND state IN ('pending', 'waiting', 'running')
`[0]?.next_at;
return typeof next === "number" ? { rescheduleAt: next } : void 0;
}
/**
* Mirror one run's authoritative deadline into the Lifecycle job queue:
* a non-terminal run with a `next_at` gets one job (id = `task:` plus the
* run id, so a retime is a same-id replace); anything else cancels the
* mirror. The prefix keeps caller-selected run IDs inside Tasks' own job
* namespace. Every durable mutation of a run's deadline or state funnels
* through here.
*
* @returns False when the queue already carried exactly this wake and
* nothing was written — a same-values upsert is still a billed row write.
*/
async function _syncWake(runId) {
const next = _get_store.call(_assertClassBrand(_Tasks_brand, this)).sql`
SELECT next_at FROM cf_agents_task_runs
WHERE run_id = ${runId}
AND state IN ('pending', 'waiting', 'running')
`[0]?.next_at ?? null;
if (this.lifecycle.routes.source) return await this.lifecycle.routes.toRoot({
type: "syncWake",
runId,
next
});
const jobId = `${WAKE_JOB_PREFIX}${runId}`;
if (next === null) {
await this.lifecycle.jobs.cancel(jobId);
return true;
}
const existing = this.lifecycle.jobs.get(jobId);
if (existing?.fn === WAKE_JOB_FN && existing.time === next && existing.retry?.maxAttempts === WAKE_JOB_RETRY.maxAttempts) return false;
await this.lifecycle.jobs.push({
id: jobId,
fn: WAKE_JOB_FN,
time: next,
payload: { runId },
retry: WAKE_JOB_RETRY
});
return true;
}
/** Mirror a routed facet's run deadline into this root's job queue. */
async function _syncRoutedWake(owner, runId, next) {
const jobId = `${WAKE_JOB_PREFIX}${owner.key}:${runId}`;
if (next === null) {
await this.lifecycle.jobs.cancel(jobId);
return true;
}
const existing = this.lifecycle.jobs.get(jobId);
if (existing?.fn === WAKE_JOB_FN && existing.time === next && existing.retry?.maxAttempts === WAKE_JOB_RETRY.maxAttempts) return false;
await this.lifecycle.jobs.push({
id: jobId,
fn: WAKE_JOB_FN,
time: next,
payload: {
runId,
owner_path: owner.data,
owner_path_key: owner.key
},
retry: WAKE_JOB_RETRY
});
return true;
}
/** Mirror every non-terminal run into the queue (startup reconcile). */
async function _syncAllWakes() {
const rows = _get_store.call(_assertClassBrand(_Tasks_brand, this)).sql`
SELECT run_id FROM cf_agents_task_runs
WHERE state IN ('pending', 'waiting', 'running')
AND next_at IS NOT NULL
`;
let pushed = false;
for (const { run_id } of rows) if (await _assertClassBrand(_Tasks_brand, this, _syncWake).call(this, run_id)) pushed = true;
if (rows.length > 0 && !pushed) await this.lifecycle.jobs.rearm();
}
async function _accept(definition, input, options = {}, startMode = "warm") {
await this.lifecycle.ready();
if (options.runId !== void 0 && options.runId.length === 0) throw new Error("runId must be a non-empty string when provided");
if (options.idempotencyKey !== void 0 && options.idempotencyKey.length === 0) throw new Error("idempotencyKey must be a non-empty string when provided");
const inputJson = serializeTaskValue(input, `input for Task definition "${definition}"`);
const metadataJson = serializeTaskValue(options.metadata, `metadata for Task definition "${definition}"`);
const existing = (options.runId !== void 0 ? _get_store.call(_assertClassBrand(_Tasks_brand, this)).getRun(options.runId) : void 0) ?? (options.idempotencyKey !== void 0 ? _get_store.call(_assertClassBrand(_Tasks_brand, this)).getRunByKey(options.idempotencyKey) : void 0);
if (existing) {
if (existing.definition !== definition) throw new Error(`Task run "${existing.run_id}" already belongs to definition "${existing.definition}"; refusing to reuse its ${options.runId !== void 0 ? "run ID" : "idempotency key"} for "${definition}"`);
if (options.idempotencyKey !== void 0 && existing.idempotency_key !== options.idempotencyKey) throw new Error(`Task run "${existing.run_id}" carries idempotency key ${existing.idempotency_key === null ? "none" : `"${existing.idempotency_key}"`}; refusing to join it with conflicting key "${options.idempotencyKey}"`);
await _assertClassBrand(_Tasks_brand, this, _syncWake).call(this, existing.run_id);
return {
runId: existing.run_id,
definition,
accepted: false,
state: existing.state,
createdAt: existing.created_at
};
}
const runId = options.runId ?? `task_${nanoid()}`;
const now = Date.now();
_get_store.call(_assertClassBrand(_Tasks_brand, this)).sql`
INSERT INTO cf_agents_task_runs
(run_id, definition, input, state, metadata, idempotency_key, retain,
attempt, next_at, cancel_requested, created_at, updated_at)
VALUES
(${runId}, ${definition}, ${inputJson}, 'pending', ${metadataJson},
${options.idempotencyKey ?? null}, ${options.retain === false ? 0 : 1},
0, ${now}, 0, ${now}, ${now})
`;
await _assertClassBrand(_Tasks_brand, this, _syncWake).call(this, runId);
_assertClassBrand(_Tasks_brand, this, _emit).call(this, "task:accepted", {
runId,
definition,
accepted: true
});
if (startMode === "warm" && this.lifecycle.status() !== "starting") _assertClassBrand(_Tasks_brand, this, _executeRun).call(this, runId).catch(() => {});
return {
runId,
definition,
accepted: true,
state: "pending",
createdAt: now
};
}
/** Claim and drive one due run to its next durable boundary. */
async function _executeRun(runId) {
if (_classPrivateFieldGet2(_active, this).has(runId)) return;
const row = _get_store.call(_assertClassBrand(_Tasks_brand, this)).getRun(runId);
if (!row || TERMINAL_STATES.has(row.state)) return;
const now = Date.now();
if (row.cancel_requested === 1) {
await _assertClassBrand(_Tasks_brand, this, _settleCancelled).call(this, runId, null, row.cancel_reason ?? void 0);
return;
}
if (row.next_at !== null && row.next_at > now) return;
const handler = _assertClassBrand(_Tasks_brand, this, _resolveDefinition).call(this, row.definition);
if (!handler) {
const error = new MissingTaskDefinitionError(row.definition);
console.error(error.message);
await _assertClassBrand(_Tasks_brand, this, _settleFailed).call(this, runId, null, toErrorSummary(error));
await _assertClassBrand(_Tasks_brand, this, _observeError).call(this, error);
return;
}
const interrupted = row.state === "running" ? _assertClassBrand(_Tasks_brand, this, _interruptedStep).call(this, runId) : null;
if (row.state === "running") _assertClassBrand(_Tasks_brand, this, _emit).call(this, "task:attempt:interrupted", {
runId,
definition: row.definition,
attempt: row.attempt,
step: interrupted?.name ?? null
});
const generation = nanoid();
const attempt = row.attempt + 1;
_get_store.call(_assertClassBrand(_Tasks_brand, this)).sql`
UPDATE cf_agents_task_runs
SET state = 'running', attempt = ${attempt}, generation = ${generation},
started_at = coalesce(started_at, ${now}),
next_at = ${now + _assertClassBrand(_Tasks_brand, this, _claimTimeoutMs).call(this)}, wait_reason = NULL,
updated_at = ${now}
WHERE run_id = ${runId}
AND state IN ('pending', 'waiting', 'running')
`;
await _assertClassBrand(_Tasks_brand, this, _syncWake).call(this, runId);
const controller = new AbortController();
_assertClassBrand(_Tasks_brand, this, _emit).call(this, "task:attempt:started", {
runId,
definition: row.definition,
attempt
});
const promise = _assertClassBrand(_Tasks_brand, this, _runAttempt).call(this, row, handler, generation, attempt, controller, interrupted, now);
_classPrivateFieldGet2(_active, this).set(runId, {
generation,
controller,
promise
});
try {
await promise;
} finally {
_classPrivateFieldGet2(_active, this).delete(runId);
}
}
/** Run one claimed attempt and persist its outcome, generation-fenced. */
async function _runAttempt(row, handler, generation, attempt, controller, interrupted, claimedAtMs) {
const runId = row.run_id;
const input = deserializeTaskValue(row.input);
const step = new ReplayStep(_assertClassBrand(_Tasks_brand, this, _createEngine).call(this, runId, row.definition, generation, controller, claimedAtMs), {
startsLive: attempt === 1,
interrupted
});
try {
const resultJson = serializeTaskValue(await this.lifecycle.runInHostContext(() => handler(input, step)), `result of Task definition "${row.definition}"`);
if (_get_store.call(_assertClassBrand(_Tasks_brand, this)).fencedWrite(runId, generation, `UPDATE cf_agents_task_runs
SET state = 'completed', result = ?, generation = NULL, next_at = NULL,
settled_at = ?, updated_at = ?
WHERE run_id = ? AND generation = ? AND state = 'running'`, [
resultJson,
Date.now(),
Date.now()
])) {
_assertClassBrand(_Tasks_brand, this, _emit).call(this, "task:completed", {
runId,
definition: row.definition
});
await _assertClassBrand(_Tasks_brand, this, _finishTerminalSettlement).call(this, runId, row);
}
} catch (thrown) {
await _assertClassBrand(_Tasks_brand, this, _settleThrown).call(this, row, generation, thrown);
}
}
/** Persist a non-completed attempt outcome. */
async function _settleThrown(row, generation, thrown) {
const runId = row.run_id;
if (thrown instanceof AttemptSupersededError) return;
if (isPlatformFailure(thrown)) throw thrown;
if (isTaskCancellation(thrown)) {
await _assertClassBrand(_Tasks_brand, this, _settleCancelled).call(this, runId, generation, thrown.reason);
return;
}
if (isTaskSuspension(thrown)) {
const current = _get_store.call(_assertClassBrand(_Tasks_brand, this)).getRun(runId);
if (current?.cancel_requested === 1) {
await _assertClassBrand(_Tasks_brand, this, _settleCancelled).call(this, runId, generation, current.cancel_reason ?? void 0);
return;
}
if (_get_store.call(_assertClassBrand(_Tasks_brand, this)).fencedWrite(runId, generation, `UPDATE cf_agents_task_runs
SET state = 'waiting', wait_reason = ?, next_at = ?, generation = NULL,
updated_at = ?
WHERE run_id = ? AND generation = ? AND state = 'running'`, [
thrown.reason,
thrown.wakeAt,
Date.now()
])) {
_assertClassBrand(_Tasks_brand, this, _emit).call(this, "task:waiting", {
runId,
definition: row.definition,
reason: thrown.reason,
wakeAt: thrown.wakeAt
});
await _assertClassBrand(_Tasks_brand, this, _syncWake).call(this, runId);
}
return;
}
const summary = toErrorSummary(thrown);
if (await _assertClassBrand(_Tasks_brand, this, _settleFailed).call(this, runId, generation, summary)) console.error(`Task run "${runId}" (definition "${row.definition}") failed: ${summary.name}: ${summary.message}`);
await _assertClassBrand(_Tasks_brand, this, _observeError).call(this, thrown);
}
async function _observeError(error) {
if (!_classPrivateFieldGet2(_onError, this)) return;
try {
await this.lifecycle.runInHostContext(() => _classPrivateFieldGet2(_onError, this)?.call(this, error));
} catch {}
}
function _createEngine(runId, definition, generation, controller, claimedAtMs) {
return createTaskStepEngine({
store: _get_store.call(_assertClassBrand(_Tasks_brand, this)),
runId,
generation,
signal: controller.signal,
claimTimeoutMs: () => _assertClassBrand(_Tasks_brand, this, _claimTimeoutMs).call(this),
claimedAtMs,
claimRefreshAfterMs: CLAIM_SLACK_MS / 2,
defaults: _classPrivateFieldGet2(_stepDefaults, this),
emit: (type, payload) => _assertClassBrand(_Tasks_brand, this, _emit).call(this, type, {
runId,
definition,
...payload
})
});
}
/** The step a lost attempt left mid-execution — replay-entry evidence. */
function _interruptedStep(runId) {
const rows = _get_store.call(_assertClassBrand(_Tasks_brand, this)).sql`
SELECT step_name, attempt FROM cf_agents_task_steps
WHERE run_id = ${runId} AND state = 'running'
ORDER BY started_at DESC
LIMIT 1
`;
return rows[0] ? {
name: rows[0].step_name,
attempt: rows[0].attempt
} : null;
}
/**
* Settle one run as cancelled and sync its queue mirror. Fenced when a
* generation is supplied.
*/
async function _settleCancelled(runId, generation, reason) {
const now = Date.now();
let settled;
if (generation !== null) settled = _get_store.call(_assertClassBrand(_Tasks_brand, this)).fencedWrite(runId, generation, `UPDATE cf_agents_task_runs
SET state = 'cancelled', cancel_requested = 1, cancel_reason = ?,
generation = NULL, next_at = NULL, settled_at = ?, updated_at = ?
WHERE run_id = ? AND generation = ?
AND state = 'running'`, [
reason ?? null,
now,
now
]);
else settled = _get_store.call(_assertClassBrand(_Tasks_brand, this)).write(`UPDATE cf_agents_task_runs
SET state = 'cancelled', cancel_requested = 1, cancel_reason = ?,
generation = NULL, next_at = NULL, settled_at = ?, updated_at = ?
WHERE run_id = ?
AND state IN ('pending', 'waiting', 'running')`, [
reason ?? null,
now,
now,
runId
]) > 0;
const row = settled ? _get_store.call(_assertClassBrand(_Tasks_brand, this)).getRun(runId) : void 0;
if (row) {
_assertClassBrand(_Tasks_brand, this, _emit).call(this, "task:cancelled", {
runId,
definition: row.definition,
reason: reason ?? null
});
await _assertClassBrand(_Tasks_brand, this, _finishTerminalSettlement).call(this, runId, row);
}
}
/**
* Settle one run as failed and sync its queue mirror. Fenced when a
* generation is supplied.
*/
async function _settleFailed(runId, generation, error) {
const now = Date.now();
let settled;
if (generation !== null) settled = _get_store.call(_assertClassBrand(_Tasks_brand, this)).fencedWrite(runId, generation, `UPDATE cf_agents_task_runs
SET state = 'failed', error_name = ?, error_message = ?,
generation = NULL, next_at = NULL, settled_at = ?, updated_at = ?
WHERE run_id = ? AND generation = ?
AND state = 'running'`, [
error.name,
error.message,
now,
now
]);
else settled = _get_store.call(_assertClassBrand(_Tasks_brand, this)).write(`UPDATE cf_agents_task_runs
SET state = 'failed', error_name = ?, error_message = ?,
generation = NULL, next_at = NULL, settled_at = ?, updated_at = ?
WHERE run_id = ?
AND state IN ('pending', 'waiting', 'running')`, [
error.name,
error.message,
now,
now,
runId
]) > 0;
const row = settled ? _get_store.call(_assertClassBrand(_Tasks_brand, this)).getRun(runId) : void 0;
if (row) {
_assertClassBrand(_Tasks_brand, this, _emit).call(this, "task:failed", {
runId,
definition: row.definition,
error: error.name
});
await _assertClassBrand(_Tasks_brand, this, _finishTerminalSettlement).call(this, runId, row);
}
return settled;
}
/** Apply terminal retention policy, then remove the run's wake mirror. */
async function _finishTerminalSettlement(runId, row) {
if (row?.retain === 0) _get_store.call(_assertClassBrand(_Tasks_brand, this)).deleteRun(runId);
await _assertClassBrand(_Tasks_brand, this, _syncWake).call(this, runId);
}
/** Make deadlines sane after a fresh isolate: interrupted work wakes now. */
function _reconcile() {
const now = Date.now();
_get_store.call(_assertClassBrand(_Tasks_brand, this)).sql`
UPDATE cf_agents_task_runs SET next_at = ${now}, updated_at = ${now}
WHERE state = 'running' AND generation IS NOT NULL
`;
_get_store.call(_assertClassBrand(_Tasks_brand, this)).sql`
UPDATE cf_agents_task_runs SET next_at = ${now}, updated_at = ${now}
WHERE state IN ('pending', 'waiting') AND next_at IS NULL
`;
}
async function _snapshot(runId, definition) {
await this.lifecycle.ready();
const row = _get_store.call(_assertClassBrand(_Tasks_brand, this)).getRun(runId);
if (!row) return null;
if (definition !== void 0 && row.definition !== definition) return null;
return _get_store.call(_assertClassBrand(_Tasks_brand, this)).rowToSnapshot(row);
}
async function _snapshotByKey(idempotencyKey, definition) {
await this.lifecycle.ready();
const row = _get_store.call(_assertClassBrand(_Tasks_brand, this)).getRunByKey(idempotencyKey);
if (!row) return null;
if (definition !== void 0 && row.definition !== definition) return null;
return _get_store.call(_assertClassBrand(_Tasks_brand, this)).rowToSnapshot(row);
}
function _emit(type, payload) {
this.lifecycle.events.emit(type, payload);
}
//#endregion
export { DuplicateTaskStepError as a, TaskReplayDivergedError as c, MAX_SERIALIZED_BYTES as i, TaskSerializationError as l, setTaskDefinitionResolver as n, MissingTaskDefinitionError as o, setTaskRoutedMemoryLimitHandler as r, NonRetryableError as s, Tasks as t };
//# sourceMappingURL=tasks-D4nLqVSI.js.map