lynkr 3.0.0 → 3.2.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.
@@ -0,0 +1,320 @@
1
+ /**
2
+ * Dynamic System Prompt Optimization
3
+ * * Provides utilities for optimizing system prompts and tool descriptions
4
+ * to reduce token usage while maintaining functionality.
5
+ *
6
+ */
7
+
8
+ const logger = require('../logger');
9
+ const config = require('../config');
10
+
11
+ /**
12
+ * Compress tool descriptions to minimal format
13
+ *
14
+ * Converts verbose tool schemas to minimal versions by:
15
+ * - Shortening descriptions
16
+ * - Removing optional fields when not critical
17
+ * - Using concise parameter descriptions
18
+ *
19
+ * @param {Array} tools - Array of Anthropic-format tool definitions
20
+ * @param {string} mode - 'minimal' or 'full' (default: from config)
21
+ * @returns {Array} Optimized tool definitions
22
+ */
23
+ function compressToolDescriptions(tools, mode = null) {
24
+ if (!tools || tools.length === 0) return tools;
25
+
26
+ mode = mode || config.systemPrompt?.toolDescriptions || 'minimal';
27
+
28
+ if (mode !== 'minimal') {
29
+ return tools; // Return unmodified if not in minimal mode
30
+ }
31
+
32
+ return tools.map(tool => {
33
+ const compressed = {
34
+ name: tool.name,
35
+ input_schema: {
36
+ type: tool.input_schema.type,
37
+ properties: {},
38
+ required: tool.input_schema.required || [],
39
+ }
40
+ };
41
+
42
+ // Add minimal description only if it exists
43
+ if (tool.description) {
44
+ compressed.description = compressText(tool.description, 50);
45
+ }
46
+
47
+ // Compress property descriptions
48
+ if (tool.input_schema.properties) {
49
+ for (const [key, value] of Object.entries(tool.input_schema.properties)) {
50
+ compressed.input_schema.properties[key] = {
51
+ type: value.type,
52
+ };
53
+
54
+ // Only include description if it's critical
55
+ if (value.description && !isObviousFromName(key)) {
56
+ compressed.input_schema.properties[key].description = compressText(value.description, 30);
57
+ }
58
+
59
+ // Preserve enum, format, and other critical constraints
60
+ if (value.enum) compressed.input_schema.properties[key].enum = value.enum;
61
+ if (value.format) compressed.input_schema.properties[key].format = value.format;
62
+ if (value.items) compressed.input_schema.properties[key].items = value.items;
63
+ if (value.additionalProperties !== undefined) {
64
+ compressed.input_schema.properties[key].additionalProperties = value.additionalProperties;
65
+ }
66
+ }
67
+ }
68
+
69
+ // Preserve additionalProperties if set
70
+ if (tool.input_schema.additionalProperties !== undefined) {
71
+ compressed.input_schema.additionalProperties = tool.input_schema.additionalProperties;
72
+ }
73
+
74
+ return compressed;
75
+ });
76
+ }
77
+
78
+ /**
79
+ * Compress text to maximum length while preserving meaning
80
+ * @param {string} text - Text to compress
81
+ * @param {number} maxLength - Maximum length
82
+ * @returns {string} Compressed text
83
+ */
84
+ function compressText(text, maxLength) {
85
+ if (!text || text.length <= maxLength) return text;
86
+
87
+ // Try to cut at sentence/word boundary
88
+ let cut = text.substring(0, maxLength);
89
+ const lastPeriod = cut.lastIndexOf('.');
90
+ const lastSpace = cut.lastIndexOf(' ');
91
+
92
+ if (lastPeriod > maxLength * 0.7) {
93
+ return cut.substring(0, lastPeriod + 1);
94
+ } else if (lastSpace > maxLength * 0.8) {
95
+ return cut.substring(0, lastSpace);
96
+ }
97
+
98
+ return cut;
99
+ }
100
+
101
+ /**
102
+ * Check if property name is self-explanatory
103
+ * @param {string} name - Property name
104
+ * @returns {boolean} True if name is obvious
105
+ */
106
+ function isObviousFromName(name) {
107
+ const obvious = [
108
+ 'id', 'name', 'type', 'value', 'data', 'text', 'content',
109
+ 'message', 'query', 'command', 'url', 'path', 'file', 'filename',
110
+ 'email', 'username', 'password', 'token', 'key', 'timeout',
111
+ 'limit', 'offset', 'page', 'size', 'count', 'total', 'status'
112
+ ];
113
+ return obvious.includes(name.toLowerCase());
114
+ }
115
+
116
+ /**
117
+ * Optimize system prompt based on context
118
+ *
119
+ * Analyzes the system prompt and removes or compresses sections
120
+ * that aren't relevant to the current context.
121
+ *
122
+ * @param {string|Array} system - System prompt (string or content blocks)
123
+ * @param {Object} context - Context information
124
+ * @param {Array} context.tools - Tools available in this request
125
+ * @param {Array} context.messages - Recent messages
126
+ * @param {string} mode - 'dynamic' or 'full' (default: from config)
127
+ * @returns {string|Array} Optimized system prompt
128
+ */
129
+ function optimizeSystemPrompt(system, context = {}, mode = null) {
130
+ if (!system) return system;
131
+
132
+ mode = mode || config.systemPrompt?.mode || 'dynamic';
133
+
134
+ if (mode !== 'dynamic') {
135
+ return system; // Return unmodified if not in dynamic mode
136
+ }
137
+
138
+ // Convert to string if array of blocks
139
+ let text = typeof system === 'string' ? system : flattenBlocks(system);
140
+
141
+ const optimizations = [];
142
+ const originalLength = text.length;
143
+
144
+ // 1. Remove verbose tool usage examples if no tools present
145
+ if (!context.tools || context.tools.length === 0) {
146
+ text = removeSection(text, /# Tool Usage Examples?[\s\S]*?(?=\n#|\n\n[A-Z]|$)/gi, optimizations, 'tool examples');
147
+ text = removeSection(text, /<tool_usage>[\s\S]*?<\/tool_usage>/gi, optimizations, 'tool usage blocks');
148
+ }
149
+
150
+ // 2. Remove file operation guidelines if no file tools
151
+ const hasFileTools = context.tools?.some(t =>
152
+ ['Read', 'Write', 'Edit', 'Glob', 'Grep'].includes(t.name)
153
+ );
154
+ if (!hasFileTools) {
155
+ text = removeSection(text, /# File Operations?[\s\S]*?(?=\n#|\n\n[A-Z]|$)/gi, optimizations, 'file operations');
156
+ }
157
+
158
+ // 3. Remove git guidelines if no git tools
159
+ const hasGitTools = context.tools?.some(t =>
160
+ t.name.toLowerCase().includes('git')
161
+ );
162
+ if (!hasGitTools) {
163
+ text = removeSection(text, /# Git.*?[\s\S]*?(?=\n#|\n\n[A-Z]|$)/gi, optimizations, 'git guidelines');
164
+ text = removeSection(text, /## Committing changes[\s\S]*?(?=\n#|\n\n[A-Z]|$)/gi, optimizations, 'git commit guidelines');
165
+ }
166
+
167
+ // 4. Remove web search guidelines if no web tools
168
+ const hasWebTools = context.tools?.some(t =>
169
+ ['WebSearch', 'WebFetch'].includes(t.name)
170
+ );
171
+ if (!hasWebTools) {
172
+ text = removeSection(text, /# Web.*?[\s\S]*?(?=\n#|\n\n[A-Z]|$)/gi, optimizations, 'web guidelines');
173
+ }
174
+
175
+ // 5. Compress code review guidelines if no recent code edits
176
+ const hasRecentEdits = context.messages?.some(m =>
177
+ typeof m.content === 'string' && m.content.toLowerCase().includes('edit')
178
+ );
179
+ if (!hasRecentEdits) {
180
+ text = removeSection(text, /# Code Review[\s\S]*?(?=\n#|\n\n[A-Z]|$)/gi, optimizations, 'code review');
181
+ }
182
+
183
+ // 6. Remove verbose examples and keep only essential instructions
184
+ text = text.replace(/(<example>[\s\S]*?<\/example>\s*){3,}/g, (match) => {
185
+ // Keep first two examples, remove rest
186
+ const examples = match.match(/<example>[\s\S]*?<\/example>/g) || [];
187
+ optimizations.push('excessive examples');
188
+ return examples.slice(0, 2).join('\n\n');
189
+ });
190
+
191
+ // 7. Compress whitespace
192
+ text = text.replace(/\n{4,}/g, '\n\n\n'); // Max 2 blank lines
193
+ text = text.replace(/[ \t]+\n/g, '\n'); // Remove trailing spaces
194
+
195
+ const finalLength = text.length;
196
+ const saved = originalLength - finalLength;
197
+
198
+ if (saved > 100 && optimizations.length > 0) {
199
+ logger.debug({
200
+ originalLength,
201
+ finalLength,
202
+ saved,
203
+ percentage: ((saved / originalLength) * 100).toFixed(1),
204
+ optimizations: [...new Set(optimizations)]
205
+ }, 'System prompt optimization applied');
206
+ }
207
+
208
+ // Return in original format (string or blocks)
209
+ return typeof system === 'string' ? text : text;
210
+ }
211
+
212
+ /**
213
+ * Remove a section from text using regex
214
+ * @param {string} text - Text to modify
215
+ * @param {RegExp} pattern - Pattern to match
216
+ * @param {Array} optimizations - Array to track optimizations
217
+ * @param {string} label - Label for this optimization
218
+ * @returns {string} Modified text
219
+ */
220
+ function removeSection(text, pattern, optimizations, label) {
221
+ const matches = text.match(pattern);
222
+ if (matches && matches.length > 0) {
223
+ optimizations.push(label);
224
+ return text.replace(pattern, '');
225
+ }
226
+ return text;
227
+ }
228
+
229
+ /**
230
+ * Flatten content blocks to text
231
+ * @param {Array} blocks - Content blocks
232
+ * @returns {string} Flattened text
233
+ */
234
+ function flattenBlocks(blocks) {
235
+ if (!Array.isArray(blocks)) return String(blocks || '');
236
+
237
+ return blocks
238
+ .map(block => {
239
+ if (typeof block === 'string') return block;
240
+ if (block.type === 'text' && block.text) return block.text;
241
+ if (block.text) return block.text;
242
+ return '';
243
+ })
244
+ .filter(Boolean)
245
+ .join('\n\n');
246
+ }
247
+
248
+ /**
249
+ * Analyze context to determine what optimizations are safe
250
+ * @param {Object} context - Request context
251
+ * @returns {Object} Analysis results
252
+ */
253
+ function analyzeContext(context) {
254
+ const analysis = {
255
+ hasTools: Boolean(context.tools && context.tools.length > 0),
256
+ toolCount: context.tools?.length || 0,
257
+ toolNames: context.tools?.map(t => t.name) || [],
258
+ messageCount: context.messages?.length || 0,
259
+ hasFileOps: false,
260
+ hasGitOps: false,
261
+ hasWebOps: false,
262
+ hasBashOps: false,
263
+ };
264
+
265
+ if (context.tools) {
266
+ analysis.hasFileOps = context.tools.some(t =>
267
+ ['Read', 'Write', 'Edit', 'Glob', 'Grep'].includes(t.name)
268
+ );
269
+ analysis.hasGitOps = context.tools.some(t =>
270
+ t.name.toLowerCase().includes('git')
271
+ );
272
+ analysis.hasWebOps = context.tools.some(t =>
273
+ ['WebSearch', 'WebFetch'].includes(t.name)
274
+ );
275
+ analysis.hasBashOps = context.tools.some(t =>
276
+ ['Bash', 'BashOutput', 'KillShell'].includes(t.name)
277
+ );
278
+ }
279
+
280
+ return analysis;
281
+ }
282
+
283
+ /**
284
+ * Calculate token savings from optimizations
285
+ * @param {string|Array} original - Original system prompt
286
+ * @param {string|Array} optimized - Optimized system prompt
287
+ * @returns {Object} Savings statistics
288
+ */
289
+ function calculateSavings(original, optimized) {
290
+ const origText = typeof original === 'string' ? original : flattenBlocks(original);
291
+ const optText = typeof optimized === 'string' ? optimized : flattenBlocks(optimized);
292
+
293
+ const origLength = origText.length;
294
+ const optLength = optText.length;
295
+ const saved = origLength - optLength;
296
+
297
+ // Rough token estimate (4 chars ≈ 1 token)
298
+ const tokensOriginal = Math.ceil(origLength / 4);
299
+ const tokensOptimized = Math.ceil(optLength / 4);
300
+ const tokensSaved = tokensOriginal - tokensOptimized;
301
+
302
+ return {
303
+ originalChars: origLength,
304
+ optimizedChars: optLength,
305
+ charsSaved: saved,
306
+ tokensOriginal,
307
+ tokensOptimized,
308
+ tokensSaved,
309
+ percentage: origLength > 0 ? ((saved / origLength) * 100).toFixed(1) : '0.0'
310
+ };
311
+ }
312
+
313
+ module.exports = {
314
+ compressToolDescriptions,
315
+ optimizeSystemPrompt,
316
+ analyzeContext,
317
+ calculateSavings,
318
+ compressText,
319
+ flattenBlocks,
320
+ };
@@ -1,4 +1,5 @@
1
1
  const logger = require("../logger");
2
+ const { truncateToolOutput } = require("./truncate");
2
3
 
3
4
  const registry = new Map();
4
5
  const registryLowercase = new Map();
@@ -216,14 +217,22 @@ async function executeToolCall(call, context = {}) {
216
217
  context,
217
218
  );
218
219
  const formatted = normalizeHandlerResult(result);
220
+
221
+ // Apply tool output truncation for token efficiency
222
+ const truncatedContent = truncateToolOutput(normalisedCall.name, formatted.content);
223
+
219
224
  return {
220
225
  id: normalisedCall.id,
221
226
  name: normalisedCall.name,
222
227
  arguments: normalisedCall.arguments,
223
228
  ...formatted,
229
+ content: truncatedContent,
224
230
  metadata: {
225
231
  ...(formatted.metadata ?? {}),
226
232
  registered: true,
233
+ truncated: truncatedContent !== formatted.content,
234
+ originalLength: formatted.content?.length,
235
+ truncatedLength: truncatedContent?.length
227
236
  },
228
237
  };
229
238
  } catch (err) {
@@ -0,0 +1,356 @@
1
+ /**
2
+ * Smart Tool Selection Module
3
+ *
4
+ * Intelligently selects relevant tools based on request type classification.
5
+ * Reduces tool token overhead by 50-70% for non-coding queries.
6
+ *
7
+ * @module tools/smart-selection
8
+ */
9
+
10
+ const logger = require('../logger');
11
+
12
+ /**
13
+ * Tool selection map: request type → relevant tools
14
+ */
15
+ const TOOL_SELECTION_MAP = {
16
+ conversational: [], // No tools needed for greetings
17
+ simple_qa: [], // No tools needed for simple questions
18
+
19
+ research: [
20
+ 'Read', 'Grep', 'Glob', // File search
21
+ 'WebSearch', 'WebFetch' // Web research
22
+ ],
23
+
24
+ file_reading: [
25
+ 'Read', 'Grep', 'Glob' // Read-only tools
26
+ ],
27
+
28
+ file_modification: [
29
+ 'Read', 'Write', 'Edit', // Full I/O
30
+ 'Grep', 'Glob', 'Bash' // Support tools
31
+ ],
32
+
33
+ code_execution: [
34
+ 'Read', 'Write', 'Edit', // File operations
35
+ 'Bash', 'Grep', 'Glob' // Execution + search
36
+ ],
37
+
38
+ coding: [
39
+ 'Read', 'Write', 'Edit', // Core file ops
40
+ 'Bash', 'Grep', 'Glob' // Support tools
41
+ ],
42
+
43
+ complex_task: [
44
+ 'Read', 'Write', 'Edit', // Tier 1
45
+ 'Bash', 'Grep', 'Glob', // Tier 1
46
+ 'WebSearch', 'WebFetch', // Tier 2
47
+ 'Task', 'TodoWrite', 'AskUserQuestion' // Tier 3+4
48
+ ]
49
+ };
50
+
51
+ /**
52
+ * Extract content from last user message
53
+ */
54
+ function getLastUserMessage(payload) {
55
+ if (!Array.isArray(payload.messages) || payload.messages.length === 0) {
56
+ return null;
57
+ }
58
+
59
+ // Find last user message
60
+ for (let i = payload.messages.length - 1; i >= 0; i--) {
61
+ const msg = payload.messages[i];
62
+ if (msg?.role === 'user') {
63
+ return msg;
64
+ }
65
+ }
66
+
67
+ return null;
68
+ }
69
+
70
+ /**
71
+ * Extract text content from message (handles string or array format)
72
+ */
73
+ function extractContent(message) {
74
+ if (!message) return '';
75
+
76
+ if (typeof message.content === 'string') {
77
+ return message.content;
78
+ }
79
+
80
+ if (Array.isArray(message.content)) {
81
+ return message.content
82
+ .filter(block => block?.type === 'text')
83
+ .map(block => block.text || '')
84
+ .join(' ');
85
+ }
86
+
87
+ return '';
88
+ }
89
+
90
+ /**
91
+ * Check if content matches greeting patterns
92
+ */
93
+ function isGreeting(content) {
94
+ const greetingPattern = /^(hi|hello|hey|good morning|good afternoon|good evening|howdy|greetings|sup|yo)[\s\.\!\?]*$/i;
95
+ return greetingPattern.test(content.trim());
96
+ }
97
+
98
+ /**
99
+ * Check if content is short and non-technical
100
+ */
101
+ function isShortNonTechnical(content) {
102
+ const trimmed = content.trim();
103
+
104
+ // Very short messages (< 20 chars)
105
+ if (trimmed.length >= 20) return false;
106
+
107
+ // Check for technical keywords
108
+ const technicalKeywords = /code|file|function|error|bug|fix|write|read|create|implement|class|module|import|export|async|await/i;
109
+ return !technicalKeywords.test(trimmed);
110
+ }
111
+
112
+ /**
113
+ * Check if content is a simple question
114
+ */
115
+ function isSimpleQuestion(content) {
116
+ const questionPattern = /^(what is|what's|how does|when|where|why|explain|define|tell me about|can you explain)/i;
117
+ return questionPattern.test(content.trim());
118
+ }
119
+
120
+ /**
121
+ * Check for technical keywords
122
+ */
123
+ function hasTechnicalKeywords(content) {
124
+ const technicalPattern = /code|function|class|file|module|import|export|async|await|promise|callback|api|database|server|client|component|method|variable|array|object|string|number/i;
125
+ return technicalPattern.test(content);
126
+ }
127
+
128
+ /**
129
+ * Check for explanation/research keywords
130
+ */
131
+ function hasExplanationKeywords(content) {
132
+ const explanationPattern = /explain|describe|summarize|what does|how does|tell me about|give me an overview|clarify|elaborate/i;
133
+ return explanationPattern.test(content);
134
+ }
135
+
136
+ /**
137
+ * Check for web/search keywords
138
+ */
139
+ function hasWebKeywords(content) {
140
+ const webPattern = /search|lookup|find info|google|documentation|docs|website|url|link|online|internet|browse/i;
141
+ return webPattern.test(content);
142
+ }
143
+
144
+ /**
145
+ * Check for file reading keywords
146
+ */
147
+ function hasReadKeywords(content) {
148
+ const readPattern = /read|show|display|view|cat|check|inspect|look at|see|examine|review|print|output/i;
149
+ return readPattern.test(content);
150
+ }
151
+
152
+ /**
153
+ * Check for file writing/modification keywords
154
+ */
155
+ function hasWriteKeywords(content) {
156
+ const writePattern = /write|create|add|update|modify|change|fix|delete|remove|insert|append|replace|save/i;
157
+ return writePattern.test(content);
158
+ }
159
+
160
+ /**
161
+ * Check for edit/refactor keywords
162
+ */
163
+ function hasEditKeywords(content) {
164
+ const editPattern = /edit|refactor|rename|move|reorganize|restructure|rewrite/i;
165
+ return editPattern.test(content);
166
+ }
167
+
168
+ /**
169
+ * Check for execution/testing keywords
170
+ */
171
+ function hasExecutionKeywords(content) {
172
+ const executionPattern = /run|execute|test|compile|build|deploy|start|install|launch|boot|fire up|npm|git|python|node|docker|bash|sh|cmd/i;
173
+ return executionPattern.test(content);
174
+ }
175
+
176
+ /**
177
+ * Check for complex task keywords
178
+ */
179
+ function hasComplexKeywords(content) {
180
+ const complexPattern = /implement|build|create|develop|design|architect|plan|strategy|approach|help with|work on|improve|optimize|enhance|refactor|migrate/i;
181
+ return complexPattern.test(content);
182
+ }
183
+
184
+ /**
185
+ * Classify request type based on content analysis
186
+ *
187
+ * @param {Object} payload - Request payload with messages
188
+ * @returns {Object} Classification result { type, confidence, keywords }
189
+ */
190
+ function classifyRequestType(payload) {
191
+ const lastMessage = getLastUserMessage(payload);
192
+
193
+ if (!lastMessage) {
194
+ logger.debug('No user message found for classification');
195
+ return { type: 'coding', confidence: 0.5, keywords: [] };
196
+ }
197
+
198
+ const content = extractContent(lastMessage);
199
+ const contentLower = content.toLowerCase();
200
+ const messageCount = payload.messages?.length ?? 0;
201
+
202
+ logger.debug({
203
+ contentLength: content.length,
204
+ messageCount,
205
+ contentPreview: content.substring(0, 100)
206
+ }, 'Classifying request type');
207
+
208
+ // 1. Conversational (no tools)
209
+ if (isGreeting(contentLower)) {
210
+ return { type: 'conversational', confidence: 1.0, keywords: ['greeting'] };
211
+ }
212
+
213
+ if (isShortNonTechnical(contentLower)) {
214
+ return { type: 'conversational', confidence: 0.8, keywords: ['short', 'non-technical'] };
215
+ }
216
+
217
+ // 2. Simple Q&A (no tools)
218
+ if (isSimpleQuestion(contentLower) && !hasTechnicalKeywords(contentLower)) {
219
+ return { type: 'simple_qa', confidence: 0.9, keywords: ['question', 'non-technical'] };
220
+ }
221
+
222
+ // 3. Research/Explanation (minimal tools)
223
+ if (hasExplanationKeywords(contentLower)) {
224
+ return { type: 'research', confidence: 0.85, keywords: ['explanation'] };
225
+ }
226
+
227
+ if (hasWebKeywords(contentLower)) {
228
+ return { type: 'research', confidence: 0.9, keywords: ['web', 'search'] };
229
+ }
230
+
231
+ // 4. File reading (read-only tools)
232
+ if (hasReadKeywords(contentLower) && !hasWriteKeywords(contentLower)) {
233
+ return { type: 'file_reading', confidence: 0.8, keywords: ['read'] };
234
+ }
235
+
236
+ // 5. File modification (full I/O tools)
237
+ if (hasWriteKeywords(contentLower) || hasEditKeywords(contentLower)) {
238
+ return { type: 'file_modification', confidence: 0.85, keywords: ['write', 'edit'] };
239
+ }
240
+
241
+ // 6. Execution/Testing (execution tools)
242
+ if (hasExecutionKeywords(contentLower)) {
243
+ return { type: 'code_execution', confidence: 0.8, keywords: ['execution'] };
244
+ }
245
+
246
+ // 7. Complex task (all tools)
247
+ if (hasComplexKeywords(contentLower)) {
248
+ return { type: 'complex_task', confidence: 0.75, keywords: ['complex'] };
249
+ }
250
+
251
+ // Long conversations likely need more tools
252
+ if (messageCount > 10) {
253
+ return { type: 'complex_task', confidence: 0.7, keywords: ['long_conversation'] };
254
+ }
255
+
256
+ // Default: coding (core tools)
257
+ return { type: 'coding', confidence: 0.6, keywords: ['default'] };
258
+ }
259
+
260
+ /**
261
+ * Estimate token count for tools (rough approximation)
262
+ */
263
+ function estimateToolTokens(tools) {
264
+ if (!Array.isArray(tools)) return 0;
265
+
266
+ // Average: ~175 tokens per tool (based on STANDARD_TOOLS analysis)
267
+ return tools.length * 175;
268
+ }
269
+
270
+ /**
271
+ * Select relevant tools based on classification
272
+ *
273
+ * @param {Array} tools - Available tools
274
+ * @param {Object} classification - Classification result from classifyRequestType
275
+ * @param {Object} options - Selection options (provider, tokenBudget, config)
276
+ * @returns {Array} Filtered list of relevant tools
277
+ */
278
+ function selectToolsSmartly(tools, classification, options = {}) {
279
+ if (!Array.isArray(tools) || tools.length === 0) {
280
+ return tools;
281
+ }
282
+
283
+ const { provider = 'databricks', tokenBudget = 2500, config = {} } = options;
284
+ const requestType = classification.type || 'coding';
285
+
286
+ // Get relevant tool names for this request type
287
+ const relevantToolNames = TOOL_SELECTION_MAP[requestType] || TOOL_SELECTION_MAP.coding;
288
+
289
+ logger.debug({
290
+ requestType,
291
+ confidence: classification.confidence,
292
+ relevantToolNames,
293
+ mode: config.mode
294
+ }, 'Selecting tools for request type');
295
+
296
+ // Filter to relevant tools only
297
+ let selectedTools = tools.filter(tool => relevantToolNames.includes(tool.name));
298
+
299
+ // Mode-specific adjustments
300
+ if (config.mode === 'aggressive') {
301
+ // Aggressive: Further reduce tools for ambiguous cases
302
+ if (classification.confidence < 0.7 && selectedTools.length > 4) {
303
+ selectedTools = selectedTools.slice(0, 4);
304
+ }
305
+ } else if (config.mode === 'conservative') {
306
+ // Conservative: Include one extra tier of tools for safety
307
+ if (requestType === 'file_reading' && !relevantToolNames.includes('Bash')) {
308
+ const bashTool = tools.find(t => t.name === 'Bash');
309
+ if (bashTool) selectedTools.push(bashTool);
310
+ }
311
+ }
312
+
313
+ // Provider-specific limits
314
+ if (provider === 'ollama' && selectedTools.length > 8) {
315
+ logger.debug({
316
+ originalCount: selectedTools.length,
317
+ limit: 8
318
+ }, 'Limiting tools for Ollama provider');
319
+ selectedTools = selectedTools.slice(0, 8);
320
+ }
321
+
322
+ // Token budget enforcement
323
+ const estimatedTokens = estimateToolTokens(selectedTools);
324
+ if (estimatedTokens > tokenBudget) {
325
+ const targetCount = Math.floor(tokenBudget / 175);
326
+ logger.debug({
327
+ estimatedTokens,
328
+ tokenBudget,
329
+ targetCount
330
+ }, 'Enforcing token budget on tools');
331
+ selectedTools = selectedTools.slice(0, Math.max(targetCount, 0));
332
+ }
333
+
334
+ // Minimal mode override (if configured)
335
+ if (config.minimalMode) {
336
+ const minimalTools = ['Read', 'Write', 'Edit', 'Bash'];
337
+ selectedTools = selectedTools.filter(t => minimalTools.includes(t.name));
338
+ }
339
+
340
+ logger.info({
341
+ requestType,
342
+ originalToolCount: tools.length,
343
+ selectedToolCount: selectedTools.length,
344
+ removedToolCount: tools.length - selectedTools.length,
345
+ estimatedTokenSavings: estimateToolTokens(tools) - estimateToolTokens(selectedTools)
346
+ }, 'Smart tool selection completed');
347
+
348
+ return selectedTools;
349
+ }
350
+
351
+ module.exports = {
352
+ classifyRequestType,
353
+ selectToolsSmartly,
354
+ estimateToolTokens,
355
+ TOOL_SELECTION_MAP
356
+ };