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
74 lines
2.4 KiB
TypeScript
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}` });
|
|
}
|
|
});
|
|
});
|
|
}
|