feat(tasks): classify task complexity and route to appropriate model tier

Co-authored-by: tomwattsca <17442606+tomwattsca@users.noreply.github.com>
This commit is contained in:
nyk 2026-03-14 16:51:11 +07:00 committed by GitHub
parent 2fe7785433
commit 8386cfbec0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1 changed files with 61 additions and 1 deletions

View File

@ -20,6 +20,61 @@ interface DispatchableTask {
tags?: string[]
}
// ---------------------------------------------------------------------------
// Model routing
// ---------------------------------------------------------------------------
/**
* Classify a task's complexity and return the appropriate model ID to pass
* to the OpenClaw gateway. Uses keyword signals on title + description.
*
* Tiers:
* ROUTINE cheap model (Haiku) file ops, status checks, formatting
* MODERATE mid model (Sonnet) code gen, summaries, analysis, drafts
* COMPLEX premium model (Opus) debugging, architecture, novel problems
*
* The caller may override this by setting agent.config.dispatchModel.
*/
function classifyTaskModel(task: DispatchableTask): string | null {
// Allow per-agent config override
if (task.agent_config) {
try {
const cfg = JSON.parse(task.agent_config)
if (typeof cfg.dispatchModel === 'string' && cfg.dispatchModel) return cfg.dispatchModel
} catch { /* ignore */ }
}
const text = `${task.title} ${task.description ?? ''}`.toLowerCase()
const priority = task.priority?.toLowerCase() ?? ''
// Complex signals → Opus
const complexSignals = [
'debug', 'diagnos', 'architect', 'design system', 'security audit',
'root cause', 'investigate', 'incident', 'failure', 'broken', 'not working',
'refactor', 'migration', 'performance optim', 'why is',
]
if (priority === 'critical' || complexSignals.some(s => text.includes(s))) {
return '9router/cc/claude-opus-4-6'
}
// Routine signals → Haiku
const routineSignals = [
'status check', 'health check', 'ping', 'list ', 'fetch ', 'format',
'rename', 'move file', 'read file', 'update readme', 'bump version',
'send message', 'post to', 'notify', 'summarize', 'translate',
'quick ', 'simple ', 'routine ', 'minor ',
]
if (priority === 'low' && routineSignals.some(s => text.includes(s))) {
return '9router/cc/claude-haiku-4-5-20251001'
}
if (routineSignals.some(s => text.includes(s)) && priority !== 'high' && priority !== 'critical') {
return '9router/cc/claude-haiku-4-5-20251001'
}
// Default: let the agent's own configured model handle it (no override)
return null
}
/** Extract the gateway agent identifier from the agent's config JSON.
* Falls back to agent_name (display name) if openclawId is not set. */
function resolveGatewayAgentId(task: DispatchableTask): string {
@ -372,12 +427,17 @@ export async function dispatchAssignedTasks(): Promise<{ ok: boolean; message: s
// Step 1: Invoke via gateway
const gatewayAgentId = resolveGatewayAgentId(task)
const invokeParams = {
const dispatchModel = classifyTaskModel(task)
const invokeParams: Record<string, unknown> = {
message: prompt,
agentId: gatewayAgentId,
idempotencyKey: `task-dispatch-${task.id}-${Date.now()}`,
deliver: false,
}
// Route to appropriate model tier based on task complexity.
// null = no override, agent uses its own configured default model.
if (dispatchModel) invokeParams.model = dispatchModel
// Use --expect-final to block until the agent completes and returns the full
// response payload (result.payloads[0].text). The two-step agent → agent.wait
// pattern only returns lifecycle metadata and never includes the agent's text.