Files
delegate-ai-mcp/src/qwen-delegate.ts
T
haylanandClaude-Bot 061a1f381d Rewrite README to match locked TS conventions; minor tool/doc tweaks
Reflects the resolved wayfinder map (issue #1): no build process ever,
separate MCP server process + registration per backend, no lint/format
tooling. Also includes small wording tweaks to the qwen_delegate tool
description and timeout comment made outside this session.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CRnb5Gqdu7gVTQrwAqFdfJ
2026-09-06 18:12:42 +02:00

74 lines
2.4 KiB
TypeScript

// 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 "<prompt>"` 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<DelegateResult> {
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}` });
}
});
});
}