v0.3.0-rc.1: live skill registry + fast advisor scaffold
Roots out the v0.2.x failure mode: Pi-extracted lessons routinely named
hallucinated skill ids (relocate.surface, choose.safe.surface,
survive.shelter, gather.visible_log, …). All 47 Pi-lessons in the live DB
had applied_count=0 because normalisePreferSkill couldn't find them.
Fix:
1. runtime/skill-registry.js — single source of truth derived from
skills/index.js. Exports listSkillIds, isRegistered, and a
prompt-ready block (skillRegistryPrompt) grouped by namespace.
2. Pi prompts (coach/postmortem, coach/reflect) embed the live registry
with a "USE ONLY THESE, never invent" instruction. Lessons are
filtered at write-time too — anything not in the registry and not a
known mode name gets dropped.
3. coach/advice.js — normalisePreferSkill now returns null for unknown
ids, hardening consult() against any hallucinations that slip
through. Warn-logged for visibility.
Also lays the LLM substrate for the rest of v0.3.0:
- runtime/llm/provider.js — OpenAI-compatible chat client. Configured
via PEPA_FAST_LLM_{BASE_URL,API_KEY,MODEL,TIMEOUT_MS}. Safe no-op
unless API_KEY is set. Supports JSON-mode.
- runtime/coach/fast-advisor.js — tactical advisor tier (scaffold).
Exposes advise() that asks the fast LLM what to do RIGHT NOW when
the reflex is wedged/stuck. Rejects hallucinated skill ids using the
registry. Rate-limited 6/h, 30s cooldown. Not auto-triggered yet —
wired into reflex in rc.3 (awareness layer).
Tests: 279 green (+24 vs rc.3): 5 registry, 9 provider, 10 advisor.
See dev/v0.3.0/PLAN.md for the full iteration design (manifesto needs
ladder, event-driven awareness, skill pre-emption) and STATUS.md for
shipped/pending tracking.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -26,6 +26,7 @@ import {
|
||||
poiNearby,
|
||||
recordPOI,
|
||||
} from "../knowledge/index.js";
|
||||
import { isRegistered, skillRegistryPrompt } from "../skill-registry.js";
|
||||
import { info, warn } from "../log.js";
|
||||
|
||||
const COACH_INTERVAL_MS = 5 * 60 * 1000; // 5 min between coach passes
|
||||
@@ -278,22 +279,38 @@ export async function drainOnce({ askPi, stateDir, force = false } = {}) {
|
||||
}
|
||||
|
||||
let lessonsCount = 0;
|
||||
let rejectedPreferCount = 0;
|
||||
for (const item of asArray(parsed.lessons ?? parsed)) {
|
||||
if (!item || !item.lesson) continue;
|
||||
// Skill ids referenced by Pi must be in the live registry.
|
||||
// Mode names (e.g. "night_shelter") are tolerated at write time and
|
||||
// translated at consult time by advice.js#normalisePreferSkill.
|
||||
let preferSkill = item.prefer_skill ?? null;
|
||||
if (preferSkill && !isRegistered(preferSkill) && !isLikelyModeName(preferSkill)) {
|
||||
rejectedPreferCount += 1;
|
||||
preferSkill = null;
|
||||
}
|
||||
let avoidSkill = item.avoid_skill ?? null;
|
||||
if (avoidSkill && !isRegistered(avoidSkill) && !isLikelyModeName(avoidSkill)) {
|
||||
avoidSkill = null;
|
||||
}
|
||||
recordLesson({
|
||||
text: item.lesson,
|
||||
category: item.category ?? "survival",
|
||||
triggerSkill: item.trigger_skill ?? null,
|
||||
triggerHostile: item.trigger_hostile ?? null,
|
||||
triggerSituation: item.trigger_situation ?? null,
|
||||
avoidSkill: item.avoid_skill ?? null,
|
||||
preferSkill: item.prefer_skill ?? null,
|
||||
avoidSkill,
|
||||
preferSkill,
|
||||
confidence: clamp(Number(item.confidence) || 0.6, 0.1, 0.95),
|
||||
source: "pi-coach",
|
||||
sourceRef: item.source_ref ?? null,
|
||||
});
|
||||
lessonsCount += 1;
|
||||
}
|
||||
if (rejectedPreferCount > 0) {
|
||||
warn("coach", `dropped prefer_skill from ${rejectedPreferCount} lessons (not in registry)`);
|
||||
}
|
||||
|
||||
// Write one postmortem per death; if Pi grouped them, share the same lesson.
|
||||
const groupLesson = parsed.lessons?.[0]?.lesson ?? parsed.lesson ?? null;
|
||||
@@ -313,6 +330,17 @@ export async function drainOnce({ askPi, stateDir, force = false } = {}) {
|
||||
return { ok: true, analysed: pending.length, lessons: lessonsCount };
|
||||
}
|
||||
|
||||
// Mode names from runtime/modes.js (advice.js#MODE_TO_SKILL) — we accept
|
||||
// these at write time because advice.js maps them to real skills at consult.
|
||||
const KNOWN_MODE_NAMES = new Set([
|
||||
"self_preservation", "night_shelter", "hunger", "shelter",
|
||||
"flee", "sleep", "eat", "tunnel_out", "tunnel-out", "explore", "wander",
|
||||
]);
|
||||
function isLikelyModeName(s) {
|
||||
if (!s || typeof s !== "string") return false;
|
||||
return KNOWN_MODE_NAMES.has(s.toLowerCase().trim());
|
||||
}
|
||||
|
||||
function buildPrompt(deaths) {
|
||||
const summary = deaths.map((d) => {
|
||||
const ctx = safeParse(d.context_blob);
|
||||
@@ -334,6 +362,8 @@ function buildPrompt(deaths) {
|
||||
"The bot is trying to gather wood, craft tools, build a small village, and survive nights.",
|
||||
"It's currently dying repeatedly. Your job: extract 1-3 short, generalised lessons it can apply on respawn.",
|
||||
"",
|
||||
skillRegistryPrompt({ limit: 1800 }),
|
||||
"",
|
||||
"DEATHS:",
|
||||
summary,
|
||||
"",
|
||||
@@ -343,12 +373,13 @@ function buildPrompt(deaths) {
|
||||
' { "lesson": "...", "category": "combat|pathing|crafting|survival|social",',
|
||||
' "trigger_skill": "<skill id or null>",',
|
||||
' "trigger_hostile": "<mob name or null>",',
|
||||
' "avoid_skill": "<skill to NOT dispatch or null>",',
|
||||
' "prefer_skill": "<alternative skill or null>",',
|
||||
' "avoid_skill": "<registered skill id to NOT dispatch, or null>",',
|
||||
' "prefer_skill": "<registered skill id to use instead, or null>",',
|
||||
' "confidence": 0.7 }',
|
||||
' ] }',
|
||||
"",
|
||||
"Keep each lesson under 30 words. Be specific (e.g., \"attack creeper with fists\" rather than \"don't fight\").",
|
||||
"CRITICAL: avoid_skill and prefer_skill MUST be one of the registered ids above, or null.",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
@@ -390,4 +421,4 @@ function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); }
|
||||
function safeParse(s) { try { return JSON.parse(s); } catch { return null; } }
|
||||
|
||||
// Test-only exports
|
||||
export const __testing = { captureDeath, buildPrompt, extractJson, inferCause };
|
||||
export const __testing = { captureDeath, buildPrompt, extractJson, inferCause, isLikelyModeName, KNOWN_MODE_NAMES };
|
||||
|
||||
Reference in New Issue
Block a user