Skip to content

Commit a6207dd

Browse files
committed
Route root model on the launch-time seed prompt when present
When the user's first prompt is on the command line (`codex "<prompt>"`, `codex exec "<prompt>"`, or after `--`), route the root-session model on that actual prompt instead of a generic placeholder. A bare interactive launch has no prompt yet and falls back to the placeholder — the root model can't be re-routed once the TUI is running (no hook/MCP seam exists for it on either Codex or Claude Code). Adds a shared, conservative `extract_seed_prompt` to routing.py: it skips value-taking options and their values, treats unknown flags as booleans, and honors the `--` positional separator, returning None (→ placeholder) unless it is confident it found the prompt. Co-authored-by: Isaac
1 parent 8b9957f commit a6207dd

3 files changed

Lines changed: 118 additions & 6 deletions

File tree

src/ucode/smart_routing/codex_routing.py

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,42 @@ def route_launch_model(state: dict, tool_args: list[str]):
5656
return request_routing_decision(workspace, token, task, models)
5757

5858

59+
# Codex CLI options that consume a following value (from `codex --help`); their
60+
# values must not be mistaken for the seed prompt when parsing launch args.
61+
_CODEX_VALUE_OPTIONS = frozenset(
62+
{
63+
"-c",
64+
"--config",
65+
"-i",
66+
"--image",
67+
"-m",
68+
"--model",
69+
"-p",
70+
"--profile",
71+
"-s",
72+
"--sandbox",
73+
"-a",
74+
"--ask-for-approval",
75+
"-C",
76+
"--cd",
77+
"--add-dir",
78+
"--enable",
79+
"--disable",
80+
"--local-provider",
81+
"--remote",
82+
"--remote-auth-token-env",
83+
}
84+
)
85+
86+
5987
def _launch_routing_task(tool_args: list[str]) -> str:
60-
if "exec" in tool_args:
61-
prompt_parts = tool_args[tool_args.index("exec") + 1 :]
62-
if prompt_parts:
63-
return " ".join(prompt_parts)
64-
if tool_args:
65-
return "Start a Codex session with options: " + " ".join(tool_args)
88+
# Route on the user's real first prompt when it's on the command line
89+
# (`codex "<prompt>"`, `codex exec "<prompt>"`, or after `--`). A bare
90+
# interactive launch has no prompt yet, so fall back to a generic task — the
91+
# root model can't be re-routed once the TUI is running (no hook/MCP seam).
92+
seed = routing.extract_seed_prompt(tool_args, _CODEX_VALUE_OPTIONS)
93+
if seed:
94+
return seed
6695
return f"Start an interactive Codex coding session in {Path.cwd().name}."
6796

6897

src/ucode/smart_routing/routing.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,60 @@ def normalize_model(model: str) -> str:
4848
return tail.lower()
4949

5050

51+
def extract_seed_prompt(tool_args: list[str], value_options: frozenset[str]) -> str | None:
52+
"""Recover a launch-time seed prompt from the passthrough CLI args.
53+
54+
A coding agent launched as ``<tool> [OPTIONS] [PROMPT]`` (or with an explicit
55+
``exec <PROMPT>`` subcommand) may carry the user's first prompt on the command
56+
line. When it does, routing the root-session model on that real prompt beats
57+
the generic placeholder. Everything after a ``--`` separator is treated as
58+
positional (the agent's own convention for "stop parsing flags").
59+
60+
``value_options`` is the set of the tool's flags that consume a following
61+
value (e.g. ``-m``/``--model``); their values are skipped so a flag argument
62+
is never mistaken for the prompt. Returns the joined positional tokens, or
63+
None when the args carry no unambiguous prompt (bare launch, or only flags) —
64+
the caller then falls back to its placeholder task.
65+
66+
Conservative by design: an unrecognized ``--flag`` (not in ``value_options``)
67+
is treated as a boolean and skipped, and ``--flag=value`` forms are skipped
68+
whole. We only return text we are confident is the user's prompt.
69+
"""
70+
if "exec" in tool_args:
71+
after = tool_args[tool_args.index("exec") + 1 :]
72+
positionals = _positional_args(after, value_options)
73+
return " ".join(positionals) if positionals else None
74+
positionals = _positional_args(tool_args, value_options)
75+
return " ".join(positionals) if positionals else None
76+
77+
78+
def _positional_args(args: list[str], value_options: frozenset[str]) -> list[str]:
79+
positionals: list[str] = []
80+
i = 0
81+
seen_double_dash = False
82+
while i < len(args):
83+
arg = args[i]
84+
if seen_double_dash:
85+
positionals.append(arg)
86+
i += 1
87+
continue
88+
if arg == "--":
89+
seen_double_dash = True
90+
i += 1
91+
continue
92+
if arg.startswith("-") and arg != "-":
93+
# `--opt=value` carries its own value; a bare option in value_options
94+
# consumes the next token. Anything else is a boolean flag we skip.
95+
if "=" not in arg and arg in value_options:
96+
i += 2
97+
else:
98+
i += 1
99+
continue
100+
positionals.append(arg)
101+
i += 1
102+
return positionals
103+
104+
51105
def select_route(
52106
workspace: str,
53107
token: str,

tests/test_agent_codex.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,35 @@ def test_disable_removes_only_ucode_hooks(self, tmp_path, monkeypatch):
366366
def test_launch_task_uses_exec_prompt(self):
367367
assert codex_routing._launch_routing_task(["exec", "fix the parser"]) == "fix the parser"
368368

369+
def test_launch_task_uses_positional_interactive_prompt(self):
370+
# `codex "fix the parser"` — the seed prompt is routed directly, not
371+
# wrapped in a placeholder.
372+
assert codex_routing._launch_routing_task(["fix the parser"]) == "fix the parser"
373+
374+
def test_launch_task_skips_value_option_before_prompt(self):
375+
# `-m <model>` consumes its value; the model id must not be taken as the
376+
# prompt.
377+
assert (
378+
codex_routing._launch_routing_task(["-m", "gpt-5", "refactor the parser"])
379+
== "refactor the parser"
380+
)
381+
382+
def test_launch_task_honors_double_dash(self):
383+
assert (
384+
codex_routing._launch_routing_task(["--", "--not-a-flag prompt"])
385+
== "--not-a-flag prompt"
386+
)
387+
388+
def test_launch_task_bare_launch_falls_back_to_placeholder(self):
389+
# No prompt on the command line → generic task (root model can't be
390+
# re-routed once the TUI is up).
391+
task = codex_routing._launch_routing_task([])
392+
assert "interactive Codex" in task
393+
394+
def test_launch_task_flags_only_falls_back_to_placeholder(self):
395+
task = codex_routing._launch_routing_task(["--search", "-m", "gpt-5"])
396+
assert "interactive Codex" in task
397+
369398

370399
class TestCodexRemoveLegacyProfile:
371400
def test_drops_provider_block_on_modern_path(self, tmp_path, monkeypatch):

0 commit comments

Comments
 (0)