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: unknown

View on GitHub (pinned to 27111382b4)

Solutions

  1. Feed the message verbatim back into the LLM context as a correction and re-prompt; the message is already phrased as a directive.
  2. Ensure the planner prompt instructs the model to reply with ONLY a single ```js fence and no surrounding prose.
  3. If English (non-CJK) prose prefixes are common, extend stripEdgeProseLines to treat leading ASCII sentences without JS structure as prose too.
  4. 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

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


AI-assisted analysis of moeru-ai/airi@27111382b4 (2026-08-12). Data as JSON: /api/errors/fc8a88e515313e25. Report an issue: GitHub.