refactor(minecraft): code cleanup, better naming

This commit is contained in:
Rin
2026-02-18 11:09:59 +08:00
committed by Neko Ayaka
parent bb507d7451
commit 5e715f7ec4
9 changed files with 48 additions and 317 deletions
+1 -1
View File
@@ -80,7 +80,7 @@ export class ChatAgentImpl extends AbstractAgent implements ChatAgent {
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.
// No, on('chat') filters out bot's own messages in CognitiveEngine 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.
@@ -1,4 +1,4 @@
import { Vec3 } from 'vec3'
import type { Vec3 } from 'vec3'
export interface SelfState {
status: 'idle' | 'moving' | 'working' | 'chatting' | 'busy'
@@ -12,17 +12,17 @@ export interface SelfState {
export interface EnvironmentState {
time: string // 'day' | 'night' | 'sunset' | 'sunrise'
weather: 'clear' | 'rain' | 'thunder'
nearbyPlayers: string[]
nearbyAgents: 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
currentGoal: string
currentThought: string
executionStrategy: string
self: SelfState
environment: EnvironmentState
}
export class Blackboard {
@@ -44,7 +44,7 @@ export class Blackboard {
environment: {
time: 'day',
weather: 'clear',
nearbyPlayers: [],
nearbyAgents: [],
nearbyEntities: [],
lightLevel: 15,
},
@@ -60,24 +60,24 @@ export class Blackboard {
// Setters (Partial updates allowed)
public update(updates: Partial<BlackboardState>): void {
this._state = { ...this._state, ...updates }
this._state = { ...this._state, ...updates }
}
public updateSelf(updates: Partial<SelfState>): void {
this._state.self = { ...this._state.self, ...updates }
this._state.self = { ...this._state.self, ...updates }
}
public updateEnvironment(updates: Partial<EnvironmentState>): void {
this._state.environment = { ...this._state.environment, ...updates }
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] }
}
// 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, nearbyAgents: [...this._state.environment.nearbyAgents], nearbyEntities: [...this._state.environment.nearbyEntities] },
}
}
}
@@ -4,7 +4,7 @@ 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 type { MineflayerWithAgents, StimulusPayload } from '../types'
import { system, user } from 'neuri/openai'
@@ -38,10 +38,10 @@ export class Brain {
public init(bot: MineflayerWithAgents): void {
this.deps.logger.log('Brain: Initializing...')
// Listen to User Intents (Chat/Voice)
// Listen to Stimuli (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}`)
this.deps.eventManager.on<StimulusPayload>('stimulus', async (event) => {
this.deps.logger.log(`Brain: Received stimulus from ${event.source.id}: ${event.payload.content}`)
await this.processEvent(bot, event)
})
@@ -49,7 +49,7 @@ export class Brain {
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',
type: 'feedback',
payload: {
status: 'success',
action,
@@ -63,7 +63,7 @@ export class Brain {
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',
type: 'feedback',
payload: {
status: 'failure',
action,
@@ -85,13 +85,13 @@ export class Brain {
// 2. Orient (Contextualize Event)
let contextMsg = ''
if (event.type === 'user_intent') {
contextMsg = `User ${event.source.id} says: "${event.payload.content}"`
if (event.type === 'stimulus') {
contextMsg = `${event.source.type} stimulus from ${event.source.id}: "${event.payload.content}"`
}
else if (event.type === 'action:feedback') {
else if (event.type === '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)}`
contextMsg = `Internal Feedback: ${actionDesc} ${status}. Result: ${JSON.stringify(result || error)}`
}
// 3. Decide (LLM Call)
@@ -130,7 +130,7 @@ export class Brain {
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),
nearbyAgents: Object.keys(bot.bot.players).filter(p => p !== bot.bot.username),
})
}
@@ -1,269 +0,0 @@
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'
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
// 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)
}
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)
})
}
private async handleUserIntent(bot: MineflayerWithAgents, event: BotEvent<UserIntentPayload>): Promise<void> {
const { payload, source } = event
const { content, metadata } = payload
const username = metadata?.displayName || source.id
// Layered Architecture: Check for inhibition from Reflex layer
if (event.handled) {
this.deps.logger.log('Orchestrator: Intent already handled by Reflex layer, inhibiting Conscious processing')
return
}
// 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')
this.taskManager.cancelPrimaryTask('High-priority event received')
// Note: We continue to process this event as a 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 processing')
this.broadcastTaskStatus()
try {
// 1. Update memory
bot.memory.chatHistory.push(user(`${username}: ${content}`))
// 2. Execute task with cancellation support
// 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 initiate task')
}
}
private async executeTaskWithCancellation(
bot: MineflayerWithAgents,
event: BotEvent<UserIntentPayload>,
task: { id: string, cancellationToken: CancellationToken }, // Use TaskContext type if imported
): Promise<void> {
const { payload, source } = event
const { content } = payload
const { cancellationToken, id: taskId } = task
try {
// Planning phase
if (cancellationToken.isCancelled)
return
this.taskManager.updateTaskStatus(taskId, 'planning')
this.broadcastTaskStatus()
this.deps.logger.log('Orchestrator: Starting planning phase')
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')
// CONFLICT RESOLUTION
if (plan.requiresAction) {
// If this task requires action, check if it conflicts with a primary task
const primaryTask = this.taskManager.getPrimaryTask()
// 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')
// 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)
// Abort this secondary task
this.taskManager.completeTask(taskId)
this.broadcastTaskStatus()
return
}
}
// Execution phase
if (cancellationToken.isCancelled)
return
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 {
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 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 broadcastTaskStatus(): void {
const debugServer = DebugServer.getInstance()
debugServer.broadcast('task-status', {
currentTask: this.taskManager.getPrimaryTask(), // For backward compatibility with UI
activeTasks: this.taskManager.getAllActiveTasks(),
history: this.taskManager.getTaskHistory(),
})
}
}
@@ -29,7 +29,7 @@ export function generateBrainSystemPrompt(
思绪: "${blackboard.thought}"
策略: "${blackboard.strategy}"
自身: 位置${blackboard.self.location} 生命${blackboard.self.health} 饱食${blackboard.self.food}
环境: ${blackboard.environment.time} ${blackboard.environment.weather} 玩家[${blackboard.environment.nearbyPlayers.join(',')}]
环境: ${blackboard.environment.time} ${blackboard.environment.weather} 附近智体[${blackboard.environment.nearbyAgents.join(',')}]
可用动作:
${availableActionsJson}
+5 -5
View File
@@ -1,11 +1,11 @@
import type { MineflayerPlugin } from '../libs/mineflayer'
import type { LLMAgentOptions, MineflayerWithAgents } from './types'
import type { CognitiveEngineOptions, MineflayerWithAgents } from './types'
import { config } from '../composables/config'
import { ChatMessageHandler } from '../libs/mineflayer'
import { createAgentContainer } from './container'
export function LLMAgent(options: LLMAgentOptions): MineflayerPlugin {
export function CognitiveEngine(options: CognitiveEngineOptions): MineflayerPlugin {
let container: ReturnType<typeof createAgentContainer>
return {
@@ -44,7 +44,7 @@ export function LLMAgent(options: LLMAgentOptions): MineflayerPlugin {
return
eventManager.emit({
type: 'user_intent',
type: 'stimulus',
payload: {
content: message,
metadata: {
@@ -61,11 +61,11 @@ export function LLMAgent(options: LLMAgentOptions): MineflayerPlugin {
options.airiClient.onEvent('input:text:voice', (event) => {
eventManager.emit({
type: 'user_intent',
type: 'stimulus',
payload: {
content: event.data.transcription,
metadata: {
displayName: (event.data.discord?.guildMember as any)?.nick || (event.data.discord?.guildMember as any)?.user?.username || 'Voice User',
displayName: (event.data.discord?.guildMember as any)?.nick || (event.data.discord?.guildMember as any)?.user?.username || 'Voice Stimulus',
},
},
source: {
@@ -1,7 +1,7 @@
import type { Logg } from '@guiiai/logg'
import type { EventManager } from '../perception/event-manager'
import type { BotEvent, MineflayerWithAgents, UserIntentPayload } from '../types'
import type { BotEvent, MineflayerWithAgents, StimulusPayload } from '../types'
export class ReflexManager {
constructor(
@@ -12,15 +12,15 @@ export class ReflexManager {
) {}
public init(bot: MineflayerWithAgents): void {
// Listen to user intents as a "subconscious" filter
this.deps.eventManager.on<UserIntentPayload>('user_intent', (event) => {
this.handleUserIntent(bot, event)
// Listen to stimuli as a "subconscious" filter
this.deps.eventManager.on<StimulusPayload>('stimulus', (event) => {
this.onStimulus(bot, event)
})
// TODO: Listen to world_update for physical reflexes (dodge, flee)
}
private handleUserIntent(bot: MineflayerWithAgents, event: BotEvent<UserIntentPayload>): void {
private onStimulus(bot: MineflayerWithAgents, event: BotEvent<StimulusPayload>): void {
const { content } = event.payload
const lowerContent = content.toLowerCase().trim()
+4 -4
View File
@@ -23,16 +23,16 @@ export interface MineflayerWithAgents extends Mineflayer {
chat: ChatAgent
}
export interface LLMAgentOptions {
export interface CognitiveEngineOptions {
agent: Neuri
airiClient: Client
}
export type EventType = 'user_intent' | 'world_update' | 'system_alert'
export type EventType = 'stimulus' | 'perception' | 'feedback' | 'world_update' | 'system_alert'
export interface BotEventSource {
type: 'minecraft' | 'airi' | 'system'
id: string // username or session id
id: string // Agent/Source identifier
reply?: (message: string) => void
}
@@ -46,7 +46,7 @@ export interface BotEvent<T = any> {
handled?: boolean // Set by Reflex layer to inhibit Conscious layer
}
export interface UserIntentPayload {
export interface StimulusPayload {
content: string
metadata?: {
entity?: any // prismarine-entity Entity
+3 -3
View File
@@ -9,7 +9,7 @@ import { pathfinder as MineflayerPathfinder } from 'mineflayer-pathfinder'
import { plugin as MineflayerPVP } from 'mineflayer-pvp'
import { plugin as MineflayerTool } from 'mineflayer-tool'
import { LLMAgent } from './cognitive'
import { CognitiveEngine } from './cognitive'
import { initBot } from './composables/bot'
import { config, initEnv } from './composables/config'
import { createNeuriAgent } from './composables/neuri'
@@ -44,9 +44,9 @@ async function main() {
url: config.airi.wsBaseUrl,
})
// Dynamically load LLMAgent after the bot is initialized
// Dynamically load CognitiveEngine after the bot is initialized
const agent = await createNeuriAgent(bot)
await bot.loadPlugin(LLMAgent({ agent, airiClient }))
await bot.loadPlugin(CognitiveEngine({ agent, airiClient }))
process.on('SIGINT', () => {
bot.stop()