UNPKG

agents

Version:

A home for your AI agents

406 lines (405 loc) 17.3 kB
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