sandbox connected and streaming

This commit is contained in:
2026-03-30 23:29:03 -04:00
parent c3a8fae132
commit 998f69fa1a
130 changed files with 7416 additions and 2123 deletions

View File

@@ -0,0 +1,11 @@
// Tools exports
export * from './platform/index.js';
export * from './mcp/index.js';
export {
ToolRegistry,
initializeToolRegistry,
getToolRegistry,
type AgentToolConfig,
type PlatformServices,
} from './tool-registry.js';

View File

@@ -0,0 +1,7 @@
// MCP tool wrappers exports
export {
createMCPToolWrapper,
createMCPToolWrappers,
type MCPToolInfo,
} from './mcp-tool-wrapper.js';

View File

@@ -0,0 +1,186 @@
import { DynamicStructuredTool } from '@langchain/core/tools';
import { z } from 'zod';
import type { FastifyBaseLogger } from 'fastify';
import type { MCPClientConnector } from '../../harness/mcp-client.js';
/**
* MCP Tool Wrapper
*
* Wraps remote MCP server tools as standard LangChain tools.
* Provides dynamic tool creation based on MCP tool definitions.
*/
export interface MCPToolInfo {
name: string;
description?: string;
inputSchema?: {
type: string;
properties?: Record<string, any>;
required?: string[];
};
}
/**
* Create a LangChain tool from an MCP tool definition
*/
export function createMCPToolWrapper(
toolInfo: MCPToolInfo,
mcpClient: MCPClientConnector,
logger: FastifyBaseLogger,
onImage?: (image: { data: string; mimeType: string }) => void
): DynamicStructuredTool {
// Convert MCP input schema to Zod schema
const zodSchema = mcpInputSchemaToZod(toolInfo.inputSchema);
return new DynamicStructuredTool({
name: toolInfo.name,
description: toolInfo.description || `MCP tool: ${toolInfo.name}`,
schema: zodSchema,
func: async (input: Record<string, unknown>) => {
try {
const result = await mcpClient.callTool(toolInfo.name, input);
logger.info({ tool: toolInfo.name }, 'MCP tool call completed');
// Handle different MCP result formats
if (typeof result === 'string') {
return result;
}
// Handle structured MCP responses with content arrays
if (result && typeof result === 'object') {
// Extract text content from MCP response
const textParts: string[] = [];
// Check for content array (standard MCP format)
if (Array.isArray((result as any).content)) {
logger.debug({ tool: toolInfo.name, itemCount: (result as any).content.length }, 'Processing MCP content array');
for (const item of (result as any).content) {
if (item.type === 'text' && item.text) {
textParts.push(item.text);
} else if (item.type === 'image' && item.data && item.mimeType) {
logger.info({ tool: toolInfo.name, mimeType: item.mimeType }, 'Capturing image from MCP response');
onImage?.({ data: item.data, mimeType: item.mimeType });
}
}
if (textParts.length > 0) {
return textParts.join('\n\n');
}
}
// Check for nested execution.content
if ((result as any).execution && Array.isArray((result as any).execution.content)) {
for (const item of (result as any).execution.content) {
if (item.type === 'text' && item.text) {
textParts.push(item.text);
} else if (item.type === 'image' && item.data && item.mimeType) {
onImage?.({ data: item.data, mimeType: item.mimeType });
}
}
if (textParts.length > 0) {
return textParts.join('\n\n');
}
}
// Fallback: stringify the result
return JSON.stringify(result, null, 2);
}
return String(result || '');
} catch (error) {
logger.error({ error, tool: toolInfo.name, input }, 'MCP tool call failed');
return `Error calling MCP tool ${toolInfo.name}: ${error instanceof Error ? error.message : String(error)}`;
}
},
});
}
/**
* Convert MCP input schema to Zod schema
*/
function mcpInputSchemaToZod(inputSchema?: MCPToolInfo['inputSchema']): z.ZodObject<any> {
if (!inputSchema || !inputSchema.properties) {
// Generic schema that accepts any properties
return z.object({}).passthrough();
}
const properties = inputSchema.properties;
const required = inputSchema.required || [];
const zodFields: Record<string, z.ZodTypeAny> = {};
for (const [key, prop] of Object.entries(properties)) {
let zodType: z.ZodTypeAny;
// Map JSON Schema types to Zod types
switch (prop.type) {
case 'string':
zodType = z.string().describe(prop.description || '');
break;
case 'number':
zodType = z.number().describe(prop.description || '');
break;
case 'integer':
zodType = z.number().int().describe(prop.description || '');
break;
case 'boolean':
zodType = z.boolean().describe(prop.description || '');
break;
case 'array':
// Handle array items
if (prop.items) {
const itemType = getZodTypeForProperty(prop.items);
zodType = z.array(itemType).describe(prop.description || '');
} else {
zodType = z.array(z.any()).describe(prop.description || '');
}
break;
case 'object':
zodType = z.object({}).passthrough().describe(prop.description || '');
break;
default:
zodType = z.any().describe(prop.description || '');
}
// Make optional if not required
if (!required.includes(key)) {
zodType = zodType.optional();
}
zodFields[key] = zodType;
}
return z.object(zodFields);
}
/**
* Helper to get Zod type for a property definition
*/
function getZodTypeForProperty(prop: any): z.ZodTypeAny {
switch (prop.type) {
case 'string':
return z.string();
case 'number':
return z.number();
case 'integer':
return z.number().int();
case 'boolean':
return z.boolean();
case 'object':
return z.object({}).passthrough();
default:
return z.any();
}
}
/**
* Create multiple MCP tool wrappers from tool list
*/
export function createMCPToolWrappers(
toolInfos: MCPToolInfo[],
mcpClient: MCPClientConnector,
logger: FastifyBaseLogger,
onImage?: (image: { data: string; mimeType: string }) => void
): DynamicStructuredTool[] {
return toolInfos.map(toolInfo => createMCPToolWrapper(toolInfo, mcpClient, logger, onImage));
}

View File

@@ -0,0 +1,253 @@
import { DynamicStructuredTool } from '@langchain/core/tools';
import { z } from 'zod';
import type { FastifyBaseLogger } from 'fastify';
import type { OHLCService } from '../../services/ohlc-service.js';
import type { WorkspaceManager } from '../../workspace/workspace-manager.js';
import type { ChartState } from '../../workspace/types.js';
import * as chrono from 'chrono-node';
/**
* Get Chart Data Tool
*
* Standard LangChain tool for fetching OHLCV+ data with workspace defaults.
* Allows agent to override any parameter for historical or alternative ticker queries.
*/
export interface GetChartDataToolConfig {
ohlcService: OHLCService;
workspaceManager: WorkspaceManager;
logger: FastifyBaseLogger;
}
export function createGetChartDataTool(config: GetChartDataToolConfig): DynamicStructuredTool {
const { ohlcService, workspaceManager, logger } = config;
return new DynamicStructuredTool({
name: 'get_chart_data',
description: `Fetch OHLCV+ data for current chart or any ticker/timeframe. All parameters are optional and default to workspace chart state.
**IMPORTANT: Use this tool ONLY for quick, casual data viewing. For any analysis, plotting, statistics, or deep research, use the 'research' tool instead.**
Parameters:
- ticker (optional): Market symbol (defaults to workspace chartState.symbol)
- period (optional): OHLC period in seconds (defaults to workspace chartState.period)
- from_time (optional): Start time as Unix timestamp (number or string like "1774126800") OR date string like "2 days ago", "2024-01-01" (defaults to workspace chartState.start_time)
- to_time (optional): End time as Unix timestamp (number or string like "1774732500") OR date string like "now", "yesterday" (defaults to workspace chartState.end_time)
- countback (optional): Limit number of bars returned
- columns (optional): Extra columns beyond OHLC: ["volume", "buy_vol", "sell_vol", "open_time", "high_time", "low_time", "close_time", "open_interest"]`,
schema: z.object({
ticker: z.string().optional().describe('Market symbol (defaults to workspace chartState.symbol)'),
period: z.number().optional().describe('OHLC period in seconds (defaults to workspace chartState.period)'),
from_time: z.union([z.number(), z.string()]).optional().describe('Start time: Unix seconds OR date string (defaults to workspace chartState.start_time)'),
to_time: z.union([z.number(), z.string()]).optional().describe('End time: Unix seconds OR date string (defaults to workspace chartState.end_time)'),
countback: z.number().optional().describe('Limit number of bars returned'),
columns: z.array(z.enum(['volume', 'buy_vol', 'sell_vol', 'open_time', 'high_time', 'low_time', 'close_time', 'open_interest'])).optional().describe('Extra columns beyond OHLC'),
}),
func: async ({ ticker, period, from_time, to_time, countback, columns }) => {
logger.debug({ ticker, period, from_time, to_time, countback, columns }, 'Executing get_chart_data tool');
try {
// Get workspace chart state
const chartState = await getChartState(workspaceManager, logger);
// Build request with workspace defaults
const finalTicker = ticker ?? chartState.symbol;
const finalPeriod = period ?? parsePeriod(chartState.period);
const finalFromTime = await parseTime(from_time, chartState.start_time, logger);
const finalToTime = await parseTime(to_time, chartState.end_time, logger);
const requestedColumns = columns ?? [];
// Validate we have all required parameters
if (!finalTicker) {
return JSON.stringify({ error: 'Ticker not specified and not available in workspace' });
}
if (!finalPeriod) {
return JSON.stringify({ error: 'Period not specified and not available in workspace' });
}
if (!finalFromTime) {
return JSON.stringify({ error: 'from_time not specified and not available in workspace' });
}
if (!finalToTime) {
return JSON.stringify({ error: 'to_time not specified and not available in workspace' });
}
logger.debug({
ticker: finalTicker,
period: finalPeriod,
from_time: finalFromTime,
to_time: finalToTime,
countback,
columns: requestedColumns,
}, 'Fetching OHLC data');
// Fetch data from OHLCService
const historyResult = await ohlcService.fetchOHLC(
finalTicker,
finalPeriod.toString(),
finalFromTime,
finalToTime,
countback
);
if (historyResult.noData || !historyResult.bars || historyResult.bars.length === 0) {
return JSON.stringify({
ticker: finalTicker,
period: finalPeriod,
timeRange: { start: finalFromTime, end: finalToTime },
bars: [],
});
}
// Filter/format bars with requested columns
const bars = historyResult.bars.map(bar => {
const result: any = {
time: bar.time,
open: bar.open,
high: bar.high,
low: bar.low,
close: bar.close,
ticker: finalTicker,
};
// Add optional columns if requested
for (const col of requestedColumns) {
if (col === 'volume' && bar.volume !== undefined) {
result.volume = bar.volume;
} else if (col === 'buy_vol' && bar.buy_vol !== undefined) {
result.buy_vol = bar.buy_vol;
} else if (col === 'sell_vol' && bar.sell_vol !== undefined) {
result.sell_vol = bar.sell_vol;
} else if (col === 'open_time' && bar.open_time !== undefined) {
result.open_time = bar.open_time;
} else if (col === 'high_time' && bar.high_time !== undefined) {
result.high_time = bar.high_time;
} else if (col === 'low_time' && bar.low_time !== undefined) {
result.low_time = bar.low_time;
} else if (col === 'close_time' && bar.close_time !== undefined) {
result.close_time = bar.close_time;
} else if (col === 'open_interest' && bar.open_interest !== undefined) {
result.open_interest = bar.open_interest;
}
}
return result;
});
logger.info({ ticker: finalTicker, barCount: bars.length }, 'Chart data fetched successfully');
return JSON.stringify({
ticker: finalTicker,
period: finalPeriod,
timeRange: {
start: finalFromTime,
end: finalToTime,
},
bars,
});
} catch (error) {
logger.error({ error }, 'Get chart data tool failed');
return JSON.stringify({
error: error instanceof Error ? error.message : String(error),
});
}
},
});
}
/**
* Get chart state from workspace
*/
async function getChartState(workspaceManager: WorkspaceManager, logger: FastifyBaseLogger): Promise<ChartState> {
try {
const chartState = workspaceManager.getState<ChartState>('chartState');
if (!chartState) {
// Return default chart state
return {
symbol: 'BINANCE:BTC/USDT',
start_time: null,
end_time: null,
period: '15',
selected_shapes: [],
};
}
return chartState;
} catch (error) {
logger.error({ error }, 'Failed to get chart state from workspace');
// Return default chart state
return {
symbol: 'BINANCE:BTC/USDT',
start_time: null,
end_time: null,
period: '15',
selected_shapes: [],
};
}
}
/**
* Parse period string to seconds
* Handles period as either a number (already in seconds) or string (minutes)
*/
function parsePeriod(period: string | number | null): number | null {
if (period === null) {
return null;
}
if (typeof period === 'number') {
return period;
}
// Period in workspace is stored as string representing minutes
// Convert to seconds
const minutes = parseInt(period, 10);
if (isNaN(minutes)) {
return null;
}
return minutes * 60;
}
/**
* Parse time parameter (Unix seconds, date string, or null)
* Returns Unix timestamp in seconds
*/
async function parseTime(
timeParam: number | string | null | undefined,
workspaceDefault: number | null,
logger: FastifyBaseLogger
): Promise<number | null> {
// Use workspace default if param not provided
if (timeParam === undefined || timeParam === null) {
return workspaceDefault;
}
// If it's already a number, assume Unix seconds
if (typeof timeParam === 'number') {
return timeParam;
}
// Try to parse string as numeric Unix timestamp first
const numericTimestamp = parseInt(timeParam, 10);
if (!isNaN(numericTimestamp) && numericTimestamp.toString() === timeParam) {
// String is a valid integer - treat as Unix seconds
logger.debug({ timeParam, parsedTimestamp: numericTimestamp }, 'Parsed string as Unix timestamp');
return numericTimestamp;
}
// Parse date string using chrono
try {
const parsed = chrono.parseDate(timeParam);
if (!parsed) {
logger.warn({ timeParam }, 'Failed to parse time string');
return null;
}
// Convert to Unix seconds
return Math.floor(parsed.getTime() / 1000);
} catch (error) {
logger.error({ error, timeParam }, 'Error parsing time string');
return null;
}
}

View File

@@ -0,0 +1,11 @@
// Platform tools exports
export {
createSymbolLookupTool,
type SymbolLookupToolConfig,
} from './symbol-lookup.tool.js';
export {
createGetChartDataTool,
type GetChartDataToolConfig,
} from './get-chart-data.tool.js';

View File

@@ -0,0 +1,53 @@
import { DynamicStructuredTool } from '@langchain/core/tools';
import { z } from 'zod';
import type { FastifyBaseLogger } from 'fastify';
import type { ResearchSubagent } from '../../harness/subagents/research/index.js';
import type { SubagentContext } from '../../harness/subagents/base-subagent.js';
export interface ResearchAgentToolConfig {
researchSubagent: ResearchSubagent;
context: SubagentContext;
logger: FastifyBaseLogger;
}
/**
* Creates a LangChain tool that delegates to the research subagent.
* This is the standard LangChain pattern for exposing a subagent as a tool
* to a parent agent.
*/
export function createResearchAgentTool(config: ResearchAgentToolConfig): DynamicStructuredTool {
const { researchSubagent, context, logger } = config;
return new DynamicStructuredTool({
name: 'research',
description: `Delegate to the research subagent for data analysis, charting, statistics, and Python script execution.
Use this tool for:
- Plotting charts with technical indicators (EMA, RSI, MACD, Bollinger Bands, etc.)
- Statistical analysis of price data
- Custom research scripts using the DataAPI and ChartingAPI
- Any task requiring code execution or matplotlib charts
The research subagent will write and execute Python scripts, capture output and charts, and return results.`,
schema: z.object({
instruction: z.string().describe('The research task or analysis to perform. Be specific about what data, indicators, timeframes, and output you want.'),
}),
func: async ({ instruction }: { instruction: string }): Promise<string> => {
logger.info({ instruction: instruction.substring(0, 100) }, 'Delegating to research subagent');
try {
const result = await researchSubagent.executeWithImages(context, instruction);
// Return in the format that AgentHarness.processToolResult() knows how to handle
// (extracts images and passes them to channelAdapter)
return JSON.stringify({
text: result.text,
images: result.images,
});
} catch (error) {
logger.error({ error, errorMessage: (error as Error)?.message }, 'Research subagent failed');
throw error;
}
},
});
}

View File

@@ -0,0 +1,78 @@
import { DynamicStructuredTool } from '@langchain/core/tools';
import { z } from 'zod';
import type { FastifyBaseLogger } from 'fastify';
import type { SymbolIndexService } from '../../services/symbol-index-service.js';
/**
* Symbol Lookup Tool
*
* Standard LangChain tool for symbol search and resolution.
* Supports two modes:
* - search: Find symbols matching a query
* - resolve: Get detailed metadata for a specific symbol
*/
export interface SymbolLookupToolConfig {
symbolIndexService: SymbolIndexService;
logger: FastifyBaseLogger;
}
export function createSymbolLookupTool(config: SymbolLookupToolConfig): DynamicStructuredTool {
const { symbolIndexService, logger } = config;
return new DynamicStructuredTool({
name: 'symbol_lookup',
description: `Search for market symbols or resolve symbol metadata. Use 'search' mode to find symbols matching a query, or 'resolve' mode to get detailed metadata for a specific symbol.
Parameters:
- mode (required): Either 'search' or 'resolve'
- query (required): Search query (for search mode) or symbol ticker (for resolve mode)
- limit (optional): Maximum number of search results (search mode only, default: 30)`,
schema: z.object({
mode: z.enum(['search', 'resolve']).describe('Operation mode: search for symbols or resolve a specific symbol'),
query: z.string().describe('Search query (for search mode) or symbol ticker (for resolve mode)'),
limit: z.number().optional().default(30).describe('Maximum number of search results (search mode only, default: 30)'),
}),
func: async ({ mode, query, limit }) => {
logger.debug({ mode, query, limit }, 'Executing symbol_lookup tool');
try {
if (mode === 'search') {
const results = await symbolIndexService.search(query, limit);
logger.info({ query, resultCount: results.length }, 'Symbol search completed');
return JSON.stringify({
mode: 'search',
query,
count: results.length,
results,
});
} else {
const symbolInfo = await symbolIndexService.resolveSymbol(query);
if (!symbolInfo) {
logger.warn({ symbol: query }, 'Symbol not found');
return JSON.stringify({
error: `Symbol not found: ${query}`,
symbol: query,
});
}
logger.info({ symbol: query }, 'Symbol resolved');
return JSON.stringify({
mode: 'resolve',
symbol: query,
symbolInfo,
});
}
} catch (error) {
logger.error({ error, mode, query }, 'Symbol lookup tool failed');
return JSON.stringify({
error: error instanceof Error ? error.message : String(error),
});
}
},
});
}

View File

@@ -0,0 +1,291 @@
import type { DynamicStructuredTool } from '@langchain/core/tools';
import type { FastifyBaseLogger } from 'fastify';
import type { MCPClientConnector } from '../harness/mcp-client.js';
import type { OHLCService } from '../services/ohlc-service.js';
import type { SymbolIndexService } from '../services/symbol-index-service.js';
import type { WorkspaceManager } from '../workspace/workspace-manager.js';
import { createSymbolLookupTool } from './platform/symbol-lookup.tool.js';
import { createGetChartDataTool } from './platform/get-chart-data.tool.js';
import { createMCPToolWrappers, type MCPToolInfo } from './mcp/mcp-tool-wrapper.js';
/**
* Agent tool configuration
* Specifies which tools are available to which agent
*/
export interface AgentToolConfig {
/** Agent name (e.g., 'main', 'research', 'code-reviewer') */
agentName: string;
/** Platform tool names to include */
platformTools: string[];
/** MCP tool patterns/names to include (supports wildcards like 'category_*') */
mcpTools: string[];
}
/**
* Platform services required for creating platform tools
* Can be provided as direct references or getter functions (for lazy initialization)
*/
export interface PlatformServices {
ohlcService?: OHLCService | (() => OHLCService | undefined);
symbolIndexService?: SymbolIndexService | (() => SymbolIndexService | undefined);
workspaceManager?: WorkspaceManager | (() => WorkspaceManager | undefined);
}
/**
* Tool Registry
*
* Manages tool creation and agent-to-tool mappings.
* Supports:
* - Platform tools (local services like symbol lookup, chart data)
* - Remote MCP tools (per-user, session-scoped)
* - Configurable tool routing (which tools for which agents)
*/
export class ToolRegistry {
private logger: FastifyBaseLogger;
private platformServices: PlatformServices;
private agentToolConfigs: Map<string, AgentToolConfig> = new Map();
constructor(logger: FastifyBaseLogger, platformServices: PlatformServices) {
this.logger = logger;
this.platformServices = platformServices;
}
/**
* Register agent tool configuration
*/
registerAgentTools(config: AgentToolConfig): void {
this.agentToolConfigs.set(config.agentName, config);
this.logger.debug(
{
agent: config.agentName,
platformTools: config.platformTools,
mcpTools: config.mcpTools,
},
'Registered agent tool configuration'
);
}
/**
* Get tools for a specific agent
*
* @param agentName - Name of the agent ('main', 'research', etc.)
* @param mcpClient - MCP client for remote tools (optional)
* @param availableMCPTools - List of available MCP tools from user's server (optional)
* @param workspaceManager - Workspace manager for this session (optional, used by some platform tools)
* @returns Array of tools for this agent
*/
async getToolsForAgent(
agentName: string,
mcpClient?: MCPClientConnector,
availableMCPTools?: MCPToolInfo[],
workspaceManager?: WorkspaceManager,
onImage?: (image: { data: string; mimeType: string }) => void
): Promise<DynamicStructuredTool[]> {
const config = this.agentToolConfigs.get(agentName);
if (!config) {
this.logger.warn({ agent: agentName }, 'No tool configuration found for agent');
return [];
}
const tools: DynamicStructuredTool[] = [];
// Add platform tools
for (const toolName of config.platformTools) {
const tool = await this.getPlatformTool(toolName, workspaceManager);
if (tool) {
tools.push(tool);
} else {
this.logger.warn({ agent: agentName, tool: toolName }, 'Platform tool not found');
}
}
// Add MCP tools (if MCP client and tools are available)
if (mcpClient && availableMCPTools && availableMCPTools.length > 0) {
const filteredMCPTools = this.filterMCPTools(availableMCPTools, config.mcpTools);
const mcpToolInstances = createMCPToolWrappers(filteredMCPTools, mcpClient, this.logger, onImage);
tools.push(...mcpToolInstances);
this.logger.debug(
{
agent: agentName,
mcpToolCount: mcpToolInstances.length,
mcpToolNames: mcpToolInstances.map(t => t.name),
},
'Added MCP tools for agent'
);
}
this.logger.info(
{
agent: agentName,
toolCount: tools.length,
toolNames: tools.map(t => t.name),
},
'Retrieved tools for agent'
);
return tools;
}
/**
* Get a platform tool by name
*
* @param toolName - Name of the tool to create
* @param sessionWorkspaceManager - Optional session-specific workspace manager
*/
private async getPlatformTool(
toolName: string,
sessionWorkspaceManager?: WorkspaceManager
): Promise<DynamicStructuredTool | null> {
// Don't cache tools - recreate each time to get latest services
// (services might be initialized asynchronously after registry creation)
// Create tool based on name
let tool: DynamicStructuredTool | null = null;
switch (toolName) {
case 'symbol_lookup': {
const symbolIndexService = this.resolveService(this.platformServices.symbolIndexService);
if (symbolIndexService) {
tool = createSymbolLookupTool({
symbolIndexService,
logger: this.logger,
});
} else {
this.logger.warn('SymbolIndexService not available for symbol_lookup tool');
}
break;
}
case 'get_chart_data': {
const ohlcService = this.resolveService(this.platformServices.ohlcService);
// Use session workspace manager if provided, otherwise try global
const workspaceManager = sessionWorkspaceManager ||
this.resolveService(this.platformServices.workspaceManager);
if (ohlcService && workspaceManager) {
tool = createGetChartDataTool({
ohlcService,
workspaceManager,
logger: this.logger,
});
} else {
this.logger.warn(
{ hasOHLC: !!ohlcService, hasWorkspace: !!workspaceManager },
'OHLCService or WorkspaceManager not available for get_chart_data tool'
);
}
break;
}
default:
this.logger.warn({ tool: toolName }, 'Unknown platform tool');
return null;
}
return tool;
}
/**
* Resolve a service (handle both direct references and getter functions)
*/
private resolveService<T>(service: T | (() => T | undefined) | undefined): T | undefined {
// Check if it's a function by checking the type more carefully
if (service && typeof (service as any) === 'function' && !(service as any).prototype) {
// It's a getter function (arrow function or function expression, not a class)
return (service as () => T | undefined)();
}
return service as T | undefined;
}
/**
* Filter MCP tools based on patterns/names
* Supports wildcards like 'category_*' or exact names like 'execute_research'
*/
private filterMCPTools(availableTools: MCPToolInfo[], patterns: string[]): MCPToolInfo[] {
if (patterns.length === 0) {
return [];
}
return availableTools.filter(tool => {
for (const pattern of patterns) {
if (this.matchesPattern(tool.name, pattern)) {
return true;
}
}
return false;
});
}
/**
* Check if a tool name matches a pattern
* Supports wildcards: 'category_*' matches 'category_write', 'category_read', etc.
*/
private matchesPattern(toolName: string, pattern: string): boolean {
if (pattern === toolName) {
return true; // Exact match
}
if (pattern.includes('*')) {
// Convert wildcard pattern to regex
const regexPattern = pattern
.replace(/\*/g, '.*')
.replace(/\?/g, '.');
const regex = new RegExp(`^${regexPattern}$`);
return regex.test(toolName);
}
return false;
}
/**
* Get all registered agent names
*/
getRegisteredAgents(): string[] {
return Array.from(this.agentToolConfigs.keys());
}
/**
* Get tool configuration for an agent
*/
getAgentToolConfig(agentName: string): AgentToolConfig | null {
return this.agentToolConfigs.get(agentName) || null;
}
}
/**
* Global registry instance (initialized at gateway startup)
*/
let globalToolRegistry: ToolRegistry | null = null;
/**
* Initialize the global tool registry
*/
export function initializeToolRegistry(
logger: FastifyBaseLogger,
platformServices: PlatformServices
): ToolRegistry {
if (globalToolRegistry) {
logger.warn('Global tool registry already initialized');
return globalToolRegistry;
}
globalToolRegistry = new ToolRegistry(logger, platformServices);
logger.info('Tool registry initialized');
return globalToolRegistry;
}
/**
* Get the global tool registry
*/
export function getToolRegistry(): ToolRegistry {
if (!globalToolRegistry) {
throw new Error('Tool registry not initialized. Call initializeToolRegistry() first.');
}
return globalToolRegistry;
}