fix(minecraft): persist prevRun returnRaw for typed value reuse

This commit is contained in:
Rin
2026-02-18 11:14:41 +08:00
committed by Neko Ayaka
parent 1f7e9468c4
commit 42e5e33745
3 changed files with 40 additions and 9 deletions
@@ -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}`)
@@ -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<string, unknown>): Promise<ActionRuntimeResult> {
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<string, unknown>): 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)
@@ -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.