@cogitator-ai/worker 0.9.24 → 0.11.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/README.md +76 -22
- package/dist/connection.d.ts +1 -17
- package/dist/connection.d.ts.map +1 -1
- package/dist/connection.js +2 -47
- package/dist/connection.js.map +1 -1
- package/dist/distributed-swarm-worker.d.ts +73 -14
- package/dist/distributed-swarm-worker.d.ts.map +1 -1
- package/dist/distributed-swarm-worker.js +181 -31
- package/dist/distributed-swarm-worker.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/metrics.d.ts +14 -0
- package/dist/metrics.d.ts.map +1 -1
- package/dist/metrics.js +18 -1
- package/dist/metrics.js.map +1 -1
- package/dist/processors/swarm-agent.d.ts +6 -5
- package/dist/processors/swarm-agent.d.ts.map +1 -1
- package/dist/processors/swarm-agent.js +6 -2
- package/dist/processors/swarm-agent.js.map +1 -1
- package/dist/processors/swarm.d.ts.map +1 -1
- package/dist/processors/swarm.js +10 -0
- package/dist/processors/swarm.js.map +1 -1
- package/dist/processors/workflow.d.ts +20 -7
- package/dist/processors/workflow.d.ts.map +1 -1
- package/dist/processors/workflow.js +238 -119
- package/dist/processors/workflow.js.map +1 -1
- package/dist/queue.d.ts +30 -2
- package/dist/queue.d.ts.map +1 -1
- package/dist/queue.js +94 -33
- package/dist/queue.js.map +1 -1
- package/dist/types.d.ts +44 -18
- package/dist/types.d.ts.map +1 -1
- package/dist/worker.d.ts +12 -0
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +59 -3
- package/dist/worker.js.map +1 -1
- package/package.json +6 -5
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ Distributed job queue for Cogitator agent execution. Built on BullMQ for reliabl
|
|
|
8
8
|
pnpm add @cogitator-ai/worker @cogitator-ai/core ioredis
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
BullMQ and `@cogitator-ai/
|
|
11
|
+
BullMQ, `@cogitator-ai/swarms` and `@cogitator-ai/workflows` come as dependencies, `ioredis` is a peer dependency. Website docs: [Worker Queues](https://cogitator.app/docs/deployment/worker-queues), [Distributed Swarms](https://cogitator.app/docs/swarms/distributed).
|
|
12
12
|
|
|
13
13
|
## Features
|
|
14
14
|
|
|
@@ -16,7 +16,9 @@ BullMQ and `@cogitator-ai/swarms` come as dependencies; `ioredis` is a peer depe
|
|
|
16
16
|
- **Job Types** - Agents, workflow graphs, swarms and distributed swarm turns
|
|
17
17
|
- **Worker Runtime** - Run jobs with your own `Cogitator` (provider keys, memory) and tool implementations
|
|
18
18
|
- **Distributed Swarms** - `DistributedSwarmWorker` executes agent turns for `@cogitator-ai/swarms`
|
|
19
|
-
- **Auto-Retry** - Exponential backoff for failed jobs
|
|
19
|
+
- **Auto-Retry** - Exponential backoff for failed jobs, a retried workflow job goes on from its last checkpoint
|
|
20
|
+
- **Job Ids** - Adding a job under an id the queue holds gives that job back, so producers can retry an add
|
|
21
|
+
- **Usage** - Workflow and swarm results carry tokens and cost, the metrics count them by job type
|
|
20
22
|
- **Priority Queue** - Process important jobs first
|
|
21
23
|
- **Delayed Jobs** - Schedule jobs for later execution
|
|
22
24
|
- **Prometheus Metrics** - Built-in HPA support
|
|
@@ -129,6 +131,8 @@ interface QueueConfig {
|
|
|
129
131
|
}
|
|
130
132
|
```
|
|
131
133
|
|
|
134
|
+
`redis` is the `RedisConnectionConfig` of `@cogitator-ai/types`, the same config `DistributedSwarmWorker` and distributed swarms take. `resolveRedisOptions(redis)` gives the settings it resolves to for one server.
|
|
135
|
+
|
|
132
136
|
### Adding Jobs
|
|
133
137
|
|
|
134
138
|
**Agent Jobs:**
|
|
@@ -188,7 +192,7 @@ The worker routes `model` exactly like the same agent in-process: a prefix that
|
|
|
188
192
|
|
|
189
193
|
**Workflow Jobs:**
|
|
190
194
|
|
|
191
|
-
Workflow jobs run
|
|
195
|
+
Workflow jobs run on the `WorkflowExecutor` of `@cogitator-ai/workflows`, over a shared state object initialised from the job input. Nodes run in rounds, each running at once every node whose nodes before it all settled, and a graph with many roots starts at all of them.
|
|
192
196
|
|
|
193
197
|
```typescript
|
|
194
198
|
const workflowConfig: SerializedWorkflow = {
|
|
@@ -233,7 +237,9 @@ await queue.addWorkflowJob(workflowConfig, { ticket: 'App crashes on login' });
|
|
|
233
237
|
| `condition` | `{ key, operator, value? }` — `equals`, `not-equals`, `contains`, `exists`, `gt`, `lt`; outgoing edges use `condition: 'true' \| 'false'` |
|
|
234
238
|
| `parallel` | `{}` — fan-out marker; successors run concurrently |
|
|
235
239
|
|
|
236
|
-
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).
|
|
240
|
+
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). The run keeps its own bookkeeping (which nodes ran, the branch a condition took, the usage of each agent node) in state keys starting with `__cogitator:`, which are left out of the job's `output`. Input keys, node ids and output keys starting with `__cogitator:` are refused.
|
|
241
|
+
|
|
242
|
+
A workflow job keeps one checkpoint, saved over after every node under the job's id and `runId`, so jobs sharing a `runId` never resume from each other. A job BullMQ retries (`attempts`) goes on from it instead of running its finished agent nodes again. The checkpoint is deleted once the run finished, and once no attempt follows a failure: after the last attempt, a cancelled job or a paused agent run. A job a shutdown stopped keeps it for the worker that takes the job up. `WorkerPool` keeps the checkpoints in its Redis under `<queue prefix>:<queue name>:workflow-checkpoints` (`cogitator`, or `{cogitator}` in cluster mode) for `checkpointTtl` seconds after the last save (7 days by default), so the checkpoint of a run whose worker crashed goes on its own. `checkpoints` in the worker config takes a `CheckpointStore` of your own.
|
|
237
243
|
|
|
238
244
|
**Swarm Jobs:**
|
|
239
245
|
|
|
@@ -260,6 +266,18 @@ await queue.addSwarmJob(swarmConfig, 'Write an article about AI', {
|
|
|
260
266
|
| `debate` | `debate` | `maxRounds` rounds, `coordinator` moderates |
|
|
261
267
|
| `voting` | `consensus` | `consensusThreshold`, `maxRounds`, `coordinator` breaks ties |
|
|
262
268
|
|
|
269
|
+
**Job Ids:**
|
|
270
|
+
|
|
271
|
+
`addAgentJob`, `resumeAgentJob`, `addWorkflowJob`, `addSwarmJob` and `addSwarmAgentJob` take a `jobId`, and the job is added under it. Adding a job whose id the queue holds already gives that job back instead of adding a second one, so a producer can retry an add, such as after a timeout, without running the work twice:
|
|
272
|
+
|
|
273
|
+
```typescript
|
|
274
|
+
const job = await queue.addWorkflowJob(workflowConfig, input, { jobId: `triage-${ticketId}` });
|
|
275
|
+
const again = await queue.addWorkflowJob(workflowConfig, input, { jobId: `triage-${ticketId}` });
|
|
276
|
+
// again.id === job.id, one job runs
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
Two adds racing with the same id both get the job the queue holds. A job the queue removed (past `removeOnComplete` or `removeOnFail`) is gone, and its id adds a new job. BullMQ builds Redis keys from the id, so an empty id, one with `:`, an integer and a name of the queue's own keys (`meta`, `id`, `events`, `stalled-check` and the like) are refused, and an id the queue holds as another kind of job (an agent job for `addWorkflowJob`, say) throws. Without `jobId` every add gets a new id.
|
|
280
|
+
|
|
263
281
|
### Queue Methods
|
|
264
282
|
|
|
265
283
|
```typescript
|
|
@@ -335,6 +353,8 @@ interface WorkerConfig extends QueueConfig {
|
|
|
335
353
|
stalledInterval?: number; // Default: 30000ms
|
|
336
354
|
cogitator?: Cogitator; // Default: new Cogitator()
|
|
337
355
|
tools?: Tool[]; // Tool implementations, resolved by name
|
|
356
|
+
checkpoints?: CheckpointStore; // Where workflow jobs keep their checkpoints. Default: the pool's Redis
|
|
357
|
+
checkpointTtl?: number; // Seconds the default store keeps a checkpoint after its last save. Default: 7 days
|
|
338
358
|
}
|
|
339
359
|
```
|
|
340
360
|
|
|
@@ -391,7 +411,7 @@ Built-in processors handle each job type.
|
|
|
391
411
|
|
|
392
412
|
### Using Processors Directly
|
|
393
413
|
|
|
394
|
-
Processors take the job payload and an optional runtime (`{ cogitator, tools }`):
|
|
414
|
+
Processors take the job payload and an optional runtime (`{ cogitator, tools, checkpoints }`):
|
|
395
415
|
|
|
396
416
|
```typescript
|
|
397
417
|
import { processAgentJob, processWorkflowJob, processSwarmJob } from '@cogitator-ai/worker';
|
|
@@ -414,7 +434,9 @@ const swarmResult = await processSwarmJob(
|
|
|
414
434
|
);
|
|
415
435
|
```
|
|
416
436
|
|
|
417
|
-
`
|
|
437
|
+
`processWorkflowJob` called on its own runs without checkpoints unless the runtime has `checkpoints` (a `CheckpointStore`). With them, pass `{ willRetry }` as the third argument: a run whose failure another attempt follows (`willRetry(error)` returns `true`) keeps its checkpoint for it, any other failure deletes it.
|
|
438
|
+
|
|
439
|
+
`processSwarmAgentJob(payload, { publisher, isFinalAttempt, ...runtime })` executes one distributed swarm turn and hands the result (tagged with the job id) to the coordinator with `deliverSwarmJobResult` of `@cogitator-ai/swarms`: when the job names `stateKeys.result` and the publisher has `set`, the result is kept there for `payload.resultTtl` ms (one hour by default), then it is published to `payload.stateKeys.results`. `publisher` is a `SwarmResultPublisher`, now an alias of `SwarmResultClient` (`publish`, optional `set`). `executeSwarmAgentJob` returns the result without publishing.
|
|
418
440
|
|
|
419
441
|
---
|
|
420
442
|
|
|
@@ -428,7 +450,7 @@ import { DistributedSwarmWorker } from '@cogitator-ai/worker';
|
|
|
428
450
|
|
|
429
451
|
const worker = new DistributedSwarmWorker(
|
|
430
452
|
{
|
|
431
|
-
redis: {
|
|
453
|
+
redis: { url: 'redis://localhost:6379' }, // the same Redis as the swarm's distributed.redis
|
|
432
454
|
keyPrefix: 'swarm', // must match the swarm's distributed.redis.keyPrefix
|
|
433
455
|
queue: 'swarm-agent-jobs', // must match distributed.queue
|
|
434
456
|
concurrency: 4,
|
|
@@ -439,6 +461,7 @@ const worker = new DistributedSwarmWorker(
|
|
|
439
461
|
onJobCompleted: (job) => console.log('done', job.agentName),
|
|
440
462
|
onJobFailed: (job, error) => console.error(job.agentName, error.message),
|
|
441
463
|
onJobCancelled: (job) => console.log('the swarm gave up on', job.jobId),
|
|
464
|
+
onJobsRecovered: (workerId, count) => console.warn(`${count} turns of ${workerId} requeued`),
|
|
442
465
|
onError: (error) => console.error(error),
|
|
443
466
|
}
|
|
444
467
|
);
|
|
@@ -449,6 +472,18 @@ process.on('SIGTERM', () => void worker.stop()); // waits for in-flight turns
|
|
|
449
472
|
|
|
450
473
|
Failed turns are reported back to the swarm as errors, so the swarm's own `errorHandling` (retry, failover, skip) applies. A turn the swarm gave up on (its run aborted or timed out, the swarm closed, another attempt answered) is skipped, or aborted while it runs (checked every `cancelCheckInterval` ms, default 1000), and reported to `onJobCancelled` instead of being published. Each turn runs on the model its agent would use in-process, routed by the worker's `cogitator`, so give the worker the same `llm.backends`, plugins and provider keys as the process that runs the swarm.
|
|
451
474
|
|
|
475
|
+
`redis` takes a `url` (`rediss://` for TLS), `host`, `port`, `username`, `password`, `db`, `tls` or `cluster.nodes`, like the queue. The turns sit in the list `<keyPrefix>:jobs:{<queue>}`, whose braces are a Redis Cluster hash tag that keeps the queue and its worker keys in one slot. Earlier versions used `<keyPrefix>:jobs:<queue>`, so upgrade the processes that run swarms and the workers together.
|
|
476
|
+
|
|
477
|
+
The queue is reliable:
|
|
478
|
+
|
|
479
|
+
- A worker moves each turn with `BLMOVE` (Redis 6.2 or newer) from the queue to its own list `<queue key>:processing:<workerId>`, and takes it off once the turn is done.
|
|
480
|
+
- Every `heartbeatInterval` ms (default 5000) it sets its heartbeat key `<queue key>:heartbeat:<workerId>`, which expires after three intervals, and stays in the set `<queue key>:workers`.
|
|
481
|
+
- On `start()` and on every beat, a worker moves the turns of workers whose heartbeat is gone back to the head of the queue, drops those workers from the set and reports `onJobsRecovered(workerId, count)`.
|
|
482
|
+
- A worker also checks its own list on every beat: a turn on it that the worker does not run at two beats in a row, such as one a `BLMOVE` moved while a dropped connection lost its reply, goes back to the head of the queue and is reported with the worker's own id.
|
|
483
|
+
- A worker silent for three intervals counts as gone, so a turn on a stalled worker can run twice.
|
|
484
|
+
- The result is kept under the job's `stateKeys.result` for `resultTtl` ms before it is published, so the coordinator finds it after a missed message, and a worker skips a job whose result is there (such as one a dead worker answered before it took the turn off its list). A result the worker cannot keep under its key is still published.
|
|
485
|
+
- `stop()` waits for in-flight turns, hands back what is left on its list, leaves the set and deletes its heartbeat. `getWorkerId()` gives the worker's id, new on every `start()`.
|
|
486
|
+
|
|
452
487
|
---
|
|
453
488
|
|
|
454
489
|
## Job Results
|
|
@@ -508,8 +543,17 @@ interface WorkflowJobResult {
|
|
|
508
543
|
type: 'workflow';
|
|
509
544
|
output: Record<string, unknown>;
|
|
510
545
|
nodeResults: Record<string, unknown>;
|
|
546
|
+
usage: JobUsage; // what the agent nodes used and cost
|
|
511
547
|
duration: number;
|
|
512
548
|
}
|
|
549
|
+
|
|
550
|
+
interface JobUsage {
|
|
551
|
+
inputTokens?: number; // absent where the runs reported only their totals
|
|
552
|
+
outputTokens?: number;
|
|
553
|
+
totalTokens: number;
|
|
554
|
+
cost: number; // USD
|
|
555
|
+
runs: number; // agent runs counted
|
|
556
|
+
}
|
|
513
557
|
```
|
|
514
558
|
|
|
515
559
|
### Swarm Job Result
|
|
@@ -519,6 +563,8 @@ interface SwarmJobResult {
|
|
|
519
563
|
type: 'swarm';
|
|
520
564
|
output: string;
|
|
521
565
|
rounds: number;
|
|
566
|
+
// what the swarm's agent runs used and cost, in all and by agent
|
|
567
|
+
usage: JobUsage & { byAgent: Record<string, { tokens: number; cost: number; runs: number }> };
|
|
522
568
|
agentOutputs: {
|
|
523
569
|
agent: string;
|
|
524
570
|
output: string;
|
|
@@ -526,6 +572,8 @@ interface SwarmJobResult {
|
|
|
526
572
|
}
|
|
527
573
|
```
|
|
528
574
|
|
|
575
|
+
A swarm reports only total tokens, so its `usage` has no `inputTokens` or `outputTokens`.
|
|
576
|
+
|
|
529
577
|
---
|
|
530
578
|
|
|
531
579
|
## Prometheus Metrics
|
|
@@ -554,20 +602,24 @@ app.listen(9090);
|
|
|
554
602
|
|
|
555
603
|
### Available Metrics
|
|
556
604
|
|
|
557
|
-
| Metric
|
|
558
|
-
|
|
|
559
|
-
| `cogitator_queue_depth`
|
|
560
|
-
| `cogitator_queue_waiting`
|
|
561
|
-
| `cogitator_queue_active`
|
|
562
|
-
| `cogitator_queue_completed`
|
|
563
|
-
| `cogitator_queue_failed`
|
|
564
|
-
| `cogitator_queue_delayed`
|
|
565
|
-
| `cogitator_workers_total`
|
|
566
|
-
| `cogitator_job_duration_seconds`
|
|
567
|
-
| `cogitator_jobs_by_type_total`
|
|
568
|
-
| `cogitator_jobs_failed_total`
|
|
569
|
-
|
|
570
|
-
|
|
605
|
+
| Metric | Type | Description |
|
|
606
|
+
| ----------------------------------- | --------- | ----------------------------------------------------------- |
|
|
607
|
+
| `cogitator_queue_depth` | gauge | Total waiting + delayed jobs |
|
|
608
|
+
| `cogitator_queue_waiting` | gauge | Jobs ready to run, prioritized jobs included |
|
|
609
|
+
| `cogitator_queue_active` | gauge | Jobs currently being processed |
|
|
610
|
+
| `cogitator_queue_completed` | gauge | Completed jobs kept in Redis (capped by `removeOnComplete`) |
|
|
611
|
+
| `cogitator_queue_failed` | gauge | Failed jobs kept in Redis (capped by `removeOnFail`) |
|
|
612
|
+
| `cogitator_queue_delayed` | gauge | Scheduled/delayed jobs |
|
|
613
|
+
| `cogitator_workers_total` | gauge | Workers connected to the queue |
|
|
614
|
+
| `cogitator_job_duration_seconds` | histogram | Job processing time |
|
|
615
|
+
| `cogitator_jobs_by_type_total` | counter | Jobs by type |
|
|
616
|
+
| `cogitator_jobs_failed_total` | counter | Jobs that failed their last attempt, by `type` |
|
|
617
|
+
| `cogitator_job_tokens_total` | counter | Tokens the agent runs of completed jobs used, by `type` |
|
|
618
|
+
| `cogitator_job_input_tokens_total` | counter | Input tokens of completed jobs, by `type` |
|
|
619
|
+
| `cogitator_job_output_tokens_total` | counter | Output tokens of completed jobs, by `type` |
|
|
620
|
+
| `cogitator_job_cost_usd_total` | counter | USD the agent runs of completed jobs cost, by `type` |
|
|
621
|
+
|
|
622
|
+
`cogitator_queue_completed` and `cogitator_queue_failed` can go down as BullMQ trims old jobs, so alert on `cogitator_jobs_failed_total` instead (for example `increase(cogitator_jobs_failed_total[5m]) > 5`). The per-type counters appear after the first job of that kind. The usage counters take the `usage` of completed agent, workflow, swarm and swarm-agent jobs, input and output tokens only where the job reported them apart (swarm jobs do not).
|
|
571
623
|
|
|
572
624
|
### Duration Histogram
|
|
573
625
|
|
|
@@ -587,7 +639,7 @@ histogram.reset();
|
|
|
587
639
|
|
|
588
640
|
### Metrics Collector
|
|
589
641
|
|
|
590
|
-
`WorkerPool` keeps one in `pool.metrics`: it calls `recordJob(type, durationMs)` for each completed job and `recordFailure(type)` for each job that failed its last attempt (retried attempts are not counted). Use your own collector when you process jobs outside the pool.
|
|
642
|
+
`WorkerPool` keeps one in `pool.metrics`: it calls `recordJob(type, durationMs)` and `recordUsage(type, result.usage)` for each completed job and `recordFailure(type)` for each job that failed its last attempt (retried attempts are not counted). Use your own collector when you process jobs outside the pool.
|
|
591
643
|
|
|
592
644
|
```typescript
|
|
593
645
|
import { MetricsCollector } from '@cogitator-ai/worker';
|
|
@@ -596,6 +648,7 @@ const collector = new MetricsCollector();
|
|
|
596
648
|
|
|
597
649
|
collector.recordJob('agent', 1500);
|
|
598
650
|
collector.recordJob('workflow', 3200);
|
|
651
|
+
collector.recordUsage('workflow', workflowResult.usage); // { inputTokens?, outputTokens?, totalTokens, cost }
|
|
599
652
|
collector.recordFailure('agent');
|
|
600
653
|
|
|
601
654
|
const output = collector.format(queueMetrics, { queue: 'main' });
|
|
@@ -885,6 +938,7 @@ import type {
|
|
|
885
938
|
AgentJobResult,
|
|
886
939
|
WorkflowJobResult,
|
|
887
940
|
SwarmJobResult,
|
|
941
|
+
JobUsage,
|
|
888
942
|
SwarmAgentJobResult,
|
|
889
943
|
|
|
890
944
|
// Configuration
|
package/dist/connection.d.ts
CHANGED
|
@@ -1,23 +1,8 @@
|
|
|
1
1
|
import { Cluster, Redis } from 'ioredis';
|
|
2
2
|
import type { ConnectionOptions } from 'bullmq';
|
|
3
3
|
import type { QueueConfig } from './types.js';
|
|
4
|
+
export { resolveRedisOptions, type RedisConnectionOptions } from '@cogitator-ai/swarms';
|
|
4
5
|
export declare const DEFAULT_QUEUE_NAME = "cogitator-jobs";
|
|
5
|
-
/** A single Redis server's connection settings, the url read and the explicit fields applied. */
|
|
6
|
-
export interface RedisConnectionOptions {
|
|
7
|
-
host: string;
|
|
8
|
-
port: number;
|
|
9
|
-
username?: string;
|
|
10
|
-
password?: string;
|
|
11
|
-
db?: number;
|
|
12
|
-
tls?: Record<string, never>;
|
|
13
|
-
}
|
|
14
|
-
/**
|
|
15
|
-
* Connection settings from the queue's redis config: `url` first (`redis://` or `rediss://` for
|
|
16
|
-
* TLS, `user:password@`, a `/db` path), then the explicit fields, which win.
|
|
17
|
-
*
|
|
18
|
-
* @throws Error for a url that is not redis:// or rediss://, or a db path that is not a number
|
|
19
|
-
*/
|
|
20
|
-
export declare function resolveRedisOptions(redis: RedisConnectionConfig): RedisConnectionOptions;
|
|
21
6
|
export type RedisClient = Redis | Cluster;
|
|
22
7
|
type RedisConnectionConfig = QueueConfig['redis'];
|
|
23
8
|
/**
|
|
@@ -41,5 +26,4 @@ export declare function createBullConnection(redis: RedisConnectionConfig, optio
|
|
|
41
26
|
* Plain client (e.g. for publishing swarm results) using the same Redis configuration.
|
|
42
27
|
*/
|
|
43
28
|
export declare function createRedisClient(redis: RedisConnectionConfig): RedisClient;
|
|
44
|
-
export {};
|
|
45
29
|
//# sourceMappingURL=connection.d.ts.map
|
package/dist/connection.d.ts.map
CHANGED
|
@@ -1 +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;
|
|
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;AAEhD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAE3C,OAAO,EAAE,mBAAmB,EAAE,KAAK,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAExF,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,CA4BhB;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,qBAAqB,GAAG,WAAW,CAS3E"}
|
package/dist/connection.js
CHANGED
|
@@ -1,58 +1,13 @@
|
|
|
1
1
|
import { Cluster, Redis } from 'ioredis';
|
|
2
|
+
import { clusterNodeOptions, resolveRedisOptions } from '@cogitator-ai/swarms';
|
|
3
|
+
export { resolveRedisOptions } from '@cogitator-ai/swarms';
|
|
2
4
|
export const DEFAULT_QUEUE_NAME = 'cogitator-jobs';
|
|
3
|
-
/**
|
|
4
|
-
* Connection settings from the queue's redis config: `url` first (`redis://` or `rediss://` for
|
|
5
|
-
* TLS, `user:password@`, a `/db` path), then the explicit fields, which win.
|
|
6
|
-
*
|
|
7
|
-
* @throws Error for a url that is not redis:// or rediss://, or a db path that is not a number
|
|
8
|
-
*/
|
|
9
|
-
export function resolveRedisOptions(redis) {
|
|
10
|
-
let fromUrl = {};
|
|
11
|
-
if (redis.url) {
|
|
12
|
-
const url = new URL(redis.url);
|
|
13
|
-
if (url.protocol !== 'redis:' && url.protocol !== 'rediss:') {
|
|
14
|
-
throw new Error(`Redis url must start with redis:// or rediss://, got ${url.protocol}//`);
|
|
15
|
-
}
|
|
16
|
-
const dbPath = url.pathname.replace(/^\//, '');
|
|
17
|
-
if (dbPath !== '' && !/^\d+$/.test(dbPath)) {
|
|
18
|
-
throw new Error(`Redis url database must be a number, got "${dbPath}"`);
|
|
19
|
-
}
|
|
20
|
-
fromUrl = {
|
|
21
|
-
...(url.hostname && { host: url.hostname }),
|
|
22
|
-
...(url.port && { port: Number(url.port) }),
|
|
23
|
-
...(url.username && { username: decodeURIComponent(url.username) }),
|
|
24
|
-
...(url.password && { password: decodeURIComponent(url.password) }),
|
|
25
|
-
...(dbPath !== '' && { db: Number(dbPath) }),
|
|
26
|
-
tlsFromUrl: url.protocol === 'rediss:',
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
const tls = redis.tls ?? fromUrl.tlsFromUrl ?? false;
|
|
30
|
-
const username = redis.username ?? fromUrl.username;
|
|
31
|
-
const password = redis.password ?? fromUrl.password;
|
|
32
|
-
const db = redis.db ?? fromUrl.db;
|
|
33
|
-
return {
|
|
34
|
-
host: redis.host ?? fromUrl.host ?? 'localhost',
|
|
35
|
-
port: redis.port ?? fromUrl.port ?? 6379,
|
|
36
|
-
...(username !== undefined && { username }),
|
|
37
|
-
...(password !== undefined && { password }),
|
|
38
|
-
...(db !== undefined && { db }),
|
|
39
|
-
...(tls && { tls: {} }),
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
5
|
/**
|
|
43
6
|
* BullMQ key prefix; cluster mode needs a hash tag so all queue keys share a slot.
|
|
44
7
|
*/
|
|
45
8
|
export function queuePrefix(redis) {
|
|
46
9
|
return redis.cluster ? '{cogitator}' : 'cogitator';
|
|
47
10
|
}
|
|
48
|
-
/**
|
|
49
|
-
* What every cluster node connection needs from the config: the credentials and TLS of the url
|
|
50
|
-
* and the explicit fields, without a host, port or database (a cluster has no database).
|
|
51
|
-
*/
|
|
52
|
-
function clusterNodeOptions(redis) {
|
|
53
|
-
const { host: _host, port: _port, db: _db, ...rest } = resolveRedisOptions(redis);
|
|
54
|
-
return rest;
|
|
55
|
-
}
|
|
56
11
|
/**
|
|
57
12
|
* Connection for BullMQ queues and workers. Blocking (worker) connections must not limit
|
|
58
13
|
* retries per request, as BullMQ requires. Cluster instances are shared with BullMQ, which
|
package/dist/connection.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"connection.js","sourceRoot":"","sources":["../src/connection.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"connection.js","sourceRoot":"","sources":["../src/connection.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAEzC,OAAO,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAG/E,OAAO,EAAE,mBAAmB,EAA+B,MAAM,sBAAsB,CAAC;AAExF,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,GAAG,kBAAkB,CAAC,KAAK,CAAC,EAAE,oBAAoB,EAAE;SACrE,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,EAAE,GAAG,mBAAmB,CAAC,KAAK,CAAC,EAAE,oBAAoB,EAAE;QACnE,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,kBAAkB,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IACvF,CAAC;IAED,OAAO,IAAI,KAAK,CAAC,mBAAmB,CAAC,KAAK,CAAC,CAAC,CAAC;AAC/C,CAAC"}
|
|
@@ -2,18 +2,18 @@
|
|
|
2
2
|
* Worker node for distributed swarms.
|
|
3
3
|
*
|
|
4
4
|
* Consumes agent turns dispatched by `DistributedSwarmCoordinator` (from
|
|
5
|
-
* `@cogitator-ai/swarms`), runs them with the worker's Cogitator and tools, and
|
|
6
|
-
*
|
|
5
|
+
* `@cogitator-ai/swarms`), runs them with the worker's Cogitator and tools, and hands each
|
|
6
|
+
* result back to the coordinator. A turn moves from the queue to this worker's own list while
|
|
7
|
+
* it runs, so a turn of a worker that died goes back to the queue for another one.
|
|
7
8
|
*/
|
|
9
|
+
import type { RedisConnectionConfig } from '@cogitator-ai/types';
|
|
8
10
|
import type { SwarmAgentJobPayload, SwarmAgentJobResult, WorkerRuntime } from './types.js';
|
|
9
11
|
export interface DistributedSwarmWorkerConfig extends WorkerRuntime {
|
|
10
|
-
/**
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
db?: number;
|
|
16
|
-
};
|
|
12
|
+
/**
|
|
13
|
+
* Redis holding the swarm job queue (must match the swarm's `distributed.redis`): a url, host
|
|
14
|
+
* and port, TLS, or a cluster. Default: localhost:6379
|
|
15
|
+
*/
|
|
16
|
+
redis?: RedisConnectionConfig;
|
|
17
17
|
/** Swarm key prefix (must match `distributed.redis.keyPrefix`, default: 'swarm') */
|
|
18
18
|
keyPrefix?: string;
|
|
19
19
|
/** Job queue name (must match `distributed.queue`, default: 'swarm-agent-jobs') */
|
|
@@ -28,39 +28,98 @@ export interface DistributedSwarmWorkerConfig extends WorkerRuntime {
|
|
|
28
28
|
* cancelled turn is aborted and its result is not published
|
|
29
29
|
*/
|
|
30
30
|
cancelCheckInterval?: number;
|
|
31
|
+
/**
|
|
32
|
+
* How often, in ms, the worker tells the queue it is alive and returns the turns of workers
|
|
33
|
+
* that stopped doing so to the queue (default: 5000). A worker silent for three intervals
|
|
34
|
+
* counts as gone, so a turn that runs past that on a stalled worker can run twice
|
|
35
|
+
*/
|
|
36
|
+
heartbeatInterval?: number;
|
|
31
37
|
}
|
|
32
38
|
export interface DistributedSwarmWorkerEvents {
|
|
33
39
|
onJobStarted?: (job: SwarmAgentJobPayload) => void;
|
|
34
40
|
onJobCompleted?: (job: SwarmAgentJobPayload, result: SwarmAgentJobResult) => void;
|
|
35
41
|
onJobFailed?: (job: SwarmAgentJobPayload, error: Error) => void;
|
|
36
|
-
/**
|
|
42
|
+
/**
|
|
43
|
+
* The coordinator gave up on the turn or already has its result: it was skipped, or aborted
|
|
44
|
+
* while it ran
|
|
45
|
+
*/
|
|
37
46
|
onJobCancelled?: (job: SwarmAgentJobPayload) => void;
|
|
47
|
+
/**
|
|
48
|
+
* Turns went back to the queue: those of a worker that stopped beating, or ones on this
|
|
49
|
+
* worker's own list it never got (`workerId` is then its own)
|
|
50
|
+
*/
|
|
51
|
+
onJobsRecovered?: (workerId: string, count: number) => void;
|
|
38
52
|
onError?: (error: Error) => void;
|
|
39
53
|
}
|
|
40
54
|
export declare class DistributedSwarmWorker {
|
|
41
55
|
private readonly config;
|
|
42
56
|
private readonly events;
|
|
43
57
|
private readonly runtime;
|
|
44
|
-
private readonly
|
|
58
|
+
private readonly keys;
|
|
59
|
+
/** This worker's id in the queue's worker set, new for every start */
|
|
60
|
+
private workerId;
|
|
45
61
|
private consumer?;
|
|
46
62
|
private publisher?;
|
|
47
63
|
private loop?;
|
|
64
|
+
/** The next beat, scheduled once the one before it finished */
|
|
65
|
+
private beat?;
|
|
66
|
+
/** The beat running now, which `stop()` waits for */
|
|
67
|
+
private beating?;
|
|
68
|
+
/** Whether the worker beats: from `start()` until `stop()` saw its last turn finish */
|
|
69
|
+
private alive;
|
|
48
70
|
private running;
|
|
49
71
|
private readonly active;
|
|
72
|
+
/** The turns this worker took and runs, as moved: one entry per copy */
|
|
73
|
+
private readonly holding;
|
|
74
|
+
/** Turns on this worker's list it did not run at the last beat, with how many copies */
|
|
75
|
+
private strays;
|
|
50
76
|
constructor(config?: DistributedSwarmWorkerConfig, events?: DistributedSwarmWorkerEvents);
|
|
51
77
|
/**
|
|
52
|
-
* Start consuming jobs. Resolves once the worker is connected
|
|
78
|
+
* Start consuming jobs. Resolves once the worker is connected, has joined the queue's workers
|
|
79
|
+
* and has returned the turns of workers that are gone to the queue.
|
|
53
80
|
*/
|
|
54
81
|
start(): Promise<void>;
|
|
55
82
|
/**
|
|
56
|
-
* Stop taking new jobs, wait for in-flight jobs to finish
|
|
83
|
+
* Stop taking new jobs, wait for in-flight jobs to finish, hand back to the queue whatever is
|
|
84
|
+
* still on this worker's list, leave the queue's workers and close connections.
|
|
57
85
|
*/
|
|
58
86
|
stop(): Promise<void>;
|
|
87
|
+
/** This worker's id in the queue's worker set while it runs */
|
|
88
|
+
getWorkerId(): string;
|
|
59
89
|
isRunning(): boolean;
|
|
60
90
|
getActiveJobCount(): number;
|
|
61
91
|
private consume;
|
|
92
|
+
/**
|
|
93
|
+
* Beat once `heartbeatInterval` ms from now, and again that long after it finished: one beat at
|
|
94
|
+
* a time, so a slow Redis never runs two `reclaim()` at once or shortens the grace between two
|
|
95
|
+
*/
|
|
96
|
+
private scheduleBeat;
|
|
97
|
+
private heartbeatInterval;
|
|
98
|
+
/** Tell the queue this worker is alive: its heartbeat first, so no one takes it for gone */
|
|
99
|
+
private heartbeat;
|
|
100
|
+
/**
|
|
101
|
+
* Hand back to the queue the turns on this worker's own list it does not run, at the second
|
|
102
|
+
* beat in a row that finds them there: a BLMOVE whose reply a dropped connection lost (and
|
|
103
|
+
* ioredis sent again) moved a turn this worker never got. One beat of grace lets a reply that
|
|
104
|
+
* is still on its way arrive first
|
|
105
|
+
*/
|
|
106
|
+
private reclaim;
|
|
107
|
+
/** Return the turns of every worker whose heartbeat is gone to the queue */
|
|
108
|
+
private recover;
|
|
109
|
+
/**
|
|
110
|
+
* Move the turns on a worker's list back to the head of the queue, one at a time so two
|
|
111
|
+
* workers recovering it at once never move a turn twice
|
|
112
|
+
*
|
|
113
|
+
* @returns how many turns this call moved
|
|
114
|
+
*/
|
|
115
|
+
private requeue;
|
|
116
|
+
private reportError;
|
|
62
117
|
private handle;
|
|
63
|
-
/**
|
|
118
|
+
/**
|
|
119
|
+
* Whether the coordinator gave up on the job (only coordinators that name a cancel set), or a
|
|
120
|
+
* worker answered it already, such as one that died after answering and before taking the job
|
|
121
|
+
* off its list
|
|
122
|
+
*/
|
|
64
123
|
private isCancelled;
|
|
65
124
|
/** Abort `controller` once the coordinator cancels the job while it runs. */
|
|
66
125
|
private watchCancellation;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"distributed-swarm-worker.d.ts","sourceRoot":"","sources":["../src/distributed-swarm-worker.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"distributed-swarm-worker.d.ts","sourceRoot":"","sources":["../src/distributed-swarm-worker.ts"],"names":[],"mappings":"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"}
|