Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions src/ucode/smart_routing/routing.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,8 +202,20 @@ def route_spawn_tool(
tool_input = payload.get("tool_input")
if not isinstance(tool_input, dict):
return None
task_name = tool_input.get("task_name") or tool_input.get("agent_name")
task = task_name if isinstance(task_name, str) and task_name else default_task_label
# Derive the routing task from the first available plaintext field. `message`
# carries the actual subagent task content — prefer it when present and a
# plaintext string (Codex encrypts it at send-time, but the PreToolUse hook
# fires before that, so it may be readable here). When `message` is an
# encrypted dict (or absent), fall back to `task_name` / `agent_name`
# (weaker labels), then the generic default.
task = next(
(
value
for field in ("message", "task_name", "agent_name")
if isinstance(value := tool_input.get(field), str) and value
),
default_task_label,
)
decision, _ = decision_fn(task)
if decision is None:
return None
Expand Down
58 changes: 58 additions & 0 deletions tests/test_codex_routing.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,64 @@ def test_non_spawn_tool_has_no_opinion():
)


def test_spawn_routes_on_plaintext_message_when_present(monkeypatch):
# When the spawn's `message` is a plaintext string at PreToolUse (before
# Codex encrypts it at send-time), routing uses it as the task — giving the
# router real signal instead of the generic fallback.
captured = {}

def fake_decision(*args, **kwargs):
captured["task"] = args[2] if len(args) > 2 else kwargs.get("task")
return (
codex_routing.RoutingDecision(model="databricks-gpt-5-5", raw_model="gpt-5-6-sol"),
None,
)

monkeypatch.setattr(codex_routing, "request_routing_decision", fake_decision)
codex_routing.route_pre_tool_use(
{
"tool_name": "collaborationspawn_agent",
"tool_input": {
"task_name": "task_3",
"message": "Review the parser error handling and add missing null checks",
},
},
workspace=WS,
token="token",
available_models=["databricks-gpt-5-5"],
)
assert captured["task"] == "Review the parser error handling and add missing null checks"


def test_spawn_falls_through_encrypted_message_to_task_name(monkeypatch):
# When `message` is an encrypted dict (not a plaintext string), routing
# falls through to `task_name` — no regression from the encrypted case.
captured = {}

def fake_decision(*args, **kwargs):
captured["task"] = args[2] if len(args) > 2 else kwargs.get("task")
return (
codex_routing.RoutingDecision(model="databricks-gpt-5-5", raw_model="gpt-5-6-sol"),
None,
)

monkeypatch.setattr(codex_routing, "request_routing_decision", fake_decision)
codex_routing.route_pre_tool_use(
{
"tool_name": "collaborationspawn_agent",
"tool_input": {
"task_name": "reviewer",
"message": {"encrypted": "opaque-ciphertext"},
},
},
workspace=WS,
token="token",
available_models=["databricks-gpt-5-5"],
)
# Encrypted dict skipped (not a string), fell through to task_name.
assert captured["task"] == "reviewer"


def test_canary_and_audit_are_written(tmp_path, monkeypatch):
canary = tmp_path / "canary.json"
audit = tmp_path / "audit.jsonl"
Expand Down
Loading