feat(byoa): run agents on Qwen Code - #113
Conversation
The guard anchored on `RUNNABLE_ENGINE_IDS = new Set([...])`. yetone#116 collapses that duplicate by deriving the set from an ordered `RUNNABLE_ENGINES` tuple, and the guard then reported "anchor not found" — correctly, by its own design, but it would have made this the thing blocking a change that removes the very duplication it exists to police. It now reads whichever shape is present. Verified against all three open branches: main's Set literal, yetone#116's tuple, and yetone#113's added engine.
|
Thanks for this — the adapter work and the 314-line test file are solid, and all six checks were green when you pushed. I can't merge it as-is: it now conflicts with main in four files ( The good news is that a rebase should make this PR noticeably smaller, because two things landed since you opened it that were built for exactly this problem. #116 collapsed the engine lists to one declaration. #114 added a CI guard that will tell you if you miss one.
You already touched One note on Rebase onto main, let the guard run, and ping me — happy to take another look. |
Finishes yetone#75's list. `qwen` was already in ENGINE_LABEL / ENGINE_BIN and ENGINE_VERSION_SPECS, so the Computers tab could report its version and Cumora still could not wake it. The adapter is thin for a reason worth stating: Qwen Code is a Gemini CLI fork and shares its flags (-o stream-json, -p, -r, -m), but it does NOT share Gemini's output. It emits Claude Code's envelope — {type:'assistant',session_id,message:{model,content,usage}} closing on {type:'result',subtype,is_error,num_turns,usage} — which is exactly the shape spawnEngine already sniffs. So the wake path needs no parser of its own, and gets session id, terminating usage, real model and one ledger hop per assistant message for free. An adapter derived from the Gemini one would have parsed nothing. That is also why triage needs the only new code here: `-o json` returns those same events wrapped in a JSON array rather than Claude's {result,usage} object, so there is no envelope to unwrap and the reply has to be assembled from the assistant text blocks. Triage runs --safe-mode, qwen's own switch for "ignore every customization": context files, hooks, extensions, skills and MCP servers stay unloaded. It leaves auth alone, and --yolo is an argv flag that still wins over it, so nothing can stall waiting for an approval nobody is there to give. Every claim above was checked against a real @qwen-code/qwen-code 0.22.3, including that `-y` genuinely sets approval mode (its help dropped the entry in the subcommand restructure, but the headless yolo warning fires only when getApprovalMode() === 'yolo', and it fires). Rebased onto the BYOA sandboxing work. Qwen lands in the compatibility tier, and needs no code to put it there: SANDBOXED_ENGINE_IDS is ['claude','codex'], so runnableEngineIds() already excludes qwen unless CUMORA_BYOA_ALLOW_UNSANDBOXED=1 is set. Verified — default resolves to claude, codex; with the opt-in, claude, codex, gemini, qwen. The docs table gains a Qwen column in that vocabulary, and the agent-cli prose is left as main rewrote it, since it deliberately stopped enumerating engines. byoa-qwen goes in the shared BYOA_SOURCES whitelist: normalizeByoaSource falls back to 'byoa-claude' for anything unknown, so omitting it would have attributed every qwen run's spend to Claude rather than failing loudly. The PAIRABLE record from yetone#116 caught the same class of omission at compile time during this rebase, which is what it was added for. Also refreshes a redetect fixture that used 'qwen' as its example of an engine with no adapter — true until this commit.
e1effbb to
3ef0405
Compare
Finishes the list in #75.
qwenwas already inENGINE_LABEL/ENGINE_BINandENGINE_VERSION_SPECS, so the Computers tab could report its version — and Cumora still could not wake it.The one thing worth knowing about this fork
Qwen Code is a Gemini CLI fork and shares its flags —
-o stream-json,-p,-r,-m. It does not share Gemini's output. Real bytes from@qwen-code/qwen-code@0.22.3:{"type":"result","subtype":"error_during_execution","uuid":"…","session_id":"…","is_error":true,"duration_ms":0,"num_turns":0,"usage":{"input_tokens":0,"output_tokens":0},"permission_denials":[],"error":{"message":"No auth type is selected…"}}That is Claude Code's envelope, not Gemini's
{type,timestamp,status,stats}. An adapter derived from #99 would have parsed nothing at all.It is also why this adapter is thin.
spawnEnginealready sniffs exactly that shape, so the wake path needs no parser of its own and gets the session id, the terminating usage, the real model, and one ledger hop per assistant message for free.What did need new code
Triage.
-o jsonis not Claude's{result,usage}object here — it is the same stream events wrapped in a JSON array — so there is no envelope to unwrap and the reply has to be assembled from the assistanttextblocks.qwenReplyFromStream()does that and is exported and unit-tested directly (pure, and the one part a fake binary can't exercise throughspawnEngine).Triage runs
--safe-mode, which is qwen's own switch for "ignore every customization": context files, hooks, extensions, skills and MCP servers stay unloaded — the same thing--strict-mcp-configbuys the Claude triage path, and it keeps a cold triage spawn cheap. Checked in the shipped bundle that it gates only tools/permissions/context/hooks and notsecurity.auth, so it cannot break an operator's credentials, and that--yolo(an argv flag) still wins over it, so nothing stalls on an approval nobody is there to give.Correcting myself from #99
I said in #99 that qwen accepts
-ywithout documenting it, and that building unattended behaviour on that would be unsafe. Half right: 0.22.3's help really has no-yentry — it was dropped in the subcommand restructure — but the flag is not merely accepted. From the shipped bundle:The headless yolo warning fires only when approval mode is actually yolo, and running
qwen -o stream-json -yprints it. So-ygenuinely takes effect; it is a working flag with a help regression, not undocumented behaviour. That was worth checking rather than leaving as a hunch.Shape
qwen --output-format stream-json --yolo [--resume <id>] [--model X], prompt on stdin--resume <id>. Deliberately not-c/--continue: that resumes the newest session for the project, and one machine runs many agents out of many homesQWEN.md+.qwen/skills/--safe-mode --yolo [--model $CUMORA_TRIAGE_MODEL]CUMORA_QWEN_ARGS, which keeps--resumeand the stdin promptThe big-brain doctor probe passes no
--modelat all. Qwen Code is multi-provider (its own OAuth, DashScope, any OpenAI-compatible base URL), so there is no cheap model id that is right for everyone — pinning one would report on a model the operator's wakes never use. A test caught me getting this wrong the first time: myask()always appended--model, so the big probe was silently running on the cheap triage model.One thing that would have failed silently
byoa-qwengoes into the sharedBYOA_SOURCESwhitelist.normalizeByoaSource()falls back to'byoa-claude'for anything unknown, so omitting it would not have errored — it would have attributed every qwen run's spend to Claude in the ledger.Drive-by
cli-version.tstold the next person to keep the catalog in sync withelectron/main.cjs LOCAL_CLIS, which no longer exists — the catalog moved into that file itself. Misdirecting the list of things-to-update is how one of two copies gets fixed and the other doesn't, which is precisely what caused #102. One comment, corrected to the lists that do exist.Checks
16 tests in
agents-computer-engine-qwen.test.tsdriving a fakeqwenon PATH that replays the captured envelope: turn parsing with no adapter-side parser, per-assistant-message hops, resume-by-id (and not--continue), the no-auth failure, text assembly,tool_useblocks contributing no text, a failed result with text already streamed, banner noise and truncated lines,--safe-modetriage,CUMORA_TRIAGE_MODELreaching both triage and the small probe, the big probe leaving the model alone, seedHome layout, the args override, and detection.76/76 across the engine and guard test files locally; typecheck,
biome lint ., and both guards clean.