/**
* Intent Router
*
* Task 5.2: Intent Router with Capability Matching
* Constraint: Explicit tool selection → Capability-based routing
*
* Witness Outcome: "greet" → routes to example-tool | "create-resource" → routes to data-tool
*/
import type { ToolRegistry, ToolMetadata } from './tool-registry.js';
export interface RoutingDecision {
toolName: string;
confidence: number; // 0-1
reason: string;
}
/**
* Intent Router
*
* Routes actions to tools based on capability matching.
* Handles ambiguous actions (multiple tools match) and unknown actions (no tool matches).
*/
export class IntentRouter {
constructor(private registry: ToolRegistry) {}
/**
* Route action to best-match tool
*
* @param action - The action to route (e.g., "greet", "create-resource")
* @param explicitTool - Optional explicit tool selection (for debugging/override)
* @returns Routing decision or null if no tool found
*/
route(action: string, explicitTool?: string): RoutingDecision | null {
// Handle explicit tool selection (for debugging/override)
if (explicitTool) {
const tool = this.registry.getTool(explicitTool);
if (tool) {
console.error(`[IntentRouter] Explicit tool selection: ${explicitTool}`);
return {
toolName: explicitTool,
confidence: 1.0,
reason: 'Explicit tool selection',
};
} else {
console.error(`[IntentRouter] Explicit tool not found: ${explicitTool}`);
return null;
}
}
// Find tools with matching capability
const matches = this.registry.findToolsByCapability(action);
if (matches.length === 0) {
console.error(`[IntentRouter] No tool found for action: ${action}`);
return null;
}
if (matches.length === 1) {
console.error(`[IntentRouter] Single match: ${action} → ${matches[0].name}`);
return {
toolName: matches[0].name,
confidence: 1.0,
reason: 'Single capability match',
};
}
// Multiple matches - use heuristic scoring
const scored = matches.map(tool => ({
tool,
score: this.scoreMatch(action, tool),
}));
scored.sort((a, b) => b.score - a.score);
const best = scored[0];
console.error(`[IntentRouter] Multiple matches (${matches.length}): ${action} → ${best.tool.name} (confidence: ${best.score.toFixed(2)})`);
return {
toolName: best.tool.name,
confidence: best.score,
reason: `Best match from ${matches.length} candidates`,
};
}
/**
* Score match between action and tool
*
* Scoring heuristics:
* - Exact capability match: 1.0
* - Partial match (substring): 0.7
* - No match: 0.0
*/
private scoreMatch(action: string, tool: ToolMetadata): number {
// Exact capability match
if (tool.capabilities.includes(action)) {
return 1.0;
}
// Partial match (e.g., "read" matches "read-resource")
const partialMatches = tool.capabilities.filter(cap =>
cap.includes(action) || action.includes(cap)
);
if (partialMatches.length > 0) {
return 0.7;
}
return 0.0;
}
/**
* Find all matching tools for action (for debugging)
*/
findMatches(action: string): ToolMetadata[] {
return this.registry.findToolsByCapability(action);
}
/**
* List all available capabilities across all tools
*/
listCapabilities(): string[] {
const allMetadata = this.registry.listToolMetadata();
const capabilities = new Set<string>();
for (const metadata of allMetadata) {
for (const cap of metadata.capabilities) {
capabilities.add(cap);
}
}
return Array.from(capabilities).sort();
}
}