agents
Version:
A home for your AI agents
406 lines (405 loc) • 17.3 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, isPlatformFailure, resolveRetryConfig, tryN, validateRetryOptions } from "./retries.js";
//#region src/queue/queue.ts
/**
* Lifecycle background-work vocabulary. Queue validates items, resolves
* named callbacks, and pushes jobs due immediately into the Lifecycle-owned
* job queue; Lifecycle runs the alarm event loop, retry policy, and physical
* alarm arming. Queue owns no storage of its own — each item is one job
* whose `fn` is the callback name and whose payload carries the item's
* payload and owner.
*/
const queueCallbackResolvers = /* @__PURE__ */ new WeakMap();
/**
* @internal Supply a composition-root fallback for callback names outside the
* registered map. Agent uses this to keep its historical name-based queue
* API (`this.queue("methodName", payload)`) working: the resolver looks the
* method up on the Agent, and the resolved handler still runs inside the
* Lifecycle host boundary.
*/
function setQueueCallbackResolver(queue, resolver) {
queueCallbackResolvers.set(queue, resolver);
}
const QUEUE_SCHEMA_VERSION_KEY = "cf_agents:queue_schema_version";
/** Version 1: queue items live in the Lifecycle job queue. */
const CURRENT_QUEUE_SCHEMA_VERSION = 1;
const DEFAULT_RETRY = {
maxAttempts: 3,
baseDelayMs: 100,
maxDelayMs: 3e3
};
function isQueueJobPayload(value) {
return typeof value === "object" && value !== null && "owner_path" in value;
}
var _handlers = /* @__PURE__ */ new WeakMap();
var _retryDefaults = /* @__PURE__ */ new WeakMap();
var _onError = /* @__PURE__ */ new WeakMap();
var _lastTime = /* @__PURE__ */ new WeakMap();
var _Queue_brand = /* @__PURE__ */ new WeakSet();
/**
* Durable background work for a Lifecycle Object.
*
* Register callbacks in the constructor and install the instance with
* `Lifecycle.use()`. Each pushed item becomes a job due immediately in the
* Lifecycle job queue; Lifecycle owns the physical alarm and the alarm event
* loop, drives items one at a time in push order, retries a throwing
* callback per its retry policy, and Queue runs registered callbacks through
* Lifecycle's host invocation boundary. An item that still fails after its
* last attempt is dropped after `queue:error` and the `onError` hook.
*
* Items survive the Durable Object leaving memory: an isolate that dies mid
* callback wakes again on the Lifecycle deadman alarm and resumes the queue.
* Callbacks should therefore be idempotent.
*
* @experimental The API surface may change before stabilizing.
*/
var Queue = class extends LifecycleCapability {
/**
* Create a durable Queue.
*
* @param options - Registered callbacks plus optional retry and error
* policy. Registering `callbacks` types {@link push} against the map —
* names and payloads are checked where the handlers are declared and where
* they are pushed. Names outside the map are rejected unless a
* composition-root resolver supplies them — the internal aperture behind
* `Agent`'s name-based queue API.
*/
constructor(options = {}) {
super("queue");
_classPrivateMethodInitSpec(this, _Queue_brand);
_classPrivateFieldInitSpec(this, _handlers, void 0);
_classPrivateFieldInitSpec(this, _retryDefaults, void 0);
_classPrivateFieldInitSpec(this, _onError, void 0);
_classPrivateFieldInitSpec(this, _lastTime, null);
_classPrivateFieldSet2(_handlers, this, options.callbacks ?? {});
_classPrivateFieldSet2(_retryDefaults, this, resolveRetryConfig(options.retry, DEFAULT_RETRY));
_classPrivateFieldSet2(_onError, this, options.onError);
}
/** Migrate legacy `cf_agents_queues` rows into the Lifecycle job queue. */
async onStart() {
const storage = this.lifecycle.storage;
if ((await storage.get(QUEUE_SCHEMA_VERSION_KEY) ?? 0) >= CURRENT_QUEUE_SCHEMA_VERSION) return;
await _assertClassBrand(_Queue_brand, this, _migrateLegacyQueueTable).call(this, storage);
await storage.put(QUEUE_SCHEMA_VERSION_KEY, CURRENT_QUEUE_SCHEMA_VERSION);
}
/** Drive one due item dispatched by the Lifecycle event loop. */
async onJob(context) {
const { job, attempt } = context;
if (!isQueueJobPayload(job.payload)) {
console.error(`Malformed queue item ${job.id}; dropping it`);
return;
}
const item = _assertClassBrand(_Queue_brand, this, _jobToItem).call(this, job, job.payload);
if (attempt > 1) _assertClassBrand(_Queue_brand, this, _emit).call(this, "queue:retry", {
callback: job.fn,
id: job.id,
attempt,
maxAttempts: resolveRetryConfig(job.retry, _classPrivateFieldGet2(_retryDefaults, this)).maxAttempts
});
if (job.payload.owner_path) {
try {
await this.lifecycle.routes.to({
key: job.payload.owner_path_key ?? job.payload.owner_path,
data: job.payload.owner_path
}, {
type: "dispatch",
item
});
} catch (error) {
if (isPlatformFailure(error)) throw error;
console.error(`error dispatching queue callback "${job.fn}"`, error);
_assertClassBrand(_Queue_brand, this, _emit).call(this, "queue:error", {
callback: job.fn,
id: job.id,
error: error instanceof Error ? error.message : String(error),
attempts: 0
});
try {
await _classPrivateFieldGet2(_onError, this)?.call(this, error);
} catch {}
return "yield";
}
return;
}
const handler = _assertClassBrand(_Queue_brand, this, _resolveCallback).call(this, job.fn);
if (!handler) {
console.error(`callback ${job.fn} not found`);
return;
}
await this.lifecycle.runInHostContext(() => handler(item.payload, item));
}
/** Observe one item's terminal application failure; the item is dropped. */
async onJobError(context, error) {
const { job } = context;
const { maxAttempts } = resolveRetryConfig(job.retry, _classPrivateFieldGet2(_retryDefaults, this));
console.error(`queue callback "${job.fn}" failed after ${maxAttempts} attempts`, error);
_assertClassBrand(_Queue_brand, this, _emit).call(this, "queue:error", {
callback: job.fn,
id: job.id,
error: error instanceof Error ? error.message : String(error),
attempts: maxAttempts
});
try {
await _classPrivateFieldGet2(_onError, this)?.call(this, error);
} catch {}
}
/** Handle Queue protocol messages routed by another Lifecycle. */
async onRoute(context) {
const message = context.payload;
const owner = context.source ?? null;
switch (message.type) {
case "push": return _assertClassBrand(_Queue_brand, this, _insert).call(this, owner, message.callback, message.payload, message.options);
case "get": return _assertClassBrand(_Queue_brand, this, _getForOwner).call(this, owner, message.id);
case "list": return _assertClassBrand(_Queue_brand, this, _listForOwner).call(this, owner, message.criteria);
case "cancel": return _assertClassBrand(_Queue_brand, this, _cancelForOwner).call(this, owner, message.id);
case "cancelAll": return _assertClassBrand(_Queue_brand, this, _cancelAllForOwner).call(this, owner, message.callback);
case "dispatch":
await _assertClassBrand(_Queue_brand, this, _executeRouted).call(this, message.item);
return true;
default: throw new Error("Unknown routed Queue message");
}
}
/**
* Push one item for a registered callback. The item is due immediately;
* the Lifecycle alarm event loop runs it in push order after this call
* returns.
*
* Once the Lifecycle has started (and this Queue is not routed through
* another Lifecycle), the item row is written synchronously before this
* method's promise is even returned, so a caller may pair a push with its
* own writes in one synchronous block — the item then commits atomically
* with them.
*/
async push(callback, payload, options) {
if (this.lifecycle.status() !== "started") await this.lifecycle.ready();
_assertClassBrand(_Queue_brand, this, _validatePush).call(this, callback, options);
const item = this.lifecycle.routes.source ? await this.lifecycle.routes.toRoot({
type: "push",
callback,
payload,
options
}) : await _assertClassBrand(_Queue_brand, this, _insert).call(this, null, callback, payload, options);
_assertClassBrand(_Queue_brand, this, _emit).call(this, "queue:create", {
callback,
id: item.id
});
return item;
}
/** Cancel one pending item. Returns false when no item matched. */
async cancel(id) {
await this.lifecycle.ready();
if (this.lifecycle.routes.source) return await this.lifecycle.routes.toRoot({
type: "cancel",
id
});
return _assertClassBrand(_Queue_brand, this, _cancelForOwner).call(this, null, id);
}
/** Cancel every pending item, or every item for one callback. Returns the count. */
async cancelAll(callback) {
await this.lifecycle.ready();
if (this.lifecycle.routes.source) return await this.lifecycle.routes.toRoot({
type: "cancelAll",
callback
});
return _assertClassBrand(_Queue_brand, this, _cancelAllForOwner).call(this, null, callback);
}
/** Read one pending item. */
async get(id) {
await this.lifecycle.ready();
if (this.lifecycle.routes.source) return await this.lifecycle.routes.toRoot({
type: "get",
id
});
return _assertClassBrand(_Queue_brand, this, _getForOwner).call(this, null, id);
}
/** List pending items in push order, optionally filtered by callback. */
async list(criteria) {
await this.lifecycle.ready();
if (this.lifecycle.routes.source) return await this.lifecycle.routes.toRoot({
type: "list",
criteria
});
return _assertClassBrand(_Queue_brand, this, _listForOwner).call(this, null, criteria);
}
/** @internal Remove items owned by one routed Lifecycle subtree. */
async __DO_NOT_USE_WILL_BREAK__cleanupRoutePrefix(prefix) {
for (const { job, envelope } of _assertClassBrand(_Queue_brand, this, _ownedJobs).call(this)) {
const ownerKey = envelope.owner_path_key ?? envelope.owner_path;
if (!envelope.owner_path || ownerKey === null) continue;
if (ownerKey !== prefix && !ownerKey.startsWith(`${prefix}/`)) continue;
await this.lifecycle.jobs.cancel(job.id);
}
}
};
/**
* Move every `cf_agents_queues` row into the job queue and drop the
* table. Idempotent: a missing table means a fresh object or a completed
* migration. Rows keep their insertion order.
*
* TEMPORARY: one-shot upgrade path for objects that had queued items when
* this release landed. Remove in the next minor release (with the schema
* version bump that gates it), once every deployed object has migrated.
*/
async function _migrateLegacyQueueTable(storage) {
if (storage.sql.exec("SELECT name FROM sqlite_master WHERE type='table' AND name='cf_agents_queues'").toArray().length === 0) return;
const rows = storage.sql.exec("SELECT * FROM cf_agents_queues ORDER BY created_at ASC, rowid ASC").toArray();
for (const row of rows) {
let payload;
try {
payload = typeof row.payload === "string" ? JSON.parse(row.payload) : void 0;
} catch (error) {
console.error(`Skipping queue item "${row.id}" during job-queue migration: its payload is not valid JSON`, error);
continue;
}
let retry;
try {
retry = typeof row.retry_options === "string" ? JSON.parse(row.retry_options) : void 0;
} catch {
retry = void 0;
}
await this.lifecycle.jobs.push({
id: row.id,
fn: row.callback,
time: _assertClassBrand(_Queue_brand, this, _nextTime).call(this),
payload: {
payload,
owner_path: null,
owner_path_key: null
},
retry: resolveRetryConfig(retry, _classPrivateFieldGet2(_retryDefaults, this))
});
}
storage.sql.exec("DROP TABLE cf_agents_queues");
}
/**
* Execute a routed item locally with the historical retry handling. Runs
* on the owning (facet) Queue, outside the root's event loop, so it applies
* its own in-process retry budget. Platform-class failures re-throw so the
* root preserves the item and the durable alarm retries on a fresh
* invocation.
*/
async function _executeRouted(item) {
const handler = _assertClassBrand(_Queue_brand, this, _resolveCallback).call(this, item.callback);
if (!handler) {
console.error(`callback ${item.callback} not found`);
return;
}
const { maxAttempts, baseDelayMs, maxDelayMs } = resolveRetryConfig(item.retry, _classPrivateFieldGet2(_retryDefaults, this));
try {
await tryN(maxAttempts, async (attempt) => {
if (attempt > 1) _assertClassBrand(_Queue_brand, this, _emit).call(this, "queue:retry", {
callback: item.callback,
id: item.id,
attempt,
maxAttempts
});
await this.lifecycle.runInHostContext(() => handler(item.payload, item));
}, {
baseDelayMs,
maxDelayMs,
shouldRetry: (error) => !isDurableObjectCodeUpdateReset(error)
});
} catch (error) {
if (isPlatformFailure(error)) throw error;
console.error(`queue callback "${item.callback}" failed after ${maxAttempts} attempts`, error);
_assertClassBrand(_Queue_brand, this, _emit).call(this, "queue:error", {
callback: item.callback,
id: item.id,
error: error instanceof Error ? error.message : String(error),
attempts: maxAttempts
});
try {
await _classPrivateFieldGet2(_onError, this)?.call(this, error);
} catch {}
}
}
function _validatePush(callback, options) {
if (typeof callback !== "string") throw new Error("Callback must be a string");
if (!_assertClassBrand(_Queue_brand, this, _hasCallback).call(this, callback)) throw new Error(`Unknown queue callback "${callback}": not registered on this Queue`);
if (options?.retry) validateRetryOptions(options.retry, _classPrivateFieldGet2(_retryDefaults, this));
if (options?.id !== void 0 && options.id.trim() === "") throw new Error("Queue item ids must be non-empty");
}
/** Resolve a name to its registered or composition-root-supplied handler. */
function _resolveCallback(name) {
const handler = _classPrivateFieldGet2(_handlers, this)[name];
if (handler) return handler;
return queueCallbackResolvers.get(this)?.(name);
}
function _hasCallback(name) {
return _assertClassBrand(_Queue_brand, this, _resolveCallback).call(this, name) !== void 0;
}
/** The next strictly increasing due time, never before now. */
function _nextTime() {
if (_classPrivateFieldGet2(_lastTime, this) === null) _classPrivateFieldSet2(_lastTime, this, _assertClassBrand(_Queue_brand, this, _ownedJobs).call(this).reduce((lastTime, { job }) => Math.max(lastTime, job.time), 0));
const time = Math.max(Date.now(), _classPrivateFieldGet2(_lastTime, this) + 1);
_classPrivateFieldSet2(_lastTime, this, time);
return time;
}
async function _insert(owner, callback, payload, options) {
const existing = options?.id !== void 0 ? this.lifecycle.jobs.get(options.id) : void 0;
const keepTime = existing !== void 0 && _assertClassBrand(_Queue_brand, this, _getForOwner).call(this, owner, existing.id) !== void 0;
const job = await this.lifecycle.jobs.push({
id: options?.id,
fn: callback,
time: keepTime ? existing.time : _assertClassBrand(_Queue_brand, this, _nextTime).call(this),
payload: {
payload,
owner_path: owner?.data ?? null,
owner_path_key: owner?.key ?? null
},
retry: resolveRetryConfig(options?.retry, _classPrivateFieldGet2(_retryDefaults, this))
});
return _assertClassBrand(_Queue_brand, this, _jobToItem).call(this, job, job.payload);
}
function _jobToItem(job, envelope) {
return {
id: job.id,
callback: job.fn,
payload: envelope.payload,
createdAt: job.createdAt,
retry: job.retry
};
}
/** Every queue job this Queue owns, in push order. */
function _ownedJobs() {
const owned = [];
for (const job of this.lifecycle.jobs.list()) if (isQueueJobPayload(job.payload)) owned.push({
job,
envelope: job.payload
});
return owned;
}
function _getForOwner(owner, id) {
const ownerKey = owner?.key ?? null;
const job = this.lifecycle.jobs.get(id);
if (!job || !isQueueJobPayload(job.payload)) return void 0;
if ((job.payload.owner_path_key ?? null) !== ownerKey) return void 0;
return _assertClassBrand(_Queue_brand, this, _jobToItem).call(this, job, job.payload);
}
function _listForOwner(owner, criteria = {}) {
const ownerKey = owner?.key ?? null;
const items = [];
for (const { job, envelope } of _assertClassBrand(_Queue_brand, this, _ownedJobs).call(this)) {
if ((envelope.owner_path_key ?? null) !== ownerKey) continue;
if (criteria.callback && job.fn !== criteria.callback) continue;
items.push(_assertClassBrand(_Queue_brand, this, _jobToItem).call(this, job, envelope));
}
return items;
}
async function _cancelForOwner(owner, id) {
if (!_assertClassBrand(_Queue_brand, this, _getForOwner).call(this, owner, id)) return false;
return this.lifecycle.jobs.cancel(id);
}
async function _cancelAllForOwner(owner, callback) {
let cancelled = 0;
for (const item of _assertClassBrand(_Queue_brand, this, _listForOwner).call(this, owner, { callback })) if (await this.lifecycle.jobs.cancel(item.id)) cancelled++;
return cancelled;
}
function _emit(type, payload) {
this.lifecycle.events.emit(type, payload);
}
//#endregion
export { setQueueCallbackResolver as n, Queue as t };
//# sourceMappingURL=queue-BC9F5cUo.js.map