feat(minecraft): improve error handling and auto-craft intermediates...but it doesn't work very well

1. Wrap neuri tool invocations to catch ActionError and return as
   result strings instead of throwing. This allows the LLM to learn
   from tool failures during its reasoning phase.

2. Enhance craftRecipe to use the recipe planner for auto-crafting
   intermediate materials. If the bot has raw materials (e.g., logs)
   but not direct ingredients (e.g., planks), it will automatically
   craft the intermediates first.

3. Add memory note about the dual tool interface discovery (neuri
   native tool calls vs JSON actions through TaskExecutor) for future
   optimization work.
This commit is contained in:
Rin
2026-02-18 11:12:46 +08:00
committed by Neko Ayaka
parent b4f606e538
commit d9b99bdda1
2 changed files with 65 additions and 9 deletions
@@ -8,6 +8,7 @@ import { agent } from 'neuri'
import { system, user } from 'neuri/openai'
import { BaseLLMHandler } from '../../cognitive/conscious/handler'
import { ActionError } from '../../utils/errors'
import { useLogger } from '../../utils/logger'
import { generateActionSystemPrompt } from './system-prompt'
import { actionsList } from './tools'
@@ -25,7 +26,19 @@ export async function createActionNeuriAgent(mineflayer: Mineflayer): Promise<Ag
logger.withFields({ name: action.name, parameters }).log('Calling action')
mineflayer.memory.actions.push(action)
const fn = action.perform(mineflayer)
return await fn(...Object.values(parameters))
try {
return await fn(...Object.values(parameters))
}
catch (error) {
// Return ActionError as a result string instead of throwing
// This allows the LLM to learn from tool failures during its reasoning phase
if (error instanceof ActionError) {
logger.withError(error).warn('Action failed during tool call')
return `[FAILED] ${error.code}: ${error.message}${error.context ? ` (${JSON.stringify(error.context)})` : ''}`
}
// Re-throw non-ActionError errors (unexpected failures)
throw error
}
},
{ description: action.description },
)
+51 -8
View File
@@ -7,6 +7,7 @@ import type { Mineflayer } from '../libs/mineflayer'
import { ActionError } from '../utils/errors'
import { useLogger } from '../utils/logger'
import { McData } from '../utils/mcdata'
import { planRecipe } from '../utils/recipe-planner'
import { ensureCraftingTable } from './actions/ensure'
import { collectBlock, placeBlock } from './blocks'
import { goToNearestBlock, goToPosition, moveAway } from './movement'
@@ -40,8 +41,7 @@ export async function craftRecipe(
try {
await mineflayer.bot.craft(recipe, num, craftingTable ?? undefined)
logger.log(
`Successfully crafted ${num} ${itemName}${
craftingTable ? ' using crafting table' : ''
`Successfully crafted ${num} ${itemName}${craftingTable ? ' using crafting table' : ''
}.`,
)
return true
@@ -78,8 +78,7 @@ export async function craftRecipe(
}
catch (err) {
logger.log(
`Attempt ${attempts + 1} to move to crafting table failed: ${
(err as Error).message
`Attempt ${attempts + 1} to move to crafting table failed: ${(err as Error).message
}`,
)
if (err instanceof ActionError)
@@ -137,12 +136,56 @@ export async function craftRecipe(
}
}
// RECURSION GUARD:
// RECURSION GUARD + AUTO-CRAFT INTERMEDIATE MATERIALS:
// If we failed to craft basic items (planks, sticks) without a table,
// do NOT try to find a table. These items do not need a table.
// Seeking a table often triggers "ensureCraftingTable" -> "ensurePlanks" -> infinite loop.
// check if we can craft from raw materials using the recipe planner.
if (itemName.includes('planks') || itemName === 'stick' || itemName === 'crafting_table') {
logger.log(`Recursion Guard: Skipping crafting table search for basic item: ${itemName}`)
logger.log(`Recursion Guard: Checking if we can craft ${itemName} from raw materials`)
// Use the recipe planner to see if we can craft this item
const plan = planRecipe(mineflayer.bot, itemName, num)
if (plan.status === 'unknown_item') {
throw new ActionError('UNKNOWN', `Unknown item: ${itemName}`)
}
// If we can craft now (have all materials including intermediates), do it
if (plan.canCraftNow && plan.steps.length > 0) {
logger.log(`Recipe planner found craftable path with ${plan.steps.length} steps`)
// Craft all intermediate steps first (in reverse order = base materials first)
for (const step of [...plan.steps].reverse()) {
if (step.action === 'craft') {
logger.log(`Auto-crafting intermediate: ${step.amount}x ${step.item}`)
// Use direct bot.craft for intermediates to avoid infinite recursion
const stepItemId = mcData.getItemId(step.item)
if (!stepItemId) {
throw new ActionError('UNKNOWN', `Unknown intermediate item: ${step.item}`)
}
const stepRecipes = mineflayer.bot.recipesFor(stepItemId, null, 1, null)
if (stepRecipes && stepRecipes.length > 0) {
const outputPerCraft = stepRecipes[0].result?.count ?? 1
const craftCount = Math.ceil(step.amount / outputPerCraft)
await mineflayer.bot.craft(stepRecipes[0], craftCount)
logger.log(`Successfully crafted ${craftCount}x ${step.item}`)
}
}
}
return true
}
// Can't craft - provide helpful error message
if (Object.keys(plan.missing).length > 0) {
const missingList = Object.entries(plan.missing)
.map(([item, count]) => `${count}x ${item}`)
.join(', ')
throw new ActionError('RESOURCE_MISSING', `Cannot craft ${itemName} - missing: ${missingList}`, {
item: itemName,
missing: plan.missing,
})
}
throw new ActionError('RESOURCE_MISSING', `Cannot craft ${itemName} - missing ingredients`, { item: itemName })
}