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.
390 lines (334 loc) • 13 kB
JavaScript
/**
* Dynamic System Prompt Optimization
* * Provides utilities for optimizing system prompts and tool descriptions
* to reduce token usage while maintaining functionality.
*
*/
const logger = require('../logger');
const config = require('../config');
/**
* Agent Delegation Instructions
*
* These instructions tell all models how to use the Task tool for spawning subagents.
* Added to system prompt when Task tool is available.
*/
const AGENT_DELEGATION_INSTRUCTIONS = `
## Task Delegation (Subagents)
You have access to the **Task** tool which spawns specialized agents to handle complex work autonomously.
### WHEN TO USE the Task Tool:
| User Request Keywords | Action |
|----------------------|--------|
| "explore", "dig into", "understand", "analyze" the codebase | \`Task(subagent_type="Explore")\` |
| "plan", "design", "architect" an implementation | \`Task(subagent_type="Plan")\` |
| Complex multi-file research or investigation | \`Task(subagent_type="general-purpose")\` |
### HOW TO CALL the Task Tool:
\`\`\`
Task(
subagent_type: "Explore",
description: "Explore project structure",
prompt: "Find main entry points, understand the architecture, read key configuration files, and provide a comprehensive summary of what this project does and how it's organized."
)
\`\`\`
### AGENT TYPES:
- **Explore**: Fast codebase exploration using Glob, Grep, Read tools. Use for searching files, understanding project structure, finding code patterns.
- **Plan**: Implementation planning and architecture design. Use for designing features, planning refactoring, or architectural decisions.
- **general-purpose**: Complex multi-step tasks with access to all tools.
### IMPORTANT:
- Subagents run independently and return a summary of their findings
- Use Explore agent for ANY codebase navigation or search tasks instead of doing it yourself
- The subagent will handle all the file reading and searching, then return results to you
`;
/**
* Compress tool descriptions to minimal format
*
* Converts verbose tool schemas to minimal versions by:
* - Shortening descriptions
* - Removing optional fields when not critical
* - Using concise parameter descriptions
*
* @param {Array} tools - Array of Anthropic-format tool definitions
* @param {string} mode - 'minimal' or 'full' (default: from config)
* @returns {Array} Optimized tool definitions
*/
function compressToolDescriptions(tools, mode = null) {
if (!tools || tools.length === 0) return tools;
mode = mode || config.systemPrompt?.toolDescriptions || 'minimal';
if (mode !== 'minimal') {
return tools; // Return unmodified if not in minimal mode
}
return tools.map(tool => {
const compressed = {
name: tool.name,
input_schema: {
type: tool.input_schema.type,
properties: {},
required: tool.input_schema.required || [],
}
};
// Add minimal description only if it exists
if (tool.description) {
compressed.description = compressText(tool.description, 50);
}
// Compress property descriptions
if (tool.input_schema.properties) {
for (const [key, value] of Object.entries(tool.input_schema.properties)) {
compressed.input_schema.properties[key] = {
type: value.type,
};
// Only include description if it's critical
if (value.description && !isObviousFromName(key)) {
compressed.input_schema.properties[key].description = compressText(value.description, 30);
}
// Preserve enum, format, and other critical constraints
if (value.enum) compressed.input_schema.properties[key].enum = value.enum;
if (value.format) compressed.input_schema.properties[key].format = value.format;
if (value.items) compressed.input_schema.properties[key].items = value.items;
if (value.additionalProperties !== undefined) {
compressed.input_schema.properties[key].additionalProperties = value.additionalProperties;
}
}
}
// Preserve additionalProperties if set
if (tool.input_schema.additionalProperties !== undefined) {
compressed.input_schema.additionalProperties = tool.input_schema.additionalProperties;
}
return compressed;
});
}
/**
* Compress text to maximum length while preserving meaning
* @param {string} text - Text to compress
* @param {number} maxLength - Maximum length
* @returns {string} Compressed text
*/
function compressText(text, maxLength) {
if (!text || text.length <= maxLength) return text;
// Try to cut at sentence/word boundary
let cut = text.substring(0, maxLength);
const lastPeriod = cut.lastIndexOf('.');
const lastSpace = cut.lastIndexOf(' ');
if (lastPeriod > maxLength * 0.7) {
return cut.substring(0, lastPeriod + 1);
} else if (lastSpace > maxLength * 0.8) {
return cut.substring(0, lastSpace);
}
return cut;
}
/**
* Check if property name is self-explanatory
* @param {string} name - Property name
* @returns {boolean} True if name is obvious
*/
function isObviousFromName(name) {
const obvious = [
'id', 'name', 'type', 'value', 'data', 'text', 'content',
'message', 'query', 'command', 'url', 'path', 'file', 'filename',
'email', 'username', 'password', 'token', 'key', 'timeout',
'limit', 'offset', 'page', 'size', 'count', 'total', 'status'
];
return obvious.includes(name.toLowerCase());
}
/**
* Optimize system prompt based on context
*
* Analyzes the system prompt and removes or compresses sections
* that aren't relevant to the current context.
*
* @param {string|Array} system - System prompt (string or content blocks)
* @param {Object} context - Context information
* @param {Array} context.tools - Tools available in this request
* @param {Array} context.messages - Recent messages
* @param {string} mode - 'dynamic' or 'full' (default: from config)
* @returns {string|Array} Optimized system prompt
*/
function optimizeSystemPrompt(system, context = {}, mode = null) {
if (!system) return system;
mode = mode || config.systemPrompt?.mode || 'dynamic';
if (mode !== 'dynamic') {
return system; // Return unmodified if not in dynamic mode
}
// Convert to string if array of blocks
let text = typeof system === 'string' ? system : flattenBlocks(system);
const optimizations = [];
const originalLength = text.length;
// 1. Remove verbose tool usage examples if no tools present
if (!context.tools || context.tools.length === 0) {
text = removeSection(text, /# Tool Usage Examples?[\s\S]*?(?=\n#|\n\n[A-Z]|$)/gi, optimizations, 'tool examples');
text = removeSection(text, /<tool_usage>[\s\S]*?<\/tool_usage>/gi, optimizations, 'tool usage blocks');
}
// 2. Remove file operation guidelines if no file tools
const hasFileTools = context.tools?.some(t =>
['Read', 'Write', 'Edit', 'Glob', 'Grep'].includes(t.name)
);
if (!hasFileTools) {
text = removeSection(text, /# File Operations?[\s\S]*?(?=\n#|\n\n[A-Z]|$)/gi, optimizations, 'file operations');
}
// 3. Remove git guidelines if no git tools
const hasGitTools = context.tools?.some(t =>
t.name.toLowerCase().includes('git')
);
if (!hasGitTools) {
text = removeSection(text, /# Git.*?[\s\S]*?(?=\n#|\n\n[A-Z]|$)/gi, optimizations, 'git guidelines');
text = removeSection(text, /## Committing changes[\s\S]*?(?=\n#|\n\n[A-Z]|$)/gi, optimizations, 'git commit guidelines');
}
// 4. Remove web search guidelines if no web tools
const hasWebTools = context.tools?.some(t =>
['WebSearch', 'WebFetch'].includes(t.name)
);
if (!hasWebTools) {
text = removeSection(text, /# Web.*?[\s\S]*?(?=\n#|\n\n[A-Z]|$)/gi, optimizations, 'web guidelines');
}
// 5. Compress code review guidelines if no recent code edits
const hasRecentEdits = context.messages?.some(m =>
typeof m.content === 'string' && m.content.toLowerCase().includes('edit')
);
if (!hasRecentEdits) {
text = removeSection(text, /# Code Review[\s\S]*?(?=\n#|\n\n[A-Z]|$)/gi, optimizations, 'code review');
}
// 6. Remove verbose examples and keep only essential instructions
text = text.replace(/(<example>[\s\S]*?<\/example>\s*){3,}/g, (match) => {
// Keep first two examples, remove rest
const examples = match.match(/<example>[\s\S]*?<\/example>/g) || [];
optimizations.push('excessive examples');
return examples.slice(0, 2).join('\n\n');
});
// 7. Compress whitespace
text = text.replace(/\n{4,}/g, '\n\n\n'); // Max 2 blank lines
text = text.replace(/[ \t]+\n/g, '\n'); // Remove trailing spaces
const finalLength = text.length;
const saved = originalLength - finalLength;
if (saved > 100 && optimizations.length > 0) {
logger.debug({
originalLength,
finalLength,
saved,
percentage: ((saved / originalLength) * 100).toFixed(1),
optimizations: [...new Set(optimizations)]
}, 'System prompt optimization applied');
}
// Return in original format (string or blocks)
return typeof system === 'string' ? text : text;
}
/**
* Remove a section from text using regex
* @param {string} text - Text to modify
* @param {RegExp} pattern - Pattern to match
* @param {Array} optimizations - Array to track optimizations
* @param {string} label - Label for this optimization
* @returns {string} Modified text
*/
function removeSection(text, pattern, optimizations, label) {
const matches = text.match(pattern);
if (matches && matches.length > 0) {
optimizations.push(label);
return text.replace(pattern, '');
}
return text;
}
/**
* Flatten content blocks to text
* @param {Array} blocks - Content blocks
* @returns {string} Flattened text
*/
function flattenBlocks(blocks) {
if (!Array.isArray(blocks)) return String(blocks || '');
return blocks
.map(block => {
if (typeof block === 'string') return block;
if (block.type === 'text' && block.text) return block.text;
if (block.text) return block.text;
return '';
})
.filter(Boolean)
.join('\n\n');
}
/**
* Analyze context to determine what optimizations are safe
* @param {Object} context - Request context
* @returns {Object} Analysis results
*/
function analyzeContext(context) {
const analysis = {
hasTools: Boolean(context.tools && context.tools.length > 0),
toolCount: context.tools?.length || 0,
toolNames: context.tools?.map(t => t.name) || [],
messageCount: context.messages?.length || 0,
hasFileOps: false,
hasGitOps: false,
hasWebOps: false,
hasBashOps: false,
};
if (context.tools) {
analysis.hasFileOps = context.tools.some(t =>
['Read', 'Write', 'Edit', 'Glob', 'Grep'].includes(t.name)
);
analysis.hasGitOps = context.tools.some(t =>
t.name.toLowerCase().includes('git')
);
analysis.hasWebOps = context.tools.some(t =>
['WebSearch', 'WebFetch'].includes(t.name)
);
analysis.hasBashOps = context.tools.some(t =>
['Bash', 'BashOutput', 'KillShell'].includes(t.name)
);
}
return analysis;
}
/**
* Calculate token savings from optimizations
* @param {string|Array} original - Original system prompt
* @param {string|Array} optimized - Optimized system prompt
* @returns {Object} Savings statistics
*/
function calculateSavings(original, optimized) {
const origText = typeof original === 'string' ? original : flattenBlocks(original);
const optText = typeof optimized === 'string' ? optimized : flattenBlocks(optimized);
const origLength = origText.length;
const optLength = optText.length;
const saved = origLength - optLength;
// Rough token estimate (4 chars ≈ 1 token)
const tokensOriginal = Math.ceil(origLength / 4);
const tokensOptimized = Math.ceil(optLength / 4);
const tokensSaved = tokensOriginal - tokensOptimized;
return {
originalChars: origLength,
optimizedChars: optLength,
charsSaved: saved,
tokensOriginal,
tokensOptimized,
tokensSaved,
percentage: origLength > 0 ? ((saved / origLength) * 100).toFixed(1) : '0.0'
};
}
/**
* Inject agent delegation instructions into system prompt
* @param {string} systemPrompt - Existing system prompt
* @param {Array} tools - Available tools
* @returns {string} System prompt with agent instructions added
*/
function injectAgentInstructions(systemPrompt, tools = []) {
// Check if Task tool is available
const hasTaskTool = tools?.some(t =>
t.name === 'Task' || t.function?.name === 'Task'
);
if (!hasTaskTool) {
return systemPrompt;
}
// Don't add if already present
if (systemPrompt && systemPrompt.includes('Task Delegation')) {
return systemPrompt;
}
// Append agent instructions
const basePrompt = systemPrompt || '';
return basePrompt + '\n\n' + AGENT_DELEGATION_INSTRUCTIONS;
}
module.exports = {
compressToolDescriptions,
optimizeSystemPrompt,
analyzeContext,
calculateSavings,
compressText,
flattenBlocks,
injectAgentInstructions,
AGENT_DELEGATION_INSTRUCTIONS,
};