// Core delegation logic: spawn the qwen-code CLI non-interactively and capture its reply. // Kept separate from the MCP server wiring (qwen-delegate-server.ts) so it's unit-testable // without a live MCP connection or a live qwen endpoint. import { spawn, type ChildProcess } from "node:child_process"; export interface DelegateResult { ok: boolean; output: string; // stdout (ok) or combined error detail (!ok) } export interface DelegateOptions { /** Milliseconds before giving up. qwen-code has been observed taking 3-6 min for a trivial, * takes time to warm up on the first request, * prompt against a local model proxy, so default generous — see research/qwen-mcp-delegation.md. */ timeoutMs?: number; /** Injectable for tests. Defaults to node:child_process's spawn. */ spawnFn?: typeof spawn; } const DEFAULT_TIMEOUT_MS = 10 * 60 * 1000; // 10 min /** * Runs `qwen -p ""` non-interactively and returns its trimmed stdout. * qwen-code prints startup warnings (e.g. failed MCP sub-servers) to stderr; those are * ignored on success and surfaced only when the run itself fails. */ export function delegateToQwen ( prompt: string, options: DelegateOptions = {}, ): Promise { const { timeoutMs = DEFAULT_TIMEOUT_MS, spawnFn = spawn } = options; return new Promise((resolve) => { const child: ChildProcess = spawnFn("qwen", ["-p", prompt], { shell: true, // qwen.cmd on Windows needs a shell to resolve stdio: ["ignore", "pipe", "pipe"], }); let stdout = ""; let stderr = ""; let settled = false; const timer = setTimeout(() => { if (settled) return; settled = true; child.kill(); resolve({ ok: false, output: `qwen timed out after ${timeoutMs}ms` }); }, timeoutMs); child.stdout?.on("data", (chunk) => (stdout += chunk)); child.stderr?.on("data", (chunk) => (stderr += chunk)); child.on("error", (err) => { if (settled) return; settled = true; clearTimeout(timer); resolve({ ok: false, output: `failed to start qwen: ${err.message}` }); }); child.on("close", (code) => { if (settled) return; settled = true; clearTimeout(timer); if (code === 0) { resolve({ ok: true, output: stdout.trim() }); } else { const detail = stderr.trim() || stdout.trim() || "(no output)"; resolve({ ok: false, output: `qwen exited with code ${code}: ${detail}` }); } }); }); }