feat(runtime/coach,reflex): retrieval-augmented dispatch via learned lessons

This closes the learning loop. Lessons in knowledge.db now actually
influence behaviour:

- runtime/coach/advice.js: consult({plannedSkillId, snapshot}) reads
  knowledge.topAdvice() and returns 'override' / 'avoid' / 'proceed'.
  When a lesson says "avoid <skill>" with prefer="survive.flee" (etc.),
  the dispatcher swaps in the alternative.

- runtime/reflex.js:
  * curriculumReflex now consults advice before dispatch; on 'avoid'
    backs off the planned skill + sets wander hint; on 'override'
    dispatches the lesson's preferred alternative.
  * defendReflex (dist≤4 melee branch) consults advice too — so a
    creeper at 4m honours the starter rule "attack creeper → flee".
    Failure outcomes feed back via markApplied so confidence stays
    grounded.

SAFE_OVERRIDES whitelist contains only known runSkill targets
(survive.flee, survive.sleep, survive.eat, recovery.tunnel-out,
explore.far/wander, village.build-shelter); unknown prefers fall back
to plain 'avoid'.

7 advice tests; total suite 237 green.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-27 13:23:20 +03:00
co-authored by Claude Opus 4.7
parent 5449ad2e0d
commit e183aaec4e
4 changed files with 206 additions and 4 deletions
+29 -3
View File
@@ -25,6 +25,7 @@ import {
wander,
} from "./actions.js";
import { runSkill, getSkill } from "./skills/index.js";
import { consult as consultAdvice, reportOutcome as reportAdviceOutcome } from "./coach/advice.js";
import { situationHash } from "./scenario-memory.js";
import { tickModes } from "./modes.js";
@@ -200,6 +201,15 @@ function defendReflex(ctx) {
ctx.defendAttackStuck = null;
return dispatchDefendFlee(ctx, hostile, dist, { ignoreCooldown: true });
}
// v0.2.0 — consult learned lessons. If knowledge says "do not
// attack <hostile> in this state" (e.g. creeper rule, or no-weapon
// rule learned from post-mortems), flee instead. This is the
// closing of the learning loop for emergency combat.
const advice = consultAdvice({ plannedSkillId: `attack ${hostile.name}`, snapshot: s });
if (advice.action === "avoid" || advice.action === "override") {
if (advice.lessonId) reportAdviceOutcome({ lessonId: advice.lessonId, succeeded: false });
return dispatchDefendFlee(ctx, hostile, dist, { ignoreCooldown: true });
}
ctx.dispatch(
() => attackNearestUntilClear(ctx.bot, hostile.name, {
maxSwings: ctx.defendAttackMaxSwings,
@@ -441,10 +451,26 @@ function curriculumReflex(ctx) {
}
}
// v0.2.0 — consult learned lessons. If a high-confidence lesson says
// "avoid <skillId> in this situation", swap to its preferred
// alternative (or back off entirely if no safe alternative is named).
const advice = consultAdvice({ plannedSkillId: skillId, snapshot: ctx.snapshot });
let dispatchSkillId = skillId;
if (advice.action === "avoid") {
ctx.skillBackoff = ctx.skillBackoff ?? {};
ctx.skillBackoff[skillId] = Date.now() + SKILL_BACKOFF_MS;
ctx.skillBackoff["__wander_hint__"] = Date.now() + SKILL_BACKOFF_MS;
return { action: "noop", kind: "curriculum-advice-avoid", label: skillId, lessonId: advice.lessonId };
}
if (advice.action === "override" && advice.overrideSkillId) {
dispatchSkillId = advice.overrideSkillId;
}
ctx.lastCurriculumAt = Date.now();
ctx.dispatch(() => runSkill(skillId, ctx), skillId, {
ctx.dispatch(() => runSkill(dispatchSkillId, ctx), dispatchSkillId, {
onComplete: (res) => {
ctx.skillBackoff = ctx.skillBackoff ?? {};
if (advice.lessonId) reportAdviceOutcome({ lessonId: advice.lessonId, succeeded: !!res?.ok });
if (res?.recovery?.hint === "wander") {
// Same fix the old autonomous reflex applied for "no reachable
// log" — switch to exploration for a minute.
@@ -456,7 +482,7 @@ function curriculumReflex(ctx) {
// retried on the very next tick. Hold for SKILL_BACKOFF_MS.
const cooldownCodes = new Set(["missing_tool", "missing_material", "no_target", "no_food_source", "unsupported_version", "no_chest", "no_space", "nothing_to_deposit"]);
if (cooldownCodes.has(res?.code)) {
ctx.skillBackoff[skillId] = Date.now() + SKILL_BACKOFF_MS;
ctx.skillBackoff[dispatchSkillId] = Date.now() + SKILL_BACKOFF_MS;
}
} else {
// Success clears the wander hint immediately.
@@ -465,7 +491,7 @@ function curriculumReflex(ctx) {
}
},
});
return { action: "dispatched", kind: "curriculum-skill", label: skillId };
return { action: "dispatched", kind: "curriculum-skill", label: dispatchSkillId };
}
// ---- idle ------------------------------------------------------------------