Skip to content

Commit f29d4d0

Browse files
committed
Surface the router rationale in the shown message, and log it
The subagent-routing hook fetched the gateway's rationale but only put it in permissionDecisionReason, so the line the harness surfaces (the systemMessage, e.g. "Using Smart Routing. Routing to gpt-5.6-sol.") omitted the "why". Append the rationale to the systemMessage too, so users see the reason alongside the model. Also persist the rationale in the decisions.jsonl record, so an empty value cleanly distinguishes "the gateway returned no rationale" from a display-side placement bug. Co-authored-by: Isaac
1 parent 579a5cb commit f29d4d0

2 files changed

Lines changed: 47 additions & 3 deletions

File tree

src/ucode/smart_routing/routing.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -200,14 +200,17 @@ def route_spawn_tool(
200200
if record_decision is not None:
201201
record_decision(payload, task, decision, routed_model)
202202
routing_message = f"Using Smart Routing. Routing to {routed_model}."
203+
# Append the router's rationale to BOTH the systemMessage (the line the
204+
# harness surfaces to the user) and permissionDecisionReason, so the "why"
205+
# is visible, not just the "what". Empty when the gateway returns none.
206+
if decision.rationale:
207+
routing_message += f" {decision.rationale}"
203208
output: dict[str, Any] = {
204209
"hookEventName": "PreToolUse",
205210
"permissionDecision": "allow",
206211
"updatedInput": {**tool_input, "model": routed_model},
207212
"permissionDecisionReason": routing_message,
208213
}
209-
if decision.rationale:
210-
output["permissionDecisionReason"] += f" {decision.rationale}"
211214
return {"systemMessage": routing_message, "hookSpecificOutput": output}
212215

213216

@@ -271,6 +274,9 @@ def write_decision_record(
271274
"task_name": task_name,
272275
"router_model": decision.raw_model,
273276
"requested_model": requested_model,
277+
# Persisted for diagnosis: an empty value means the gateway returned
278+
# no rationale (vs. a placement bug in how we surface it).
279+
"rationale": decision.rationale,
274280
"at": time.time(),
275281
},
276282
)

tests/test_codex_routing.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,11 @@ def test_spawn_rewrite_preserves_original_input(monkeypatch):
143143
)
144144

145145
hook = output["hookSpecificOutput"]
146-
assert output["systemMessage"] == "Using Smart Routing. Routing to gpt-5.5."
146+
# The rationale is surfaced in BOTH the systemMessage (shown to the user) and
147+
# permissionDecisionReason, so the "why" is visible, not just the "what".
148+
assert output["systemMessage"] == (
149+
"Using Smart Routing. Routing to gpt-5.5. Review needs deeper reasoning."
150+
)
147151
assert hook["permissionDecision"] == "allow"
148152
assert hook["updatedInput"] == {
149153
"task_name": "reviewer",
@@ -276,3 +280,37 @@ def test_decision_is_reconciled_with_actual_subagent_model(tmp_path, monkeypatch
276280
assert record["router_model"] == "gpt-5-6-luna"
277281
assert record["requested_model"] == "gpt-5.6-luna"
278282
assert record["matches_router_decision"] is True
283+
284+
285+
def test_decision_record_persists_rationale(tmp_path, monkeypatch):
286+
decisions = tmp_path / "decisions.jsonl"
287+
monkeypatch.setattr(codex_routing, "DECISIONS_PATH", decisions)
288+
monkeypatch.setattr(
289+
codex_routing,
290+
"request_routing_decision",
291+
lambda *args, **kwargs: (
292+
codex_routing.RoutingDecision(
293+
model="system.ai.gpt-5-6-sol",
294+
raw_model="gpt-5-6-sol",
295+
rationale="Cross-cutting refactor needs the strongest model.",
296+
),
297+
None,
298+
),
299+
)
300+
301+
codex_routing.route_pre_tool_use(
302+
{
303+
"session_id": "s1",
304+
"tool_name": "collaborationspawn_agent",
305+
"tool_input": {"task_name": "refactor", "message": "encrypted"},
306+
},
307+
workspace=WS,
308+
token="token",
309+
available_models=["system.ai.gpt-5-6-sol", "system.ai.gpt-5-6-luna"],
310+
audit_decision=True,
311+
)
312+
313+
# The rationale is persisted so an empty value distinguishes "gateway
314+
# returned none" from a display-placement bug.
315+
record = json.loads(decisions.read_text().strip())
316+
assert record["rationale"] == "Cross-cutting refactor needs the strongest model."

0 commit comments

Comments
 (0)