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.
- package/README.md +87 -0
- package/agents/001.yaml +32 -0
- package/agents/example.yaml +6 -0
- package/bin/anorion.js +8093 -0
- package/package.json +72 -0
- package/scripts/cli.ts +182 -0
- package/scripts/postinstall.js +6 -0
- package/scripts/setup.ts +255 -0
- package/src/agents/pipeline.ts +231 -0
- package/src/agents/registry.ts +153 -0
- package/src/agents/runtime.ts +593 -0
- package/src/agents/session.ts +338 -0
- package/src/agents/subagent.ts +185 -0
- package/src/bridge/client.ts +221 -0
- package/src/bridge/federator.ts +221 -0
- package/src/bridge/protocol.ts +88 -0
- package/src/bridge/server.ts +221 -0
- package/src/channels/base.ts +43 -0
- package/src/channels/router.ts +122 -0
- package/src/channels/telegram.ts +592 -0
- package/src/channels/webhook.ts +143 -0
- package/src/cli/index.ts +1036 -0
- package/src/cli/interactive.ts +26 -0
- package/src/gateway/routes-v2.ts +165 -0
- package/src/gateway/server.ts +512 -0
- package/src/gateway/ws.ts +75 -0
- package/src/index.ts +182 -0
- package/src/llm/provider.ts +243 -0
- package/src/llm/providers.ts +381 -0
- package/src/memory/context.ts +125 -0
- package/src/memory/store.ts +214 -0
- package/src/scheduler/cron.ts +239 -0
- package/src/shared/audit.ts +231 -0
- package/src/shared/config.ts +129 -0
- package/src/shared/db/index.ts +165 -0
- package/src/shared/db/prepared.ts +111 -0
- package/src/shared/db/schema.ts +84 -0
- package/src/shared/events.ts +79 -0
- package/src/shared/logger.ts +10 -0
- package/src/shared/metrics.ts +190 -0
- package/src/shared/rbac.ts +151 -0
- package/src/shared/token-budget.ts +157 -0
- package/src/shared/types.ts +166 -0
- package/src/tools/builtin/echo.ts +19 -0
- package/src/tools/builtin/file-read.ts +78 -0
- package/src/tools/builtin/file-write.ts +64 -0
- package/src/tools/builtin/http-request.ts +63 -0
- package/src/tools/builtin/memory.ts +71 -0
- package/src/tools/builtin/shell.ts +94 -0
- package/src/tools/builtin/web-search.ts +22 -0
- package/src/tools/executor.ts +126 -0
- package/src/tools/registry.ts +56 -0
- package/src/tools/skill-manager.ts +252 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Interactive prompts for CLI — works with Node and Bun
|
|
2
|
+
|
|
3
|
+
export async function prompt(question: string, defaultValue?: string): Promise<string> {
|
|
4
|
+
process.stdout.write(`\n${question}${defaultValue ? ` [${defaultValue}]` : ''}: `);
|
|
5
|
+
const line = await readline();
|
|
6
|
+
return line.trim() || defaultValue || '';
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function confirm(question: string, defaultYes = true): Promise<boolean> {
|
|
10
|
+
const answer = await prompt(`${question} (${defaultYes ? 'Y/n' : 'y/N'})`);
|
|
11
|
+
if (!answer) return defaultYes;
|
|
12
|
+
return answer.toLowerCase().startsWith('y');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function choose(question: string, options: string[]): Promise<number> {
|
|
16
|
+
console.log(`\n${question}`);
|
|
17
|
+
options.forEach((opt, i) => console.log(` ${i + 1}) ${opt}`));
|
|
18
|
+
const answer = parseInt(await prompt('Choose'));
|
|
19
|
+
return isNaN(answer) ? 0 : answer - 1;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function readline(): Promise<string> {
|
|
23
|
+
return new Promise((resolve) => {
|
|
24
|
+
process.stdin.once('data', (data) => resolve(data.toString().trim()));
|
|
25
|
+
});
|
|
26
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// Extended API routes for Wave 2 features
|
|
2
|
+
// These get mounted onto the main Hono app
|
|
3
|
+
|
|
4
|
+
import { Hono } from 'hono';
|
|
5
|
+
import { metrics } from '../shared/metrics';
|
|
6
|
+
import { auditLog } from '../shared/audit';
|
|
7
|
+
import { tokenBudget } from '../shared/token-budget';
|
|
8
|
+
import { apiKeyManager } from '../shared/rbac';
|
|
9
|
+
import { skillManager } from '../tools/skill-manager';
|
|
10
|
+
import { listPipelines, getPipeline, executePipeline, registerPipeline } from '../agents/pipeline';
|
|
11
|
+
import { logger } from '../shared/logger';
|
|
12
|
+
|
|
13
|
+
const app = new Hono();
|
|
14
|
+
|
|
15
|
+
// ── Prometheus Metrics ──
|
|
16
|
+
app.get('/metrics', (c) => {
|
|
17
|
+
const text = metrics.render();
|
|
18
|
+
return c.text(text, 200, { 'Content-Type': 'text/plain; version=0.0.4' });
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// ── Stats Dashboard ──
|
|
22
|
+
app.get('/api/v1/stats', (c) => {
|
|
23
|
+
return c.json({
|
|
24
|
+
uptime: process.uptime(),
|
|
25
|
+
memory: process.memoryUsage(),
|
|
26
|
+
tokens: tokenBudget.getGlobalUsage(),
|
|
27
|
+
audit: auditLog.getStats(),
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// ── Audit Log ──
|
|
32
|
+
app.get('/api/v1/audit', (c) => {
|
|
33
|
+
const entries = auditLog.query({
|
|
34
|
+
agentId: c.req.query('agentId'),
|
|
35
|
+
sessionId: c.req.query('sessionId'),
|
|
36
|
+
action: c.req.query('action'),
|
|
37
|
+
since: c.req.query('since'),
|
|
38
|
+
until: c.req.query('until'),
|
|
39
|
+
limit: Number(c.req.query('limit')) || 100,
|
|
40
|
+
offset: Number(c.req.query('offset')) || 0,
|
|
41
|
+
});
|
|
42
|
+
return c.json({ entries });
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
app.get('/api/v1/audit/stats', (c) => {
|
|
46
|
+
return c.json(auditLog.getStats());
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
app.delete('/api/v1/audit', async (c) => {
|
|
50
|
+
const days = Number(c.req.query('retentionDays')) || 30;
|
|
51
|
+
const deleted = auditLog.purge(days);
|
|
52
|
+
return c.json({ deleted });
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// ── Token Budget ──
|
|
56
|
+
app.get('/api/v1/tokens', (c) => {
|
|
57
|
+
return c.json({
|
|
58
|
+
global: tokenBudget.getGlobalUsage(),
|
|
59
|
+
config: tokenBudget.getConfig(),
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
app.get('/api/v1/tokens/:agentId', (c) => {
|
|
64
|
+
const usage = tokenBudget.getUsage(c.req.param('agentId'));
|
|
65
|
+
return c.json({ usage });
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
app.patch('/api/v1/tokens/config', async (c) => {
|
|
69
|
+
const body = await c.req.json();
|
|
70
|
+
tokenBudget.updateConfig(body);
|
|
71
|
+
return c.json({ config: tokenBudget.getConfig() });
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
app.post('/api/v1/tokens/:agentId/reset', (c) => {
|
|
75
|
+
tokenBudget.resetAgent(c.req.param('agentId'));
|
|
76
|
+
return c.json({ ok: true });
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// ── API Keys (RBAC) ──
|
|
80
|
+
app.get('/api/v1/keys', (c) => {
|
|
81
|
+
return c.json({ keys: apiKeyManager.list() });
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
app.post('/api/v1/keys', async (c) => {
|
|
85
|
+
const body = await c.req.json();
|
|
86
|
+
if (!body.name || !body.role) return c.json({ error: 'name and role are required' }, 400);
|
|
87
|
+
const result = apiKeyManager.create({
|
|
88
|
+
name: body.name,
|
|
89
|
+
role: body.role,
|
|
90
|
+
permissions: body.permissions,
|
|
91
|
+
});
|
|
92
|
+
return c.json({ key: result.key, entry: result.entry }, 201);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
app.delete('/api/v1/keys/:id', (c) => {
|
|
96
|
+
const ok = apiKeyManager.revoke(c.req.param('id'));
|
|
97
|
+
if (!ok) return c.json({ error: 'Key not found' }, 404);
|
|
98
|
+
return c.json({ ok: true });
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
app.get('/api/v1/keys/roles', (c) => {
|
|
102
|
+
return c.json({ roles: apiKeyManager.getRoles() });
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// ── Skills ──
|
|
106
|
+
app.get('/api/v1/skills', (c) => {
|
|
107
|
+
return c.json({ skills: skillManager.list() });
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
app.post('/api/v1/skills/:name/reload', async (c) => {
|
|
111
|
+
const ok = await skillManager.reload(c.req.param('name'));
|
|
112
|
+
if (!ok) return c.json({ error: 'Skill not found or reload failed' }, 404);
|
|
113
|
+
return c.json({ ok: true });
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
app.patch('/api/v1/skills/:name', async (c) => {
|
|
117
|
+
const body = await c.req.json();
|
|
118
|
+
if (body.enabled !== undefined) {
|
|
119
|
+
skillManager.setEnabled(c.req.param('name'), body.enabled);
|
|
120
|
+
}
|
|
121
|
+
if (body.config) {
|
|
122
|
+
skillManager.setSkillConfig(c.req.param('name'), body.config);
|
|
123
|
+
}
|
|
124
|
+
return c.json({ ok: true });
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// ── Pipelines ──
|
|
128
|
+
app.get('/api/v1/pipelines', (c) => {
|
|
129
|
+
return c.json({ pipelines: listPipelines() });
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
app.get('/api/v1/pipelines/:name', (c) => {
|
|
133
|
+
const pipeline = getPipeline(c.req.param('name'));
|
|
134
|
+
if (!pipeline) return c.json({ error: 'Pipeline not found' }, 404);
|
|
135
|
+
return c.json({ pipeline });
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
app.post('/api/v1/pipelines', async (c) => {
|
|
139
|
+
const body = await c.req.json();
|
|
140
|
+
if (!body.name || !body.steps || !Array.isArray(body.steps)) {
|
|
141
|
+
return c.json({ error: 'name and steps array are required' }, 400);
|
|
142
|
+
}
|
|
143
|
+
registerPipeline({
|
|
144
|
+
name: body.name,
|
|
145
|
+
description: body.description,
|
|
146
|
+
steps: body.steps,
|
|
147
|
+
chainMode: body.chainMode || 'last-output',
|
|
148
|
+
onFailure: body.onFailure || 'stop',
|
|
149
|
+
maxRetries: body.maxRetries || 0,
|
|
150
|
+
});
|
|
151
|
+
return c.json({ ok: true }, 201);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
app.post('/api/v1/pipelines/:name/execute', async (c) => {
|
|
155
|
+
const body = await c.req.json();
|
|
156
|
+
if (!body.input) return c.json({ error: 'input is required' }, 400);
|
|
157
|
+
try {
|
|
158
|
+
const result = await executePipeline(c.req.param('name'), body.input, body.sessionId);
|
|
159
|
+
return c.json(result);
|
|
160
|
+
} catch (err) {
|
|
161
|
+
return c.json({ error: (err as Error).message }, 500);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
export default app;
|
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import { cors } from 'hono/cors';
|
|
3
|
+
import { stream } from 'hono/streaming';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { agentRegistry } from '../agents/registry';
|
|
6
|
+
import { toolRegistry } from '../tools/registry';
|
|
7
|
+
import { sessionManager } from '../agents/session';
|
|
8
|
+
import { sendMessage, streamMessage } from '../agents/runtime';
|
|
9
|
+
import { channelRouter } from '../channels/router';
|
|
10
|
+
import { logger } from '../shared/logger';
|
|
11
|
+
import { scheduleManager } from '../scheduler/cron';
|
|
12
|
+
import { memoryManager } from '../memory/store';
|
|
13
|
+
import { spawnSubAgent, listChildren, killChild } from '../agents/subagent';
|
|
14
|
+
|
|
15
|
+
const app = new Hono();
|
|
16
|
+
|
|
17
|
+
// ── Zod Schemas ──
|
|
18
|
+
|
|
19
|
+
const CreateAgentSchema = z.object({
|
|
20
|
+
name: z.string().min(1),
|
|
21
|
+
model: z.string().optional().default('openai/gpt-4o'),
|
|
22
|
+
systemPrompt: z.string().optional().default('You are a helpful assistant.'),
|
|
23
|
+
tools: z.array(z.string()).optional().default([]),
|
|
24
|
+
maxIterations: z.number().int().min(1).max(100).optional().default(10 as any as never),
|
|
25
|
+
timeoutMs: z.number().int().min(1000).optional().default(120000 as any as never),
|
|
26
|
+
tags: z.array(z.string()).optional(),
|
|
27
|
+
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const UpdateAgentSchema = z.object({
|
|
31
|
+
name: z.string().min(1).optional(),
|
|
32
|
+
model: z.string().optional(),
|
|
33
|
+
systemPrompt: z.string().optional(),
|
|
34
|
+
tools: z.array(z.string()).optional(),
|
|
35
|
+
maxIterations: z.number().int().min(1).max(100).optional(),
|
|
36
|
+
timeoutMs: z.number().int().min(1000).optional(),
|
|
37
|
+
tags: z.array(z.string()).optional(),
|
|
38
|
+
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const SendMessageSchema = z.object({
|
|
42
|
+
text: z.string().min(1),
|
|
43
|
+
sessionId: z.string().optional(),
|
|
44
|
+
channelId: z.string().optional(),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const CreateScheduleSchema = z.object({
|
|
48
|
+
name: z.string().min(1),
|
|
49
|
+
agentId: z.string().min(1),
|
|
50
|
+
schedule: z.string().min(1),
|
|
51
|
+
payload: z.record(z.string(), z.unknown()),
|
|
52
|
+
enabled: z.boolean().optional().default(true as any as never),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const SaveMemorySchema = z.object({
|
|
56
|
+
key: z.string().min(1),
|
|
57
|
+
value: z.unknown(),
|
|
58
|
+
category: z.string().optional().default('fact'),
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const SpawnSchema = z.object({
|
|
62
|
+
prompt: z.string().min(1),
|
|
63
|
+
ttl_seconds: z.number().int().positive().optional(),
|
|
64
|
+
systemPrompt: z.string().optional(),
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const StreamSchema = z.object({
|
|
68
|
+
text: z.string().min(1),
|
|
69
|
+
sessionId: z.string().optional(),
|
|
70
|
+
channelId: z.string().optional(),
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// ── Validation Helper ──
|
|
74
|
+
|
|
75
|
+
function validate<T>(schema: z.ZodSchema<T>, data: unknown): { success: true; data: T } | { success: false; error: any } {
|
|
76
|
+
const result = schema.safeParse(data);
|
|
77
|
+
if (result.success) return { success: true, data: result.data };
|
|
78
|
+
return { success: false, error: result.error.flatten() };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ── Rate Limiting ──
|
|
82
|
+
|
|
83
|
+
interface RateBucket {
|
|
84
|
+
timestamps: number[];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const rateBuckets = new Map<string, RateBucket>();
|
|
88
|
+
|
|
89
|
+
function rateLimit(maxRequests: number, windowMs: number) {
|
|
90
|
+
return async (c: any, next: any) => {
|
|
91
|
+
const ip = c.req.header('x-forwarded-for')?.split(',')[0]?.trim()
|
|
92
|
+
|| c.req.header('x-real-ip')
|
|
93
|
+
|| c.req.header('cf-connecting-ip')
|
|
94
|
+
|| 'unknown';
|
|
95
|
+
|
|
96
|
+
const now = Date.now();
|
|
97
|
+
const bucket = rateBuckets.get(ip) || { timestamps: [] };
|
|
98
|
+
|
|
99
|
+
// Slide window: keep only timestamps within window
|
|
100
|
+
bucket.timestamps = bucket.timestamps.filter((t: number) => now - t < windowMs);
|
|
101
|
+
|
|
102
|
+
if (bucket.timestamps.length >= maxRequests) {
|
|
103
|
+
const oldest = bucket.timestamps[0]!;
|
|
104
|
+
const retryAfter = Math.ceil((oldest + windowMs - now) / 1000);
|
|
105
|
+
return c.json({ error: 'Rate limit exceeded' }, 429, { 'Retry-After': String(retryAfter) });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
bucket.timestamps.push(now);
|
|
109
|
+
rateBuckets.set(ip, bucket);
|
|
110
|
+
|
|
111
|
+
return next();
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const messageRateLimit = rateLimit(60, 60_000); // 60 req/min
|
|
116
|
+
const readRateLimit = rateLimit(120, 60_000); // 120 req/min
|
|
117
|
+
|
|
118
|
+
// ── CORS ──
|
|
119
|
+
app.use('*', cors());
|
|
120
|
+
|
|
121
|
+
// ── Auth Middleware ──
|
|
122
|
+
|
|
123
|
+
const validKeys = new Map<string, string[]>();
|
|
124
|
+
export function setApiKeys(keys: { name: string; key: string; scopes: string[] }[]) {
|
|
125
|
+
for (const k of keys) {
|
|
126
|
+
validKeys.set(k.key, k.scopes);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const noAuthPaths = ['/health'];
|
|
131
|
+
|
|
132
|
+
app.use('*', async (c, next) => {
|
|
133
|
+
if (noAuthPaths.includes(c.req.path)) return next();
|
|
134
|
+
|
|
135
|
+
const hasRealKeys = [...validKeys.keys()].some((k) => k !== 'anorion-dev-key');
|
|
136
|
+
if (!hasRealKeys) return next();
|
|
137
|
+
|
|
138
|
+
const apiKey = c.req.header('X-API-Key');
|
|
139
|
+
if (!apiKey || !validKeys.has(apiKey)) {
|
|
140
|
+
return c.json({ error: 'Unauthorized' }, 401);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return next();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// ── Health ──
|
|
147
|
+
app.get('/health', readRateLimit, (c) => c.json({
|
|
148
|
+
status: 'ok',
|
|
149
|
+
uptime: process.uptime(),
|
|
150
|
+
timestamp: new Date().toISOString(),
|
|
151
|
+
agents: agentRegistry.list().length,
|
|
152
|
+
}));
|
|
153
|
+
|
|
154
|
+
// ── Agents ──
|
|
155
|
+
|
|
156
|
+
app.get('/api/v1/agents', readRateLimit, (c) => {
|
|
157
|
+
return c.json({ agents: agentRegistry.list() });
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
app.post('/api/v1/agents', messageRateLimit, async (c) => {
|
|
161
|
+
const body = await c.req.json();
|
|
162
|
+
const parsed = validate(CreateAgentSchema, body);
|
|
163
|
+
if (!parsed.success) {
|
|
164
|
+
return c.json({ error: 'Validation failed', fields: parsed.error.fieldErrors }, 400);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const data = parsed.data;
|
|
168
|
+
const agent = await agentRegistry.create({
|
|
169
|
+
id: crypto.randomUUID().slice(0, 10),
|
|
170
|
+
name: data.name,
|
|
171
|
+
model: data.model,
|
|
172
|
+
systemPrompt: data.systemPrompt,
|
|
173
|
+
tools: data.tools,
|
|
174
|
+
maxIterations: data.maxIterations,
|
|
175
|
+
timeoutMs: data.timeoutMs,
|
|
176
|
+
tags: data.tags,
|
|
177
|
+
metadata: data.metadata,
|
|
178
|
+
} as any);
|
|
179
|
+
|
|
180
|
+
if (agent.tools.length > 0) {
|
|
181
|
+
toolRegistry.bindTools(agent.id, agent.tools);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return c.json({ agent }, 201);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
function resolveAgent(idOrName: string) {
|
|
188
|
+
return agentRegistry.get(idOrName) || agentRegistry.getByName(idOrName);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
app.get('/api/v1/agents/:id', readRateLimit, (c) => {
|
|
192
|
+
const agent = resolveAgent(c.req.param('id'));
|
|
193
|
+
if (!agent) return c.json({ error: 'Agent not found' }, 404);
|
|
194
|
+
return c.json({ agent });
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
app.patch('/api/v1/agents/:id', messageRateLimit, async (c) => {
|
|
198
|
+
const body = await c.req.json();
|
|
199
|
+
const parsed = validate(UpdateAgentSchema, body);
|
|
200
|
+
if (!parsed.success) {
|
|
201
|
+
return c.json({ error: 'Validation failed', fields: parsed.error.fieldErrors }, 400);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const agent = resolveAgent(c.req.param('id'));
|
|
205
|
+
if (!agent) return c.json({ error: 'Agent not found' }, 404);
|
|
206
|
+
const updated = await agentRegistry.update(agent.id, parsed.data);
|
|
207
|
+
|
|
208
|
+
if (parsed.data.tools) {
|
|
209
|
+
toolRegistry.bindTools(agent.id, parsed.data.tools);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return c.json({ agent: updated });
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
app.delete('/api/v1/agents/:id', messageRateLimit, async (c) => {
|
|
216
|
+
const agent = resolveAgent(c.req.param('id'));
|
|
217
|
+
if (!agent) return c.json({ error: 'Agent not found' }, 404);
|
|
218
|
+
await agentRegistry.delete(agent.id);
|
|
219
|
+
return c.json({ ok: true });
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// ── SSE Streaming Endpoint ──
|
|
223
|
+
|
|
224
|
+
app.post('/api/v1/agents/:id/stream', messageRateLimit, async (c) => {
|
|
225
|
+
const body = await c.req.json();
|
|
226
|
+
const parsed = validate(StreamSchema, body);
|
|
227
|
+
if (!parsed.success) {
|
|
228
|
+
return c.json({ error: 'Validation failed', fields: parsed.error.fieldErrors }, 400);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const agent = resolveAgent(c.req.param('id'));
|
|
232
|
+
if (!agent) return c.json({ error: 'Agent not found' }, 404);
|
|
233
|
+
|
|
234
|
+
return stream(c, async (stream) => {
|
|
235
|
+
// SSE headers
|
|
236
|
+
c.header('Content-Type', 'text/event-stream');
|
|
237
|
+
c.header('Cache-Control', 'no-cache');
|
|
238
|
+
c.header('Connection', 'keep-alive');
|
|
239
|
+
|
|
240
|
+
try {
|
|
241
|
+
const gen = streamMessage({
|
|
242
|
+
agentId: agent.id,
|
|
243
|
+
text: parsed.data.text,
|
|
244
|
+
sessionId: parsed.data.sessionId,
|
|
245
|
+
channelId: parsed.data.channelId,
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
let lastSessionId = '';
|
|
249
|
+
for await (const { sessionId, chunk } of gen) {
|
|
250
|
+
lastSessionId = sessionId;
|
|
251
|
+
if (chunk.type === 'delta') {
|
|
252
|
+
await stream.write(`event: delta\ndata: ${JSON.stringify({ content: chunk.content, sessionId })}\n\n`);
|
|
253
|
+
} else if (chunk.type === 'tool_call') {
|
|
254
|
+
await stream.write(`event: tool_call\ndata: ${JSON.stringify({ toolName: chunk.name, toolCallId: chunk.id, sessionId })}\n\n`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
await stream.write(`event: done\ndata: ${JSON.stringify({ sessionId: lastSessionId, timestamp: Date.now() })}\n\n`);
|
|
259
|
+
} catch (err) {
|
|
260
|
+
await stream.write(`event: error\ndata: ${JSON.stringify({ error: (err as Error).message })}\n\n`);
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// ── Messages ──
|
|
266
|
+
|
|
267
|
+
app.post('/api/v1/agents/:id/messages', messageRateLimit, async (c) => {
|
|
268
|
+
const body = await c.req.json();
|
|
269
|
+
const parsed = validate(SendMessageSchema, body);
|
|
270
|
+
if (!parsed.success) {
|
|
271
|
+
return c.json({ error: 'Validation failed', fields: parsed.error.fieldErrors }, 400);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const agent = resolveAgent(c.req.param('id'));
|
|
275
|
+
if (!agent) return c.json({ error: 'Agent not found' }, 404);
|
|
276
|
+
|
|
277
|
+
try {
|
|
278
|
+
const result = await sendMessage({
|
|
279
|
+
agentId: agent.id,
|
|
280
|
+
sessionId: parsed.data.sessionId,
|
|
281
|
+
text: parsed.data.text,
|
|
282
|
+
channelId: parsed.data.channelId,
|
|
283
|
+
});
|
|
284
|
+
return c.json(result);
|
|
285
|
+
} catch (err) {
|
|
286
|
+
logger.error({ error: (err as Error).message }, 'Message failed');
|
|
287
|
+
return c.json({ error: (err as Error).message }, 500);
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
app.get('/api/v1/agents/:id/sessions', readRateLimit, async (c) => {
|
|
292
|
+
const agent = resolveAgent(c.req.param('id'));
|
|
293
|
+
if (!agent) return c.json({ error: 'Agent not found' }, 404);
|
|
294
|
+
const sessions = await sessionManager.listByAgent(agent.id);
|
|
295
|
+
return c.json({ sessions });
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
// ── Tools ──
|
|
299
|
+
|
|
300
|
+
app.get('/api/v1/tools', readRateLimit, (c) => {
|
|
301
|
+
return c.json({
|
|
302
|
+
tools: toolRegistry.list().map((t) => ({
|
|
303
|
+
name: t.name,
|
|
304
|
+
description: t.description,
|
|
305
|
+
parameters: t.parameters,
|
|
306
|
+
category: t.category,
|
|
307
|
+
})),
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
// ── Channels ──
|
|
312
|
+
|
|
313
|
+
app.get('/api/v1/channels', readRateLimit, (c) => {
|
|
314
|
+
return c.json({ channels: channelRouter.listChannels() });
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
app.post('/api/v1/channels/:name/start', messageRateLimit, async (c) => {
|
|
318
|
+
const name = c.req.param('name');
|
|
319
|
+
const ok = await channelRouter.startChannel(name);
|
|
320
|
+
if (!ok) return c.json({ error: `Channel not found: ${name}` }, 404);
|
|
321
|
+
return c.json({ ok: true, channel: name });
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
app.post('/api/v1/channels/:name/stop', messageRateLimit, async (c) => {
|
|
325
|
+
const name = c.req.param('name');
|
|
326
|
+
const ok = await channelRouter.stopChannel(name);
|
|
327
|
+
if (!ok) return c.json({ error: `Channel not found: ${name}` }, 404);
|
|
328
|
+
return c.json({ ok: true, channel: name });
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
// ── Schedules ──
|
|
332
|
+
|
|
333
|
+
app.post('/api/v1/schedules', messageRateLimit, async (c) => {
|
|
334
|
+
const body = await c.req.json();
|
|
335
|
+
const parsed = validate(CreateScheduleSchema, body);
|
|
336
|
+
if (!parsed.success) {
|
|
337
|
+
return c.json({ error: 'Validation failed', fields: parsed.error.fieldErrors }, 400);
|
|
338
|
+
}
|
|
339
|
+
try {
|
|
340
|
+
const job = await scheduleManager.create({
|
|
341
|
+
...parsed.data,
|
|
342
|
+
payload: JSON.stringify(parsed.data.payload),
|
|
343
|
+
} as any);
|
|
344
|
+
return c.json({ schedule: job }, 201);
|
|
345
|
+
} catch (err) {
|
|
346
|
+
return c.json({ error: (err as Error).message }, 400);
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
app.get('/api/v1/schedules', readRateLimit, (c) => c.json({ schedules: scheduleManager.list() }));
|
|
351
|
+
|
|
352
|
+
app.get('/api/v1/schedules/:id', readRateLimit, (c) => {
|
|
353
|
+
const job = scheduleManager.get(c.req.param('id'));
|
|
354
|
+
if (!job) return c.json({ error: 'Schedule not found' }, 404);
|
|
355
|
+
return c.json({ schedule: job });
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
app.patch('/api/v1/schedules/:id', messageRateLimit, async (c) => {
|
|
359
|
+
const body = await c.req.json();
|
|
360
|
+
try {
|
|
361
|
+
const updated = await scheduleManager.update(c.req.param('id'), body);
|
|
362
|
+
if (!updated) return c.json({ error: 'Schedule not found' }, 404);
|
|
363
|
+
return c.json({ schedule: updated });
|
|
364
|
+
} catch (err) {
|
|
365
|
+
return c.json({ error: (err as Error).message }, 400);
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
app.delete('/api/v1/schedules/:id', messageRateLimit, async (c) => {
|
|
370
|
+
const ok = await scheduleManager.remove(c.req.param('id'));
|
|
371
|
+
if (!ok) return c.json({ error: 'Schedule not found' }, 404);
|
|
372
|
+
return c.json({ ok: true });
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
app.post('/api/v1/schedules/:id/trigger', messageRateLimit, async (c) => {
|
|
376
|
+
const result = await scheduleManager.trigger(c.req.param('id'));
|
|
377
|
+
if (!result.success) return c.json({ error: result.error }, 404);
|
|
378
|
+
return c.json({ triggered: true });
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
// ── Memory ──
|
|
382
|
+
|
|
383
|
+
app.get('/api/v1/agents/:id/memory', readRateLimit, (c) => {
|
|
384
|
+
const agent = resolveAgent(c.req.param('id'));
|
|
385
|
+
if (!agent) return c.json({ error: 'Agent not found' }, 404);
|
|
386
|
+
return c.json({ memories: memoryManager.load(agent.id) });
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
app.post('/api/v1/agents/:id/memory', messageRateLimit, async (c) => {
|
|
390
|
+
const agent = resolveAgent(c.req.param('id'));
|
|
391
|
+
if (!agent) return c.json({ error: 'Agent not found' }, 404);
|
|
392
|
+
const body = await c.req.json();
|
|
393
|
+
const parsed = validate(SaveMemorySchema, body);
|
|
394
|
+
if (!parsed.success) {
|
|
395
|
+
return c.json({ error: 'Validation failed', fields: parsed.error.fieldErrors }, 400);
|
|
396
|
+
}
|
|
397
|
+
const entry = memoryManager.save(agent.id, parsed.data.category as any, parsed.data.key, parsed.data.value);
|
|
398
|
+
return c.json({ memory: entry }, 201);
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
app.post('/api/v1/agents/:id/memory/search', readRateLimit, async (c) => {
|
|
402
|
+
const agent = resolveAgent(c.req.param('id'));
|
|
403
|
+
if (!agent) return c.json({ error: 'Agent not found' }, 404);
|
|
404
|
+
const body = await c.req.json();
|
|
405
|
+
return c.json({ memories: memoryManager.search(agent.id, body.query || '') });
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
app.delete('/api/v1/agents/:id/memory/:key', messageRateLimit, (c) => {
|
|
409
|
+
const agent = resolveAgent(c.req.param('id'));
|
|
410
|
+
if (!agent) return c.json({ error: 'Agent not found' }, 404);
|
|
411
|
+
const ok = memoryManager.forget(agent.id, c.req.param('key'));
|
|
412
|
+
if (!ok) return c.json({ error: 'Memory not found' }, 404);
|
|
413
|
+
return c.json({ ok: true });
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
// ── Sub-agents ──
|
|
417
|
+
|
|
418
|
+
app.get('/api/v1/agents/:id/children', readRateLimit, (c) => {
|
|
419
|
+
const agent = resolveAgent(c.req.param('id'));
|
|
420
|
+
if (!agent) return c.json({ error: 'Agent not found' }, 404);
|
|
421
|
+
return c.json({ children: listChildren(agent.id) });
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
app.post('/api/v1/agents/:id/spawn', messageRateLimit, async (c) => {
|
|
425
|
+
const agent = resolveAgent(c.req.param('id'));
|
|
426
|
+
if (!agent) return c.json({ error: 'Agent not found' }, 404);
|
|
427
|
+
const body = await c.req.json();
|
|
428
|
+
const parsed = validate(SpawnSchema, body);
|
|
429
|
+
if (!parsed.success) {
|
|
430
|
+
return c.json({ error: 'Validation failed', fields: parsed.error.fieldErrors }, 400);
|
|
431
|
+
}
|
|
432
|
+
try {
|
|
433
|
+
const result = await spawnSubAgent({
|
|
434
|
+
parentId: agent.id,
|
|
435
|
+
prompt: parsed.data.prompt,
|
|
436
|
+
ttl: parsed.data.ttl_seconds ? parsed.data.ttl_seconds * 1000 : undefined,
|
|
437
|
+
systemPrompt: parsed.data.systemPrompt,
|
|
438
|
+
});
|
|
439
|
+
return c.json({ result });
|
|
440
|
+
} catch (err) {
|
|
441
|
+
return c.json({ error: (err as Error).message }, 500);
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
app.delete('/api/v1/agents/:id/children/:childId', messageRateLimit, (c) => {
|
|
446
|
+
const agent = resolveAgent(c.req.param('id'));
|
|
447
|
+
if (!agent) return c.json({ error: 'Agent not found' }, 404);
|
|
448
|
+
const ok = killChild(agent.id, c.req.param('childId'));
|
|
449
|
+
if (!ok) return c.json({ error: 'Child not found' }, 404);
|
|
450
|
+
return c.json({ ok: true });
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
// ── Bridge ──
|
|
454
|
+
|
|
455
|
+
let federatorRef: import('../bridge/federator').Federator | null = null;
|
|
456
|
+
|
|
457
|
+
export function setBridge(federator: import('../bridge/federator').Federator) {
|
|
458
|
+
federatorRef = federator;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
export function registerBridgeRoutes(hono: Hono) {
|
|
462
|
+
hono.get('/api/v1/bridge/status', (c) => {
|
|
463
|
+
if (!federatorRef) return c.json({ enabled: false });
|
|
464
|
+
return c.json(federatorRef.getStatus());
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
hono.get('/api/v1/bridge/peers', (c) => {
|
|
468
|
+
if (!federatorRef) return c.json({ peers: [] });
|
|
469
|
+
return c.json({ peers: federatorRef.getPeers() });
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
hono.post('/api/v1/bridge/peers', async (c) => {
|
|
473
|
+
const body = await c.req.json();
|
|
474
|
+
if (!body.url) return c.json({ error: 'url is required' }, 400);
|
|
475
|
+
if (!federatorRef) return c.json({ error: 'Bridge not enabled' }, 400);
|
|
476
|
+
await federatorRef.connectPeer(body.url, body.secret || '');
|
|
477
|
+
return c.json({ ok: true, url: body.url });
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
hono.delete('/api/v1/bridge/peers/:id', (c) => {
|
|
481
|
+
if (!federatorRef) return c.json({ error: 'Bridge not enabled' }, 400);
|
|
482
|
+
const id = c.req.param('id');
|
|
483
|
+
for (const peer of federatorRef.getPeers()) {
|
|
484
|
+
if (peer.id === id) {
|
|
485
|
+
federatorRef.disconnectPeer(peer.url);
|
|
486
|
+
return c.json({ ok: true });
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
return c.json({ error: 'Peer not found' }, 404);
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
hono.get('/api/v1/bridge/agents', (c) => {
|
|
493
|
+
if (!federatorRef) return c.json({ local: agentRegistry.list(), remote: [] });
|
|
494
|
+
return c.json(federatorRef.getAllAgents());
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
hono.post('/api/v1/bridge/agents/:id/messages', async (c) => {
|
|
498
|
+
const body = await c.req.json();
|
|
499
|
+
if (!body.text) return c.json({ error: 'text is required' }, 400);
|
|
500
|
+
if (!federatorRef) return c.json({ error: 'Bridge not enabled' }, 400);
|
|
501
|
+
try {
|
|
502
|
+
const result = await federatorRef.routeMessage(c.req.param('id'), body.text, body.sessionId, body.channelId);
|
|
503
|
+
if (result.error) return c.json({ error: result.error }, 500);
|
|
504
|
+
return c.json({ content: result.content });
|
|
505
|
+
} catch (err) {
|
|
506
|
+
return c.json({ error: (err as Error).message }, 500);
|
|
507
|
+
}
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
export default app;
|
|
512
|
+
export type AppType = typeof app;
|