diff --git a/services/minecraft/src/cognitive/conscious/js-planner.test.ts b/services/minecraft/src/cognitive/conscious/js-planner.test.ts index 3de42477e..e23063598 100644 --- a/services/minecraft/src/cognitive/conscious/js-planner.test.ts +++ b/services/minecraft/src/cognitive/conscious/js-planner.test.ts @@ -84,6 +84,23 @@ describe('javaScriptPlanner', () => { expect(planned.actions.map(a => a.action)).toEqual([{ tool: 'chat', params: { message: 'count=2' } }]) }) + it('persists typed previous return via prevRun.returnRaw', async () => { + const planner = new JavaScriptPlanner() + const executeAction = vi.fn(async action => `ok:${action.tool}`) + + await planner.evaluate(` + const inv = [{ name: "oak_log", count: 2 }] + return inv + `, actions, globals, executeAction) + + const planned = await planner.evaluate(` + const inv = prevRun.returnRaw + await chat(inv.map(item => item.count + " " + item.name).join(", ")) + `, actions, globals, executeAction) + + expect(planned.actions.map(a => a.action)).toEqual([{ tool: 'chat', params: { message: '2 oak_log' } }]) + }) + it('provides snapshot globals in script scope', async () => { const planner = new JavaScriptPlanner() const executeAction = vi.fn(async action => `ok:${action.tool}`) diff --git a/services/minecraft/src/cognitive/conscious/js-planner.ts b/services/minecraft/src/cognitive/conscious/js-planner.ts index 48a7c9d12..5088a3644 100644 --- a/services/minecraft/src/cognitive/conscious/js-planner.ts +++ b/services/minecraft/src/cognitive/conscious/js-planner.ts @@ -152,6 +152,7 @@ export class JavaScriptPlanner { if (isRecord(this.sandbox.lastRun)) { this.sandbox.lastRun.returnValue = returnValue + this.sandbox.lastRun.returnRaw = result } return { @@ -282,7 +283,7 @@ export class JavaScriptPlanner { }) this.defineGlobalTool('log', (...args: unknown[]) => { if (!this.activeRun) - throw new Error('log() is only allowed during planner evaluation') + throw new Error('log() is only allowed during REPL evaluation') const rendered = args.map(arg => inspect(arg, { depth: 4, breakLength: 120 })).join(' ') this.activeRun.logs.push(rendered) @@ -415,6 +416,7 @@ export class JavaScriptPlanner { this.sandbox.lastRun = { actions: run.executed, logs: run.logs, + returnRaw: undefined, } } @@ -446,7 +448,7 @@ export class JavaScriptPlanner { private async runAction(tool: string, params: Record): Promise { if (!this.activeRun) { - throw new Error('Tool calls are only allowed during planner evaluation') + throw new Error('Tool calls are only allowed during REPL evaluation') } if (this.activeRun.sawSkip && tool !== 'skip') { @@ -513,7 +515,7 @@ export class JavaScriptPlanner { private validateAction(tool: string, params: Record): ValidationResult { if (!this.activeRun) - throw new Error('Tool calls are only allowed during planner evaluation') + throw new Error('Tool calls are only allowed during REPL evaluation') const action = this.activeRun.actionsByName.get(tool) if (!action) diff --git a/services/minecraft/src/cognitive/conscious/prompts/brain-prompt.md b/services/minecraft/src/cognitive/conscious/prompts/brain-prompt.md index 169d58db2..b316b55d8 100644 --- a/services/minecraft/src/cognitive/conscious/prompts/brain-prompt.md +++ b/services/minecraft/src/cognitive/conscious/prompts/brain-prompt.md @@ -18,6 +18,7 @@ You are an autonomous agent playing Minecraft. - Use `await` on tool calls when later logic depends on the result. - Globals refreshed every turn: `snapshot`, `self`, `environment`, `social`, `threat`, `attention`, `autonomy`, `event`, `now`, `query`, `bot`, `mineflayer`, `currentInput`, `llmLog`. - Persistent globals: `mem` (cross-turn memory), `lastRun` (this run), `prevRun` (previous run), `lastAction` (latest action result), `log(...)`. + - Cross-turn result access: use `prevRun.returnRaw` for typed values (arrays/objects); `prevRun.returnValue` is stringified for display/logging. - `forget_conversation()` clears conversation memory (`conversationHistory` and `lastLlmInputSnapshot`) for prompt/debug reset workflows. - Last script outcome is also echoed in the next turn as `[SCRIPT]` context (return value, action stats, and logs). - Maximum actions per turn: 5. If you need more, break down your task to perform in multiple turns. @@ -61,15 +62,20 @@ Core query entrypoints: Composable patterns: - `const ores = query.blocks().within(24).isOre().names().uniq().list()` -- `const me = query.self(); return me` -- `const snap = query.snapshot(20); return snap.inventory.summary` +- `const me = query.self(); me` +- `const snap = query.snapshot(20); snap.inventory.summary` - `const nearestLog = query.blocks().whereName(["oak_log", "birch_log"]).first()` - `const nearbyPlayers = query.entities().whereType("player").within(32).list()` - `const inv = query.inventory().countByName(); const hasFood = (inv.bread ?? 0) > 0` - `const hasPickaxe = query.inventory().has("stone_pickaxe", 1)` -- `const invSummary = query.inventory().summary(); return invSummary` +- `const invSummary = query.inventory().summary(); invSummary` +- `const invLine = query.inventory().summary().map(({ name, count }) => `${count} ${name}`).join(", "); invLine` - `const craftableTools = query.craftable().whereIncludes("pickaxe").uniq().list()` +Inventory summary shape reminder: +- `query.inventory().summary()` returns an **array** of `{ name, count }`. +- Do **not** use `Object.entries(summary)` for inventory summary formatting. + Callable-only reminder (strict): - Query helpers that are functions must be called with `()`. - Never return function references as values (invalid): `query.inventory().summary` @@ -100,16 +106,22 @@ Silent-eval pattern (strongly encouraged): - Turn A: `let blocksToMine = someFunc(); blocksToMine` - Turn B: inspect `[SCRIPT]` return / `llmLog`, then act: `await collectBlocks({ type: ..., num: ... })` - Prefer this when a wrong action would be costly, dangerous, or hard to undo. +- A `no_actions` follow-up after an eval-only turn is normal; treat it as the handoff turn for action/reporting. Value-first rule (mandatory for read -> action flows): -- If a request depends on observed world/query data, first run an evaluation-only turn and `return` the concrete value. +- If a request depends on observed world/query data, first run an evaluation-only turn and end with the concrete value expression. - Do not call world/chat tools in that first turn. +- End eval turns with a concrete final expression (for example `inv`, `target`, `summary`) so `[SCRIPT]` captures it. - In the next turn, use `[SCRIPT] Last eval return=...` as the source of truth for tool parameters/messages. +- Do not re-query the same read value in the follow-up turn; use the persisted value to avoid TOCTOU drift. - Avoid acting on unresolved intermediate variables when a concrete returned value can be verified first. - For explicit user tasks (e.g. "get X", "craft Y", "go to Z"), do not stay in repeated evaluation-only turns. - After one evaluation turn, the next turn must either: - call at least one action/chat tool toward completion, or - call `giveUp({ reason, cooldown_seconds })` with a concrete blocker. +- Example (read -> chat report): + - Turn A: `const inv = query.inventory().summary(); inv` + - Turn B: `const inv = prevRun.returnValue; const text = Array.isArray(inv) && inv.length ? inv.map(({ name, count }) => `${count} ${name}`).join(", ") : "nothing"; await chat({ message: `I have: ${text}`, feedback: false })` # Response Format You must respond with JavaScript only (no markdown code fences). @@ -161,9 +173,9 @@ Common patterns: - Plan with `mem.plan`, execute in small steps, and verify each step before continuing. - Prefer deterministic scripts: no random branching unless needed. - Keep per-turn scripts short and focused on one tactical objective. -- Prefer "evaluate then act" loops: first compute and return candidate values (no actions), then perform tools in the next turn using confirmed values. +- Prefer "evaluate then act" loops: first compute and surface candidate values (no actions), then perform tools in the next turn using confirmed values. - For read->chat/report tasks, always prefer: - - Turn A: `const value = ...; return value` + - Turn A: `const value = ...; value` - Turn B: construct tool params/messages from confirmed returned value. - If you hit repeated failures with no progress, call `await giveUp({ reason, cooldown_seconds })` once instead of retry-spamming. - Treat `environment.nearbyPlayersGaze` as a weak hint, not a command. Never move solely because someone looked somewhere unless they also gave a clear instruction.