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,221 @@
|
|
|
1
|
+
// Bridge Server — inbound WebSocket acceptor on /bridge path
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
BridgeMessage,
|
|
5
|
+
HelloPayload,
|
|
6
|
+
MessageForwardPayload,
|
|
7
|
+
MessageResponsePayload,
|
|
8
|
+
} from './protocol';
|
|
9
|
+
import {
|
|
10
|
+
createBridgeMessage,
|
|
11
|
+
parseBridgeMessage,
|
|
12
|
+
} from './protocol';
|
|
13
|
+
import { logger } from '../shared/logger';
|
|
14
|
+
import { agentRegistry } from '../agents/registry';
|
|
15
|
+
|
|
16
|
+
interface BridgeConnection {
|
|
17
|
+
gatewayId: string;
|
|
18
|
+
ws: { send: (data: string) => void; addEventListener: (type: string, fn: (ev: any) => void) => void; close: (code?: number, reason?: string) => void; readyState?: number };
|
|
19
|
+
connectedAt: number;
|
|
20
|
+
lastPong: number;
|
|
21
|
+
agents: { id: string; name: string; status: string }[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class BridgeServer {
|
|
25
|
+
private connections = new Map<string, BridgeConnection>(); // gatewayId -> conn
|
|
26
|
+
private secret: string;
|
|
27
|
+
private ownGatewayId: string;
|
|
28
|
+
private pongTimeoutMs = 60_000;
|
|
29
|
+
private pendingResponses = new Map<string, (msg: BridgeMessage) => void>();
|
|
30
|
+
|
|
31
|
+
constructor(ownGatewayId: string, secret: string) {
|
|
32
|
+
this.ownGatewayId = ownGatewayId;
|
|
33
|
+
this.secret = secret;
|
|
34
|
+
|
|
35
|
+
// Health check: disconnect stale peers
|
|
36
|
+
setInterval(() => {
|
|
37
|
+
const now = Date.now();
|
|
38
|
+
for (const [id, conn] of this.connections) {
|
|
39
|
+
if (now - conn.lastPong > this.pongTimeoutMs) {
|
|
40
|
+
logger.warn({ peerId: id }, 'Bridge peer health check failed, disconnecting');
|
|
41
|
+
conn.ws.close(4008, 'Health check timeout');
|
|
42
|
+
this.connections.delete(id);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}, 30_000);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Handle an upgraded WebSocket connection for /bridge (called from Bun.serve websocket handlers) */
|
|
49
|
+
handleConnection(ws: { send: (data: string) => void; addEventListener: (type: string, fn: (ev: any) => void) => void; close: (code?: number, reason?: string) => void }, secret?: string): boolean {
|
|
50
|
+
if (secret !== this.secret) {
|
|
51
|
+
logger.warn('Bridge connection rejected: invalid secret');
|
|
52
|
+
ws.close(4001, 'Unauthorized');
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const conn: BridgeConnection = {
|
|
57
|
+
gatewayId: '',
|
|
58
|
+
ws,
|
|
59
|
+
connectedAt: Date.now(),
|
|
60
|
+
lastPong: Date.now(),
|
|
61
|
+
agents: [],
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
ws.addEventListener('message', (event) => {
|
|
65
|
+
const msg = parseBridgeMessage(event.data as string);
|
|
66
|
+
if (!msg) {
|
|
67
|
+
ws.send(JSON.stringify({ error: 'Invalid message' }));
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
void this.handleMessage(conn, msg);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
ws.addEventListener('close', () => {
|
|
74
|
+
if (conn.gatewayId) {
|
|
75
|
+
this.connections.delete(conn.gatewayId);
|
|
76
|
+
logger.info({ peerId: conn.gatewayId }, 'Bridge peer disconnected');
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Legacy attach — no-op for Bun.serve(), use handleConnection instead */
|
|
84
|
+
attach(_server: unknown): void {
|
|
85
|
+
logger.info('Bridge server ready on /bridge (via Bun.serve)');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private async handleMessage(conn: BridgeConnection, msg: BridgeMessage): Promise<void> {
|
|
89
|
+
switch (msg.type) {
|
|
90
|
+
case 'hello': {
|
|
91
|
+
const payload = msg.payload as HelloPayload;
|
|
92
|
+
conn.gatewayId = msg.gatewayId;
|
|
93
|
+
conn.agents = payload.agents || [];
|
|
94
|
+
conn.lastPong = Date.now();
|
|
95
|
+
this.connections.set(msg.gatewayId, conn);
|
|
96
|
+
|
|
97
|
+
logger.info({ peerId: msg.gatewayId, agentCount: conn.agents.length }, 'Bridge peer registered');
|
|
98
|
+
|
|
99
|
+
// Send hello-ack
|
|
100
|
+
const ack = createBridgeMessage('hello-ack', this.ownGatewayId, {
|
|
101
|
+
gatewayId: this.ownGatewayId,
|
|
102
|
+
agents: agentRegistry.list().map((a) => ({ id: a.id, name: a.name, status: a.state })),
|
|
103
|
+
});
|
|
104
|
+
conn.ws.send(JSON.stringify(ack));
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
case 'agent-register':
|
|
109
|
+
case 'agent-update': {
|
|
110
|
+
conn.lastPong = Date.now();
|
|
111
|
+
// Update peer's agent list
|
|
112
|
+
if (msg.type === 'agent-register') {
|
|
113
|
+
conn.agents = (msg.payload as { agents: BridgeConnection['agents'] }).agents || [];
|
|
114
|
+
}
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
case 'message-forward': {
|
|
119
|
+
conn.lastPong = Date.now();
|
|
120
|
+
await this.handleMessageForward(conn, msg);
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
case 'message-response': {
|
|
125
|
+
conn.lastPong = Date.now();
|
|
126
|
+
const payload = msg.payload as MessageResponsePayload;
|
|
127
|
+
const handler = this.pendingResponses.get(payload.requestMessageId);
|
|
128
|
+
if (handler) {
|
|
129
|
+
handler(msg);
|
|
130
|
+
this.pendingResponses.delete(payload.requestMessageId);
|
|
131
|
+
}
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
case 'health-ping': {
|
|
136
|
+
conn.lastPong = Date.now();
|
|
137
|
+
conn.ws.send(JSON.stringify(createBridgeMessage('health-pong', this.ownGatewayId, {})));
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
case 'health-pong': {
|
|
142
|
+
conn.lastPong = Date.now();
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
case 'unsubscribe': {
|
|
147
|
+
this.connections.delete(conn.gatewayId);
|
|
148
|
+
conn.ws.close();
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private async handleMessageForward(conn: BridgeConnection, msg: BridgeMessage): Promise<void> {
|
|
155
|
+
const payload = msg.payload as MessageForwardPayload;
|
|
156
|
+
const { sendMessage } = await import('../agents/runtime');
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
const result = await sendMessage({
|
|
160
|
+
agentId: payload.targetAgentId,
|
|
161
|
+
sessionId: payload.sessionId,
|
|
162
|
+
text: payload.text,
|
|
163
|
+
channelId: payload.channelId,
|
|
164
|
+
});
|
|
165
|
+
const response = createBridgeMessage('message-response', this.ownGatewayId, {
|
|
166
|
+
requestMessageId: msg.messageId,
|
|
167
|
+
content: result.content,
|
|
168
|
+
} as MessageResponsePayload);
|
|
169
|
+
conn.ws.send(JSON.stringify(response));
|
|
170
|
+
} catch (err) {
|
|
171
|
+
const response = createBridgeMessage('message-response', this.ownGatewayId, {
|
|
172
|
+
requestMessageId: msg.messageId,
|
|
173
|
+
content: '',
|
|
174
|
+
error: (err as Error).message,
|
|
175
|
+
} as MessageResponsePayload);
|
|
176
|
+
conn.ws.send(JSON.stringify(response));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Send a message to a specific peer gateway and wait for response
|
|
181
|
+
async sendToPeer(
|
|
182
|
+
gatewayId: string,
|
|
183
|
+
msg: BridgeMessage,
|
|
184
|
+
timeoutMs = 30_000,
|
|
185
|
+
): Promise<BridgeMessage> {
|
|
186
|
+
const conn = this.connections.get(gatewayId);
|
|
187
|
+
if (!conn || (conn.ws as any).readyState !== 1) {
|
|
188
|
+
throw new Error(`Peer not connected: ${gatewayId}`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return new Promise((resolve, reject) => {
|
|
192
|
+
const timer = setTimeout(() => {
|
|
193
|
+
this.pendingResponses.delete(msg.messageId);
|
|
194
|
+
reject(new Error('Bridge request timeout'));
|
|
195
|
+
}, timeoutMs);
|
|
196
|
+
this.pendingResponses.set(msg.messageId, (response) => {
|
|
197
|
+
clearTimeout(timer);
|
|
198
|
+
resolve(response);
|
|
199
|
+
});
|
|
200
|
+
conn.ws.send(JSON.stringify(msg));
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Broadcast agent update to all connected peers
|
|
205
|
+
broadcastAgentUpdate(agentId: string, status: string): void {
|
|
206
|
+
const msg = createBridgeMessage('agent-update', this.ownGatewayId, { agentId, status });
|
|
207
|
+
for (const [, conn] of this.connections) {
|
|
208
|
+
if ((conn.ws as any).readyState === 1) {
|
|
209
|
+
conn.ws.send(JSON.stringify(msg));
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
getConnections(): Map<string, BridgeConnection> {
|
|
215
|
+
return this.connections;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
getConnection(gatewayId: string): BridgeConnection | undefined {
|
|
219
|
+
return this.connections.get(gatewayId);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { MessageEnvelope } from '../shared/types';
|
|
2
|
+
|
|
3
|
+
export interface InlineKeyboardButton {
|
|
4
|
+
text: string;
|
|
5
|
+
url?: string;
|
|
6
|
+
callback_data?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface InlineKeyboard {
|
|
10
|
+
buttons: InlineKeyboardButton[][];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface MediaAttachment {
|
|
14
|
+
type: 'photo' | 'document' | 'voice' | 'video';
|
|
15
|
+
data: Buffer | string; // Buffer or URL/file path
|
|
16
|
+
filename?: string;
|
|
17
|
+
caption?: string;
|
|
18
|
+
mimeType?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface StreamChunk {
|
|
22
|
+
text: string;
|
|
23
|
+
done: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ChannelAdapter {
|
|
27
|
+
name: string;
|
|
28
|
+
start(): Promise<void>;
|
|
29
|
+
stop(): Promise<void>;
|
|
30
|
+
onMessage(handler: (envelope: MessageEnvelope) => void): void;
|
|
31
|
+
send(envelope: MessageEnvelope, response: string): Promise<void>;
|
|
32
|
+
sendMedia?(envelope: MessageEnvelope, media: MediaAttachment): Promise<void>;
|
|
33
|
+
sendWithKeyboard?(
|
|
34
|
+
envelope: MessageEnvelope,
|
|
35
|
+
response: string,
|
|
36
|
+
keyboard: InlineKeyboard,
|
|
37
|
+
): Promise<void>;
|
|
38
|
+
startTyping?(chatId: string): Promise<void>;
|
|
39
|
+
stopTyping?(chatId: string): void;
|
|
40
|
+
startStreaming?(envelope: MessageEnvelope, initialText?: string): Promise<string>;
|
|
41
|
+
editStreamingMessage?(messageId: string, text: string): Promise<void>;
|
|
42
|
+
finishStreaming?(messageId: string, finalText: string): Promise<void>;
|
|
43
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import type { MessageEnvelope } from '../shared/types';
|
|
2
|
+
import type { ChannelAdapter } from './base';
|
|
3
|
+
import { sendMessage } from '../agents/runtime';
|
|
4
|
+
import { logger } from '../shared/logger';
|
|
5
|
+
|
|
6
|
+
interface RouteRule {
|
|
7
|
+
channelType: string;
|
|
8
|
+
channelId?: string;
|
|
9
|
+
agentName: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface RouterConfig {
|
|
13
|
+
routes: RouteRule[];
|
|
14
|
+
defaultAgent: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
class ChannelRouter {
|
|
18
|
+
private channels = new Map<string, ChannelAdapter>();
|
|
19
|
+
private rules: RouteRule[] = [];
|
|
20
|
+
private defaultAgent = 'example';
|
|
21
|
+
|
|
22
|
+
configure(config: RouterConfig): void {
|
|
23
|
+
this.rules = config.routes;
|
|
24
|
+
this.defaultAgent = config.defaultAgent;
|
|
25
|
+
logger.info(
|
|
26
|
+
{ rules: this.rules.length, default: this.defaultAgent },
|
|
27
|
+
'Channel router configured',
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
registerChannel(channel: ChannelAdapter): void {
|
|
32
|
+
this.channels.set(channel.name, channel);
|
|
33
|
+
logger.info({ channel: channel.name }, 'Channel registered');
|
|
34
|
+
|
|
35
|
+
channel.onMessage((envelope) => {
|
|
36
|
+
this.routeMessage(channel, envelope).catch((err) => {
|
|
37
|
+
logger.error({ error: (err as Error).message, envelope: envelope.id }, 'Route error');
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private async routeMessage(channel: ChannelAdapter, envelope: MessageEnvelope): Promise<void> {
|
|
43
|
+
const channelType = (envelope.metadata?.channelType as string) || channel.name;
|
|
44
|
+
|
|
45
|
+
// Find matching route
|
|
46
|
+
let agentName: string | undefined;
|
|
47
|
+
|
|
48
|
+
for (const rule of this.rules) {
|
|
49
|
+
if (rule.channelType === channelType) {
|
|
50
|
+
if (!rule.channelId || rule.channelId === envelope.channelId) {
|
|
51
|
+
agentName = rule.agentName;
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!agentName) {
|
|
58
|
+
agentName = this.defaultAgent;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
logger.info(
|
|
62
|
+
{
|
|
63
|
+
envelope: envelope.id,
|
|
64
|
+
from: envelope.from,
|
|
65
|
+
channel: channelType,
|
|
66
|
+
agent: agentName,
|
|
67
|
+
},
|
|
68
|
+
'Routing message',
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
const result = await sendMessage({
|
|
73
|
+
agentId: agentName,
|
|
74
|
+
text: envelope.text,
|
|
75
|
+
channelId: envelope.channelId,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const reply = result.content || 'I processed your request but had no text response to share.';
|
|
79
|
+
if (!result.content) {
|
|
80
|
+
logger.warn({ agent: agentName, sessionId: result.sessionId }, 'Agent returned empty content, sending fallback');
|
|
81
|
+
}
|
|
82
|
+
await channel.send(envelope, reply);
|
|
83
|
+
} catch (err) {
|
|
84
|
+
const errorMsg = `Error: ${(err as Error).message}`;
|
|
85
|
+
logger.error({ error: errorMsg, agent: agentName }, 'Agent processing failed');
|
|
86
|
+
await channel.send(envelope, errorMsg);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
getChannel(name: string): ChannelAdapter | undefined {
|
|
91
|
+
return this.channels.get(name);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
listChannels(): { name: string; running: boolean }[] {
|
|
95
|
+
return [...this.channels.keys()].map((name) => ({
|
|
96
|
+
name,
|
|
97
|
+
running: true,
|
|
98
|
+
}));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async startChannel(name: string): Promise<boolean> {
|
|
102
|
+
const channel = this.channels.get(name);
|
|
103
|
+
if (!channel) return false;
|
|
104
|
+
await channel.start();
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async stopChannel(name: string): Promise<boolean> {
|
|
109
|
+
const channel = this.channels.get(name);
|
|
110
|
+
if (!channel) return false;
|
|
111
|
+
await channel.stop();
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async stopAll(): Promise<void> {
|
|
116
|
+
for (const channel of this.channels.values()) {
|
|
117
|
+
await channel.stop();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export const channelRouter = new ChannelRouter();
|