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.
321 lines (273 loc) • 9.43 kB
JavaScript
/**
* Agentic Workflow Detector
* Detects multi-step tool chains and autonomous agent patterns
* Used to boost complexity tier for agentic workloads
*/
const logger = require('../logger');
// Agent type classification with tier requirements
const AGENT_TYPES = {
SINGLE_SHOT: {
minTier: 'SIMPLE',
scoreBoost: 0,
description: 'Simple request-response, no tool chains',
},
TOOL_CHAIN: {
minTier: 'MEDIUM',
scoreBoost: 15,
requiresToolUse: true,
description: 'Sequential tool usage (read -> edit -> test)',
},
ITERATIVE: {
minTier: 'COMPLEX',
scoreBoost: 25,
requiresToolUse: true,
description: 'Retry loops, debugging cycles, iterative refinement',
},
AUTONOMOUS: {
minTier: 'REASONING',
scoreBoost: 35,
requiresToolUse: true,
description: 'Open-ended tasks, full autonomy, complex decision making',
},
};
// Detection patterns
const PATTERNS = {
// Tool chain indicators
toolChain: /\b(then\s+use|after\s+that|next\s+step|finally|first.*then|step\s*\d+)\b/i,
// Iterative work indicators
iterative: /\b(keep\s+trying|until|repeat|loop|retry|iterate|fix.*again|try.*different|debug)\b/i,
// Autonomous work indicators
autonomous: /\b(figure\s+out|solve|complete\s+the\s+task|do\s+whatever|make\s+it\s+work|find\s+a\s+way|whatever\s+it\s+takes)\b/i,
// Multi-file work
multiFile: /\b(multiple\s+files?|across\s+(the\s+)?codebase|all\s+files?|refactor\s+entire|whole\s+project|everywhere)\b/i,
// Planning indicators
planning: /\b(plan|design|architect|strategy|roadmap|approach|how\s+would\s+you)\b/i,
// Implementation indicators
implementation: /\b(implement|build|create|develop|write|code|add\s+feature)\b/i,
// Analysis indicators
analysis: /\b(analyze|investigate|understand|explain|why\s+is|what\s+causes|root\s+cause)\b/i,
// Testing indicators
testing: /\b(test|verify|validate|check|ensure|confirm|make\s+sure)\b/i,
};
// High-complexity tools that indicate agentic work
const AGENTIC_TOOLS = new Set([
// Execution tools
'Bash', 'bash', 'shell', 'execute', 'run_command',
// Write tools
'Write', 'write_file', 'fs_write', 'create_file',
// Edit tools
'Edit', 'edit_file', 'fs_edit', 'edit_patch', 'str_replace_editor',
// Agent tools
'Task', 'agent_task', 'spawn_agent', 'delegate',
// Git tools
'Git', 'git_commit', 'git_push', 'git_create_branch',
// Test tools
'Test', 'run_tests', 'pytest', 'jest',
// Notebook tools
'NotebookEdit', 'notebook_edit',
]);
// Read-only tools (lower complexity)
const READ_ONLY_TOOLS = new Set([
'Read', 'read_file', 'fs_read',
'Glob', 'glob', 'find_files',
'Grep', 'grep', 'search', 'ripgrep',
'WebFetch', 'web_fetch', 'fetch_url',
'WebSearch', 'web_search',
]);
class AgenticDetector {
/**
* Detect agentic workflow patterns
* @param {Object} payload - Request payload with messages and tools
* @returns {Object} Detection result
*/
detect(payload) {
const messages = payload?.messages || [];
const tools = payload?.tools || [];
const content = this._extractContent(messages);
let score = 0;
const signals = [];
// Signal 1: Tool count (many tools = likely multi-step)
const toolCount = tools.length;
if (toolCount > 10) {
score += 25;
signals.push({ signal: 'very_high_tool_count', value: toolCount, weight: 25 });
} else if (toolCount > 5) {
score += 15;
signals.push({ signal: 'high_tool_count', value: toolCount, weight: 15 });
} else if (toolCount > 3) {
score += 8;
signals.push({ signal: 'moderate_tool_count', value: toolCount, weight: 8 });
}
// Signal 2: Agentic tools present (Bash, Write, Edit, Task)
const agenticToolCount = tools.filter(t => {
const name = t.name || t.function?.name || '';
return AGENTIC_TOOLS.has(name);
}).length;
if (agenticToolCount > 3) {
score += 25;
signals.push({ signal: 'many_agentic_tools', value: agenticToolCount, weight: 25 });
} else if (agenticToolCount > 1) {
score += 15;
signals.push({ signal: 'has_agentic_tools', value: agenticToolCount, weight: 15 });
} else if (agenticToolCount === 1) {
score += 8;
signals.push({ signal: 'single_agentic_tool', value: agenticToolCount, weight: 8 });
}
// Signal 3: Prior tool results (already in agentic loop)
const toolResultCount = this._countToolResults(messages);
if (toolResultCount > 5) {
score += 30;
signals.push({ signal: 'deep_tool_loop', value: toolResultCount, weight: 30 });
} else if (toolResultCount > 2) {
score += 20;
signals.push({ signal: 'active_tool_loop', value: toolResultCount, weight: 20 });
} else if (toolResultCount > 0) {
score += 10;
signals.push({ signal: 'has_tool_results', value: toolResultCount, weight: 10 });
}
// Signal 4: Pattern matching on content
if (PATTERNS.autonomous.test(content)) {
score += 25;
signals.push({ signal: 'autonomous_pattern', weight: 25 });
}
if (PATTERNS.iterative.test(content)) {
score += 20;
signals.push({ signal: 'iterative_pattern', weight: 20 });
}
if (PATTERNS.toolChain.test(content)) {
score += 15;
signals.push({ signal: 'tool_chain_pattern', weight: 15 });
}
if (PATTERNS.multiFile.test(content)) {
score += 15;
signals.push({ signal: 'multi_file_work', weight: 15 });
}
if (PATTERNS.planning.test(content)) {
score += 10;
signals.push({ signal: 'planning_required', weight: 10 });
}
if (PATTERNS.implementation.test(content) && PATTERNS.testing.test(content)) {
score += 15;
signals.push({ signal: 'implementation_with_testing', weight: 15 });
}
// Signal 5: Conversation depth
const messageCount = messages.length;
if (messageCount > 15) {
score += 20;
signals.push({ signal: 'very_deep_conversation', value: messageCount, weight: 20 });
} else if (messageCount > 8) {
score += 12;
signals.push({ signal: 'deep_conversation', value: messageCount, weight: 12 });
} else if (messageCount > 4) {
score += 6;
signals.push({ signal: 'ongoing_conversation', value: messageCount, weight: 6 });
}
// Signal 6: Content length (longer prompts often = more complex tasks)
if (content.length > 2000) {
score += 10;
signals.push({ signal: 'long_prompt', value: content.length, weight: 10 });
}
// Determine agent type
const agentType = this._classifyAgentType(score, signals);
const isAgentic = score >= 25;
const result = {
isAgentic,
agentType,
confidence: Math.min(score / 100, 1),
score,
signals,
minTier: AGENT_TYPES[agentType].minTier,
scoreBoost: AGENT_TYPES[agentType].scoreBoost,
description: AGENT_TYPES[agentType].description,
};
if (isAgentic) {
logger.debug({
agentType,
score,
signalCount: signals.length,
toolCount,
toolResultCount,
}, '[AgenticDetector] Agentic workflow detected');
}
return result;
}
/**
* Classify agent type based on score and signals
*/
_classifyAgentType(score, signals) {
// Check for specific signal combinations
const hasAutonomousPattern = signals.some(s => s.signal === 'autonomous_pattern');
const hasDeepToolLoop = signals.some(s => s.signal === 'deep_tool_loop');
const hasManyAgenticTools = signals.some(s => s.signal === 'many_agentic_tools');
// Autonomous: high score + autonomous pattern or very deep tool usage
if (score >= 60 || (hasAutonomousPattern && score >= 40)) {
return 'AUTONOMOUS';
}
// Iterative: moderate-high score with tool loops
if (score >= 40 || (hasDeepToolLoop && score >= 30)) {
return 'ITERATIVE';
}
// Tool chain: some tool usage indicated
if (score >= 20 || hasManyAgenticTools) {
return 'TOOL_CHAIN';
}
return 'SINGLE_SHOT';
}
/**
* Extract user content from messages
*/
_extractContent(messages) {
const userMsgs = messages.filter(m => m?.role === 'user');
if (userMsgs.length === 0) return '';
// Get last user message
const last = userMsgs[userMsgs.length - 1];
if (typeof last.content === 'string') {
return last.content;
}
if (Array.isArray(last.content)) {
return last.content
.filter(block => block?.type === 'text')
.map(block => block.text || '')
.join(' ');
}
return '';
}
/**
* Count tool results in conversation
*/
_countToolResults(messages) {
let count = 0;
for (const msg of messages) {
if (msg?.role === 'user' && Array.isArray(msg.content)) {
count += msg.content.filter(c => c?.type === 'tool_result').length;
}
}
return count;
}
/**
* Get detection stats for debugging
*/
getPatternStats(content) {
const stats = {};
for (const [name, pattern] of Object.entries(PATTERNS)) {
stats[name] = pattern.test(content);
}
return stats;
}
}
// Singleton instance
let instance = null;
function getAgenticDetector() {
if (!instance) {
instance = new AgenticDetector();
}
return instance;
}
module.exports = {
AgenticDetector,
getAgenticDetector,
AGENT_TYPES,
PATTERNS,
AGENTIC_TOOLS,
READ_ONLY_TOOLS,
};