Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 119 additions & 0 deletions bin/rx-acp-dispatch.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
#!/usr/bin/env node
// rx-acp-dispatch — 通过 Reasonix ACP(Agent Client Protocol)派发规格任务。
//
// 为什么不是 `reasonix run`:run 模式只把工具调用作为文本流式输出,不真正执行,
// 文件永远不会落盘。ACP(stdio NDJSON JSON-RPC)才会真正驱动 write_file / run_command
// 等工具落地改动。
//
// 用法: node rx-acp-dispatch.mjs <project_root> <spec_file> <transcript> [budget] [model]
// 退出码: 0 = end_turn 正常完成;非 0 = 协议错误 / cancelled / 超时。

import { spawn } from "node:child_process";
import { readFileSync } from "node:fs";

const [, , PROJECT_ROOT, SPEC_FILE, TRANSCRIPT, BUDGET, MODEL] = process.argv;

if (!PROJECT_ROOT || !SPEC_FILE || !TRANSCRIPT) {
console.error("用法: rx-acp-dispatch.mjs <project_root> <spec_file> <transcript> [budget] [model]");
process.exit(2);
}

const TASK = readFileSync(SPEC_FILE, "utf-8");

const args = ["acp", "--dir", PROJECT_ROOT, "--transcript", TRANSCRIPT];
if (BUDGET) args.push("--budget", BUDGET);
if (MODEL) args.push("--model", MODEL);

const child = spawn("reasonix", args, {
stdio: ["pipe", "pipe", "inherit"],
shell: process.platform === "win32",
env: process.env,
});

let buf = "";
let nextId = 1;
const pending = new Map();

function send(method, params) {
const id = nextId++;
child.stdin.write(JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n");
return new Promise((res) => pending.set(id, res));
}
function respond(id, result) {
child.stdin.write(JSON.stringify({ jsonrpc: "2.0", id, result }) + "\n");
}

child.stdout.on("data", (chunk) => {
buf += chunk.toString();
let nl;
while ((nl = buf.indexOf("\n")) >= 0) {
const line = buf.slice(0, nl).trim();
buf = buf.slice(nl + 1);
if (!line) continue;
let m;
try { m = JSON.parse(line); } catch { continue; }

// agent → client:工具执行权限请求。editMode=auto/yolo 下 agent 本应自动放行,
// 但这里仍显式批准,保证非交互管道不卡住。
if (m.method === "session/request_permission") {
const opts = m.params?.options || [];
const allow = opts.find((o) => /allow/.test(o.optionId)) || opts[0];
respond(m.id, { outcome: { outcome: "selected", optionId: allow?.optionId || "allow_always" } });
continue;
}
// agent → client:进度通知。工具调用打印到 stderr(不污染 stdout 协议流)。
if (m.method === "session/update") {
const u = m.params?.update;
if (u?.sessionUpdate === "tool_call") {
process.stderr.write(` [工具] ${u.title || u.kind || ""}\n`);
} else if (u?.sessionUpdate === "agent_message_chunk") {
process.stderr.write(u.content?.text || "");
}
continue;
}
// 对本端请求的响应
if (m.id != null && pending.has(m.id)) {
pending.get(m.id)(m);
pending.delete(m.id);
}
}
});

child.on("error", (e) => {
console.error("无法启动 reasonix:", e.message);
process.exit(3);
});

const TIMEOUT_MS = 180000;
const timer = setTimeout(() => {
console.error("\n[超时] ACP 派发超过 180s,强制终止");
child.kill();
process.exit(4);
}, TIMEOUT_MS);

(async () => {
try {
await send("initialize", { protocolVersion: 1, clientCapabilities: {} });

const ns = await send("session/new", { cwd: PROJECT_ROOT, mcpServers: [] });
const sid = ns.result?.sessionId;
if (!sid) {
console.error("session/new 失败:", JSON.stringify(ns.error || ns.result));
clearTimeout(timer); child.kill(); process.exit(5);
}

const pr = await send("session/prompt", {
sessionId: sid,
prompt: [{ type: "text", text: TASK }],
});
const stop = pr.result?.stopReason;
process.stderr.write(`\n[stopReason] ${stop}\n`);

clearTimeout(timer);
child.kill();
process.exit(stop === "end_turn" ? 0 : 1);
} catch (e) {
console.error("ACP 派发异常:", e.message);
clearTimeout(timer); child.kill(); process.exit(6);
}
})();
12 changes: 10 additions & 2 deletions bin/rx-doctor
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ set -uo pipefail
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
PLUGIN_ROOT=$(dirname "$SCRIPT_DIR")
FAIL=0
# python3 在部分 Windows 环境是 Store 占位符:command -v 能找到、但一运行就退 49。
# 因此逐个实跑 --version,取第一个真正可用的解释器。
PYTHON=""
for _py in python3 python; do
if command -v "$_py" >/dev/null 2>&1 && "$_py" --version >/dev/null 2>&1; then
PYTHON="$_py"; break
fi
done

echo "═══ Reasonix 链路体检 ═══"
echo ""
Expand Down Expand Up @@ -51,8 +59,8 @@ echo ""

# [4] task state(当前工作目录)
echo "[4] task state(当前目录)"
if [ -f ".reasonix-tasks/state.json" ]; then
python3 -c "
if [ -f ".reasonix-tasks/state.json" ] && [ -n "$PYTHON" ]; then
"$PYTHON" -c "
import json, sys
try:
with open('.reasonix-tasks/state.json', encoding='utf-8') as f:
Expand Down
29 changes: 24 additions & 5 deletions bin/rx-go
Original file line number Diff line number Diff line change
Expand Up @@ -73,15 +73,34 @@ STDOUT_LOG="$LOGS_DIR/${TIMESTAMP}-${SLUG}.out"
# --- dispatch 信息 ---
echo "rx-go dispatch · 规格: $SPEC_FILE · 预算: \$$BUDGET · preset: $PRESET"

# --- dispatch ---
TASK_CONTENT=$(cat "$SPEC_FILE")
# --- preset → reasonix 模型映射 ---
# auto 用 config.json 默认模型(不指定 model);flash/pro 显式指定
MODEL=""
case "$PRESET" in
flash) MODEL="deepseek-v4-flash" ;;
pro) MODEL="deepseek-v4-pro" ;;
auto|*) MODEL="" ;;
esac

# --- dispatch(经 ACP,真正执行工具落盘)---
# 不用 `reasonix run`:run 只把工具调用作为文本输出、不执行,文件不会落盘。
# 改走 reasonix acp(stdio JSON-RPC),由 rx-acp-dispatch.mjs 驱动 write_file 等工具真正落地。
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$PROJECT_ROOT"
EXIT_CODE=0
reasonix run --budget "$BUDGET" --preset "$PRESET" --transcript "$TRANSCRIPT" "$TASK_CONTENT" > "$STDOUT_LOG" 2>&1 || EXIT_CODE=$?
node "$SCRIPT_DIR/rx-acp-dispatch.mjs" "$PROJECT_ROOT" "$SPEC_FILE" "$TRANSCRIPT" "$BUDGET" "$MODEL" > "$STDOUT_LOG" 2>&1 || EXIT_CODE=$?

# --- 写 task state(.reasonix-tasks/state.json,#6)---
if [ -d ".reasonix-tasks" ]; then
python3 - "$SLUG" "$BUDGET" "$PRESET" "$EXIT_CODE" "$TRANSCRIPT" <<'PYEOF'
# python3 在部分 Windows 环境是 Store 占位符:command -v 能找到、但一运行就退 49。
# 因此逐个实跑 --version,取第一个真正可用的解释器。
PYTHON=""
for _py in python3 python; do
if command -v "$_py" >/dev/null 2>&1 && "$_py" --version >/dev/null 2>&1; then
PYTHON="$_py"; break
fi
done
if [ -d ".reasonix-tasks" ] && [ -n "$PYTHON" ]; then
"$PYTHON" - "$SLUG" "$BUDGET" "$PRESET" "$EXIT_CODE" "$TRANSCRIPT" <<'PYEOF'
import json, sys, os, datetime
slug, budget, preset, exit_code, transcript = sys.argv[1:6]
sf = ".reasonix-tasks/state.json"
Expand Down