moeru-ai/airi · warning · Error
Your last reply was natural language, not JavaScript, so not
Error message
Your last reply was natural language, not JavaScript, so nothing ran. Reply with ONLY executable JavaScript that calls the action API — optionally inside a single ```js code block (only the code inside runs). To say something to the player, that is also code: call chat, e.g. await chat({ message: "..." }). Never reply in prose. What it means
Thrown by JavaScriptPlanner.evaluate() as a syntax firewall before the model's reply ever reaches the sandbox. extractJavaScriptCandidate() pulls a fenced ```js block or strips CJK prose edge-lines from the raw content; isSyntacticallyValidScript() then compiles the candidate with `new Function('return (async () => {\n...\n})()')` (compile-only, no execution). If that throws a SyntaxError, this directive message is raised instead of letting the isolate emit an opaque 'Unexpected identifier' death-spiral. It is a corrective instruction meant to be fed back to the LLM so its next turn is real code.
Source
Thrown at integrations/minecraft/src/cognitive/conscious/js-planner.ts:724
this.bridgeTimeoutMs = options.bridgeTimeoutMs ?? 30_000
this.maxBridgeCalls = options.maxBridgeCalls ?? 64
this.timeoutMs = options.timeoutMs ?? 750
this.maxActionsPerTurn = options.maxActionsPerTurn ?? 5
this.memoryLimitMb = options.memoryLimitMb ?? 32
}
public async evaluate(
content: string,
availableActions: Action[],
globals: RuntimeGlobals,
executeAction: (action: ActionInstruction) => Promise<unknown>,
): Promise<JavaScriptRunResult> {
const script = extractJavaScriptCandidate(content)
// Firewall: reject natural-language replies before they reach the sandbox, with a directive
// correction the model can actually act on (instead of an opaque "Unexpected identifier" loop).
if (!isSyntacticallyValidScript(script)) {
throw new Error(
'Your last reply was natural language, not JavaScript, so nothing ran. '
+ 'Reply with ONLY executable JavaScript that calls the action API — optionally inside a single '
+ '```js code block (only the code inside runs). To say something to the player, that is also code: '
+ 'call chat, e.g. await chat({ message: "..." }). Never reply in prose.',
)
}
const run: ActivePlannerRun = {
actionCount: 0,
actionsByName: new Map(availableActions.map(action => [action.name, action])),
executeAction,
executed: [],
logs: [],
sawSkip: false,
}
this.activeRun = run
let result: unknownView on GitHub (pinned to 27111382b4)
Solutions
- Feed the message verbatim back into the LLM context as a correction and re-prompt; the message is already phrased as a directive.
- Ensure the planner prompt instructs the model to reply with ONLY a single ```js fence and no surrounding prose.
- If English (non-CJK) prose prefixes are common, extend stripEdgeProseLines to treat leading ASCII sentences without JS structure as prose too.
- Check that the fence regex in extractJavaScriptCandidate matches the model's actual fence style (no stray inner whitespace/language tag mismatches).
Example fix
// before (model reply that triggers it):
// Sure! I'll go mine the stone now.
// await mineBlockAt({ position: { x: 10, y: 64, z: -5 } })
//
// after (model reply that passes the firewall):
// ```js
// await mineBlockAt({ position: { x: 10, y: 64, z: -5 } })
// ``` Defensive patterns
Strategy: validation
Validate before calling
// Before calling evaluate(), pre-screen the model reply the same way the firewall does.
import { extractJavaScriptCandidate, isSyntacticallyValidScript } from './js-planner'
function isExecutableReply(content: string): boolean {
const candidate = extractJavaScriptCandidate(content)
return isSyntacticallyValidScript(candidate)
}
// if (!isExecutableReply(reply)) { re-prompt the model for code-only output } Try / catch
// Planner callers should treat evaluate() as fallible and branch on the syntax-firewall error:
// try {
// const result = await planner.evaluate(content, actions, globals, exec)
// } catch (e) {
// if (errorMessageFrom(e)?.startsWith('Your last reply was natural language')) {
// // feed the directive back to the LLM and retry once
// } else throw e
// } Prevention
- Configure the planner system prompt to demand a single fenced ```js block and nothing else.
- Reject model replies containing more than one code fence before evaluation.
- For CJK-heavy models, verify stripEdgeProseLines covers the prose style they emit.
- Keep one retry budget for the firewall correction so a single prose reply does not fail the turn.
When it happens
Trigger: The LLM driving the cognitive planner replies with pure prose, markdown that is not a code fence, partially-fenced code (missing closing ```), or un-fenced code prefixed by a non-CJK natural-language line that stripEdgeProseLines cannot remove (e.g. English intro line). Also fires when the candidate contains valid CJK identifiers that compile as an expression but the surrounding prose breaks syntax, or when the model emits a language other than JS inside the fence (e.g. python).
Common situations: Cold-start or low-temperature models that narrate intent ('Let me go mine stone.') before acting; models trained on chat formats that wrap code in explanations; prompt templates that do not strongly enforce code-only output; locale-specific models that emit Chinese/Japanese/Korean reasoning lines; a fence typo such as ``` javascript with an inner space, or a stray backtick inside the code that closes the fence early.
Related errors
- Expectation failed: Condition evaluated to false
- Expectation failed: Expected movedDistance >= ${threshold},
- Expectation failed: expectNear(target) requires last action
- Expectation failed: expectNear() requires target argument or
- Expectation failed: Expected distance <= ${maxDist}, got ${d
AI-assisted analysis of moeru-ai/airi@27111382b4 (2026-08-12).
Data as JSON: /api/errors/fc8a88e515313e25.
Report an issue: GitHub.