652 lines
18 KiB
TypeScript
652 lines
18 KiB
TypeScript
import type { Neuri } from 'neuri'
|
|
|
|
import type { Action } from '../../libs/mineflayer/action'
|
|
import type { ActionAgent, AgentConfig, MemoryAgent, Plan, PlanningAgent } from '../../libs/mineflayer/base-agent'
|
|
import type { PlanStep } from './adapter'
|
|
|
|
import { AbstractAgent } from '../../libs/mineflayer/base-agent'
|
|
import { ActionAgentImpl } from '../action'
|
|
import { PlanningLLMHandler } from './adapter'
|
|
|
|
interface PlanContext {
|
|
goal: string
|
|
currentStep: number
|
|
startTime: number
|
|
lastUpdate: number
|
|
retryCount: number
|
|
isGenerating: boolean
|
|
pendingSteps: PlanStep[]
|
|
}
|
|
|
|
export interface PlanningAgentConfig extends AgentConfig {
|
|
llm: {
|
|
agent: Neuri
|
|
model?: string
|
|
}
|
|
}
|
|
|
|
export class PlanningAgentImpl extends AbstractAgent implements PlanningAgent {
|
|
public readonly type = 'planning' as const
|
|
private currentPlan: Plan | null = null
|
|
private context: PlanContext | null = null
|
|
private actionAgent: ActionAgent | null = null
|
|
private memoryAgent: MemoryAgent | null = null
|
|
private llmConfig: PlanningAgentConfig['llm']
|
|
private llmHandler: PlanningLLMHandler
|
|
|
|
constructor(config: PlanningAgentConfig) {
|
|
super(config)
|
|
this.llmConfig = config.llm
|
|
this.llmHandler = new PlanningLLMHandler({
|
|
agent: this.llmConfig.agent,
|
|
model: this.llmConfig.model,
|
|
})
|
|
}
|
|
|
|
protected async initializeAgent(): Promise<void> {
|
|
this.logger.log('Initializing planning agent')
|
|
|
|
// Create action agent directly
|
|
this.actionAgent = new ActionAgentImpl({
|
|
id: 'action',
|
|
type: 'action',
|
|
})
|
|
await this.actionAgent.init()
|
|
|
|
// Set event listener
|
|
this.on('message', async ({ sender, message }) => {
|
|
await this.handleAgentMessage(sender, message)
|
|
})
|
|
|
|
this.on('interrupt', () => {
|
|
this.handleInterrupt()
|
|
})
|
|
}
|
|
|
|
protected async destroyAgent(): Promise<void> {
|
|
this.currentPlan = null
|
|
this.context = null
|
|
this.actionAgent = null
|
|
this.memoryAgent = null
|
|
this.removeAllListeners()
|
|
}
|
|
|
|
public async createPlan(goal: string): Promise<Plan> {
|
|
if (!this.initialized) {
|
|
throw new Error('Planning agent not initialized')
|
|
}
|
|
|
|
this.logger.withField('goal', goal).log('Creating plan')
|
|
|
|
try {
|
|
// Check memory for existing plan
|
|
const cachedPlan = await this.loadCachedPlan(goal)
|
|
if (cachedPlan) {
|
|
this.logger.log('Using cached plan')
|
|
return cachedPlan
|
|
}
|
|
|
|
// Get available actions from action agent
|
|
const availableActions = this.actionAgent?.getAvailableActions() ?? []
|
|
|
|
// Check if the goal requires actions
|
|
const requirements = this.parseGoalRequirements(goal)
|
|
const requiresAction = this.doesGoalRequireAction(requirements)
|
|
|
|
// If no actions needed, return empty plan
|
|
if (!requiresAction) {
|
|
this.logger.log('Goal does not require actions')
|
|
return {
|
|
goal,
|
|
steps: [],
|
|
status: 'completed',
|
|
requiresAction: false,
|
|
}
|
|
}
|
|
|
|
// Create plan steps based on available actions and goal
|
|
const steps = await this.generatePlanSteps(goal, availableActions, 'system')
|
|
|
|
// Create new plan
|
|
const plan: Plan = {
|
|
goal,
|
|
steps,
|
|
status: 'pending',
|
|
requiresAction: true,
|
|
}
|
|
|
|
// Cache the plan
|
|
await this.cachePlan(plan)
|
|
|
|
this.currentPlan = plan
|
|
this.context = {
|
|
goal,
|
|
currentStep: 0,
|
|
startTime: Date.now(),
|
|
lastUpdate: Date.now(),
|
|
retryCount: 0,
|
|
isGenerating: false,
|
|
pendingSteps: [],
|
|
}
|
|
|
|
return plan
|
|
}
|
|
catch (error) {
|
|
this.logger.withError(error).error('Failed to create plan')
|
|
throw error
|
|
}
|
|
}
|
|
|
|
public async executePlan(plan: Plan): Promise<void> {
|
|
if (!this.initialized) {
|
|
throw new Error('Planning agent not initialized')
|
|
}
|
|
|
|
if (!plan.requiresAction) {
|
|
this.logger.log('Plan does not require actions, skipping execution')
|
|
return
|
|
}
|
|
|
|
if (!this.actionAgent) {
|
|
throw new Error('Action agent not available')
|
|
}
|
|
|
|
this.logger.withField('plan', plan).log('Executing plan')
|
|
|
|
try {
|
|
plan.status = 'in_progress'
|
|
this.currentPlan = plan
|
|
|
|
// Execute each step
|
|
for (const step of plan.steps) {
|
|
try {
|
|
this.logger.withField('step', step).log('Executing step')
|
|
await this.actionAgent.performAction(step)
|
|
}
|
|
catch (stepError) {
|
|
this.logger.withError(stepError).error('Failed to execute step')
|
|
|
|
// Attempt to adjust plan and retry
|
|
if (this.context && this.context.retryCount < 3) {
|
|
this.context.retryCount++
|
|
// Adjust plan and restart
|
|
const adjustedPlan = await this.adjustPlan(
|
|
plan,
|
|
stepError instanceof Error ? stepError.message : 'Unknown error',
|
|
'system',
|
|
)
|
|
await this.executePlan(adjustedPlan)
|
|
return
|
|
}
|
|
|
|
throw stepError
|
|
}
|
|
}
|
|
|
|
plan.status = 'completed'
|
|
}
|
|
catch (error) {
|
|
plan.status = 'failed'
|
|
throw error
|
|
}
|
|
finally {
|
|
this.context = null
|
|
}
|
|
}
|
|
|
|
// private async generateStepsStream(
|
|
// goal: string,
|
|
// availableActions: Action[],
|
|
// sender: string,
|
|
// ): Promise<void> {
|
|
// if (!this.context) {
|
|
// return
|
|
// }
|
|
|
|
// try {
|
|
// // Generate all steps at once
|
|
// const steps = await this.llmHandler.generatePlan(goal, availableActions, sender)
|
|
// if (!this.context.isGenerating) {
|
|
// return
|
|
// }
|
|
|
|
// // Add all steps to pending queue
|
|
// this.context.pendingSteps.push(...steps)
|
|
// this.logger.withField('steps', steps).log('Generated steps')
|
|
// }
|
|
// catch (error) {
|
|
// this.logger.withError(error).error('Failed to generate steps')
|
|
// throw error
|
|
// }
|
|
// finally {
|
|
// this.context.isGenerating = false
|
|
// }
|
|
// }
|
|
|
|
// private async executeStepsStream(): Promise<void> {
|
|
// if (!this.context || !this.actionAgent) {
|
|
// return
|
|
// }
|
|
|
|
// try {
|
|
// while (this.context.isGenerating || this.context.pendingSteps.length > 0) {
|
|
// // Wait for steps to be available
|
|
// if (this.context.pendingSteps.length === 0) {
|
|
// await new Promise(resolve => setTimeout(resolve, 100))
|
|
// continue
|
|
// }
|
|
|
|
// // Execute next step
|
|
// const step = this.context.pendingSteps.shift()
|
|
// if (!step) {
|
|
// continue
|
|
// }
|
|
|
|
// try {
|
|
// this.logger.withField('step', step).log('Executing step')
|
|
// await this.actionAgent.performAction(step)
|
|
// this.context.lastUpdate = Date.now()
|
|
// this.context.currentStep++
|
|
// }
|
|
// catch (stepError) {
|
|
// this.logger.withError(stepError).error('Failed to execute step')
|
|
|
|
// // Attempt to adjust plan and retry
|
|
// if (this.context.retryCount < 3) {
|
|
// this.context.retryCount++
|
|
// // Stop current generation
|
|
// this.context.isGenerating = false
|
|
// this.context.pendingSteps = []
|
|
// // Adjust plan and restart
|
|
// const adjustedPlan = await this.adjustPlan(
|
|
// this.currentPlan!,
|
|
// stepError instanceof Error ? stepError.message : 'Unknown error',
|
|
// 'system',
|
|
// )
|
|
// await this.executePlan(adjustedPlan)
|
|
// return
|
|
// }
|
|
|
|
// throw stepError
|
|
// }
|
|
// }
|
|
// }
|
|
// catch (error) {
|
|
// this.logger.withError(error).error('Failed to execute steps')
|
|
// throw error
|
|
// }
|
|
// }
|
|
|
|
// private async *createStepGenerator(
|
|
// goal: string,
|
|
// availableActions: Action[],
|
|
// ): AsyncGenerator<PlanStep[], void, unknown> {
|
|
// // Use LLM to generate plan in chunks
|
|
// this.logger.log('Generating plan using LLM')
|
|
// const chunkSize = 3 // Generate 3 steps at a time
|
|
// let currentChunk = 1
|
|
|
|
// while (true) {
|
|
// const steps = await this.llmHandler.generatePlan(
|
|
// goal,
|
|
// availableActions,
|
|
// `Generate steps ${currentChunk * chunkSize - 2} to ${currentChunk * chunkSize}`,
|
|
// )
|
|
|
|
// if (steps.length === 0) {
|
|
// break
|
|
// }
|
|
|
|
// yield steps
|
|
// currentChunk++
|
|
|
|
// // Check if we've generated enough steps or if the goal is achieved
|
|
// if (steps.length < chunkSize || await this.isGoalAchieved(goal)) {
|
|
// break
|
|
// }
|
|
// }
|
|
// }
|
|
|
|
// private async isGoalAchieved(goal: string): Promise<boolean> {
|
|
// if (!this.context || !this.actionAgent) {
|
|
// return false
|
|
// }
|
|
|
|
// const requirements = this.parseGoalRequirements(goal)
|
|
|
|
// // Check inventory for required items
|
|
// if (requirements.needsItems && requirements.items) {
|
|
// const inventorySteps = this.generateGatheringSteps(requirements.items)
|
|
// if (inventorySteps.length > 0) {
|
|
// this.context.pendingSteps.push(...inventorySteps)
|
|
// return false
|
|
// }
|
|
// }
|
|
|
|
// // Check location requirements
|
|
// if (requirements.needsMovement && requirements.location) {
|
|
// const movementSteps = this.generateMovementSteps(requirements.location)
|
|
// if (movementSteps.length > 0) {
|
|
// this.context.pendingSteps.push(...movementSteps)
|
|
// return false
|
|
// }
|
|
// }
|
|
|
|
// // Check interaction requirements
|
|
// if (requirements.needsInteraction && requirements.target) {
|
|
// const interactionSteps = this.generateInteractionSteps(requirements.target)
|
|
// if (interactionSteps.length > 0) {
|
|
// this.context.pendingSteps.push(...interactionSteps)
|
|
// return false
|
|
// }
|
|
// }
|
|
|
|
// return true
|
|
// }
|
|
|
|
public async adjustPlan(plan: Plan, feedback: string, sender: string): Promise<Plan> {
|
|
if (!this.initialized) {
|
|
throw new Error('Planning agent not initialized')
|
|
}
|
|
|
|
this.logger.withFields({ plan, feedback }).log('Adjusting plan')
|
|
|
|
try {
|
|
// If there's a current context, use it to adjust the plan
|
|
if (this.context) {
|
|
const currentStep = this.context.currentStep
|
|
const availableActions = this.actionAgent?.getAvailableActions() ?? []
|
|
|
|
// Generate recovery steps based on feedback
|
|
const recoverySteps = this.generateRecoverySteps(feedback)
|
|
|
|
// Generate new steps from the current point
|
|
const newSteps = await this.generatePlanSteps(plan.goal, availableActions, sender, feedback)
|
|
|
|
// Create adjusted plan
|
|
const adjustedPlan: Plan = {
|
|
goal: plan.goal,
|
|
steps: [
|
|
...plan.steps.slice(0, currentStep),
|
|
...recoverySteps,
|
|
...newSteps,
|
|
],
|
|
status: 'pending',
|
|
requiresAction: true,
|
|
}
|
|
|
|
return adjustedPlan
|
|
}
|
|
|
|
// If no context, create a new plan
|
|
return this.createPlan(plan.goal)
|
|
}
|
|
catch (error) {
|
|
this.logger.withError(error).error('Failed to adjust plan')
|
|
throw error
|
|
}
|
|
}
|
|
|
|
// private generateGatheringSteps(items: string[]): PlanStep[] {
|
|
// const steps: PlanStep[] = []
|
|
|
|
// for (const item of items) {
|
|
// steps.push(
|
|
// {
|
|
// description: `Search for ${item} in the surrounding area`,
|
|
// tool: 'searchForBlock',
|
|
// params: {
|
|
// blockType: item,
|
|
// range: 64,
|
|
// },
|
|
// },
|
|
// {
|
|
// description: `Collect ${item} from the found location`,
|
|
// tool: 'collectBlocks',
|
|
// params: {
|
|
// blockType: item,
|
|
// count: 1,
|
|
// },
|
|
// },
|
|
// )
|
|
// }
|
|
|
|
// return steps
|
|
// }
|
|
|
|
// private generateMovementSteps(location: { x?: number, y?: number, z?: number }): PlanStep[] {
|
|
// if (location.x !== undefined && location.y !== undefined && location.z !== undefined) {
|
|
// return [{
|
|
// description: `Move to coordinates (${location.x}, ${location.y}, ${location.z})`,
|
|
// tool: 'goToCoordinates',
|
|
// params: {
|
|
// x: location.x,
|
|
// y: location.y,
|
|
// z: location.z,
|
|
// },
|
|
// }]
|
|
// }
|
|
// return []
|
|
// }
|
|
|
|
// private generateInteractionSteps(target: string): PlanStep[] {
|
|
// return [{
|
|
// description: `Interact with ${target}`,
|
|
// tool: 'activate',
|
|
// params: {
|
|
// target,
|
|
// },
|
|
// }]
|
|
// }
|
|
|
|
private generateRecoverySteps(feedback: string): PlanStep[] {
|
|
const steps: PlanStep[] = []
|
|
|
|
if (feedback.includes('not found')) {
|
|
steps.push({
|
|
description: 'Search in a wider area',
|
|
tool: 'searchForBlock',
|
|
params: {
|
|
blockType: 'oak_log',
|
|
range: 64,
|
|
},
|
|
})
|
|
}
|
|
|
|
if (feedback.includes('inventory full')) {
|
|
steps.push({
|
|
description: 'Clear inventory space',
|
|
tool: 'discard',
|
|
params: {
|
|
blockType: 'oak_log',
|
|
count: 1,
|
|
},
|
|
})
|
|
}
|
|
|
|
if (feedback.includes('blocked') || feedback.includes('cannot reach')) {
|
|
steps.push({
|
|
description: 'Move away from obstacles',
|
|
tool: 'moveAway',
|
|
params: {
|
|
range: 64,
|
|
},
|
|
})
|
|
}
|
|
|
|
if (feedback.includes('too far')) {
|
|
steps.push({
|
|
description: 'Move closer to target',
|
|
tool: 'moveAway',
|
|
params: {
|
|
range: 64,
|
|
},
|
|
})
|
|
}
|
|
|
|
if (feedback.includes('need tool')) {
|
|
steps.push(
|
|
{
|
|
description: 'Craft a wooden pickaxe',
|
|
tool: 'craftRecipe',
|
|
params: {
|
|
recipe: 'oak_pickaxe',
|
|
},
|
|
},
|
|
{
|
|
description: 'Equip the wooden pickaxe',
|
|
tool: 'equip',
|
|
params: {
|
|
item: 'oak_pickaxe',
|
|
},
|
|
},
|
|
)
|
|
}
|
|
|
|
return steps
|
|
}
|
|
|
|
private async loadCachedPlan(goal: string): Promise<Plan | null> {
|
|
if (!this.memoryAgent)
|
|
return null
|
|
|
|
const cachedPlan = this.memoryAgent.recall<Plan>(`plan:${goal}`)
|
|
if (cachedPlan && this.isPlanValid(cachedPlan)) {
|
|
return cachedPlan
|
|
}
|
|
return null
|
|
}
|
|
|
|
private async cachePlan(plan: Plan): Promise<void> {
|
|
if (!this.memoryAgent)
|
|
return
|
|
|
|
this.memoryAgent.remember(`plan:${plan.goal}`, plan)
|
|
}
|
|
|
|
private isPlanValid(_plan: Plan): boolean {
|
|
// Add validation logic here
|
|
return true
|
|
}
|
|
|
|
private async handleAgentMessage(sender: string, message: string): Promise<void> {
|
|
if (sender === 'system') {
|
|
if (message.includes('interrupt')) {
|
|
this.handleInterrupt()
|
|
}
|
|
}
|
|
else {
|
|
// Process message and potentially adjust plan
|
|
this.logger.withFields({ sender, message }).log('Processing agent message')
|
|
|
|
// If there's a current plan, try to adjust it based on the message
|
|
if (this.currentPlan) {
|
|
await this.adjustPlan(this.currentPlan, message, sender)
|
|
}
|
|
}
|
|
}
|
|
|
|
private handleInterrupt(): void {
|
|
if (this.currentPlan) {
|
|
this.currentPlan.status = 'failed'
|
|
this.context = null
|
|
}
|
|
}
|
|
|
|
private doesGoalRequireAction(requirements: ReturnType<typeof this.parseGoalRequirements>): boolean {
|
|
// Check if any requirement indicates need for action
|
|
return requirements.needsItems
|
|
|| requirements.needsMovement
|
|
|| requirements.needsInteraction
|
|
|| requirements.needsCrafting
|
|
|| requirements.needsCombat
|
|
}
|
|
|
|
private async generatePlanSteps(
|
|
goal: string,
|
|
availableActions: Action[],
|
|
sender: string,
|
|
feedback?: string,
|
|
): Promise<PlanStep[]> {
|
|
// Generate all steps at once
|
|
this.logger.log('Generating plan using LLM')
|
|
return await this.llmHandler.generatePlan(goal, availableActions, sender, feedback)
|
|
}
|
|
|
|
private parseGoalRequirements(goal: string): {
|
|
needsItems: boolean
|
|
items?: string[]
|
|
needsMovement: boolean
|
|
location?: { x?: number, y?: number, z?: number }
|
|
needsInteraction: boolean
|
|
target?: string
|
|
needsCrafting: boolean
|
|
needsCombat: boolean
|
|
} {
|
|
const requirements = {
|
|
needsItems: false,
|
|
items: [] as string[],
|
|
needsMovement: false,
|
|
location: undefined as { x?: number, y?: number, z?: number } | undefined,
|
|
needsInteraction: false,
|
|
target: undefined as string | undefined,
|
|
needsCrafting: false,
|
|
needsCombat: false,
|
|
}
|
|
|
|
const goalLower = goal.toLowerCase()
|
|
|
|
// Extract items from goal
|
|
const itemMatches = goalLower.match(/(collect|get|find|craft|make|build|use|equip) (\w+)/g)
|
|
if (itemMatches) {
|
|
requirements.needsItems = true
|
|
requirements.items = itemMatches.map(match => match.split(' ')[1])
|
|
}
|
|
|
|
// Extract location from goal
|
|
const locationMatches = goalLower.match(/(go to|move to|at) (\d+)[, ]+(\d+)[, ]+(\d+)/g)
|
|
if (locationMatches) {
|
|
requirements.needsMovement = true
|
|
const [x, y, z] = locationMatches[0].split(/[, ]+/).slice(-3).map(Number)
|
|
requirements.location = { x, y, z }
|
|
}
|
|
|
|
// Extract target from goal
|
|
const targetMatches = goalLower.match(/(interact with|use|open|activate) (\w+)/g)
|
|
if (targetMatches) {
|
|
requirements.needsInteraction = true
|
|
requirements.target = targetMatches[0].split(' ').pop()
|
|
}
|
|
|
|
// Check for item-related actions
|
|
if (goalLower.includes('collect') || goalLower.includes('get') || goalLower.includes('find')) {
|
|
requirements.needsItems = true
|
|
requirements.needsMovement = true
|
|
}
|
|
|
|
// Check for movement-related actions
|
|
if (goalLower.includes('go to') || goalLower.includes('move to') || goalLower.includes('follow')) {
|
|
requirements.needsMovement = true
|
|
}
|
|
|
|
// Check for interaction-related actions
|
|
if (goalLower.includes('interact') || goalLower.includes('use') || goalLower.includes('open')) {
|
|
requirements.needsInteraction = true
|
|
}
|
|
|
|
// Check for crafting-related actions
|
|
if (goalLower.includes('craft') || goalLower.includes('make') || goalLower.includes('build')) {
|
|
requirements.needsCrafting = true
|
|
requirements.needsItems = true
|
|
}
|
|
|
|
// Check for combat-related actions
|
|
if (goalLower.includes('attack') || goalLower.includes('fight') || goalLower.includes('kill')) {
|
|
requirements.needsCombat = true
|
|
requirements.needsMovement = true
|
|
}
|
|
|
|
return requirements
|
|
}
|
|
}
|