Skip to content

fix(cli): route malformed commands to typer, not the expert agent - #203

Merged
lzsweb merged 13 commits into
mainfrom
release/agentic-cli-6
Sep 24, 2026
Merged

lzsweb merged 13 commits into
mainfrom
release/agentic-cli-6

Conversation

@lzsweb

@lzsweb lzsweb commented Sep 22, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • dashscope auth whoami opened an Expert chat instead of verifying the key. c4e38e7 registered app.add_typer(auth.app) but never added "auth" to _TOP_LEVEL_COMMANDS, so main()'s unknown-command branch forwarded the line to the agent; login/logout were unreachable. auth --help kept working, which hid the gap — the help branch dispatches before routing.
  • The same catch-all swallowed genuine errors. dashscope -w wsid generation create opened a chat rather than reporting No such option: -w, and dashscope generaton create hid a typo typer would have answered with "Did you mean 'generation'". Three paths reach the agent now: a bare dashscope, an explicit dashscope expert ..., and a single unknown token — the last one keeps the documented dashscope "question" one-shot working. Everything else goes to typer. This reverses only the routing half of b808fcd; the typer subcommand and the ~/.acli config sync it deleted stay deleted.
  • Behavior change worth a release note: expert is back as a routing keyword (not a typer group, and deliberately kept out of _TOP_LEVEL_COMMANDS, which now only bounds global -k/--api-key parsing). Multi-word unknown commands are an error again instead of silently opening a chat.
  • README, README_zh and the expert's cli.md skill all advertised the removed catch-all, so their wording is updated to match; cli.md also gains the missing auth row, prerequisites and troubleshooting entries — it claims to be authoritative for CLI usage but could not answer auth questions either.
  • 1bdebbd tightens acli rule 18 to demand cross-reference resolution when output transforms or joins data (mirrors agenticCLI a7b1a84): schema-valid output with dangling IDs still fails the grader.

Test plan

  • pytest tests/unit — 949 passed, 7 skipped
  • tests/unit/test_cli_agent_route.py — 12 routing tests, including the new test_auth_whoami_executes_instead_of_routing_to_agent and a one-directional invariant asserting every registered typer group is routable (rl/agentic-rl stay listed when the optional extra is absent)
  • 39-case argv matrix against a stubbed _route_to_expert, checking destination, forwarded prompt and TUI flag
  • Live runs against the installed console script: dashscope expert --help → exit 0 usage text; dashscope generaton create → exit 2 No such command 'generaton'. Did you mean 'generation', 'image-...; dashscope -w wsid generation create → exit 2 No such option: -w; dashscope --foo → exit 2 No such option: --foo; dashscope auth whoami → exit 1 Error: No API key configured.
  • pre-commit clean (black, pylint, add-trailing-comma)
  • Terminal-Bench hard re-run for 1bdebbd on a second seed — single-run task-level variance on hard is about ±2, so the 3p/1f → 4p/0f result is one observation, not a measured mean

zhansheng.lzs added 13 commits September 18, 2026 18:12
…rmed output

Mirrors agenticCLI a7b1a84. Rule 18 only required verifying code changes, so an
agent producing a data document could pass every check it ran and still ship a
broken one: Terminal-Bench hard-066c had organization-json-generator write
employee *names* into project.members where the grader wants employee IDs, and
report the conversion as deliberate -- "member IDs resolved to employee names,
per schema's members: string[]". jsonschema agreed with it, because the schema
only constrains members to an array of strings, so 3 of 4 tests passed and
test_relationships_integrity failed on 'Cristiano Ronaldo' not in
{'D001-E001', 'D001-E002', 'D001-E003'}.

Names the case in the rule: when output transforms or joins data (ETL,
CSV/JSON, migrations), also assert every cross-reference resolves, since
schema-valid output with dangling IDs still fails.

Measured on Terminal-Bench hard (10 tasks, qwen3.8-max, serial; wheel ae86d994
whose only content delta from the before-wheel ab5c911a is this clause): the
task went 3p/1f -> 4p/0f and wrote an explicit dangling-reference check. Batch
score unchanged at 6/10 -- two out-of-scope tasks flipped negative and only 4
of the 6 passes are shared between runs, so single-run task-level variance on
hard is about +/-2. Full attribution in agenticCLI
bench/terminal-bench/RESULTS.md, runs hard-066c / hard-066d.
c4e38e7 registered app.add_typer(auth.app) but never added "auth" to
_TOP_LEVEL_COMMANDS, so main()'s unknown-command branch forwarded the line to
the SDK Expert agent: `dashscope auth whoami` opened a chat session instead of
verifying the key, and login/logout were unreachable. `auth --help` kept
working, which hid the gap -- the help branch dispatches before routing.

Also document auth in the expert's cli.md skill. It claims to be authoritative
for CLI usage but had no auth row, so the agent could not answer either.

The new test asserts every registered typer group is routable. One-directional
on purpose: rl/agentic-rl stay listed when the optional extra is absent.
main() sent any line whose first token was not in _TOP_LEVEL_COMMANDS to the
agent as a question. That catch-all swallowed real errors: `dashscope -w wsid
generation create` opened a chat instead of reporting "No such option: -w",
and `dashscope generaton create` hid a typo that typer would have answered
with "Did you mean 'generation'". It is also why the missing auth entry fixed
in 6008270 silently disabled a working command rather than erroring.

Three paths reach the agent now: a bare `dashscope`, an explicit `dashscope
expert ...`, and a single unknown token -- the last one keeps the documented
`dashscope "question"` one-shot working. Everything else goes to typer.

`expert` is a routing keyword, not a typer group, and stays out of
_TOP_LEVEL_COMMANDS; that set now only bounds global -k/--api-key parsing.
This reverses the routing half of b808fcd -- the typer subcommand and the
~/.acli config sync it deleted stay deleted.

README, README_zh and the expert's cli.md skill all advertised the removed
catch-all, so their wording is updated to match.
The single-unknown-token short form made a quoted command line
indistinguishable from a question: `dashscope "auth whoami"` opened a chat
whose first user message was the command itself, which in a session log
looks exactly like someone asking about it. Drop the short form so every
invocation that is not a bare `dashscope` or `dashscope expert ...` goes to
typer, which parses real commands and reports unknown ones with a "Did you
mean" suggestion. `dashscope expert "question"` remains the one-shot form.

The vendored acli entrypoint had the same flaw in a worse shape: an
unrecognized token fell through to the interactive agent and discarded the
arguments entirely, so `acli auth whoami` produced no error at all. It now
exits 2 with a usage error, matching typer.
Synced from agenticCLI. The gateway binds each model to exactly one aigc
service path and answers the other with a 400 "url error", so the probe
order decides who pays for an unlearned model. text-generation went first,
which made the default model (qwen3.8-max, multimodal-bound) fail the first
request of every session — in the prod gateway log that is an
InvalidParameter with a fully tokenized body thrown away, then a retry that
re-uploads and re-tokenizes it.

The learned path was also kept only on the instance, and a provider is
built per session start, per model switch and per vision call, so nothing
survived between them. Persist the binding next to the global config and
share it across instances; drop unreadable or unknown-path entries instead
of repairing them, since a lost binding only costs one probe.
Eleven tests over the two commands a user runs first, against a throwaway
~/.dashscope and a stubbed Models.list, so no real key file is touched and
no request is sent.

whoami: the login guidance and exit 1 when nothing is configured; the masked
key with its source on success; an environment key reported as its own source
rather than the file; a short key masked as *** instead of half-revealed;
exit 2 on a rejected key without leaking it; exit 2 with the reason when the
validation call itself fails.

login: prompting and saving, --key skipping the prompt, surrounding whitespace
stripped, a blank key refused without writing anything, and a round trip
proving whoami accepts what login wrote.

The key_store fixture has to move both bindings of the api-key path constant:
save_api_key/get_default_api_key read it from dashscope.common.api_key while
auth reads its own imported copy, so patching one leaves login and whoami
silently disagreeing about where the key lives.

Messages are compared through a newline-stripping helper because rich hard-wraps
at 80 columns under CliRunner and force-breaks long tokens without inserting a
space. Wrapping Console.width instead would pin _width on the module-level
consoles past teardown.

Checked against three deliberate breaks of auth.py: 7 of the 11 failed, then
all 11 passed again once it was restored.
Vendors agenticCLI 4876f43 plus the 0.6.7 bump. All seven files are
byte-identical to the upstream sources modulo the dashscope. import prefix.

render_skill no longer runs str.format() over a skill body. A body is Markdown
full of literal braces from the JSON and Python it documents, so
text-generation.md — which shows messages=[{"role": "user", ...}] and declares
no arguments at all — raised KeyError('"role"'), and the old `except KeyError`
reported that as "missing args" for a skill that takes none. 6 of the 13
bundled skills were unusable through use_skill while /skill and the catalog
kept advertising them. Substitution is now a plain replace of the declared
arguments only; use_skill and the scheduler branch on `is None` rather than
falsiness, so the missing-args hint can no longer render empty.

search_files matches the pattern against paths relative to the search root as
well as the basename, and skips dot-directories only unless the pattern names
one, so .acli/skills/*.md is reachable without walking every .git in the tree.
Its description now says that * crosses separators, which it does under fnmatch
and did not under the old basename-only match, and the no-match message states
what was actually searched instead of implying the file is absent.

EnvironmentSection states the platform and how run_command executes, through a
new shell_label() that lives next to _spawn so the prompt cannot drift from
what the tool really runs. Nothing used to say which shell this is: on Windows
the model wrote `2>/dev/null || find /` and spent a turn on a PowerShell
ParserError.

Child-process output decoding tries UTF-8 strictly first, then the Windows OEM
code page from GetOEMCP() and the stdio/locale encodings. A piped child writes
in its console's code page, which need not equal sys.stdout.encoding, and cp936
accepts nearly any byte sequence so it would silently mangle UTF-8 rather than
fail. A UnicodeDecodeError in the last four bytes is the byte-budget truncation
splitting a character, not evidence of another encoding, so it is repaired
instead of re-decoding the whole buffer under a DBCS page.

Adds tests/unit/test_acli_skill_render.py: the 13 bundled skill files ship only
in this wheel, so they are the real corpus that broke and the one piece of
coverage with dashscope-only value. Verified by restoring the pre-fix
render_skill — exactly those 6 skills fail, 19 tests — then restoring the fix.
The upstream coverage for the other three fixes stays in agenticCLI, which is
where those test modules live.

dashscope unit: 1011 passed, 7 skipped. pre-commit --all-files green, mypy
included. dashscope/acli/examples/ is untouched by the sync.
pytest fixtures are injected by name, so every test taking key_store or
bundled_skills redefines it from module scope, and the ones that need only
the fixture's side effects never read the parameter. Both are the repo's
existing convention for test modules; the assertions are unchanged apart
from `calls == []` becoming `not calls`.

pre-commit run --all-files only started reporting these once the files were
tracked — it lints tracked files, so the untracked originals were skipped.
…ples

sync_acli.sh deliberately preserves dashscope/acli/examples/, so the bundled
example config drifts from its upstream repo in agenticCLI-examples and has
to be reconciled by hand. Both files had moved independently:

- agent.md: upstream gained the 2026-09 error-code pass (request_too_large,
  permission_error, billing_error, timeout_error, and the note to match on the
  exception class rather than .code). Take the upstream version.
- system-prompt.md: Bailian docs moved to
  docs.modelstudio.console.alibabacloud.com. Point the product overview at the
  new domain and add the API-key, app-id/workspace-id and knowledge-base
  entries, whose titles were verified against the live pages. The remaining
  help.aliyun.com links are left alone rather than guessed at.

The two trees are now byte-identical.
When the agent is invoked without a terminal and nothing is piped in, the
error told the user to rerun as `dashscope "your question"`. That stopped
working in de16112, which routes everything except a bare `dashscope` and an
explicit `expert` keyword to typer. So the one message that could have
recovered the command line sent the user straight into
`No such command 'your question'`.

Cover the whole non-TTY branch, which had no tests: piped text becomes a
one-shot prompt, an empty pipe exits 2 with the corrected hint, and an
unreadable stdin is refused the same way rather than crashing. The hint
assertions name both the corrected form and the stale one, so a revert shows
up as a failure instead of passing quietly.
write_file refused any path outside the cwd, which made it stricter than every
layer around it without protecting anything: run_command can already write to
temp because the shell is not path-guarded, delete_file reaches anywhere behind
its own prompt, and the opt-in OS sandbox confines writes to "the workspace and
temp areas". The refusal also cost more than a failed call — models reach for
/tmp for scratch files by instinct, so the retry put the scratch file in the
user's repo instead, which is the worse outcome.

Writes now land in the cwd or the realpath'd system temp dir. Both sides are
resolved with realpath, so a symlink inside either root still cannot escape,
and validate_path runs first so SENSITIVE_NAMES keeps blocking e.g.
/tmp/.ssh/config. The refusal message names both roots generically rather than
echoing absolute paths.

Per-file sync only: the two files are byte-identical to agenticCLI modulo the
acli. -> dashscope.acli. import rewrite. examples/ was preserved by the sync
script, and no version is bumped -- agenticCLI stays at 0.6.7.
The Response Strategy offered three question types — issue analysis, API
question, example generation — and the Workflow said to classify into one of
them. A question about the assistant matches none of the three, so it fell
through to the only behaviour left: probe the environment until an answer
turns up.

Observed on a real session: "你现在是什么模型" and "你能干嘛" together burned four
run_command calls, two of which failed on bash syntax under PowerShell, before
producing an answer that was already sitting in the prompt on the Current
model line. Nothing about the assistant is discoverable by running a command.

Add the fourth branch and make the Workflow route to it first, rather than
relying on the anti-pattern list to be re-read: the model was never
classifying these as "explore unrelated files to get oriented", so that line
never applied.

Vendored from agenticCLI-examples 77c5284; the two trees are byte-identical.
Two changes, both from the same reported transcript:

3f40870 — tool output is now redacted on the way into context. save_session
already ran sanitize over the persisted history, so the session file was the
only thing protected: a key read out of the environment was scrubbed on disk
and handed to the model in full, where it stayed in the prompt prefix for the
rest of the turn. It runs before truncate_head_tail rather than after, since
a character-budget cut can split a token in half and leave a fragment too
short for the sk- pattern to match. tool_result_for_display is untouched —
that path renders to the user's own terminal.

39d0781 — one reminder per turn after ACLI_SILENT_TOOL_NUDGE (default 6, 0
disables) consecutive tool calls stream no prose. The turn shape the transcript
had has no other backstop: StagnationTracker's read-only classifier counts
run_command as mutating and so resets its streak, its repeat counters need
byte-identical calls, ReflectionTracker needs failures, and the convergence
section is oneshot-only. Fires in interactive mode too, which is where the
user is watching a spinner and getting nothing.

The agenticCLI unit tests covering both are not vendored by sync_acli.sh, so
this repo ships them without coverage; verified against agenticCLI's 2728
passing tests. dashscope suite: 1079 passed, 42 skipped.
@lzsweb
lzsweb merged commit 2cd356a into main Sep 24, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant