feat(minecraft): architecture redesign, blackboard pattern and OODA

This commit is contained in:
Rin
2026-02-18 11:09:58 +08:00
committed by Neko Ayaka
parent 9b698ae42d
commit 8facbdacfc
14 changed files with 885 additions and 408 deletions
+39 -84
View File
@@ -106,7 +106,7 @@ You can also give the bot natural language commands, and it will try to understa
## 🧠 Cognitive Architecture
AIRI's Minecraft agent is built on a **three-layered cognitive architecture** inspired by cognitive science, enabling both reactive and deliberate behaviors. This design allows the bot to respond instantly to urgent situations while maintaining the ability to plan and execute complex tasks.
AIRI's Minecraft agent is built on a **four-layered cognitive architecture** inspired by cognitive science, enabling reactive, conscious, and physically grounded behaviors.
### Architecture Overview
@@ -124,136 +124,91 @@ graph TB
RM --> FSM
end
subgraph "Layer C: Conscious"
subgraph "Layer C: Conscious (Reasoning)"
ORC[Orchestrator]
Planning[Planning Agent]
Action[Action Agent]
Chat[Chat Agent]
ORC --> Planning
ORC --> Action
Planner[Planning Agent (LLM)]
Chat[Chat Agent (LLM)]
ORC --> Planner
ORC --> Chat
end
subgraph "Layer D: Action (Execution)"
TE[Task Executor]
AA[Action Agent]
Planner -->|Plan| TE
TE -->|Action Steps| AA
end
EM -->|High Priority| RM
EM -->|All Events| ORC
RM -.->|Inhibition Signal| ORC
ORC -->|Execution Request| TE
style EM fill:#e1f5ff
style RM fill:#fff4e1
style ORC fill:#ffe1f5
style TE fill:#dcedc8
```
### Layer A: Perception
**Location**: `src/cognitive/perception/`
The perception layer acts as the sensory input hub, receiving and preprocessing all events from the Minecraft world and external sources.
The perception layer acts as the sensory input hub, receiving and preprocesses all events from the Minecraft world and external sources.
**Components**:
- **Event Manager** (`event-manager.ts`): Centralized event distribution system
- Emits standardized `BotEvent` objects
- Supports event prioritization (TODO: salience detection)
- Manages temporal context (TODO: short-term event memory)
**Event Types**:
- `user_intent`: Player chat messages, voice commands
- `world_update`: Block changes, entity movements, damage events
- `system_alert`: Internal system notifications
**Event Flow**:
```typescript
// Example: Chat message → Event
{
type: 'user_intent',
payload: { content: 'build a house' },
source: { type: 'minecraft', id: 'player123' },
timestamp: 1234567890,
priority: 0, // Default priority
handled: false // Not yet processed
}
```
- Supports event prioritization and concurrency
### Layer B: Reflex
**Location**: `src/cognitive/reflex/`
The reflex layer handles immediate, instinctive reactions without LLM overhead. It operates on a finite state machine (FSM) pattern for predictable, fast responses.
The reflex layer handles immediate, instinctive reactions. It operates on a finite state machine (FSM) pattern for predictable, fast responses.
**Components**:
- **Reflex Manager** (`reflex-manager.ts`): Coordinates all reflex behaviors
- Subscribes to high-priority events
- Executes instant responses
- Sets inhibition signals to prevent unnecessary LLM calls
**Current Reflexes**:
-**Greeting Reflex**: Instantly responds to "hi" or "hello"
- 🚧 **Dodge Reflex** (TODO): Avoid incoming projectiles
- 🚧 **Survival Reflex** (TODO): Auto-eat when hungry, flee from danger
**Inhibition Mechanism**:
When a reflex handles an event, it sets `event.handled = true`, preventing the expensive Conscious layer from processing the same event.
```typescript
// Example: Greeting reflex
if (content === 'hi') {
bot.chat('Hi there! (Reflex)')
event.handled = true // Inhibit Conscious processing
}
```
- **Reflex Manager** (`reflex-manager.ts`): Coordinates reflex behaviors
- **Inhibition**: Reflexes can inhibit Conscious layer processing to prevent redundant responses.
### Layer C: Conscious
**Location**: `src/cognitive/conscious/`
The conscious layer handles complex reasoning, planning, and decision-making using LLM-powered agents.
The conscious layer handles complex reasoning, planning, and high-level decision-making. No physical execution happens here anymore.
**Components**:
- **Orchestrator** (`orchestrator.ts`): Main coordinator for deliberate actions
- Checks inhibition signals from Reflex layer
- Manages processing state (prevents concurrent operations)
- Coordinates Planning → Execution → Response flow
- **Orchestrator**: Coordinates "Thinking" vs "Chatting" tasks.
- **Task Manager**: Manages concurrent Primary (Physical) and Secondary (Mental) tasks.
- **Planning Agent**: pure LLM reasoning to generate plans.
- **Chat Agent**: Generates natural language responses.
- **Planning Agent**: Creates multi-step plans to achieve goals
- **Action Agent**: Executes atomic actions (move, mine, build)
- **Chat Agent**: Generates natural language responses
### Layer D: Action
**Processing Pipeline**:
```
1. Check Inhibition → 2. Update Memory → 3. Create Plan →
4. Execute Actions → 5. Generate Response → 6. Reply
```
**Location**: `src/cognitive/action/`
**State Management**:
- Uses `isProcessing` lock to prevent race conditions
- Future: Queue system for handling concurrent intents
The action layer is responsible for the actual execution of tasks in the world. It isolates "Doing" from "Thinking".
**Components**:
- **Task Executor**: Receives a `Plan` and executes it step-by-step. Handles retry logic and errors.
- **Action Agent**: The interface to low-level Mineflayer skills (move, place, break).
### 🔄 Event Flow Example
**Scenario 1: Simple Greeting (Reflex)**
```
Player: "hi"
[Perception] EventManager emits user_intent
[Reflex] ReflexManager detects greeting → Replies instantly
[Conscious] Orchestrator sees handled=true → Skips processing
```
**Scenario 2: Complex Command (Conscious)**
**Scenario: "Build a house"**
```
Player: "build a house"
[Perception] EventManager emits user_intent
[Perception] Event detected
[Reflex] ReflexManager ignores (not a reflex trigger)
[Conscious] Architect plans the structure
[Conscious] Orchestrator processes:
- PlanningAgent creates building plan
- ActionAgent executes steps (gather, place blocks)
- ChatAgent generates response
[Action] Executor takes the plan and manages the construction loop:
- Step 1: Collect wood (calls ActionAgent)
- Step 2: Craft planks
- Step 3: Build walls
Bot: "I've built a small house for you!"
[Conscious] ChatAgent confirms completion: "House is ready!"
```
### 📁 Project Structure
@@ -1,6 +1,8 @@
import type { Mineflayer } from '../../libs/mineflayer'
import type { ChatAgent } from '../../libs/mineflayer/base-agent'
import type { ChatAgentConfig, ChatContext } from './types'
import { useBot } from '../../composables/bot'
import { AbstractAgent } from '../../libs/mineflayer/base-agent'
import { generateChatResponse } from './llm'
@@ -10,13 +12,17 @@ export class ChatAgentImpl extends AbstractAgent implements ChatAgent {
private maxHistoryLength: number
private idleTimeout: number
private llmConfig: ChatAgentConfig['llm']
private mineflayer: Mineflayer
constructor(config: ChatAgentConfig) {
super(config)
this.activeChats = new Map()
this.maxHistoryLength = config.maxHistoryLength ?? 50
this.idleTimeout = config.idleTimeout ?? 5 * 60 * 1000 // 5 minutes
this.maxHistoryLength = config.maxHistoryLength ?? 50
this.idleTimeout = config.idleTimeout ?? 5 * 60 * 1000 // 5 minutes
this.llmConfig = config.llm
this.mineflayer = useBot().bot
}
protected async initializeAgent(): Promise<void> {
@@ -67,6 +73,25 @@ export class ChatAgentImpl extends AbstractAgent implements ChatAgent {
}
}
public async sendMessage(message: string): Promise<void> {
if (!this.initialized) {
throw new Error('Chat agent not initialized')
}
this.logger.withField('message', message).log('Sending message')
// We also record our own messages in history (handled by on('chat') listener usually?
// No, on('chat') filters out bot's own messages in LLMAgent plugin usually to avoid loops.
// So we manually add to history?
// Wait, the orchestrator call bot.chat() before. Did it record it?
// In processMessage, we added generated response to history.
// Here we are just sending. We should record it.
// But we don't know the receiver context? Global chat.
// If it's global chat, we treat it as broadcast.
// Just send for now.
this.mineflayer.bot.chat(message)
}
public startConversation(player: string): void {
if (!this.initialized) {
throw new Error('Chat agent not initialized')
@@ -1,12 +1,10 @@
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 { AgentConfig, MemoryAgent, Plan, PlanningAgent } from '../../libs/mineflayer/base-agent'
import type { PlanStep } from './adapter'
import { AbstractAgent } from '../../libs/mineflayer/base-agent'
import { ActionError } from '../../utils/errors'
import { ActionAgentImpl } from '../action'
import { PlanningLLMHandler } from './adapter'
interface PlanContext {
@@ -17,6 +15,7 @@ interface PlanContext {
retryCount: number
isGenerating: boolean
pendingSteps: PlanStep[]
availableActions?: Action[]
}
export interface PlanningAgentConfig extends AgentConfig {
@@ -30,7 +29,6 @@ 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
@@ -47,13 +45,6 @@ export class PlanningAgentImpl extends AbstractAgent implements PlanningAgent {
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)
@@ -67,12 +58,11 @@ export class PlanningAgentImpl extends AbstractAgent implements PlanningAgent {
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> {
public async createPlan(goal: string, availableActions: Action[] = []): Promise<Plan> {
if (!this.initialized) {
throw new Error('Planning agent not initialized')
}
@@ -87,8 +77,7 @@ export class PlanningAgentImpl extends AbstractAgent implements PlanningAgent {
return cachedPlan
}
// Get available actions from action agent
const availableActions = this.actionAgent?.getAvailableActions() ?? []
// Actions passed from Orchestrator/Executor
// Check if the goal requires actions
const requirements = this.parseGoalRequirements(goal)
@@ -128,6 +117,7 @@ export class PlanningAgentImpl extends AbstractAgent implements PlanningAgent {
retryCount: 0,
isGenerating: false,
pendingSteps: [],
availableActions,
}
return plan
@@ -138,83 +128,7 @@ export class PlanningAgentImpl extends AbstractAgent implements PlanningAgent {
}
}
public async executePlan(plan: Plan, cancellationToken?: any): 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) {
// Check for cancellation before each step
if (cancellationToken?.isCancelled) {
this.logger.log('Plan execution cancelled')
plan.status = 'cancelled'
return
}
try {
this.logger.withField('step', step).log('Executing step')
await this.actionAgent.performAction(step)
}
catch (stepError) {
if (stepError instanceof ActionError) {
this.logger.withError(stepError).warn('Step execution failed with ActionError')
// If it's a resource failure or crafting failure that we've already tried to fix (implied by fail-fast skills),
// then we should abort and report failure instead of looping.
// We can check error types or context.
if (stepError.code === 'RESOURCE_MISSING' || stepError.code === 'CRAFTING_FAILED' || stepError.code === 'INVENTORY_FULL') {
// For now, fail fast on these hard errors.
// In the future we might want a "replanning" phase here, but NOT a blind retry.
throw 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, cancellationToken)
return
}
throw stepError
}
}
plan.status = 'completed'
}
catch (error) {
plan.status = 'failed'
throw error // This will be caught by handleChatMessage and reported to user
}
finally {
this.context = null
}
}
public async adjustPlan(plan: Plan, feedback: string, sender: string): Promise<Plan> {
public async adjustPlan(plan: Plan, feedback: string, sender: string, availableActions: Action[] = []): Promise<Plan> {
if (!this.initialized) {
throw new Error('Planning agent not initialized')
}
@@ -225,13 +139,13 @@ export class PlanningAgentImpl extends AbstractAgent implements PlanningAgent {
// 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() ?? []
const actions = availableActions.length > 0 ? availableActions : (this.context.availableActions || [])
// 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)
const newSteps = await this.generatePlanSteps(plan.goal, actions, sender, feedback)
// Create adjusted plan
const adjustedPlan: Plan = {
@@ -249,7 +163,7 @@ export class PlanningAgentImpl extends AbstractAgent implements PlanningAgent {
}
// If no context, create a new plan
return this.createPlan(plan.goal)
return this.createPlan(plan.goal, availableActions)
}
catch (error) {
this.logger.withError(error).error('Failed to adjust plan')
@@ -0,0 +1,148 @@
import type { ActionAgent, ChatAgent, Plan } from '../../libs/mineflayer/base-agent'
import type { Logger } from '../../utils/logger'
import type { ActionInstruction } from './types'
import { EventEmitter } from 'node:events'
import { ActionError } from '../../utils/errors'
interface CancellationToken {
isCancelled: boolean
}
interface TaskExecutorConfig {
logger: Logger
actionAgent: ActionAgent
chatAgent: ChatAgent
}
export class TaskExecutor extends EventEmitter {
private actionAgent: ActionAgent
private chatAgent: ChatAgent
private logger: Logger
private initialized = false
constructor(config: TaskExecutorConfig) {
super()
this.logger = config.logger
this.actionAgent = config.actionAgent
this.chatAgent = config.chatAgent
}
public async initialize(): Promise<void> {
if (this.initialized)
return
this.logger.log('Initializing Task Executor')
// ActionAgent is initialized by container/orchestrator
this.initialized = true
}
public async destroy(): Promise<void> {
this.initialized = false
// ActionAgentImpl doesn't expose destroy publicly in interface but defines it?
// Checking AbstractAgent, yes it has destroy().
// We cast to access it or trust it's handled.
// For now, assume we don't need explicit destroy of ActionAgent if it just clears listeners.
}
public async executePlan(plan: Plan, cancellationToken?: CancellationToken): Promise<void> {
if (!this.initialized) {
throw new Error('TaskExecutor not initialized')
}
if (!plan.requiresAction) {
this.logger.log('Plan does not require actions, skipping execution')
return
}
this.logger.withField('plan', plan).log('Executing plan')
try {
plan.status = 'in_progress'
// Execute each step
for (const step of plan.steps) {
// Check for cancellation before each step
if (cancellationToken?.isCancelled) {
this.logger.log('Plan execution cancelled')
plan.status = 'cancelled'
return
}
try {
this.logger.withField('step', step).log('Executing step')
await this.actionAgent.performAction(step)
}
catch (stepError: any) {
if (stepError instanceof ActionError) {
this.logger.withError(stepError).warn('Step execution failed with ActionError')
// Fail fast on hard errors
if (stepError.code === 'RESOURCE_MISSING' || stepError.code === 'CRAFTING_FAILED' || stepError.code === 'INVENTORY_FULL') {
throw stepError
}
}
this.logger.withError(stepError).error('Failed to execute step')
// Re-throw to let Orchestrator handle retry logic
throw stepError
}
}
plan.status = 'completed'
}
catch (error) {
plan.status = 'failed'
throw error
}
}
public executeActions(actions: ActionInstruction[], cancellationToken?: CancellationToken): void {
if (!this.initialized) {
throw new Error('TaskExecutor not initialized')
}
this.logger.withField('count', actions.length).log('Executing actions')
// Execute each action independently and asynchronously
actions.forEach(async (action) => {
if (cancellationToken?.isCancelled) {
this.logger.log('Action execution cancelled before start')
return
}
this.emit('action:started', { action })
try {
let result: string | void
if (action.type === 'physical') {
result = await this.actionAgent.performAction(action.step)
}
else if (action.type === 'chat') {
await this.chatAgent.sendMessage(action.message)
result = 'Message sent'
}
else {
throw new Error(`Unknown action type: ${(action as any).type}`)
}
if (cancellationToken?.isCancelled) {
// If cancelled during execution (and agent didn't throw), we might still consider it cancelled?
// But usually agents throw if cancelled.
// Just emit completed for now if it finished.
}
this.emit('action:completed', { action, result })
}
catch (error) {
this.logger.withError(error).error('Action execution failed')
this.emit('action:failed', { action, error })
}
})
}
public getAvailableActions() {
return this.actionAgent.getAvailableActions()
}
}
@@ -0,0 +1,20 @@
import type { PlanStep } from '../../agents/planning/adapter'
export type ActionType = 'physical' | 'chat'
export interface BaseActionInstruction {
type: ActionType
description?: string
}
export interface PhysicalActionInstruction extends BaseActionInstruction {
type: 'physical'
step: PlanStep
}
export interface ChatActionInstruction extends BaseActionInstruction {
type: 'chat'
message: string
}
export type ActionInstruction = PhysicalActionInstruction | ChatActionInstruction
@@ -0,0 +1,83 @@
import { Vec3 } from 'vec3'
export interface SelfState {
status: 'idle' | 'moving' | 'working' | 'chatting' | 'busy'
location: Vec3 | null
holding: string | null
health: number
food: number
oxygen: number
}
export interface EnvironmentState {
time: string // 'day' | 'night' | 'sunset' | 'sunrise'
weather: 'clear' | 'rain' | 'thunder'
nearbyPlayers: string[]
nearbyEntities: string[] // significant entities (mobs, dropped items of interest)
lightLevel: number
}
export interface BlackboardState {
currentGoal: string
currentThought: string
executionStrategy: string
self: SelfState
environment: EnvironmentState
}
export class Blackboard {
private _state: BlackboardState
constructor() {
this._state = {
currentGoal: 'Idle',
currentThought: 'I am waiting for something to happen.',
executionStrategy: 'Observe surroundings.',
self: {
status: 'idle',
location: null,
holding: null,
health: 20,
food: 20,
oxygen: 20,
},
environment: {
time: 'day',
weather: 'clear',
nearbyPlayers: [],
nearbyEntities: [],
lightLevel: 15,
},
}
}
// Getters
public get goal(): string { return this._state.currentGoal }
public get thought(): string { return this._state.currentThought }
public get strategy(): string { return this._state.executionStrategy }
public get self(): SelfState { return this._state.self }
public get environment(): EnvironmentState { return this._state.environment }
// Setters (Partial updates allowed)
public update(updates: Partial<BlackboardState>): void {
this._state = { ...this._state, ...updates }
}
public updateSelf(updates: Partial<SelfState>): void {
this._state.self = { ...this._state.self, ...updates }
}
public updateEnvironment(updates: Partial<EnvironmentState>): void {
this._state.environment = { ...this._state.environment, ...updates }
}
public getSnapshot(): BlackboardState {
// Return a deep copy or safe reference?
// For now, return a shallow copy of the state structure
return {
...this._state,
self: { ...this._state.self }, // location (Vec3) is an object, but usually treated efficiently.
environment: { ...this._state.environment, nearbyPlayers: [...this._state.environment.nearbyPlayers], nearbyEntities: [...this._state.environment.nearbyEntities] }
}
}
}
@@ -0,0 +1,215 @@
import type { Logg } from '@guiiai/logg'
import type { Neuri } from 'neuri'
import type { TaskExecutor } from '../action/task-executor'
import type { ActionInstruction } from '../action/types'
import type { EventManager } from '../perception/event-manager'
import type { MineflayerWithAgents, UserIntentPayload } from '../types'
import { system, user } from 'neuri/openai'
import { zodToJsonSchema } from 'zod-to-json-schema'
import { Blackboard } from './blackboard'
interface BrainDeps {
eventManager: EventManager
neuri: Neuri
logger: Logg
taskExecutor: TaskExecutor
}
interface BrainResponse {
thought: string
blackboard: {
currentGoal?: string
currentThought?: string
executionStrategy?: string
}
actions: ActionInstruction[]
}
export class Brain {
private blackboard: Blackboard
constructor(private readonly deps: BrainDeps) {
this.blackboard = new Blackboard()
}
public init(bot: MineflayerWithAgents): void {
this.deps.logger.log('Brain: Initializing...')
// Listen to User Intents (Chat/Voice)
// We treat these as "Sensory Inputs" that trigger the Cognitive Cycle
this.deps.eventManager.on<UserIntentPayload>('user_intent', async (event) => {
this.deps.logger.log(`Brain: Received intent from ${event.source.id}: ${event.payload.content}`)
await this.processEvent(bot, event)
})
// Listen to Task Execution Events (Action Feedback)
this.deps.taskExecutor.on('action:completed', async ({ action, result }) => {
this.deps.logger.log(`Brain: Action completed: ${action.type}`)
await this.processEvent(bot, {
type: 'action:feedback',
payload: {
status: 'success',
action,
result,
},
source: { type: 'system', id: 'executor' },
timestamp: Date.now(),
})
})
this.deps.taskExecutor.on('action:failed', async ({ action, error }) => {
this.deps.logger.withError(error).warn(`Brain: Action failed: ${action.type}`)
await this.processEvent(bot, {
type: 'action:feedback',
payload: {
status: 'failure',
action,
error: error.message || error,
},
source: { type: 'system', id: 'executor' },
timestamp: Date.now(),
})
})
this.deps.logger.log('Brain: Online.')
}
private async processEvent(bot: MineflayerWithAgents, event: any): Promise<void> {
// OODA Loop: Observe -> Orient -> Decide -> Act
// 1. Observe (Update Blackboard with Environment Sense)
this.updatePerception(bot)
// 2. Orient (Contextualize Event)
let contextMsg = ''
if (event.type === 'user_intent') {
contextMsg = `User ${event.source.id} says: "${event.payload.content}"`
}
else if (event.type === 'action:feedback') {
const { status, result, error, action } = event.payload
const actionDesc = action.type === 'physical' ? action.step.tool : 'chat'
contextMsg = `Action Feedback: ${actionDesc} ${status}. Result: ${JSON.stringify(result || error)}`
}
// 3. Decide (LLM Call)
const systemPrompt = this.generateSystemPrompt(this.blackboard)
const decision = await this.decide(systemPrompt, contextMsg)
if (!decision) {
this.deps.logger.warn('Brain: No decision made.')
return
}
// 4. Act (Execute Decision)
this.deps.logger.log(`Brain: Thought: ${decision.thought}`)
// Update Blackboard
this.blackboard.update({
currentGoal: decision.blackboard.currentGoal || this.blackboard.goal,
currentThought: decision.blackboard.currentThought || this.blackboard.thought,
executionStrategy: decision.blackboard.executionStrategy || this.blackboard.strategy,
})
// Issue Actions
if (decision.actions && decision.actions.length > 0) {
this.deps.taskExecutor.executeActions(decision.actions)
}
}
private updatePerception(bot: MineflayerWithAgents): void {
const pos = bot.bot.entity.position
this.blackboard.updateSelf({
location: pos,
health: bot.bot.health,
food: bot.bot.food,
})
this.blackboard.updateEnvironment({
time: bot.bot.time.isDay ? 'day' : 'night',
weather: bot.bot.isRaining ? 'rain' : 'clear',
nearbyPlayers: Object.keys(bot.bot.players).filter(p => p !== bot.bot.username),
})
}
private async decide(sysPrompt: string, userMsg: string): Promise<BrainResponse | null> {
try {
const response = await this.deps.neuri.handleStateless(
[
system(sysPrompt),
user(userMsg),
],
async (ctx) => {
const completion = await ctx.reroute('action', ctx.messages, {
response_format: { type: 'json_object' },
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any) as any
if (!completion || !completion.choices?.[0]?.message?.content) {
throw new Error('LLM failed to return content')
}
return completion.choices[0].message.content
},
)
if (!response)
return null
const parsed = JSON.parse(response) as BrainResponse
return parsed
}
catch (err) {
this.deps.logger.withError(err).error('Brain: Decision failed')
return null
}
}
private generateSystemPrompt(blackboard: Blackboard): string {
const actions = this.deps.taskExecutor.getAvailableActions()
const actionDefinitions = actions.map((a) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const schema = zodToJsonSchema(a.schema as any)
return {
name: a.name,
description: a.description,
parameters: schema,
}
})
const availableActionsJson = JSON.stringify(actionDefinitions, null, 2)
return `你是Minecraft自主Agent的大脑。
当前状态(黑板):
目标: "${blackboard.goal}"
思绪: "${blackboard.thought}"
策略: "${blackboard.strategy}"
自身: 位置${blackboard.self.location} 生命${blackboard.self.health} 饱食${blackboard.self.food}
环境: ${blackboard.environment.time} ${blackboard.environment.weather} 玩家[${blackboard.environment.nearbyPlayers.join(',')}]
可用动作:
${availableActionsJson}
规则:
1. 可执行上述物理动作(physical)或聊天动作(chat)
2. 可并行执行不冲突的多个动作(如聊天+行走)
3. 必须输出JSON
输出格式:
{
"thought": "推理过程",
"blackboard": {
"currentGoal": "更新的目标",
"currentThought": "内心独白",
"executionStrategy": "短期计划"
},
"actions": [
{"type":"chat","message":"..."},
{"type":"physical","step":{"tool":"动作名","params":{...}}}
]
}
`
}
}
@@ -1,6 +1,7 @@
import type { Logg } from '@guiiai/logg'
import type { Neuri, NeuriContext } from 'neuri'
import type { TaskExecutor } from '../action/task-executor'
import type { EventManager } from '../perception/event-manager'
import type { BotEvent, MineflayerWithAgents, UserIntentPayload } from '../types'
import type { CancellationToken } from './task-state'
@@ -9,20 +10,24 @@ import { withRetry } from '@moeru/std'
import { system, user } from 'neuri/openai'
import { DebugServer } from '../../debug-server'
import { ActionError } from '../../utils/errors'
import { handleLLMCompletion } from './completion'
import { generateStatusPrompt } from './prompt'
import { TaskManager } from './task-manager'
export class Orchestrator {
private taskManager: TaskManager
private eventQueue: Array<BotEvent<UserIntentPayload>> = []
private isProcessingQueue = false
// We no longer need an event queue for blocking purposes,
// but we might keep it if we want to handle explicit queuing later.
// For now, removing the blocking logic.
constructor(
private readonly deps: {
eventManager: EventManager
neuri: Neuri
logger: Logg
taskExecutor: TaskExecutor
},
) {
this.taskManager = new TaskManager(deps.logger)
@@ -30,6 +35,16 @@ export class Orchestrator {
public init(bot: MineflayerWithAgents): void {
this.deps.eventManager.on<UserIntentPayload>('user_intent', async (event) => {
// Don't await here to allow event loop to continue?
// Actually, if we await, the next event won't process until this one finishes
// ONLY IF eventManager awaits listeners.
// Assuming we want true parallelism, we should probably not await the full task execution,
// but we should await the initial decision making.
// However, handleUserIntent is async void, so awaiting it in event emitter is standard.
// To ensure non-blocking, handleUserIntent should return quickly.
// Let's await it, but ensure handleUserIntent doesn't block on long operations
// before deciding if it's a new task.
await this.handleUserIntent(bot, event)
})
}
@@ -45,41 +60,24 @@ export class Orchestrator {
return
}
// Check if there's a current task
if (this.taskManager.hasCurrentTask()) {
// Determine if we should cancel current task or queue this event
if (this.shouldCancelCurrentTask(event)) {
this.deps.logger
.withFields({ currentTaskId: this.taskManager.getCurrentTask()?.id, priority: event.priority })
.log('Orchestrator: Cancelling current task for high-priority event')
this.taskManager.cancelCurrentTask('High-priority event received')
this.broadcastTaskStatus()
}
else {
// Queue the event for later processing
this.eventQueue.push(event)
this.deps.logger
.withFields({ queueSize: this.eventQueue.length, username, event: content })
.log('Orchestrator: Event queued')
this.broadcastTaskStatus()
// High priority interruption check
if (this.shouldCancelPrimaryTask(event)) {
this.deps.logger
.withFields({
currentPrimaryId: this.taskManager.getPrimaryTask()?.id,
priority: event.priority,
})
.log('Orchestrator: Cancelling primary task for high-priority event')
// Notify user that we're busy
const busyMessage = 'I\'m busy right now, I\'ll get to that in a moment!'
if (source.reply) {
source.reply(busyMessage)
}
else {
bot.bot.chat(busyMessage)
}
return
}
this.taskManager.cancelPrimaryTask('High-priority event received')
// Note: We continue to process this event as a new task
}
// Create new task
// Create new task (Secondary by default if primary exists, Primary if none exists)
const task = this.taskManager.createTask(content)
this.deps.logger
.withFields({ username, content, taskId: task.id })
.log('Orchestrator: Starting new task')
.log('Orchestrator: Starting new task processing')
this.broadcastTaskStatus()
try {
@@ -87,126 +85,184 @@ export class Orchestrator {
bot.memory.chatHistory.push(user(`${username}: ${content}`))
// 2. Execute task with cancellation support
await this.executeTaskWithCancellation(bot, event, task.cancellationToken)
// This will now handle conflicts internally
this.executeTaskWithCancellation(bot, event, task).catch((err) => {
this.deps.logger.withError(err).error('Orchestrator: Async task execution failed')
})
// We return immediately to allow event loop to process next event
// (Effectively making it fire-and-forget from the EventManager's perspective)
}
catch (error) {
this.deps.logger.withError(error).warn('Orchestrator: Failed to process intent')
const errorMessage = `Sorry, I encountered an error: ${error instanceof Error ? error.message : 'Unknown error'}`
if (source.reply) {
source.reply(errorMessage)
}
else {
bot.bot.chat(errorMessage)
}
}
finally {
this.taskManager.completeCurrentTask()
this.broadcastTaskStatus()
// Process next queued event
this.processNextQueuedEvent(bot)
this.deps.logger.withError(error).warn('Orchestrator: Failed to initiate task')
}
}
private async executeTaskWithCancellation(
bot: MineflayerWithAgents,
event: BotEvent<UserIntentPayload>,
cancellationToken: CancellationToken,
task: { id: string, cancellationToken: CancellationToken }, // Use TaskContext type if imported
): Promise<void> {
const { payload, source } = event
const { content } = payload
const { cancellationToken, id: taskId } = task
// Planning phase
if (cancellationToken.isCancelled)
return
this.taskManager.updateTaskStatus('planning')
this.broadcastTaskStatus()
this.deps.logger.log('Orchestrator: Starting planning phase')
try {
// Planning phase
if (cancellationToken.isCancelled)
return
this.taskManager.updateTaskStatus(taskId, 'planning')
this.broadcastTaskStatus()
this.deps.logger.log('Orchestrator: Starting planning phase')
const plan = await bot.planning.createPlan(content)
this.taskManager.setTaskPlan(plan)
this.deps.logger.withFields({ steps: plan.steps.length }).log('Orchestrator: Plan created')
const availableActions = this.deps.taskExecutor.getAvailableActions()
const plan = await bot.planning.createPlan(content, availableActions)
this.taskManager.setTaskPlan(taskId, plan)
this.deps.logger.withFields({ steps: plan.steps.length }).log('Orchestrator: Plan created')
// Execution phase
if (cancellationToken.isCancelled)
return
this.taskManager.updateTaskStatus('executing')
this.broadcastTaskStatus()
this.deps.logger.log('Orchestrator: Executing plan')
// CONFLICT RESOLUTION
if (plan.requiresAction) {
// If this task requires action, check if it conflicts with a primary task
const primaryTask = this.taskManager.getPrimaryTask()
await bot.planning.executePlan(plan, cancellationToken)
this.deps.logger.log('Orchestrator: Plan executed successfully')
// If there is a primary task and IT IS NOT THIS TASK
if (primaryTask && primaryTask.id !== taskId) {
this.deps.logger.log('Orchestrator: Conflict detected - Secondary task requires action while Primary is busy')
// Response generation phase
if (cancellationToken.isCancelled)
return
this.taskManager.updateTaskStatus('responding')
this.broadcastTaskStatus()
this.deps.logger.log('Orchestrator: Generating response')
// Conflict Policy: Reject secondary action tasks
const busyMessage = `I'm currently busy with "${primaryTask.goal}". Please ask me to "${content}" later or tell me to stop.`
if (source.reply)
source.reply(busyMessage)
else bot.bot.chat(busyMessage)
const statusPrompt = await generateStatusPrompt(bot)
const taskContext = this.taskManager.getTaskContextForLLM()
// Abort this secondary task
this.taskManager.completeTask(taskId)
this.broadcastTaskStatus()
return
}
}
const response = await this.deps.neuri.handleStateless(
[
...bot.memory.chatHistory,
system(statusPrompt),
system(`Task Context: ${taskContext}`),
],
async (c: NeuriContext) => {
this.deps.logger.log('Orchestrator: thinking...')
return withRetry<NeuriContext, string>(
ctx => handleLLMCompletion(ctx, bot, this.deps.logger),
{
retry: 3,
retryDelay: 1000,
},
)(c)
},
)
// Execution phase
if (cancellationToken.isCancelled)
return
// Reply
if (cancellationToken.isCancelled)
return
if (response) {
this.deps.logger.withFields({ response }).log('Orchestrator: Responded')
if (source.reply) {
source.reply(response)
if (plan.requiresAction) {
this.taskManager.updateTaskStatus(taskId, 'executing')
this.broadcastTaskStatus()
this.deps.logger.log('Orchestrator: Executing plan')
// Retry loop implementation
let currentPlan = plan
let retryCount = 0
const MAX_RETRIES = 3
while (retryCount < MAX_RETRIES) {
if (cancellationToken.isCancelled)
return
try {
await this.deps.taskExecutor.executePlan(currentPlan, cancellationToken)
this.deps.logger.log('Orchestrator: Plan executed successfully')
break // Success
}
catch (error: any) {
if (cancellationToken.isCancelled)
return
// Check if it's an actionable error
const isActionError = error instanceof ActionError
if (!isActionError)
throw error // Re-throw system errors
retryCount++
if (retryCount >= MAX_RETRIES)
throw error // Give up
this.deps.logger.withError(error).warn(`Orchestrator: Plan execution failed (Attempt ${retryCount}/${MAX_RETRIES}). Adjusting plan...`)
// Adjust plan
const availableActions = this.deps.taskExecutor.getAvailableActions()
currentPlan = await bot.planning.adjustPlan(
currentPlan,
error.message,
'system',
availableActions,
)
this.taskManager.setTaskPlan(taskId, currentPlan)
this.broadcastTaskStatus()
}
}
}
else {
bot.bot.chat(response)
this.deps.logger.log('Orchestrator: No physical actions required, skipping execution phase')
}
// Response generation phase
if (cancellationToken.isCancelled)
return
this.taskManager.updateTaskStatus(taskId, 'responding')
this.broadcastTaskStatus()
this.deps.logger.log('Orchestrator: Generating response')
const statusPrompt = await generateStatusPrompt(bot)
const taskContext = this.taskManager.getTaskContextForLLM()
const response = await this.deps.neuri.handleStateless(
[
...bot.memory.chatHistory,
system(statusPrompt),
system(`Task Context:\n${taskContext}`), // Provide full context of all tasks
],
async (c: NeuriContext) => {
this.deps.logger.log('Orchestrator: thinking...')
return withRetry<NeuriContext, string>(
ctx => handleLLMCompletion(ctx, bot, this.deps.logger),
{
retry: 3,
retryDelay: 1000,
},
)(c)
},
)
// Reply
if (cancellationToken.isCancelled)
return
if (response) {
this.deps.logger.withFields({ response }).log('Orchestrator: Responded')
if (source.reply) {
source.reply(response)
}
else {
bot.bot.chat(response)
}
}
}
catch (error) {
this.deps.logger.withError(error).warn(`Orchestrator: Task ${taskId} processing failed`)
// Optional: notify user of failure
}
finally {
this.taskManager.completeTask(taskId)
this.broadcastTaskStatus()
}
}
private shouldCancelCurrentTask(event: BotEvent<UserIntentPayload>): boolean {
private shouldCancelPrimaryTask(event: BotEvent<UserIntentPayload>): boolean {
const primaryTask = this.taskManager.getPrimaryTask()
if (!primaryTask)
return false
// High priority events should cancel current task
const HIGH_PRIORITY_THRESHOLD = 8
return (event.priority ?? 5) >= HIGH_PRIORITY_THRESHOLD
}
private async processNextQueuedEvent(bot: MineflayerWithAgents): Promise<void> {
// Prevent concurrent queue processing
if (this.isProcessingQueue || this.eventQueue.length === 0) {
return
}
this.isProcessingQueue = true
const nextEvent = this.eventQueue.shift()
if (nextEvent) {
this.deps.logger.log('Orchestrator: Processing next queued event')
await this.handleUserIntent(bot, nextEvent)
}
this.isProcessingQueue = false
}
private broadcastTaskStatus(): void {
const debugServer = DebugServer.getInstance()
debugServer.broadcast('task-status', {
currentTask: this.taskManager.getCurrentTask(),
queueSize: this.eventQueue.length,
queue: this.eventQueue,
currentTask: this.taskManager.getPrimaryTask(), // For backward compatibility with UI
activeTasks: this.taskManager.getAllActiveTasks(),
history: this.taskManager.getTaskHistory(),
})
}
@@ -6,14 +6,17 @@ import type { TaskContext, TaskStatus } from './task-state'
import { createCancellationToken } from './task-state'
export class TaskManager {
private currentTask: TaskContext | null = null
private primaryTask: TaskContext | null = null
private secondaryTasks: Map<string, TaskContext> = new Map()
private taskHistory: TaskContext[] = []
private readonly maxHistorySize = 10
constructor(private readonly logger: Logg) {}
/**
* Create a new task with cancellation support
* Create a new task.
* If strictlySecondary is true, it will always be created as specific type (e.g. for forced background tasks).
* Otherwise, if no primary task exists, new task becomes primary.
*/
public createTask(goal: string): TaskContext {
const task: TaskContext = {
@@ -24,132 +27,166 @@ export class TaskManager {
cancellationToken: createCancellationToken(),
}
this.currentTask = task
this.logger.withFields({ taskId: task.id, goal }).log('TaskManager: Created new task')
if (!this.primaryTask) {
this.primaryTask = task
this.logger.withFields({ taskId: task.id, goal, type: 'primary' }).log('TaskManager: Created new primary task')
} else {
this.secondaryTasks.set(task.id, task)
this.logger.withFields({ taskId: task.id, goal, type: 'secondary' }).log('TaskManager: Created new secondary task')
}
return task
}
/**
* Update the status of the current task
* Update the status of a specific task
*/
public updateTaskStatus(status: TaskStatus, currentStep?: string): void {
if (!this.currentTask) {
this.logger.warn('TaskManager: No current task to update')
public updateTaskStatus(taskId: string, status: TaskStatus, currentStep?: string): void {
const task = this.getTaskById(taskId)
if (!task) {
this.logger.warn(`TaskManager: Task ${taskId} not found for update`)
return
}
this.currentTask.status = status
task.status = status
if (currentStep) {
this.currentTask.currentStep = currentStep
task.currentStep = currentStep
}
this.logger.withFields({
taskId: this.currentTask.id,
taskId: task.id,
status,
currentStep,
}).log('TaskManager: Updated task status')
}
/**
* Set the plan for the current task
* Set the plan for a specific task
*/
public setTaskPlan(plan: Plan): void {
if (!this.currentTask) {
this.logger.warn('TaskManager: No current task to set plan for')
public setTaskPlan(taskId: string, plan: Plan): void {
const task = this.getTaskById(taskId)
if (!task) {
this.logger.warn(`TaskManager: Task ${taskId} not found to set plan`)
return
}
this.currentTask.plan = plan
this.logger.withFields({ taskId: this.currentTask.id }).log('TaskManager: Set task plan')
task.plan = plan
this.logger.withFields({ taskId: task.id }).log('TaskManager: Set task plan')
}
/**
* Cancel the current task
* Cancel a specific task. If no taskId provided, cancels primary task.
*/
public cancelCurrentTask(reason?: string): void {
if (!this.currentTask) {
this.logger.warn('TaskManager: No current task to cancel')
public cancelTask(taskId: string, reason?: string): void {
const task = this.getTaskById(taskId)
if (!task) {
this.logger.warn(`TaskManager: Task ${taskId} not found to cancel`)
return
}
this.logger.withFields({
taskId: this.currentTask.id,
taskId: task.id,
reason,
}).log('TaskManager: Cancelling current task')
}).log('TaskManager: Cancelling task')
this.currentTask.status = 'cancelling'
this.currentTask.cancellationToken.cancel()
task.status = 'cancelling'
task.cancellationToken.cancel()
// Move to history
this.addToHistory(this.currentTask)
this.currentTask = null
// We don't remove it yet, we wait for completeTask to be called
this.addToHistory(task)
// Cleanup reference immediately to allow new primary tasks if this was primary?
// No, we should wait for the orchestrator to call completeTask/cleanup.
}
/**
* Complete the current task
* Cancel currently active primary task
*/
public completeCurrentTask(): void {
if (!this.currentTask) {
return
public cancelPrimaryTask(reason?: string): void {
if (this.primaryTask) {
this.cancelTask(this.primaryTask.id, reason)
}
this.logger.withFields({ taskId: this.currentTask.id }).log('TaskManager: Task completed')
// Move to history
this.addToHistory(this.currentTask)
this.currentTask = null
}
/**
* Get the current task
* Complete a task and remove it from active list
*/
public getCurrentTask(): TaskContext | null {
return this.currentTask
public completeTask(taskId: string): void {
const task = this.getTaskById(taskId)
if (!task) return
this.logger.withFields({ taskId: task.id }).log('TaskManager: Task completed')
this.addToHistory(task)
if (this.primaryTask?.id === taskId) {
this.primaryTask = null
} else {
this.secondaryTasks.delete(taskId)
}
}
/**
* Check if there is a current task
* Get the current primary task
*/
public hasCurrentTask(): boolean {
return this.currentTask !== null
public getPrimaryTask(): TaskContext | null {
return this.primaryTask
}
/**
* Check if can accept a new task
* Check if there is a primary task running
*/
public canAcceptNewTask(): boolean {
return this.currentTask === null
public hasPrimaryTask(): boolean {
return this.primaryTask !== null
}
/**
* Get formatted task context for LLM
* Get a task by ID
*/
public getTaskById(taskId: string): TaskContext | null {
if (this.primaryTask?.id === taskId) return this.primaryTask
return this.secondaryTasks.get(taskId) || null
}
/**
* Get formatted task context for LLM.
* Includes Primary Task and summary of Secondary Tasks.
*/
public getTaskContextForLLM(): string {
if (!this.currentTask) {
return 'No active task'
const lines: string[] = []
// Primary Task
if (this.primaryTask) {
const { goal, status, startTime, currentStep, plan } = this.primaryTask
const elapsedSeconds = Math.floor((Date.now() - startTime) / 1000)
lines.push(`[PRIMARY TASK] (${status.toUpperCase()}): "${goal}"`)
lines.push(`- Duration: ${elapsedSeconds}s`)
if (currentStep) lines.push(`- Step: ${currentStep}`)
if (plan) lines.push(`- Plan: ${plan.steps.length} steps (${plan.status})`)
} else {
lines.push('No primary task active.')
}
const { goal, status, startTime, currentStep, plan } = this.currentTask
const elapsedSeconds = Math.floor((Date.now() - startTime) / 1000)
const lines = [
`Current Task: [${status.toUpperCase()}] ${goal}`,
`- Started: ${elapsedSeconds} seconds ago`,
]
if (currentStep) {
lines.push(`- Current Step: ${currentStep}`)
}
if (plan) {
const totalSteps = plan.steps.length
lines.push(`- Plan: ${totalSteps} steps total`)
// Secondary Tasks
if (this.secondaryTasks.size > 0) {
lines.push('\n[SECONDARY TASKS]')
for (const task of this.secondaryTasks.values()) {
lines.push(`- [${task.status.toUpperCase()}] "${task.goal}"`)
}
}
return lines.join('\n')
}
/**
* Get all active tasks for debugging
*/
public getAllActiveTasks(): TaskContext[] {
const tasks: TaskContext[] = []
if (this.primaryTask) tasks.push(this.primaryTask)
tasks.push(...this.secondaryTasks.values())
return tasks
}
/**
* Get task history
*/
@@ -162,9 +199,10 @@ export class TaskManager {
}
private addToHistory(task: TaskContext): void {
// Only add if not already in history (simple check)
if (this.taskHistory.some(t => t.id === task.id)) return
this.taskHistory.push(task)
// Limit history size
if (this.taskHistory.length > this.maxHistorySize) {
this.taskHistory.shift()
}
@@ -7,7 +7,8 @@ import { asClass, asFunction, createContainer, InjectionMode } from 'awilix'
import { ActionAgentImpl } from '../agents/action'
import { ChatAgentImpl } from '../agents/chat'
import { PlanningAgentImpl } from '../agents/planning'
import { Orchestrator } from './conscious/orchestrator'
import { TaskExecutor } from './action/task-executor'
import { Brain } from './conscious/brain'
import { EventManager } from './perception/event-manager'
import { ReflexManager } from './reflex/reflex-manager'
@@ -18,7 +19,8 @@ export interface ContainerServices {
chatAgent: ChatAgentImpl
neuri: Neuri
eventManager: EventManager
orchestrator: Orchestrator
taskExecutor: TaskExecutor
brain: Brain
reflexManager: ReflexManager
}
@@ -73,7 +75,9 @@ export function createAgentContainer(options: {
eventManager: asClass(EventManager).singleton(),
orchestrator: asClass(Orchestrator).singleton(),
taskExecutor: asClass(TaskExecutor).singleton(),
brain: asClass(Brain).singleton(),
reflexManager: asClass(ReflexManager).singleton(),
})
+13 -13
View File
@@ -1,46 +1,41 @@
import type { MineflayerPlugin } from '../libs/mineflayer'
import type { LLMAgentOptions, MineflayerWithAgents } from './types'
import { system } from 'neuri/openai'
import { config } from '../composables/config'
import { ChatMessageHandler } from '../libs/mineflayer'
import { generateActionAgentPrompt } from './conscious/prompt'
import { createAgentContainer } from './container'
export function LLMAgent(options: LLMAgentOptions): MineflayerPlugin {
let container: ReturnType<typeof createAgentContainer>
return {
async created(bot) {
// Create container and get required services
const container = createAgentContainer({
container = createAgentContainer({
neuri: options.agent,
model: config.openai.model,
})
const actionAgent = container.resolve('actionAgent')
const planningAgent = container.resolve('planningAgent')
const chatAgent = container.resolve('chatAgent')
const eventManager = container.resolve('eventManager')
const orchestrator = container.resolve('orchestrator')
const brain = container.resolve('brain')
const reflexManager = container.resolve('reflexManager')
const taskExecutor = container.resolve('taskExecutor')
// Initialize agents
await actionAgent.init()
await planningAgent.init()
await chatAgent.init()
await taskExecutor.initialize()
// Type conversion
const botWithAgents = bot as unknown as MineflayerWithAgents
botWithAgents.action = actionAgent
botWithAgents.planning = planningAgent
botWithAgents.chat = chatAgent
// Initialize layers
reflexManager.init(botWithAgents)
orchestrator.init(botWithAgents)
// Initialize system prompt
bot.memory.chatHistory.push(system(generateActionAgentPrompt(bot)))
brain.init(botWithAgents)
// Set message handling via EventManager
const chatHandler = new ChatMessageHandler(bot.username)
@@ -89,8 +84,13 @@ export function LLMAgent(options: LLMAgentOptions): MineflayerPlugin {
async beforeCleanup(bot) {
const botWithAgents = bot as unknown as MineflayerWithAgents
await botWithAgents.action?.destroy()
await botWithAgents.planning?.destroy()
await botWithAgents.chat?.destroy()
if (container) {
const taskExecutor = container.resolve('taskExecutor')
await taskExecutor.destroy()
}
bot.bot.removeAllListeners('chat')
},
}
@@ -44,14 +44,14 @@ export interface Plan {
export interface PlanningAgent extends BaseAgent {
type: 'planning'
createPlan: (goal: string) => Promise<Plan>
executePlan: (plan: Plan, cancellationToken?: any) => Promise<void>
adjustPlan: (plan: Plan, feedback: string, sender: string) => Promise<Plan>
createPlan: (goal: string, availableActions?: Action[]) => Promise<Plan>
adjustPlan: (plan: Plan, feedback: string, sender: string, availableActions?: Action[]) => Promise<Plan>
}
export interface ChatAgent extends BaseAgent {
type: 'chat'
processMessage: (message: string, sender: string) => Promise<string>
sendMessage: (message: string) => Promise<void>
startConversation: (player: string) => void
endConversation: (player: string) => void
}
+3 -6
View File
@@ -305,15 +305,12 @@ export async function clearNearestFurnace(mineflayer: Mineflayer): Promise<boole
const furnace = await mineflayer.bot.openFurnace(furnaceBlock)
logger.log('opened furnace...')
// Take the items out of the furnace
let smeltedItem: Item | null = null
let inputItem: Item | null = null
let fuelItem: Item | null = null
if (furnace.outputItem())
smeltedItem = await furnace.takeOutput()
await furnace.takeOutput()
if (furnace.inputItem())
inputItem = await furnace.takeInput()
await furnace.takeInput()
if (furnace.fuelItem())
fuelItem = await furnace.takeFuel()
await furnace.takeFuel()
await mineflayer.bot.closeWindow(furnace)
return true
+41 -19
View File
@@ -446,38 +446,60 @@
}
function updateTaskStatus(status) {
// Update current task
// Update active tasks
const taskInfo = document.getElementById('task-info');
if (status.currentTask) {
const task = status.currentTask;
const elapsed = Math.floor((Date.now() - task.startTime) / 1000);
const statusColor = {
'idle': '#888',
'planning': '#ffb74d',
'executing': '#4a9eff',
'responding': '#9c27b0',
'cancelling': '#ef5350'
}[task.status] || '#888';
// Handle new multi-task structure or fallback to single task
let tasks = [];
if (status.activeTasks && Array.isArray(status.activeTasks)) {
tasks = status.activeTasks;
} else if (status.currentTask) {
tasks = [status.currentTask];
}
taskInfo.innerHTML = `
<div style="padding: 1rem;">
if (tasks.length > 0) {
taskInfo.innerHTML = tasks.map(task => {
// Determine if primary based on data or assumption
// Note: TaskManager now logs 'type' but task object structure might not have it explicitly unless we added it to TaskContext
// But we can infer order or just display all.
// Let's assume the first one might be primary if we used getAllActiveTasks() order (primary first)
const elapsed = Math.floor((Date.now() - task.startTime) / 1000);
const statusColor = {
'idle': '#888',
'planning': '#ffb74d',
'executing': '#4a9eff',
'responding': '#9c27b0',
'cancelling': '#ef5350'
}[task.status] || '#888';
// Visual distinction for secondary vs primary could be nice, but simple list is fine for now
return `
<div style="padding: 1rem; border-bottom: 1px solid #333; last-child: border-bottom: none;">
<div style="display: flex; justify-content: space-between; margin-bottom: 0.5rem;">
<span style="font-weight: bold; font-size: 1.1em;">${task.goal}</span>
<span class="badge" style="background-color: ${statusColor}">${task.status.toUpperCase()}</span>
</div>
<div style="color: #888; font-size: 0.9em;">
<div>Task ID: ${task.id}</div>
<div>Elapsed: ${elapsed}s</div>
${task.currentStep ? `<div>Current Step: ${task.currentStep}</div>` : ''}
${task.plan ? `<div>Plan: ${task.plan.steps.length} steps (${task.plan.status})</div>` : ''}
<div style="display: flex; gap: 1rem;">
<span>ID: <span title="${task.id}">${task.id.substr(0,12)}...</span></span>
<span>Time: ${elapsed}s</span>
</div>
${task.currentStep ? `<div style="margin-top:0.25rem;">Step: ${task.currentStep}</div>` : ''}
${task.plan ? `<div style="margin-top:0.25rem;">Plan: ${task.plan.steps.length} steps (${task.plan.status})</div>` : ''}
</div>
</div>
`;
`;
}).join('');
} else {
taskInfo.innerHTML = '<div style="padding: 1rem; color: #666;">No active task</div>';
taskInfo.innerHTML = '<div style="padding: 1rem; color: #666;">No active tasks</div>';
}
// Update queue
// ... (rest same, removing queue updates here as they are separate chunks?)
// Actually I need to include the rest of the function or it will be cut off.
// Let's just replace the task rendering part if I target specific lines.
const queueCount = document.getElementById('queue-count');
const queueList = document.getElementById('queue-list');
queueCount.textContent = status.queueSize || 0;