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,78 @@
1
+ import type { ToolDefinition, ToolContext, ToolResult } from '../../shared/types';
2
+ import { readFileSync, existsSync } from 'fs';
3
+ import { resolve, join } from 'path';
4
+
5
+ // Configurable allowed paths (default: current working directory)
6
+ const DEFAULT_ALLOWED_PATHS = [process.cwd()];
7
+ const allowedPaths: string[] = (globalThis as any).__SANDBOX_ALLOWED_PATHS || DEFAULT_ALLOWED_PATHS;
8
+
9
+ function isPathAllowed(inputPath: string): { ok: boolean; resolved: string } {
10
+ const resolved = resolve(inputPath);
11
+
12
+ // Block path traversal
13
+ if (inputPath.includes('..')) {
14
+ return { ok: false, resolved };
15
+ }
16
+
17
+ for (const allowed of allowedPaths) {
18
+ const allowedResolved = resolve(allowed);
19
+ if (resolved.startsWith(allowedResolved + '/') || resolved === allowedResolved) {
20
+ return { ok: true, resolved };
21
+ }
22
+ }
23
+
24
+ return { ok: false, resolved };
25
+ }
26
+
27
+ export function setAllowedPaths(paths: string[]) {
28
+ allowedPaths.length = 0;
29
+ allowedPaths.push(...paths);
30
+ }
31
+
32
+ const fileReadTool: ToolDefinition = {
33
+ name: 'file-read',
34
+ description: 'Read a file. Returns content with optional line limits. Paths are sandboxed to allowed directories.',
35
+ parameters: {
36
+ type: 'object',
37
+ properties: {
38
+ path: { type: 'string', description: 'File path to read' },
39
+ offset: { type: 'number', description: 'Line number to start from (1-indexed)' },
40
+ limit: { type: 'number', description: 'Maximum number of lines to read' },
41
+ },
42
+ required: ['path'],
43
+ },
44
+ category: 'filesystem',
45
+ timeoutMs: 5000,
46
+ maxOutputBytes: 500_000,
47
+ execute: async (params): Promise<ToolResult> => {
48
+ const filePath = String(params.path);
49
+
50
+ const { ok, resolved } = isPathAllowed(filePath);
51
+ if (!ok) {
52
+ return { content: '', error: `Path not allowed (sandboxed): ${resolved}. Allowed paths: ${allowedPaths.join(', ')}` };
53
+ }
54
+
55
+ if (!existsSync(resolved)) {
56
+ return { content: '', error: `File not found: ${resolved}` };
57
+ }
58
+
59
+ try {
60
+ const content = readFileSync(resolved, 'utf-8');
61
+ const lines = content.split('\n');
62
+ const offset = Number(params.offset) || 1;
63
+ const limit = Number(params.limit) || lines.length;
64
+
65
+ const sliced = lines.slice(offset - 1, offset - 1 + limit);
66
+ const result = sliced.join('\n');
67
+
68
+ const truncated = sliced.length < lines.length;
69
+ return {
70
+ content: result + (truncated ? `\n\n[Showing lines ${offset}-${offset + sliced.length - 1} of ${lines.length}]` : ''),
71
+ };
72
+ } catch (err: unknown) {
73
+ return { content: '', error: (err as Error).message };
74
+ }
75
+ },
76
+ };
77
+
78
+ export default fileReadTool;
@@ -0,0 +1,64 @@
1
+ import type { ToolDefinition, ToolContext, ToolResult } from '../../shared/types';
2
+ import { writeFileSync, mkdirSync } from 'fs';
3
+ import { dirname, resolve } from 'path';
4
+
5
+ // Configurable allowed paths (default: current working directory)
6
+ const DEFAULT_ALLOWED_PATHS = [process.cwd()];
7
+ const allowedPaths: string[] = (globalThis as any).__SANDBOX_ALLOWED_PATHS || DEFAULT_ALLOWED_PATHS;
8
+
9
+ function isPathAllowed(inputPath: string): { ok: boolean; resolved: string } {
10
+ const resolved = resolve(inputPath);
11
+
12
+ // Block path traversal
13
+ if (inputPath.includes('..')) {
14
+ return { ok: false, resolved };
15
+ }
16
+
17
+ for (const allowed of allowedPaths) {
18
+ const allowedResolved = resolve(allowed);
19
+ if (resolved.startsWith(allowedResolved + '/') || resolved === allowedResolved) {
20
+ return { ok: true, resolved };
21
+ }
22
+ }
23
+
24
+ return { ok: false, resolved };
25
+ }
26
+
27
+ export function setAllowedPaths(paths: string[]) {
28
+ allowedPaths.length = 0;
29
+ allowedPaths.push(...paths);
30
+ }
31
+
32
+ const fileWriteTool: ToolDefinition = {
33
+ name: 'file-write',
34
+ description: 'Write content to a file. Creates directories if needed. Paths are sandboxed to allowed directories.',
35
+ parameters: {
36
+ type: 'object',
37
+ properties: {
38
+ path: { type: 'string', description: 'File path to write' },
39
+ content: { type: 'string', description: 'Content to write' },
40
+ },
41
+ required: ['path', 'content'],
42
+ },
43
+ category: 'filesystem',
44
+ timeoutMs: 5000,
45
+ execute: async (params): Promise<ToolResult> => {
46
+ const filePath = String(params.path);
47
+ const content = String(params.content);
48
+
49
+ const { ok, resolved } = isPathAllowed(filePath);
50
+ if (!ok) {
51
+ return { content: '', error: `Path not allowed (sandboxed): ${resolved}. Allowed paths: ${allowedPaths.join(', ')}` };
52
+ }
53
+
54
+ try {
55
+ mkdirSync(dirname(resolved), { recursive: true });
56
+ writeFileSync(resolved, content, 'utf-8');
57
+ return { content: `Written ${content.length} bytes to ${resolved}` };
58
+ } catch (err: unknown) {
59
+ return { content: '', error: (err as Error).message };
60
+ }
61
+ },
62
+ };
63
+
64
+ export default fileWriteTool;
@@ -0,0 +1,63 @@
1
+ import type { ToolDefinition, ToolContext, ToolResult } from '../../shared/types';
2
+
3
+ const httpRequestTool: ToolDefinition = {
4
+ name: 'http-request',
5
+ description: 'Make an HTTP request. Returns status code, headers, and body.',
6
+ parameters: {
7
+ type: 'object',
8
+ properties: {
9
+ url: { type: 'string', description: 'The URL to request' },
10
+ method: { type: 'string', description: 'HTTP method (default GET)' },
11
+ headers: { type: 'object', description: 'Request headers' },
12
+ body: { type: 'string', description: 'Request body (for POST/PUT)' },
13
+ timeout: { type: 'number', description: 'Timeout in milliseconds (default 15000)' },
14
+ },
15
+ required: ['url'],
16
+ },
17
+ category: 'system',
18
+ timeoutMs: 30000,
19
+ maxOutputBytes: 500_000,
20
+ execute: async (params, ctx): Promise<ToolResult> => {
21
+ const url = String(params.url);
22
+ const method = String(params.method || 'GET').toUpperCase();
23
+ const headers = (params.headers as Record<string, string>) || {};
24
+ const body = params.body ? String(params.body) : undefined;
25
+ const timeout = Number(params.timeout) || 15000;
26
+
27
+ const controller = new AbortController();
28
+ const timer = setTimeout(() => controller.abort(), timeout);
29
+ const signal = ctx.signal || controller.signal;
30
+
31
+ try {
32
+ const response = await fetch(url, {
33
+ method,
34
+ headers,
35
+ body,
36
+ signal,
37
+ });
38
+
39
+ const responseHeaders: Record<string, string> = {};
40
+ response.headers.forEach((v, k) => {
41
+ responseHeaders[k] = v;
42
+ });
43
+
44
+ const responseText = await response.text();
45
+
46
+ return {
47
+ content: JSON.stringify({
48
+ status: response.status,
49
+ statusText: response.statusText,
50
+ headers: responseHeaders,
51
+ body: responseText.slice(0, 100_000),
52
+ }, null, 2),
53
+ };
54
+ } catch (err: unknown) {
55
+ const e = err as Error;
56
+ return { content: '', error: e.name === 'AbortError' ? `Request timed out after ${timeout}ms` : e.message };
57
+ } finally {
58
+ clearTimeout(timer);
59
+ }
60
+ },
61
+ };
62
+
63
+ export default httpRequestTool;
@@ -0,0 +1,71 @@
1
+ import type { ToolDefinition, ToolContext, ToolResult } from '../shared/types';
2
+ import { memoryManager, type MemoryCategory } from '../../memory/store';
3
+ import { logger } from '../../shared/logger';
4
+
5
+ const VALID_CATEGORIES: MemoryCategory[] = ['identity', 'preference', 'fact', 'lesson', 'context'];
6
+
7
+ export const memorySaveTool: ToolDefinition = {
8
+ name: 'memory-save',
9
+ description: 'Save a memory entry for later recall. Use this to remember important facts, preferences, or context.',
10
+ parameters: {
11
+ type: 'object',
12
+ properties: {
13
+ key: { type: 'string', description: 'Short identifier for the memory' },
14
+ value: { type: 'string', description: 'The content to remember' },
15
+ category: {
16
+ type: 'string',
17
+ enum: VALID_CATEGORIES,
18
+ description: 'Category: identity (who), preference (likes/dislikes), fact (things known), lesson (learned rules), context (situational)',
19
+ },
20
+ },
21
+ required: ['key', 'value'],
22
+ },
23
+ execute: async (params: Record<string, unknown>, ctx: ToolContext): Promise<ToolResult> => {
24
+ const { key, value, category } = params;
25
+ if (!key || !value) return { content: 'Error: key and value are required', error: 'missing fields' };
26
+
27
+ const cat = (VALID_CATEGORIES.includes(category as MemoryCategory) ? category : 'fact') as MemoryCategory;
28
+ const entry = memoryManager.save(ctx.agentId, cat, String(key), String(value));
29
+ return { content: `Memory saved: [${entry.category}] ${entry.key}` };
30
+ },
31
+ };
32
+
33
+ export const memorySearchTool: ToolDefinition = {
34
+ name: 'memory-search',
35
+ description: 'Search through saved memories by keyword.',
36
+ parameters: {
37
+ type: 'object',
38
+ properties: {
39
+ query: { type: 'string', description: 'Search terms' },
40
+ },
41
+ required: ['query'],
42
+ },
43
+ execute: async (params: Record<string, unknown>, ctx: ToolContext): Promise<ToolResult> => {
44
+ const results = memoryManager.search(ctx.agentId, String(params.query || ''));
45
+ if (results.length === 0) return { content: 'No matching memories found.' };
46
+ const lines = results.map((r) => `[${r.category}] ${r.key}: ${typeof r.value === 'string' ? r.value : JSON.stringify(r.value)}`);
47
+ return { content: lines.join('\n') };
48
+ },
49
+ };
50
+
51
+ export const memoryListTool: ToolDefinition = {
52
+ name: 'memory-list',
53
+ description: 'List all saved memories, optionally filtered by category.',
54
+ parameters: {
55
+ type: 'object',
56
+ properties: {
57
+ category: { type: 'string', enum: VALID_CATEGORIES, description: 'Optional category filter' },
58
+ },
59
+ },
60
+ execute: async (params: Record<string, unknown>, ctx: ToolContext): Promise<ToolResult> => {
61
+ let entries;
62
+ if (params.category && VALID_CATEGORIES.includes(params.category as MemoryCategory)) {
63
+ entries = memoryManager.loadByCategory(ctx.agentId, params.category as MemoryCategory);
64
+ } else {
65
+ entries = memoryManager.load(ctx.agentId);
66
+ }
67
+ if (entries.length === 0) return { content: 'No memories saved.' };
68
+ const lines = entries.map((e) => `[${e.category}] ${e.key}: ${typeof e.value === 'string' ? e.value : JSON.stringify(e.value)}`);
69
+ return { content: lines.join('\n') };
70
+ },
71
+ };
@@ -0,0 +1,94 @@
1
+ import type { ToolDefinition, ToolContext, ToolResult } from '../../shared/types';
2
+ import { resolve } from 'path';
3
+
4
+ // Configurable blocked commands
5
+ const DEFAULT_BLOCKED_COMMANDS = ['rm -rf /', 'mkfs', 'dd if='];
6
+ const blockedCommands: string[] = [...DEFAULT_BLOCKED_COMMANDS];
7
+
8
+ // Configurable allowed directories for cwd
9
+ const DEFAULT_ALLOWED_DIRS: string[] = [];
10
+ let allowedDirectories: string[] = [...DEFAULT_ALLOWED_DIRS];
11
+
12
+ export function setBlockedCommands(commands: string[]) {
13
+ blockedCommands.length = 0;
14
+ blockedCommands.push(...commands);
15
+ }
16
+
17
+ export function setAllowedDirectories(dirs: string[]) {
18
+ allowedDirectories = dirs.map(d => resolve(d));
19
+ }
20
+
21
+ function isCommandBlocked(command: string): boolean {
22
+ const lowerCmd = command.toLowerCase().trim();
23
+ return blockedCommands.some(blocked => lowerCmd.includes(blocked.toLowerCase()));
24
+ }
25
+
26
+ function isCwdAllowed(cwd: string | undefined): boolean {
27
+ if (!cwd || allowedDirectories.length === 0) return true; // no cwd or no restrictions
28
+ const resolved = resolve(cwd);
29
+ return allowedDirectories.some(allowed => resolved.startsWith(allowed + '/') || resolved === allowed);
30
+ }
31
+
32
+ const shellTool: ToolDefinition = {
33
+ name: 'shell',
34
+ description: 'Execute a shell command. Certain dangerous commands are blocked. cwd can be restricted to allowed directories.',
35
+ parameters: {
36
+ type: 'object',
37
+ properties: {
38
+ command: { type: 'string', description: 'The shell command to execute' },
39
+ timeout: { type: 'number', description: 'Timeout in milliseconds (default 30000)' },
40
+ cwd: { type: 'string', description: 'Working directory' },
41
+ },
42
+ required: ['command'],
43
+ },
44
+ category: 'system',
45
+ timeoutMs: 60000,
46
+ execute: async (params, _ctx): Promise<ToolResult> => {
47
+ const command = String(params.command);
48
+ const timeout = Number(params.timeout) || 30000;
49
+ const cwd = params.cwd ? String(params.cwd) : undefined;
50
+
51
+ // Check blocked commands
52
+ if (isCommandBlocked(command)) {
53
+ return { content: '', error: `Command blocked (sandboxed): "${command}"` };
54
+ }
55
+
56
+ // Check allowed directories for cwd
57
+ if (cwd && !isCwdAllowed(cwd)) {
58
+ return { content: '', error: `Working directory not allowed (sandboxed): ${cwd}` };
59
+ }
60
+
61
+ try {
62
+ const proc = Bun.spawn(['sh', '-c', command], {
63
+ cwd,
64
+ stdout: 'pipe',
65
+ stderr: 'pipe',
66
+ });
67
+
68
+ const exitCode = await Promise.race([
69
+ proc.exited,
70
+ new Promise<null>((_, reject) => setTimeout(() => { proc.kill(); reject(new Error('timeout')); }, timeout)),
71
+ ]);
72
+
73
+ const stdout = await new Response(proc.stdout).text();
74
+ const stderr = await new Response(proc.stderr).text();
75
+ const output = [stdout.trim(), stderr.trim()].filter(Boolean).join('\n');
76
+
77
+ if (exitCode === null) {
78
+ return { content: output, error: `Command timed out after ${timeout}ms` };
79
+ }
80
+ if (exitCode !== 0) {
81
+ return { content: output || '', error: `Exit code ${exitCode}` };
82
+ }
83
+ return { content: output || '(no output)' };
84
+ } catch (err: unknown) {
85
+ const e = err as { message?: string };
86
+ if (e.message === 'timeout') {
87
+ return { content: '', error: `Command timed out after ${timeout}ms` };
88
+ }
89
+ return { content: '', error: e.message || 'Unknown error' };
90
+ }
91
+ },
92
+ };
93
+
94
+ export default shellTool;
@@ -0,0 +1,22 @@
1
+ import type { ToolDefinition, ToolContext, ToolResult } from '../../shared/types';
2
+ import { logger } from '../../shared/logger';
3
+
4
+ const webSearchTool: ToolDefinition = {
5
+ name: 'web-search',
6
+ description: 'Search the web. (Placeholder — Brave API integration coming soon.)',
7
+ parameters: {
8
+ type: 'object',
9
+ properties: {
10
+ query: { type: 'string', description: 'Search query' },
11
+ },
12
+ required: ['query'],
13
+ },
14
+ category: 'search',
15
+ timeoutMs: 5000,
16
+ execute: async (params): Promise<ToolResult> => {
17
+ logger.info({ query: params.query }, 'Web search requested (not yet configured)');
18
+ return { content: 'Web search is not yet configured. Configure a Brave API key to enable searching.' };
19
+ },
20
+ };
21
+
22
+ export default webSearchTool;
@@ -0,0 +1,126 @@
1
+ import type { ToolDefinition, ToolContext, ToolResult } from '../shared/types';
2
+ import { logger } from '../shared/logger';
3
+
4
+ // ── Tool result cache ──
5
+
6
+ interface CacheEntry {
7
+ result: ToolResult;
8
+ expiresAt: number;
9
+ }
10
+
11
+ const cache = new Map<string, CacheEntry>();
12
+
13
+ function cacheKey(toolName: string, params: Record<string, unknown>): string {
14
+ return `${toolName}:${JSON.stringify(params)}`;
15
+ }
16
+
17
+ function getCached(tool: ToolDefinition, params: Record<string, unknown>): ToolResult | null {
18
+ if (!tool.cacheable) return null;
19
+ const entry = cache.get(cacheKey(tool.name, params));
20
+ if (!entry) return null;
21
+ if (Date.now() > entry.expiresAt) {
22
+ cache.delete(cacheKey(tool.name, params));
23
+ return null;
24
+ }
25
+ return entry.result;
26
+ }
27
+
28
+ function setCache(tool: ToolDefinition, params: Record<string, unknown>, result: ToolResult): void {
29
+ if (!tool.cacheable) return;
30
+ cache.set(cacheKey(tool.name, params), {
31
+ result,
32
+ expiresAt: Date.now() + (tool.cacheTtlMs ?? 60_000),
33
+ });
34
+ }
35
+
36
+ // ── Single tool execution ──
37
+
38
+ export async function executeTool(
39
+ tool: ToolDefinition,
40
+ params: Record<string, unknown>,
41
+ ctx: Partial<ToolContext> & { agentId: string; sessionId: string },
42
+ ): Promise<ToolResult> {
43
+ // Check cache first
44
+ const cached = getCached(tool, params);
45
+ if (cached) {
46
+ logger.debug({ tool: tool.name }, 'Tool cache hit');
47
+ return cached;
48
+ }
49
+
50
+ const timeout = tool.timeoutMs ?? 30_000;
51
+ const maxBytes = tool.maxOutputBytes ?? 1_000_000;
52
+
53
+ const controller = new AbortController();
54
+ const timer = setTimeout(() => controller.abort(), timeout);
55
+
56
+ // Link parent signal if provided
57
+ if (ctx.signal) {
58
+ if (ctx.signal.aborted) {
59
+ clearTimeout(timer);
60
+ return { content: '', error: 'Parent signal already aborted' };
61
+ }
62
+ ctx.signal.addEventListener('abort', () => controller.abort(), { once: true });
63
+ }
64
+
65
+ try {
66
+ const fullCtx: ToolContext = {
67
+ agentId: ctx.agentId,
68
+ sessionId: ctx.sessionId,
69
+ signal: controller.signal,
70
+ };
71
+
72
+ const startMs = Date.now();
73
+ const result = await tool.execute(params, fullCtx);
74
+ const durationMs = Date.now() - startMs;
75
+
76
+ if (result.content.length > maxBytes) {
77
+ result.content = result.content.slice(0, maxBytes) + '\n[TRUNCATED]';
78
+ }
79
+
80
+ logger.info({ tool: tool.name, durationMs, cached: false }, 'Tool executed');
81
+ setCache(tool, params, result);
82
+ return result;
83
+ } catch (err) {
84
+ const error = err as Error;
85
+ if (error.name === 'AbortError') {
86
+ return { content: '', error: `Tool timed out after ${timeout}ms` };
87
+ }
88
+ logger.error({ tool: tool.name, error: error.message }, 'Tool execution failed');
89
+ return { content: '', error: error.message };
90
+ } finally {
91
+ clearTimeout(timer);
92
+ }
93
+ }
94
+
95
+ // ── Parallel tool execution ──
96
+
97
+ export interface ParallelToolCall {
98
+ tool: ToolDefinition;
99
+ params: Record<string, unknown>;
100
+ }
101
+
102
+ export async function executeToolsParallel(
103
+ calls: ParallelToolCall[],
104
+ ctx: Partial<ToolContext> & { agentId: string; sessionId: string },
105
+ ): Promise<ToolResult[]> {
106
+ if (calls.length === 0) return [];
107
+
108
+ // Execute all in parallel — each tool gets its own AbortController
109
+ const results = await Promise.all(
110
+ calls.map((call) => executeTool(call.tool, call.params, ctx)),
111
+ );
112
+
113
+ return results;
114
+ }
115
+
116
+ // ── Cache management ──
117
+
118
+ export function clearToolCache(toolName?: string): void {
119
+ if (!toolName) {
120
+ cache.clear();
121
+ return;
122
+ }
123
+ for (const key of cache.keys()) {
124
+ if (key.startsWith(`${toolName}:`)) cache.delete(key);
125
+ }
126
+ }
@@ -0,0 +1,56 @@
1
+ import type { ToolDefinition, ToolContext, ToolResult } from '../shared/types';
2
+ import { logger } from '../shared/logger';
3
+
4
+ class ToolRegistry {
5
+ private tools = new Map<string, ToolDefinition>();
6
+ private perAgent = new Map<string, Set<string>>();
7
+
8
+ register(def: ToolDefinition): void {
9
+ if (this.tools.has(def.name)) {
10
+ throw new Error(`Tool already registered: ${def.name}`);
11
+ }
12
+ this.tools.set(def.name, def);
13
+ logger.debug({ tool: def.name }, 'Tool registered');
14
+ }
15
+
16
+ get(name: string): ToolDefinition | undefined {
17
+ return this.tools.get(name);
18
+ }
19
+
20
+ list(): ToolDefinition[] {
21
+ return [...this.tools.values()];
22
+ }
23
+
24
+ listForAgent(agentId: string): ToolDefinition[] {
25
+ const allowed = this.perAgent.get(agentId);
26
+ if (!allowed) return [];
27
+ return [...allowed].map((name) => this.tools.get(name)!).filter(Boolean);
28
+ }
29
+
30
+ getToolNamesForAgent(agentId: string): string[] {
31
+ return [...(this.perAgent.get(agentId) || [])];
32
+ }
33
+
34
+ bindTools(agentId: string, toolNames: string[]): void {
35
+ const bound = new Set<string>();
36
+ for (const name of toolNames) {
37
+ if (!this.tools.has(name)) {
38
+ logger.warn({ tool: name }, 'Tool not found, skipping');
39
+ continue;
40
+ }
41
+ bound.add(name);
42
+ }
43
+ this.perAgent.set(agentId, bound);
44
+ logger.debug({ agentId, tools: [...bound] }, 'Tools bound to agent');
45
+ }
46
+
47
+ getSchemasForAgent(agentId: string) {
48
+ return this.listForAgent(agentId).map((t) => ({
49
+ name: t.name,
50
+ description: t.description,
51
+ parameters: t.parameters,
52
+ }));
53
+ }
54
+ }
55
+
56
+ export const toolRegistry = new ToolRegistry();