feat(qwen): add Qwen Code (qwen_cli) provider (#376)#412
Conversation
Add Qwen Code (`qwen`) — Alibaba's Gemini-CLI-derived Ink TUI coding agent — as a first-class CAO provider, modeled on the sibling antigravity_cli provider. - QwenCliProvider: launches `qwen --approval-mode yolo` with `--append-system-prompt` role injection, `--model`, and a per-terminal `--mcp-config` file carrying CAO_TERMINAL_ID; footer-anchored status detection with pyte stale-footer resolution (get_status_from_screen). - wiring: ProviderType enum, manager branch, launch workspace set, terminal_service (runtime skill prompt + soft enforcement), tool_mapping (gemini-style native names), /agents/providers endpoint. - tests: unit tests at 100% provider coverage against real captured qwen TUI fixtures; TestQwenCli* e2e classes across handoff/assign/ send_message/allowed_tools/supervisor_orchestration/skills; provider manager test; dedicated CI workflow. - docs: docs/qwen-cli.md, README provider table + enumerations, CHANGELOG. Auth is user-managed (OpenAI-compatible env or qwen-oauth); tool restrictions are soft (SECURITY_PROMPT) under yolo. Verified live on a real cao-server (tmux backend): the qwen terminal reaches IDLE, launches with the correct command + per-terminal MCP config, transitions idle->processing->completed, its cao-mcp-server connects with the matching CAO_TERMINAL_ID, and a cross-provider inbox message from a claude_code terminal is delivered and submitted into the qwen worker. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ot silently off) Boolean env flags were parsed as `os.environ.get(...).lower() == "true"`, so the common truthy spellings `1` / `yes` / `on` evaluated to False. For CAO_PYTE_STATUS that silently DISABLED pyte screen-detection — which qwen_cli and antigravity_cli require (their raw pipe-pane retains a stale "esc to cancel" → false PROCESSING) — so a finished qwen turn was never detected COMPLETED and the blocking handoff/assign hung the full timeout. - add env_bool(name, default) in constants.py: accepts 1/true/yes/on and 0/false/no/off (case- and whitespace-insensitive); unset, empty, or unrecognized values fall back to the default rather than flipping the flag. - route all four boolean env sites through it: CAO_PYTE_STATUS, CAO_EAGER_INBOX_DELIVERY (constants.py), CAO_ENABLE_WORKING_DIRECTORY, CAO_ENABLE_SENDER_ID_INJECTION (mcp_server/server.py). - correct the CAO_PYTE_STATUS comment to list all opt-in providers (claude_code, kimi_cli, qwen_cli, antigravity_cli). TDD: 24 new tests in test/test_constants.py::TestEnvBool cover the truthy/falsy spellings, default fallback, and a CAO_PYTE_STATUS=1 regression. Full suite: 3841 passed / 21 skipped; black + isort clean. Verified live: a claude_code->qwen_cli handoff that hung on CAO_PYTE_STATUS=1 returns the worker's result in ~9s once pyte is actually enabled. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ints Reframe qwen auth around the simplest path: configure qwen once (like any standalone qwen user) and every CAO-spawned worker inherits it — no export before cao-server, no --env. qwen loads ~/.qwen/.env natively (bundled dotenv), and an already-set process env var still wins, so the existing --env / export flows remain as overrides. - primary path: a one-time ~/.qwen/.env with just OPENAI_API_KEY (+ base URL); model defaults via the profile `model:` field, so the only secret a user plugs in is the API key. - region tables: shared DashScope (China / International-Singapore) and the Model Studio workspace-gateway region codes (cn-beijing, ap-southeast-1, us-east-1, eu-central-1, ap-northeast-1). - fix stale guidance that said `CAO_PYTE_STATUS=1` (the value that silently disabled pyte): note pyte is required for qwen_cli, on by default, and the flag now accepts 1/true/yes/on. Verified live: with a credential-free cao-server (no OPENAI_* in env) and no --env, a qwen worker authenticated purely from ~/.qwen/.env and completed a real turn. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…er callbacks (awslabs#376) qwen-code registers a native `send_message` tool (its team / background-task messaging feature). Its bare name collides with cao-mcp-server's `send_message`, which qwen surfaces under the prefixed name `mcp__cao-mcp-server__send_message`. When a CAO worker is told to "send_message" its result back to the supervisor, the model matches the shorter native tool and calls it, failing with "No active team and no task_id provided" — so assign/handoff callbacks from a qwen worker never route back and the supervisor waits forever. Pass `--exclude-tools send_message` on launch so the colliding native tool is dropped, leaving `mcp__cao-mcp-server__send_message` as the only send-message-shaped tool the model can pick. CAO orchestration never uses qwen-code's native team messaging, so nothing is lost. Verified: with the flag, `/mcp` tool listing shows only the MCP send_message, and a qwen worker's assign result now routes back to the supervisor end-to-end. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
anilkmr-a2z
left a comment
There was a problem hiding this comment.
Review summary: Well-structured provider that mirrors the Antigravity CLI sibling closely. 2 must-fix findings (correctness), 1 nit (test assertion imprecision).
| cfg = server_config.model_dump(exclude_none=True) | ||
| entry = { | ||
| "command": cfg.get("command", ""), | ||
| "args": cfg.get("args", []), |
There was a problem hiding this comment.
must-fix (correctness) -- The design doc (section 5) declares paste_enter_count = 1 (Ink single-Enter submit), and the unit test asserts in (1, 2), but the implementation never overrides the BaseProvider default of 2. For an Ink TUI where single-Enter submits after bracketed paste, sending a second Enter injects a stray newline that either starts a new turn (doubling input) or is silently consumed. The Antigravity CLI sibling (same Ink scaffold) also relies on the base default of 2, but the design doc here explicitly documents 1.
Fix: Add a class-level override:
@property
def paste_enter_count(self) -> int:
return 1Or, if live testing confirms double-Enter is correct for qwen, update the design doc to match reality and tighten the test assertion to == 2.
| r"|(?:trust (?:this )?folder|Do you trust the files in this folder)" | ||
| r"|(?:Get started|Sign in with)" | ||
| ) | ||
|
|
There was a problem hiding this comment.
must-fix (correctness) -- _handle_startup_dialog is synchronous (time.sleep in a while loop), but it is called from async initialize(). On the default asyncio event loop this blocks the entire event loop for up to startup_prompt_handler_timeout seconds (server setting, typically 30-60s). The sibling antigravity_cli has the same pattern (inherited tech debt), so this is not a regression introduced by this PR alone, but worth flagging because a qwen startup that hits the theme picker on first install will freeze the CAO server's request handling for up to 30s.
For parity with the sibling this is acceptable as-is, but please leave a # TODO: convert to async polling (same tech debt as antigravity_cli) comment so the next contributor knows the intent.
|
|
||
|
|
||
| def test_paste_enter_count_is_valid(): | ||
| assert make_provider().paste_enter_count in (1, 2) |
There was a problem hiding this comment.
nit (test precision) -- assert make_provider().paste_enter_count in (1, 2) passes for both possible values and therefore validates nothing -- it will never fail regardless of whether the provider overrides the base default. Either assert the exact expected value (== 1 per the design doc, or == 2 if double-Enter is validated), or remove the test if the value is intentionally unspecified.
Qwen Code (
qwen_cli) providerAdds the Qwen Code (
qwen_cli) provider so CAO can driveqwenas a first-class agent — supervisor or worker — including cross-provider orchestration (handoff/assign/send_message).Closes #376.
Provider
src/cli_agent_orchestrator/providers/qwen_cli.py— launchesqwen --approval-mode yolowith a per-terminal--mcp-config,--append-system-prompt(CAO role + skills + security prompt when tool-restricted), model selection, full status detection (PROCESSING / IDLE / COMPLETED / ERROR / WAITING_USER_ANSWER), response extraction, and first-run dialog handling (theme / folder-trust).~/.qwen/.envor~/.qwen/settings.json) orqwen-oauth.--providerCLI choices, cross-provider resolution, and the README provider table.Native
send_messagetool collision (fixed)qwen-code ships a native
send_messagetool (its team / background-task messaging feature) whose bare name shadows cao-mcp-server'ssend_message, which qwen surfaces under the prefixed namemcp__cao-mcp-server__send_message. A worker told to "send_message" its result back matched the native tool →No active team and no task_id provided→assign/handoffcallbacks from a qwen worker never routed back to the supervisor.Launch now passes
--exclude-tools send_message, dropping the colliding native tool somcp__cao-mcp-server__send_messageis the only send-message-shaped tool the model can pick. CAO never uses qwen-code's native team messaging, so nothing is lost. Full analysis in the #376 comment thread.Verified end-to-end: a qwen worker's
assignresult now routes back to the supervisor (supervisor receives[Message from terminal <qwen_id>]and combines it with the other worker's result).Tests
test/providers/test_qwen_cli_unit.py— command building, status detection, response extraction, per-terminal MCP config, andtest_build_command_excludes_native_send_message.Docs
docs/qwen-cli.md(provider guide), design doc, README provider table + valid--providervalues.Note on branch contents
The branch also carries a small related fix,
fix(config): tolerant env_bool()(soCAO_PYTE_STATUS=1reliably enables pyte), and the qwen docs commits. Happy to split any of these out if preferred.