zouroboros-swarm 5.0.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 (81) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +187 -0
  3. package/dist/api/server.d.ts +28 -0
  4. package/dist/api/server.js +223 -0
  5. package/dist/budget/governor.d.ts +48 -0
  6. package/dist/budget/governor.js +114 -0
  7. package/dist/budget/pricing.d.ts +12 -0
  8. package/dist/budget/pricing.js +44 -0
  9. package/dist/cascade/manager.d.ts +62 -0
  10. package/dist/cascade/manager.js +221 -0
  11. package/dist/circuit/breaker.d.ts +24 -0
  12. package/dist/circuit/breaker.js +130 -0
  13. package/dist/cli/index.d.ts +7 -0
  14. package/dist/cli/index.js +241 -0
  15. package/dist/context/sharing.d.ts +63 -0
  16. package/dist/context/sharing.js +169 -0
  17. package/dist/dag/executor.d.ts +74 -0
  18. package/dist/dag/executor.js +354 -0
  19. package/dist/db/schema.d.ts +9 -0
  20. package/dist/db/schema.js +100 -0
  21. package/dist/executor/bridge.d.ts +20 -0
  22. package/dist/executor/bridge.js +140 -0
  23. package/dist/executor/doctor.d.ts +14 -0
  24. package/dist/executor/doctor.js +172 -0
  25. package/dist/executor/gemini-daemon.d.ts +18 -0
  26. package/dist/executor/gemini-daemon.js +206 -0
  27. package/dist/executor/gemini-warmup.d.ts +13 -0
  28. package/dist/executor/gemini-warmup.js +226 -0
  29. package/dist/executor/register.d.ts +17 -0
  30. package/dist/executor/register.js +142 -0
  31. package/dist/executor/test-harness.d.ts +12 -0
  32. package/dist/executor/test-harness.js +116 -0
  33. package/dist/executor/types/executor.d.ts +95 -0
  34. package/dist/executor/types/executor.js +7 -0
  35. package/dist/heartbeat/scheduler.d.ts +39 -0
  36. package/dist/heartbeat/scheduler.js +118 -0
  37. package/dist/hierarchical.d.ts +28 -0
  38. package/dist/hierarchical.js +183 -0
  39. package/dist/index.d.ts +32 -0
  40. package/dist/index.js +49 -0
  41. package/dist/orchestrator.d.ts +48 -0
  42. package/dist/orchestrator.js +273 -0
  43. package/dist/rag/enrichment.d.ts +27 -0
  44. package/dist/rag/enrichment.js +154 -0
  45. package/dist/rag/index.d.ts +1 -0
  46. package/dist/rag/index.js +1 -0
  47. package/dist/registry/loader.d.ts +14 -0
  48. package/dist/registry/loader.js +38 -0
  49. package/dist/roles/persona-seeder.d.ts +30 -0
  50. package/dist/roles/persona-seeder.js +76 -0
  51. package/dist/roles/registry.d.ts +36 -0
  52. package/dist/roles/registry.js +92 -0
  53. package/dist/routing/engine.d.ts +30 -0
  54. package/dist/routing/engine.js +170 -0
  55. package/dist/selector/executor-selector.d.ts +25 -0
  56. package/dist/selector/executor-selector.js +144 -0
  57. package/dist/stagnation/detector.d.ts +52 -0
  58. package/dist/stagnation/detector.js +150 -0
  59. package/dist/streaming/capture.d.ts +51 -0
  60. package/dist/streaming/capture.js +140 -0
  61. package/dist/tokens/optimizer.d.ts +61 -0
  62. package/dist/tokens/optimizer.js +148 -0
  63. package/dist/transport/acp-transport.d.ts +42 -0
  64. package/dist/transport/acp-transport.js +252 -0
  65. package/dist/transport/bridge-transport.d.ts +22 -0
  66. package/dist/transport/bridge-transport.js +48 -0
  67. package/dist/transport/factory.d.ts +10 -0
  68. package/dist/transport/factory.js +33 -0
  69. package/dist/transport/types.d.ts +49 -0
  70. package/dist/transport/types.js +7 -0
  71. package/dist/types.d.ts +195 -0
  72. package/dist/types.js +6 -0
  73. package/dist/verification/capabilities.d.ts +60 -0
  74. package/dist/verification/capabilities.js +279 -0
  75. package/dist/verification/gap-audit.d.ts +43 -0
  76. package/dist/verification/gap-audit.js +292 -0
  77. package/dist/verification/index.d.ts +14 -0
  78. package/dist/verification/index.js +11 -0
  79. package/dist/verification/verify-wiring.d.ts +45 -0
  80. package/dist/verification/verify-wiring.js +290 -0
  81. package/package.json +63 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 marlandoj
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,187 @@
1
+ # zouroboros-swarm
2
+
3
+ > Multi-agent orchestration with circuit breakers and 6-signal routing
4
+
5
+ ## Features
6
+
7
+ - **Circuit Breaker V2** — CLOSED/OPEN/HALF_OPEN states with category-aware failure tracking
8
+ - **6-Signal Composite Routing** — Capability, health, complexity fit, history, procedure, temporal
9
+ - **Hierarchical Orchestration** — Hermes/Claude parent tasks can self-decompose under centralized delegation policy
10
+ - **Executor Bridges** — Claude Code, Hermes, Gemini, Codex CLI integration
11
+ - **DAG Execution** — Streaming and wave-based task execution modes
12
+ - **Registry-Based** — JSON registry for executor configuration
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ npm install zouroboros-swarm
18
+ # or
19
+ pnpm add zouroboros-swarm
20
+ ```
21
+
22
+ ### ACP Adapter Prerequisites
23
+
24
+ The swarm uses the [Agent Client Protocol (ACP)](https://github.com/agentclientprotocol) to communicate with Claude Code, Codex, and Gemini executors. Install the required adapter binaries before running:
25
+
26
+ ```bash
27
+ # Install all ACP adapters
28
+ bash packages/swarm/scripts/install-acp-adapters.sh
29
+
30
+ # Verify installation
31
+ bash packages/swarm/scripts/install-acp-adapters.sh --check
32
+
33
+ # Update to latest versions
34
+ bash packages/swarm/scripts/install-acp-adapters.sh --update
35
+ ```
36
+
37
+ | Executor | Adapter | npm Package |
38
+ |---|---|---|
39
+ | Claude Code | `claude-agent-acp` | `@zed-industries/claude-agent-acp` |
40
+ | Codex | `codex-acp` | `@zed-industries/codex-acp` |
41
+ | Gemini | `gemini --acp` | `@google/gemini-cli` |
42
+ | Hermes | bridge (no ACP adapter) | — |
43
+
44
+ ## Quick Start
45
+
46
+ ```typescript
47
+ import { SwarmOrchestrator } from 'zouroboros-swarm';
48
+
49
+ const orchestrator = new SwarmOrchestrator({
50
+ localConcurrency: 8,
51
+ timeoutSeconds: 600,
52
+ routingStrategy: 'balanced',
53
+ dagMode: 'streaming',
54
+ });
55
+
56
+ const tasks = [
57
+ { id: '1', persona: 'developer', task: 'Fix the auth bug in login.ts', priority: 'high' },
58
+ { id: '2', persona: 'reviewer', task: 'Review the PR for error handling', priority: 'medium', dependsOn: ['1'] },
59
+ ];
60
+
61
+ const results = await orchestrator.run(tasks);
62
+ ```
63
+
64
+ ## CLI Usage
65
+
66
+ ```bash
67
+ # Run a swarm campaign
68
+ zouroboros-swarm ./tasks.json
69
+
70
+ # With options
71
+ zouroboros-swarm ./tasks.json --mode waves --concurrency 4 --strategy fast
72
+
73
+ # Inspect a completed run
74
+ zouroboros-swarm status <swarm-id>
75
+
76
+ # Inspect executor routing and delegation history
77
+ zouroboros-swarm history 10
78
+
79
+ # Health check
80
+ zouroboros-swarm doctor
81
+ ```
82
+
83
+ `status <swarm-id>` now surfaces persisted hierarchical telemetry from the results file, including delegated parent count, child task count, artifact count, reroutes, and effective executors.
84
+
85
+ `history [limit]` reads `executor-history.db` and prints delegation-aware routing history per executor/category, including:
86
+ - base success rate
87
+ - delegated attempt/success rate
88
+ - child success rate
89
+ - average child count
90
+ - average child duration
91
+
92
+ ## Task Format
93
+
94
+ ```json
95
+ [
96
+ {
97
+ "id": "task-1",
98
+ "persona": "developer",
99
+ "task": "Implement user authentication",
100
+ "priority": "high",
101
+ "executor": "claude-code",
102
+ "dependsOn": [],
103
+ "timeoutSeconds": 600
104
+ },
105
+ {
106
+ "id": "task-2",
107
+ "persona": "tester",
108
+ "task": "Write tests for auth",
109
+ "priority": "medium",
110
+ "dependsOn": ["task-1"]
111
+ }
112
+ ]
113
+ ```
114
+
115
+ Hierarchical delegation is available through an optional `delegation` block on each task:
116
+
117
+ ```json
118
+ {
119
+ "id": "implementation-safe",
120
+ "executor": "claude-code",
121
+ "task": "Implement the parser cleanup and synthesize the result.",
122
+ "delegation": {
123
+ "mode": "auto",
124
+ "maxChildren": 2,
125
+ "writeScopes": [
126
+ { "childId": "parser-a", "paths": ["src/parser/a.ts"] },
127
+ { "childId": "parser-b", "paths": ["src/parser/b.ts"] }
128
+ ]
129
+ }
130
+ }
131
+ ```
132
+
133
+ - `mode: "auto"` enables executor-side self-decomposition when policy allows it.
134
+ - Mutation tasks require disjoint `writeScopes`; otherwise they are forced to remain leaf tasks.
135
+ - Results now persist parent/child telemetry, including `delegated`, `effectiveExecutor`, `childRecords`, and artifact lists.
136
+
137
+ Example status output for a completed hierarchical run:
138
+
139
+ ```text
140
+ 🔍 Swarm Status: hierarchical-broader-validation-test
141
+ Status: complete
142
+ Results: ~/.swarm/results/hierarchical-broader-validation-test.json
143
+ Outcome: 4/4 succeeded, 0 failed
144
+ Duration: 4s
145
+ Delegated: 3 parent / 5 child
146
+ Artifacts: 4
147
+ Reroutes: 1
148
+ Executors: hermes, claude-code
149
+ ```
150
+
151
+ ## Routing Strategies
152
+
153
+ | Strategy | Best For | Weight Focus |
154
+ |----------|----------|--------------|
155
+ | `fast` | Quick iterations | Complexity fit (40%), Health (20%) |
156
+ | `reliable` | Production tasks | Health (35%), History (18%) |
157
+ | `balanced` | General use | Even distribution |
158
+ | `explore` | New domains | Capability (35%), Complexity (18%) |
159
+
160
+ ## Circuit Breaker States
161
+
162
+ - **CLOSED** — Normal operation, requests pass through
163
+ - **OPEN** — Failure threshold exceeded, requests blocked
164
+ - **HALF_OPEN** — Testing if service recovered
165
+
166
+ ## Executor Registry
167
+
168
+ Create `~/.zouroboros/executors.json`:
169
+
170
+ ```json
171
+ {
172
+ "executors": [
173
+ {
174
+ "id": "claude-code",
175
+ "name": "Claude Code",
176
+ "executor": "local",
177
+ "bridge": "bridges/claude-code-bridge.sh",
178
+ "expertise": ["code-generation", "debugging", "refactoring"],
179
+ "bestFor": ["Complex multi-file changes"]
180
+ }
181
+ ]
182
+ }
183
+ ```
184
+
185
+ ## License
186
+
187
+ MIT
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Swarm API Server — embedded Hono server with SSE support.
3
+ *
4
+ * Exposes swarm state as REST endpoints + real-time SSE activity stream.
5
+ * Registered as a Zo user service for persistent hosting.
6
+ */
7
+ import { Hono } from 'hono';
8
+ import { BudgetGovernor } from '../budget/governor.js';
9
+ import { HeartbeatScheduler } from '../heartbeat/scheduler.js';
10
+ import { RoleRegistry } from '../roles/registry.js';
11
+ export interface SwarmAPIConfig {
12
+ port: number;
13
+ authToken?: string;
14
+ dbPath?: string;
15
+ }
16
+ export interface SSEEvent {
17
+ type: string;
18
+ data: Record<string, unknown>;
19
+ timestamp: number;
20
+ }
21
+ export declare function createSwarmAPI(config: SwarmAPIConfig): {
22
+ app: Hono<import("hono/types").BlankEnv, import("hono/types").BlankSchema, "/">;
23
+ budget: BudgetGovernor;
24
+ heartbeat: HeartbeatScheduler;
25
+ roles: RoleRegistry;
26
+ broadcastSSE: (event: SSEEvent) => void;
27
+ };
28
+ export declare function startSwarmServer(config?: Partial<SwarmAPIConfig>): void;
@@ -0,0 +1,223 @@
1
+ /**
2
+ * Swarm API Server — embedded Hono server with SSE support.
3
+ *
4
+ * Exposes swarm state as REST endpoints + real-time SSE activity stream.
5
+ * Registered as a Zo user service for persistent hosting.
6
+ */
7
+ import { Hono } from 'hono';
8
+ import { cors } from 'hono/cors';
9
+ import { BudgetGovernor } from '../budget/governor.js';
10
+ import { HeartbeatScheduler } from '../heartbeat/scheduler.js';
11
+ import { RoleRegistry } from '../roles/registry.js';
12
+ import { getDb } from '../db/schema.js';
13
+ export function createSwarmAPI(config) {
14
+ const app = new Hono();
15
+ const budget = new BudgetGovernor(config.dbPath);
16
+ const heartbeat = new HeartbeatScheduler(config.dbPath);
17
+ const roles = new RoleRegistry(config.dbPath);
18
+ const db = getDb(config.dbPath);
19
+ const sseClients = new Set();
20
+ const eventLog = [];
21
+ app.use('*', cors());
22
+ if (config.authToken) {
23
+ app.use('/api/swarm/*', async (c, next) => {
24
+ const auth = c.req.header('authorization');
25
+ if (!auth?.startsWith('Bearer ') || auth.slice(7) !== config.authToken) {
26
+ return c.json({ error: 'Unauthorized' }, 401);
27
+ }
28
+ await next();
29
+ });
30
+ }
31
+ function broadcastSSE(event) {
32
+ eventLog.push(event);
33
+ if (eventLog.length > 1000)
34
+ eventLog.splice(0, eventLog.length - 500);
35
+ const payload = `data: ${JSON.stringify(event)}\n\n`;
36
+ for (const controller of sseClients) {
37
+ try {
38
+ controller.enqueue(new TextEncoder().encode(payload));
39
+ }
40
+ catch {
41
+ sseClients.delete(controller);
42
+ }
43
+ }
44
+ }
45
+ budget.on((evt) => broadcastSSE({ type: evt.type, data: evt.data, timestamp: evt.timestamp }));
46
+ heartbeat.on((evt) => broadcastSSE({
47
+ type: 'heartbeat',
48
+ data: { swarmId: evt.swarmId, beat: evt.beatNumber, status: evt.status },
49
+ timestamp: evt.timestamp,
50
+ }));
51
+ // --- Status ---
52
+ app.get('/api/swarm/status', (c) => {
53
+ return c.json({
54
+ status: 'running',
55
+ timestamp: Date.now(),
56
+ executors: ['claude-code', 'gemini', 'codex', 'hermes'],
57
+ heartbeats: {
58
+ active: Array.from({ length: 4 }, (_, i) => ['claude-code', 'gemini', 'codex', 'hermes'][i])
59
+ .filter(id => heartbeat.isRunning(id)),
60
+ },
61
+ });
62
+ });
63
+ // --- Tasks ---
64
+ app.get('/api/swarm/tasks', (c) => {
65
+ const executor = c.req.query('executor');
66
+ const status = c.req.query('status');
67
+ // Tasks are managed by the orchestrator runtime; return from event log
68
+ const taskEvents = eventLog.filter(e => e.type.startsWith('task:') &&
69
+ (!executor || e.data.executorId === executor) &&
70
+ (!status || e.data.status === status));
71
+ return c.json({ tasks: taskEvents, count: taskEvents.length });
72
+ });
73
+ app.get('/api/swarm/tasks/:id', (c) => {
74
+ const taskId = c.req.param('id');
75
+ const events = eventLog.filter(e => e.data.taskId === taskId);
76
+ return c.json({ taskId, events });
77
+ });
78
+ // --- Budget ---
79
+ app.get('/api/swarm/budget', (c) => {
80
+ const swarmId = c.req.query('swarmId') ?? 'default';
81
+ const state = budget.getState(swarmId);
82
+ return c.json(state);
83
+ });
84
+ app.post('/api/swarm/budget/init', async (c) => {
85
+ const body = await c.req.json();
86
+ budget.initSwarm({
87
+ swarmId: body.swarmId ?? 'default',
88
+ totalBudgetUSD: body.totalBudgetUSD,
89
+ perExecutorLimits: body.perExecutorLimits,
90
+ alertThresholdPct: body.alertThresholdPct,
91
+ hardCapAction: body.hardCapAction ?? 'downgrade',
92
+ });
93
+ return c.json({ success: true });
94
+ });
95
+ // --- Health ---
96
+ app.get('/api/swarm/health', (c) => {
97
+ const executors = ['claude-code', 'gemini', 'codex', 'hermes'];
98
+ const health = {};
99
+ for (const id of executors) {
100
+ health[id] = {
101
+ state: 'CLOSED',
102
+ failures: 0,
103
+ heartbeatActive: heartbeat.isRunning(id),
104
+ beatCount: heartbeat.getBeatCount(id),
105
+ };
106
+ }
107
+ return c.json({ executors: health, timestamp: Date.now() });
108
+ });
109
+ // --- Roles CRUD ---
110
+ app.get('/api/swarm/roles', (c) => {
111
+ return c.json({ roles: roles.list() });
112
+ });
113
+ app.get('/api/swarm/roles/:id', (c) => {
114
+ const role = roles.get(c.req.param('id'));
115
+ if (!role)
116
+ return c.json({ error: 'Role not found' }, 404);
117
+ return c.json(role);
118
+ });
119
+ app.post('/api/swarm/roles', async (c) => {
120
+ const body = await c.req.json();
121
+ try {
122
+ const role = roles.create(body);
123
+ broadcastSSE({ type: 'role:created', data: { role }, timestamp: Date.now() });
124
+ return c.json(role, 201);
125
+ }
126
+ catch (err) {
127
+ return c.json({ error: err.message }, 400);
128
+ }
129
+ });
130
+ app.put('/api/swarm/roles/:id', async (c) => {
131
+ const body = await c.req.json();
132
+ const updated = roles.update(c.req.param('id'), body);
133
+ if (!updated)
134
+ return c.json({ error: 'Role not found' }, 404);
135
+ broadcastSSE({ type: 'role:updated', data: { role: updated }, timestamp: Date.now() });
136
+ return c.json(updated);
137
+ });
138
+ app.delete('/api/swarm/roles/:id', (c) => {
139
+ const deleted = roles.delete(c.req.param('id'));
140
+ if (!deleted)
141
+ return c.json({ error: 'Role not found' }, 404);
142
+ broadcastSSE({ type: 'role:deleted', data: { roleId: c.req.param('id') }, timestamp: Date.now() });
143
+ return c.json({ success: true });
144
+ });
145
+ // --- SSE Activity Stream ---
146
+ app.get('/api/swarm/activity', (c) => {
147
+ const stream = new ReadableStream({
148
+ start(controller) {
149
+ sseClients.add(controller);
150
+ const recent = eventLog.slice(-20);
151
+ for (const event of recent) {
152
+ controller.enqueue(new TextEncoder().encode(`data: ${JSON.stringify(event)}\n\n`));
153
+ }
154
+ },
155
+ cancel(controller) {
156
+ sseClients.delete(controller);
157
+ },
158
+ });
159
+ return new Response(stream, {
160
+ headers: {
161
+ 'Content-Type': 'text/event-stream',
162
+ 'Cache-Control': 'no-cache',
163
+ 'Connection': 'keep-alive',
164
+ },
165
+ });
166
+ });
167
+ // --- Dispatch ---
168
+ app.post('/api/swarm/dispatch', async (c) => {
169
+ const body = await c.req.json();
170
+ const swarmId = body.swarmId ?? `swarm-${Date.now()}`;
171
+ broadcastSSE({
172
+ type: 'swarm:dispatched',
173
+ data: { swarmId, taskCount: body.tasks?.length ?? 0 },
174
+ timestamp: Date.now(),
175
+ });
176
+ return c.json({ swarmId, status: 'dispatched', message: 'Swarm dispatch queued' });
177
+ });
178
+ // --- Pause / Resume / Abort ---
179
+ app.post('/api/swarm/pause/:taskId', (c) => {
180
+ const taskId = c.req.param('taskId');
181
+ broadcastSSE({ type: 'task:paused', data: { taskId }, timestamp: Date.now() });
182
+ return c.json({ taskId, status: 'paused' });
183
+ });
184
+ app.post('/api/swarm/resume/:taskId', (c) => {
185
+ const taskId = c.req.param('taskId');
186
+ broadcastSSE({ type: 'task:resumed', data: { taskId }, timestamp: Date.now() });
187
+ return c.json({ taskId, status: 'resumed' });
188
+ });
189
+ app.post('/api/swarm/abort/:swarmId', (c) => {
190
+ const swarmId = c.req.param('swarmId');
191
+ heartbeat.stop(swarmId);
192
+ broadcastSSE({ type: 'swarm:aborted', data: { swarmId }, timestamp: Date.now() });
193
+ return c.json({ swarmId, status: 'aborted' });
194
+ });
195
+ // --- Heartbeat control ---
196
+ app.post('/api/swarm/heartbeat/start', async (c) => {
197
+ const body = await c.req.json();
198
+ heartbeat.start({
199
+ swarmId: body.swarmId ?? 'default',
200
+ intervalMs: body.intervalMs ?? 60000,
201
+ maxBeats: body.maxBeats ?? 0,
202
+ onIdle: body.onIdle ?? 'sleep',
203
+ });
204
+ return c.json({ success: true, swarmId: body.swarmId ?? 'default' });
205
+ });
206
+ app.post('/api/swarm/heartbeat/stop', async (c) => {
207
+ const body = await c.req.json();
208
+ heartbeat.stop(body.swarmId ?? 'default');
209
+ return c.json({ success: true });
210
+ });
211
+ return { app, budget, heartbeat, roles, broadcastSSE };
212
+ }
213
+ export function startSwarmServer(config) {
214
+ const port = config?.port ?? parseInt(process.env.PORT ?? '3847', 10);
215
+ const authToken = config?.authToken ?? process.env.SWARM_API_TOKEN;
216
+ const dbPath = config?.dbPath;
217
+ const { app } = createSwarmAPI({ port, authToken, dbPath });
218
+ Bun.serve({ port, fetch: app.fetch });
219
+ console.log(`Swarm API server listening on port ${port}`);
220
+ }
221
+ if (import.meta.main) {
222
+ startSwarmServer();
223
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Budget Governor — per-executor and per-swarm cost tracking with hard caps.
3
+ *
4
+ * Tracks token usage, normalizes to USD, emits alerts, and enforces caps.
5
+ * Hard cap action: "downgrade" switches remaining tasks to cheapest executor.
6
+ */
7
+ export type HardCapAction = 'pause' | 'abort' | 'downgrade';
8
+ export interface BudgetConfig {
9
+ swarmId: string;
10
+ totalBudgetUSD: number;
11
+ perExecutorLimits?: Record<string, number>;
12
+ alertThresholdPct?: number;
13
+ hardCapAction?: HardCapAction;
14
+ }
15
+ export interface BudgetState {
16
+ swarmId: string;
17
+ totalSpentUSD: number;
18
+ totalBudgetUSD: number;
19
+ remaining: number;
20
+ perExecutor: Record<string, number>;
21
+ alertFired: boolean;
22
+ capReached: boolean;
23
+ hardCapAction: HardCapAction;
24
+ }
25
+ export interface BudgetEvent {
26
+ type: 'budget:update' | 'budget:alert' | 'budget:cap' | 'budget:downgrade';
27
+ swarmId: string;
28
+ data: Record<string, unknown>;
29
+ timestamp: number;
30
+ }
31
+ type BudgetListener = (event: BudgetEvent) => void;
32
+ export declare class BudgetGovernor {
33
+ private db;
34
+ private listeners;
35
+ constructor(dbPath?: string);
36
+ on(listener: BudgetListener): void;
37
+ private emit;
38
+ initSwarm(config: BudgetConfig): void;
39
+ recordUsage(swarmId: string, executorId: string, model: string, inputTokens: number, outputTokens: number): BudgetState;
40
+ getState(swarmId: string): BudgetState;
41
+ checkExecutorLimit(swarmId: string, executorId: string): boolean;
42
+ getDowngradeTarget(executorId: string): {
43
+ executorId: string;
44
+ model: string;
45
+ };
46
+ private getConfig;
47
+ }
48
+ export {};
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Budget Governor — per-executor and per-swarm cost tracking with hard caps.
3
+ *
4
+ * Tracks token usage, normalizes to USD, emits alerts, and enforces caps.
5
+ * Hard cap action: "downgrade" switches remaining tasks to cheapest executor.
6
+ */
7
+ import { getDb } from '../db/schema.js';
8
+ import { estimateCostUSD, getCheapestModel } from './pricing.js';
9
+ export class BudgetGovernor {
10
+ db;
11
+ listeners = [];
12
+ constructor(dbPath) {
13
+ this.db = getDb(dbPath);
14
+ }
15
+ on(listener) {
16
+ this.listeners.push(listener);
17
+ }
18
+ emit(event) {
19
+ for (const listener of this.listeners) {
20
+ try {
21
+ listener(event);
22
+ }
23
+ catch { }
24
+ }
25
+ }
26
+ initSwarm(config) {
27
+ this.db.run(`INSERT OR REPLACE INTO budget_config (swarm_id, total_budget_usd, alert_threshold_pct, hard_cap_action)
28
+ VALUES (?, ?, ?, ?)`, [config.swarmId, config.totalBudgetUSD, config.alertThresholdPct ?? 80, config.hardCapAction ?? 'downgrade']);
29
+ if (config.perExecutorLimits) {
30
+ const stmt = this.db.prepare('INSERT OR REPLACE INTO budget_per_executor (swarm_id, executor_id, limit_usd) VALUES (?, ?, ?)');
31
+ for (const [execId, limit] of Object.entries(config.perExecutorLimits)) {
32
+ stmt.run(config.swarmId, execId, limit);
33
+ }
34
+ }
35
+ }
36
+ recordUsage(swarmId, executorId, model, inputTokens, outputTokens) {
37
+ const cost = estimateCostUSD(model, inputTokens, outputTokens);
38
+ const totalTokens = inputTokens + outputTokens;
39
+ this.db.run(`INSERT INTO swarm_budget (swarm_id, executor_id, tokens_used, cost_usd, updated_at)
40
+ VALUES (?, ?, ?, ?, unixepoch())
41
+ ON CONFLICT(swarm_id, executor_id) DO UPDATE SET
42
+ tokens_used = tokens_used + excluded.tokens_used,
43
+ cost_usd = cost_usd + excluded.cost_usd,
44
+ updated_at = unixepoch()`, [swarmId, executorId, totalTokens, cost]);
45
+ const state = this.getState(swarmId);
46
+ this.emit({
47
+ type: 'budget:update',
48
+ swarmId,
49
+ data: { executorId, cost, totalSpent: state.totalSpentUSD },
50
+ timestamp: Date.now(),
51
+ });
52
+ // Check alert threshold
53
+ const config = this.getConfig(swarmId);
54
+ if (config) {
55
+ const pct = (state.totalSpentUSD / config.total_budget_usd) * 100;
56
+ if (pct >= config.alert_threshold_pct && !state.alertFired) {
57
+ this.emit({
58
+ type: 'budget:alert',
59
+ swarmId,
60
+ data: { percentUsed: pct, spent: state.totalSpentUSD, budget: config.total_budget_usd },
61
+ timestamp: Date.now(),
62
+ });
63
+ }
64
+ if (state.totalSpentUSD >= config.total_budget_usd) {
65
+ this.emit({
66
+ type: 'budget:cap',
67
+ swarmId,
68
+ data: { action: config.hard_cap_action, spent: state.totalSpentUSD },
69
+ timestamp: Date.now(),
70
+ });
71
+ }
72
+ }
73
+ return state;
74
+ }
75
+ getState(swarmId) {
76
+ const config = this.getConfig(swarmId);
77
+ const totalBudget = config?.total_budget_usd ?? 0;
78
+ const hardCapAction = (config?.hard_cap_action ?? 'downgrade');
79
+ const alertThreshold = config?.alert_threshold_pct ?? 80;
80
+ const rows = this.db.query('SELECT executor_id, cost_usd FROM swarm_budget WHERE swarm_id = ?').all(swarmId);
81
+ const perExecutor = {};
82
+ let totalSpent = 0;
83
+ for (const row of rows) {
84
+ perExecutor[row.executor_id] = row.cost_usd;
85
+ totalSpent += row.cost_usd;
86
+ }
87
+ const pct = totalBudget > 0 ? (totalSpent / totalBudget) * 100 : 0;
88
+ return {
89
+ swarmId,
90
+ totalSpentUSD: totalSpent,
91
+ totalBudgetUSD: totalBudget,
92
+ remaining: Math.max(0, totalBudget - totalSpent),
93
+ perExecutor,
94
+ alertFired: pct >= alertThreshold,
95
+ capReached: totalBudget > 0 && totalSpent >= totalBudget,
96
+ hardCapAction,
97
+ };
98
+ }
99
+ checkExecutorLimit(swarmId, executorId) {
100
+ const limit = this.db.query('SELECT limit_usd FROM budget_per_executor WHERE swarm_id = ? AND executor_id = ?').get(swarmId, executorId);
101
+ if (!limit)
102
+ return true;
103
+ const usage = this.db.query('SELECT cost_usd FROM swarm_budget WHERE swarm_id = ? AND executor_id = ?').get(swarmId, executorId);
104
+ return (usage?.cost_usd ?? 0) < limit.limit_usd;
105
+ }
106
+ getDowngradeTarget(executorId) {
107
+ const cheapestModel = getCheapestModel(executorId);
108
+ const cheapestExecutor = executorId === 'hermes' ? 'hermes' : 'gemini';
109
+ return { executorId: cheapestExecutor, model: cheapestModel };
110
+ }
111
+ getConfig(swarmId) {
112
+ return this.db.query('SELECT * FROM budget_config WHERE swarm_id = ?').get(swarmId);
113
+ }
114
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Per-model pricing tables for budget normalization.
3
+ *
4
+ * Prices are in USD per 1M tokens. Updated to reflect published rates.
5
+ */
6
+ export interface ModelPricing {
7
+ inputPer1M: number;
8
+ outputPer1M: number;
9
+ }
10
+ export declare function getModelPricing(model: string): ModelPricing;
11
+ export declare function estimateCostUSD(model: string, inputTokens: number, outputTokens: number): number;
12
+ export declare function getCheapestModel(executorId: string): string;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Per-model pricing tables for budget normalization.
3
+ *
4
+ * Prices are in USD per 1M tokens. Updated to reflect published rates.
5
+ */
6
+ const PRICING = {
7
+ // Anthropic
8
+ 'opus': { inputPer1M: 15.00, outputPer1M: 75.00 },
9
+ 'claude-opus-4-6': { inputPer1M: 15.00, outputPer1M: 75.00 },
10
+ 'sonnet': { inputPer1M: 3.00, outputPer1M: 15.00 },
11
+ 'claude-sonnet-4-6': { inputPer1M: 3.00, outputPer1M: 15.00 },
12
+ 'haiku': { inputPer1M: 0.25, outputPer1M: 1.25 },
13
+ 'claude-haiku-4-5': { inputPer1M: 0.25, outputPer1M: 1.25 },
14
+ // Google
15
+ 'gemini-2.5-pro': { inputPer1M: 1.25, outputPer1M: 10.00 },
16
+ 'gemini-2.5-flash': { inputPer1M: 0.15, outputPer1M: 0.60 },
17
+ 'pro': { inputPer1M: 1.25, outputPer1M: 10.00 },
18
+ 'flash': { inputPer1M: 0.15, outputPer1M: 0.60 },
19
+ // OpenAI
20
+ 'gpt-5.x': { inputPer1M: 2.50, outputPer1M: 10.00 },
21
+ 'gpt-4.1': { inputPer1M: 2.00, outputPer1M: 8.00 },
22
+ 'o3': { inputPer1M: 10.00, outputPer1M: 40.00 },
23
+ // Free / BYOK
24
+ 'byok': { inputPer1M: 0, outputPer1M: 0 },
25
+ 'free': { inputPer1M: 0, outputPer1M: 0 },
26
+ };
27
+ export function getModelPricing(model) {
28
+ const key = model.toLowerCase();
29
+ return PRICING[key] ?? { inputPer1M: 1.00, outputPer1M: 5.00 };
30
+ }
31
+ export function estimateCostUSD(model, inputTokens, outputTokens) {
32
+ const pricing = getModelPricing(model);
33
+ return (inputTokens / 1_000_000) * pricing.inputPer1M +
34
+ (outputTokens / 1_000_000) * pricing.outputPer1M;
35
+ }
36
+ export function getCheapestModel(executorId) {
37
+ switch (executorId) {
38
+ case 'hermes': return 'byok';
39
+ case 'gemini': return 'flash';
40
+ case 'codex': return 'gpt-4.1';
41
+ case 'claude-code': return 'haiku';
42
+ default: return 'byok';
43
+ }
44
+ }