From 9e31bf33d5bd74ab9523b6666621660718e3e5fb Mon Sep 17 00:00:00 2001 From: Rin Date: Wed, 4 Feb 2026 00:39:30 +0800 Subject: [PATCH] refactor(minecraft): change action return types from string to unknown and improve search actions Replace string return type with unknown in ActionRegistry/TaskExecutor to support structured responses, change searchForBlock/searchForEntity from navigation to pure search returning structured data with found/block/entity/distance fields, add validation result handling in js-planner to return failed validations as ActionRuntimeResult instead of throwing, use nullish coalescing (??) instead of OR ( --- .../src/cognitive/action/action-registry.ts | 4 +- .../src/cognitive/action/llm-actions.ts | 64 ++++++++++++++++--- .../src/cognitive/action/task-executor.ts | 6 +- .../cognitive/conscious/js-planner.test.ts | 16 +++++ .../src/cognitive/conscious/js-planner.ts | 46 +++++++++---- .../conscious/prompts/brain-prompt.ts | 28 +++++++- .../minecraft/src/libs/mineflayer/action.ts | 2 +- 7 files changed, 136 insertions(+), 30 deletions(-) diff --git a/services/minecraft/src/cognitive/action/action-registry.ts b/services/minecraft/src/cognitive/action/action-registry.ts index 85c82fd57..265e4cbb9 100644 --- a/services/minecraft/src/cognitive/action/action-registry.ts +++ b/services/minecraft/src/cognitive/action/action-registry.ts @@ -32,7 +32,7 @@ export class ActionRegistry { /** * Perform an action by name */ - public async performAction(step: { description?: string, tool: string, params: any }): Promise { + public async performAction(step: { description?: string, tool: string, params: any }): Promise { if (!this.mineflayer) { throw new Error('Mineflayer instance not set in ActionRegistry') } @@ -51,7 +51,7 @@ export class ActionRegistry { const paramValues = Object.keys((schema as any).shape || {}).map(key => parsedParams[key]) const result = await actionFn(...paramValues) - return result || `Action ${step.tool} completed` + return result ?? `Action ${step.tool} completed` } catch (error) { throw error diff --git a/services/minecraft/src/cognitive/action/llm-actions.ts b/services/minecraft/src/cognitive/action/llm-actions.ts index bd40f1013..90dd15ea2 100644 --- a/services/minecraft/src/cognitive/action/llm-actions.ts +++ b/services/minecraft/src/cognitive/action/llm-actions.ts @@ -181,28 +181,72 @@ export const actionsList: Action[] = [ }, { name: 'searchForBlock', - description: 'Find and go to the nearest block of a given type in a given range.', + description: 'Find the nearest block of a given type in a given range and return its coordinates.', execution: 'async', schema: z.object({ - type: z.string().describe('The block type to go to.'), - search_range: z.number().describe('The range to search for the block.').min(32).max(512), + type: z.string().describe('The block type to search for.'), + search_range: z.number().describe('The range to search for the block.').min(1).max(512), }), perform: mineflayer => async (block_type: string, range: number) => { - const block = await skills.goToNearestBlock(mineflayer, block_type, 4, range) - return `Arrived at nearest [${block.name}] at (${block.position.x}, ${block.position.y}, ${block.position.z})` // TODO more spacial context? + const block = world.getNearestBlock(mineflayer, block_type, range) + if (!block) { + return { + found: false, + query: { type: block_type, range }, + } + } + + const distance = mineflayer.bot.entity.position.distanceTo(block.position) + return { + found: true, + block: { + name: block.name, + position: { + x: block.position.x, + y: block.position.y, + z: block.position.z, + }, + }, + distance, + } }, }, { name: 'searchForEntity', - description: 'Find and go to the nearest entity of a given type in a given range.', + description: 'Find the nearest entity of a given type in a given range and return its coordinates.', execution: 'async', schema: z.object({ - type: z.string().describe('The type of entity to go to.'), - search_range: z.number().describe('The range to search for the entity.').min(32).max(512), + type: z.string().describe('The type of entity to search for.'), + search_range: z.number().describe('The range to search for the entity.').min(1).max(512), }), perform: mineflayer => async (entity_type: string, range: number) => { - await skills.goToNearestEntity(mineflayer, entity_type, 4, range) - return `Arrived at nearest [${entity_type}]` + const entity = world.getNearestEntityWhere( + mineflayer, + current => current.name === entity_type, + range, + ) + + if (!entity) { + return { + found: false, + query: { type: entity_type, range }, + } + } + + const distance = mineflayer.bot.entity.position.distanceTo(entity.position) + return { + found: true, + entity: { + name: entity.name, + type: entity.type, + position: { + x: entity.position.x, + y: entity.position.y, + z: entity.position.z, + }, + }, + distance, + } }, }, // { diff --git a/services/minecraft/src/cognitive/action/task-executor.ts b/services/minecraft/src/cognitive/action/task-executor.ts index 4bb7618bd..f6dbe5b8b 100644 --- a/services/minecraft/src/cognitive/action/task-executor.ts +++ b/services/minecraft/src/cognitive/action/task-executor.ts @@ -53,7 +53,7 @@ export class TaskExecutor extends EventEmitter { } } - public async executeActionWithResult(action: ActionInstruction, cancellationToken?: CancellationToken): Promise { + public async executeActionWithResult(action: ActionInstruction, cancellationToken?: CancellationToken): Promise { if (!this.initialized) { throw new Error('TaskExecutor not initialized') } @@ -66,11 +66,11 @@ export class TaskExecutor extends EventEmitter { return this.runSingleAction(action) } - private async runSingleAction(action: ActionInstruction): Promise { + private async runSingleAction(action: ActionInstruction): Promise { this.emit('action:started', { action }) try { - let result: string | void + let result: unknown if (action.tool === 'chat') { // Handle chat action via mineflayer directly diff --git a/services/minecraft/src/cognitive/conscious/js-planner.test.ts b/services/minecraft/src/cognitive/conscious/js-planner.test.ts index 94ff1a028..40739b9a3 100644 --- a/services/minecraft/src/cognitive/conscious/js-planner.test.ts +++ b/services/minecraft/src/cognitive/conscious/js-planner.test.ts @@ -90,6 +90,22 @@ describe('JavaScriptPlanner', () => { await expect(planner.evaluate('await skip(); await chat("oops")', actions, globals, executeAction)).rejects.toThrow(/skip\(\) cannot be mixed/i) }) + it('returns structured validation failures without aborting the script', async () => { + const planner = new JavaScriptPlanner() + const executeAction = vi.fn(async action => `ok:${action.tool}`) + const planned = await planner.evaluate(` + const first = await goToPlayer({ player_name: "Alex", closeness: -1 }) + if (!first.ok) { + await chat("fallback") + } + `, actions, globals, executeAction) + + expect(planned.actions[0]?.ok).toBe(false) + expect(planned.actions[0]?.error).toMatch(/Invalid tool parameters/i) + expect(executeAction).toHaveBeenCalledTimes(1) + expect(planned.actions[1]?.action.tool).toBe('chat') + }) + it('enforces timeout on long-running scripts', async () => { const planner = new JavaScriptPlanner({ timeoutMs: 20 }) const executeAction = vi.fn(async action => `ok:${action.tool}`) diff --git a/services/minecraft/src/cognitive/conscious/js-planner.ts b/services/minecraft/src/cognitive/conscious/js-planner.ts index c7395b340..db06b09eb 100644 --- a/services/minecraft/src/cognitive/conscious/js-planner.ts +++ b/services/minecraft/src/cognitive/conscious/js-planner.ts @@ -13,19 +13,24 @@ interface JavaScriptPlannerOptions { interface ActionRuntimeResult { action: ActionInstruction ok: boolean - result?: string + result?: unknown error?: string } interface ActivePlannerRun { actionCount: number actionsByName: Map - executeAction: (action: ActionInstruction) => Promise + executeAction: (action: ActionInstruction) => Promise executed: ActionRuntimeResult[] logs: string[] sawSkip: boolean } +interface ValidationResult { + action?: ActionInstruction + error?: string +} + function isRecord(value: unknown): value is Record { return typeof value === 'object' && value !== null && !Array.isArray(value) } @@ -85,7 +90,7 @@ export class JavaScriptPlanner { content: string, availableActions: Action[], globals: RuntimeGlobals, - executeAction: (action: ActionInstruction) => Promise, + executeAction: (action: ActionInstruction) => Promise, ): Promise { const script = extractJavaScriptCandidate(content) const run: ActivePlannerRun = { @@ -212,13 +217,10 @@ export class JavaScriptPlanner { this.activeRun.sawSkip = true } - const action = tool === 'skip' - ? { tool: 'skip', params: {} as Record } - : this.validateAction(tool, params) - this.activeRun.actionCount++ if (tool === 'skip') { + const action: ActionInstruction = { tool: 'skip', params: {} } const runtimeResult: ActionRuntimeResult = { action, ok: true, @@ -229,12 +231,25 @@ export class JavaScriptPlanner { return runtimeResult } + const validation = this.validateAction(tool, params) + if (!validation.action) { + const runtimeResult: ActionRuntimeResult = { + action: { tool, params }, + ok: false, + error: validation.error ?? `Invalid tool parameters for ${tool}`, + } + this.activeRun.executed.push(runtimeResult) + this.sandbox.lastAction = runtimeResult + return runtimeResult + } + const action = validation.action + try { const result = await this.activeRun.executeAction(action) const runtimeResult: ActionRuntimeResult = { action, ok: true, - result: typeof result === 'string' ? result : undefined, + result, } this.activeRun.executed.push(runtimeResult) this.sandbox.lastAction = runtimeResult @@ -248,11 +263,11 @@ export class JavaScriptPlanner { } this.activeRun.executed.push(runtimeResult) this.sandbox.lastAction = runtimeResult - throw error + return runtimeResult } } - private validateAction(tool: string, params: Record): ActionInstruction { + private validateAction(tool: string, params: Record): ValidationResult { if (!this.activeRun) throw new Error('Tool calls are only allowed during planner evaluation') @@ -260,10 +275,15 @@ export class JavaScriptPlanner { if (!action) throw new Error(`Unknown tool: ${tool}`) - return { - tool, - params: action.schema.parse(params), + const parsed = action.schema.safeParse(params) + if (!parsed.success) { + const details = parsed.error.issues.map(issue => `${issue.path.join('.') || 'root'}: ${issue.message}`).join('; ') + return { + error: `Invalid tool parameters for ${tool}: ${details}`, + } } + + return { action: { tool, params: parsed.data } } } private defineGlobalTool(name: string, fn: (...args: unknown[]) => unknown): void { diff --git a/services/minecraft/src/cognitive/conscious/prompts/brain-prompt.ts b/services/minecraft/src/cognitive/conscious/prompts/brain-prompt.ts index c15f79145..6659242ce 100644 --- a/services/minecraft/src/cognitive/conscious/prompts/brain-prompt.ts +++ b/services/minecraft/src/cognitive/conscious/prompts/brain-prompt.ts @@ -34,6 +34,31 @@ function getZodTypeName(def: any): string { return type || 'any' } +function getZodConstraintHint(def: any): string { + if (!def) + return '' + + const checks = Array.isArray(def.checks) ? def.checks : [] + const hints: string[] = [] + + for (const check of checks) { + if (check?.kind === 'min' && typeof check.value === 'number') { + hints.push(`min=${check.value}`) + } + if (check?.kind === 'max' && typeof check.value === 'number') { + hints.push(`max=${check.value}`) + } + if (check?.def?.check === 'greater_than' && typeof check.def.value === 'number') { + hints.push(`min=${check.def.inclusive ? check.def.value : check.def.value + 1}`) + } + if (check?.def?.check === 'less_than' && typeof check.def.value === 'number') { + hints.push(`max=${check.def.inclusive ? check.def.value : check.def.value - 1}`) + } + } + + return hints.length > 0 ? ` (${hints.join(', ')})` : '' +} + export function generateBrainSystemPrompt(availableActions: Action[]): string { const toolsFormatted = availableActions.map((a) => { const paramKeys = Object.keys(a.schema.shape) @@ -45,8 +70,9 @@ export function generateBrainSystemPrompt(availableActions: Action[]): string { params = Object.entries(a.schema.shape).map(([key, val]: [string, any]) => { const def = val._def const type = getZodTypeName(def) + const constraints = getZodConstraintHint(def) const desc = val.description ? ` - ${val.description}` : '' - return ` * @param {${type}} ${key}${desc}` + return ` * @param {${type}${constraints}} ${key}${desc}` }).join('\n') } diff --git a/services/minecraft/src/libs/mineflayer/action.ts b/services/minecraft/src/libs/mineflayer/action.ts index 79a80504f..033de9930 100644 --- a/services/minecraft/src/libs/mineflayer/action.ts +++ b/services/minecraft/src/libs/mineflayer/action.ts @@ -2,7 +2,7 @@ import type { z } from 'zod' import type { Mineflayer } from './core' -type ActionResult = string | Promise +type ActionResult = unknown | Promise export interface Action { readonly name: string