fix(minecraft): llm can now obtain expression results

Add rewriteTrailingExpressionToReturn helper that rewrites final non-statement expressions to return statements, apply to both normalizeReplCode and LLM planner evaluation paths to capture trailing expression values (e.g. `inv;` returns inventory array), skip rewrite if code contains explicit return/ends with block syntax/is statement keyword, add tests verifying debug REPL and multi-line LLM scripts capture trailing expression
This commit is contained in:
Rin
2026-02-18 11:14:39 +08:00
committed by Neko Ayaka
parent b32c2cac89
commit 27c3b7f84a
2 changed files with 63 additions and 2 deletions
@@ -72,6 +72,18 @@ function createPerceptionEvent() {
}
describe('brain no-action follow-up', () => {
it('returns trailing expression values in debug repl scripts', async () => {
const brain: any = new Brain(createDeps('await skip()'))
const result = await brain.executeDebugRepl(`
const inv = [{ name: 'oak_sapling', count: 1 }]
inv;
`)
expect(result.error).toBeUndefined()
expect(result.returnValue).toContain('oak_sapling')
})
it('queues exactly one synthetic follow-up on no-action result', async () => {
const brain: any = new Brain(createDeps('1 + 1'))
const enqueueSpy = vi.fn(async () => undefined)
@@ -88,6 +100,21 @@ describe('brain no-action follow-up', () => {
})
})
it('captures trailing expression return for llm multi-line scripts', async () => {
const brain: any = new Brain(createDeps(`
const inv = [{ name: 'oak_sapling', count: 1 }]
inv;
`))
const enqueueSpy = vi.fn(async () => undefined)
brain.enqueueEvent = enqueueSpy
await brain.processEvent({} as any, createPerceptionEvent())
expect(enqueueSpy).toHaveBeenCalledTimes(1)
const queuedEvent = (enqueueSpy.mock.calls[0] as any[])?.[1]
expect(queuedEvent?.payload?.returnValue).toContain('oak_sapling')
})
it('does not chain follow-up from follow-up event source', async () => {
const brain: any = new Brain(createDeps('1 + 1'))
const enqueueSpy = vi.fn(async () => undefined)
@@ -389,10 +389,44 @@ export class Brain {
private normalizeReplCode(code: string): string {
// Simple top-level var persistence for REPL UX:
// const/let/var foo = ... -> globalThis.foo = ...
return code.replace(
const normalized = code.replace(
/^\s*(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*([^\n]+?)\s*(?:;\s*)?$/gm,
(_full: string, name: string, valueExpr: string) => `globalThis.${name} = ${valueExpr}`,
)
return this.rewriteTrailingExpressionToReturn(normalized)
}
private rewriteTrailingExpressionToReturn(code: string): string {
if (/\breturn\b/.test(code))
return code
const lines = code.split('\n')
let lastIndex = -1
for (let i = lines.length - 1; i >= 0; i--) {
if (lines[i].trim().length > 0) {
lastIndex = i
break
}
}
if (lastIndex < 0)
return code
const rawLine = lines[lastIndex]
const trimmedLine = rawLine.trim()
if (trimmedLine.length === 0 || trimmedLine.startsWith('//'))
return code
if (trimmedLine === '}' || trimmedLine.endsWith('{'))
return code
const expression = trimmedLine.replace(/;$/, '').trim()
const blocked = /^(?:if|for|while|switch|const|let|var|function|class|try|catch|finally|return|throw|import|export)\b/
if (blocked.test(expression))
return code
const indent = rawLine.match(/^\s*/)?.[0] ?? ''
lines[lastIndex] = `${indent}return (${expression})`
return lines.join('\n')
}
private toDebugReplActions(actions: Array<{
@@ -766,7 +800,7 @@ export class Brain {
const codeToEvaluate = this.planner.canEvaluateAsExpression(result)
? `return (\n${result}\n)`
: result
: this.rewriteTrailingExpressionToReturn(result)
const runResult = await this.planner.evaluate(
codeToEvaluate,