nyk tomwattsca commited on
Commit
d91ef14
·
unverified ·
1 Parent(s): 58fae19

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

Browse files

Co-authored-by: tomwattsca <17442606+tomwattsca@users.noreply.github.com>

Files changed (1) hide show
  1. 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 invokeParams = {
 
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.