Files
moeka-project/services/minecraft/src/cognitive/reflex/reflex-manager.ts
T
2026-02-18 11:12:19 +08:00

162 lines
5.2 KiB
TypeScript

import type { Logg } from '@guiiai/logg'
import type { TaskExecutor } from '../action/task-executor'
import type { EventBus, TracedEvent } from '../os'
import type { PerceptionAPI } from '../perception/perception-api'
import type { PerceptionSignal } from '../perception/types/signals'
import type { MineflayerWithAgents } from '../types'
import type { ReflexContextState } from './context'
import { DebugService } from '../../debug'
import { greetingBehavior } from './behaviors/greeting'
import { lookAtBehavior } from './behaviors/look-at'
import { teabagBehavior } from './behaviors/teabag'
import { ReflexRuntime } from './runtime'
export class ReflexManager {
private bot: MineflayerWithAgents | null = null
private readonly runtime: ReflexRuntime
private unsubscribe: (() => void) | null = null
private unsubscribeTaskExecutor: (() => void) | null = null
private inFlightActionsCount = 0
constructor(
private readonly deps: {
eventBus: EventBus
perception: PerceptionAPI
taskExecutor: TaskExecutor
logger: Logg
},
) {
this.runtime = new ReflexRuntime({
logger: this.deps.logger,
onBehaviorEnd: () => this.emitReflexState(),
onModeChange: () => this.emitReflexState(),
})
this.runtime.registerBehavior(greetingBehavior)
this.runtime.registerBehavior(lookAtBehavior)
this.runtime.registerBehavior(teabagBehavior)
}
public init(bot: MineflayerWithAgents): void {
this.bot = bot
// Subscribe to all signals from RuleEngine
this.unsubscribe = this.deps.eventBus.subscribe('signal:*', (event) => {
this.onSignal(event as TracedEvent<PerceptionSignal>)
})
const onStarted = () => {
if (this.inFlightActionsCount === 0)
this.runtime.transitionMode('work', this.bot)
this.inFlightActionsCount++
}
const onEnded = () => {
this.inFlightActionsCount = Math.max(0, this.inFlightActionsCount - 1)
if (this.inFlightActionsCount === 0)
this.runtime.transitionMode('idle', this.bot)
}
this.deps.taskExecutor.on('action:started', onStarted)
this.deps.taskExecutor.on('action:completed', onEnded)
this.deps.taskExecutor.on('action:failed', onEnded)
this.unsubscribeTaskExecutor = () => {
// Node's EventEmitter supports off() but we keep a fallback for compatibility.
; (this.deps.taskExecutor as any).off?.('action:started', onStarted)
; (this.deps.taskExecutor as any).off?.('action:completed', onEnded)
; (this.deps.taskExecutor as any).off?.('action:failed', onEnded)
; (this.deps.taskExecutor as any).removeListener?.('action:started', onStarted)
; (this.deps.taskExecutor as any).removeListener?.('action:completed', onEnded)
; (this.deps.taskExecutor as any).removeListener?.('action:failed', onEnded)
}
}
public destroy(): void {
if (this.bot)
this.runtime.transitionMode('idle', this.bot)
if (this.unsubscribe) {
this.unsubscribe()
this.unsubscribe = null
}
if (this.unsubscribeTaskExecutor) {
this.unsubscribeTaskExecutor()
this.unsubscribeTaskExecutor = null
}
this.inFlightActionsCount = 0
this.bot = null
}
public getContextSnapshot(): ReflexContextState {
return this.runtime.getContext().getSnapshot()
}
public getMode(): ReturnType<ReflexRuntime['getMode']> {
return this.runtime.getMode()
}
public updateEnvironment(patch: Partial<ReflexContextState['environment']>): void {
this.runtime.getContext().updateEnvironment(patch)
}
private onSignal(event: TracedEvent<PerceptionSignal>): void {
const bot = this.bot
if (!bot)
return
const signal = event.payload
const now = Date.now()
// Update Context
this.runtime.getContext().updateNow(now)
this.runtime.getContext().updateAttention({
lastSignalType: signal.type,
lastSignalSourceId: signal.sourceId ?? null,
lastSignalAt: now,
})
if (signal.type === 'social_gesture') {
this.runtime.getContext().updateSocial({
lastGesture: (signal.metadata as any)?.gesture ?? 'unknown',
lastGestureAt: now,
})
}
if (signal.type === 'chat_message') {
const username = typeof (signal.metadata as any)?.username === 'string'
? String((signal.metadata as any).username)
: (signal.sourceId ?? null)
const message = typeof (signal.metadata as any)?.message === 'string'
? String((signal.metadata as any).message)
: null
this.runtime.getContext().updateSocial({
lastSpeaker: username,
lastMessage: message,
lastMessageAt: now,
})
}
// If it's a chat message (simulated via signal for now, or direct?)
// For now we rely on signal metadata or separate chat event.
// Assuming 'signal:social:chat' or similar might exist later.
// For greeting behavior compatibility, we might need to map specific signals to social state.
// Trigger behavior selection
this.runtime.tick(bot, 0, this.deps.perception)
this.emitReflexState()
}
private emitReflexState(): void {
DebugService.getInstance().emitReflexState({
mode: this.runtime.getMode(),
activeBehaviorId: this.runtime.getActiveBehaviorId(),
context: this.runtime.getContext().getSnapshot(),
})
}
}