feat(minecraft): run conscious brain in js repl with runtime globals

This commit is contained in:
Rin
2026-02-18 11:14:37 +08:00
committed by Neko Ayaka
parent 4e1ba4537a
commit b02d4a9b8b
8 changed files with 277 additions and 222 deletions
@@ -7,7 +7,6 @@ import { collectBlock } from '../../skills/actions/collect-block'
import { discard, equip, putInChest, takeFromChest } from '../../skills/actions/inventory'
import { activateNearestBlock, breakBlockAt, placeBlock } from '../../skills/actions/world-interactions'
import { ActionError } from '../../utils/errors'
import { javascriptRepl } from '../../utils/javascript-repl'
import { useLogger } from '../../utils/logger'
import { describeRecipePlan, planRecipe } from '../../utils/recipe-planner'
@@ -38,15 +37,6 @@ export const actionsList: Action[] = [
return `Sent message: "${message}"`
},
},
{
name: 'eval',
description: 'Evaluate JavaScript code in a persistent REPL context. Use this for quick calculations or temporary memory.',
execution: 'sync',
schema: z.object({
code: z.string().min(1).describe('JavaScript source code to evaluate.'),
}),
perform: () => (code: string): string => javascriptRepl.evaluate(code),
},
// {\n // name: 'setReflexMode',
// description: 'Set (or clear) your reflex mode override. Use work/wander to disable idle-only reflex behaviors. Set override to null to return to automatic mode selection.',
// execution: 'sequential',
@@ -45,24 +45,28 @@ export class TaskExecutor extends EventEmitter {
}
public async executeAction(action: ActionInstruction, cancellationToken?: CancellationToken): Promise<void> {
if (!this.initialized) {
throw new Error('TaskExecutor not initialized')
}
if (cancellationToken?.isCancelled) {
this.logger.log('Action execution cancelled before start')
return
}
try {
await this.runSingleAction(action)
await this.executeActionWithResult(action, cancellationToken)
}
catch (error) {
// Errors handled in runSingleAction event emission
}
}
private async runSingleAction(action: ActionInstruction): Promise<void> {
public async executeActionWithResult(action: ActionInstruction, cancellationToken?: CancellationToken): Promise<string | void> {
if (!this.initialized) {
throw new Error('TaskExecutor not initialized')
}
if (cancellationToken?.isCancelled) {
this.logger.log('Action execution cancelled before start')
return 'Action cancelled'
}
return this.runSingleAction(action)
}
private async runSingleAction(action: ActionInstruction): Promise<string | void> {
this.emit('action:started', { action })
try {
@@ -79,7 +83,7 @@ export class TaskExecutor extends EventEmitter {
}
this.mineflayer.bot.chat(message)
result = 'Message sent'
result = `Sent message: "${message}"`
}
else if (action.tool === 'skip') {
result = 'Skipped turn'
@@ -96,6 +100,7 @@ export class TaskExecutor extends EventEmitter {
}
this.emit('action:completed', { action, result })
return result
}
catch (error) {
this.logger.withError(error).error('Action execution failed')
@@ -14,7 +14,6 @@ import { buildConsciousContextView } from './context-view'
import { JavaScriptPlanner } from './js-planner'
import { LLMAgent } from './llm-agent'
import {
extractJsonCandidate,
isLikelyAuthOrBadArgError,
isRateLimitError,
sleep,
@@ -23,10 +22,6 @@ import {
import { generateBrainSystemPrompt } from './prompts/brain-prompt'
import { createCancellationToken, type CancellationToken } from './task-state'
interface BrainResponse {
action: ActionInstruction & { id?: string }
}
interface BrainDeps {
eventBus: EventBus
llmAgent: LLMAgent
@@ -73,9 +68,6 @@ export class Brain {
this.deps.taskExecutor.on('action:completed', async ({ action, result }) => {
this.deps.logger.log('INFO', `Brain: Action completed: ${action.tool}`)
// Suppress feedback for chat actions on success
if (action.tool === 'chat') return
this.enqueueEvent(bot, {
type: 'feedback',
payload: { status: 'success', action, result },
@@ -231,8 +223,6 @@ export class Brain {
}
try {
const actions = this.parseResponse(result)
// Only append to conversation history after successful parsing (avoid dirty data on retry)
this.conversationHistory.push({ role: 'user', content: userMessage })
// Store reasoning in the assistant message's reasoning field (if available)
@@ -243,17 +233,46 @@ export class Brain {
...(capturedReasoning && { reasoning: capturedReasoning }),
} as Message)
if (actions.length === 1 && actions[0]?.tool === 'skip') {
const actionDefs = new Map(this.deps.taskExecutor.getAvailableActions().map(action => [action.name, action]))
let turnCancellationToken: CancellationToken | undefined
const runResult = await this.planner.evaluate(
result,
this.deps.taskExecutor.getAvailableActions(),
{ event, snapshot: snapshot as unknown as Record<string, unknown> },
async (action: ActionInstruction) => {
const actionDef = actionDefs.get(action.tool)
const isPhysicalAction = action.tool !== 'skip' && !actionDef?.readonly
if (isPhysicalAction) {
if (!turnCancellationToken) {
this.currentCancellationToken?.cancel()
this.currentCancellationToken = createCancellationToken()
turnCancellationToken = this.currentCancellationToken
}
return this.deps.taskExecutor.executeActionWithResult(action, turnCancellationToken)
}
return this.deps.taskExecutor.executeActionWithResult(action)
},
)
if (runResult.actions.length === 0 || runResult.actions.every(item => item.action.tool === 'skip')) {
this.deps.logger.log('INFO', 'Brain: Skipping turn (observing)')
return
}
this.deps.logger.log('INFO', `Brain: Planned ${actions.length} action(s)`, {
actions: actions.map(action => ({ tool: action.tool, params: action.params })),
this.deps.logger.log('INFO', `Brain: Executed ${runResult.actions.length} action(s)`, {
actions: runResult.actions.map(item => ({
tool: item.action.tool,
ok: item.ok,
result: item.result,
error: item.error,
})),
logs: runResult.logs,
returnValue: runResult.returnValue,
})
this.executePlannedActions(actions)
} catch (err) {
this.deps.logger.withError(err).error('Brain: Failed to execute decision')
void this.enqueueEvent(bot, {
@@ -295,59 +314,8 @@ export class Brain {
// Note: We don't update this.lastContextView here; caller does it after building message
}
parts.push('[RUNTIME] Globals are refreshed every turn: snapshot, self, environment, social, threat, attention, event, now, mem, lastRun, lastAction.')
return parts.join('\n\n')
}
private parseResponse(content: string): ActionInstruction[] {
const availableActions = this.deps.taskExecutor.getAvailableActions()
const trimmed = content.trim()
if (trimmed.startsWith('{')) {
try {
const parsed = JSON.parse(extractJsonCandidate(content)) as BrainResponse
if (parsed?.action?.tool === 'skip') {
return [{ tool: 'skip', params: {} }]
}
if (parsed?.action?.tool) {
const actionDef = availableActions.find(action => action.name === parsed.action.tool)
if (!actionDef) {
throw new Error(`Unknown tool in legacy JSON response: ${parsed.action.tool}`)
}
return [{
tool: actionDef.name,
params: actionDef.schema.parse(parsed.action.params ?? {}),
}]
}
} catch {
// Fallback to JS planner parsing.
}
}
return this.planner.evaluate(content, availableActions)
}
private executePlannedActions(actions: ActionInstruction[]): void {
const availableActions = this.deps.taskExecutor.getAvailableActions()
const actionDefs = new Map(availableActions.map(action => [action.name, action]))
const hasNonReadonlyAction = actions.some(action => !actionDefs.get(action.tool)?.readonly && action.tool !== 'skip')
let token: CancellationToken | undefined
if (hasNonReadonlyAction) {
if (this.currentCancellationToken) {
this.currentCancellationToken.cancel()
}
this.currentCancellationToken = createCancellationToken()
token = this.currentCancellationToken
}
void (async () => {
for (const action of actions) {
if (token?.isCancelled)
return
await this.deps.taskExecutor.executeAction(action, token)
}
})()
}
}
@@ -1,6 +1,6 @@
import type { Action } from '../../libs/mineflayer/action'
import { describe, expect, it } from 'vitest'
import { describe, expect, it, vi } from 'vitest'
import { z } from 'zod'
import { JavaScriptPlanner } from './js-planner'
@@ -24,51 +24,76 @@ const actions: Action[] = [
]
describe('JavaScriptPlanner', () => {
it('maps positional and object tool args into validated action instructions', () => {
const planner = new JavaScriptPlanner()
const planned = planner.evaluate(`
chat("hello")
goToPlayer({ player_name: "Alex", closeness: 2 })
`, actions)
const globals = {
event: {
type: 'perception',
payload: { type: 'chat_message' },
source: { type: 'minecraft', id: 'test' },
timestamp: Date.now(),
},
snapshot: {
self: { health: 20, food: 20, location: { x: 0, y: 64, z: 0 } },
environment: { nearbyPlayers: [] },
social: {},
threat: {},
attention: {},
},
} as any
expect(planned).toEqual([
it('maps positional/object args and executes tools in order', async () => {
const planner = new JavaScriptPlanner()
const executeAction = vi.fn(async action => `ok:${action.tool}`)
const planned = await planner.evaluate(`
await chat("hello")
await goToPlayer({ player_name: "Alex", closeness: 2 })
`, actions, globals, executeAction)
expect(executeAction).toHaveBeenCalledTimes(2)
expect(executeAction).toHaveBeenNthCalledWith(1, { tool: 'chat', params: { message: 'hello' } })
expect(executeAction).toHaveBeenNthCalledWith(2, { tool: 'goToPlayer', params: { player_name: 'Alex', closeness: 2 } })
expect(planned.actions.map(a => a.action)).toEqual([
{ tool: 'chat', params: { message: 'hello' } },
{ tool: 'goToPlayer', params: { player_name: 'Alex', closeness: 2 } },
])
})
it('supports dynamic dispatch with use(toolName, params)', () => {
it('supports dynamic dispatch with use(toolName, params)', async () => {
const planner = new JavaScriptPlanner()
const planned = planner.evaluate(`use("chat", { message: "via-use" })`, actions)
const executeAction = vi.fn(async action => `ok:${action.tool}`)
const planned = await planner.evaluate(`await use("chat", { message: "via-use" })`, actions, globals, executeAction)
expect(planned).toEqual([{ tool: 'chat', params: { message: 'via-use' } }])
expect(planned.actions.map(a => a.action)).toEqual([{ tool: 'chat', params: { message: 'via-use' } }])
})
it('persists script variables across turns', () => {
it('persists script variables across turns with mem', async () => {
const planner = new JavaScriptPlanner()
const executeAction = vi.fn(async action => `ok:${action.tool}`)
planner.evaluate('const count = 2', actions)
const planned = planner.evaluate('chat("count=" + count)', actions)
await planner.evaluate('mem.count = 2', actions, globals, executeAction)
const planned = await planner.evaluate('await chat("count=" + mem.count)', actions, globals, executeAction)
expect(planned).toEqual([{ tool: 'chat', params: { message: 'count=2' } }])
expect(planned.actions.map(a => a.action)).toEqual([{ tool: 'chat', params: { message: 'count=2' } }])
})
it('returns skip when no tool is called', () => {
it('provides snapshot globals in script scope', async () => {
const planner = new JavaScriptPlanner()
const planned = planner.evaluate('const x = 1 + 1', actions)
const executeAction = vi.fn(async action => `ok:${action.tool}`)
const planned = await planner.evaluate('await chat("hp=" + self.health)', actions, globals, executeAction)
expect(planned).toEqual([{ tool: 'skip', params: {} }])
expect(planned.actions.map(a => a.action)).toEqual([{ tool: 'chat', params: { message: 'hp=20' } }])
})
it('rejects mixed skip + tool calls', () => {
it('rejects mixed skip + tool calls', async () => {
const planner = new JavaScriptPlanner()
const executeAction = vi.fn(async action => `ok:${action.tool}`)
expect(() => planner.evaluate('skip(); chat("oops")', actions)).toThrow(/skip\(\) cannot be mixed/i)
await expect(planner.evaluate('await skip(); await chat("oops")', actions, globals, executeAction)).rejects.toThrow(/skip\(\) cannot be mixed/i)
})
it('enforces timeout on long-running scripts', () => {
it('enforces timeout on long-running scripts', async () => {
const planner = new JavaScriptPlanner({ timeoutMs: 20 })
const executeAction = vi.fn(async action => `ok:${action.tool}`)
expect(() => planner.evaluate('while (true) {}', actions)).toThrow(/Script execution timed out/i)
await expect(planner.evaluate('while (true) {}', actions, globals, executeAction)).rejects.toThrow(/Script execution timed out/i)
})
})
@@ -1,5 +1,6 @@
import type { Action } from '../../libs/mineflayer/action'
import type { ActionInstruction } from '../action/types'
import type { BotEvent } from '../types'
import { inspect } from 'node:util'
import vm from 'node:vm'
@@ -9,21 +10,53 @@ interface JavaScriptPlannerOptions {
maxActionsPerTurn?: number
}
interface ActionIntent {
index: number
params: Record<string, unknown>
tool: string
interface ActionRuntimeResult {
action: ActionInstruction
ok: boolean
result?: string
error?: string
}
interface ActivePlannerRun {
actions: ActionInstruction[]
actionCount: number
actionsByName: Map<string, Action>
executeAction: (action: ActionInstruction) => Promise<string | void>
executed: ActionRuntimeResult[]
logs: string[]
sawSkip: boolean
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value)
}
function deepFreeze<T>(value: T): T {
if (!value || typeof value !== 'object')
return value
for (const key of Object.keys(value as Record<string, unknown>)) {
const child = (value as Record<string, unknown>)[key]
deepFreeze(child)
}
return Object.freeze(value)
}
function toStructuredClone<T>(value: T): T {
return JSON.parse(JSON.stringify(value)) as T
}
export interface RuntimeGlobals {
event: BotEvent
snapshot: Record<string, unknown>
}
export interface JavaScriptRunResult {
actions: ActionRuntimeResult[]
logs: string[]
returnValue?: string
}
export function extractJavaScriptCandidate(input: string): string {
const trimmed = input.trim()
const fenced = trimmed.match(/^```(?:js|javascript|ts|typescript)?\s*([\s\S]*?)\s*```$/i)
@@ -48,59 +81,94 @@ export class JavaScriptPlanner {
this.installBuiltins()
}
public evaluate(content: string, availableActions: Action[]): ActionInstruction[] {
public async evaluate(
content: string,
availableActions: Action[],
globals: RuntimeGlobals,
executeAction: (action: ActionInstruction) => Promise<string | void>,
): Promise<JavaScriptRunResult> {
const script = extractJavaScriptCandidate(content)
const run: ActivePlannerRun = {
actions: [],
actionCount: 0,
actionsByName: new Map(availableActions.map(action => [action.name, action])),
executeAction,
executed: [],
logs: [],
sawSkip: false,
}
this.activeRun = run
this.installActionTools(availableActions)
this.bindRuntimeGlobals(globals, run)
try {
const result = new vm.Script(script).runInContext(this.context, { timeout: this.timeoutMs })
if (typeof result !== 'undefined' && run.actions.length === 0) {
// Keep this visible in traces for debugging, without affecting behavior.
this.sandbox.__lastEvalResult = inspect(result, { depth: 2, breakLength: 100 })
const wrapped = `(async () => {\n${script}\n})()`
const result = await new vm.Script(wrapped).runInContext(this.context, { timeout: this.timeoutMs })
const returnValue = typeof result === 'undefined'
? undefined
: inspect(result, { depth: 2, breakLength: 100 })
return {
actions: run.executed,
logs: run.logs,
returnValue,
}
}
finally {
this.activeRun = null
}
if (run.actions.length === 0)
return [{ tool: 'skip', params: {} }]
const containsSkip = run.actions.some(action => action.tool === 'skip')
if (containsSkip && run.actions.length > 1) {
throw new Error('skip() cannot be mixed with other tool calls in the same script')
}
return run.actions
}
private installBuiltins(): void {
this.defineGlobalTool('skip', () => this.enqueueAction('skip', {}))
this.defineGlobalTool('skip', async () => this.runAction('skip', {}))
this.defineGlobalTool('use', (toolName: unknown, params?: unknown) => {
if (typeof toolName !== 'string' || toolName.length === 0) {
throw new Error('use(toolName, params) requires a non-empty string toolName')
}
const mappedParams = isRecord(params) ? params : {}
return this.enqueueAction(toolName, mappedParams)
return this.runAction(toolName, mappedParams)
})
this.defineGlobalTool('log', (...args: unknown[]) => {
if (!this.activeRun)
throw new Error('log() is only allowed during planner evaluation')
const rendered = args.map(arg => inspect(arg, { depth: 4, breakLength: 120 })).join(' ')
this.activeRun.logs.push(rendered)
return rendered
})
this.defineGlobalValue('mem', {})
}
private installActionTools(availableActions: Action[]): void {
for (const action of availableActions) {
this.defineGlobalTool(action.name, (...args: unknown[]) => {
this.defineGlobalTool(action.name, async (...args: unknown[]) => {
const params = this.mapArgsToParams(action, args)
return this.enqueueAction(action.name, params)
return this.runAction(action.name, params)
})
}
}
private bindRuntimeGlobals(globals: RuntimeGlobals, run: ActivePlannerRun): void {
const snapshot = deepFreeze(toStructuredClone(globals.snapshot))
const event = deepFreeze(toStructuredClone(globals.event))
this.sandbox.prevRun = this.sandbox.lastRun ?? null
this.sandbox.snapshot = snapshot
this.sandbox.event = event
this.sandbox.now = Date.now()
this.sandbox.self = snapshot.self
this.sandbox.environment = snapshot.environment
this.sandbox.social = snapshot.social
this.sandbox.threat = snapshot.threat
this.sandbox.attention = snapshot.attention
this.sandbox.lastRun = {
actions: run.executed,
logs: run.logs,
}
}
private mapArgsToParams(action: Action, args: unknown[]): Record<string, unknown> {
const shape = action.schema.shape as Record<string, unknown>
const keys = Object.keys(shape)
@@ -127,44 +195,87 @@ export class JavaScriptPlanner {
return params
}
private enqueueAction(tool: string, params: Record<string, unknown>): ActionIntent {
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')
}
if (this.activeRun.actions.length >= this.maxActionsPerTurn) {
if (this.activeRun.sawSkip && tool !== 'skip') {
throw new Error('skip() cannot be mixed with other tool calls in the same script')
}
if (this.activeRun.actionCount >= this.maxActionsPerTurn) {
throw new Error(`Action limit exceeded: max ${this.maxActionsPerTurn} actions per turn`)
}
if (tool !== 'skip') {
const action = this.activeRun.actionsByName.get(tool)
if (!action)
throw new Error(`Unknown tool: ${tool}`)
const parsed = action.schema.parse(params)
this.activeRun.actions.push({
tool,
params: parsed,
})
}
else {
this.activeRun.actions.push({ tool: 'skip', params: {} })
if (tool === 'skip') {
this.activeRun.sawSkip = true
}
const intent: ActionIntent = {
index: this.activeRun.actions.length - 1,
const action = tool === 'skip'
? { tool: 'skip', params: {} as Record<string, unknown> }
: this.validateAction(tool, params)
this.activeRun.actionCount++
if (tool === 'skip') {
const runtimeResult: ActionRuntimeResult = {
action,
ok: true,
result: 'Skipped turn',
}
this.activeRun.executed.push(runtimeResult)
this.sandbox.lastAction = runtimeResult
return runtimeResult
}
try {
const result = await this.activeRun.executeAction(action)
const runtimeResult: ActionRuntimeResult = {
action,
ok: true,
result: typeof result === 'string' ? result : undefined,
}
this.activeRun.executed.push(runtimeResult)
this.sandbox.lastAction = runtimeResult
return runtimeResult
}
catch (error) {
const runtimeResult: ActionRuntimeResult = {
action,
ok: false,
error: error instanceof Error ? error.message : String(error),
}
this.activeRun.executed.push(runtimeResult)
this.sandbox.lastAction = runtimeResult
throw error
}
}
private validateAction(tool: string, params: Record<string, unknown>): ActionInstruction {
if (!this.activeRun)
throw new Error('Tool calls are only allowed during planner evaluation')
const action = this.activeRun.actionsByName.get(tool)
if (!action)
throw new Error(`Unknown tool: ${tool}`)
return {
tool,
params,
params: action.schema.parse(params),
}
return Object.freeze(intent)
}
private defineGlobalTool(name: string, fn: (...args: unknown[]) => unknown): void {
this.defineGlobalValue(name, fn)
}
private defineGlobalValue(name: string, value: unknown): void {
if (Object.prototype.hasOwnProperty.call(this.sandbox, name))
return
Object.defineProperty(this.sandbox, name, {
value: fn,
value,
configurable: false,
enumerable: true,
writable: false,
@@ -66,7 +66,7 @@ You are an autonomous agent playing Minecraft.
# Self-Knowledge & Capabilities
1. **Stateful Existence**: You maintain a memory of the conversation, but it's crucial to be aware that old history messages are less relevant than recent.
2. **Action Script Per Turn**: You can output one JavaScript script each turn, and it can queue multiple tool calls.
2. **Action Script Per Turn**: You can output one JavaScript script each turn, and it can execute multiple tool calls.
3. **Interruption**: The world is real-time. Events (chat, damage, etc.) may happen *while* you are performing an action.
- If a new critical event occurs, you may need to change your plans.
- Feedback for your actions will arrive as a message starting with \`[FEEDBACK]\`.
@@ -77,13 +77,11 @@ You are an autonomous agent playing Minecraft.
- It's possible for a fresh event to reach you while you're in the middle of a action, in that case, remember the action is still running in the background.
- If the new situation requires you to change plan, you can use the stop tool to stop background actions or initiate a new one, which will automatically replace the old one.
- Feel free to send chats while background actions are running, it will not interrupt them.
6. **JavaScript Scratchpad**: The \`eval\` tool is available, you may use it as a persistent scratchpad for calculations and short-term memory.
- Values defined in \`eval\` persist across turns.
- \`eval\` has a timeout, so avoid long-running code.
- Prefer direct world tools for world interaction; use \`eval\` for reasoning support.
7. **Planner Runtime**: Your script runs in a persistent JavaScript context with a timeout.
- Tool functions (listed below) queue actions; they do not execute instantly.
- The runtime validates queued actions before execution.
6. **Planner Runtime**: Your script runs in a persistent JavaScript context with a timeout.
- Tool functions (listed below) execute actions and return results.
- Use \`await\` on tool calls when later logic depends on the result.
- Globals refreshed every turn: \`snapshot\`, \`self\`, \`environment\`, \`social\`, \`threat\`, \`attention\`, \`event\`, \`now\`.
- Persistent globals: \`mem\` (cross-turn memory), \`lastRun\` (this run), \`prevRun\` (previous run), \`lastAction\` (latest action result), \`log(...)\`.
- Maximum actions per turn: 5.
# Available Tools
@@ -94,22 +92,29 @@ ${toolsFormatted}
# Response Format
You must respond with JavaScript only (no markdown code fences).
Call tool functions directly to queue actions.
If you want to do nothing, call \`skip()\`.
Call tool functions directly.
Use \`await\` when branching on action outcomes.
If you want to do nothing, call \`await skip()\`.
You can also use \`use(toolName, paramsObject)\` for dynamic tool calls.
Examples:
- \`chat("hello")\`
- \`goToPlayer("Alex", 2)\`
- \`goToPlayer({ player_name: "Alex", closeness: 2 })\`
- \`const steps = 3; for (let i = 0; i < steps; i++) chat("step " + i)\`
- \`skip()\`
- \`await chat("hello")\`
- \`const sent = await chat("HP=" + self.health); log(sent)\`
- \`const arrived = await goToPlayer({ player_name: "Alex", closeness: 2 }); if (!arrived) await chat("failed")\`
- \`if (self.health < 10) await consume({ item_name: "bread" })\`
- \`await skip()\`
# Usage Convention (Important)
- Plan with \`mem.plan\`, execute in small steps, and verify each step before continuing.
- Treat action results as potentially unreliable; check outcomes against \`snapshot\`/feedback before committing to the next step.
- Prefer deterministic scripts: no random branching unless needed.
- Keep per-turn scripts short and focused on one tactical objective.
# Rules
- **Native Reasoning**: You can think before outputting your action.
- **Strict JavaScript Output**: Output ONLY executable JavaScript. Comments are possible but discouraged and will be ignored.
- **Handling Feedback**: When you perform an action, you will see a \`[FEEDBACK]\` message in the history later with the result. Use this to verify success.
- **Tool Choice**: If a dedicated tool exists for a task, use it. Use \`eval\` for computation, scratch memory, or inspecting previously stored variables.
- **Tool Choice**: If a dedicated tool exists for a task, use it.
- **Skip Rule**: If you call \`skip()\`, do not call any other tool in the same turn.
`
}
@@ -1,19 +0,0 @@
import { describe, expect, it } from 'vitest'
import { JavaScriptRepl } from './javascript-repl'
describe('javascript-repl', () => {
it('persists variables between eval calls', () => {
const repl = new JavaScriptRepl()
repl.evaluate('const var_foo = 6 * 7')
expect(repl.evaluate('var_foo')).toBe('42')
})
it('times out long-running scripts', () => {
const repl = new JavaScriptRepl({ timeoutMs: 20 })
expect(() => repl.evaluate('while (true) {}')).toThrow(/Script execution timed out/i)
})
})
@@ -1,30 +0,0 @@
import { inspect } from 'node:util'
import vm from 'node:vm'
interface JavaScriptReplOptions {
timeoutMs?: number
}
export class JavaScriptRepl {
private readonly context: vm.Context
private readonly timeoutMs: number
constructor(options: JavaScriptReplOptions = {}) {
this.timeoutMs = options.timeoutMs ?? 500
this.context = vm.createContext({})
}
public evaluate(code: string): string {
const script = new vm.Script(code)
const result = script.runInContext(this.context, { timeout: this.timeoutMs })
if (result === undefined)
return 'undefined'
return typeof result === 'string'
? result
: inspect(result, { depth: 4, maxArrayLength: 100, breakLength: 120 })
}
}
export const javascriptRepl = new JavaScriptRepl()