diff --git a/services/minecraft/src/cognitive/conscious/js-planner.test.ts b/services/minecraft/src/cognitive/conscious/js-planner.test.ts index 296ab0de4..ebb2cb623 100644 --- a/services/minecraft/src/cognitive/conscious/js-planner.test.ts +++ b/services/minecraft/src/cognitive/conscious/js-planner.test.ts @@ -126,6 +126,19 @@ describe('javaScriptPlanner', () => { expect(planned.actions.map(a => a.action)).toEqual([{ tool: 'chat', params: { message: '2 oak_log' } }]) }) + it('does not expose stringified return mirror on prevRun', 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('return Object.prototype.hasOwnProperty.call(prevRun, "returnValue")', actions, globals, executeAction) + expect(planned.returnValue).toBe('false') + }) + 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 bd67a7b03..31640617e 100644 --- a/services/minecraft/src/cognitive/conscious/js-planner.ts +++ b/services/minecraft/src/cognitive/conscious/js-planner.ts @@ -160,7 +160,6 @@ export class JavaScriptPlanner { }) if (isRecord(this.sandbox.lastRun)) { - this.sandbox.lastRun.returnValue = returnValue this.sandbox.lastRun.returnRaw = result } diff --git a/services/minecraft/src/cognitive/conscious/prompts/brain-prompt.md b/services/minecraft/src/cognitive/conscious/prompts/brain-prompt.md index 2dc812974..25f6238b7 100644 --- a/services/minecraft/src/cognitive/conscious/prompts/brain-prompt.md +++ b/services/minecraft/src/cognitive/conscious/prompts/brain-prompt.md @@ -21,7 +21,7 @@ You are an autonomous agent playing Minecraft. - Globals refreshed every turn: `snapshot`, `self`, `environment`, `social`, `threat`, `attention`, `autonomy`, `event`, `now`, `query`, `bot`, `mineflayer`, `currentInput`, `llmLog`, `actionQueue`, `noActionBudget`, `errorBurstGuard`. - Persistent globals: `mem` (cross-turn memory), `lastRun` (this run), `prevRun` (previous run), `lastAction` (latest action result), `log(...)`. - Budget helpers: `setNoActionBudget(n)` and `getNoActionBudget()` control/inspect eval-only no-action follow-up budget. - - Cross-turn result access: use `prevRun.returnRaw` for typed values (arrays/objects); `prevRun.returnValue` is stringified for display/logging. + - Cross-turn result access: use `prevRun.returnRaw` for typed values (arrays/objects). If you need text output, stringify `returnRaw` explicitly. - `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 tool calls per turn: 5. @@ -116,7 +116,7 @@ Silent-eval pattern (strongly encouraged): - Use no-action evaluation turns to inspect uncertain values before committing to world actions. - Good pattern: - Turn A: `let blocksToMine = someFunc(); blocksToMine` - - Turn B: inspect `[SCRIPT]` return / `llmLog`, then act: `await collectBlocks({ type: ..., num: ... })` + - Turn B: inspect `prevRun.returnRaw` / `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; follow-ups are budgeted and can chain for multi-step reasoning. - Default no-action follow-up budget is 3 and max is 8. @@ -127,8 +127,10 @@ Value-first rule (mandatory for read -> action flows): - 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. +- In the next turn, use `prevRun.returnRaw` 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. +- For typed follow-up logic, use `prevRun.returnRaw` (or `lastRun.returnRaw` for current-turn chaining). +- If you need a string for chat/logging, stringify raw data yourself (for example `JSON.stringify(prevRun.returnRaw)`). - 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 a small number of evaluation turns, the next turn must either: @@ -137,7 +139,8 @@ Value-first rule (mandatory for read -> action flows): - explicitly increase no-action budget for this scenario via `setNoActionBudget(n)`. - 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 })` + - Turn B: `const inv = prevRun.returnRaw; const text = Array.isArray(inv) && inv.length ? inv.map(({ name, count }) => `${count} ${name}`).join(", ") : "nothing"; await chat({ message: `I have: ${text}`, feedback: false })` + - Turn B (raw -> explicit stringify): `const coords = prevRun.returnRaw; await chat({ message: Array.isArray(coords) ? JSON.stringify(coords) : "[]", feedback: false })` # Response Format You must respond with JavaScript only (no markdown code fences).