Files
pepa-pi-bot/runtime/perceive.js
T
mayatnikovandClaude Opus 4.7 c9794e0bbf feat(runtime): script-driven reflex daemon + Ink TUI dashboard
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>
2026-05-25 15:59:33 +03:00

70 lines
1.8 KiB
JavaScript

// Build a compact, JSON-safe snapshot of the world around the bot. Used both
// for reflex decisions and for periodic IPC STATUS events.
function vec3ToObj(v) {
if (!v) return null;
return { x: Math.round(v.x * 100) / 100, y: Math.round(v.y * 100) / 100, z: Math.round(v.z * 100) / 100 };
}
const HOSTILE = new Set([
"zombie",
"skeleton",
"creeper",
"spider",
"witch",
"pillager",
"vindicator",
"husk",
"stray",
"drowned",
"phantom",
"enderman",
"slime",
"magma_cube",
"hoglin",
"piglin_brute",
"ravager",
"warden",
"breeze",
"bogged",
]);
export function snapshot(bot) {
if (!bot || !bot.entity) {
return { connected: false };
}
const pos = bot.entity.position;
const entities = Object.values(bot.entities || {});
const players = entities.filter((e) => e.type === "player" && e.username && e.username !== bot.username);
const hostiles = entities.filter((e) => HOSTILE.has((e.name || "").toLowerCase()));
const closestHostile = hostiles.reduce((best, e) => {
const d = e.position.distanceTo(pos);
return !best || d < best.d ? { d, e } : best;
}, null);
const inventory = (bot.inventory?.items?.() ?? []).reduce((acc, item) => {
acc[item.name] = (acc[item.name] ?? 0) + item.count;
return acc;
}, {});
return {
connected: true,
username: bot.username,
position: vec3ToObj(pos),
health: bot.health,
food: bot.food,
saturation: bot.foodSaturation,
experience: bot.experience?.level,
time: bot.time?.timeOfDay,
isDay: bot.time?.isDay,
weather: { rain: bot.isRaining, thunder: bot.thundering },
dimension: bot.game?.dimension,
inventory,
players: players.map((p) => ({ name: p.username, distance: Math.round(p.position.distanceTo(pos)) })),
hostileCount: hostiles.length,
closestHostile: closestHostile
? { name: closestHostile.e.name, distance: Math.round(closestHostile.d * 10) / 10 }
: null,
};
}