Context-Aware Routing
The Context-Aware Routing pattern uses a router agent to read the user's request, classify it into a category, and dispatch to the specialist whose domain matches.
Classic primitives: DefaultPattern, StringLLMCondition (LLM-evaluated routing inside the framework), or ExpressionContextCondition over a router-tool-set domain field.
Key Characteristics#
- Router agent thin. The router's only job is to classify and call the matching
classify_as_<category>tool. - Dynamic
Handoff. Each classify tool returnsHandoff(target=<specialist>)directly. The framework reads thetargetfrom the tool result and routes the next turn to that specialist without any graph condition — noContextEqualsrules needed. - Specialist's reply terminates. A
FromSpeaker(<specialist>) → TerminateTargetrule closes the workflow after the specialist speaks.
Routing Mechanics#
Each classify_as_<category> tool returns Handoff(target=<specialist_name>). The workflow adapter reads the Handoff.target from the tool's ToolResultEvent and stamps it onto the outgoing packet as routing.target. When fold processes that packet, expected_next_speaker is set directly from routing.target, bypassing the transition graph entirely. No ContextEquals state variable is needed.
Why not ContextEquals?
The original idiom — tools call set_context then return a string; the graph uses ContextEquals to pick the next speaker — stalls in practice.
set_context emits a non-substantive EV_CONTEXT_SET envelope, and ContextEquals only evaluates when a substantive EV_PACKET follows. But the router's classify tool also returns a plain string. If the router produces no text body (as a minimal implementation would), the round is silent: build_round_envelope returns None, no EV_PACKET is posted, fold is never called, and ContextEquals never fires. The channel stalls.
Handoff sidesteps this entirely — the routing target is resolved from the tool result, not from a graph condition evaluated on a subsequent envelope.
Agent Flow#
sequenceDiagram
participant User as user
participant Router as router
participant Billing as billing
participant Technical as technical
participant General as general
User->>Router: question
Router->>Router: classify_as_<category>(reason) → Handoff(target=<category>)
Note over Router: Handoff.target routes directly to specialist
alt Handoff(target="billing")
Router->>Billing: AgentTarget(billing) via Handoff
Billing->>User: answer; FromSpeaker(billing) → TerminateTarget("billing_resolved")
else Handoff(target="technical")
Router->>Technical: AgentTarget(technical) via Handoff
Technical->>User: answer; FromSpeaker(technical) → TerminateTarget("technical_resolved")
else Handoff(target="general")
Router->>General: AgentTarget(general) via Handoff
General->>User: answer; FromSpeaker(general) → TerminateTarget("general_resolved")
end Migrating from Classic to Beta?#
| Classic | Beta |
|---|---|
StringLLMCondition (framework asks LLM at the transition) | Router agent's LLM turn calls classify_as_<category> tool; tool returns Handoff(target=specialist) |
ReplyResult(context_variables={"category": "billing"}, target=AgentTarget(billing)) | return Handoff(target="billing", reason=reason) directly from the classify tool |
ExpressionContextCondition(...) per category | No graph condition needed — Handoff.target is authoritative |
Code#
Tip
The router uses real Sonnet (the classification is the LLM-driven part). Each specialist also uses real Sonnet to give a domain-flavoured reply.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 | |
Output#
channel: 8b4d...
user: I tried to upgrade my subscription but the API is returning a 500 error. The status page says everything is green. Help?
[tool] classify_as_technical('API returning 500 error during subscription upgrade — technical issue')
router: [Handed off via classify_as_technical] API returning 500 error during subscription upgrade — technical issue
technical: Capture the full request (endpoint, payload, response headers, and request-id) and re-try the upgrade — if the 500 persists, share the request-id so we can trace it server-side; check whether the failure is tied to a specific plan tier, payment method, or coupon, as those code paths are the most common 500 sources during upgrades.
closed: reason='technical_resolved'