feat(minecraft): reflex behaviors WIP

This commit is contained in:
Rin
2026-02-18 11:12:02 +08:00
committed by Neko Ayaka
parent 7158c55e9a
commit 54380744c0
6 changed files with 122 additions and 31 deletions
@@ -116,7 +116,10 @@ export function createAgentContainer(options: {
}
}),
reflexManager: asClass(ReflexManager).singleton(),
// Reflex Manager (Reactive Layer)
reflexManager: asFunction(({ eventBus, logger }) =>
new ReflexManager({ eventBus, logger }),
).singleton(),
})
return container
@@ -46,6 +46,9 @@ export function CognitiveEngine(options: CognitiveEngineOptions): MineflayerPlug
reflexManager.init(botWithAgents)
brain.init(botWithAgents)
const ruleEngine = container.resolve('ruleEngine')
ruleEngine.init()
// Initialize perception pipeline (raw events + detectors)
perceptionPipeline.init(botWithAgents)
@@ -0,0 +1,42 @@
import type { ReflexBehavior } from '../types/behavior'
export const lookAtBehavior: ReflexBehavior = {
id: 'look-at',
modes: ['idle', 'social'],
cooldownMs: 1000,
when: (ctx) => {
// Check if we have a recent attention signal
const { lastSignalType, lastSignalAt } = ctx.attention
if (!lastSignalType || !lastSignalAt)
return false
// Must be fresh (within 2 seconds)
if (ctx.now - lastSignalAt > 2000)
return false
// Respond to entity_attention signals
return lastSignalType === 'entity_attention'
},
score: () => {
// High priority but not override-level (100)
// Allows critical survival behaviors to take precedence
return 50
},
run: async ({ bot, context }) => {
const { lastSignalSourceId } = context.getSnapshot().attention
if (!lastSignalSourceId)
return
// Find the entity
const target = bot.bot.entities[Number(lastSignalSourceId)]
if (!target)
return
// Look at the entity smoothly
await bot.bot.lookAt(target.position.offset(0, target.height * 0.85, 0), true)
},
}
@@ -29,12 +29,19 @@ export interface ReflexThreatState {
lastThreatSource: string | null
}
export interface ReflexAttentionState {
lastSignalType: string | null
lastSignalSourceId: string | null
lastSignalAt: number | null
}
export interface ReflexContextState {
now: number
self: ReflexSelfState
environment: ReflexEnvironmentState
social: ReflexSocialState
threat: ReflexThreatState
attention: ReflexAttentionState
}
export class ReflexContext {
@@ -68,6 +75,11 @@ export class ReflexContext {
lastThreatAt: null,
lastThreatSource: null,
},
attention: {
lastSignalType: null,
lastSignalSourceId: null,
lastSignalAt: null,
},
}
}
@@ -85,6 +97,7 @@ export class ReflexContext {
lastGreetingAtBySpeaker: { ...this.state.social.lastGreetingAtBySpeaker },
},
threat: { ...this.state.threat },
attention: { ...this.state.attention },
}
}
@@ -107,4 +120,8 @@ export class ReflexContext {
public updateThreat(patch: Partial<ReflexThreatState>): void {
this.state.threat = { ...this.state.threat, ...patch }
}
public updateAttention(patch: Partial<ReflexAttentionState>): void {
this.state.attention = { ...this.state.attention, ...patch }
}
}
@@ -1,6 +1,5 @@
import { describe, expect, it, vi } from 'vitest'
import { EventManager } from '../perception/event-manager'
import { ReflexManager } from './reflex-manager'
function makeLogger() {
@@ -36,24 +35,37 @@ function makeBot() {
describe('reflexManager', () => {
it('handles greeting via reflex and marks stimulus event handled', () => {
const eventManager = new EventManager()
// Mock EventBus
const eventBus = {
subscribe: vi.fn(),
emit: vi.fn(),
emitChild: vi.fn(),
} as any
const logger = makeLogger()
const reflex = new ReflexManager({ eventManager, logger })
const reflex = new ReflexManager({ eventBus, logger }) // Now accepts eventBus
const bot = makeBot()
reflex.init(bot)
const stimulus: any = {
type: 'stimulus',
payload: { content: 'hello' },
source: { type: 'minecraft', id: 'alice' },
// Verify subscription
expect(eventBus.subscribe).toHaveBeenCalledWith('signal:*', expect.any(Function))
// Manually trigger handler to test logic
const handler = eventBus.subscribe.mock.calls[0][1]
const signalEvent = {
type: 'signal:social',
payload: { type: 'social', description: 'hello' },
source: { component: 'ruleEngine', id: 'test' },
timestamp: Date.now(),
// ... other traced event props ...
}
eventManager.emit(stimulus)
handler(signalEvent)
expect(stimulus.handled).toBe(true)
expect(bot.bot.chat).toHaveBeenCalled()
// TODO: Ideally we assert that tick() was called.
// Since tick is internal/called via runtime, we might need to inspect side effects or spy on runtime.
// For now, ensure it doesn't crash.
reflex.destroy()
})
@@ -1,24 +1,22 @@
import type { Logg } from '@guiiai/logg'
import type { EventManager } from '../perception/event-manager'
import type { EventBus, TracedEvent } from '../os'
import type { PerceptionSignal } from '../perception/types/signals'
import type { BotEvent, MineflayerWithAgents } from '../types'
import type { MineflayerWithAgents } from '../types'
import type { ReflexContextState } from './context'
import { greetingBehavior } from './behaviors/greeting'
import { lookAtBehavior } from './behaviors/look-at'
import { ReflexRuntime } from './runtime'
export class ReflexManager {
private bot: MineflayerWithAgents | null = null
private readonly runtime: ReflexRuntime
private readonly onPerceptionHandler = (event: BotEvent<PerceptionSignal>) => {
this.onPerception(event)
}
private unsubscribe: (() => void) | null = null
constructor(
private readonly deps: {
eventManager: EventManager
eventBus: EventBus
logger: Logg
},
) {
@@ -27,15 +25,22 @@ export class ReflexManager {
})
this.runtime.registerBehavior(greetingBehavior)
this.runtime.registerBehavior(lookAtBehavior)
}
public init(bot: MineflayerWithAgents): void {
this.bot = bot
this.deps.eventManager.on<PerceptionSignal>('perception', this.onPerceptionHandler)
// Subscribe to all signals from RuleEngine
this.unsubscribe = this.deps.eventBus.subscribe('signal:*', (event) => {
this.onSignal(event as TracedEvent<PerceptionSignal>)
})
}
public destroy(): void {
this.deps.eventManager.off<PerceptionSignal>('perception', this.onPerceptionHandler)
if (this.unsubscribe) {
this.unsubscribe()
this.unsubscribe = null
}
this.bot = null
}
@@ -43,25 +48,34 @@ export class ReflexManager {
return this.runtime.getContext().getSnapshot()
}
private onPerception(event: BotEvent<PerceptionSignal>): void {
private onSignal(event: TracedEvent<PerceptionSignal>): void {
const bot = this.bot
if (!bot)
return
const signal = event.payload
const message = `Signal triggered: ${signal.type} - ${signal.description}`
bot.bot.chat(message)
const now = Date.now()
// Create log message (can be throttled later if too spammy)
this.deps.logger.withFields({
type: signal.type,
description: signal.description,
}).log('ReflexManager: signal received')
// Update Context
this.runtime.getContext().updateNow(now)
this.runtime.getContext().updateSocial({
lastSpeaker: event.source.id,
lastMessage: message,
lastMessageAt: now,
this.runtime.getContext().updateAttention({
lastSignalType: signal.type,
lastSignalSourceId: signal.sourceId ?? null,
lastSignalAt: now,
})
const behaviorId = this.runtime.tick(bot, 0)
if (behaviorId)
event.handled = true
// 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)
}
}