UNPKG

agents

Version:

A home for your AI agents

224 lines (180 loc) 8.85 kB
# Queue Durable background work for an Agent or any Lifecycle Object. A queued item persists a callback name and a payload, runs from the Durable Object's alarm loop one item at a time in push order, retries when the callback throws, and survives the object leaving memory. ## Queue Lifecycle capability > **Experimental.** The `Queue` capability and the `agents/lifecycle` surface > it builds on may change between releases. Agent's established queue methods > (`this.queue()` and friends) are stable. `Queue` is a reusable Lifecycle capability. A plain Lifecycle Object can install it without extending `Agent`: ```typescript import { DurableObject } from "cloudflare:workers"; import { Lifecycle } from "agents/lifecycle"; import { Queue, type QueueItem } from "agents/queue"; export class UploadObject extends DurableObject<Env> { readonly queue = new Queue({ callbacks: { processUpload: ( payload: { fileName: string }, item: QueueItem<{ fileName: string }> ) => { console.log(item.id, payload.fileName); } } }); readonly lifecycle = Lifecycle.install(this).use(this.queue); async enqueueUpload(fileName: string): Promise<string> { const item = await this.queue.push("processUpload", { fileName }); return item.id; } } ``` Queue owns no storage of its own. Each item is one job in the Lifecycle job queue, due immediately; Lifecycle owns the physical Durable Object alarm, the alarm event loop, retry policy, the deadman pre-arm, and the memory-limit circuit breaker. See [Durable Object lifecycle](./lifecycle.md#the-job-queue). Queue's API is small: callbacks are registered by name in the constructor, `push()` creates items typed against that registration, and `get()`, `list()`, `cancel()`, and `cancelAll()` manage them. All of these are asynchronous and work inside routed sub-agents. Queue Lifecycle hooks run without ambient host context. Registered callbacks are user code, so they run inside the host invocation context with the Lifecycle Object available through `getCurrentAgent()`. ### `new Queue(options?)` | Option | Description | | ----------- | ------------------------------------------------------------------------------------------------------------- | | `callbacks` | Named callbacks `(payload, item) => unknown`. Register in a field initializer so names re-bind on every wake. | | `retry` | Default retry policy: `{ maxAttempts: 3, baseDelayMs: 100, maxDelayMs: 3000 }`. See [Retries](./retries.md). | | `onError` | Observes an item's terminal failure after its attempts are exhausted. Runs without host context. | ### `push(callback, payload?, options?)` Queues one item and returns the `QueueItem`. The item is durable before the returned promise resolves, and it runs from the next alarm, so the caller never waits for the callback. - `options.retry` overrides the Queue's retry policy for this item. - `options.id` sets a stable id. A push with an existing id replaces that item in place, keeping its position in the queue (and superseding a dispatch of it still in flight); use it for idempotent enqueues keyed by your own identifier. Once the Lifecycle has started, the item row is written synchronously before `push()` returns its promise, so a push made in the same synchronous block as your own SQL writes commits atomically with them. ### `get(id)`, `list(criteria?)`, `cancel(id)`, `cancelAll(callback?)` `list()` returns pending items in push order; `{ callback }` filters by callback name. `cancel()` returns whether an item was removed; `cancelAll()` returns how many were. ### QueueItem ```typescript type QueueItem<T = unknown> = { id: string; // Unique identifier callback: string; // Callback name payload: T; // Data passed to the callback createdAt: number; // Unix timestamp in seconds retry?: RetryOptions; // Resolved retry policy }; ``` ## Using the queue through Agent `Agent` installs a `Queue` and resolves callback names to its own methods, so `this.queue("methodName", payload)` keeps working with no registration: ```typescript class MyAgent extends Agent { async processEmail( payload: { email: string; subject: string }, item: QueueItem<{ email: string; subject: string }> ) { console.log(`Processing ${item.id}: ${payload.subject}`); } async onMessage(message: string) { const id = await this.queue("processEmail", { email: "user@example.com", subject: "Welcome!" }); console.log(`Queued task ${id}`); } } ``` | Method | Description | | ------------------------------------ | ------------------------------------------------------------------- | | `queue(callback, payload, options?)` | Queue a method by name. Returns the item id. Accepts `retry`, `id`. | | `dequeue(id)` | Remove a pending item. Resolves to whether one was removed. | | `dequeueAll()` | Remove every pending item. Resolves to the count. | | `dequeueAllByCallback(callback)` | Remove every pending item for one callback. Resolves to the count. | | `getQueue(id)` | Read a pending item. | | `getQueues(key, value)` | Pending items whose payload has `key` equal to `value`. | ## How queue processing works 1. **Validation**: `queue()` checks the callback exists and validates any retry options. Invalid input throws immediately. 2. **Durable first**: the item is a row in the `cf_agents_jobs` job queue before `queue()` resolves. Items created before this capability existed are migrated out of the old `cf_agents_queues` table on the next start. 3. **Alarm-driven**: every push re-arms the Durable Object alarm. The Lifecycle event loop runs due items in push order, awaiting each callback before starting the next, in a fresh invocation rather than the request that queued them. 4. **Retries**: a throwing callback is retried with exponential backoff per its retry policy, emitting `queue:retry` on each extra attempt. 5. **Terminal failure**: after the last attempt the item is dropped, a `queue:error` event is emitted, and Agent's `onError` (or the Queue's `onError` option) observes the error. 6. **Recovery**: an isolate that dies mid-callback wakes again on the Lifecycle deadman alarm and resumes the queue, so callbacks should be idempotent. ## Use cases Queue work that must not block the current request or WebSocket message: ```typescript class DataProcessor extends Agent { async processLargeDataset(data: { datasetId: string; userId: string }) { const results = await this.heavyComputation(data.datasetId); await this.notifyUser(data.userId, results); } async onDataUpload(uploadData: { id: string; userId: string }) { await this.queue("processLargeDataset", { datasetId: uploadData.id, userId: uploadData.userId }); return { message: "Data upload received, processing started" }; } } ``` Split large requests into batches that run one after another: ```typescript class BatchProcessor extends Agent { async processBatch(data: { items: unknown[]; batchId: string }) { for (const item of data.items) { await this.processItem(item); } } async onLargeRequest(items: unknown[]) { const batchSize = 10; for (let i = 0; i < items.length; i += batchSize) { await this.queue("processBatch", { items: items.slice(i, i + batchSize), batchId: `batch-${i / batchSize + 1}` }); } } } ``` For work that should run at a specific time or on a schedule, use [Scheduling](./scheduling.md) instead. Both ride the same Lifecycle job queue. ## Best practices 1. **Keep payloads small**: payloads are JSON-serialized into the job row. 2. **Idempotent callbacks**: an item can run again after an isolate reset. 3. **Bounded callbacks**: the event loop awaits each callback, so a long one delays every other job on the object. Detach unbounded work and return. 4. **Stable ids for dedupe**: pass `id` when the same logical task may be queued more than once. ## Error handling and retries ```typescript await this.queue("reliableTask", payload, { retry: { maxAttempts: 5, baseDelayMs: 500 } }); ``` See [Retries](./retries.md) for full documentation on retry options, and [Observability](./observability.md) for the `queue:create`, `queue:retry`, and `queue:error` events. ## Limitations - Items run sequentially, not in parallel. - No priority system (FIFO only). - Failed items are dropped after all retry attempts (no dead-letter queue).