@agentium/queue 3.1.2 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,284 +1,483 @@
1
- var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
- get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
- }) : x)(function(x) {
4
- if (typeof require !== "undefined") return require.apply(this, arguments);
5
- throw Error('Dynamic require of "' + x + '" is not supported');
6
- });
7
-
8
- // src/job-producer.ts
1
+ import { createRequire } from "node:module";
2
+ import { createHash, randomUUID } from "node:crypto";
3
+ import { durableCanonical, durableTaskDigest } from "@agentium/core";
4
+ //#region src/connection.ts
5
+ function queueConnection(value) {
6
+ if (typeof value !== "string") return {
7
+ ...value,
8
+ ...value.tls ? { tls: {} } : { tls: void 0 }
9
+ };
10
+ const url = new URL(value);
11
+ if (!["redis:", "rediss:"].includes(url.protocol)) throw new Error("Queue connection URL must use redis or rediss");
12
+ const db = url.pathname === "/" || !url.pathname ? 0 : Number(url.pathname.slice(1));
13
+ if (!Number.isSafeInteger(db) || db < 0) throw new Error("Invalid Redis database");
14
+ return {
15
+ host: url.hostname.replace(/^\[(.*)\]$/, "$1"),
16
+ port: Number(url.port || 6379),
17
+ db,
18
+ ...url.username ? { username: decodeURIComponent(url.username) } : {},
19
+ ...url.password ? { password: decodeURIComponent(url.password) } : {},
20
+ ...url.protocol === "rediss:" ? { tls: {} } : {}
21
+ };
22
+ }
23
+ //#endregion
24
+ //#region src/durable.ts
25
+ const requireQueue = createRequire(import.meta.url);
26
+ function envelope(task) {
27
+ return {
28
+ schemaVersion: 1,
29
+ type: "durable",
30
+ tenantId: task.identity.tenantId,
31
+ taskId: task.id,
32
+ inputDigest: durableTaskDigest(task)
33
+ };
34
+ }
35
+ function parseEnvelope(value) {
36
+ if (!value || typeof value !== "object") throw new Error("Invalid durable queue envelope");
37
+ const input = value;
38
+ if (Object.keys(input).some((key) => ![
39
+ "schemaVersion",
40
+ "type",
41
+ "tenantId",
42
+ "taskId",
43
+ "inputDigest"
44
+ ].includes(key)) || input.schemaVersion !== 1 || input.type !== "durable" || typeof input.tenantId !== "string" || !input.tenantId.trim() || input.tenantId.length > 512 || typeof input.taskId !== "string" || !input.taskId.trim() || input.taskId.length > 512 || typeof input.inputDigest !== "string" || !/^sha256:[a-f0-9]{64}$/.test(input.inputDigest)) throw new Error("Invalid durable queue envelope");
45
+ return input;
46
+ }
47
+ /** Queue delivery is a hint; Mongo/another atomic task store owns authoritative state.
48
+ * Persistence precedes enqueue. If Redis fails, call wake() after retrying admission.
49
+ * The stable task key prevents a delivery retry from creating a second execution.
50
+ */
51
+ var DurableAgentQueue = class {
52
+ config;
53
+ queue;
54
+ constructor(config) {
55
+ this.config = config;
56
+ const { Queue } = requireQueue("bullmq");
57
+ this.queue = new Queue(config.queueName ?? "agentium-durable", {
58
+ connection: queueConnection(config.connection),
59
+ defaultJobOptions: config.defaultJobOptions
60
+ });
61
+ }
62
+ async enqueue(task) {
63
+ const copy = JSON.parse(durableCanonical(task));
64
+ if (!copy.driver?.id || !Number.isSafeInteger(copy.driver.version) || copy.driver.version < 1) throw new Error("Durable tasks require a versioned driver reference");
65
+ await this.config.admit(structuredClone(copy));
66
+ const key = {
67
+ tenantId: copy.identity.tenantId,
68
+ taskId: copy.id
69
+ };
70
+ const existing = await this.config.supervisor.get(key);
71
+ if (existing) {
72
+ if (durableTaskDigest(existing) !== durableTaskDigest(copy)) throw new Error("Task ID already has a different immutable envelope");
73
+ } else try {
74
+ await this.config.supervisor.create(copy);
75
+ } catch (error) {
76
+ const raced = await this.config.supervisor.get(key);
77
+ if (!raced || durableTaskDigest(raced) !== durableTaskDigest(copy)) throw error;
78
+ }
79
+ return this.wake(key);
80
+ }
81
+ /** Explicit re-delivery after approval, reconciliation, or a failed initial enqueue. */
82
+ async wake(key) {
83
+ const task = await this.config.supervisor.get(key);
84
+ if (!task) throw new Error("Durable task not found");
85
+ await this.config.admit(structuredClone(task));
86
+ const job = await this.queue.add("durable", envelope(task), {
87
+ jobId: randomUUID(),
88
+ removeOnComplete: true
89
+ });
90
+ return {
91
+ taskId: task.id,
92
+ deliveryId: job.id
93
+ };
94
+ }
95
+ async cancel(key, reason) {
96
+ const task = await this.config.supervisor.get(key);
97
+ if (!task) throw new Error("Durable task not found");
98
+ await this.config.admit(structuredClone(task));
99
+ await this.config.supervisor.cancel(key, reason);
100
+ await this.wake(key);
101
+ }
102
+ async close() {
103
+ await this.queue.close();
104
+ }
105
+ };
106
+ var DurableAgentWorker = class {
107
+ worker;
108
+ constructor(config) {
109
+ const drivers = /* @__PURE__ */ new Map();
110
+ for (const driver of config.drivers) {
111
+ const key = `${driver.id}@${driver.version}`;
112
+ if (!driver.id || !Number.isSafeInteger(driver.version) || driver.version < 1 || driver.recoverable !== true || drivers.has(key)) throw new Error("Durable drivers require unique versioned, explicitly recoverable registrations");
113
+ drivers.set(key, driver);
114
+ }
115
+ const workerId = config.workerId ?? `worker-${randomUUID()}`;
116
+ if (!Number.isSafeInteger(config.concurrency ?? 5) || (config.concurrency ?? 5) < 1) throw new Error("Worker concurrency must be positive");
117
+ const { Worker, DelayedError } = requireQueue("bullmq");
118
+ this.worker = new Worker(config.queueName ?? "agentium-durable", async (job, token) => {
119
+ const delivery = parseEnvelope(job.data);
120
+ const key = {
121
+ tenantId: delivery.tenantId,
122
+ taskId: delivery.taskId
123
+ };
124
+ const task = await config.supervisor.get(key);
125
+ if (!task || envelope(task).inputDigest !== delivery.inputDigest) throw new Error("Durable delivery does not match persisted task");
126
+ await config.admit(structuredClone(task));
127
+ const driver = task.driver ? drivers.get(`${task.driver.id}@${task.driver.version}`) : void 0;
128
+ if (!driver) throw new Error("Durable execution driver is not registered");
129
+ const result = await config.supervisor.run(key, workerId, async (context) => {
130
+ await config.admit(structuredClone(context.task));
131
+ return driver.execute(context);
132
+ });
133
+ if (result.lease && (result.state === "running" || result.state === "cancel_requested")) {
134
+ await job.moveToDelayed(Date.now() + Math.max(25, Math.min(config.supervisor.leaseMs, result.lease.expiresAt - result.updatedAt)), token);
135
+ throw new DelayedError();
136
+ }
137
+ return {
138
+ taskId: result.id,
139
+ state: result.state,
140
+ revision: result.revision
141
+ };
142
+ }, {
143
+ connection: queueConnection(config.connection),
144
+ concurrency: config.concurrency ?? 5
145
+ });
146
+ this.worker.on("error", (error) => config.onError?.(error));
147
+ }
148
+ /** Drains owned work. Active durable cancellation is explicit through queue.cancel(). */
149
+ async stop() {
150
+ await this.worker.close();
151
+ }
152
+ };
153
+ //#endregion
154
+ //#region src/job-producer.ts
155
+ const require$1 = createRequire(import.meta.url);
156
+ const LEGACY_MIGRATION = "Legacy repeat schedule maintenance requires BullMQ v5. Stop writers/workers, use BullMQ 5.81.5 to pause, drain, inventory and migrate legacy repeats, then restart with BullMQ v6.";
9
157
  var AgentQueue = class {
10
- queue;
11
- queueEvents;
12
- queueName;
13
- constructor(config) {
14
- this.queueName = config.queueName ?? "agentium:jobs";
15
- const connection = typeof config.connection === "string" ? { url: config.connection } : config.connection;
16
- try {
17
- const { Queue, QueueEvents } = __require("bullmq");
18
- this.queue = new Queue(this.queueName, {
19
- connection,
20
- defaultJobOptions: config.defaultJobOptions
21
- });
22
- this.queueEvents = new QueueEvents(this.queueName, {
23
- connection
24
- });
25
- } catch (err) {
26
- if (err?.code === "MODULE_NOT_FOUND" || err?.message?.includes("Cannot find module")) {
27
- throw new Error("bullmq and ioredis are required for AgentQueue. Install them: npm install bullmq ioredis");
28
- }
29
- throw err;
30
- }
31
- }
32
- async enqueueAgentRun(opts) {
33
- const payload = {
34
- type: "agent",
35
- agentName: opts.agentName,
36
- input: opts.input,
37
- sessionId: opts.sessionId,
38
- userId: opts.userId
39
- };
40
- const jobOpts = {};
41
- if (opts.priority !== void 0) jobOpts.priority = opts.priority;
42
- if (opts.delay !== void 0) jobOpts.delay = opts.delay;
43
- if (opts.attempts !== void 0) jobOpts.attempts = opts.attempts;
44
- if (opts.backoff) jobOpts.backoff = opts.backoff;
45
- if (opts.repeat) jobOpts.repeat = opts.repeat;
46
- const job = await this.queue.add(`agent:${opts.agentName}`, payload, jobOpts);
47
- return { jobId: job.id };
48
- }
49
- async enqueueWorkflow(opts) {
50
- const payload = {
51
- type: "workflow",
52
- workflowName: opts.workflowName,
53
- initialState: opts.initialState,
54
- sessionId: opts.sessionId
55
- };
56
- const jobOpts = {};
57
- if (opts.priority !== void 0) jobOpts.priority = opts.priority;
58
- if (opts.delay !== void 0) jobOpts.delay = opts.delay;
59
- if (opts.attempts !== void 0) jobOpts.attempts = opts.attempts;
60
- if (opts.backoff) jobOpts.backoff = opts.backoff;
61
- if (opts.repeat) jobOpts.repeat = opts.repeat;
62
- const job = await this.queue.add(`workflow:${opts.workflowName}`, payload, jobOpts);
63
- return { jobId: job.id };
64
- }
65
- async enqueueTeamRun(opts) {
66
- const payload = {
67
- type: "team",
68
- teamName: opts.teamName,
69
- input: opts.input,
70
- sessionId: opts.sessionId,
71
- userId: opts.userId
72
- };
73
- const jobOpts = {};
74
- if (opts.priority !== void 0) jobOpts.priority = opts.priority;
75
- if (opts.delay !== void 0) jobOpts.delay = opts.delay;
76
- if (opts.attempts !== void 0) jobOpts.attempts = opts.attempts;
77
- if (opts.backoff) jobOpts.backoff = opts.backoff;
78
- if (opts.repeat) jobOpts.repeat = opts.repeat;
79
- const job = await this.queue.add(`team:${opts.teamName}`, payload, jobOpts);
80
- return { jobId: job.id };
81
- }
82
- async getJobStatus(jobId) {
83
- const job = await this.queue.getJob(jobId);
84
- if (!job) {
85
- throw new Error(`Job ${jobId} not found`);
86
- }
87
- const state = await job.getState();
88
- return {
89
- jobId: job.id,
90
- state,
91
- progress: typeof job.progress === "number" ? job.progress : void 0,
92
- result: job.returnvalue,
93
- error: job.failedReason,
94
- createdAt: new Date(job.timestamp),
95
- processedAt: job.processedOn ? new Date(job.processedOn) : void 0,
96
- finishedAt: job.finishedOn ? new Date(job.finishedOn) : void 0
97
- };
98
- }
99
- async cancelJob(jobId) {
100
- const job = await this.queue.getJob(jobId);
101
- if (job) {
102
- await job.remove();
103
- }
104
- }
105
- onCompleted(handler) {
106
- this.queueEvents.on("completed", ({ jobId, returnvalue }) => {
107
- handler(jobId, returnvalue);
108
- });
109
- }
110
- onFailed(handler) {
111
- this.queueEvents.on("failed", ({ jobId, failedReason }) => {
112
- handler(jobId, new Error(failedReason));
113
- });
114
- }
115
- async schedule(opts) {
116
- if (!opts.agent && !opts.workflow) {
117
- throw new Error("schedule() requires either agent or workflow");
118
- }
119
- if (opts.agent && opts.workflow) {
120
- throw new Error("schedule() accepts either agent or workflow, not both");
121
- }
122
- const repeat = { pattern: opts.cron, ...opts.timezone ? { tz: opts.timezone } : {} };
123
- if (opts.agent) {
124
- return this.enqueueAgentRun({
125
- agentName: opts.agent.name,
126
- input: opts.agent.input,
127
- sessionId: opts.agent.sessionId,
128
- userId: opts.agent.userId,
129
- repeat
130
- });
131
- }
132
- if (opts.team) {
133
- return this.enqueueTeamRun({
134
- teamName: opts.team.name,
135
- input: opts.team.input,
136
- sessionId: opts.team.sessionId,
137
- userId: opts.team.userId,
138
- repeat
139
- });
140
- }
141
- return this.enqueueWorkflow({
142
- workflowName: opts.workflow.name,
143
- initialState: opts.workflow.initialState,
144
- repeat
145
- });
146
- }
147
- async unschedule(id) {
148
- const jobs = await this.queue.getRepeatableJobs();
149
- const match = jobs.find((j) => j.name === `agent:${id}` || j.name === `workflow:${id}` || j.name === id);
150
- if (!match) {
151
- throw new Error(`Schedule "${id}" not found`);
152
- }
153
- await this.queue.removeRepeatableByKey(match.key);
154
- }
155
- async listSchedules() {
156
- const jobs = await this.queue.getRepeatableJobs();
157
- return jobs.map((j) => ({
158
- id: j.name,
159
- pattern: j.pattern ?? j.cron,
160
- timezone: j.tz ?? void 0,
161
- next: j.next ? new Date(j.next) : /* @__PURE__ */ new Date()
162
- }));
163
- }
164
- async close() {
165
- const results = await Promise.allSettled([this.queue.close(), this.queueEvents.close()]);
166
- for (const r of results) {
167
- if (r.status === "rejected") console.warn("[JobProducer] Error during close:", r.reason);
168
- }
169
- }
158
+ queue;
159
+ queueEvents;
160
+ queueName;
161
+ constructor(config) {
162
+ this.queueName = config.queueName ?? "agentium-jobs";
163
+ const connection = queueConnection(config.connection);
164
+ try {
165
+ const { Queue, QueueEvents } = require$1("bullmq");
166
+ this.queue = new Queue(this.queueName, {
167
+ connection,
168
+ defaultJobOptions: config.defaultJobOptions
169
+ });
170
+ this.queueEvents = new QueueEvents(this.queueName, { connection });
171
+ } catch (err) {
172
+ if (err?.code === "MODULE_NOT_FOUND" || err?.message?.includes("Cannot find module")) throw new Error("bullmq and ioredis are required for AgentQueue. Install them: npm install bullmq ioredis");
173
+ throw err;
174
+ }
175
+ }
176
+ async enqueueAgentRun(opts) {
177
+ const payload = {
178
+ type: "agent",
179
+ agentName: opts.agentName,
180
+ input: opts.input,
181
+ sessionId: opts.sessionId,
182
+ tenantId: opts.tenantId,
183
+ userId: opts.userId
184
+ };
185
+ const jobOpts = {};
186
+ if (opts.priority !== void 0) jobOpts.priority = opts.priority;
187
+ if (opts.delay !== void 0) jobOpts.delay = opts.delay;
188
+ if (opts.attempts !== void 0) jobOpts.attempts = opts.attempts;
189
+ if (opts.backoff) jobOpts.backoff = opts.backoff;
190
+ if (opts.repeat) return this.enqueueRecurring(`agent:${opts.agentName}`, payload, opts.repeat, jobOpts);
191
+ return { jobId: (await this.queue.add(`agent:${opts.agentName}`, payload, jobOpts)).id };
192
+ }
193
+ async enqueueWorkflow(opts) {
194
+ const payload = {
195
+ type: "workflow",
196
+ workflowName: opts.workflowName,
197
+ userId: opts.userId,
198
+ initialState: opts.initialState,
199
+ sessionId: opts.sessionId,
200
+ tenantId: opts.tenantId
201
+ };
202
+ const jobOpts = {};
203
+ if (opts.priority !== void 0) jobOpts.priority = opts.priority;
204
+ if (opts.delay !== void 0) jobOpts.delay = opts.delay;
205
+ if (opts.attempts !== void 0) jobOpts.attempts = opts.attempts;
206
+ if (opts.backoff) jobOpts.backoff = opts.backoff;
207
+ if (opts.repeat) return this.enqueueRecurring(`workflow:${opts.workflowName}`, payload, opts.repeat, jobOpts);
208
+ return { jobId: (await this.queue.add(`workflow:${opts.workflowName}`, payload, jobOpts)).id };
209
+ }
210
+ async enqueueTeamRun(opts) {
211
+ const payload = {
212
+ type: "team",
213
+ teamName: opts.teamName,
214
+ input: opts.input,
215
+ sessionId: opts.sessionId,
216
+ tenantId: opts.tenantId,
217
+ userId: opts.userId
218
+ };
219
+ const jobOpts = {};
220
+ if (opts.priority !== void 0) jobOpts.priority = opts.priority;
221
+ if (opts.delay !== void 0) jobOpts.delay = opts.delay;
222
+ if (opts.attempts !== void 0) jobOpts.attempts = opts.attempts;
223
+ if (opts.backoff) jobOpts.backoff = opts.backoff;
224
+ if (opts.repeat) return this.enqueueRecurring(`team:${opts.teamName}`, payload, opts.repeat, jobOpts);
225
+ return { jobId: (await this.queue.add(`team:${opts.teamName}`, payload, jobOpts)).id };
226
+ }
227
+ async getJobStatus(jobId) {
228
+ const job = await this.queue.getJob(jobId);
229
+ if (!job) throw new Error(`Job ${jobId} not found`);
230
+ const state = await job.getState();
231
+ return {
232
+ jobId: job.id,
233
+ state,
234
+ progress: typeof job.progress === "number" ? job.progress : void 0,
235
+ result: job.returnvalue,
236
+ error: job.failedReason,
237
+ createdAt: new Date(job.timestamp),
238
+ processedAt: job.processedOn ? new Date(job.processedOn) : void 0,
239
+ finishedAt: job.finishedOn ? new Date(job.finishedOn) : void 0
240
+ };
241
+ }
242
+ async cancelJob(jobId) {
243
+ const job = await this.queue.getJob(jobId);
244
+ if (job) await job.remove();
245
+ }
246
+ onCompleted(handler) {
247
+ this.queueEvents.on("completed", ({ jobId, returnvalue }) => {
248
+ handler(jobId, returnvalue);
249
+ });
250
+ }
251
+ onFailed(handler) {
252
+ this.queueEvents.on("failed", ({ jobId, failedReason }) => {
253
+ handler(jobId, new Error(failedReason));
254
+ });
255
+ }
256
+ async enqueueRecurring(name, data, repeat, options) {
257
+ const id = `repeat-${createHash("sha256").update(JSON.stringify({
258
+ name,
259
+ data,
260
+ repeat
261
+ })).digest("hex")}`;
262
+ await this.assertNoLegacySchedule(name, repeat.pattern);
263
+ return { jobId: (await this.queue.upsertJobScheduler(id, {
264
+ pattern: repeat.pattern,
265
+ tz: repeat.timezone
266
+ }, {
267
+ name,
268
+ data,
269
+ opts: options
270
+ })).id };
271
+ }
272
+ async schedulerRecords() {
273
+ try {
274
+ return await this.queue.getJobSchedulers(0, -1, true);
275
+ } catch (cause) {
276
+ if (cause instanceof Error && /legacy repeatable job/i.test(cause.message)) throw new Error(LEGACY_MIGRATION, { cause });
277
+ throw cause;
278
+ }
279
+ }
280
+ hasLegacyMaintenance() {
281
+ return typeof this.queue.getRepeatableJobs === "function" && typeof this.queue.removeRepeatableByKey === "function";
282
+ }
283
+ async assertNoLegacySchedule(name, pattern) {
284
+ if (!this.hasLegacyMaintenance()) {
285
+ if ((await this.schedulerRecords()).some((job) => typeof job.iterationCount !== "number")) throw new Error(LEGACY_MIGRATION);
286
+ return;
287
+ }
288
+ if ((await this.listLegacySchedules()).some((item) => item.name === name && item.pattern === pattern)) throw new Error("A legacy repeat schedule already exists; pause and explicitly migrate it before creating a Job Scheduler");
289
+ }
290
+ async schedule(opts) {
291
+ if (!opts.id || !opts.cron) throw new Error("schedule() requires a stable id and cron");
292
+ if ([
293
+ opts.agent,
294
+ opts.workflow,
295
+ opts.team
296
+ ].filter(Boolean).length !== 1) throw new Error("schedule() requires exactly one agent, workflow or team");
297
+ let data;
298
+ let name;
299
+ if (opts.agent) {
300
+ name = `agent:${opts.agent.name}`;
301
+ data = {
302
+ type: "agent",
303
+ agentName: opts.agent.name,
304
+ input: opts.agent.input,
305
+ sessionId: opts.agent.sessionId,
306
+ userId: opts.agent.userId,
307
+ tenantId: opts.agent.tenantId
308
+ };
309
+ } else if (opts.team) {
310
+ name = `team:${opts.team.name}`;
311
+ data = {
312
+ type: "team",
313
+ teamName: opts.team.name,
314
+ input: opts.team.input,
315
+ sessionId: opts.team.sessionId,
316
+ userId: opts.team.userId,
317
+ tenantId: opts.team.tenantId
318
+ };
319
+ } else {
320
+ name = `workflow:${opts.workflow.name}`;
321
+ data = {
322
+ type: "workflow",
323
+ workflowName: opts.workflow.name,
324
+ initialState: opts.workflow.initialState,
325
+ sessionId: opts.workflow.sessionId,
326
+ userId: opts.workflow.userId,
327
+ tenantId: opts.workflow.tenantId
328
+ };
329
+ }
330
+ await this.assertNoLegacySchedule(name, opts.cron);
331
+ return { jobId: (await this.queue.upsertJobScheduler(opts.id, {
332
+ pattern: opts.cron,
333
+ tz: opts.timezone
334
+ }, {
335
+ name,
336
+ data
337
+ })).id };
338
+ }
339
+ async unschedule(id) {
340
+ if (!await this.queue.removeJobScheduler(id)) throw new Error(`Schedule "${id}" not found`);
341
+ }
342
+ async listSchedules() {
343
+ const jobs = await this.schedulerRecords();
344
+ if (!this.hasLegacyMaintenance() && jobs.some((job) => typeof job.iterationCount !== "number")) throw new Error(LEGACY_MIGRATION);
345
+ return jobs.filter((job) => typeof job.iterationCount === "number").map((job) => ({
346
+ id: job.key,
347
+ pattern: job.pattern ?? "",
348
+ timezone: job.tz,
349
+ next: new Date(job.next)
350
+ }));
351
+ }
352
+ /** BullMQ v5 maintenance only. Never silently converts persisted legacy repeat records. */
353
+ async listLegacySchedules() {
354
+ if (!this.hasLegacyMaintenance()) throw new Error(LEGACY_MIGRATION);
355
+ const schedulerIds = new Set((await this.schedulerRecords()).filter((job) => typeof job.iterationCount === "number").map((job) => job.key));
356
+ return (await this.queue.getRepeatableJobs()).filter((job) => !schedulerIds.has(job.key)).map((job) => ({
357
+ key: job.key,
358
+ name: job.name,
359
+ pattern: job.pattern,
360
+ timezone: job.tz,
361
+ next: job.next
362
+ }));
363
+ }
364
+ async pause() {
365
+ await this.queue.pause();
366
+ }
367
+ async resume() {
368
+ await this.queue.resume();
369
+ }
370
+ /** Maintenance-only removal: callers retain inventory/payload backups and stop concurrent schedule writers. */
371
+ async removeLegacySchedule(key) {
372
+ if (!this.hasLegacyMaintenance()) throw new Error(LEGACY_MIGRATION);
373
+ if (!await this.queue.isPaused() || await this.queue.getActiveCount() !== 0) throw new Error("Pause the queue and drain active jobs before migrating schedules");
374
+ if (!(await this.listLegacySchedules()).some((job) => job.key === key)) throw new Error("Legacy schedule not found");
375
+ await this.queue.removeRepeatableByKey(key);
376
+ }
377
+ async close() {
378
+ const results = await Promise.allSettled([this.queue.close(), this.queueEvents.close()]);
379
+ for (const r of results) if (r.status === "rejected") console.warn("[JobProducer] Error during close:", r.reason);
380
+ }
170
381
  };
171
-
172
- // src/job-worker.ts
382
+ //#endregion
383
+ //#region src/job-worker.ts
384
+ const require = createRequire(import.meta.url);
173
385
  var AgentWorker = class {
174
- worker;
175
- constructor(config) {
176
- const queueName = config.queueName ?? "agentium:jobs";
177
- const concurrency = config.concurrency ?? 5;
178
- const connection = typeof config.connection === "string" ? { url: config.connection } : config.connection;
179
- const defaultJobOptions = {
180
- attempts: config.attempts ?? 3,
181
- backoff: {
182
- type: "exponential",
183
- delay: config.backoffDelay ?? 1e3
184
- }
185
- };
186
- try {
187
- const { Worker } = __require("bullmq");
188
- this.worker = new Worker(
189
- queueName,
190
- async (job) => {
191
- const payload = job.data;
192
- if (payload.type === "agent") {
193
- const agent = config.agentRegistry[payload.agentName];
194
- if (!agent) {
195
- throw new Error(`Agent "${payload.agentName}" not found in registry`);
196
- }
197
- const onChunk = (_evt) => {
198
- job.updateProgress(typeof job.progress === "number" ? job.progress + 1 : 1);
199
- };
200
- agent.eventBus.on("run.stream.chunk", onChunk);
201
- try {
202
- const result = await agent.run(payload.input, {
203
- sessionId: payload.sessionId,
204
- userId: payload.userId
205
- });
206
- return result;
207
- } finally {
208
- agent.eventBus.off("run.stream.chunk", onChunk);
209
- }
210
- }
211
- if (payload.type === "workflow") {
212
- const workflow = config.workflowRegistry?.[payload.workflowName];
213
- if (!workflow) {
214
- throw new Error(`Workflow "${payload.workflowName}" not found in registry`);
215
- }
216
- const result = await workflow.run({
217
- sessionId: payload.sessionId
218
- });
219
- return result;
220
- }
221
- if (payload.type === "team") {
222
- const team = config.teamRegistry?.[payload.teamName];
223
- if (!team) {
224
- throw new Error(`Team "${payload.teamName}" not found in registry`);
225
- }
226
- const result = await team.run(payload.input, {
227
- sessionId: payload.sessionId,
228
- userId: payload.userId
229
- });
230
- return result;
231
- }
232
- throw new Error(`Unknown job type: ${payload.type}`);
233
- },
234
- {
235
- connection,
236
- concurrency,
237
- defaultJobOptions
238
- }
239
- );
240
- } catch (err) {
241
- if (err?.code === "MODULE_NOT_FOUND" || err?.message?.includes("Cannot find module")) {
242
- throw new Error("bullmq and ioredis are required for AgentWorker. Install them: npm install bullmq ioredis");
243
- }
244
- throw err;
245
- }
246
- }
247
- start() {
248
- }
249
- async stop(timeoutMs = 3e4) {
250
- await Promise.race([
251
- this.worker.close(),
252
- new Promise((_, reject) => setTimeout(() => reject(new Error("Worker drain timeout")), timeoutMs))
253
- ]).catch((err) => {
254
- console.warn("[JobWorker] Error during stop:", err?.message ?? err);
255
- });
256
- }
386
+ worker;
387
+ constructor(config) {
388
+ const legacy = config;
389
+ if (legacy.attempts !== void 0 || legacy.backoffDelay !== void 0) throw new Error("Configure retries on AgentQueue defaultJobOptions or enqueue options; Worker retry options never controlled jobs");
390
+ const queueName = config.queueName ?? "agentium-jobs";
391
+ const concurrency = config.concurrency ?? 5;
392
+ const connection = queueConnection(config.connection);
393
+ try {
394
+ const { Worker } = require("bullmq");
395
+ this.worker = new Worker(queueName, async (job) => {
396
+ const payload = job.data;
397
+ if (payload.type === "agent") {
398
+ const agent = config.agentRegistry[payload.agentName];
399
+ if (!agent) throw new Error(`Agent "${payload.agentName}" not found in registry`);
400
+ const runId = randomUUID();
401
+ let progress = 0;
402
+ let progressWrites = Promise.resolve();
403
+ const onChunk = (event) => {
404
+ if (event.runId !== runId) return;
405
+ const next = ++progress;
406
+ progressWrites = progressWrites.then(() => job.updateProgress(next)).catch(() => {});
407
+ };
408
+ agent.eventBus.on("run.stream.chunk", onChunk);
409
+ try {
410
+ return await agent.run(payload.input, {
411
+ runId,
412
+ sessionId: payload.sessionId,
413
+ userId: payload.userId,
414
+ tenantId: payload.tenantId
415
+ });
416
+ } finally {
417
+ agent.eventBus.off("run.stream.chunk", onChunk);
418
+ await progressWrites;
419
+ }
420
+ }
421
+ if (payload.type === "workflow") {
422
+ const workflow = config.workflowRegistry?.[payload.workflowName];
423
+ if (!workflow) throw new Error(`Workflow "${payload.workflowName}" not found in registry`);
424
+ return await workflow.run({
425
+ sessionId: payload.sessionId,
426
+ initialState: payload.initialState,
427
+ userId: payload.userId,
428
+ tenantId: payload.tenantId
429
+ });
430
+ }
431
+ if (payload.type === "team") {
432
+ const team = config.teamRegistry?.[payload.teamName];
433
+ if (!team) throw new Error(`Team "${payload.teamName}" not found in registry`);
434
+ return await team.run(payload.input, {
435
+ sessionId: payload.sessionId,
436
+ userId: payload.userId,
437
+ tenantId: payload.tenantId
438
+ });
439
+ }
440
+ throw new Error(`Unknown job type: ${payload.type}`);
441
+ }, {
442
+ connection,
443
+ concurrency
444
+ });
445
+ } catch (err) {
446
+ if (err?.code === "MODULE_NOT_FOUND" || err?.message?.includes("Cannot find module")) throw new Error("bullmq and ioredis are required for AgentWorker. Install them: npm install bullmq ioredis");
447
+ throw err;
448
+ }
449
+ }
450
+ start() {}
451
+ async stop(timeoutMs = 3e4) {
452
+ let timer;
453
+ try {
454
+ await Promise.race([this.worker.close(), new Promise((_, reject) => {
455
+ timer = setTimeout(() => reject(/* @__PURE__ */ new Error("Worker drain timeout")), timeoutMs);
456
+ })]);
457
+ } finally {
458
+ if (timer) clearTimeout(timer);
459
+ }
460
+ }
257
461
  };
258
-
259
- // src/progress.ts
462
+ //#endregion
463
+ //#region src/progress.ts
260
464
  function bridgeEventBusToJob(eventBus, job, runId) {
261
- let progressCount = 0;
262
- const onChunk = (evt) => {
263
- if (evt.runId !== runId) return;
264
- progressCount++;
265
- job.updateProgress(progressCount).catch(() => {
266
- });
267
- };
268
- const onToolCall = (evt) => {
269
- if (evt.runId !== runId) return;
270
- job.log(`Tool call: ${evt.toolName}`).catch(() => {
271
- });
272
- };
273
- eventBus.on("run.stream.chunk", onChunk);
274
- eventBus.on("tool.call", onToolCall);
275
- return () => {
276
- eventBus.off("run.stream.chunk", onChunk);
277
- eventBus.off("tool.call", onToolCall);
278
- };
465
+ let progressCount = 0;
466
+ const onChunk = (evt) => {
467
+ if (evt.runId !== runId) return;
468
+ progressCount++;
469
+ job.updateProgress(progressCount).catch(() => {});
470
+ };
471
+ const onToolCall = (evt) => {
472
+ if (evt.runId !== runId) return;
473
+ job.log(`Tool call: ${evt.toolName}`).catch(() => {});
474
+ };
475
+ eventBus.on("run.stream.chunk", onChunk);
476
+ eventBus.on("tool.call", onToolCall);
477
+ return () => {
478
+ eventBus.off("run.stream.chunk", onChunk);
479
+ eventBus.off("tool.call", onToolCall);
480
+ };
279
481
  }
280
- export {
281
- AgentQueue,
282
- AgentWorker,
283
- bridgeEventBusToJob
284
- };
482
+ //#endregion
483
+ export { AgentQueue, AgentWorker, DurableAgentQueue, DurableAgentWorker, bridgeEventBusToJob };