/ src / lib / server / capability-router.ts
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  }