Phase 1 of plans/autonomous-survival-bot-prd.md. The bot must always be able to answer "what am I doing and why am I not doing more?" without parsing the log stream. New modules: - runtime/state.js: pure FSM classifier emitting emergency / working / recovering / planning / social / idle from snapshot + reflex context. - runtime/no-progress.js: sliding-window detector that watches position and inventory; when both are unchanged for 60 s+, emits one stable reason code from REASONS (waiting_for_day, night_hostile_nearby, no_food_source, inventory_full, no_reachable_target, planner_empty, awaiting_action_cooldown). - runtime/viewer.js: optional prismarine-viewer launcher behind VIEWER_PORT. Lazy import so the dep is not required by default. Wiring: - runtime/bot.js: tick() now computes runtimeState + noProgressReason every tick and stamps them on the snapshot along with activeSkill, currentMilestone (read from plan.md, cached 30 s), lastResult, failuresByCode and lastEscalation. - runtime/bot.js: dispatchAction records lastResult and lastFailureAt for the recovering-state classifier. - runtime/planner.js: exports isPlannerBusy(), readNextMilestone() and planExists() so the runtime can show planning state + current milestone without spawning extra Pi calls. - runtime/config.js: adds VIEWER_PORT support. TUI: - tui/tui.tsx: StatusBar gains a state badge, current-skill row, milestone row, no-progress reason warning, last-result line with ok/fail color, failures-by-class summary and last-escalation age. Co-authored-by: Yuriy Mayatnikov <mayatnikov@me.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
58 lines
1.7 KiB
JavaScript
58 lines
1.7 KiB
JavaScript
// Finite-state classifier for the runtime. Each tick the bot is in exactly
|
|
// one of:
|
|
//
|
|
// emergency — survival is at risk (low HP, hostile in melee, drowning…)
|
|
// working — an async action is in flight (reflexCtx.busy === true)
|
|
// recovering — last action failed recently and we are in its cooldown
|
|
// planning — the LLM planner is computing a fresh plan.md
|
|
// social — we just produced a chat reply (cools down social activity)
|
|
// idle — none of the above; the bot is up but nothing is happening
|
|
//
|
|
// Pure function: takes the current observed inputs and returns a string.
|
|
// No side effects; the caller decides what to do with the classification.
|
|
|
|
export const STATES = Object.freeze({
|
|
EMERGENCY: "emergency",
|
|
WORKING: "working",
|
|
RECOVERING: "recovering",
|
|
PLANNING: "planning",
|
|
SOCIAL: "social",
|
|
IDLE: "idle",
|
|
});
|
|
|
|
const EMERGENCY_HP = 8;
|
|
const EMERGENCY_HOSTILE_DISTANCE = 4;
|
|
const RECOVERING_WINDOW_MS = 30_000;
|
|
const SOCIAL_WINDOW_MS = 5_000;
|
|
|
|
export function computeState({
|
|
snapshot,
|
|
ctx,
|
|
plannerInFlight = false,
|
|
lastChatReplyAt = 0,
|
|
lastFailureAt = 0,
|
|
now = Date.now(),
|
|
}) {
|
|
if (!snapshot?.connected) return STATES.IDLE;
|
|
|
|
const hp = snapshot.health ?? 20;
|
|
if (hp <= EMERGENCY_HP) return STATES.EMERGENCY;
|
|
if (snapshot.closestHostile && snapshot.closestHostile.distance <= EMERGENCY_HOSTILE_DISTANCE) {
|
|
return STATES.EMERGENCY;
|
|
}
|
|
|
|
if (ctx?.busy) return STATES.WORKING;
|
|
|
|
if (lastFailureAt && now - lastFailureAt < RECOVERING_WINDOW_MS) {
|
|
return STATES.RECOVERING;
|
|
}
|
|
|
|
if (plannerInFlight) return STATES.PLANNING;
|
|
|
|
if (lastChatReplyAt && now - lastChatReplyAt < SOCIAL_WINDOW_MS) {
|
|
return STATES.SOCIAL;
|
|
}
|
|
|
|
return STATES.IDLE;
|
|
}
|