capability-router.ts
1 import type { AppSettings } from '@/types' 2 import { dedup } from '@/lib/shared-utils' 3 import { getToolsForCapability, TOOL_CAPABILITY } from './tool-planning' 4 import type { MessageClassification } from '@/lib/server/chat-execution/message-classifier' 5 6 export type TaskIntent = 7 | 'coding' 8 | 'research' 9 | 'browsing' 10 | 'outreach' 11 | 'scheduling' 12 | 'general' 13 14 export interface CapabilityRoutingDecision { 15 intent: TaskIntent 16 confidence: number 17 preferredTools: string[] 18 preferredDelegates: DelegateTool[] 19 primaryUrl?: string 20 } 21 22 type DelegateTool = 'delegate_to_claude_code' | 'delegate_to_codex_cli' | 'delegate_to_opencode_cli' | 'delegate_to_gemini_cli' | 'delegate_to_copilot_cli' | 'delegate_to_droid_cli' | 'delegate_to_cursor_cli' | 'delegate_to_qwen_code_cli' 23 24 function findFirstUrl(text: string): string | undefined { 25 const m = text.match(/https?:\/\/[^\s<>"')]+/i) 26 return m?.[0] 27 } 28 29 function dedupe(values: string[]): string[] { 30 return dedup(values.filter(Boolean)) 31 } 32 33 function preferredToolsForCapabilities(enabledExtensions: string[], capabilities: string[], fallback: string[] = []): string[] { 34 const preferred = capabilities.flatMap((capability) => getToolsForCapability(enabledExtensions, capability)) 35 return dedupe(preferred.length > 0 ? preferred : fallback) 36 } 37 38 function normalizeDelegateOrder(value: unknown): DelegateTool[] { 39 const fallback: DelegateTool[] = [ 40 'delegate_to_claude_code', 41 'delegate_to_codex_cli', 42 'delegate_to_opencode_cli', 43 'delegate_to_gemini_cli', 44 'delegate_to_copilot_cli', 45 'delegate_to_droid_cli', 46 'delegate_to_cursor_cli', 47 'delegate_to_qwen_code_cli', 48 ] 49 if (!Array.isArray(value) || !value.length) return fallback 50 51 const mapped: DelegateTool[] = [] 52 for (const raw of value) { 53 if (raw === 'claude') mapped.push('delegate_to_claude_code') 54 else if (raw === 'codex') mapped.push('delegate_to_codex_cli') 55 else if (raw === 'opencode') mapped.push('delegate_to_opencode_cli') 56 else if (raw === 'gemini') mapped.push('delegate_to_gemini_cli') 57 else if (raw === 'copilot') mapped.push('delegate_to_copilot_cli') 58 else if (raw === 'droid') mapped.push('delegate_to_droid_cli') 59 else if (raw === 'cursor') mapped.push('delegate_to_cursor_cli') 60 else if (raw === 'qwen') mapped.push('delegate_to_qwen_code_cli') 61 } 62 if (!mapped.length) return fallback 63 const deduped = dedup(mapped) 64 for (const tool of fallback) { 65 if (!deduped.includes(tool)) deduped.push(tool) 66 } 67 return deduped 68 } 69 70 export function routeTaskIntent( 71 message: string, 72 enabledExtensions: string[], 73 settings?: AppSettings | null, 74 classification?: MessageClassification | null, 75 ): CapabilityRoutingDecision { 76 const url = findFirstUrl(message || '') 77 const delegateOrder = normalizeDelegateOrder(settings?.autonomyPreferredDelegates) 78 const intent = classification?.taskIntent || 'general' 79 const confidence = classification?.confidence ?? 0 80 const wantsVoiceDelivery = classification?.wantsVoiceDelivery === true 81 const wantsScreenshots = classification?.wantsScreenshots === true 82 const wantsOutboundDelivery = classification?.wantsOutboundDelivery === true 83 84 if (intent === 'coding') { 85 return { 86 intent: 'coding', 87 confidence, 88 preferredTools: ['claude_code', 'codex_cli', 'opencode_cli', 'gemini_cli', 'cursor_cli', 'qwen_code_cli', 'shell', 'files', 'edit_file'], 89 preferredDelegates: delegateOrder, 90 primaryUrl: url, 91 } 92 } 93 94 if (intent === 'outreach') { 95 return { 96 intent: 'outreach', 97 confidence, 98 preferredTools: preferredToolsForCapabilities( 99 enabledExtensions, 100 [ 101 ...(wantsVoiceDelivery ? [TOOL_CAPABILITY.deliveryVoiceNote] : []), 102 ...(wantsScreenshots ? [TOOL_CAPABILITY.deliveryMedia] : []), 103 ...(wantsOutboundDelivery || wantsVoiceDelivery ? [TOOL_CAPABILITY.deliveryMessage] : []), 104 TOOL_CAPABILITY.deliveryMessage, 105 ], 106 ['connector_message_tool', 'manage_connectors', 'manage_sessions'], 107 ), 108 preferredDelegates: delegateOrder, 109 primaryUrl: url, 110 } 111 } 112 113 if (intent === 'scheduling') { 114 return { 115 intent: 'scheduling', 116 confidence, 117 preferredTools: ['manage_schedules', 'manage_tasks'], 118 preferredDelegates: delegateOrder, 119 primaryUrl: url, 120 } 121 } 122 123 if (intent === 'browsing') { 124 return { 125 intent: 'browsing', 126 confidence, 127 preferredTools: preferredToolsForCapabilities( 128 enabledExtensions, 129 [ 130 TOOL_CAPABILITY.browserCapture, 131 TOOL_CAPABILITY.browserNavigate, 132 TOOL_CAPABILITY.researchFetch, 133 ], 134 ['browser', 'web_fetch'], 135 ), 136 preferredDelegates: delegateOrder, 137 primaryUrl: url, 138 } 139 } 140 141 if (intent === 'research') { 142 const preferred = preferredToolsForCapabilities( 143 enabledExtensions, 144 [ 145 TOOL_CAPABILITY.researchSearch, 146 TOOL_CAPABILITY.researchFetch, 147 ...(wantsScreenshots ? [TOOL_CAPABILITY.browserCapture] : []), 148 ...(wantsVoiceDelivery ? [TOOL_CAPABILITY.deliveryVoiceNote] : []), 149 ...(wantsOutboundDelivery ? [TOOL_CAPABILITY.deliveryMedia, TOOL_CAPABILITY.deliveryMessage] : []), 150 ], 151 ['web_search', 'web_fetch', 'browser'], 152 ) 153 return { 154 intent: 'research', 155 confidence, 156 preferredTools: preferred, 157 preferredDelegates: delegateOrder, 158 primaryUrl: url, 159 } 160 } 161 162 return { 163 intent: 'general', 164 confidence, 165 preferredTools: [], 166 preferredDelegates: delegateOrder, 167 primaryUrl: url, 168 } 169 }