anorion 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.
Files changed (53) hide show
  1. package/README.md +87 -0
  2. package/agents/001.yaml +32 -0
  3. package/agents/example.yaml +6 -0
  4. package/bin/anorion.js +8093 -0
  5. package/package.json +72 -0
  6. package/scripts/cli.ts +182 -0
  7. package/scripts/postinstall.js +6 -0
  8. package/scripts/setup.ts +255 -0
  9. package/src/agents/pipeline.ts +231 -0
  10. package/src/agents/registry.ts +153 -0
  11. package/src/agents/runtime.ts +593 -0
  12. package/src/agents/session.ts +338 -0
  13. package/src/agents/subagent.ts +185 -0
  14. package/src/bridge/client.ts +221 -0
  15. package/src/bridge/federator.ts +221 -0
  16. package/src/bridge/protocol.ts +88 -0
  17. package/src/bridge/server.ts +221 -0
  18. package/src/channels/base.ts +43 -0
  19. package/src/channels/router.ts +122 -0
  20. package/src/channels/telegram.ts +592 -0
  21. package/src/channels/webhook.ts +143 -0
  22. package/src/cli/index.ts +1036 -0
  23. package/src/cli/interactive.ts +26 -0
  24. package/src/gateway/routes-v2.ts +165 -0
  25. package/src/gateway/server.ts +512 -0
  26. package/src/gateway/ws.ts +75 -0
  27. package/src/index.ts +182 -0
  28. package/src/llm/provider.ts +243 -0
  29. package/src/llm/providers.ts +381 -0
  30. package/src/memory/context.ts +125 -0
  31. package/src/memory/store.ts +214 -0
  32. package/src/scheduler/cron.ts +239 -0
  33. package/src/shared/audit.ts +231 -0
  34. package/src/shared/config.ts +129 -0
  35. package/src/shared/db/index.ts +165 -0
  36. package/src/shared/db/prepared.ts +111 -0
  37. package/src/shared/db/schema.ts +84 -0
  38. package/src/shared/events.ts +79 -0
  39. package/src/shared/logger.ts +10 -0
  40. package/src/shared/metrics.ts +190 -0
  41. package/src/shared/rbac.ts +151 -0
  42. package/src/shared/token-budget.ts +157 -0
  43. package/src/shared/types.ts +166 -0
  44. package/src/tools/builtin/echo.ts +19 -0
  45. package/src/tools/builtin/file-read.ts +78 -0
  46. package/src/tools/builtin/file-write.ts +64 -0
  47. package/src/tools/builtin/http-request.ts +63 -0
  48. package/src/tools/builtin/memory.ts +71 -0
  49. package/src/tools/builtin/shell.ts +94 -0
  50. package/src/tools/builtin/web-search.ts +22 -0
  51. package/src/tools/executor.ts +126 -0
  52. package/src/tools/registry.ts +56 -0
  53. package/src/tools/skill-manager.ts +252 -0
@@ -0,0 +1,129 @@
1
+ import { z } from 'zod';
2
+ import { readFileSync, existsSync } from 'fs';
3
+ import { resolve } from 'path';
4
+ import { parse as parseYaml } from 'yaml';
5
+
6
+ const apiKeySchema = z.object({
7
+ name: z.string(),
8
+ key: z.string(),
9
+ scopes: z.array(z.string()).default(['*']),
10
+ });
11
+
12
+ const gatewaySchema = z.object({
13
+ host: z.string().default('0.0.0.0'),
14
+ port: z.number().default(4250),
15
+ apiKeys: z.array(apiKeySchema).default([]),
16
+ database: z.string().default('./data/anorion.db'),
17
+ });
18
+
19
+ const agentsSchema = z.object({
20
+ dir: z.string().default('./agents'),
21
+ defaultModel: z.string().default('openai/gpt-4o'),
22
+ defaultTimeoutMs: z.number().default(120000),
23
+ maxSubagents: z.number().default(5),
24
+ idleTimeoutMs: z.number().default(1800000),
25
+ });
26
+
27
+ const schedulerSchema = z.object({
28
+ enabled: z.boolean().default(true),
29
+ });
30
+
31
+ const bridgeSchema = z.object({
32
+ enabled: z.boolean().default(false),
33
+ secret: z.string().default('anorion-bridge-dev'),
34
+ port: z.number().default(4260),
35
+ peers: z.array(z.object({
36
+ url: z.string(),
37
+ secret: z.string().optional(),
38
+ })).default([]),
39
+ });
40
+
41
+ const memorySchema = z.object({
42
+ provider: z.enum(['file', 'sqlite']).default('file'),
43
+ directory: z.string().default('./data/memory'),
44
+ });
45
+
46
+ const telegramChannelSchema = z.object({
47
+ enabled: z.boolean().default(false),
48
+ botToken: z.string().default(''),
49
+ allowedUsers: z.array(z.string()).default([]),
50
+ defaultAgent: z.string().default('example'),
51
+ });
52
+
53
+ const webhookChannelSchema = z.object({
54
+ enabled: z.boolean().default(false),
55
+ inboundSecret: z.string().default(''),
56
+ outboundUrls: z.array(z.string()).default([]),
57
+ allowedIps: z.array(z.string()).default([]),
58
+ });
59
+
60
+ const channelsSchema = z.object({
61
+ telegram: telegramChannelSchema.default({} as any),
62
+ webhook: webhookChannelSchema.default({} as any),
63
+ });
64
+
65
+ const skillsSchema = z.object({
66
+ dir: z.string().default('./skills'),
67
+ watch: z.boolean().default(false),
68
+ });
69
+
70
+ const auditSchema = z.object({
71
+ enabled: z.boolean().default(true),
72
+ dbPath: z.string().default('./data/audit.db'),
73
+ });
74
+
75
+ const tokenBudgetSchema = z.object({
76
+ enabled: z.boolean().default(false),
77
+ sessionLimit: z.number().default(500_000),
78
+ dailyLimit: z.number().default(2_000_000),
79
+ globalDailyLimit: z.number().default(10_000_000),
80
+ mode: z.enum(['track', 'enforce']).default('enforce'),
81
+ });
82
+
83
+ const pipelinesSchema = z.object({
84
+ dir: z.string().optional(),
85
+ });
86
+
87
+ const configSchema = z.object({
88
+ gateway: gatewaySchema.default({} as any),
89
+ agents: agentsSchema.default({} as any),
90
+ scheduler: schedulerSchema.default({} as any),
91
+ bridge: bridgeSchema.default({} as any),
92
+ memory: memorySchema.default({} as any),
93
+ channels: channelsSchema.default({} as any),
94
+ skills: skillsSchema.default({} as any),
95
+ audit: auditSchema.default({} as any),
96
+ tokenBudget: tokenBudgetSchema.default({} as any),
97
+ pipelines: pipelinesSchema.default({} as any),
98
+ });
99
+
100
+ export type AnorionConfig = z.infer<typeof configSchema>;
101
+
102
+ function substituteEnv(obj: unknown): unknown {
103
+ if (typeof obj === 'string') {
104
+ return obj.replace(/\$\{(\w+)(?::-([^}]*))?\}/g, (_, varName, defaultVal) => process.env[varName] || defaultVal || '');
105
+ }
106
+ if (Array.isArray(obj)) return obj.map(substituteEnv);
107
+ if (obj && typeof obj === 'object') {
108
+ const out: Record<string, unknown> = {};
109
+ for (const [k, v] of Object.entries(obj as Record<string, unknown>)) {
110
+ out[k] = substituteEnv(v);
111
+ }
112
+ return out;
113
+ }
114
+ return obj;
115
+ }
116
+
117
+ export function loadConfig(configPath?: string): AnorionConfig {
118
+ const path = configPath || resolve(process.cwd(), 'anorion.yaml');
119
+
120
+ if (!existsSync(path)) {
121
+ // Return defaults
122
+ return configSchema.parse({});
123
+ }
124
+
125
+ const raw = readFileSync(path, 'utf-8');
126
+ const parsed = parseYaml(raw);
127
+ const substituted = substituteEnv(parsed) as Record<string, unknown>;
128
+ return configSchema.parse(substituted);
129
+ }
@@ -0,0 +1,165 @@
1
+ import { Database } from 'bun:sqlite';
2
+ import { drizzle } from 'drizzle-orm/bun-sqlite';
3
+ import { resolve } from 'path';
4
+ import { mkdirSync } from 'fs';
5
+ import * as schema from './schema';
6
+ import { PreparedStatements } from './prepared';
7
+ import { logger } from '../logger';
8
+
9
+ export type Db = ReturnType<typeof drizzle<typeof schema>>;
10
+
11
+ export interface DatabaseResult {
12
+ db: Db;
13
+ raw: Database;
14
+ prepared: PreparedStatements;
15
+ }
16
+
17
+ export function initDatabase(dbPath: string): DatabaseResult {
18
+ const fullPath = resolve(process.cwd(), dbPath);
19
+ const dir = fullPath.slice(0, fullPath.lastIndexOf('/'));
20
+ mkdirSync(dir, { recursive: true });
21
+
22
+ const sqlite = new Database(fullPath);
23
+ sqlite.exec('PRAGMA journal_mode = WAL');
24
+ sqlite.exec('PRAGMA foreign_keys = ON');
25
+ sqlite.exec('PRAGMA synchronous = NORMAL');
26
+ sqlite.exec('PRAGMA cache_size = -20000');
27
+ sqlite.exec('PRAGMA temp_store = MEMORY');
28
+ sqlite.exec('PRAGMA mmap_size = 268435456');
29
+
30
+ const db = drizzle(sqlite, { schema });
31
+
32
+ sqlite.exec(`
33
+ CREATE TABLE IF NOT EXISTS agents (
34
+ id TEXT PRIMARY KEY,
35
+ name TEXT NOT NULL UNIQUE,
36
+ model TEXT NOT NULL,
37
+ fallback_model TEXT,
38
+ system_prompt TEXT NOT NULL DEFAULT 'You are a helpful assistant.',
39
+ tools TEXT NOT NULL DEFAULT '[]',
40
+ max_iterations INTEGER DEFAULT 10,
41
+ timeout_ms INTEGER DEFAULT 120000,
42
+ state TEXT NOT NULL DEFAULT 'idle',
43
+ tags TEXT DEFAULT '[]',
44
+ metadata TEXT DEFAULT '{}',
45
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
46
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
47
+ );
48
+
49
+ CREATE TABLE IF NOT EXISTS sessions (
50
+ id TEXT PRIMARY KEY,
51
+ agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
52
+ channel_id TEXT,
53
+ status TEXT NOT NULL DEFAULT 'active',
54
+ tokens_used INTEGER DEFAULT 0,
55
+ message_count INTEGER DEFAULT 0,
56
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
57
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
58
+ last_active TEXT NOT NULL DEFAULT (datetime('now'))
59
+ );
60
+
61
+ CREATE TABLE IF NOT EXISTS messages (
62
+ id TEXT PRIMARY KEY,
63
+ session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
64
+ agent_id TEXT NOT NULL REFERENCES agents(id),
65
+ role TEXT NOT NULL,
66
+ content TEXT NOT NULL DEFAULT '',
67
+ tool_calls TEXT,
68
+ tool_results TEXT,
69
+ tokens_in INTEGER,
70
+ tokens_out INTEGER,
71
+ model TEXT,
72
+ duration_ms INTEGER,
73
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
74
+ );
75
+
76
+ CREATE TABLE IF NOT EXISTS tools (
77
+ name TEXT PRIMARY KEY,
78
+ description TEXT NOT NULL,
79
+ schema TEXT NOT NULL,
80
+ category TEXT DEFAULT 'system',
81
+ timeout_ms INTEGER DEFAULT 30000,
82
+ max_output_bytes INTEGER DEFAULT 1000000,
83
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
84
+ );
85
+
86
+ CREATE TABLE IF NOT EXISTS schedules (
87
+ id TEXT PRIMARY KEY,
88
+ agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
89
+ name TEXT NOT NULL,
90
+ cron_expr TEXT NOT NULL,
91
+ task TEXT NOT NULL,
92
+ enabled INTEGER DEFAULT 1,
93
+ last_run TEXT,
94
+ next_run TEXT,
95
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
96
+ );
97
+
98
+ CREATE TABLE IF NOT EXISTS memory_entries (
99
+ id TEXT PRIMARY KEY,
100
+ agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
101
+ key TEXT NOT NULL,
102
+ value TEXT NOT NULL,
103
+ category TEXT DEFAULT 'general',
104
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
105
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
106
+ );
107
+
108
+ CREATE TABLE IF NOT EXISTS api_keys (
109
+ id TEXT PRIMARY KEY,
110
+ name TEXT NOT NULL,
111
+ key_hash TEXT NOT NULL,
112
+ scopes TEXT NOT NULL DEFAULT '["*"]',
113
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
114
+ );
115
+
116
+ -- Existing indexes
117
+ CREATE INDEX IF NOT EXISTS idx_sessions_agent ON sessions(agent_id);
118
+ CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id, created_at);
119
+ CREATE INDEX IF NOT EXISTS idx_messages_agent ON messages(agent_id, created_at);
120
+ CREATE INDEX IF NOT EXISTS idx_memory_agent ON memory_entries(agent_id);
121
+
122
+ -- Missing indexes
123
+ CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);
124
+ CREATE INDEX IF NOT EXISTS idx_sessions_last_active ON sessions(last_active);
125
+ CREATE INDEX IF NOT EXISTS idx_sessions_channel ON sessions(channel_id);
126
+ CREATE INDEX IF NOT EXISTS idx_schedules_agent ON schedules(agent_id);
127
+ CREATE INDEX IF NOT EXISTS idx_schedules_enabled ON schedules(enabled);
128
+ CREATE INDEX IF NOT EXISTS idx_api_keys_hash ON api_keys(key_hash);
129
+
130
+ -- Ensure unique index on (agent_id, key) for upsert support
131
+ DROP INDEX IF EXISTS idx_memory_agent_key;
132
+ CREATE UNIQUE INDEX idx_memory_agent_key ON memory_entries(agent_id, key);
133
+ `);
134
+
135
+ // FTS5 full-text search for memory entries (external content with sync triggers)
136
+ sqlite.exec(`
137
+ CREATE VIRTUAL TABLE IF NOT EXISTS memory_fts USING fts5(
138
+ key, value, category,
139
+ content='memory_entries',
140
+ content_rowid='rowid'
141
+ );
142
+
143
+ CREATE TRIGGER IF NOT EXISTS memory_fts_ai AFTER INSERT ON memory_entries BEGIN
144
+ INSERT INTO memory_fts(rowid, key, value, category)
145
+ VALUES (new.rowid, new.key, new.value, new.category);
146
+ END;
147
+
148
+ CREATE TRIGGER IF NOT EXISTS memory_fts_ad AFTER DELETE ON memory_entries BEGIN
149
+ INSERT INTO memory_fts(memory_fts, rowid, key, value, category)
150
+ VALUES ('delete', old.rowid, old.key, old.value, old.category);
151
+ END;
152
+
153
+ CREATE TRIGGER IF NOT EXISTS memory_fts_au AFTER UPDATE ON memory_entries BEGIN
154
+ INSERT INTO memory_fts(memory_fts, rowid, key, value, category)
155
+ VALUES ('delete', old.rowid, old.key, old.value, old.category);
156
+ INSERT INTO memory_fts(rowid, key, value, category)
157
+ VALUES (new.rowid, new.key, new.value, new.category);
158
+ END;
159
+ `);
160
+
161
+ const prepared = new PreparedStatements(sqlite);
162
+
163
+ logger.info({ path: fullPath }, 'Database initialized');
164
+ return { db, raw: sqlite, prepared };
165
+ }
@@ -0,0 +1,111 @@
1
+ import type { Database as BunDatabase, Statement } from 'bun:sqlite';
2
+
3
+ /** Thin wrapper around bun:sqlite prepared statements with lazy init. */
4
+ export class PreparedStatements {
5
+ // Agents
6
+ readonly agentInsert: Statement;
7
+ readonly agentUpdate: Statement;
8
+ readonly agentDelete: Statement;
9
+ readonly agentGetById: Statement;
10
+ readonly agentGetByName: Statement;
11
+
12
+ // Sessions
13
+ readonly sessionInsert: Statement;
14
+ readonly sessionGetById: Statement;
15
+ readonly sessionListByAgent: Statement;
16
+ readonly sessionSetStatus: Statement;
17
+ readonly sessionUpdateActivity: Statement;
18
+
19
+ // Messages
20
+ readonly messageInsert: Statement;
21
+ readonly messageListBySession: Statement;
22
+
23
+ // Schedules
24
+ readonly scheduleInsert: Statement;
25
+ readonly scheduleUpdate: Statement;
26
+ readonly scheduleDelete: Statement;
27
+ readonly scheduleSetLastRun: Statement;
28
+ readonly scheduleGetAll: Statement;
29
+
30
+ constructor(db: BunDatabase) {
31
+ // Agents
32
+ this.agentInsert = db.prepare(
33
+ `INSERT INTO agents (id, name, model, fallback_model, system_prompt, tools, max_iterations, timeout_ms, state, tags, metadata, created_at, updated_at)
34
+ VALUES ($id, $name, $model, $fallbackModel, $systemPrompt, $tools, $maxIterations, $timeoutMs, $state, $tags, $metadata, $createdAt, $updatedAt)
35
+ ON CONFLICT (name) DO UPDATE SET id=$id, model=$model, fallback_model=$fallbackModel, system_prompt=$systemPrompt, tools=$tools, max_iterations=$maxIterations, timeout_ms=$timeoutMs, state=$state, tags=$tags, metadata=$metadata, updated_at=$updatedAt`,
36
+ );
37
+
38
+ this.agentUpdate = db.prepare(
39
+ `UPDATE agents SET name=$name, model=$model, fallback_model=$fallbackModel, system_prompt=$systemPrompt, tools=$tools,
40
+ max_iterations=$maxIterations, timeout_ms=$timeoutMs, tags=$tags, metadata=$metadata, updated_at=$updatedAt
41
+ WHERE id = $id`,
42
+ );
43
+
44
+ this.agentDelete = db.prepare(
45
+ `DELETE FROM agents WHERE id = $id`,
46
+ );
47
+
48
+ this.agentGetById = db.prepare(
49
+ `SELECT * FROM agents WHERE id = $id`,
50
+ );
51
+
52
+ this.agentGetByName = db.prepare(
53
+ `SELECT * FROM agents WHERE name = $name`,
54
+ );
55
+
56
+ // Sessions
57
+ this.sessionInsert = db.prepare(
58
+ `INSERT INTO sessions (id, agent_id, channel_id, status, tokens_used, message_count, created_at, updated_at, last_active)
59
+ VALUES ($id, $agentId, $channelId, $status, $tokensUsed, $messageCount, $createdAt, $updatedAt, $lastActive)`,
60
+ );
61
+
62
+ this.sessionGetById = db.prepare(
63
+ `SELECT * FROM sessions WHERE id = $id`,
64
+ );
65
+
66
+ this.sessionListByAgent = db.prepare(
67
+ `SELECT * FROM sessions WHERE agent_id = $agentId ORDER BY last_active DESC`,
68
+ );
69
+
70
+ this.sessionSetStatus = db.prepare(
71
+ `UPDATE sessions SET status = $status, updated_at = $updatedAt WHERE id = $id`,
72
+ );
73
+
74
+ this.sessionUpdateActivity = db.prepare(
75
+ `UPDATE sessions SET last_active = $lastActive, updated_at = $updatedAt, tokens_used = $tokensUsed, message_count = $messageCount, status = $status
76
+ WHERE id = $id`,
77
+ );
78
+
79
+ // Messages
80
+ this.messageInsert = db.prepare(
81
+ `INSERT INTO messages (id, session_id, agent_id, role, content, tool_calls, tool_results, tokens_in, tokens_out, model, duration_ms, created_at)
82
+ VALUES ($id, $sessionId, $agentId, $role, $content, $toolCalls, $toolResults, $tokensIn, $tokensOut, $model, $durationMs, $createdAt)`,
83
+ );
84
+
85
+ this.messageListBySession = db.prepare(
86
+ `SELECT * FROM messages WHERE session_id = $sessionId ORDER BY created_at DESC LIMIT $limit`,
87
+ );
88
+
89
+ // Schedules
90
+ this.scheduleInsert = db.prepare(
91
+ `INSERT INTO schedules (id, agent_id, name, cron_expr, task, enabled, created_at)
92
+ VALUES ($id, $agentId, $name, $cronExpr, $task, $enabled, $createdAt)`,
93
+ );
94
+
95
+ this.scheduleUpdate = db.prepare(
96
+ `UPDATE schedules SET name=$name, cron_expr=$cronExpr, task=$task, enabled=$enabled WHERE id = $id`,
97
+ );
98
+
99
+ this.scheduleDelete = db.prepare(
100
+ `DELETE FROM schedules WHERE id = $id`,
101
+ );
102
+
103
+ this.scheduleSetLastRun = db.prepare(
104
+ `UPDATE schedules SET last_run = $lastRun WHERE id = $id`,
105
+ );
106
+
107
+ this.scheduleGetAll = db.prepare(
108
+ `SELECT * FROM schedules`,
109
+ );
110
+ }
111
+ }
@@ -0,0 +1,84 @@
1
+ import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';
2
+
3
+ export const agents = sqliteTable('agents', {
4
+ id: text('id').primaryKey(),
5
+ name: text('name').notNull().unique(),
6
+ model: text('model').notNull(),
7
+ fallbackModel: text('fallback_model'),
8
+ systemPrompt: text('system_prompt').notNull().default('You are a helpful assistant.'),
9
+ tools: text('tools').notNull().default('[]'),
10
+ maxIterations: integer('max_iterations').default(10),
11
+ timeoutMs: integer('timeout_ms').default(120000),
12
+ state: text('state').notNull().default('idle'),
13
+ tags: text('tags').default('[]'),
14
+ metadata: text('metadata').default('{}'),
15
+ createdAt: text('created_at').notNull().default("datetime('now')"),
16
+ updatedAt: text('updated_at').notNull().default("datetime('now')"),
17
+ });
18
+
19
+ export const sessions = sqliteTable('sessions', {
20
+ id: text('id').primaryKey(),
21
+ agentId: text('agent_id').notNull().references(() => agents.id, { onDelete: 'cascade' }),
22
+ channelId: text('channel_id'),
23
+ status: text('status').notNull().default('active'),
24
+ tokensUsed: integer('tokens_used').default(0),
25
+ messageCount: integer('message_count').default(0),
26
+ createdAt: text('created_at').notNull().default("datetime('now')"),
27
+ updatedAt: text('updated_at').notNull().default("datetime('now')"),
28
+ lastActive: text('last_active').notNull().default("datetime('now')"),
29
+ });
30
+
31
+ export const messages = sqliteTable('messages', {
32
+ id: text('id').primaryKey(),
33
+ sessionId: text('session_id').notNull().references(() => sessions.id, { onDelete: 'cascade' }),
34
+ agentId: text('agent_id').notNull().references(() => agents.id),
35
+ role: text('role').notNull(),
36
+ content: text('content').notNull().default(''),
37
+ toolCalls: text('tool_calls'),
38
+ toolResults: text('tool_results'),
39
+ tokensIn: integer('tokens_in'),
40
+ tokensOut: integer('tokens_out'),
41
+ model: text('model'),
42
+ durationMs: integer('duration_ms'),
43
+ createdAt: text('created_at').notNull().default("datetime('now')"),
44
+ });
45
+
46
+ export const tools = sqliteTable('tools', {
47
+ name: text('name').primaryKey(),
48
+ description: text('description').notNull(),
49
+ schema: text('schema').notNull(),
50
+ category: text('category').default('system'),
51
+ timeoutMs: integer('timeout_ms').default(30000),
52
+ maxOutputBytes: integer('max_output_bytes').default(1000000),
53
+ createdAt: text('created_at').notNull().default("datetime('now')"),
54
+ });
55
+
56
+ export const schedules = sqliteTable('schedules', {
57
+ id: text('id').primaryKey(),
58
+ agentId: text('agent_id').notNull().references(() => agents.id, { onDelete: 'cascade' }),
59
+ name: text('name').notNull(),
60
+ cronExpr: text('cron_expr').notNull(),
61
+ task: text('task').notNull(),
62
+ enabled: integer('enabled', { mode: 'boolean' }).default(true),
63
+ lastRun: text('last_run'),
64
+ nextRun: text('next_run'),
65
+ createdAt: text('created_at').notNull().default("datetime('now')"),
66
+ });
67
+
68
+ export const memoryEntries = sqliteTable('memory_entries', {
69
+ id: text('id').primaryKey(),
70
+ agentId: text('agent_id').notNull().references(() => agents.id, { onDelete: 'cascade' }),
71
+ key: text('key').notNull(),
72
+ value: text('value').notNull(),
73
+ category: text('category').default('general'),
74
+ createdAt: text('created_at').notNull().default("datetime('now')"),
75
+ updatedAt: text('updated_at').notNull().default("datetime('now')"),
76
+ });
77
+
78
+ export const apiKeys = sqliteTable('api_keys', {
79
+ id: text('id').primaryKey(),
80
+ name: text('name').notNull(),
81
+ keyHash: text('key_hash').notNull(),
82
+ scopes: text('scopes').notNull().default('["*"]'),
83
+ createdAt: text('created_at').notNull().default("datetime('now')"),
84
+ });
@@ -0,0 +1,79 @@
1
+ // Simple typed pub/sub event bus for decoupled communication
2
+ // Used by runtime → WS broadcasting, metrics, audit log, etc.
3
+
4
+ type EventHandler<T = unknown> = (event: T) => void;
5
+
6
+ interface EventSubscription {
7
+ unsubscribe: () => void;
8
+ }
9
+
10
+ class EventBus {
11
+ private handlers = new Map<string, Set<EventHandler>>();
12
+ private onceHandlers = new Map<string, Set<EventHandler>>();
13
+
14
+ on<T>(event: string, handler: EventHandler<T>): EventSubscription {
15
+ if (!this.handlers.has(event)) {
16
+ this.handlers.set(event, new Set());
17
+ }
18
+ const set = this.handlers.get(event)!;
19
+ const wrapped = handler as EventHandler;
20
+ set.add(wrapped);
21
+ return { unsubscribe: () => set.delete(wrapped) };
22
+ }
23
+
24
+ once<T>(event: string, handler: EventHandler<T>): void {
25
+ if (!this.onceHandlers.has(event)) {
26
+ this.onceHandlers.set(event, new Set());
27
+ }
28
+ this.onceHandlers.get(event)!.add(handler as EventHandler);
29
+ }
30
+
31
+ emit<T>(event: string, data: T): void {
32
+ const handlers = this.handlers.get(event);
33
+ if (handlers) {
34
+ for (const h of handlers) {
35
+ try { h(data); } catch { /* swallow */ }
36
+ }
37
+ }
38
+ const once = this.onceHandlers.get(event);
39
+ if (once) {
40
+ this.onceHandlers.delete(event);
41
+ for (const h of once) {
42
+ try { h(data); } catch { /* swallow */ }
43
+ }
44
+ }
45
+ }
46
+
47
+ removeAllFor(event: string): void {
48
+ this.handlers.delete(event);
49
+ this.onceHandlers.delete(event);
50
+ }
51
+
52
+ eventNames(): string[] {
53
+ return [...new Set([...this.handlers.keys(), ...this.onceHandlers.keys()])];
54
+ }
55
+ }
56
+
57
+ export const eventBus = new EventBus();
58
+
59
+ // Typed event definitions
60
+ export interface AgentEvents {
61
+ 'agent:processing': { agentId: string; sessionId: string; timestamp: number };
62
+ 'agent:tool-call': { agentId: string; sessionId: string; toolName: string; toolCallId: string; timestamp: number };
63
+ 'agent:response': { agentId: string; sessionId: string; content: string; durationMs: number; tokensUsed?: number; timestamp: number };
64
+ 'agent:error': { agentId: string; sessionId: string; error: string; timestamp: number };
65
+ 'agent:idle': { agentId: string; timestamp: number };
66
+ 'session:created': { sessionId: string; agentId: string; timestamp: number };
67
+ 'session:destroyed': { sessionId: string; agentId: string; timestamp: number };
68
+ 'session:idle': { sessionId: string; agentId: string; timestamp: number };
69
+ 'tool:executed': { agentId: string; sessionId: string; toolName: string; durationMs: number; success: boolean; timestamp: number };
70
+ 'bridge:peer:connected': { gatewayId: string; url: string; timestamp: number };
71
+ 'bridge:peer:disconnected': { gatewayId: string; url: string; timestamp: number };
72
+ 'bridge:message:forwarded': { gatewayId: string; targetAgentId: string; timestamp: number };
73
+ 'memory:saved': { agentId: string; key: string; category: string; timestamp: number };
74
+ 'memory:forgotten': { agentId: string; key: string; timestamp: number };
75
+ 'schedule:executed': { scheduleId: string; agentId: string; success: boolean; timestamp: number };
76
+ 'token:usage': { agentId: string; sessionId: string; model: string; promptTokens: number; completionTokens: number; timestamp: number };
77
+ }
78
+
79
+ export type EventName = keyof AgentEvents;
@@ -0,0 +1,10 @@
1
+ import pino from 'pino';
2
+
3
+ const isDev = process.env.NODE_ENV !== 'production';
4
+
5
+ export const logger = pino({
6
+ level: process.env.LOG_LEVEL || (isDev ? 'debug' : 'info'),
7
+ ...(isDev
8
+ ? { transport: { target: 'pino-pretty', options: { colorize: true } } }
9
+ : {}),
10
+ });