lynkr
Version:
Self-hosted LLM gateway and tier-routing proxy for Claude Code, Cursor, and Codex. Routes across Ollama, AWS Bedrock, OpenRouter, Databricks, Azure OpenAI, llama.cpp, and LM Studio with prompt caching, MCP tools, and 60-80% cost savings.
359 lines (316 loc) • 10 kB
JavaScript
/**
* Lazy Tool Loader
*
* Loads tool categories on-demand based on prompt content analysis.
* Reduces startup time and memory by only loading tools when needed.
*
* @module tools/lazy-loader
*/
const logger = require('../logger');
// Track which tool categories have been loaded
const loadedCategories = new Set();
// Core tools that are always loaded at startup
const CORE_CATEGORIES = ['stubs', 'workspace', 'execution'];
// Tool categories with their registration functions and keyword triggers
const TOOL_CATEGORIES = {
stubs: {
keywords: [], // Always loaded
loader: () => require('./stubs').registerStubTools,
priority: 0,
},
workspace: {
keywords: ['file', 'read', 'write', 'edit', 'create', 'delete', 'list', 'directory', 'folder', 'path'],
loader: () => require('./workspace').registerWorkspaceTools,
priority: 0,
},
execution: {
keywords: ['run', 'execute', 'shell', 'bash', 'command', 'terminal', 'npm', 'node', 'python', 'script'],
loader: () => require('./execution').registerExecutionTools,
priority: 0,
},
web: {
keywords: ['web', 'search', 'fetch', 'url', 'http', 'https', 'api', 'request', 'internet', 'online', 'browse', 'website'],
loader: () => require('./web').registerWebTools,
priority: 1,
},
indexer: {
keywords: ['index', 'search', 'find', 'symbol', 'reference', 'grep', 'scan', 'codebase'],
loader: () => require('./indexer').registerIndexerTools,
priority: 1,
},
edits: {
keywords: ['edit', 'patch', 'modify', 'change', 'update', 'replace', 'refactor'],
loader: () => require('./edits').registerEditTools,
priority: 1,
},
git: {
keywords: ['git', 'commit', 'push', 'pull', 'branch', 'merge', 'rebase', 'stash', 'checkout', 'clone', 'diff', 'status', 'log', 'remote', 'fetch', 'pr', 'pull request'],
loader: () => require('./git').registerGitTools,
priority: 2,
},
tasks: {
keywords: ['task', 'todo', 'subtask', 'agent', 'spawn', 'background'],
loader: () => require('./tasks').registerTaskTools,
priority: 2,
},
tinyfish: {
keywords: ['tinyfish', 'web_agent', 'automate', 'scrape', 'extract', 'crawl', 'browser'],
loader: () => require('./tinyfish').registerTinyFishTools,
priority: 2,
},
tests: {
keywords: ['test', 'jest', 'mocha', 'pytest', 'unittest', 'spec', 'coverage', 'assert'],
loader: () => require('./tests').registerTestTools,
priority: 2,
},
mcp: {
keywords: ['mcp', 'server', 'sandbox', 'container', 'docker'],
loader: () => require('./mcp').registerMcpTools,
priority: 3,
},
agentTask: {
keywords: ['agent', 'subagent', 'spawn', 'delegate', 'parallel'],
loader: () => require('./agent-task').registerAgentTaskTool,
priority: 2,
},
'code-mode': {
keywords: ['mcp', 'execute', 'server', 'tool', 'code mode'],
loader: () => require('./code-mode').registerCodeModeTools,
priority: 3,
},
};
/**
* Load a specific tool category
* @param {string} category - Category name
* @returns {boolean} - True if loaded, false if already loaded or failed
*/
function loadCategory(category) {
if (loadedCategories.has(category)) {
return false;
}
const config = TOOL_CATEGORIES[category];
if (!config) {
logger.warn({ category }, '[LazyLoader] Unknown tool category');
return false;
}
try {
const registerFn = config.loader();
if (typeof registerFn === 'function') {
registerFn();
}
loadedCategories.add(category);
logger.debug({ category }, '[LazyLoader] Tool category loaded');
return true;
} catch (err) {
logger.error({ category, error: err.message }, '[LazyLoader] Failed to load tool category');
return false;
}
}
/**
* Load core tools (called at startup)
*/
function loadCoreTools() {
const startTime = Date.now();
for (const category of CORE_CATEGORIES) {
loadCategory(category);
}
logger.info({
loadedCategories: Array.from(loadedCategories),
duration: Date.now() - startTime,
}, '[LazyLoader] Core tools loaded');
}
/**
* Load all tools (for backwards compatibility or when lazy loading is disabled)
*/
function loadAllTools() {
const startTime = Date.now();
for (const category of Object.keys(TOOL_CATEGORIES)) {
loadCategory(category);
}
logger.info({
loadedCategories: Array.from(loadedCategories),
duration: Date.now() - startTime,
}, '[LazyLoader] All tools loaded');
}
/**
* Analyze prompt content and determine which tool categories are needed
* @param {string|Array} content - Prompt content (string or messages array)
* @returns {string[]} - List of category names that should be loaded
*/
function analyzePromptForTools(content) {
// Extract text from various input formats
let text = '';
if (typeof content === 'string') {
text = content.toLowerCase();
} else if (Array.isArray(content)) {
// Extract from messages array
text = content
.map(msg => {
if (typeof msg.content === 'string') {
return msg.content;
}
if (Array.isArray(msg.content)) {
return msg.content
.filter(part => part.type === 'text' || part.type === 'input_text')
.map(part => part.text || part.input_text || '')
.join(' ');
}
return '';
})
.join(' ')
.toLowerCase();
}
if (!text) return [];
const neededCategories = new Set();
// Check each category's keywords
for (const [category, config] of Object.entries(TOOL_CATEGORIES)) {
// Skip already loaded categories
if (loadedCategories.has(category)) continue;
// Check if any keyword matches
for (const keyword of config.keywords) {
if (text.includes(keyword.toLowerCase())) {
neededCategories.add(category);
break;
}
}
}
// Sort by priority (lower = load first)
return Array.from(neededCategories).sort((a, b) => {
return (TOOL_CATEGORIES[a]?.priority ?? 99) - (TOOL_CATEGORIES[b]?.priority ?? 99);
});
}
/**
* Ensure tools needed for a prompt are loaded
* @param {string|Array} content - Prompt content
* @returns {{ loaded: string[], alreadyLoaded: string[] }}
*/
function ensureToolsForPrompt(content) {
const neededCategories = analyzePromptForTools(content);
const loaded = [];
const alreadyLoaded = [];
for (const category of neededCategories) {
if (loadedCategories.has(category)) {
alreadyLoaded.push(category);
} else if (loadCategory(category)) {
loaded.push(category);
}
}
if (loaded.length > 0) {
logger.info({ loaded, triggered: content?.substring?.(0, 100) }, '[LazyLoader] Loaded tools for prompt');
}
return { loaded, alreadyLoaded };
}
/**
* Load a tool category by tool name (called when a tool is requested but not found)
* @param {string} toolName - Name of the tool being requested
* @returns {boolean} - True if a category was loaded
*/
function loadCategoryForTool(toolName) {
if (!toolName) return false;
const lowerName = toolName.toLowerCase();
// Map tool names to categories
const toolToCategory = {
// Git tools
'workspace_git_status': 'git',
'workspace_git_stage': 'git',
'workspace_git_unstage': 'git',
'workspace_git_commit': 'git',
'workspace_git_push': 'git',
'workspace_git_pull': 'git',
'workspace_git_branches': 'git',
'workspace_git_checkout': 'git',
'workspace_git_stash': 'git',
'workspace_git_merge': 'git',
'workspace_git_rebase': 'git',
'workspace_git_conflicts': 'git',
'workspace_diff': 'git',
'workspace_diff_review': 'git',
'workspace_diff_summary': 'git',
'workspace_diff_by_commit': 'git',
'workspace_release_notes': 'git',
'workspace_changelog_generate': 'git',
'workspace_pr_template_generate': 'git',
'workspace_git_patch_plan': 'git',
// Web tools
'web_search': 'web',
'web_fetch': 'web',
// Indexer tools
'workspace_search': 'indexer',
'workspace_symbol_search': 'indexer',
'workspace_symbol_references': 'indexer',
'workspace_index_rebuild': 'indexer',
// Edit tools
'edit_patch': 'edits',
// Task tools
'task_create': 'tasks',
'task_update': 'tasks',
'task_list': 'tasks',
// Test tools
'workspace_test_run': 'tests',
'workspace_test_summary': 'tests',
'workspace_test_history': 'tests',
// MCP tools
'workspace_sandbox_sessions': 'mcp',
'workspace_mcp_servers': 'mcp',
// Code Mode meta-tools
'mcp_list_tools': 'code-mode',
'mcp_tool_info': 'code-mode',
'mcp_tool_docs': 'code-mode',
'mcp_execute': 'code-mode',
// Agent task
// TinyFish (web agent)
'web_agent': 'tinyfish',
'agent_task': 'agentTask',
};
// Direct mapping
const category = toolToCategory[lowerName];
if (category && !loadedCategories.has(category)) {
return loadCategory(category);
}
// Fuzzy matching by prefix
for (const [toolPattern, cat] of Object.entries(toolToCategory)) {
if (lowerName.startsWith(toolPattern.split('_')[0]) && !loadedCategories.has(cat)) {
return loadCategory(cat);
}
}
return false;
}
/**
* Get statistics about loaded tools
*/
function getLoaderStats() {
const allCategories = Object.keys(TOOL_CATEGORIES);
return {
loaded: Array.from(loadedCategories),
notLoaded: allCategories.filter(c => !loadedCategories.has(c)),
totalCategories: allCategories.length,
loadedCount: loadedCategories.size,
};
}
/**
* Check if a category is loaded
* @param {string} category
* @returns {boolean}
*/
function isCategoryLoaded(category) {
return loadedCategories.has(category);
}
/**
* Reset loader state (for testing)
*/
function resetLoader() {
loadedCategories.clear();
}
module.exports = {
loadCoreTools,
loadAllTools,
loadCategory,
loadCategoryForTool,
analyzePromptForTools,
ensureToolsForPrompt,
getLoaderStats,
isCategoryLoaded,
resetLoader,
TOOL_CATEGORIES,
CORE_CATEGORIES,
};