Pure-Pi runtime had three failure modes in practice: - slow: 20-60s per decision because LLM was in the hot path - expensive: every tick (defend, eat, idle) paid for a reasoning pass - invisible: required tmux capture-pane to know what the bot was doing New runtime/ layer is a long-running Node daemon that owns the MC connection, ticks a priority-ordered reflex chain (defend > eat > sleep > idle) with NO LLM in the hot path, and exposes status + commands over a Unix-socket IPC. tui/ is an Ink dashboard that attaches over IPC and can detach freely — multiple TUI clients can connect at once. Pi/Codex are still available, but as on-demand escalation: TUI hotkey 'a' spawns `pi -p "<prompt>"` as a subprocess and streams stdout into the dashboard. The self-improvement loop (proposals → operator approval → Pi-driven patch → hot reload) is documented in docs/runtime.md but not yet wired. Reflex bodies are stubs today — they log decisions but don't drive Mineflayer actions yet. The priority chain, IPC contract, and TUI are fully working; subsequent commits will fill in defend/eat/sleep bodies and wire automatic escalation. Run with `npm run bot` + `npm run tui`. Pi-only fallback stays at `npm run agent`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
45 lines
1.4 KiB
JavaScript
45 lines
1.4 KiB
JavaScript
// Spawn `pi -p "<prompt>"` as a one-shot subprocess and stream stdout/stderr
|
|
// to the caller. Used for headless escalation: the bot's reflex / TUI can ask
|
|
// Pi a single question without keeping a long-lived TUI session open.
|
|
|
|
import { spawn } from "node:child_process";
|
|
import { info, warn } from "./log.js";
|
|
|
|
// Resolve `pi` lazily — user has it on PATH via vite-plus shim.
|
|
const PI_BIN = process.env.PI_BIN || "pi";
|
|
|
|
export function askPi({ prompt, onChunk, onDone, cwd, signal }) {
|
|
const startedAt = Date.now();
|
|
info("pi-bridge", `spawning pi -p (${prompt.length} chars)`);
|
|
|
|
const child = spawn(PI_BIN, ["-p", prompt], {
|
|
cwd: cwd || process.cwd(),
|
|
env: { ...process.env, CI: "1" },
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
signal,
|
|
});
|
|
|
|
child.stdout.setEncoding("utf8");
|
|
child.stderr.setEncoding("utf8");
|
|
|
|
child.stdout.on("data", (chunk) => {
|
|
onChunk?.({ stream: "stdout", text: chunk });
|
|
});
|
|
child.stderr.on("data", (chunk) => {
|
|
onChunk?.({ stream: "stderr", text: chunk });
|
|
});
|
|
|
|
child.on("error", (err) => {
|
|
warn("pi-bridge", `pi subprocess failed to start: ${err?.message ?? err}`);
|
|
onDone?.({ code: -1, durationMs: Date.now() - startedAt, error: String(err) });
|
|
});
|
|
|
|
child.on("exit", (code) => {
|
|
const durationMs = Date.now() - startedAt;
|
|
info("pi-bridge", `pi exited code=${code} after ${durationMs}ms`);
|
|
onDone?.({ code: code ?? -1, durationMs });
|
|
});
|
|
|
|
return child;
|
|
}
|