Skip to content

feat(byoa): run agents on Qwen Code - #113

Merged
yetone merged 1 commit into
yetone:mainfrom
WhichPaths:feat/qwen-engine
Aug 31, 2026
Merged

feat(byoa): run agents on Qwen Code#113
yetone merged 1 commit into
yetone:mainfrom
WhichPaths:feat/qwen-engine

Conversation

@WhichPaths

Copy link
Copy Markdown
Collaborator

Finishes the list in #75. 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 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. spawnEngine already 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 json is 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 assistant text blocks. qwenReplyFromStream() does that and is exported and unit-tested directly (pure, and the one part a fake binary can't exercise through spawnEngine).

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-config buys 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 not security.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 -y without documenting it, and that building unattended behaviour on that would be unsafe. Half right: 0.22.3's help really has no -y entry — it was dropped in the subcommand restructure — but the flag is not merely accepted. From the shipped bundle:

function getHeadlessYoloSafetyWarning(config, env) {
  if (config.getApprovalMode() !== "yolo") return null
  
}

The headless yolo warning fires only when approval mode is actually yolo, and running qwen -o stream-json -y prints it. So -y genuinely 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

Run qwen --output-format stream-json --yolo [--resume <id>] [--model X], prompt on stdin
Persistent session none — 0.22.3 has no stream-json input mode
Continuity --resume <id>. Deliberately not -c/--continue: that resumes the newest session for the project, and one machine runs many agents out of many homes
Persona QWEN.md + .qwen/skills/
Triage --safe-mode --yolo [--model $CUMORA_TRIAGE_MODEL]
Override CUMORA_QWEN_ARGS, which keeps --resume and the stdin prompt

The big-brain doctor probe passes no --model at 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: my ask() always appended --model, so the big probe was silently running on the cheap triage model.

One thing that would have failed silently

byoa-qwen goes into the shared BYOA_SOURCES whitelist. 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.ts told the next person to keep the catalog in sync with electron/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.ts driving a fake qwen on 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_use blocks contributing no text, a failed result with text already streamed, banner noise and truncated lines, --safe-mode triage, CUMORA_TRIAGE_MODEL reaching 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.

WhichPaths added a commit to WhichPaths/cumora that referenced this pull request Aug 30, 2026
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.
@yetone

yetone commented Aug 30, 2026

Copy link
Copy Markdown
Owner

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 (registry.ts, router.ts, src/api/client.ts, src/lib/engines.ts). Please rebase.

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. src/lib/engines.ts now derives the ordered UI list, the RunnableEngineId type, and the membership set from a single RUNNABLE_ENGINES array — and qwen is already present in ENGINE_LABEL and ENGINE_BIN as a detect-only entry. So most of your +1/-1 client-side edits collapse into adding 'qwen' to that one array; several of the files you touched no longer need touching at all.

#114 added a CI guard that will tell you if you miss one. scripts/guard-engine-registry.mjs (+ server/src/__tests__/guard-engine-registry.test.ts) walks every list an engine has to appear in and fails naming the ones that are incomplete. Its header comment names the failure mode this PR was most at risk of:

Missing one does not error — BYOA_SOURCES in particular is read through normalizeByoaSource(), which maps anything unknown to 'byoa-claude', so a half-wired engine silently bills its runs to Claude.

You already touched byoa-source.ts and llm-ledger.ts, so you were tracking that — but after the rebase the guard proves it instead of you and I having to check by eye. Run node --import tsx --test server/src/__tests__/guard-engine-registry.test.ts locally and it will enumerate anything still missing.

One note on server/src/agents/computer/cli-version.ts: ENGINE_VERSION_SPECS already carries a qwen entry (@qwen-code/qwen-code), and #117 has since reworked the spawn path in that file for Windows shims — so re-check your +3/-1 there against the new version rather than replaying it.

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.
@yetone
yetone merged commit b01894f into yetone:main Aug 31, 2026
7 checks passed
@yetone yetone mentioned this pull request Aug 31, 2026
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.

2 participants