Spaces:
Runtime error
Runtime error
nyk tomwattsca commited on
feat(tasks): classify task complexity and route to appropriate model tier
Browse filesCo-authored-by: tomwattsca <17442606+tomwattsca@users.noreply.github.com>
- src/lib/task-dispatch.ts +61 -1
src/lib/task-dispatch.ts
CHANGED
|
@@ -20,6 +20,61 @@ interface DispatchableTask {
|
|
| 20 |
tags?: string[]
|
| 21 |
}
|
| 22 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
/** Extract the gateway agent identifier from the agent's config JSON.
|
| 24 |
* Falls back to agent_name (display name) if openclawId is not set. */
|
| 25 |
function resolveGatewayAgentId(task: DispatchableTask): string {
|
|
@@ -372,12 +427,17 @@ export async function dispatchAssignedTasks(): Promise<{ ok: boolean; message: s
|
|
| 372 |
|
| 373 |
// Step 1: Invoke via gateway
|
| 374 |
const gatewayAgentId = resolveGatewayAgentId(task)
|
| 375 |
-
const
|
|
|
|
| 376 |
message: prompt,
|
| 377 |
agentId: gatewayAgentId,
|
| 378 |
idempotencyKey: `task-dispatch-${task.id}-${Date.now()}`,
|
| 379 |
deliver: false,
|
| 380 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 381 |
// Use --expect-final to block until the agent completes and returns the full
|
| 382 |
// response payload (result.payloads[0].text). The two-step agent → agent.wait
|
| 383 |
// pattern only returns lifecycle metadata and never includes the agent's text.
|
|
|
|
| 20 |
tags?: string[]
|
| 21 |
}
|
| 22 |
|
| 23 |
+
// ---------------------------------------------------------------------------
|
| 24 |
+
// Model routing
|
| 25 |
+
// ---------------------------------------------------------------------------
|
| 26 |
+
|
| 27 |
+
/**
|
| 28 |
+
* Classify a task's complexity and return the appropriate model ID to pass
|
| 29 |
+
* to the OpenClaw gateway. Uses keyword signals on title + description.
|
| 30 |
+
*
|
| 31 |
+
* Tiers:
|
| 32 |
+
* ROUTINE → cheap model (Haiku) — file ops, status checks, formatting
|
| 33 |
+
* MODERATE → mid model (Sonnet) — code gen, summaries, analysis, drafts
|
| 34 |
+
* COMPLEX → premium model (Opus) — debugging, architecture, novel problems
|
| 35 |
+
*
|
| 36 |
+
* The caller may override this by setting agent.config.dispatchModel.
|
| 37 |
+
*/
|
| 38 |
+
function classifyTaskModel(task: DispatchableTask): string | null {
|
| 39 |
+
// Allow per-agent config override
|
| 40 |
+
if (task.agent_config) {
|
| 41 |
+
try {
|
| 42 |
+
const cfg = JSON.parse(task.agent_config)
|
| 43 |
+
if (typeof cfg.dispatchModel === 'string' && cfg.dispatchModel) return cfg.dispatchModel
|
| 44 |
+
} catch { /* ignore */ }
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
const text = `${task.title} ${task.description ?? ''}`.toLowerCase()
|
| 48 |
+
const priority = task.priority?.toLowerCase() ?? ''
|
| 49 |
+
|
| 50 |
+
// Complex signals → Opus
|
| 51 |
+
const complexSignals = [
|
| 52 |
+
'debug', 'diagnos', 'architect', 'design system', 'security audit',
|
| 53 |
+
'root cause', 'investigate', 'incident', 'failure', 'broken', 'not working',
|
| 54 |
+
'refactor', 'migration', 'performance optim', 'why is',
|
| 55 |
+
]
|
| 56 |
+
if (priority === 'critical' || complexSignals.some(s => text.includes(s))) {
|
| 57 |
+
return '9router/cc/claude-opus-4-6'
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
// Routine signals → Haiku
|
| 61 |
+
const routineSignals = [
|
| 62 |
+
'status check', 'health check', 'ping', 'list ', 'fetch ', 'format',
|
| 63 |
+
'rename', 'move file', 'read file', 'update readme', 'bump version',
|
| 64 |
+
'send message', 'post to', 'notify', 'summarize', 'translate',
|
| 65 |
+
'quick ', 'simple ', 'routine ', 'minor ',
|
| 66 |
+
]
|
| 67 |
+
if (priority === 'low' && routineSignals.some(s => text.includes(s))) {
|
| 68 |
+
return '9router/cc/claude-haiku-4-5-20251001'
|
| 69 |
+
}
|
| 70 |
+
if (routineSignals.some(s => text.includes(s)) && priority !== 'high' && priority !== 'critical') {
|
| 71 |
+
return '9router/cc/claude-haiku-4-5-20251001'
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
// Default: let the agent's own configured model handle it (no override)
|
| 75 |
+
return null
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
/** Extract the gateway agent identifier from the agent's config JSON.
|
| 79 |
* Falls back to agent_name (display name) if openclawId is not set. */
|
| 80 |
function resolveGatewayAgentId(task: DispatchableTask): string {
|
|
|
|
| 427 |
|
| 428 |
// Step 1: Invoke via gateway
|
| 429 |
const gatewayAgentId = resolveGatewayAgentId(task)
|
| 430 |
+
const dispatchModel = classifyTaskModel(task)
|
| 431 |
+
const invokeParams: Record<string, unknown> = {
|
| 432 |
message: prompt,
|
| 433 |
agentId: gatewayAgentId,
|
| 434 |
idempotencyKey: `task-dispatch-${task.id}-${Date.now()}`,
|
| 435 |
deliver: false,
|
| 436 |
}
|
| 437 |
+
// Route to appropriate model tier based on task complexity.
|
| 438 |
+
// null = no override, agent uses its own configured default model.
|
| 439 |
+
if (dispatchModel) invokeParams.model = dispatchModel
|
| 440 |
+
|
| 441 |
// Use --expect-final to block until the agent completes and returns the full
|
| 442 |
// response payload (result.payloads[0].text). The two-step agent → agent.wait
|
| 443 |
// pattern only returns lifecycle metadata and never includes the agent's text.
|