botinabox 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,2965 @@
1
+ // src/shared/constants.ts
2
+ var EVENTS = {
3
+ // Cost
4
+ COST_RECORDED: "cost.recorded",
5
+ // Agent
6
+ AGENT_CREATED: "agent.created",
7
+ AGENT_STATUS_CHANGED: "agent.status_changed",
8
+ BUDGET_EXCEEDED: "budget.exceeded",
9
+ // Task
10
+ TASK_CREATED: "task.created",
11
+ TASK_COMPLETED: "task.completed",
12
+ TASK_FAILED: "task.failed",
13
+ TASK_CANCELLED: "task.cancelled",
14
+ // Run
15
+ RUN_STARTED: "run.started",
16
+ RUN_COMPLETED: "run.completed",
17
+ RUN_FAILED: "run.failed",
18
+ // Message pipeline
19
+ MESSAGE_INBOUND: "message.inbound",
20
+ MESSAGE_ROUTED: "message.routed",
21
+ MESSAGE_PROCESSED: "message.processed",
22
+ MESSAGE_OUTBOUND: "message.outbound",
23
+ MESSAGE_SENT: "message.sent",
24
+ // Updates
25
+ UPDATE_AVAILABLE: "update.available",
26
+ UPDATE_STARTED: "update.started",
27
+ UPDATE_COMPLETED: "update.completed",
28
+ UPDATE_FAILED: "update.failed",
29
+ // Workflow
30
+ WORKFLOW_STARTED: "workflow.started",
31
+ WORKFLOW_STEP_COMPLETED: "workflow.step_completed",
32
+ WORKFLOW_COMPLETED: "workflow.completed",
33
+ WORKFLOW_FAILED: "workflow.failed"
34
+ };
35
+ var DEFAULTS = {
36
+ TASK_POLL_INTERVAL_MS: 3e4,
37
+ NOTIFICATION_POLL_INTERVAL_MS: 5e3,
38
+ HEARTBEAT_INTERVAL_MS: 3e5,
39
+ // 5 minutes
40
+ ORPHAN_REAP_INTERVAL_MS: 3e5,
41
+ // 5 minutes
42
+ STALE_RUN_THRESHOLD_MS: 18e5,
43
+ // 30 minutes
44
+ STALE_TASK_AGE_MS: 72e5,
45
+ // 2 hours
46
+ MAX_CHAIN_DEPTH: 5,
47
+ MAX_NOTIFICATION_RETRIES: 3,
48
+ UPDATE_CHECK_INTERVAL_MS: 864e5,
49
+ // 24 hours
50
+ RENDER_WATCH_INTERVAL_MS: 3e4,
51
+ DATA_PATH: "./data/bot.db",
52
+ RENDER_OUTPUT_DIR: "./context",
53
+ LOG_PATH_TEMPLATE: "./data/runs/{runId}.ndjson",
54
+ BUDGET_WARN_PERCENT: 80
55
+ };
56
+ var TASK_STATUSES = [
57
+ "backlog",
58
+ "todo",
59
+ "in_progress",
60
+ "in_review",
61
+ "done",
62
+ "blocked",
63
+ "cancelled"
64
+ ];
65
+ var AGENT_STATUSES = [
66
+ "idle",
67
+ "running",
68
+ "paused",
69
+ "terminated",
70
+ "error"
71
+ ];
72
+ var RUN_STATUSES = [
73
+ "queued",
74
+ "running",
75
+ "succeeded",
76
+ "failed",
77
+ "cancelled"
78
+ ];
79
+
80
+ // src/core/hooks/hook-bus.ts
81
+ var HookBus = class {
82
+ registrations = /* @__PURE__ */ new Map();
83
+ nextId = 0;
84
+ register(event, handler, opts) {
85
+ const reg = {
86
+ event,
87
+ handler,
88
+ priority: opts?.priority ?? 50,
89
+ once: opts?.once ?? false,
90
+ filter: opts?.filter,
91
+ id: this.nextId++
92
+ };
93
+ const list = this.registrations.get(event) ?? [];
94
+ list.push(reg);
95
+ list.sort((a, b) => a.priority - b.priority || a.id - b.id);
96
+ this.registrations.set(event, list);
97
+ return () => {
98
+ const arr = this.registrations.get(event);
99
+ if (!arr) return;
100
+ const idx = arr.indexOf(reg);
101
+ if (idx !== -1) arr.splice(idx, 1);
102
+ };
103
+ }
104
+ async emit(event, context) {
105
+ const list = this.registrations.get(event);
106
+ if (!list || list.length === 0) return;
107
+ const snapshot = [...list];
108
+ const toRemove = [];
109
+ for (const reg of snapshot) {
110
+ if (reg.filter) {
111
+ const matches = Object.entries(reg.filter).every(([k, v]) => context[k] === v);
112
+ if (!matches) continue;
113
+ }
114
+ try {
115
+ await reg.handler(context);
116
+ } catch (err) {
117
+ console.error(`[HookBus] Handler error on event "${event}":`, err);
118
+ }
119
+ if (reg.once) toRemove.push(reg);
120
+ }
121
+ for (const r of toRemove) {
122
+ const arr = this.registrations.get(event);
123
+ if (!arr) continue;
124
+ const idx = arr.indexOf(r);
125
+ if (idx !== -1) arr.splice(idx, 1);
126
+ }
127
+ }
128
+ /** Emit synchronously (use only when async is not needed) */
129
+ emitSync(event, context) {
130
+ const list = this.registrations.get(event);
131
+ if (!list || list.length === 0) return;
132
+ const snapshot = [...list];
133
+ const toRemove = [];
134
+ for (const reg of snapshot) {
135
+ if (reg.filter) {
136
+ const matches = Object.entries(reg.filter).every(([k, v]) => context[k] === v);
137
+ if (!matches) continue;
138
+ }
139
+ try {
140
+ const result = reg.handler(context);
141
+ if (result instanceof Promise) {
142
+ result.catch((err) => console.error(`[HookBus] Async handler error on event "${event}":`, err));
143
+ }
144
+ } catch (err) {
145
+ console.error(`[HookBus] Handler error on event "${event}":`, err);
146
+ }
147
+ if (reg.once) toRemove.push(reg);
148
+ }
149
+ for (const r of toRemove) {
150
+ const arr = this.registrations.get(event);
151
+ if (!arr) continue;
152
+ const idx = arr.indexOf(r);
153
+ if (idx !== -1) arr.splice(idx, 1);
154
+ }
155
+ }
156
+ hasListeners(event) {
157
+ return (this.registrations.get(event)?.length ?? 0) > 0;
158
+ }
159
+ listRegistered() {
160
+ const result = [];
161
+ for (const [k, v] of this.registrations) {
162
+ if (v.length > 0) result.push(k);
163
+ }
164
+ return result;
165
+ }
166
+ /** Remove all handlers for an event, or all handlers if no event given */
167
+ clear(event) {
168
+ if (event) {
169
+ this.registrations.delete(event);
170
+ } else {
171
+ this.registrations.clear();
172
+ }
173
+ }
174
+ };
175
+
176
+ // src/core/config/loader.ts
177
+ import { readFileSync, existsSync } from "fs";
178
+ import { parse as parseYaml } from "yaml";
179
+
180
+ // src/core/config/defaults.ts
181
+ var DEFAULT_CONFIG = {
182
+ data: {
183
+ path: "./data/bot.db",
184
+ walMode: true
185
+ },
186
+ channels: {},
187
+ agents: [],
188
+ providers: {},
189
+ models: {
190
+ aliases: {
191
+ fast: "claude-haiku-4-5",
192
+ smart: "claude-opus-4-6",
193
+ balanced: "claude-sonnet-4-6"
194
+ },
195
+ default: "smart",
196
+ routing: {
197
+ conversation: "fast",
198
+ task_execution: "smart",
199
+ classification: "fast"
200
+ },
201
+ fallbackChain: []
202
+ },
203
+ entities: {},
204
+ security: {
205
+ fieldLengthLimits: { default: 65535 }
206
+ },
207
+ render: {
208
+ outputDir: "./context",
209
+ watchIntervalMs: 3e4
210
+ },
211
+ updates: {
212
+ policy: "auto-compatible",
213
+ checkIntervalMs: 864e5
214
+ },
215
+ budget: {
216
+ warnPercent: 80
217
+ }
218
+ };
219
+
220
+ // src/core/config/interpolate.ts
221
+ function interpolateEnv(value, env = process.env) {
222
+ if (typeof value === "string") {
223
+ return value.replace(/\$\{([^}]+)\}/g, (_, name) => {
224
+ const envVal = env[name];
225
+ if (envVal === void 0) {
226
+ return `\${${name}}`;
227
+ }
228
+ return envVal;
229
+ });
230
+ }
231
+ if (Array.isArray(value)) {
232
+ return value.map((item) => interpolateEnv(item, env));
233
+ }
234
+ if (value !== null && typeof value === "object") {
235
+ const result = {};
236
+ for (const [k, v] of Object.entries(value)) {
237
+ result[k] = interpolateEnv(v, env);
238
+ }
239
+ return result;
240
+ }
241
+ return value;
242
+ }
243
+
244
+ // src/core/config/loader.ts
245
+ function deepMerge(base, override) {
246
+ if (override === void 0 || override === null) return base;
247
+ if (base === void 0 || base === null) return override;
248
+ if (typeof base !== "object" || typeof override !== "object") return override;
249
+ if (Array.isArray(override)) return override;
250
+ const result = { ...base };
251
+ for (const [k, v] of Object.entries(override)) {
252
+ if (v !== void 0) {
253
+ result[k] = deepMerge(result[k], v);
254
+ }
255
+ }
256
+ return result;
257
+ }
258
+ function loadConfig(opts) {
259
+ const configPath = opts?.configPath ?? "botinabox.config.yml";
260
+ const errors = [];
261
+ let fileConfig = {};
262
+ if (existsSync(configPath)) {
263
+ try {
264
+ const raw = readFileSync(configPath, "utf-8");
265
+ const parsed = parseYaml(raw);
266
+ fileConfig = interpolateEnv(parsed, opts?.env ?? process.env);
267
+ } catch (err) {
268
+ errors.push({ field: "configPath", message: `Failed to parse ${configPath}: ${String(err)}` });
269
+ }
270
+ }
271
+ const merged = deepMerge(
272
+ deepMerge(DEFAULT_CONFIG, fileConfig),
273
+ opts?.overrides ?? {}
274
+ );
275
+ return { config: Object.freeze(merged), errors };
276
+ }
277
+ var _config = null;
278
+ function getConfig() {
279
+ if (!_config) throw new Error("Config not loaded \u2014 call loadConfig() first");
280
+ return _config;
281
+ }
282
+ function initConfig(opts) {
283
+ const { config, errors } = loadConfig(opts);
284
+ _config = config;
285
+ return errors;
286
+ }
287
+ function _resetConfig() {
288
+ _config = null;
289
+ }
290
+
291
+ // src/core/config/schema.ts
292
+ import Ajv from "ajv";
293
+ var ajv = new Ajv({ allErrors: true, coerceTypes: false });
294
+ var BOT_CONFIG_SCHEMA = {
295
+ type: "object",
296
+ additionalProperties: true,
297
+ properties: {
298
+ data: {
299
+ type: "object",
300
+ required: ["path", "walMode"],
301
+ properties: {
302
+ path: { type: "string", minLength: 1 },
303
+ walMode: { type: "boolean" },
304
+ backupDir: { type: "string" }
305
+ }
306
+ },
307
+ models: {
308
+ type: "object",
309
+ required: ["aliases", "default", "routing", "fallbackChain"],
310
+ properties: {
311
+ aliases: { type: "object", additionalProperties: { type: "string" } },
312
+ default: { type: "string", minLength: 1 },
313
+ routing: { type: "object", additionalProperties: { type: "string" } },
314
+ fallbackChain: { type: "array", items: { type: "string" } },
315
+ costLimit: {
316
+ type: "object",
317
+ properties: {
318
+ perRunCents: { type: "number", minimum: 0 }
319
+ }
320
+ }
321
+ }
322
+ },
323
+ security: {
324
+ type: "object",
325
+ properties: {
326
+ fieldLengthLimits: { type: "object", additionalProperties: { type: "number" } },
327
+ allowedFilePrefixes: { type: "array", items: { type: "string" } }
328
+ }
329
+ },
330
+ render: {
331
+ type: "object",
332
+ required: ["outputDir", "watchIntervalMs"],
333
+ properties: {
334
+ outputDir: { type: "string", minLength: 1 },
335
+ watchIntervalMs: { type: "number", minimum: 1e3 }
336
+ }
337
+ },
338
+ updates: {
339
+ type: "object",
340
+ required: ["policy", "checkIntervalMs"],
341
+ properties: {
342
+ policy: { type: "string", enum: ["auto-all", "auto-compatible", "auto-patch", "notify", "manual"] },
343
+ checkIntervalMs: { type: "number", minimum: 6e4 }
344
+ }
345
+ },
346
+ budget: {
347
+ type: "object",
348
+ required: ["warnPercent"],
349
+ properties: {
350
+ warnPercent: { type: "number", minimum: 1, maximum: 100 },
351
+ globalMonthlyCents: { type: "number", minimum: 0 }
352
+ }
353
+ },
354
+ agents: {
355
+ type: "array",
356
+ items: {
357
+ type: "object",
358
+ required: ["slug", "name", "adapter"],
359
+ properties: {
360
+ slug: { type: "string", minLength: 1, pattern: "^[a-z0-9-]+$" },
361
+ name: { type: "string", minLength: 1 },
362
+ adapter: { type: "string", minLength: 1 },
363
+ model: { type: "string" },
364
+ workdir: { type: "string" },
365
+ maxConcurrentRuns: { type: "number", minimum: 1 },
366
+ budgetMonthlyCents: { type: "number", minimum: 0 },
367
+ canCreateAgents: { type: "boolean" },
368
+ skipPermissions: { type: "boolean" }
369
+ }
370
+ }
371
+ },
372
+ channels: {
373
+ type: "object",
374
+ additionalProperties: {
375
+ type: "object",
376
+ required: ["enabled"],
377
+ properties: {
378
+ enabled: { type: "boolean" }
379
+ }
380
+ }
381
+ },
382
+ providers: {
383
+ type: "object",
384
+ additionalProperties: {
385
+ type: "object",
386
+ required: ["enabled"],
387
+ properties: {
388
+ enabled: { type: "boolean" }
389
+ }
390
+ }
391
+ }
392
+ }
393
+ };
394
+ var _validate = null;
395
+ function getValidator() {
396
+ if (!_validate) {
397
+ _validate = ajv.compile(BOT_CONFIG_SCHEMA);
398
+ }
399
+ return _validate;
400
+ }
401
+ function validateConfig(config) {
402
+ const validate = getValidator();
403
+ const valid = validate(config);
404
+ if (valid) return [];
405
+ return (validate.errors ?? []).map((err) => ({
406
+ path: err.instancePath || "/",
407
+ message: err.message ?? "invalid"
408
+ }));
409
+ }
410
+
411
+ // src/core/llm/provider-registry.ts
412
+ var ProviderRegistry = class {
413
+ providers = /* @__PURE__ */ new Map();
414
+ register(provider) {
415
+ if (this.providers.has(provider.id)) {
416
+ throw new Error(`Provider already registered: ${provider.id}`);
417
+ }
418
+ this.providers.set(provider.id, provider);
419
+ }
420
+ unregister(id) {
421
+ this.providers.delete(id);
422
+ }
423
+ get(id) {
424
+ return this.providers.get(id);
425
+ }
426
+ list() {
427
+ return Array.from(this.providers.values());
428
+ }
429
+ listModels() {
430
+ const models = [];
431
+ for (const provider of this.providers.values()) {
432
+ models.push(...provider.models);
433
+ }
434
+ return models;
435
+ }
436
+ };
437
+
438
+ // src/core/llm/model-router.ts
439
+ var ModelRouter = class {
440
+ constructor(registry, config) {
441
+ this.registry = registry;
442
+ this.config = config;
443
+ }
444
+ registry;
445
+ config;
446
+ /**
447
+ * Resolve a model ID or alias to a ResolvedModel.
448
+ * 1. Look up alias in config.aliases (or use as-is)
449
+ * 2. Search all registered providers for a model with that id
450
+ */
451
+ resolve(modelIdOrAlias) {
452
+ const modelId = this.config.aliases[modelIdOrAlias] ?? modelIdOrAlias;
453
+ for (const provider of this.registry.list()) {
454
+ const found = provider.models.find((m) => m.id === modelId);
455
+ if (found) {
456
+ return { provider: provider.id, model: found.id };
457
+ }
458
+ }
459
+ return void 0;
460
+ }
461
+ /**
462
+ * Try primary model, then each in config.fallbackChain.
463
+ * Throws if none found.
464
+ */
465
+ resolveWithFallback(modelIdOrAlias) {
466
+ const primary = this.resolve(modelIdOrAlias);
467
+ if (primary) return primary;
468
+ for (const fallback of this.config.fallbackChain) {
469
+ const resolved = this.resolve(fallback);
470
+ if (resolved) return resolved;
471
+ }
472
+ throw new Error(
473
+ `No available model found for "${modelIdOrAlias}" and fallback chain [${this.config.fallbackChain.join(", ")}]`
474
+ );
475
+ }
476
+ /**
477
+ * Use config.routing[purpose] ?? config.default, then resolveWithFallback.
478
+ */
479
+ resolveForPurpose(purpose) {
480
+ const modelIdOrAlias = this.config.routing[purpose] ?? this.config.default;
481
+ return this.resolveWithFallback(modelIdOrAlias);
482
+ }
483
+ /** Returns all models from all registered providers. */
484
+ listAvailable() {
485
+ return this.registry.listModels();
486
+ }
487
+ };
488
+
489
+ // src/core/llm/auto-discovery.ts
490
+ import { readdir, readFile } from "fs/promises";
491
+ import { join } from "path";
492
+ async function discoverProviders(nodeModulesPath, importer = (name) => import(name)) {
493
+ const scopeDir = join(nodeModulesPath, "@botinabox");
494
+ let entries;
495
+ try {
496
+ entries = await readdir(scopeDir);
497
+ } catch {
498
+ return [];
499
+ }
500
+ const providers = [];
501
+ for (const entry of entries) {
502
+ const pkgPath = join(scopeDir, entry, "package.json");
503
+ let pkg;
504
+ try {
505
+ const raw = await readFile(pkgPath, "utf8");
506
+ pkg = JSON.parse(raw);
507
+ } catch {
508
+ continue;
509
+ }
510
+ const botinabox = pkg.botinabox;
511
+ if (botinabox?.type !== "provider") {
512
+ continue;
513
+ }
514
+ const packageName = pkg.name;
515
+ if (!packageName) {
516
+ continue;
517
+ }
518
+ try {
519
+ const mod = await importer(packageName);
520
+ const provider = "default" in mod && mod.default ? mod.default : mod;
521
+ providers.push(provider);
522
+ } catch {
523
+ }
524
+ }
525
+ return providers;
526
+ }
527
+
528
+ // src/core/chat/channel-registry.ts
529
+ var ChannelRegistryError = class extends Error {
530
+ constructor(message) {
531
+ super(message);
532
+ this.name = "ChannelRegistryError";
533
+ }
534
+ };
535
+ var ChannelRegistry = class {
536
+ adapters = /* @__PURE__ */ new Map();
537
+ started = false;
538
+ /**
539
+ * Register a channel adapter.
540
+ * Throws if an adapter with the same id is already registered.
541
+ * If registry is already started, immediately connects the adapter.
542
+ */
543
+ register(adapter, config) {
544
+ if (this.adapters.has(adapter.id)) {
545
+ throw new ChannelRegistryError(`Channel adapter already registered: ${adapter.id}`);
546
+ }
547
+ this.adapters.set(adapter.id, { adapter, config });
548
+ if (this.started) {
549
+ void adapter.connect(config ?? {});
550
+ }
551
+ }
552
+ /**
553
+ * Unregister a channel adapter.
554
+ * Disconnects the adapter if it exists.
555
+ */
556
+ async unregister(id) {
557
+ const entry = this.adapters.get(id);
558
+ if (!entry) return;
559
+ await entry.adapter.disconnect();
560
+ this.adapters.delete(id);
561
+ }
562
+ /**
563
+ * Reconfigure an adapter: disconnect, update config, reconnect.
564
+ */
565
+ async reconfigure(id, config) {
566
+ const entry = this.adapters.get(id);
567
+ if (!entry) {
568
+ throw new ChannelRegistryError(`Channel adapter not found: ${id}`);
569
+ }
570
+ await entry.adapter.disconnect();
571
+ entry.config = config;
572
+ await entry.adapter.connect(config ?? {});
573
+ }
574
+ /**
575
+ * Run health checks on all registered adapters.
576
+ */
577
+ async healthCheck() {
578
+ const results = {};
579
+ for (const [id, entry] of this.adapters) {
580
+ try {
581
+ results[id] = await entry.adapter.healthCheck();
582
+ } catch (err) {
583
+ results[id] = { ok: false, error: String(err) };
584
+ }
585
+ }
586
+ return results;
587
+ }
588
+ /** Get an adapter by ID. */
589
+ get(id) {
590
+ return this.adapters.get(id)?.adapter;
591
+ }
592
+ /** List all registered adapters. */
593
+ list() {
594
+ return Array.from(this.adapters.values()).map((e) => e.adapter);
595
+ }
596
+ /**
597
+ * Start: connect all registered adapters and mark registry as started.
598
+ */
599
+ async start() {
600
+ this.started = true;
601
+ for (const entry of this.adapters.values()) {
602
+ await entry.adapter.connect(entry.config ?? {});
603
+ }
604
+ }
605
+ /**
606
+ * Stop: disconnect all registered adapters.
607
+ */
608
+ async stop() {
609
+ for (const entry of this.adapters.values()) {
610
+ await entry.adapter.disconnect();
611
+ }
612
+ this.started = false;
613
+ }
614
+ };
615
+
616
+ // src/core/chat/auto-discovery.ts
617
+ import { readdir as readdir2, readFile as readFile2 } from "fs/promises";
618
+ import { join as join2 } from "path";
619
+ async function discoverChannels(nodeModulesPath, importer = (name) => import(name)) {
620
+ const scopeDir = join2(nodeModulesPath, "@botinabox");
621
+ let entries;
622
+ try {
623
+ entries = await readdir2(scopeDir);
624
+ } catch {
625
+ return [];
626
+ }
627
+ const adapters = [];
628
+ for (const entry of entries) {
629
+ const pkgPath = join2(scopeDir, entry, "package.json");
630
+ let pkg;
631
+ try {
632
+ const raw = await readFile2(pkgPath, "utf8");
633
+ pkg = JSON.parse(raw);
634
+ } catch {
635
+ continue;
636
+ }
637
+ const botinabox = pkg.botinabox;
638
+ if (botinabox?.type !== "channel") {
639
+ continue;
640
+ }
641
+ const packageName = pkg.name;
642
+ if (!packageName) {
643
+ continue;
644
+ }
645
+ try {
646
+ const mod = await importer(packageName);
647
+ const adapter = "default" in mod && mod.default ? mod.default : mod;
648
+ adapters.push(adapter);
649
+ } catch {
650
+ }
651
+ }
652
+ return adapters;
653
+ }
654
+
655
+ // src/core/chat/routing.ts
656
+ function buildAgentBindings(agents) {
657
+ const bindings = /* @__PURE__ */ new Map();
658
+ for (const agent of agents) {
659
+ const agentId = agent.slug;
660
+ const cfg = agent.config ?? {};
661
+ const channels = [];
662
+ if (typeof cfg["channel"] === "string") {
663
+ channels.push(cfg["channel"]);
664
+ }
665
+ if (Array.isArray(cfg["channels"])) {
666
+ for (const ch of cfg["channels"]) {
667
+ channels.push(ch);
668
+ }
669
+ }
670
+ for (const ch of channels) {
671
+ bindings.set(ch, agentId);
672
+ }
673
+ }
674
+ return bindings;
675
+ }
676
+
677
+ // src/core/chat/policies.ts
678
+ function checkAllowlist(allowFrom, senderId) {
679
+ if (allowFrom.length === 0) return true;
680
+ return allowFrom.includes(senderId);
681
+ }
682
+ function checkMentionGate(msg, botId) {
683
+ const body = msg.body ?? "";
684
+ return body.includes(`@${botId}`) || body.includes(botId);
685
+ }
686
+
687
+ // src/core/chat/pipeline.ts
688
+ var MessagePipeline = class {
689
+ constructor(hooks, agentRegistry, taskQueue, config) {
690
+ this.hooks = hooks;
691
+ this.agentRegistry = agentRegistry;
692
+ this.taskQueue = taskQueue;
693
+ this.config = config;
694
+ this.agentBindings = buildAgentBindings(config.agents);
695
+ }
696
+ hooks;
697
+ agentRegistry;
698
+ taskQueue;
699
+ config;
700
+ agentBindings;
701
+ /**
702
+ * Process an inbound message end-to-end.
703
+ * 1. Emit 'message.inbound'
704
+ * 2. Resolve agent
705
+ * 3. Check policy (allowlist / mention gate)
706
+ * 4. Create task
707
+ * 5. Emit 'message.processed'
708
+ */
709
+ async processInbound(msg) {
710
+ await this.hooks.emit("message.inbound", { message: msg, channel: msg.channel });
711
+ const agentId = this.resolveAgent(msg);
712
+ if (agentId !== void 0) {
713
+ const allowed = this.evaluatePolicy(msg, agentId);
714
+ if (allowed) {
715
+ await this.taskQueue.create({
716
+ title: `Message from ${msg.from} on ${msg.channel}`,
717
+ description: msg.body,
718
+ assignee_id: agentId,
719
+ context: JSON.stringify({ message: msg })
720
+ });
721
+ }
722
+ }
723
+ await this.hooks.emit("message.processed", {
724
+ message: msg,
725
+ channel: msg.channel,
726
+ agentId: agentId ?? null
727
+ });
728
+ }
729
+ /**
730
+ * Resolve the best agent for a given inbound message.
731
+ * Returns agentId (slug) or undefined if no match.
732
+ */
733
+ resolveAgent(msg) {
734
+ const bound = this.agentBindings.get(msg.channel);
735
+ if (bound) return bound;
736
+ const first = this.config.agents[0];
737
+ return first?.slug;
738
+ }
739
+ /**
740
+ * Evaluate messaging policy for the given agent.
741
+ * Returns false if the message is blocked.
742
+ */
743
+ evaluatePolicy(msg, agentId) {
744
+ const channelConfig = this.config.channels[msg.channel];
745
+ const allowFrom = channelConfig?.["allowFrom"] ?? [];
746
+ if (!checkAllowlist(allowFrom, msg.from)) {
747
+ return false;
748
+ }
749
+ const mentionGated = channelConfig?.["requireMention"] ?? false;
750
+ if (mentionGated) {
751
+ if (!checkMentionGate(msg, agentId)) {
752
+ return false;
753
+ }
754
+ }
755
+ return true;
756
+ }
757
+ };
758
+
759
+ // src/core/chat/session-key.ts
760
+ var SessionKey = class _SessionKey {
761
+ constructor(agentId, channel, scope) {
762
+ this.agentId = agentId;
763
+ this.channel = channel;
764
+ this.scope = scope;
765
+ }
766
+ agentId;
767
+ channel;
768
+ scope;
769
+ toString() {
770
+ return `agent:${this.agentId}:${this.channel}:${this.scope}`;
771
+ }
772
+ toJSON() {
773
+ return {
774
+ agentId: this.agentId,
775
+ channel: this.channel,
776
+ scope: this.scope
777
+ };
778
+ }
779
+ static fromString(key) {
780
+ const parts = key.split(":");
781
+ if (parts.length < 4 || parts[0] !== "agent") {
782
+ throw new Error(`Invalid SessionKey format: ${key}`);
783
+ }
784
+ const [, agentId, channel, scope] = parts;
785
+ if (!agentId || !channel || !scope) {
786
+ throw new Error(`Invalid SessionKey format: ${key}`);
787
+ }
788
+ return new _SessionKey(agentId, channel, scope);
789
+ }
790
+ /**
791
+ * Build a session key from structured parameters.
792
+ *
793
+ * @param agentId - The agent identifier
794
+ * @param channel - The channel identifier
795
+ * @param chatType - 'dm' or 'group'
796
+ * @param peerId - The peer/user ID
797
+ * @param dmScope - DM scoping strategy
798
+ * - 'main' → single session per agent+channel regardless of peer
799
+ * - 'per-peer' → one session per (agent, peer)
800
+ * - 'per-channel-peer' → one session per (agent, channel, peer)
801
+ */
802
+ static build(agentId, channel, chatType, peerId, dmScope) {
803
+ if (chatType === "group") {
804
+ return new _SessionKey(agentId, channel, channel);
805
+ }
806
+ switch (dmScope) {
807
+ case "main":
808
+ return new _SessionKey(agentId, channel, "main");
809
+ case "per-peer":
810
+ return new _SessionKey(agentId, channel, peerId);
811
+ case "per-channel-peer":
812
+ return new _SessionKey(agentId, channel, `${channel}:${peerId}`);
813
+ }
814
+ }
815
+ };
816
+
817
+ // src/core/orchestrator/session-manager.ts
818
+ var SessionManager = class {
819
+ constructor(db) {
820
+ this.db = db;
821
+ }
822
+ db;
823
+ async save(agentId, channelId, peerId, params) {
824
+ const existing = await this._find(agentId, channelId, peerId);
825
+ if (existing) {
826
+ await this.db.update("sessions", { id: existing["id"] }, {
827
+ context: JSON.stringify(params),
828
+ last_message_at: (/* @__PURE__ */ new Date()).toISOString(),
829
+ message_count: (existing["message_count"] ?? 0) + 1
830
+ });
831
+ return existing["id"];
832
+ } else {
833
+ const row = await this.db.insert("sessions", {
834
+ agent_id: agentId,
835
+ channel: channelId,
836
+ peer_id: peerId,
837
+ context: JSON.stringify(params),
838
+ last_message_at: (/* @__PURE__ */ new Date()).toISOString(),
839
+ message_count: 1
840
+ });
841
+ return row["id"];
842
+ }
843
+ }
844
+ async load(agentId, channelId, peerId) {
845
+ const session = await this._find(agentId, channelId, peerId);
846
+ if (!session) return void 0;
847
+ const context = session["context"] ? JSON.parse(session["context"]) : {};
848
+ return { ...session, ...context };
849
+ }
850
+ async clear(agentId, channelId, peerId) {
851
+ const session = await this._find(agentId, channelId, peerId);
852
+ if (session) {
853
+ await this.db.delete("sessions", { id: session["id"] });
854
+ }
855
+ }
856
+ async shouldClear(session, opts) {
857
+ if (opts.maxRuns !== void 0) {
858
+ const messageCount = session["message_count"] ?? 0;
859
+ if (messageCount > opts.maxRuns) return true;
860
+ }
861
+ if (opts.maxAgeHours !== void 0) {
862
+ const createdAt = session["created_at"];
863
+ if (createdAt) {
864
+ const ageMs = Date.now() - new Date(createdAt).getTime();
865
+ const ageHours = ageMs / (1e3 * 60 * 60);
866
+ if (ageHours > opts.maxAgeHours) return true;
867
+ }
868
+ }
869
+ return false;
870
+ }
871
+ async _find(agentId, channelId, peerId) {
872
+ const rows = await this.db.query("sessions", {
873
+ where: { agent_id: agentId, channel: channelId, peer_id: peerId }
874
+ });
875
+ return rows[0] ?? void 0;
876
+ }
877
+ };
878
+
879
+ // src/core/chat/session-manager.ts
880
+ var ChatSessionManager = class {
881
+ inner;
882
+ constructor(db) {
883
+ this.inner = new SessionManager(db);
884
+ }
885
+ async save(key, params) {
886
+ return this.inner.save(key.agentId, key.channel, key.scope, params);
887
+ }
888
+ async load(key) {
889
+ return this.inner.load(key.agentId, key.channel, key.scope);
890
+ }
891
+ async clear(key) {
892
+ return this.inner.clear(key.agentId, key.channel, key.scope);
893
+ }
894
+ async shouldClear(session, opts) {
895
+ return this.inner.shouldClear(session, opts);
896
+ }
897
+ };
898
+
899
+ // src/core/chat/notification-queue.ts
900
+ var NotificationQueue = class {
901
+ constructor(db, hooks, channelRegistry, opts) {
902
+ this.db = db;
903
+ this.hooks = hooks;
904
+ this.channelRegistry = channelRegistry;
905
+ this.maxRetries = opts?.maxRetries ?? 3;
906
+ this.pollIntervalMs = opts?.pollIntervalMs ?? 5e3;
907
+ }
908
+ db;
909
+ hooks;
910
+ channelRegistry;
911
+ maxRetries;
912
+ pollIntervalMs;
913
+ timer = null;
914
+ /**
915
+ * Enqueue a notification for delivery.
916
+ * Returns the notification ID.
917
+ */
918
+ async enqueue(channel, recipient, payload) {
919
+ const row = await this.db.insert("notifications", {
920
+ channel,
921
+ recipient_id: recipient,
922
+ message: JSON.stringify(payload),
923
+ status: "pending",
924
+ retries: 0
925
+ });
926
+ await this.hooks.emit("notification.enqueued", {
927
+ notificationId: row["id"],
928
+ channel,
929
+ recipient
930
+ });
931
+ return row["id"];
932
+ }
933
+ /** Start the background worker polling for pending notifications. */
934
+ startWorker() {
935
+ if (this.timer) return;
936
+ this.timer = setInterval(() => {
937
+ void this.processNext();
938
+ }, this.pollIntervalMs);
939
+ }
940
+ /** Stop the background worker. */
941
+ stopWorker() {
942
+ if (this.timer) {
943
+ clearInterval(this.timer);
944
+ this.timer = null;
945
+ }
946
+ }
947
+ async processNext() {
948
+ const rows = await this.db.query("notifications", {
949
+ where: { status: "pending" },
950
+ limit: 10
951
+ });
952
+ for (const row of rows) {
953
+ const id = row["id"];
954
+ const channel = row["channel"];
955
+ const recipient = row["recipient_id"];
956
+ const retries = row["retries"] ?? 0;
957
+ let payload;
958
+ try {
959
+ payload = JSON.parse(row["message"]);
960
+ } catch {
961
+ await this.db.update("notifications", { id }, { status: "failed", error: "Invalid payload" });
962
+ continue;
963
+ }
964
+ const adapter = this.channelRegistry.get(channel);
965
+ if (!adapter) {
966
+ await this.db.update("notifications", { id }, {
967
+ status: "failed",
968
+ error: `No adapter for channel: ${channel}`
969
+ });
970
+ continue;
971
+ }
972
+ try {
973
+ await adapter.send({ peerId: recipient, threadId: payload.threadId }, payload);
974
+ await this.db.update("notifications", { id }, {
975
+ status: "sent",
976
+ sent_at: (/* @__PURE__ */ new Date()).toISOString()
977
+ });
978
+ await this.hooks.emit("notification.sent", { notificationId: id, channel, recipient });
979
+ } catch (err) {
980
+ const newRetries = retries + 1;
981
+ if (newRetries >= this.maxRetries) {
982
+ await this.db.update("notifications", { id }, {
983
+ status: "failed",
984
+ retries: newRetries,
985
+ error: String(err)
986
+ });
987
+ await this.hooks.emit("notification.failed", { notificationId: id, channel, recipient, error: String(err) });
988
+ } else {
989
+ await this.db.update("notifications", { id }, {
990
+ status: "pending",
991
+ retries: newRetries,
992
+ error: String(err)
993
+ });
994
+ }
995
+ }
996
+ }
997
+ }
998
+ };
999
+
1000
+ // src/core/chat/text-chunker.ts
1001
+ function chunkText(text, maxLen) {
1002
+ if (maxLen <= 0) throw new Error("maxLen must be > 0");
1003
+ if (text.length <= maxLen) return [text];
1004
+ const chunks = [];
1005
+ function splitInto(segment) {
1006
+ if (segment.length <= maxLen) {
1007
+ if (segment.length > 0) chunks.push(segment);
1008
+ return;
1009
+ }
1010
+ const paraIdx = segment.lastIndexOf("\n\n", maxLen);
1011
+ if (paraIdx > 0) {
1012
+ chunks.push(segment.slice(0, paraIdx));
1013
+ splitInto(segment.slice(paraIdx + 2).trimStart());
1014
+ return;
1015
+ }
1016
+ const sentIdx = segment.lastIndexOf(". ", maxLen);
1017
+ if (sentIdx > 0) {
1018
+ chunks.push(segment.slice(0, sentIdx + 1));
1019
+ splitInto(segment.slice(sentIdx + 2).trimStart());
1020
+ return;
1021
+ }
1022
+ const wordIdx = segment.lastIndexOf(" ", maxLen);
1023
+ if (wordIdx > 0) {
1024
+ chunks.push(segment.slice(0, wordIdx));
1025
+ splitInto(segment.slice(wordIdx + 1));
1026
+ return;
1027
+ }
1028
+ chunks.push(segment.slice(0, maxLen));
1029
+ splitInto(segment.slice(maxLen));
1030
+ }
1031
+ splitInto(text);
1032
+ return chunks;
1033
+ }
1034
+
1035
+ // src/core/chat/formatter.ts
1036
+ function formatText(text, mode) {
1037
+ switch (mode) {
1038
+ case "mrkdwn":
1039
+ return toMrkdwn(text);
1040
+ case "html":
1041
+ return toHtml(text);
1042
+ case "plain":
1043
+ return toPlain(text);
1044
+ }
1045
+ }
1046
+ function toMrkdwn(text) {
1047
+ let result = text.replace(/\*\*(.+?)\*\*/g, "*$1*");
1048
+ result = result.replace(/__(.+?)__/g, "*$1*");
1049
+ return result;
1050
+ }
1051
+ function toHtml(text) {
1052
+ let result = text.replace(/```([\s\S]*?)```/g, "<pre><code>$1</code></pre>");
1053
+ result = result.replace(/`(.+?)`/g, "<code>$1</code>");
1054
+ result = result.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>");
1055
+ result = result.replace(/__(.+?)__/g, "<strong>$1</strong>");
1056
+ result = result.replace(/\*(.+?)\*/g, "<em>$1</em>");
1057
+ result = result.replace(/_(.+?)_/g, "<em>$1</em>");
1058
+ return result;
1059
+ }
1060
+ function toPlain(text) {
1061
+ let result = text.replace(/```[\s\S]*?```/g, (m) => m.replace(/```/g, "").trim());
1062
+ result = result.replace(/`(.+?)`/g, "$1");
1063
+ result = result.replace(/\*\*(.+?)\*\*/g, "$1");
1064
+ result = result.replace(/__(.+?)__/g, "$1");
1065
+ result = result.replace(/\*(.+?)\*/g, "$1");
1066
+ result = result.replace(/_(.+?)_/g, "$1");
1067
+ return result;
1068
+ }
1069
+
1070
+ // src/core/data/data-store.ts
1071
+ import { Lattice } from "latticesql";
1072
+ var DataStoreError = class extends Error {
1073
+ constructor(message) {
1074
+ super(message);
1075
+ this.name = "DataStoreError";
1076
+ }
1077
+ };
1078
+ var DataStore = class {
1079
+ lattice;
1080
+ hooks;
1081
+ outputDir;
1082
+ _initialized = false;
1083
+ deferredStatements = [];
1084
+ constructor(opts) {
1085
+ this.lattice = new Lattice(opts.dbPath, {
1086
+ wal: opts.wal ?? true
1087
+ });
1088
+ this.outputDir = opts.outputDir;
1089
+ this.hooks = opts.hooks;
1090
+ }
1091
+ /**
1092
+ * Register a table definition. Must be called before init().
1093
+ *
1094
+ * tableConstraints may contain both inline constraints (FOREIGN KEY, UNIQUE)
1095
+ * and standalone SQL statements (CREATE INDEX). Standalone statements are
1096
+ * deferred and executed after init() creates the tables.
1097
+ */
1098
+ define(name, def) {
1099
+ if (this._initialized) {
1100
+ throw new DataStoreError("Cannot define tables after init()");
1101
+ }
1102
+ const inlineConstraints = [];
1103
+ for (const stmt of def.tableConstraints ?? []) {
1104
+ const upper = stmt.trimStart().toUpperCase();
1105
+ if (upper.startsWith("CREATE ") || upper.startsWith("DROP ") || upper.startsWith("ALTER ")) {
1106
+ this.deferredStatements.push(stmt);
1107
+ } else {
1108
+ inlineConstraints.push(stmt);
1109
+ }
1110
+ }
1111
+ this.lattice.define(name, {
1112
+ columns: def.columns,
1113
+ primaryKey: def.primaryKey,
1114
+ tableConstraints: inlineConstraints.length ? inlineConstraints : void 0,
1115
+ relations: def.relations,
1116
+ filter: def.filter,
1117
+ render: def.render,
1118
+ outputFile: def.outputFile
1119
+ });
1120
+ }
1121
+ /**
1122
+ * Register an entity context definition for per-entity file rendering.
1123
+ */
1124
+ defineEntityContext(name, def) {
1125
+ this.lattice.defineEntityContext(name, {
1126
+ slug: (row) => row[def.slugColumn],
1127
+ directoryRoot: def.directory,
1128
+ files: def.files,
1129
+ protectedFiles: def.protectedFiles,
1130
+ index: def.indexFile ? { outputFile: def.indexFile, render: (rows) => "" } : void 0
1131
+ });
1132
+ }
1133
+ async init(opts) {
1134
+ await this.lattice.init({ migrations: opts?.migrations });
1135
+ for (const stmt of this.deferredStatements) {
1136
+ this.lattice.db.exec(stmt);
1137
+ }
1138
+ this._initialized = true;
1139
+ }
1140
+ assertInitialized() {
1141
+ if (!this._initialized) {
1142
+ throw new DataStoreError("DataStore not initialized \u2014 call init() first");
1143
+ }
1144
+ }
1145
+ // --- CRUD -----------------------------------------------------------
1146
+ async insert(table, row) {
1147
+ this.assertInitialized();
1148
+ return this.lattice.insertReturning(table, row);
1149
+ }
1150
+ async upsert(table, row) {
1151
+ this.assertInitialized();
1152
+ const id = await this.lattice.upsert(table, row);
1153
+ const result = await this.lattice.get(table, id);
1154
+ return result ?? { ...row, id };
1155
+ }
1156
+ async update(table, pk, changes) {
1157
+ this.assertInitialized();
1158
+ return this.lattice.updateReturning(table, pk, changes);
1159
+ }
1160
+ async delete(table, pk) {
1161
+ this.assertInitialized();
1162
+ await this.lattice.delete(table, pk);
1163
+ }
1164
+ /**
1165
+ * Get a single row by primary key.
1166
+ * Returns undefined if not found (Lattice returns null).
1167
+ */
1168
+ async get(table, pk) {
1169
+ this.assertInitialized();
1170
+ const result = await this.lattice.get(table, pk);
1171
+ return result ?? void 0;
1172
+ }
1173
+ async query(table, opts) {
1174
+ this.assertInitialized();
1175
+ return this.lattice.query(table, opts);
1176
+ }
1177
+ async count(table, opts) {
1178
+ this.assertInitialized();
1179
+ return this.lattice.count(table, opts);
1180
+ }
1181
+ // --- Junctions ------------------------------------------------------
1182
+ async link(junctionTable, row) {
1183
+ this.assertInitialized();
1184
+ await this.lattice.link(junctionTable, row);
1185
+ }
1186
+ async unlink(junctionTable, row) {
1187
+ this.assertInitialized();
1188
+ await this.lattice.unlink(junctionTable, row);
1189
+ }
1190
+ // --- Migrations -----------------------------------------------------
1191
+ async migrate(migrations) {
1192
+ this.assertInitialized();
1193
+ await this.lattice.migrate(migrations);
1194
+ }
1195
+ // --- Seed -----------------------------------------------------------
1196
+ async seed(items) {
1197
+ this.assertInitialized();
1198
+ for (const item of items) {
1199
+ const naturalKey = Array.isArray(item.naturalKey) ? item.naturalKey[0] : item.naturalKey ?? "id";
1200
+ await this.lattice.seed({
1201
+ table: item.table,
1202
+ data: item.rows,
1203
+ naturalKey,
1204
+ softDeleteMissing: item.softDeleteMissing
1205
+ });
1206
+ if (item.junctions) {
1207
+ for (const junc of item.junctions) {
1208
+ for (const linkRow of junc.items) {
1209
+ await this.lattice.link(junc.table, linkRow);
1210
+ }
1211
+ }
1212
+ }
1213
+ }
1214
+ }
1215
+ // --- Rendering ------------------------------------------------------
1216
+ async render() {
1217
+ this.assertInitialized();
1218
+ if (this.outputDir) {
1219
+ await this.lattice.render(this.outputDir);
1220
+ }
1221
+ }
1222
+ async reconcile() {
1223
+ this.assertInitialized();
1224
+ if (this.outputDir) {
1225
+ await this.lattice.reconcile(this.outputDir);
1226
+ }
1227
+ }
1228
+ // --- Schema introspection ------------------------------------------
1229
+ tableInfo(table) {
1230
+ this.assertInitialized();
1231
+ return this.lattice.db.pragma(`table_info(${table})`);
1232
+ }
1233
+ // --- Lifecycle ------------------------------------------------------
1234
+ close() {
1235
+ this.lattice.close();
1236
+ this._initialized = false;
1237
+ }
1238
+ on(event, handler) {
1239
+ this.hooks?.register(event, handler);
1240
+ }
1241
+ };
1242
+
1243
+ // src/core/data/core-schema.ts
1244
+ function defineCoreTables(db) {
1245
+ db.define("agents", {
1246
+ columns: {
1247
+ id: "TEXT PRIMARY KEY",
1248
+ slug: "TEXT UNIQUE NOT NULL",
1249
+ name: "TEXT NOT NULL",
1250
+ role: "TEXT NOT NULL DEFAULT 'general'",
1251
+ status: "TEXT NOT NULL DEFAULT 'idle'",
1252
+ adapter: "TEXT NOT NULL",
1253
+ adapter_config: "TEXT NOT NULL DEFAULT '{}'",
1254
+ heartbeat_config: "TEXT NOT NULL DEFAULT '{}'",
1255
+ budget_monthly_cents: "INTEGER NOT NULL DEFAULT 0",
1256
+ spent_monthly_cents: "INTEGER NOT NULL DEFAULT 0",
1257
+ reports_to: "TEXT",
1258
+ cwd: "TEXT",
1259
+ instructions_file: "TEXT",
1260
+ created_at: "TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP",
1261
+ updated_at: "TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP",
1262
+ deleted_at: "TEXT"
1263
+ }
1264
+ });
1265
+ db.define("tasks", {
1266
+ columns: {
1267
+ id: "TEXT PRIMARY KEY",
1268
+ title: "TEXT NOT NULL",
1269
+ description: "TEXT",
1270
+ status: "TEXT NOT NULL DEFAULT 'backlog'",
1271
+ priority: "INTEGER NOT NULL DEFAULT 5",
1272
+ assignee_id: "TEXT",
1273
+ created_by: "TEXT",
1274
+ parent_id: "TEXT",
1275
+ chain_origin_id: "TEXT",
1276
+ chain_depth: "INTEGER NOT NULL DEFAULT 0",
1277
+ workflow_run_id: "TEXT",
1278
+ workflow_step_id: "TEXT",
1279
+ tags: "TEXT NOT NULL DEFAULT '[]'",
1280
+ context: "TEXT",
1281
+ result: "TEXT",
1282
+ retry_count: "INTEGER NOT NULL DEFAULT 0",
1283
+ max_retries: "INTEGER NOT NULL DEFAULT 0",
1284
+ next_retry_at: "TEXT",
1285
+ followup_agent_id: "TEXT",
1286
+ followup_template: "TEXT",
1287
+ execution_run_id: "TEXT",
1288
+ created_at: "TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP",
1289
+ updated_at: "TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP",
1290
+ deleted_at: "TEXT"
1291
+ },
1292
+ tableConstraints: [
1293
+ "CREATE INDEX IF NOT EXISTS idx_tasks_status_assignee ON tasks(status, assignee_id)",
1294
+ "CREATE INDEX IF NOT EXISTS idx_tasks_chain_origin ON tasks(chain_origin_id)"
1295
+ ]
1296
+ });
1297
+ db.define("runs", {
1298
+ columns: {
1299
+ id: "TEXT PRIMARY KEY",
1300
+ task_id: "TEXT NOT NULL",
1301
+ agent_id: "TEXT NOT NULL",
1302
+ status: "TEXT NOT NULL DEFAULT 'queued'",
1303
+ model: "TEXT",
1304
+ adapter: "TEXT",
1305
+ log_path: "TEXT",
1306
+ exit_code: "INTEGER",
1307
+ error_message: "TEXT",
1308
+ cost_cents: "INTEGER NOT NULL DEFAULT 0",
1309
+ input_tokens: "INTEGER NOT NULL DEFAULT 0",
1310
+ output_tokens: "INTEGER NOT NULL DEFAULT 0",
1311
+ started_at: "TEXT",
1312
+ completed_at: "TEXT",
1313
+ created_at: "TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP"
1314
+ },
1315
+ tableConstraints: [
1316
+ "CREATE INDEX IF NOT EXISTS idx_runs_task_id ON runs(task_id)",
1317
+ "CREATE INDEX IF NOT EXISTS idx_runs_agent_id ON runs(agent_id)",
1318
+ "CREATE INDEX IF NOT EXISTS idx_runs_status ON runs(status)"
1319
+ ]
1320
+ });
1321
+ db.define("wakeups", {
1322
+ columns: {
1323
+ id: "TEXT PRIMARY KEY",
1324
+ agent_id: "TEXT NOT NULL",
1325
+ scheduled_at: "TEXT NOT NULL",
1326
+ fired_at: "TEXT",
1327
+ run_id: "TEXT",
1328
+ context: "TEXT",
1329
+ created_at: "TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP"
1330
+ },
1331
+ tableConstraints: [
1332
+ "CREATE INDEX IF NOT EXISTS idx_wakeups_agent_scheduled ON wakeups(agent_id, scheduled_at)"
1333
+ ]
1334
+ });
1335
+ db.define("sessions", {
1336
+ columns: {
1337
+ id: "TEXT PRIMARY KEY",
1338
+ agent_id: "TEXT NOT NULL",
1339
+ channel: "TEXT NOT NULL",
1340
+ peer_id: "TEXT NOT NULL",
1341
+ last_message_at: "TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP",
1342
+ message_count: "INTEGER NOT NULL DEFAULT 0",
1343
+ context: "TEXT NOT NULL DEFAULT '{}'",
1344
+ created_at: "TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP",
1345
+ expires_at: "TEXT"
1346
+ },
1347
+ tableConstraints: [
1348
+ "CREATE UNIQUE INDEX IF NOT EXISTS idx_sessions_agent_channel_peer ON sessions(agent_id, channel, peer_id)"
1349
+ ]
1350
+ });
1351
+ db.define("skills", {
1352
+ columns: {
1353
+ id: "TEXT PRIMARY KEY",
1354
+ slug: "TEXT UNIQUE NOT NULL",
1355
+ name: "TEXT NOT NULL",
1356
+ description: "TEXT",
1357
+ category: "TEXT",
1358
+ definition: "TEXT",
1359
+ created_at: "TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP",
1360
+ updated_at: "TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP",
1361
+ deleted_at: "TEXT"
1362
+ }
1363
+ });
1364
+ db.define("agent_skills", {
1365
+ columns: {
1366
+ agent_id: "TEXT NOT NULL",
1367
+ skill_id: "TEXT NOT NULL",
1368
+ granted_at: "TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP"
1369
+ },
1370
+ primaryKey: ["agent_id", "skill_id"],
1371
+ tableConstraints: [
1372
+ "FOREIGN KEY (agent_id) REFERENCES agents(id)",
1373
+ "FOREIGN KEY (skill_id) REFERENCES skills(id)"
1374
+ ]
1375
+ });
1376
+ db.define("cost_events", {
1377
+ columns: {
1378
+ id: "TEXT PRIMARY KEY",
1379
+ agent_id: "TEXT",
1380
+ run_id: "TEXT",
1381
+ model: "TEXT NOT NULL",
1382
+ provider: "TEXT NOT NULL",
1383
+ input_tokens: "INTEGER NOT NULL DEFAULT 0",
1384
+ output_tokens: "INTEGER NOT NULL DEFAULT 0",
1385
+ cost_cents: "INTEGER NOT NULL DEFAULT 0",
1386
+ recorded_at: "TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP"
1387
+ },
1388
+ tableConstraints: [
1389
+ "CREATE INDEX IF NOT EXISTS idx_cost_events_agent ON cost_events(agent_id, recorded_at)"
1390
+ ]
1391
+ });
1392
+ db.define("budget_policies", {
1393
+ columns: {
1394
+ id: "TEXT PRIMARY KEY",
1395
+ agent_id: "TEXT",
1396
+ scope: "TEXT NOT NULL DEFAULT 'global'",
1397
+ monthly_limit_cents: "INTEGER NOT NULL",
1398
+ warn_percent: "INTEGER NOT NULL DEFAULT 80",
1399
+ created_at: "TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP",
1400
+ updated_at: "TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP"
1401
+ }
1402
+ });
1403
+ db.define("activity_log", {
1404
+ columns: {
1405
+ id: "TEXT PRIMARY KEY",
1406
+ agent_id: "TEXT",
1407
+ event_type: "TEXT NOT NULL",
1408
+ payload: "TEXT NOT NULL DEFAULT '{}'",
1409
+ recorded_at: "TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP"
1410
+ },
1411
+ tableConstraints: [
1412
+ "CREATE INDEX IF NOT EXISTS idx_activity_log_recorded ON activity_log(recorded_at)",
1413
+ "CREATE INDEX IF NOT EXISTS idx_activity_log_agent ON activity_log(agent_id, recorded_at)"
1414
+ ]
1415
+ });
1416
+ db.define("notifications", {
1417
+ columns: {
1418
+ id: "TEXT PRIMARY KEY",
1419
+ channel: "TEXT NOT NULL",
1420
+ recipient_id: "TEXT NOT NULL",
1421
+ message: "TEXT NOT NULL",
1422
+ status: "TEXT NOT NULL DEFAULT 'pending'",
1423
+ retries: "INTEGER NOT NULL DEFAULT 0",
1424
+ created_at: "TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP",
1425
+ sent_at: "TEXT",
1426
+ error: "TEXT"
1427
+ },
1428
+ tableConstraints: [
1429
+ "CREATE INDEX IF NOT EXISTS idx_notifications_pending ON notifications(status, created_at)"
1430
+ ]
1431
+ });
1432
+ db.define("config_revisions", {
1433
+ columns: {
1434
+ id: "TEXT PRIMARY KEY",
1435
+ version: "INTEGER NOT NULL",
1436
+ config_yaml: "TEXT NOT NULL",
1437
+ applied_by: "TEXT",
1438
+ applied_at: "TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP",
1439
+ notes: "TEXT"
1440
+ }
1441
+ });
1442
+ db.define("workflows", {
1443
+ columns: {
1444
+ id: "TEXT PRIMARY KEY",
1445
+ slug: "TEXT UNIQUE NOT NULL",
1446
+ name: "TEXT NOT NULL",
1447
+ description: "TEXT",
1448
+ definition: "TEXT NOT NULL DEFAULT '{}'",
1449
+ created_at: "TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP",
1450
+ updated_at: "TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP",
1451
+ deleted_at: "TEXT"
1452
+ }
1453
+ });
1454
+ db.define("workflow_runs", {
1455
+ columns: {
1456
+ id: "TEXT PRIMARY KEY",
1457
+ workflow_id: "TEXT NOT NULL",
1458
+ trigger_task_id: "TEXT",
1459
+ status: "TEXT NOT NULL DEFAULT 'running'",
1460
+ current_step: "TEXT",
1461
+ step_results: "TEXT NOT NULL DEFAULT '{}'",
1462
+ error: "TEXT",
1463
+ started_at: "TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP",
1464
+ completed_at: "TEXT"
1465
+ },
1466
+ tableConstraints: [
1467
+ "CREATE INDEX IF NOT EXISTS idx_workflow_runs_workflow ON workflow_runs(workflow_id, status)"
1468
+ ]
1469
+ });
1470
+ db.define("update_history", {
1471
+ columns: {
1472
+ id: "TEXT PRIMARY KEY",
1473
+ from_version: "TEXT NOT NULL",
1474
+ to_version: "TEXT NOT NULL",
1475
+ status: "TEXT NOT NULL DEFAULT 'pending'",
1476
+ migration_log: "TEXT",
1477
+ applied_at: "TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP",
1478
+ rolled_back_at: "TEXT"
1479
+ }
1480
+ });
1481
+ }
1482
+
1483
+ // src/core/data/core-migrations.ts
1484
+ var CORE_MIGRATIONS = [
1485
+ {
1486
+ version: "001_initial_schema",
1487
+ sql: `-- Initial schema is applied via DataStore.define() + init().
1488
+ -- This migration is a no-op placeholder for version tracking.
1489
+ SELECT 1;`
1490
+ },
1491
+ {
1492
+ version: "002_activity_log_indexes",
1493
+ sql: `CREATE INDEX IF NOT EXISTS idx_activity_log_type ON activity_log(event_type, recorded_at);`
1494
+ },
1495
+ {
1496
+ version: "003_runs_cost_index",
1497
+ sql: `CREATE INDEX IF NOT EXISTS idx_runs_cost ON runs(agent_id, completed_at) WHERE cost_cents > 0;`
1498
+ }
1499
+ ];
1500
+
1501
+ // src/core/security/sanitizer.ts
1502
+ import { Buffer as Buffer2 } from "buffer";
1503
+ var DEFAULT_FIELD_LIMIT = 65535;
1504
+ var DEFAULT_SUFFIX = "[truncated]";
1505
+ function sanitize(row, opts) {
1506
+ const limits = opts?.fieldLengthLimits ?? {};
1507
+ const suffix = opts?.truncateSuffix ?? DEFAULT_SUFFIX;
1508
+ const result = {};
1509
+ for (const [col, val] of Object.entries(row)) {
1510
+ if (typeof val !== "string") {
1511
+ result[col] = val;
1512
+ continue;
1513
+ }
1514
+ let s = val.replace(/\x00/g, "");
1515
+ s = s.replace(/[\x01-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "");
1516
+ const limit = limits[col] ?? DEFAULT_FIELD_LIMIT;
1517
+ if (Buffer2.byteLength(s) > limit) {
1518
+ const suffixBytes = Buffer2.byteLength(suffix);
1519
+ const maxContentBytes = limit - suffixBytes;
1520
+ const buf = Buffer2.from(s);
1521
+ s = buf.slice(0, maxContentBytes).toString() + suffix;
1522
+ }
1523
+ result[col] = s;
1524
+ }
1525
+ return result;
1526
+ }
1527
+
1528
+ // src/core/security/column-validator.ts
1529
+ var ColumnValidatorImpl = class {
1530
+ db;
1531
+ constructor(db) {
1532
+ this.db = db;
1533
+ }
1534
+ getValidColumns(table) {
1535
+ const rows = this.db.tableInfo(table);
1536
+ return new Set(rows.map((r) => r.name));
1537
+ }
1538
+ validateWrite(table, row) {
1539
+ const valid = this.getValidColumns(table);
1540
+ const result = {};
1541
+ for (const [col, val] of Object.entries(row)) {
1542
+ if (valid.has(col)) {
1543
+ result[col] = val;
1544
+ }
1545
+ }
1546
+ return result;
1547
+ }
1548
+ validateRead(table, columns) {
1549
+ const valid = this.getValidColumns(table);
1550
+ for (const col of columns) {
1551
+ if (!valid.has(col)) {
1552
+ throw new Error(`Unknown column: ${col} in table ${table}`);
1553
+ }
1554
+ }
1555
+ }
1556
+ invalidateCache(_table) {
1557
+ }
1558
+ };
1559
+
1560
+ // src/core/security/audit.ts
1561
+ var AuditEmitter = class {
1562
+ hooks;
1563
+ auditTables;
1564
+ constructor(hooks, opts) {
1565
+ this.hooks = hooks;
1566
+ this.auditTables = opts?.auditTables ?? [];
1567
+ }
1568
+ shouldAudit(table) {
1569
+ return this.auditTables.includes("*") || this.auditTables.includes(table);
1570
+ }
1571
+ emit(event) {
1572
+ try {
1573
+ const context = event;
1574
+ if (typeof this.hooks.emitSync === "function") {
1575
+ this.hooks.emitSync("audit", context);
1576
+ } else {
1577
+ void this.hooks.emit("audit", context);
1578
+ }
1579
+ } catch {
1580
+ }
1581
+ }
1582
+ };
1583
+
1584
+ // src/core/update/version-utils.ts
1585
+ function parseVersion(v) {
1586
+ const cleaned = v.replace(/^v/, "").split("-")[0] ?? v;
1587
+ const parts = cleaned.split(".");
1588
+ const major = parseInt(parts[0] ?? "0", 10) || 0;
1589
+ const minor = parseInt(parts[1] ?? "0", 10) || 0;
1590
+ const patch = parseInt(parts[2] ?? "0", 10) || 0;
1591
+ return [major, minor, patch];
1592
+ }
1593
+ function compareVersions(a, b) {
1594
+ const [aMaj, aMin, aPat] = parseVersion(a);
1595
+ const [bMaj, bMin, bPat] = parseVersion(b);
1596
+ if (aMaj !== bMaj) return aMaj < bMaj ? -1 : 1;
1597
+ if (aMin !== bMin) return aMin < bMin ? -1 : 1;
1598
+ if (aPat !== bPat) return aPat < bPat ? -1 : 1;
1599
+ return 0;
1600
+ }
1601
+ function classifyUpdate(from, to) {
1602
+ const [fMaj, fMin] = parseVersion(from);
1603
+ const [tMaj, tMin] = parseVersion(to);
1604
+ if (compareVersions(from, to) === 0) return "none";
1605
+ if (tMaj !== fMaj) return "major";
1606
+ if (tMin !== fMin) return "minor";
1607
+ return "patch";
1608
+ }
1609
+
1610
+ // src/core/update/update-checker.ts
1611
+ import { readFileSync as readFileSync2, readdirSync } from "fs";
1612
+ import { join as join3 } from "path";
1613
+ var UpdateChecker = class {
1614
+ constructor(nodeModulesPath, opts) {
1615
+ this.nodeModulesPath = nodeModulesPath;
1616
+ this.opts = opts;
1617
+ this.fetchFn = opts?.fetch ?? globalThis.fetch;
1618
+ }
1619
+ nodeModulesPath;
1620
+ opts;
1621
+ fetchFn;
1622
+ getInstalledPackages() {
1623
+ try {
1624
+ const botinaboxPath = join3(this.nodeModulesPath, "@botinabox");
1625
+ const entries = readdirSync(botinaboxPath, { withFileTypes: true });
1626
+ return entries.filter((e) => e.isDirectory()).map((e) => e.name);
1627
+ } catch {
1628
+ return [];
1629
+ }
1630
+ }
1631
+ async check(packageNames) {
1632
+ const installed = packageNames ?? this.getInstalledPackages();
1633
+ const updates = [];
1634
+ for (const pkg of installed) {
1635
+ try {
1636
+ const pkgJsonPath = join3(this.nodeModulesPath, "@botinabox", pkg, "package.json");
1637
+ const pkgJson = JSON.parse(readFileSync2(pkgJsonPath, "utf-8"));
1638
+ const installedVersion = pkgJson.version ?? "0.0.0";
1639
+ const response = await this.fetchFn(
1640
+ `https://registry.npmjs.org/@botinabox/${pkg}`
1641
+ );
1642
+ if (!response.ok) continue;
1643
+ const data = await response.json();
1644
+ const latestVersion = data["dist-tags"]?.["latest"];
1645
+ if (!latestVersion) continue;
1646
+ if (compareVersions(latestVersion, installedVersion) > 0) {
1647
+ const updateType = classifyUpdate(installedVersion, latestVersion);
1648
+ if (updateType !== "none") {
1649
+ updates.push({
1650
+ name: `@botinabox/${pkg}`,
1651
+ installedVersion,
1652
+ latestVersion,
1653
+ updateType
1654
+ });
1655
+ }
1656
+ }
1657
+ } catch {
1658
+ continue;
1659
+ }
1660
+ }
1661
+ return {
1662
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
1663
+ packages: updates,
1664
+ hasUpdates: updates.length > 0
1665
+ };
1666
+ }
1667
+ };
1668
+
1669
+ // src/core/update/backup-manager.ts
1670
+ import { copyFileSync, mkdirSync } from "fs";
1671
+ import { join as join4 } from "path";
1672
+ import { rmSync } from "fs";
1673
+ var BackupManager = class {
1674
+ constructor(projectRoot) {
1675
+ this.projectRoot = projectRoot;
1676
+ }
1677
+ projectRoot;
1678
+ async backup() {
1679
+ const backupDir = join4(this.projectRoot, ".botinabox-backup");
1680
+ mkdirSync(backupDir, { recursive: true });
1681
+ const src = join4(this.projectRoot, "pnpm-lock.yaml");
1682
+ const dest = join4(backupDir, "pnpm-lock.yaml.bak");
1683
+ copyFileSync(src, dest);
1684
+ return dest;
1685
+ }
1686
+ async restore(backupPath) {
1687
+ const dest = join4(this.projectRoot, "pnpm-lock.yaml");
1688
+ copyFileSync(backupPath, dest);
1689
+ }
1690
+ async cleanup(backupPath) {
1691
+ rmSync(backupPath, { force: true });
1692
+ }
1693
+ };
1694
+
1695
+ // src/core/update/update-manager.ts
1696
+ import { execSync } from "child_process";
1697
+ var UpdateManager = class {
1698
+ constructor(checker, db, hooks, opts) {
1699
+ this.checker = checker;
1700
+ this.db = db;
1701
+ this.hooks = hooks;
1702
+ this.opts = opts;
1703
+ this.backupManager = new BackupManager(opts?.projectRoot ?? process.cwd());
1704
+ }
1705
+ checker;
1706
+ db;
1707
+ hooks;
1708
+ opts;
1709
+ backupManager;
1710
+ async checkAndNotify() {
1711
+ const manifest = await this.checker.check();
1712
+ if (manifest.hasUpdates) {
1713
+ await this.hooks.emit("update.available", { manifest });
1714
+ }
1715
+ return manifest;
1716
+ }
1717
+ async applyUpdates(updates) {
1718
+ const filtered = this.filterByPolicy(updates);
1719
+ if (filtered.length === 0) return;
1720
+ if (!this.isInMaintenanceWindow()) {
1721
+ await this.hooks.emit("update.deferred", { reason: "outside maintenance window" });
1722
+ return;
1723
+ }
1724
+ let backupPath;
1725
+ const historyIds = [];
1726
+ try {
1727
+ backupPath = await this.backupManager.backup();
1728
+ for (const update of filtered) {
1729
+ const row = await this.db.insert("update_history", {
1730
+ from_version: update.installedVersion,
1731
+ to_version: update.latestVersion,
1732
+ status: "pending"
1733
+ });
1734
+ historyIds.push(row["id"]);
1735
+ }
1736
+ execSync("pnpm install", {
1737
+ cwd: this.opts?.projectRoot ?? process.cwd(),
1738
+ stdio: "ignore"
1739
+ });
1740
+ for (const id of historyIds) {
1741
+ await this.db.update("update_history", { id }, { status: "succeeded" });
1742
+ }
1743
+ await this.backupManager.cleanup(backupPath);
1744
+ await this.hooks.emit("update.completed", { updates: filtered });
1745
+ } catch (err) {
1746
+ for (const id of historyIds) {
1747
+ await this.db.update("update_history", { id }, {
1748
+ status: "failed",
1749
+ migration_log: String(err)
1750
+ });
1751
+ }
1752
+ if (backupPath) {
1753
+ try {
1754
+ await this.backupManager.restore(backupPath);
1755
+ await this.backupManager.cleanup(backupPath);
1756
+ } catch {
1757
+ }
1758
+ }
1759
+ await this.hooks.emit("update.failed", { updates: filtered, error: String(err) });
1760
+ }
1761
+ }
1762
+ isInMaintenanceWindow() {
1763
+ const window = this.opts?.maintenanceWindow;
1764
+ if (!window) return true;
1765
+ const nowHour = (/* @__PURE__ */ new Date()).getUTCHours();
1766
+ const { utcHourStart, utcHourEnd } = window;
1767
+ if (utcHourStart <= utcHourEnd) {
1768
+ return nowHour >= utcHourStart && nowHour < utcHourEnd;
1769
+ } else {
1770
+ return nowHour >= utcHourStart || nowHour < utcHourEnd;
1771
+ }
1772
+ }
1773
+ filterByPolicy(updates) {
1774
+ const policy = this.opts?.policy ?? "notify";
1775
+ switch (policy) {
1776
+ case "auto-all":
1777
+ return updates;
1778
+ case "auto-compatible":
1779
+ return updates.filter((u) => u.updateType !== "major");
1780
+ case "auto-patch":
1781
+ return updates.filter((u) => u.updateType === "patch");
1782
+ case "notify":
1783
+ case "manual":
1784
+ default:
1785
+ return [];
1786
+ }
1787
+ }
1788
+ };
1789
+
1790
+ // src/core/update/migration-hooks.ts
1791
+ async function runPackageMigrations(db, migrations) {
1792
+ await db.migrate(
1793
+ migrations.map((m) => ({
1794
+ version: `${m.package}:${m.version}`,
1795
+ sql: m.sql
1796
+ }))
1797
+ );
1798
+ }
1799
+
1800
+ // src/core/orchestrator/config-revisions.ts
1801
+ async function createConfigRevision(db, agentId, before, after) {
1802
+ const existing = await db.query("config_revisions", {
1803
+ where: { notes: agentId }
1804
+ });
1805
+ const version = existing.length + 1;
1806
+ await db.insert("config_revisions", {
1807
+ version,
1808
+ config_yaml: JSON.stringify({ agentId, before, after }),
1809
+ applied_by: agentId,
1810
+ notes: agentId
1811
+ });
1812
+ }
1813
+
1814
+ // src/core/orchestrator/agent-registry.ts
1815
+ var VALID_TRANSITIONS = {
1816
+ idle: ["running", "paused"],
1817
+ running: ["idle", "paused", "terminated"],
1818
+ paused: ["idle", "terminated"],
1819
+ terminated: []
1820
+ };
1821
+ var WRITE_ACTIVITY_LOG_STATUSES = /* @__PURE__ */ new Set(["paused", "terminated"]);
1822
+ var AgentRegistry = class {
1823
+ constructor(db, hooks) {
1824
+ this.db = db;
1825
+ this.hooks = hooks;
1826
+ }
1827
+ db;
1828
+ hooks;
1829
+ async register(agent, opts) {
1830
+ if (!agent.slug || !agent.name || !agent.adapter) {
1831
+ throw new Error("Agent must have slug, name, and adapter");
1832
+ }
1833
+ if (opts?.actorAgentId) {
1834
+ const actor = await this.db.get("agents", { id: opts.actorAgentId });
1835
+ if (!actor) throw new Error(`Actor agent not found: ${opts.actorAgentId}`);
1836
+ let canCreate = false;
1837
+ try {
1838
+ const cfg = JSON.parse(actor["adapter_config"] ?? "{}");
1839
+ canCreate = cfg["canCreateAgents"] === true;
1840
+ } catch {
1841
+ canCreate = false;
1842
+ }
1843
+ if (!canCreate) {
1844
+ throw new Error(`Agent ${opts.actorAgentId} does not have permission to create agents`);
1845
+ }
1846
+ }
1847
+ const row = await this.db.insert("agents", {
1848
+ slug: agent.slug,
1849
+ name: agent.name,
1850
+ adapter: agent.adapter,
1851
+ role: agent.role ?? "general",
1852
+ ...Object.fromEntries(
1853
+ Object.entries(agent).filter(([k]) => !["slug", "name", "adapter", "role"].includes(k))
1854
+ )
1855
+ });
1856
+ const newAgentId = row["id"];
1857
+ if (opts?.actorAgentId) {
1858
+ await this.db.insert("activity_log", {
1859
+ agent_id: opts.actorAgentId,
1860
+ event_type: "agent_created_by_agent",
1861
+ payload: JSON.stringify({
1862
+ actorAgentId: opts.actorAgentId,
1863
+ newAgentId,
1864
+ slug: agent.slug
1865
+ })
1866
+ });
1867
+ }
1868
+ await this.hooks.emit("agent.created", { agentId: newAgentId, slug: agent.slug });
1869
+ return newAgentId;
1870
+ }
1871
+ async getById(id) {
1872
+ return await this.db.get("agents", { id }) ?? void 0;
1873
+ }
1874
+ async getBySlug(slug) {
1875
+ const rows = await this.db.query("agents", { where: { slug } });
1876
+ return rows[0] ?? void 0;
1877
+ }
1878
+ async list(filter) {
1879
+ const where = {};
1880
+ if (filter?.status) where["status"] = filter.status;
1881
+ if (filter?.role) where["role"] = filter.role;
1882
+ return await this.db.query("agents", Object.keys(where).length ? { where } : void 0);
1883
+ }
1884
+ async update(id, changes) {
1885
+ const existing = await this.db.get("agents", { id });
1886
+ if (!existing) throw new Error(`Agent not found: ${id}`);
1887
+ const before = { ...existing };
1888
+ await createConfigRevision(this.db, id, before, { ...before, ...changes });
1889
+ await this.db.update("agents", { id }, {
1890
+ ...changes,
1891
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
1892
+ });
1893
+ }
1894
+ async setStatus(id, newStatus) {
1895
+ const agent = await this.db.get("agents", { id });
1896
+ if (!agent) throw new Error(`Agent not found: ${id}`);
1897
+ const currentStatus = agent["status"];
1898
+ const allowed = VALID_TRANSITIONS[currentStatus] ?? [];
1899
+ if (!allowed.includes(newStatus)) {
1900
+ throw new Error(
1901
+ `Invalid status transition: ${currentStatus} \u2192 ${newStatus}`
1902
+ );
1903
+ }
1904
+ await this.db.update("agents", { id }, {
1905
+ status: newStatus,
1906
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
1907
+ });
1908
+ if (WRITE_ACTIVITY_LOG_STATUSES.has(newStatus)) {
1909
+ await this.db.insert("activity_log", {
1910
+ agent_id: id,
1911
+ event_type: `agent.${newStatus}`,
1912
+ payload: JSON.stringify({ agentId: id, status: newStatus })
1913
+ });
1914
+ }
1915
+ }
1916
+ async terminate(id) {
1917
+ const agent = await this.db.get("agents", { id });
1918
+ if (!agent) throw new Error(`Agent not found: ${id}`);
1919
+ const currentStatus = agent["status"];
1920
+ if (currentStatus === "terminated") {
1921
+ return;
1922
+ }
1923
+ await this.db.update("agents", { id }, {
1924
+ status: "terminated",
1925
+ deleted_at: (/* @__PURE__ */ new Date()).toISOString(),
1926
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
1927
+ });
1928
+ await this.db.insert("activity_log", {
1929
+ agent_id: id,
1930
+ event_type: "agent.terminated",
1931
+ payload: JSON.stringify({ agentId: id })
1932
+ });
1933
+ }
1934
+ async seedFromConfig(agentConfigs) {
1935
+ for (const config of agentConfigs) {
1936
+ const existing = await this.getBySlug(config.slug);
1937
+ if (existing) continue;
1938
+ await this.register(config);
1939
+ }
1940
+ }
1941
+ };
1942
+
1943
+ // src/core/orchestrator/chain-guard.ts
1944
+ var MAX_CHAIN_DEPTH = 5;
1945
+ function checkChainDepth(depth, max = MAX_CHAIN_DEPTH) {
1946
+ if (depth > max) {
1947
+ throw new Error(`Chain depth limit exceeded (max ${max})`);
1948
+ }
1949
+ }
1950
+ function buildChainOrigin(parentTaskId, parentOriginId, parentDepth) {
1951
+ if (!parentTaskId) {
1952
+ return { chain_depth: 0 };
1953
+ }
1954
+ return {
1955
+ chain_origin_id: parentOriginId ?? parentTaskId,
1956
+ chain_depth: (parentDepth ?? 0) + 1
1957
+ };
1958
+ }
1959
+
1960
+ // src/core/orchestrator/task-queue.ts
1961
+ var TaskQueue = class {
1962
+ constructor(db, hooks, config) {
1963
+ this.db = db;
1964
+ this.hooks = hooks;
1965
+ this.config = config;
1966
+ this.pollIntervalMs = config?.pollIntervalMs ?? 3e4;
1967
+ this.staleThresholdMs = config?.staleThresholdMs ?? 5 * 60 * 1e3;
1968
+ }
1969
+ db;
1970
+ hooks;
1971
+ config;
1972
+ pollTimer = null;
1973
+ pollIntervalMs;
1974
+ staleThresholdMs;
1975
+ async create(task) {
1976
+ const depth = task.chain_depth ?? 0;
1977
+ checkChainDepth(depth, MAX_CHAIN_DEPTH);
1978
+ const row = await this.db.insert("tasks", {
1979
+ title: task.title,
1980
+ description: task.description,
1981
+ assignee_id: task.assignee_id,
1982
+ priority: task.priority ?? 5,
1983
+ chain_depth: depth,
1984
+ chain_origin_id: task.chain_origin_id,
1985
+ status: "todo",
1986
+ ...Object.fromEntries(
1987
+ Object.entries(task).filter(
1988
+ ([k]) => !["title", "description", "assignee_id", "priority", "chain_depth", "chain_origin_id", "status"].includes(k)
1989
+ )
1990
+ )
1991
+ });
1992
+ await this.hooks.emit("task.created", { taskId: row["id"], title: task.title });
1993
+ return row["id"];
1994
+ }
1995
+ async update(id, changes) {
1996
+ await this.db.update("tasks", { id }, {
1997
+ ...changes,
1998
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
1999
+ });
2000
+ }
2001
+ async get(id) {
2002
+ return await this.db.get("tasks", { id }) ?? void 0;
2003
+ }
2004
+ async list(filter) {
2005
+ const where = {};
2006
+ if (filter?.status) where["status"] = filter.status;
2007
+ if (filter?.assignee_id) where["assignee_id"] = filter.assignee_id;
2008
+ return await this.db.query("tasks", Object.keys(where).length ? { where } : void 0);
2009
+ }
2010
+ startPolling() {
2011
+ if (this.pollTimer) return;
2012
+ this.pollTimer = setInterval(() => {
2013
+ void this.poll();
2014
+ }, this.pollIntervalMs);
2015
+ }
2016
+ stopPolling() {
2017
+ if (this.pollTimer) {
2018
+ clearInterval(this.pollTimer);
2019
+ this.pollTimer = null;
2020
+ }
2021
+ }
2022
+ async poll() {
2023
+ const todoTasks = (await this.db.query("tasks", { where: { status: "todo" } })).filter((t) => t["assignee_id"] != null);
2024
+ const activeTasks = new Set(
2025
+ (await this.db.query("runs", { where: { status: "running" } })).map((r) => r["task_id"])
2026
+ );
2027
+ const eligible = todoTasks.filter((t) => !activeTasks.has(t["id"])).sort((a, b) => a["priority"] - b["priority"]);
2028
+ for (const task of eligible) {
2029
+ await this.hooks.emit("agent.wakeup", {
2030
+ agentId: task["assignee_id"],
2031
+ taskId: task["id"],
2032
+ source: "poll"
2033
+ });
2034
+ }
2035
+ }
2036
+ };
2037
+
2038
+ // src/core/orchestrator/template-interpolate.ts
2039
+ function interpolate(template, context) {
2040
+ return template.replace(/\{\{([^}]+)\}\}/g, (_match, path) => {
2041
+ const parts = path.trim().split(".");
2042
+ let value = context;
2043
+ for (const part of parts) {
2044
+ if (value == null || typeof value !== "object") return "";
2045
+ value = value[part];
2046
+ }
2047
+ if (value == null) return "";
2048
+ return String(value);
2049
+ });
2050
+ }
2051
+
2052
+ // src/core/orchestrator/run-manager.ts
2053
+ var DEFAULT_STALE_THRESHOLD_MS = 30 * 60 * 1e3;
2054
+ var BASE_BACKOFF_MS = 5e3;
2055
+ var DEFAULT_MAX_BACKOFF_MS = 5 * 60 * 1e3;
2056
+ var RunManager = class {
2057
+ constructor(db, hooks, config) {
2058
+ this.db = db;
2059
+ this.hooks = hooks;
2060
+ this.config = config;
2061
+ this.staleThresholdMs = config?.staleThresholdMs ?? DEFAULT_STALE_THRESHOLD_MS;
2062
+ }
2063
+ db;
2064
+ hooks;
2065
+ config;
2066
+ locks = /* @__PURE__ */ new Map();
2067
+ // agentId → runId
2068
+ orphanTimer = null;
2069
+ staleThresholdMs;
2070
+ isLocked(agentId) {
2071
+ return this.locks.has(agentId);
2072
+ }
2073
+ async startRun(agentId, taskId, adapter) {
2074
+ if (this.locks.has(agentId)) {
2075
+ throw new Error(`Agent already has an active run`);
2076
+ }
2077
+ const row = await this.db.insert("runs", {
2078
+ agent_id: agentId,
2079
+ task_id: taskId,
2080
+ adapter,
2081
+ status: "running",
2082
+ started_at: (/* @__PURE__ */ new Date()).toISOString()
2083
+ });
2084
+ const runId = row["id"];
2085
+ this.locks.set(agentId, runId);
2086
+ return runId;
2087
+ }
2088
+ async finishRun(runId, result) {
2089
+ const run = await this.db.get("runs", { id: runId });
2090
+ if (!run) throw new Error(`Run not found: ${runId}`);
2091
+ const succeeded = result.exitCode === 0;
2092
+ const status = succeeded ? "succeeded" : "failed";
2093
+ const usage = result.usage;
2094
+ await this.db.update("runs", { id: runId }, {
2095
+ status,
2096
+ completed_at: (/* @__PURE__ */ new Date()).toISOString(),
2097
+ exit_code: result.exitCode,
2098
+ cost_cents: result.costCents ?? 0,
2099
+ input_tokens: usage?.["inputTokens"] ?? 0,
2100
+ output_tokens: usage?.["outputTokens"] ?? 0,
2101
+ error_message: result.exitCode !== 0 ? result.output : void 0
2102
+ });
2103
+ const agentId = run["agent_id"];
2104
+ this.locks.delete(agentId);
2105
+ const taskId = run["task_id"];
2106
+ if (!succeeded) {
2107
+ const task = await this.db.get("tasks", { id: taskId });
2108
+ if (task) {
2109
+ const retryCount = task["retry_count"] ?? 0;
2110
+ const maxRetries = task["max_retries"] ?? 0;
2111
+ if (retryCount < maxRetries) {
2112
+ const maxBackoff = this.config?.maxBackoffMs ?? DEFAULT_MAX_BACKOFF_MS;
2113
+ const backoffMs = Math.min(BASE_BACKOFF_MS * Math.pow(2, retryCount), maxBackoff);
2114
+ const nextRetryAt = new Date(Date.now() + backoffMs).toISOString();
2115
+ await this.db.update("tasks", { id: taskId }, {
2116
+ retry_count: retryCount + 1,
2117
+ next_retry_at: nextRetryAt,
2118
+ status: "todo",
2119
+ execution_run_id: null,
2120
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
2121
+ });
2122
+ } else {
2123
+ await this.db.update("tasks", { id: taskId }, {
2124
+ status: "failed",
2125
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
2126
+ });
2127
+ }
2128
+ }
2129
+ } else {
2130
+ const task = await this.db.get("tasks", { id: taskId });
2131
+ if (task && task["followup_agent_id"]) {
2132
+ const chainDepth = (task["chain_depth"] ?? 0) + 1;
2133
+ checkChainDepth(chainDepth, MAX_CHAIN_DEPTH);
2134
+ const followupAgentId = task["followup_agent_id"];
2135
+ const followupTemplate = task["followup_template"] ?? "Followup: {{output}}";
2136
+ const context = { output: result.output ?? "" };
2137
+ const title = interpolate(followupTemplate, context);
2138
+ const chainOriginId = task["chain_origin_id"] ?? taskId;
2139
+ await this.db.insert("tasks", {
2140
+ title,
2141
+ description: title,
2142
+ assignee_id: followupAgentId,
2143
+ status: "todo",
2144
+ priority: task["priority"] ?? 5,
2145
+ chain_depth: chainDepth,
2146
+ chain_origin_id: chainOriginId
2147
+ });
2148
+ await this.hooks.emit("task.followup.created", {
2149
+ originTaskId: taskId,
2150
+ followupAgentId,
2151
+ chainDepth
2152
+ });
2153
+ }
2154
+ }
2155
+ await this.hooks.emit("run.completed", {
2156
+ runId,
2157
+ agentId,
2158
+ taskId,
2159
+ status,
2160
+ exitCode: result.exitCode
2161
+ });
2162
+ }
2163
+ async reapOrphans() {
2164
+ const cutoff = new Date(Date.now() - this.staleThresholdMs).toISOString();
2165
+ const staleRuns = (await this.db.query("runs", { where: { status: "running" } })).filter((r) => {
2166
+ const startedAt = r["started_at"];
2167
+ return startedAt != null && startedAt < cutoff;
2168
+ });
2169
+ for (const run of staleRuns) {
2170
+ await this.db.update("runs", { id: run["id"] }, {
2171
+ status: "failed",
2172
+ completed_at: (/* @__PURE__ */ new Date()).toISOString(),
2173
+ error_message: "Orphaned run reaped by RunManager"
2174
+ });
2175
+ const agentId = run["agent_id"];
2176
+ if (this.locks.get(agentId) === run["id"]) {
2177
+ this.locks.delete(agentId);
2178
+ }
2179
+ }
2180
+ }
2181
+ startOrphanReaper(intervalMs = 6e4) {
2182
+ if (this.orphanTimer) return;
2183
+ this.orphanTimer = setInterval(() => {
2184
+ void this.reapOrphans();
2185
+ }, intervalMs);
2186
+ }
2187
+ stopOrphanReaper() {
2188
+ if (this.orphanTimer) {
2189
+ clearInterval(this.orphanTimer);
2190
+ this.orphanTimer = null;
2191
+ }
2192
+ }
2193
+ };
2194
+
2195
+ // src/core/orchestrator/budget-controller.ts
2196
+ var BudgetController = class {
2197
+ constructor(db, hooks) {
2198
+ this.db = db;
2199
+ this.hooks = hooks;
2200
+ }
2201
+ db;
2202
+ hooks;
2203
+ async checkBudget(agentId) {
2204
+ const agent = await this.db.get("agents", { id: agentId });
2205
+ if (!agent) {
2206
+ throw new Error(`Agent not found: ${agentId}`);
2207
+ }
2208
+ const limitCents = agent["budget_monthly_cents"] ?? 0;
2209
+ const currentSpendCents = agent["spent_monthly_cents"] ?? 0;
2210
+ if (limitCents <= 0) {
2211
+ return { allowed: true, currentSpendCents, limitCents };
2212
+ }
2213
+ if (currentSpendCents >= limitCents) {
2214
+ return {
2215
+ allowed: false,
2216
+ reason: "Monthly budget exceeded",
2217
+ currentSpendCents,
2218
+ limitCents
2219
+ };
2220
+ }
2221
+ let warnPercent = 80;
2222
+ const agentPolicies = await this.db.query("budget_policies", {
2223
+ where: { agent_id: agentId }
2224
+ });
2225
+ if (agentPolicies.length > 0) {
2226
+ warnPercent = agentPolicies[0]["warn_percent"] ?? 80;
2227
+ }
2228
+ const warnThreshold = limitCents * (warnPercent / 100);
2229
+ if (currentSpendCents >= warnThreshold) {
2230
+ await this.hooks.emit("budget.exceeded", {
2231
+ agentId,
2232
+ currentSpendCents,
2233
+ limitCents,
2234
+ warnPercent,
2235
+ message: `Budget warning: ${currentSpendCents} of ${limitCents} cents used (${warnPercent}% threshold)`
2236
+ });
2237
+ }
2238
+ return { allowed: true, currentSpendCents, limitCents };
2239
+ }
2240
+ async resetMonthlySpend(agentId) {
2241
+ await this.db.update("agents", { id: agentId }, {
2242
+ spent_monthly_cents: 0,
2243
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
2244
+ });
2245
+ }
2246
+ async globalCheck() {
2247
+ const agents = await this.db.query("agents");
2248
+ const totalSpentCents = agents.reduce(
2249
+ (sum, a) => sum + (a["spent_monthly_cents"] ?? 0),
2250
+ 0
2251
+ );
2252
+ const globalPolicies = await this.db.query("budget_policies", {
2253
+ where: { scope: "global" }
2254
+ });
2255
+ if (globalPolicies.length === 0) {
2256
+ return { allowed: true, totalSpentCents, limitCents: 0 };
2257
+ }
2258
+ const limitCents = globalPolicies[0]["monthly_limit_cents"] ?? 0;
2259
+ if (limitCents <= 0) {
2260
+ return { allowed: true, totalSpentCents, limitCents };
2261
+ }
2262
+ return {
2263
+ allowed: totalSpentCents < limitCents,
2264
+ totalSpentCents,
2265
+ limitCents
2266
+ };
2267
+ }
2268
+ };
2269
+
2270
+ // src/core/orchestrator/dependency-resolver.ts
2271
+ function detectCycle(steps) {
2272
+ const deps = /* @__PURE__ */ new Map();
2273
+ for (const s of steps) {
2274
+ deps.set(s.id, s.dependsOn ?? []);
2275
+ }
2276
+ const WHITE = 0, GRAY = 1, BLACK = 2;
2277
+ const color = /* @__PURE__ */ new Map();
2278
+ for (const s of steps) color.set(s.id, WHITE);
2279
+ function dfs(node) {
2280
+ color.set(node, GRAY);
2281
+ for (const dep of deps.get(node) ?? []) {
2282
+ const c = color.get(dep);
2283
+ if (c === GRAY) return true;
2284
+ if (c === WHITE && dfs(dep)) return true;
2285
+ }
2286
+ color.set(node, BLACK);
2287
+ return false;
2288
+ }
2289
+ for (const s of steps) {
2290
+ if (color.get(s.id) === WHITE && dfs(s.id)) return true;
2291
+ }
2292
+ return false;
2293
+ }
2294
+ function topologicalSort(steps) {
2295
+ if (detectCycle(steps)) {
2296
+ throw new Error("Cycle detected in step dependencies");
2297
+ }
2298
+ const deps = /* @__PURE__ */ new Map();
2299
+ for (const s of steps) {
2300
+ deps.set(s.id, s.dependsOn ?? []);
2301
+ }
2302
+ const visited = /* @__PURE__ */ new Set();
2303
+ const result = [];
2304
+ function visit(id) {
2305
+ if (visited.has(id)) return;
2306
+ visited.add(id);
2307
+ for (const dep of deps.get(id) ?? []) {
2308
+ visit(dep);
2309
+ }
2310
+ result.push(id);
2311
+ }
2312
+ for (const s of steps) {
2313
+ visit(s.id);
2314
+ }
2315
+ return result;
2316
+ }
2317
+ function areDependenciesMet(taskDepsJson, completedTaskIds) {
2318
+ if (!taskDepsJson) return true;
2319
+ let deps;
2320
+ try {
2321
+ deps = JSON.parse(taskDepsJson);
2322
+ } catch {
2323
+ return true;
2324
+ }
2325
+ if (!Array.isArray(deps) || deps.length === 0) return true;
2326
+ return deps.every((id) => completedTaskIds.has(id));
2327
+ }
2328
+
2329
+ // src/core/orchestrator/workflow-engine.ts
2330
+ var WorkflowEngine = class {
2331
+ constructor(db, hooks, taskQueue) {
2332
+ this.db = db;
2333
+ this.hooks = hooks;
2334
+ this.taskQueue = taskQueue;
2335
+ this.hooks.register("task.completed", async (ctx) => {
2336
+ const taskId = ctx["taskId"];
2337
+ const output = ctx["output"] ?? "";
2338
+ if (taskId) {
2339
+ await this.onStepCompleted(taskId, output);
2340
+ }
2341
+ });
2342
+ }
2343
+ db;
2344
+ hooks;
2345
+ taskQueue;
2346
+ /**
2347
+ * Define/register a workflow.
2348
+ */
2349
+ async define(slug, def) {
2350
+ const ids = def.steps.map((s) => s.id);
2351
+ const unique = new Set(ids);
2352
+ if (unique.size !== ids.length) {
2353
+ throw new Error("Workflow has duplicate step IDs");
2354
+ }
2355
+ for (const step of def.steps) {
2356
+ for (const dep of step.dependsOn ?? []) {
2357
+ if (!unique.has(dep)) {
2358
+ throw new Error(`Step "${step.id}" depends on unknown step "${dep}"`);
2359
+ }
2360
+ }
2361
+ }
2362
+ if (detectCycle(def.steps)) {
2363
+ throw new Error("Workflow has cyclic step dependencies");
2364
+ }
2365
+ const existing = await this.db.query("workflows", { where: { slug } });
2366
+ if (existing.length > 0) {
2367
+ await this.db.update("workflows", { slug }, {
2368
+ name: def.name,
2369
+ description: def.description,
2370
+ definition: JSON.stringify(def),
2371
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
2372
+ });
2373
+ } else {
2374
+ await this.db.insert("workflows", {
2375
+ slug,
2376
+ name: def.name,
2377
+ description: def.description,
2378
+ definition: JSON.stringify(def)
2379
+ });
2380
+ }
2381
+ }
2382
+ /**
2383
+ * Start a workflow run.
2384
+ */
2385
+ async start(workflowSlug, context) {
2386
+ const workflows = await this.db.query("workflows", { where: { slug: workflowSlug } });
2387
+ if (workflows.length === 0) {
2388
+ throw new Error(`Workflow not found: ${workflowSlug}`);
2389
+ }
2390
+ const workflow = workflows[0];
2391
+ const def = JSON.parse(workflow["definition"]);
2392
+ const runRow = await this.db.insert("workflow_runs", {
2393
+ workflow_id: workflow["id"],
2394
+ status: "running",
2395
+ step_results: JSON.stringify({}),
2396
+ context: JSON.stringify(context)
2397
+ });
2398
+ const workflowRunId = runRow["id"];
2399
+ const initialSteps = def.steps.filter((s) => !s.dependsOn || s.dependsOn.length === 0);
2400
+ for (const step of initialSteps) {
2401
+ await this._createStepTask(step, workflowRunId, workflow["id"], context);
2402
+ }
2403
+ return workflowRunId;
2404
+ }
2405
+ /**
2406
+ * Called when a task with workflow_run_id completes.
2407
+ */
2408
+ async onStepCompleted(taskId, output) {
2409
+ const task = await this.db.get("tasks", { id: taskId });
2410
+ if (!task || !task["workflow_run_id"]) return;
2411
+ const workflowRunId = task["workflow_run_id"];
2412
+ const stepId = task["workflow_step_id"];
2413
+ const run = await this.db.get("workflow_runs", { id: workflowRunId });
2414
+ if (!run || run["status"] !== "running") return;
2415
+ const stepResults = JSON.parse(run["step_results"] ?? "{}");
2416
+ if (stepId) {
2417
+ stepResults[stepId] = { output, taskId };
2418
+ }
2419
+ await this.db.update("workflow_runs", { id: workflowRunId }, {
2420
+ step_results: JSON.stringify(stepResults),
2421
+ current_step: stepId
2422
+ });
2423
+ const workflow = await this.db.get("workflows", { id: run["workflow_id"] });
2424
+ if (!workflow) return;
2425
+ const def = JSON.parse(workflow["definition"]);
2426
+ const runContext = JSON.parse(run["context"] ?? "{}");
2427
+ const stepsContext = {};
2428
+ for (const [sid, result] of Object.entries(stepResults)) {
2429
+ stepsContext[sid] = result;
2430
+ }
2431
+ const fullContext = { ...runContext, steps: stepsContext };
2432
+ const allRunTasks = await this.db.query("tasks", { where: { workflow_run_id: workflowRunId } });
2433
+ const completedStepIds = new Set(
2434
+ allRunTasks.filter((t) => t["status"] === "done" || t["id"] === taskId).map((t) => t["workflow_step_id"]).filter(Boolean)
2435
+ );
2436
+ if (stepId) completedStepIds.add(stepId);
2437
+ const startedStepIds = new Set(
2438
+ allRunTasks.map((t) => t["workflow_step_id"]).filter(Boolean)
2439
+ );
2440
+ const nextSteps = def.steps.filter((s) => {
2441
+ if (startedStepIds.has(s.id) && s.id !== stepId) return false;
2442
+ if (s.id === stepId) return false;
2443
+ if (!s.dependsOn || s.dependsOn.length === 0) return false;
2444
+ return s.dependsOn.every((dep) => completedStepIds.has(dep));
2445
+ });
2446
+ if (nextSteps.length === 0) {
2447
+ const allStepIds = new Set(def.steps.map((s) => s.id));
2448
+ const allDone = [...allStepIds].every((id) => completedStepIds.has(id));
2449
+ if (allDone) {
2450
+ await this.db.update("workflow_runs", { id: workflowRunId }, {
2451
+ status: "completed",
2452
+ completed_at: (/* @__PURE__ */ new Date()).toISOString()
2453
+ });
2454
+ await this.hooks.emit("workflow.completed", { workflowRunId });
2455
+ }
2456
+ return;
2457
+ }
2458
+ for (const step of nextSteps) {
2459
+ await this._createStepTask(step, workflowRunId, workflow["id"], fullContext);
2460
+ }
2461
+ }
2462
+ /**
2463
+ * Mark a workflow run as failed.
2464
+ */
2465
+ async onStepFailed(taskId, error) {
2466
+ const task = await this.db.get("tasks", { id: taskId });
2467
+ if (!task || !task["workflow_run_id"]) return;
2468
+ const workflowRunId = task["workflow_run_id"];
2469
+ const stepId = task["workflow_step_id"];
2470
+ const run = await this.db.get("workflow_runs", { id: workflowRunId });
2471
+ if (!run || run["status"] !== "running") return;
2472
+ const workflow = await this.db.get("workflows", { id: run["workflow_id"] });
2473
+ if (!workflow) return;
2474
+ const def = JSON.parse(workflow["definition"]);
2475
+ const step = stepId ? def.steps.find((s) => s.id === stepId) : void 0;
2476
+ if (!step || step.onFail === "abort" || !step.onFail) {
2477
+ await this.db.update("workflow_runs", { id: workflowRunId }, {
2478
+ status: "failed",
2479
+ error,
2480
+ completed_at: (/* @__PURE__ */ new Date()).toISOString()
2481
+ });
2482
+ await this.hooks.emit("workflow.failed", { workflowRunId, error });
2483
+ }
2484
+ }
2485
+ async _createStepTask(step, workflowRunId, workflowId, context) {
2486
+ let assigneeId;
2487
+ if (step.agentSlug) {
2488
+ const agents = await this.db.query("agents", { where: { slug: step.agentSlug } });
2489
+ if (agents.length > 0) {
2490
+ assigneeId = agents[0]["id"];
2491
+ }
2492
+ }
2493
+ const title = interpolate(step.taskTemplate.title, context);
2494
+ const description = interpolate(step.taskTemplate.description, context);
2495
+ const taskId = await this.taskQueue.create({
2496
+ title,
2497
+ description,
2498
+ assignee_id: assigneeId,
2499
+ workflow_run_id: workflowRunId,
2500
+ workflow_step_id: step.id
2501
+ });
2502
+ return taskId;
2503
+ }
2504
+ };
2505
+
2506
+ // src/core/orchestrator/wakeup-queue.ts
2507
+ var WakeupQueue = class {
2508
+ constructor(db) {
2509
+ this.db = db;
2510
+ }
2511
+ db;
2512
+ async enqueue(agentId, source, context) {
2513
+ const row = await this.db.insert("wakeups", {
2514
+ agent_id: agentId,
2515
+ scheduled_at: (/* @__PURE__ */ new Date()).toISOString(),
2516
+ context: context ? JSON.stringify({ source, ...context }) : JSON.stringify({ source })
2517
+ });
2518
+ return row["id"];
2519
+ }
2520
+ async coalesce(agentId, context) {
2521
+ const existing = (await this.db.query("wakeups", {
2522
+ where: { agent_id: agentId }
2523
+ })).filter((w) => w["fired_at"] == null);
2524
+ if (existing.length === 0) return;
2525
+ const wakeup = existing[0];
2526
+ const currentCtx = wakeup["context"] ? JSON.parse(wakeup["context"]) : {};
2527
+ const merged = { ...currentCtx, ...context ?? {} };
2528
+ await this.db.update("wakeups", { id: wakeup["id"] }, {
2529
+ context: JSON.stringify(merged)
2530
+ });
2531
+ }
2532
+ async getNext(agentId) {
2533
+ const queued = (await this.db.query("wakeups", {
2534
+ where: { agent_id: agentId }
2535
+ })).filter((w) => w["fired_at"] == null).sort(
2536
+ (a, b) => a["scheduled_at"].localeCompare(b["scheduled_at"])
2537
+ );
2538
+ return queued[0] ?? void 0;
2539
+ }
2540
+ async markFired(wakeupId, runId) {
2541
+ await this.db.update("wakeups", { id: wakeupId }, {
2542
+ fired_at: (/* @__PURE__ */ new Date()).toISOString(),
2543
+ run_id: runId
2544
+ });
2545
+ }
2546
+ };
2547
+
2548
+ // src/core/orchestrator/ndjson-logger.ts
2549
+ import { appendFileSync } from "fs";
2550
+ var NdjsonLogger = class {
2551
+ constructor(logPath) {
2552
+ this.logPath = logPath;
2553
+ }
2554
+ logPath;
2555
+ log(stream, chunk) {
2556
+ const line = JSON.stringify({
2557
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2558
+ stream,
2559
+ chunk
2560
+ });
2561
+ appendFileSync(this.logPath, line + "\n", "utf8");
2562
+ }
2563
+ close() {
2564
+ }
2565
+ };
2566
+
2567
+ // src/core/orchestrator/heartbeat-scheduler.ts
2568
+ var HeartbeatScheduler = class {
2569
+ constructor(wakeupQueue, hooks) {
2570
+ this.wakeupQueue = wakeupQueue;
2571
+ this.hooks = hooks;
2572
+ }
2573
+ wakeupQueue;
2574
+ hooks;
2575
+ timers = /* @__PURE__ */ new Map();
2576
+ start(agents) {
2577
+ for (const agent of agents) {
2578
+ let config;
2579
+ try {
2580
+ config = JSON.parse(agent.heartbeat_config);
2581
+ } catch {
2582
+ continue;
2583
+ }
2584
+ if (!config.enabled || !config.intervalSec) continue;
2585
+ const timer = setInterval(() => {
2586
+ void this.wakeupQueue.enqueue(agent.id, "heartbeat");
2587
+ }, config.intervalSec * 1e3);
2588
+ this.timers.set(agent.id, timer);
2589
+ }
2590
+ }
2591
+ stop() {
2592
+ for (const timer of this.timers.values()) {
2593
+ clearInterval(timer);
2594
+ }
2595
+ this.timers.clear();
2596
+ }
2597
+ };
2598
+
2599
+ // src/core/orchestrator/adapters/api-adapter.ts
2600
+ import { readFileSync as readFileSync3, existsSync as existsSync2 } from "fs";
2601
+
2602
+ // src/core/orchestrator/adapters/tool-loop.ts
2603
+ async function* toolLoop(params, callLLM, executeTool) {
2604
+ const maxIterations = params.maxIterations ?? 20;
2605
+ const messages = [...params.messages];
2606
+ let iterations = 0;
2607
+ while (iterations < maxIterations) {
2608
+ if (params.signal?.aborted) {
2609
+ return;
2610
+ }
2611
+ iterations++;
2612
+ const chatParams = {
2613
+ model: params.model,
2614
+ messages,
2615
+ system: params.systemPrompt,
2616
+ tools: params.tools
2617
+ };
2618
+ const result = await callLLM(chatParams);
2619
+ if (result.content) {
2620
+ yield { type: "text", content: result.content };
2621
+ }
2622
+ if (result.stopReason === "end_turn" || result.stopReason === "stop_sequence" || result.stopReason === "max_tokens") {
2623
+ yield { type: "done", result };
2624
+ return;
2625
+ }
2626
+ if (result.stopReason === "tool_use" && result.toolUses && result.toolUses.length > 0) {
2627
+ const toolResults = [];
2628
+ for (const toolUse of result.toolUses) {
2629
+ yield { type: "tool_use", name: toolUse.name, input: toolUse.input };
2630
+ if (executeTool) {
2631
+ const toolResult = await executeTool(toolUse.name, toolUse.input);
2632
+ toolResults.push(toolResult);
2633
+ messages.push({
2634
+ role: "assistant",
2635
+ content: [
2636
+ ...result.content ? [{ type: "text", text: result.content }] : [],
2637
+ { type: "tool_use", id: toolUse.id, name: toolUse.name, input: toolUse.input }
2638
+ ]
2639
+ });
2640
+ messages.push({
2641
+ role: "user",
2642
+ content: [
2643
+ { type: "tool_result", tool_use_id: toolUse.id, content: toolResult }
2644
+ ]
2645
+ });
2646
+ }
2647
+ }
2648
+ if (!executeTool) {
2649
+ yield { type: "done", result };
2650
+ return;
2651
+ }
2652
+ } else {
2653
+ yield { type: "done", result };
2654
+ return;
2655
+ }
2656
+ }
2657
+ const finalResult = await callLLM({
2658
+ model: params.model,
2659
+ messages,
2660
+ system: params.systemPrompt,
2661
+ tools: params.tools
2662
+ });
2663
+ yield { type: "done", result: finalResult };
2664
+ }
2665
+
2666
+ // src/core/orchestrator/adapters/api-adapter.ts
2667
+ var ApiExecutionAdapter = class {
2668
+ constructor(modelRouter) {
2669
+ this.modelRouter = modelRouter;
2670
+ }
2671
+ modelRouter;
2672
+ type = "api";
2673
+ async execute(ctx) {
2674
+ const modelId = ctx.agent.model ?? "default";
2675
+ const resolved = this.modelRouter.resolve(modelId) ?? this.modelRouter.resolveForPurpose("default");
2676
+ const { provider, model } = resolved;
2677
+ const registry = this.modelRouter.registry;
2678
+ const providerImpl = registry.list().find((p) => p.id === provider);
2679
+ if (!providerImpl) {
2680
+ throw new Error(`Provider not found: ${provider}`);
2681
+ }
2682
+ let systemPrompt = "";
2683
+ if (ctx.contextFiles && ctx.contextFiles.length > 0) {
2684
+ const fileContents = [];
2685
+ for (const filePath of ctx.contextFiles) {
2686
+ if (existsSync2(filePath)) {
2687
+ const content = readFileSync3(filePath, "utf8");
2688
+ fileContents.push(`<file path="${filePath}">
2689
+ ${content}
2690
+ </file>`);
2691
+ }
2692
+ }
2693
+ if (fileContents.length > 0) {
2694
+ systemPrompt = fileContents.join("\n\n");
2695
+ }
2696
+ }
2697
+ const messages = [
2698
+ ...ctx.sessionParams?.history ?? []
2699
+ ];
2700
+ const taskContent = [
2701
+ ctx.task.description,
2702
+ ctx.task.context
2703
+ ].filter(Boolean).join("\n\n");
2704
+ if (taskContent) {
2705
+ messages.push({ role: "user", content: taskContent });
2706
+ }
2707
+ let outputText = "";
2708
+ let totalUsage = { inputTokens: 0, outputTokens: 0 };
2709
+ const history = [...messages];
2710
+ try {
2711
+ const callLLM = (params) => providerImpl.chat(params);
2712
+ for await (const event of toolLoop(
2713
+ {
2714
+ model,
2715
+ messages,
2716
+ systemPrompt: systemPrompt || void 0,
2717
+ maxIterations: 20,
2718
+ signal: ctx.abortSignal
2719
+ },
2720
+ callLLM
2721
+ )) {
2722
+ if (event.type === "text") {
2723
+ outputText += event.content;
2724
+ ctx.onLog?.("stdout", event.content);
2725
+ } else if (event.type === "done") {
2726
+ totalUsage = {
2727
+ inputTokens: (totalUsage.inputTokens ?? 0) + (event.result.usage.inputTokens ?? 0),
2728
+ outputTokens: (totalUsage.outputTokens ?? 0) + (event.result.usage.outputTokens ?? 0)
2729
+ };
2730
+ }
2731
+ }
2732
+ history.push({ role: "assistant", content: outputText });
2733
+ return {
2734
+ output: outputText,
2735
+ exitCode: 0,
2736
+ usage: { ...totalUsage, provider, model },
2737
+ sessionParams: { history }
2738
+ };
2739
+ } catch (err) {
2740
+ const message = err instanceof Error ? err.message : String(err);
2741
+ ctx.onLog?.("stderr", message);
2742
+ return {
2743
+ output: message,
2744
+ exitCode: 1,
2745
+ usage: { inputTokens: 0, outputTokens: 0, provider, model },
2746
+ sessionParams: { history }
2747
+ };
2748
+ }
2749
+ }
2750
+ };
2751
+
2752
+ // src/core/orchestrator/adapters/cli-adapter.ts
2753
+ import { createWriteStream } from "fs";
2754
+
2755
+ // src/core/orchestrator/adapters/process-manager.ts
2756
+ import { spawn } from "child_process";
2757
+
2758
+ // src/core/orchestrator/adapters/env-whitelist.ts
2759
+ var DEFAULT_ALLOWED_ENV = [
2760
+ "PATH",
2761
+ "HOME",
2762
+ "SHELL",
2763
+ "LANG",
2764
+ "USER",
2765
+ "TERM",
2766
+ "NODE_PATH",
2767
+ "TMPDIR"
2768
+ ];
2769
+ function filterEnv(allowed, extra) {
2770
+ const allowedKeys = allowed ?? DEFAULT_ALLOWED_ENV;
2771
+ const result = {};
2772
+ for (const key of allowedKeys) {
2773
+ const val = process.env[key];
2774
+ if (val !== void 0) {
2775
+ result[key] = val;
2776
+ }
2777
+ }
2778
+ if (extra) {
2779
+ Object.assign(result, extra);
2780
+ }
2781
+ return result;
2782
+ }
2783
+
2784
+ // src/core/orchestrator/adapters/process-manager.ts
2785
+ function spawnProcess(command, args, opts) {
2786
+ const env = filterEnv(opts.allowedEnvVars, opts.extraEnv);
2787
+ const child = spawn(command, args, {
2788
+ cwd: opts.cwd,
2789
+ env,
2790
+ detached: true,
2791
+ stdio: "pipe"
2792
+ });
2793
+ return child;
2794
+ }
2795
+ function killProcessGroup(pid, signal = "SIGTERM") {
2796
+ try {
2797
+ process.kill(-pid, signal);
2798
+ } catch {
2799
+ }
2800
+ }
2801
+
2802
+ // src/core/orchestrator/adapters/output-extractor.ts
2803
+ import { readFileSync as readFileSync4 } from "fs";
2804
+ var MAX_OUTPUT_BYTES = 4 * 1024 * 1024;
2805
+ function extractOutput(ndjsonContent) {
2806
+ const lines = ndjsonContent.split("\n").filter((l) => l.trim().length > 0);
2807
+ let lastOutput;
2808
+ for (const line of lines) {
2809
+ try {
2810
+ const parsed = JSON.parse(line);
2811
+ if (parsed["type"] === "result") {
2812
+ const content = parsed["result"] ?? parsed["content"] ?? parsed["output"];
2813
+ if (typeof content === "string") {
2814
+ lastOutput = content;
2815
+ }
2816
+ } else if (parsed["role"] === "assistant") {
2817
+ const content = parsed["content"];
2818
+ if (typeof content === "string") {
2819
+ lastOutput = content;
2820
+ }
2821
+ } else if (parsed["type"] === "assistant") {
2822
+ const message = parsed["message"];
2823
+ if (message && Array.isArray(message["content"])) {
2824
+ for (const block of message["content"]) {
2825
+ if (typeof block === "object" && block !== null && block["type"] === "text") {
2826
+ lastOutput = block["text"];
2827
+ }
2828
+ }
2829
+ }
2830
+ }
2831
+ } catch {
2832
+ }
2833
+ }
2834
+ if (!lastOutput) {
2835
+ lastOutput = ndjsonContent;
2836
+ }
2837
+ if (Buffer.byteLength(lastOutput, "utf8") > MAX_OUTPUT_BYTES) {
2838
+ return Buffer.from(lastOutput, "utf8").subarray(0, MAX_OUTPUT_BYTES).toString("utf8");
2839
+ }
2840
+ return lastOutput;
2841
+ }
2842
+
2843
+ // src/core/orchestrator/adapters/cli-adapter.ts
2844
+ var CliExecutionAdapter = class {
2845
+ type = "cli";
2846
+ async execute(ctx) {
2847
+ const cwd = ctx.agent.cwd ?? process.cwd();
2848
+ let config = {};
2849
+ if (ctx.agent.adapter_config) {
2850
+ try {
2851
+ config = JSON.parse(ctx.agent.adapter_config);
2852
+ } catch {
2853
+ }
2854
+ }
2855
+ const skipPermissions = ctx.agent.skip_permissions ?? config["skip_permissions"] ?? false;
2856
+ const args = [];
2857
+ if (skipPermissions) {
2858
+ args.push("--dangerously-skip-permissions");
2859
+ }
2860
+ const prompt = [
2861
+ ctx.task.title,
2862
+ ctx.task.description,
2863
+ ctx.task.context
2864
+ ].filter(Boolean).join("\n\n");
2865
+ args.push("--print", prompt);
2866
+ const child = spawnProcess("claude", args, { cwd });
2867
+ const stdoutChunks = [];
2868
+ let logStream = null;
2869
+ if (ctx.logPath) {
2870
+ logStream = createWriteStream(ctx.logPath, { flags: "a" });
2871
+ }
2872
+ const abortHandler = () => {
2873
+ if (child.pid != null) {
2874
+ killProcessGroup(child.pid);
2875
+ }
2876
+ };
2877
+ ctx.abortSignal?.addEventListener("abort", abortHandler);
2878
+ child.stdout?.on("data", (chunk) => {
2879
+ stdoutChunks.push(chunk);
2880
+ const str = chunk.toString("utf8");
2881
+ ctx.onLog?.("stdout", str);
2882
+ logStream?.write(chunk);
2883
+ });
2884
+ child.stderr?.on("data", (chunk) => {
2885
+ const str = chunk.toString("utf8");
2886
+ ctx.onLog?.("stderr", str);
2887
+ });
2888
+ const exitCode = await new Promise((resolve) => {
2889
+ child.on("close", (code) => {
2890
+ resolve(code ?? 1);
2891
+ });
2892
+ child.on("error", () => {
2893
+ resolve(1);
2894
+ });
2895
+ });
2896
+ ctx.abortSignal?.removeEventListener("abort", abortHandler);
2897
+ logStream?.end();
2898
+ const rawOutput = Buffer.concat(stdoutChunks).toString("utf8");
2899
+ const output = extractOutput(rawOutput);
2900
+ return { output, exitCode };
2901
+ }
2902
+ };
2903
+ export {
2904
+ AGENT_STATUSES,
2905
+ AgentRegistry,
2906
+ ApiExecutionAdapter,
2907
+ AuditEmitter,
2908
+ BackupManager,
2909
+ BudgetController,
2910
+ CORE_MIGRATIONS,
2911
+ ChannelRegistry,
2912
+ ChannelRegistryError,
2913
+ ChatSessionManager,
2914
+ CliExecutionAdapter,
2915
+ ColumnValidatorImpl,
2916
+ DEFAULTS,
2917
+ DEFAULT_CONFIG,
2918
+ DataStore,
2919
+ DataStoreError,
2920
+ EVENTS,
2921
+ HeartbeatScheduler,
2922
+ HookBus,
2923
+ MAX_CHAIN_DEPTH,
2924
+ MessagePipeline,
2925
+ ModelRouter,
2926
+ NdjsonLogger,
2927
+ NotificationQueue,
2928
+ ProviderRegistry,
2929
+ RUN_STATUSES,
2930
+ RunManager,
2931
+ SessionKey,
2932
+ SessionManager,
2933
+ TASK_STATUSES,
2934
+ TaskQueue,
2935
+ UpdateChecker,
2936
+ UpdateManager,
2937
+ WakeupQueue,
2938
+ WorkflowEngine,
2939
+ _resetConfig,
2940
+ areDependenciesMet,
2941
+ buildAgentBindings,
2942
+ buildChainOrigin,
2943
+ checkAllowlist,
2944
+ checkChainDepth,
2945
+ checkMentionGate,
2946
+ chunkText,
2947
+ classifyUpdate,
2948
+ compareVersions,
2949
+ createConfigRevision,
2950
+ defineCoreTables,
2951
+ detectCycle,
2952
+ discoverChannels,
2953
+ discoverProviders,
2954
+ formatText,
2955
+ getConfig,
2956
+ initConfig,
2957
+ interpolate,
2958
+ interpolateEnv,
2959
+ loadConfig,
2960
+ parseVersion,
2961
+ runPackageMigrations,
2962
+ sanitize,
2963
+ topologicalSort,
2964
+ validateConfig
2965
+ };