@cogitator-ai/worker 0.3.20 → 0.5.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.
Files changed (51) hide show
  1. package/README.md +141 -55
  2. package/dist/connection.d.ts +29 -0
  3. package/dist/connection.d.ts.map +1 -0
  4. package/dist/connection.js +63 -0
  5. package/dist/connection.js.map +1 -0
  6. package/dist/distributed-swarm-worker.d.ts +56 -0
  7. package/dist/distributed-swarm-worker.d.ts.map +1 -0
  8. package/dist/distributed-swarm-worker.js +151 -0
  9. package/dist/distributed-swarm-worker.js.map +1 -0
  10. package/dist/index.d.ts +4 -2
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +3 -1
  13. package/dist/index.js.map +1 -1
  14. package/dist/metrics.d.ts.map +1 -1
  15. package/dist/metrics.js +22 -15
  16. package/dist/metrics.js.map +1 -1
  17. package/dist/processors/agent.d.ts +12 -2
  18. package/dist/processors/agent.d.ts.map +1 -1
  19. package/dist/processors/agent.js +25 -23
  20. package/dist/processors/agent.js.map +1 -1
  21. package/dist/processors/index.d.ts +3 -3
  22. package/dist/processors/index.d.ts.map +1 -1
  23. package/dist/processors/index.js +3 -3
  24. package/dist/processors/index.js.map +1 -1
  25. package/dist/processors/shared.d.ts +26 -3
  26. package/dist/processors/shared.d.ts.map +1 -1
  27. package/dist/processors/shared.js +38 -25
  28. package/dist/processors/shared.js.map +1 -1
  29. package/dist/processors/swarm-agent.d.ts +25 -4
  30. package/dist/processors/swarm-agent.d.ts.map +1 -1
  31. package/dist/processors/swarm-agent.js +33 -47
  32. package/dist/processors/swarm-agent.js.map +1 -1
  33. package/dist/processors/swarm.d.ts +7 -2
  34. package/dist/processors/swarm.d.ts.map +1 -1
  35. package/dist/processors/swarm.js +75 -50
  36. package/dist/processors/swarm.js.map +1 -1
  37. package/dist/processors/workflow.d.ts +21 -5
  38. package/dist/processors/workflow.d.ts.map +1 -1
  39. package/dist/processors/workflow.js +304 -8
  40. package/dist/processors/workflow.js.map +1 -1
  41. package/dist/queue.d.ts +6 -5
  42. package/dist/queue.d.ts.map +1 -1
  43. package/dist/queue.js +16 -19
  44. package/dist/queue.js.map +1 -1
  45. package/dist/types.d.ts +58 -33
  46. package/dist/types.d.ts.map +1 -1
  47. package/dist/worker.d.ts +10 -1
  48. package/dist/worker.d.ts.map +1 -1
  49. package/dist/worker.js +62 -24
  50. package/dist/worker.js.map +1 -1
  51. package/package.json +16 -13
package/README.md CHANGED
@@ -5,13 +5,15 @@ Distributed job queue for Cogitator agent execution. Built on BullMQ for reliabl
5
5
  ## Installation
6
6
 
7
7
  ```bash
8
- pnpm add @cogitator-ai/worker ioredis
8
+ pnpm add @cogitator-ai/worker @cogitator-ai/core ioredis
9
9
  ```
10
10
 
11
11
  ## Features
12
12
 
13
13
  - **BullMQ-Based** - Reliable job processing with Redis
14
- - **Job Types** - Agents, workflows, and swarms
14
+ - **Job Types** - Agents, workflow graphs, swarms and distributed swarm turns
15
+ - **Worker Runtime** - Run jobs with your own `Cogitator` (provider keys, memory) and tool implementations
16
+ - **Distributed Swarms** - `DistributedSwarmWorker` executes agent turns for `@cogitator-ai/swarms`
15
17
  - **Auto-Retry** - Exponential backoff for failed jobs
16
18
  - **Priority Queue** - Process important jobs first
17
19
  - **Delayed Jobs** - Schedule jobs for later execution
@@ -35,7 +37,7 @@ const queue = new JobQueue({
35
37
  const agentConfig = {
36
38
  name: 'Assistant',
37
39
  instructions: 'You are a helpful assistant.',
38
- model: 'openai/gpt-4',
40
+ model: 'openai/gpt-6.1-sol',
39
41
  provider: 'openai' as const,
40
42
  tools: [],
41
43
  };
@@ -51,17 +53,24 @@ console.log(`Job added: ${job.id}`);
51
53
  ### Consumer: Process Jobs
52
54
 
53
55
  ```typescript
56
+ import { Cogitator } from '@cogitator-ai/core';
54
57
  import { WorkerPool } from '@cogitator-ai/worker';
55
58
 
56
59
  const pool = new WorkerPool({
57
60
  redis: { host: 'localhost', port: 6379 },
58
61
  concurrency: 5,
59
62
  workerCount: 2,
63
+ cogitator: new Cogitator({
64
+ llm: { providers: { openai: { apiKey: process.env.OPENAI_API_KEY } } },
65
+ }),
66
+ tools: [searchTool], // implementations for tools referenced by serialized agents
60
67
  });
61
68
 
62
69
  await pool.start();
63
70
  ```
64
71
 
72
+ Serialized agents reference tools by name. The worker resolves them from its `tools`; a job whose agent needs a tool the worker does not provide fails with `Tools not registered on this worker: <names>`.
73
+
65
74
  ---
66
75
 
67
76
  ## Job Queue
@@ -122,10 +131,11 @@ interface QueueConfig {
122
131
  const agentConfig: SerializedAgent = {
123
132
  name: 'Researcher',
124
133
  instructions: 'Research and summarize topics.',
125
- model: 'openai/gpt-4',
134
+ model: 'openai/gpt-6.1-sol', // or 'gpt-6.1-sol' — the provider is prepended when missing
126
135
  provider: 'openai',
127
136
  temperature: 0.7,
128
137
  maxTokens: 2048,
138
+ maxIterations: 5,
129
139
  tools: [
130
140
  {
131
141
  name: 'search',
@@ -145,38 +155,60 @@ const job = await queue.addAgentJob(agentConfig, 'Research quantum computing', {
145
155
 
146
156
  **Workflow Jobs:**
147
157
 
158
+ Workflow jobs run a DAG over a shared state object initialised from the job input. Nodes run as soon as their predecessors settle; independent branches run concurrently.
159
+
148
160
  ```typescript
149
161
  const workflowConfig: SerializedWorkflow = {
150
- id: 'data-pipeline',
151
- name: 'Data Pipeline',
162
+ id: 'triage',
163
+ name: 'Ticket triage',
152
164
  nodes: [
153
- { id: 'fetch', type: 'agent', config: { agentConfig: fetchAgent } },
154
- { id: 'process', type: 'transform', config: { transform: 'uppercase' } },
155
- { id: 'store', type: 'agent', config: { agentConfig: storeAgent } },
165
+ {
166
+ id: 'classify',
167
+ type: 'agent',
168
+ config: {
169
+ agentConfig: classifierAgent, // SerializedAgent
170
+ prompt: 'Classify this ticket as BUG or QUESTION: {{ticket}}',
171
+ outputKey: 'category',
172
+ },
173
+ },
174
+ { id: 'normalize', type: 'transform', config: { transform: 'trim', inputKey: 'category' } },
175
+ {
176
+ id: 'is-bug',
177
+ type: 'condition',
178
+ config: { key: 'normalize', operator: 'contains', value: 'BUG' },
179
+ },
180
+ {
181
+ id: 'summary',
182
+ type: 'transform',
183
+ config: { transform: 'template', template: 'Bug report: {{ticket}}' },
184
+ },
156
185
  ],
157
186
  edges: [
158
- { from: 'fetch', to: 'process' },
159
- { from: 'process', to: 'store' },
187
+ { from: 'classify', to: 'normalize' },
188
+ { from: 'normalize', to: 'is-bug' },
189
+ { from: 'is-bug', to: 'summary', condition: 'true' },
160
190
  ],
161
191
  };
162
192
 
163
- await queue.addWorkflowJob(
164
- workflowConfig,
165
- { source: 'api' },
166
- {
167
- runId: 'run-789',
168
- priority: 2,
169
- }
170
- );
193
+ await queue.addWorkflowJob(workflowConfig, { ticket: 'App crashes on login' });
171
194
  ```
172
195
 
196
+ | Node type | Config |
197
+ | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
198
+ | `agent` | `{ agentConfig, prompt?, outputKey? }` — `prompt` supports `{{path}}` placeholders; default prompt is the state as JSON |
199
+ | `transform` | `{ transform, inputKey?, outputKey?, template? }` — `uppercase`, `lowercase`, `trim`, `json-parse`, `json-stringify`, `template` |
200
+ | `condition` | `{ key, operator, value? }` — `equals`, `not-equals`, `contains`, `exists`, `gt`, `lt`; outgoing edges use `condition: 'true' \| 'false'` |
201
+ | `parallel` | `{}` — fan-out marker; successors run concurrently |
202
+
203
+ Node outputs are stored in the state under `outputKey` (default: node id). Nodes reachable only through untaken condition branches are skipped (`{ skipped: true }` in `nodeResults`). Graphs are validated before execution (unknown nodes, invalid configs, cycles).
204
+
173
205
  **Swarm Jobs:**
174
206
 
175
207
  ```typescript
176
208
  const swarmConfig: SerializedSwarm = {
177
- topology: 'collaborative',
209
+ topology: 'voting',
178
210
  agents: [researcherConfig, writerConfig, editorConfig],
179
- coordinator: coordinatorConfig,
211
+ coordinator: coordinatorConfig, // decides when no consensus is reached
180
212
  maxRounds: 3,
181
213
  consensusThreshold: 0.8,
182
214
  };
@@ -187,6 +219,14 @@ await queue.addSwarmJob(swarmConfig, 'Write an article about AI', {
187
219
  });
188
220
  ```
189
221
 
222
+ | Topology | Swarm strategy | Notes |
223
+ | --------------- | -------------- | ------------------------------------------------------------ |
224
+ | `sequential` | `pipeline` | One stage per agent, in order |
225
+ | `hierarchical` | `hierarchical` | `coordinator` is required and becomes the supervisor |
226
+ | `collaborative` | `round-robin` | |
227
+ | `debate` | `debate` | `maxRounds` rounds, `coordinator` moderates |
228
+ | `voting` | `consensus` | `consensusThreshold`, `maxRounds`; `coordinator` breaks ties |
229
+
190
230
  ### Queue Methods
191
231
 
192
232
  ```typescript
@@ -254,6 +294,8 @@ interface WorkerConfig extends QueueConfig {
254
294
  concurrency?: number; // Default: 5
255
295
  lockDuration?: number; // Default: 30000ms
256
296
  stalledInterval?: number; // Default: 30000ms
297
+ cogitator?: Cogitator; // Default: new Cogitator()
298
+ tools?: Tool[]; // Tool implementations, resolved by name
257
299
  }
258
300
  ```
259
301
 
@@ -268,7 +310,7 @@ interface WorkerConfig extends QueueConfig {
268
310
 
269
311
  ```typescript
270
312
  interface WorkerPoolEvents {
271
- onJobStarted?: (jobId: string, type: 'agent' | 'workflow' | 'swarm') => void;
313
+ onJobStarted?: (jobId: string, type: 'agent' | 'workflow' | 'swarm' | 'swarm-agent') => void;
272
314
  onJobCompleted?: (jobId: string, result: JobResult) => void;
273
315
  onJobFailed?: (jobId: string, error: Error) => void;
274
316
  onWorkerError?: (error: Error) => void;
@@ -286,7 +328,10 @@ pool.getWorkerCount();
286
328
 
287
329
  const metrics = await pool.getMetrics(await queue.getMetrics());
288
330
 
289
- // Graceful shutdown (waits up to 30s for active jobs)
331
+ // Job duration histogram and per-type counters
332
+ pool.metrics.format(await pool.getMetrics(await queue.getMetrics()));
333
+
334
+ // Graceful shutdown (waits up to 30s for active jobs, then force-closes)
290
335
  await pool.stop(30000);
291
336
 
292
337
  // Force shutdown
@@ -301,26 +346,62 @@ Built-in processors handle each job type.
301
346
 
302
347
  ### Using Processors Directly
303
348
 
349
+ Processors take the job payload and an optional runtime (`{ cogitator, tools }`):
350
+
304
351
  ```typescript
305
- import { processAgentJob, processSwarmJob, processSwarmAgentJob } from '@cogitator-ai/worker';
306
-
307
- const agentResult = await processAgentJob({
308
- type: 'agent',
309
- jobId: 'job-1',
310
- agentConfig: myAgentConfig,
311
- input: 'Hello!',
312
- threadId: 'thread-1',
313
- });
352
+ import { processAgentJob, processWorkflowJob, processSwarmJob } from '@cogitator-ai/worker';
314
353
 
315
- const swarmResult = await processSwarmJob({
316
- type: 'swarm',
317
- jobId: 'job-3',
318
- swarmConfig: mySwarmConfig,
319
- input: 'Solve this problem',
320
- });
354
+ const runtime = { cogitator, tools: [searchTool] };
355
+
356
+ const agentResult = await processAgentJob(
357
+ { type: 'agent', jobId: 'job-1', agentConfig: myAgentConfig, input: 'Hello!', threadId: 't-1' },
358
+ runtime
359
+ );
360
+
361
+ const workflowResult = await processWorkflowJob(
362
+ { type: 'workflow', jobId: 'job-2', runId: 'run-1', workflowConfig, input: { ticket: '...' } },
363
+ runtime
364
+ );
365
+
366
+ const swarmResult = await processSwarmJob(
367
+ { type: 'swarm', jobId: 'job-3', swarmConfig: mySwarmConfig, input: 'Solve this problem' },
368
+ runtime
369
+ );
370
+ ```
371
+
372
+ `processSwarmAgentJob(payload, { publisher, isFinalAttempt, ...runtime })` executes one distributed swarm turn and publishes the result (tagged with the job id) to `payload.stateKeys.results`; `executeSwarmAgentJob` returns the result without publishing.
373
+
374
+ ---
375
+
376
+ ## Distributed Swarm Workers
377
+
378
+ Swarms created with `distributed.enabled` (see `@cogitator-ai/swarms`) dispatch every agent turn to a Redis queue. `DistributedSwarmWorker` consumes those turns:
379
+
380
+ ```typescript
381
+ import { Cogitator } from '@cogitator-ai/core';
382
+ import { DistributedSwarmWorker } from '@cogitator-ai/worker';
383
+
384
+ const worker = new DistributedSwarmWorker(
385
+ {
386
+ redis: { host: 'localhost', port: 6379 },
387
+ keyPrefix: 'swarm', // must match the swarm's distributed.redis.keyPrefix
388
+ queue: 'swarm-agent-jobs', // must match distributed.queue
389
+ concurrency: 4,
390
+ cogitator: new Cogitator({ llm: { defaultModel: 'ollama/llama3.2' } }),
391
+ tools: [searchTool],
392
+ },
393
+ {
394
+ onJobCompleted: (job) => console.log('done', job.agentName),
395
+ onJobFailed: (job, error) => console.error(job.agentName, error.message),
396
+ onError: (error) => console.error(error),
397
+ }
398
+ );
399
+
400
+ await worker.start();
401
+ process.on('SIGTERM', () => void worker.stop()); // waits for in-flight turns
321
402
  ```
322
403
 
323
- > **Note:** `processWorkflowJob` is not yet implemented — it throws an error. Workflows should be executed directly via `WorkflowExecutor` from `@cogitator-ai/workflows`.
404
+ Failed turns are reported back to the swarm as errors, so the swarm's own `errorHandling` (retry, failover, skip) applies.
324
405
 
325
406
  ---
326
407
 
@@ -381,24 +462,18 @@ Built-in metrics for monitoring and Kubernetes HPA.
381
462
  ### Exposing Metrics
382
463
 
383
464
  ```typescript
384
- import {
385
- JobQueue,
386
- WorkerPool,
387
- MetricsCollector,
388
- formatPrometheusMetrics,
389
- } from '@cogitator-ai/worker';
465
+ import { JobQueue, WorkerPool } from '@cogitator-ai/worker';
390
466
  import express from 'express';
391
467
 
392
468
  const queue = new JobQueue({ redis: { host: 'localhost', port: 6379 } });
393
469
  const pool = new WorkerPool({ redis: { host: 'localhost', port: 6379 } });
394
- const metrics = new MetricsCollector();
470
+ await pool.start();
395
471
 
396
472
  const app = express();
397
473
 
398
474
  app.get('/metrics', async (req, res) => {
399
- const queueMetrics = await queue.getMetrics();
400
- const fullMetrics = await pool.getMetrics(queueMetrics);
401
- res.type('text/plain').send(metrics.format(fullMetrics));
475
+ const queueMetrics = await queue.getMetrics(); // workerCount = workers connected to the queue
476
+ res.type('text/plain').send(pool.metrics.format(queueMetrics));
402
477
  });
403
478
 
404
479
  app.listen(9090);
@@ -414,7 +489,7 @@ app.listen(9090);
414
489
  | `cogitator_queue_completed_total` | counter | Total completed jobs |
415
490
  | `cogitator_queue_failed_total` | counter | Total failed jobs |
416
491
  | `cogitator_queue_delayed` | gauge | Scheduled/delayed jobs |
417
- | `cogitator_workers_total` | gauge | Active workers |
492
+ | `cogitator_workers_total` | gauge | Workers connected to the queue |
418
493
  | `cogitator_job_duration_seconds` | histogram | Job processing time |
419
494
  | `cogitator_jobs_by_type_total` | counter | Jobs by type |
420
495
 
@@ -489,6 +564,8 @@ const queue = new JobQueue({
489
564
 
490
565
  ### Redis Cluster
491
566
 
567
+ Queues and workers connect to all cluster nodes (keys use the `{cogitator}` hash tag so they live in one slot):
568
+
492
569
  ```typescript
493
570
  const queue = new JobQueue({
494
571
  redis: {
@@ -516,11 +593,12 @@ Jobs use serialized configurations that can be stored in Redis.
516
593
  interface SerializedAgent {
517
594
  name: string;
518
595
  instructions: string;
519
- model: string;
520
- provider: 'ollama' | 'openai' | 'anthropic';
596
+ model: string; // may include the provider prefix
597
+ provider: LLMProvider; // used when model has no prefix
521
598
  temperature?: number;
522
599
  maxTokens?: number;
523
- tools: ToolSchema[];
600
+ maxIterations?: number;
601
+ tools: ToolSchema[]; // resolved by name against the worker's tools
524
602
  }
525
603
  ```
526
604
 
@@ -543,10 +621,12 @@ interface SerializedWorkflowNode {
543
621
  interface SerializedWorkflowEdge {
544
622
  from: string;
545
623
  to: string;
546
- condition?: string;
624
+ condition?: string; // 'true' | 'false' for edges leaving condition nodes
547
625
  }
548
626
  ```
549
627
 
628
+ Node configs are typed as `AgentNodeConfig`, `TransformNodeConfig` and `ConditionNodeConfig`.
629
+
550
630
  ### SerializedSwarm
551
631
 
552
632
  ```typescript
@@ -578,7 +658,7 @@ async function main() {
578
658
  const agentConfig = {
579
659
  name: 'Summarizer',
580
660
  instructions: 'Summarize the given text concisely.',
581
- model: 'openai/gpt-4',
661
+ model: 'openai/gpt-6.1-sol',
582
662
  provider: 'openai' as const,
583
663
  tools: [],
584
664
  };
@@ -694,6 +774,9 @@ import type {
694
774
  SerializedWorkflow,
695
775
  SerializedWorkflowNode,
696
776
  SerializedWorkflowEdge,
777
+ AgentNodeConfig,
778
+ TransformNodeConfig,
779
+ ConditionNodeConfig,
697
780
  SerializedSwarm,
698
781
 
699
782
  // Job payloads
@@ -713,7 +796,10 @@ import type {
713
796
  // Configuration
714
797
  QueueConfig,
715
798
  WorkerConfig,
799
+ WorkerRuntime,
716
800
  QueueMetrics,
801
+ DistributedSwarmWorkerConfig,
802
+ DistributedSwarmWorkerEvents,
717
803
  } from '@cogitator-ai/worker';
718
804
  ```
719
805
 
@@ -0,0 +1,29 @@
1
+ import { Cluster, Redis } from 'ioredis';
2
+ import type { ConnectionOptions } from 'bullmq';
3
+ import type { QueueConfig } from './types.js';
4
+ export declare const DEFAULT_QUEUE_NAME = "cogitator-jobs";
5
+ export type RedisClient = Redis | Cluster;
6
+ type RedisConnectionConfig = QueueConfig['redis'];
7
+ /**
8
+ * BullMQ key prefix; cluster mode needs a hash tag so all queue keys share a slot.
9
+ */
10
+ export declare function queuePrefix(redis: RedisConnectionConfig): string;
11
+ export interface BullConnection {
12
+ connection: ConnectionOptions;
13
+ /** Close connections that BullMQ treats as shared (cluster instances) */
14
+ dispose(): Promise<void>;
15
+ }
16
+ /**
17
+ * Connection for BullMQ queues and workers. Blocking (worker) connections must not limit
18
+ * retries per request, as BullMQ requires. Cluster instances are shared with BullMQ, which
19
+ * does not close them, so callers must `dispose()` them.
20
+ */
21
+ export declare function createBullConnection(redis: RedisConnectionConfig, options?: {
22
+ blocking?: boolean;
23
+ }): BullConnection;
24
+ /**
25
+ * Plain client (e.g. for publishing swarm results) using the same Redis configuration.
26
+ */
27
+ export declare function createRedisClient(redis: RedisConnectionConfig): RedisClient;
28
+ export {};
29
+ //# sourceMappingURL=connection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connection.d.ts","sourceRoot":"","sources":["../src/connection.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AACzC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,QAAQ,CAAC;AAChD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAE3C,eAAO,MAAM,kBAAkB,mBAAmB,CAAC;AAEnD,MAAM,MAAM,WAAW,GAAG,KAAK,GAAG,OAAO,CAAC;AAE1C,KAAK,qBAAqB,GAAG,WAAW,CAAC,OAAO,CAAC,CAAC;AAElD;;GAEG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,qBAAqB,GAAG,MAAM,CAEhE;AAED,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE,iBAAiB,CAAC;IAC9B,yEAAyE;IACzE,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,qBAAqB,EAC5B,OAAO,GAAE;IAAE,QAAQ,CAAC,EAAE,OAAO,CAAA;CAAO,GACnC,cAAc,CAiChB;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,qBAAqB,GAAG,WAAW,CAa3E"}
@@ -0,0 +1,63 @@
1
+ import { Cluster, Redis } from 'ioredis';
2
+ export const DEFAULT_QUEUE_NAME = 'cogitator-jobs';
3
+ /**
4
+ * BullMQ key prefix; cluster mode needs a hash tag so all queue keys share a slot.
5
+ */
6
+ export function queuePrefix(redis) {
7
+ return redis.cluster ? '{cogitator}' : 'cogitator';
8
+ }
9
+ /**
10
+ * Connection for BullMQ queues and workers. Blocking (worker) connections must not limit
11
+ * retries per request, as BullMQ requires. Cluster instances are shared with BullMQ, which
12
+ * does not close them, so callers must `dispose()` them.
13
+ */
14
+ export function createBullConnection(redis, options = {}) {
15
+ const maxRetriesPerRequest = options.blocking ? null : undefined;
16
+ if (redis.cluster) {
17
+ if (redis.cluster.nodes.length === 0) {
18
+ throw new Error('Redis cluster configuration requires at least one node');
19
+ }
20
+ const cluster = new Cluster(redis.cluster.nodes, {
21
+ lazyConnect: true,
22
+ redisOptions: { password: redis.password, maxRetriesPerRequest },
23
+ });
24
+ return {
25
+ connection: cluster,
26
+ dispose: async () => {
27
+ if (cluster.status === 'end')
28
+ return;
29
+ if (cluster.status === 'wait') {
30
+ cluster.disconnect();
31
+ return;
32
+ }
33
+ await cluster.quit();
34
+ },
35
+ };
36
+ }
37
+ return {
38
+ connection: {
39
+ host: redis.host ?? 'localhost',
40
+ port: redis.port ?? 6379,
41
+ password: redis.password,
42
+ maxRetriesPerRequest,
43
+ },
44
+ dispose: async () => { },
45
+ };
46
+ }
47
+ /**
48
+ * Plain client (e.g. for publishing swarm results) using the same Redis configuration.
49
+ */
50
+ export function createRedisClient(redis) {
51
+ if (redis.cluster) {
52
+ if (redis.cluster.nodes.length === 0) {
53
+ throw new Error('Redis cluster configuration requires at least one node');
54
+ }
55
+ return new Cluster(redis.cluster.nodes, { redisOptions: { password: redis.password } });
56
+ }
57
+ return new Redis({
58
+ host: redis.host ?? 'localhost',
59
+ port: redis.port ?? 6379,
60
+ password: redis.password,
61
+ });
62
+ }
63
+ //# sourceMappingURL=connection.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connection.js","sourceRoot":"","sources":["../src/connection.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAIzC,MAAM,CAAC,MAAM,kBAAkB,GAAG,gBAAgB,CAAC;AAMnD;;GAEG;AACH,MAAM,UAAU,WAAW,CAAC,KAA4B;IACtD,OAAO,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,WAAW,CAAC;AACrD,CAAC;AAQD;;;;GAIG;AACH,MAAM,UAAU,oBAAoB,CAClC,KAA4B,EAC5B,UAAkC,EAAE;IAEpC,MAAM,oBAAoB,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC;IAEjE,IAAI,KAAK,CAAC,OAAO,EAAE,CAAC;QAClB,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACrC,MAAM,IAAI,KAAK,CAAC,wDAAwD,CAAC,CAAC;QAC5E,CAAC;QACD,MAAM,OAAO,GAAG,IAAI,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,EAAE;YAC/C,WAAW,EAAE,IAAI;YACjB,YAAY,EAAE,EAAE,QAAQ,EAAE,KAAK,CAAC,QAAQ,EAAE,oBAAoB,EAAE;SACjE,CAAC,CAAC;QACH,OAAO;YACL,UAAU,EAAE,OAAO;YACnB,OAAO,EAAE,KAAK,IAAI,EAAE;gBAClB,IAAI,OAAO,CAAC,MAAM,KAAK,KAAK;oBAAE,OAAO;gBACrC,IAAI,OAAO,CAAC,MAAM,KAAK,MAAM,EAAE,CAAC;oBAC9B,OAAO,CAAC,UAAU,EAAE,CAAC;oBACrB,OAAO;gBACT,CAAC;gBACD,MAAM,OAAO,CAAC,IAAI,EAAE,CAAC;YACvB,CAAC;SACF,CAAC;IACJ,CAAC;IAED,OAAO;QACL,UAAU,EAAE;YACV,IAAI,EAAE,KAAK,CAAC,IAAI,IAAI,WAAW;YAC/B,IAAI,EAAE,KAAK,CAAC,IAAI,IAAI,IAAI;YACxB,QAAQ,EAAE,KAAK,CAAC,QAAQ;YACxB,oBAAoB;SACrB;QACD,OAAO,EAAE,KAAK,IAAI,EAAE,GAAE,CAAC;KACxB,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,iBAAiB,CAAC,KAA4B;IAC5D,IAAI,KAAK,CAAC,OAAO,EAAE,CAAC;QAClB,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACrC,MAAM,IAAI,KAAK,CAAC,wDAAwD,CAAC,CAAC;QAC5E,CAAC;QACD,OAAO,IAAI,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,YAAY,EAAE,EAAE,QAAQ,EAAE,KAAK,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;IAC1F,CAAC;IAED,OAAO,IAAI,KAAK,CAAC;QACf,IAAI,EAAE,KAAK,CAAC,IAAI,IAAI,WAAW;QAC/B,IAAI,EAAE,KAAK,CAAC,IAAI,IAAI,IAAI;QACxB,QAAQ,EAAE,KAAK,CAAC,QAAQ;KACzB,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Worker node for distributed swarms.
3
+ *
4
+ * Consumes agent turns dispatched by `DistributedSwarmCoordinator` (from
5
+ * `@cogitator-ai/swarms`), runs them with the worker's Cogitator and tools, and publishes
6
+ * each result back to the coordinator's results channel.
7
+ */
8
+ import type { SwarmAgentJobPayload, SwarmAgentJobResult, WorkerRuntime } from './types.js';
9
+ export interface DistributedSwarmWorkerConfig extends WorkerRuntime {
10
+ /** Redis holding the swarm job queue (must match the swarm's `distributed.redis`) */
11
+ redis?: {
12
+ host?: string;
13
+ port?: number;
14
+ password?: string;
15
+ db?: number;
16
+ };
17
+ /** Swarm key prefix (must match `distributed.redis.keyPrefix`, default: 'swarm') */
18
+ keyPrefix?: string;
19
+ /** Job queue name (must match `distributed.queue`, default: 'swarm-agent-jobs') */
20
+ queue?: string;
21
+ /** Agent turns processed concurrently by this worker (default: 4) */
22
+ concurrency?: number;
23
+ /** Seconds a blocking poll waits for new jobs before re-checking state (default: 1) */
24
+ pollTimeout?: number;
25
+ }
26
+ export interface DistributedSwarmWorkerEvents {
27
+ onJobStarted?: (job: SwarmAgentJobPayload) => void;
28
+ onJobCompleted?: (job: SwarmAgentJobPayload, result: SwarmAgentJobResult) => void;
29
+ onJobFailed?: (job: SwarmAgentJobPayload, error: Error) => void;
30
+ onError?: (error: Error) => void;
31
+ }
32
+ export declare class DistributedSwarmWorker {
33
+ private readonly config;
34
+ private readonly events;
35
+ private readonly runtime;
36
+ private readonly queueKey;
37
+ private consumer?;
38
+ private publisher?;
39
+ private loop?;
40
+ private running;
41
+ private readonly active;
42
+ constructor(config?: DistributedSwarmWorkerConfig, events?: DistributedSwarmWorkerEvents);
43
+ /**
44
+ * Start consuming jobs. Resolves once the worker is connected.
45
+ */
46
+ start(): Promise<void>;
47
+ /**
48
+ * Stop taking new jobs, wait for in-flight jobs to finish and close connections.
49
+ */
50
+ stop(): Promise<void>;
51
+ isRunning(): boolean;
52
+ getActiveJobCount(): number;
53
+ private consume;
54
+ private handle;
55
+ }
56
+ //# sourceMappingURL=distributed-swarm-worker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"distributed-swarm-worker.d.ts","sourceRoot":"","sources":["../src/distributed-swarm-worker.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,KAAK,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAKxF,MAAM,WAAW,4BAA6B,SAAQ,aAAa;IACjE,qFAAqF;IACrF,KAAK,CAAC,EAAE;QACN,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,EAAE,CAAC,EAAE,MAAM,CAAC;KACb,CAAC;IACF,oFAAoF;IACpF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mFAAmF;IACnF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qEAAqE;IACrE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,uFAAuF;IACvF,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,4BAA4B;IAC3C,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,oBAAoB,KAAK,IAAI,CAAC;IACnD,cAAc,CAAC,EAAE,CAAC,GAAG,EAAE,oBAAoB,EAAE,MAAM,EAAE,mBAAmB,KAAK,IAAI,CAAC;IAClF,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,oBAAoB,EAAE,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IAChE,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;CAClC;AAgBD,qBAAa,sBAAsB;IACjC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA+B;IACtD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA+B;IACtD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA0B;IAClD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,CAAQ;IACzB,OAAO,CAAC,SAAS,CAAC,CAAQ;IAC1B,OAAO,CAAC,IAAI,CAAC,CAAgB;IAC7B,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA4B;gBAGjD,MAAM,GAAE,4BAAiC,EACzC,MAAM,GAAE,4BAAiC;IAW3C;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IA2B5B;;OAEG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAc3B,SAAS,IAAI,OAAO;IAIpB,iBAAiB,IAAI,MAAM;YAIb,OAAO;YA6BP,MAAM;CAgCrB"}
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Worker node for distributed swarms.
3
+ *
4
+ * Consumes agent turns dispatched by `DistributedSwarmCoordinator` (from
5
+ * `@cogitator-ai/swarms`), runs them with the worker's Cogitator and tools, and publishes
6
+ * each result back to the coordinator's results channel.
7
+ */
8
+ import { Redis } from 'ioredis';
9
+ import { swarmJobQueueKey } from '@cogitator-ai/swarms';
10
+ import { executeSwarmAgentJob } from './processors/swarm-agent.js';
11
+ import { toErrorMessage } from './processors/shared.js';
12
+ import { Cogitator } from '@cogitator-ai/core';
13
+ function isSwarmAgentJob(value) {
14
+ if (typeof value !== 'object' || value === null)
15
+ return false;
16
+ const job = value;
17
+ return (job.type === 'swarm-agent' &&
18
+ typeof job.jobId === 'string' &&
19
+ typeof job.agentName === 'string' &&
20
+ typeof job.input === 'string' &&
21
+ typeof job.agentConfig === 'object' &&
22
+ job.agentConfig !== null &&
23
+ typeof job.stateKeys?.results === 'string');
24
+ }
25
+ export class DistributedSwarmWorker {
26
+ config;
27
+ events;
28
+ runtime;
29
+ queueKey;
30
+ consumer;
31
+ publisher;
32
+ loop;
33
+ running = false;
34
+ active = new Set();
35
+ constructor(config = {}, events = {}) {
36
+ this.config = config;
37
+ this.events = events;
38
+ this.runtime = {
39
+ cogitator: config.cogitator ?? new Cogitator(),
40
+ tools: config.tools ?? [],
41
+ };
42
+ this.queueKey = swarmJobQueueKey(config.keyPrefix, config.queue);
43
+ }
44
+ /**
45
+ * Start consuming jobs. Resolves once the worker is connected.
46
+ */
47
+ async start() {
48
+ if (this.running)
49
+ return;
50
+ const options = {
51
+ host: this.config.redis?.host ?? 'localhost',
52
+ port: this.config.redis?.port ?? 6379,
53
+ password: this.config.redis?.password,
54
+ db: this.config.redis?.db ?? 0,
55
+ lazyConnect: true,
56
+ };
57
+ const consumer = new Redis({ ...options, maxRetriesPerRequest: null });
58
+ const publisher = new Redis(options);
59
+ try {
60
+ await Promise.all([consumer.connect(), publisher.connect()]);
61
+ }
62
+ catch (error) {
63
+ consumer.disconnect();
64
+ publisher.disconnect();
65
+ throw error;
66
+ }
67
+ this.consumer = consumer;
68
+ this.publisher = publisher;
69
+ this.running = true;
70
+ this.loop = this.consume();
71
+ }
72
+ /**
73
+ * Stop taking new jobs, wait for in-flight jobs to finish and close connections.
74
+ */
75
+ async stop() {
76
+ if (!this.running)
77
+ return;
78
+ this.running = false;
79
+ this.consumer?.disconnect();
80
+ await this.loop;
81
+ await Promise.allSettled([...this.active]);
82
+ await this.publisher?.quit();
83
+ this.consumer = undefined;
84
+ this.publisher = undefined;
85
+ this.loop = undefined;
86
+ }
87
+ isRunning() {
88
+ return this.running;
89
+ }
90
+ getActiveJobCount() {
91
+ return this.active.size;
92
+ }
93
+ async consume() {
94
+ const concurrency = Math.max(1, this.config.concurrency ?? 4);
95
+ const pollTimeout = Math.max(0.1, this.config.pollTimeout ?? 1);
96
+ while (this.running) {
97
+ if (this.active.size >= concurrency) {
98
+ await Promise.race(this.active);
99
+ continue;
100
+ }
101
+ let entry;
102
+ try {
103
+ entry = await this.consumer.blpop(this.queueKey, pollTimeout);
104
+ }
105
+ catch (error) {
106
+ if (!this.running)
107
+ return;
108
+ this.events.onError?.(error instanceof Error ? error : new Error(toErrorMessage(error)));
109
+ await new Promise((resolve) => setTimeout(resolve, 1000));
110
+ continue;
111
+ }
112
+ if (!entry)
113
+ continue;
114
+ const task = this.handle(entry[1]).finally(() => {
115
+ this.active.delete(task);
116
+ });
117
+ this.active.add(task);
118
+ }
119
+ }
120
+ async handle(raw) {
121
+ let parsed;
122
+ try {
123
+ parsed = JSON.parse(raw);
124
+ }
125
+ catch {
126
+ this.events.onError?.(new Error('Discarded swarm job with invalid JSON'));
127
+ return;
128
+ }
129
+ if (!isSwarmAgentJob(parsed)) {
130
+ this.events.onError?.(new Error('Discarded malformed swarm job payload'));
131
+ return;
132
+ }
133
+ const job = parsed;
134
+ this.events.onJobStarted?.(job);
135
+ const result = await executeSwarmAgentJob(job, this.runtime);
136
+ try {
137
+ await this.publisher.publish(job.stateKeys.results, JSON.stringify(result));
138
+ }
139
+ catch (error) {
140
+ this.events.onError?.(error instanceof Error ? error : new Error(toErrorMessage(error)));
141
+ return;
142
+ }
143
+ if (result.error !== undefined) {
144
+ this.events.onJobFailed?.(job, new Error(result.error));
145
+ }
146
+ else {
147
+ this.events.onJobCompleted?.(job, result);
148
+ }
149
+ }
150
+ }
151
+ //# sourceMappingURL=distributed-swarm-worker.js.map