Skip to content

feat(qwen): add Qwen Code (qwen_cli) provider (#376) - #412

Open
SparkyWen wants to merge 5 commits into
awslabs:mainfrom
SparkyWen:feat/qwen-cli-provider
Open

feat(qwen): add Qwen Code (qwen_cli) provider (#376)#412
SparkyWen wants to merge 5 commits into
awslabs:mainfrom
SparkyWen:feat/qwen-cli-provider

Conversation

@SparkyWen

Copy link
Copy Markdown
Contributor

Qwen Code (qwen_cli) provider

Adds the Qwen Code (qwen_cli) provider so CAO can drive qwen as 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 — launches qwen --approval-mode yolo with 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).
  • Auth is user-managed: OpenAI-compatible / DashScope credentials (~/.qwen/.env or ~/.qwen/settings.json) or qwen-oauth.
  • Registered in the provider factory, --provider CLI choices, cross-provider resolution, and the README provider table.

Native send_message tool collision (fixed)

qwen-code ships a native send_message tool (its team / background-task messaging feature) whose bare name shadows cao-mcp-server's send_message, which qwen surfaces under the prefixed name mcp__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 providedassign / handoff callbacks from a qwen worker never routed back to the supervisor.

Launch now passes --exclude-tools send_message, dropping the colliding native tool so mcp__cao-mcp-server__send_message is 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 assign result 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, and test_build_command_excludes_native_send_message.

Docs

  • docs/qwen-cli.md (provider guide), design doc, README provider table + valid --provider values.

Note on branch contents

The branch also carries a small related fix, fix(config): tolerant env_bool() (so CAO_PYTE_STATUS=1 reliably enables pyte), and the qwen docs commits. Happy to split any of these out if preferred.

SparkyWen and others added 5 commits July 9, 2026 14:40
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 anilkmr-a2z left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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", []),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 1

Or, 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)"
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@codecov-commenter

codecov-commenter commented Jul 10, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
⚠️ Please upload report for BASE (main@29f175c). Learn more about missing BASE report.

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #412   +/-   ##
=======================================
  Coverage        ?   88.08%           
=======================================
  Files           ?      124           
  Lines           ?    15865           
  Branches        ?        0           
=======================================
  Hits            ?    13974           
  Misses          ?     1891           
  Partials        ?        0           
Flag Coverage Δ
unittests 88.08% <100.00%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds Qwen Code (qwen_cli) as a first-class provider so CLI Agent Orchestrator can launch and orchestrate qwen terminals (supervisor/worker) with status detection, response extraction, MCP wiring, and documentation/testing support. It also introduces a more tolerant boolean env-var parser to prevent configuration “truthy” footguns (notably for pyte-based status detection needed by Gemini-CLI-derived TUIs).

Changes:

  • Added QwenCliProvider with footer/screen-based status detection, response extraction, per-terminal --mcp-config generation, startup-dialog dismissal, and send_message native-tool collision mitigation.
  • Introduced env_bool() and migrated key boolean env flags to it (including CAO_PYTE_STATUS), with regression tests.
  • Added unit + e2e coverage, fixtures, documentation, changelog entry, and a dedicated GitHub Actions workflow for the provider.

Reviewed changes

Copilot reviewed 31 out of 31 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
src/cli_agent_orchestrator/providers/qwen_cli.py New Qwen provider implementation (launch, status detection, extraction, MCP temp config, cleanup).
src/cli_agent_orchestrator/models/provider.py Adds ProviderType.QWEN_CLI.
src/cli_agent_orchestrator/providers/manager.py Wires qwen_cli into provider factory.
src/cli_agent_orchestrator/services/terminal_service.py Adds qwen_cli to skill-prompt + soft-enforcement provider sets.
src/cli_agent_orchestrator/utils/tool_mapping.py Adds Qwen tool vocabulary mapping for launch-time summaries.
src/cli_agent_orchestrator/cli/commands/launch.py Allows qwen_cli in workspace-access-required providers.
src/cli_agent_orchestrator/api/main.py Exposes qwen_cli provider binary mapping (qwen) in provider listing.
src/cli_agent_orchestrator/constants.py Adds env_bool() and uses it for CAO_PYTE_STATUS/EAGER_INBOX_DELIVERY.
src/cli_agent_orchestrator/mcp_server/server.py Uses env_bool() for MCP server feature flags.
README.md Documents Qwen Code in provider list/table and valid --provider values.
docs/qwen-cli.md New provider guide (setup/auth, status detection, MCP, limitations).
docs/issues/376-qwen-cli-provider/design.md Design doc for issue #376 implementation.
CHANGELOG.md Adds Qwen provider entry.
test/providers/test_qwen_cli_unit.py New unit tests for command building, status detection, extraction, MCP temp config, init/startup handling.
test/providers/fixtures/qwen_cli_idle.txt Captured TUI fixture for IDLE detection.
test/providers/fixtures/qwen_cli_processing.txt Captured TUI fixture for PROCESSING detection.
test/providers/fixtures/qwen_cli_completed.txt Captured TUI fixture for COMPLETED detection.
test/providers/fixtures/qwen_cli_error.txt Captured TUI fixture for retryable error-turn handling.
test/providers/fixtures/qwen_cli_response.txt Captured TUI fixture for multiline response extraction.
test/providers/fixtures/qwen_cli_waiting.txt Captured TUI fixture for WAITING_USER_ANSWER detection.
test/providers/test_provider_manager_unit.py ProviderManager unit test for qwen_cli creation/mapping.
test/test_constants.py Adds env_bool regression tests and CAO_PYTE_STATUS parsing tests.
test/api/test_api_endpoints.py Updates provider list expectations to include qwen_cli.
test/e2e/conftest.py Adds require_qwen_cli skip fixture.
test/e2e/test_assign.py Adds Qwen e2e assign coverage.
test/e2e/test_handoff.py Adds Qwen e2e handoff coverage.
test/e2e/test_send_message.py Adds Qwen e2e send_message coverage.
test/e2e/test_supervisor_orchestration.py Adds Qwen e2e supervisor orchestration coverage.
test/e2e/test_skills.py Adds Qwen e2e skill injection coverage.
test/e2e/test_allowed_tools.py Adds Qwen allowed-tools e2e coverage (with soft-enforcement xfail where appropriate).
.github/workflows/test-qwen-cli-provider.yml New CI workflow to run Qwen provider unit tests + linting.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +260 to +271
entry = {
"command": cfg.get("command", ""),
"args": cfg.get("args", []),
}
env = dict(cfg.get("env", {}))
env["CAO_TERMINAL_ID"] = self.terminal_id
entry["env"] = env
servers[server_name] = entry

fd, path_str = tempfile.mkstemp(prefix="cao_qwen_mcp_", suffix=".json")
with os.fdopen(fd, "w") as f:
json.dump({"mcpServers": servers}, f, indent=2)
@haofeif

haofeif commented Jul 11, 2026

Copy link
Copy Markdown
Collaborator

@SparkyWen thanks for the PR. It looks great to me, while i have two questions:

(1) would you pls check whether any documents needs to be updated in examples/ folder ?
(2) Just to confirm that you did test examples/assign end to end and it is working as expected ?

@gutosantos82 gutosantos82 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Review: #412 — feat(qwen): add Qwen Code (qwen_cli) provider (#376)

Summary

Adds a qwen_cli provider (619-line qwen_cli.py + full wiring, docs, and 132 passing unit tests) so CAO can drive qwen --approval-mode yolo as supervisor or worker, including cross-provider orchestration. The implementation is high quality — security-conscious command construction, 100% line coverage on the provider, black/isort/mypy clean, and registration correctly wired everywhere the PR body claims. However, two correctness issues sit on the critical orchestration path (response extraction leaks tool-call chrome; over-broad WAITING patterns can pin a terminal), and there are two concrete completeness gaps (examples/ provider list, CHANGELOG). Recommend Request changes — the mechanics are solid but the callback-corruption risk should be resolved before merge.

Blocking (must fix before merge)

  • [correctness] providers/qwen_cli.py:570-590 (_is_chrome / extract_last_message_from_script) 🆕 — Tool-call bullet lines leak into the extracted response. In qwen the glyph marks both assistant prose and tool-call/picker lines (the waiting fixture shows ● Shell run_shell_command(ls -la) and ● 1. Yes, allow once). _is_chrome() filters separators/footers/tips/spinner/banner but has no tool-call pattern, and the extractor strips the and keeps the text. The sibling antigravity_cli.py:145 uses _TOOL_CALL_PATTERN = r"^\s*●" to drop every bullet line as chrome — qwen deliberately diverges to keep bullet prose, but that means for any tool-using turn (the common orchestration case) the "response" returned to a handoff/assign supervisor is prepended with Shell run_shell_command(...) chrome, and a turn that only ran tools then replied via send_message extracts pure garbage. Why it matters: assign/handoff callbacks — the entire point of this PR — deliver corrupted content. Fix: add a qwen tool-call line pattern (the ● <ToolName> <tool>(...) shape) to _is_chrome() so tool lines are dropped while genuine ● <prose> bullets are kept; add a fixture/test for a completed tool-using turn. Path-weighted to blocking (providers/, core callback path). Confirmed against the code; the exact end-of-turn rendering wasn't reproduced against a live binary — repro: capture a qwen turn that runs a shell tool then answers, feed the frame to extract_last_message_from_script.

Important (should fix)

  • [correctness] providers/qwen_cli.py:119-125, 457, 506 (WAITING_USER_ANSWER_PATTERN) 🆕 — Over-broad WAITING patterns can false-positive on legitimate response prose and hang orchestration. Apply this change and Do you want to proceed are phrases a coding agent routinely emits in a normal answer. WAITING is checked first (before COMPLETED) against the whole 2048-char tail (raw path) / bottom-15 rows (pyte path), which includes the response body; because blocks_orchestrated_input_while_waiting_user_answer=True, a benign completed turn matching one of these pins the terminal until timeout with no real dialog to clear it. Fix: gate the WAITING check on the ready-input-box footer being absent (a real qwen dialog replaces the Type your message or @ box), and/or narrow to picker structure ([y/n], numbered N. Yes, allow …) matched only in the bottom-most rows. Path-weighted (providers/, core path).
  • [security] providers/qwen_cli.py + terminal_service.py (--approval-mode yolo + SOFT_ENFORCEMENT) — A tool-restricted qwen profile (e.g. read-only reviewer) auto-approves every tool call and can still run arbitrary run_shell_command; the only barrier is the advisory appended SECURITY_PROMPT. Restrictions are not actually enforced. This matches the established sibling pattern (kimi_cli/codex/antigravity) and is documented (docs/qwen-cli.md "Tool Restrictions" + xfail e2e test_restricted_supervisor_cannot_bash), so it's a conscious widening, not a regression — flagging so the choice is deliberate.
  • [consistency] examples/cross-provider/README.md:149 🆕 — The "Valid provider values" list (kimi_cli, copilot_cli, opencode_cli, cursor_cli, …) is missing qwen_cli — the exact parallel enumeration the PR updated in the root README and cao launch --provider. This directly answers maintainer @haofeif's "does examples/ need updating?" — yes. (The list also pre-existingly omits hermes; add both.) Fix: append qwen_cli (and hermes).
  • [conventions] CHANGELOG.md 🆕 — The PR also fixes env parsing (env_bool() helper) so CAO_PYTE_STATUS=1/yes/on now correctly enables pyte (previously silently parsed False, affecting all screen-detection providers). That's a user-facing behavior change with only an "Added" qwen entry and no ### Fixed entry. Fix: add a ### Fixed entry for the truthy/falsy env-parsing correction.
  • [tests] test/providers/test_qwen_cli_unit.py — FOOTER_TAIL_WINDOW eviction untested — The 2 KB tail window's stated motivation ("avoid flipping to IDLE mid-response when a long answer scrolls the old spinner out") has no test. Fix: buffer = spinner + "x"*3000 + idle_footer; assert get_status == COMPLETED after a turn.

Nits (optional)

  • [tests] test/providers/test_qwen_cli_unit.py:319assert "--model" in cmd and "qwen3-coder-plus" in cmd — two independent substring checks that pass even if flag/value are non-adjacent. Use the adjacency form assert "--model qwen3-coder-plus" in cmd (as line 629 does). (This is the assertion-imprecision nit @anilkmr-a2z already raised — see Prior feedback.)
  • [correctness] providers/qwen_cli.py:1034-1063 (_handle_startup_dialog)STARTUP_DIALOG_PATTERN also matches an auth picker (Sign in with / How would you like to … authenticate); blindly pressing Enter accepts the default auth method, which under misconfigured creds could select browser-only qwen-oauth and hang init. Consider matching only theme/trust dialogs and letting auth fail fast.
  • [security/conventions] providers/qwen_cli.py:270 — MCP config written via os.fdopen(fd, "w") with platform-default encoding; prefer encoding="utf-8" per repo checklist (content is ASCII JSON, near-zero impact).
  • [security] providers/qwen_cli.py:269 (temp-file leak) — the mkstemp --mcp-config file leaks in /tmp if initialize() raises after writing it and cleanup() isn't called. 0600, no secrets (only CAO_TERMINAL_ID + server command) — housekeeping. Wrap init in try/finally.
  • [consistency] docs/issues/376-qwen-cli-provider/design.md §5 — Design doc drifted from shipped code: ERROR_PATTERN, paste_enter_count, get_status signature, and IDLE_FOOTER_PATTERN all differ from the implementation (the code is correct and cross-provider consistent; the doc is stale). Add a "final impl differs" note.
  • [consistency] constants.py:122 — CAO_PYTE_STATUS comment lists screen-detection providers but omits opencode_cli (also sets supports_screen_detection = True). The PR edits this exact list — natural place to complete it.
  • [conventions] skills/cao-session-management/SKILL.md:50 — provider enumeration not updated with qwen_cli (also omits hermes; illustrative, not authoritative).
  • [tests] test_qwen_cli_unit.pyPROCESSING_SPINNER_PATTERN branch never independently covered (footer substring always matches first); _handle_startup_dialog timeout-exhaustion path untested; _write_mcp_config empty-server-config edge untested.
  • [conventions/consistency] scope — the env_bool refactor touches shared files (constants.py, mcp_server/server.py: EAGER_INBOX_DELIVERY, ENABLE_WORKING_DIRECTORY, ENABLE_SENDER_ID_INJECTION) beyond the provider. Author already offered to split; a separate fix: PR/commit is preferable per CONTRIBUTING.

Prior feedback (already raised — not restating)

  • ↩︎ Test assertion imprecision at test_qwen_cli_unit.py:319 — already raised by @anilkmr-a2z (nit); we concur (kept as a nit above with the concrete fix, credited).
  • ↩︎ "2 must-fix correctness findings"@anilkmr-a2z's review summary cites two must-fix correctness items whose specifics weren't in the fetched comment body; our two correctness findings above (tool-call leak, over-broad WAITING) likely correspond. Weigh toward Request changes regardless of overlap.
  • ↩︎ "Does examples/ need updating?" — asked by @haofeif; answered: yes, examples/cross-provider/README.md:149 is missing qwen_cli (surfaced net-new above).
  • ↩︎ "Did you test examples/assign end-to-end?" — asked by @haofeif; not independently verifiable here (needs a live qwen CLI + auth) — see Verification.

Tests

Coverage is strong. 48 unit cases + 6 realistic captured-TUI ANSI fixtures (idle/processing/completed/error/response/waiting), registration test, env_bool/CAO_PYTE_STATUS regression, and qwen_cli parametrized across all 6 e2e suites (assign/handoff/send_message/skills/allowed_tools/supervisor_orchestration) behind a require_qwen_cli skip fixture. All hard areas covered: status detection on both raw + pyte paths, IDLE-vs-COMPLETED turn split, transient-API-error-is-COMPLETED, extraction failures, ANSI/bullet stripping, model precedence, security-prompt gating, MCP config for dict + pydantic forms, temp-file cleanup idempotency, full async initialize() paths. Gaps: the FOOTER_TAIL_WINDOW eviction path and (critically) no test for a completed tool-using turn, which is exactly where the Blocking extraction bug hides.

Verification

Ran in a clean uv/python3.12 container (uv sync succeeded — numpy built fine in-container):

  • Baseline: uv run pytest test/providers/test_qwen_cli_unit.py test/test_constants.py test/providers/test_provider_manager_unit.py -q132 passed, 0 failed (2.11s); qwen_cli.py reported 100% line coverage.
  • ✓ VERIFIED --exclude-tools send_message is passed — _build_qwen_command() returned exactly qwen --approval-mode yolo --exclude-tools send_message; test_build_command_excludes_native_send_message passes.
  • ✓ VERIFIED provider registered & resolvable — ProviderType.QWEN_CLI.value == 'qwen_cli', in PROVIDERS, manager dispatches to QwenCliProvider.
  • ✓ VERIFIED qwen_cli selectable via --provider — validated at runtime via PROVIDERS membership (not click.Choice), and listed in help/error output.
  • ✓ VERIFIED status detection on all 6 fixtures (idle→IDLE, processing→PROCESSING, completed/error→COMPLETED, waiting→WAITING_USER_ANSWER, response→IDLE + correct extraction).
  • ✓ VERIFIED black --check / isort --check / mypy all clean on qwen_cli.py.
  • ⁇ NOT VERIFIED — live end-to-end orchestration (real qwen session reaching IDLE, MCP send_message callback routing back to supervisor, startup-dialog dismissal against a real TUI). Needs a live qwen binary + auth (not installed). Manual: npm install -g @qwen-code/qwen-code, launch a CAO session with --provider qwen_cli, confirm assign/handoff callbacks route via mcp__cao-mcp-server__send_message. This is also @haofeif's examples/assign end-to-end question — recommend the author confirm it, especially given the tool-call extraction concern above.

All runnable PR claims hold; no diff↔description contradictions in what could be exercised statically/dynamically.

Verdict

Request changes — excellent implementation, fully wired and tested, but the response-extraction tool-call leak and the over-broad WAITING patterns both sit on the core assign/handoff callback path this PR exists to enable, and examples/ + CHANGELOG completeness gaps remain. Resolve the extraction bug (with a tool-turn fixture), tighten WAITING gating, add the two enumerations/entry, and this is a clean approve. --approval-mode yolo soft-enforcement is acceptable as a documented, sibling-consistent choice.

@haofeif haofeif added the feature New feature or capability label Jul 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature New feature or capability

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feat] Qwen Code (qwen) provider adapter

6 participants