feat(minecraft): scaffold raw event pipeline with leaky bucket + DI wiring

This commit is contained in:
Rin
2026-02-18 11:10:02 +08:00
committed by Neko Ayaka
parent 8d629a90a2
commit a3a29ae21a
7 changed files with 270 additions and 0 deletions
@@ -10,6 +10,7 @@ import { PlanningAgentImpl } from '../agents/planning'
import { TaskExecutor } from './action/task-executor'
import { Brain } from './conscious/brain'
import { EventManager } from './perception/event-manager'
import { PerceptionPipeline } from './perception/pipeline'
import { ReflexManager } from './reflex/reflex-manager'
export interface ContainerServices {
@@ -19,6 +20,7 @@ export interface ContainerServices {
chatAgent: ChatAgentImpl
neuri: Neuri
eventManager: EventManager
perceptionPipeline: PerceptionPipeline
taskExecutor: TaskExecutor
brain: Brain
reflexManager: ReflexManager
@@ -75,6 +77,8 @@ export function createAgentContainer(options: {
eventManager: asClass(EventManager).singleton(),
perceptionPipeline: asClass(PerceptionPipeline).singleton(),
taskExecutor: asClass(TaskExecutor).singleton(),
brain: asClass(Brain).singleton(),
+11
View File
@@ -19,6 +19,7 @@ export function CognitiveEngine(options: CognitiveEngineOptions): MineflayerPlug
const actionAgent = container.resolve('actionAgent')
const chatAgent = container.resolve('chatAgent')
const eventManager = container.resolve('eventManager')
const perceptionPipeline = container.resolve('perceptionPipeline')
const brain = container.resolve('brain')
const reflexManager = container.resolve('reflexManager')
const taskExecutor = container.resolve('taskExecutor')
@@ -37,6 +38,13 @@ export function CognitiveEngine(options: CognitiveEngineOptions): MineflayerPlug
reflexManager.init(botWithAgents)
brain.init(botWithAgents)
// Initialize perception pipeline (raw events + detectors)
perceptionPipeline.init(botWithAgents)
bot.onTick('tick', ({ delta }) => {
perceptionPipeline.tick(delta)
})
// Set message handling via EventManager
const chatHandler = new ChatMessageHandler(bot.username)
bot.bot.on('chat', (username, message) => {
@@ -89,6 +97,9 @@ export function CognitiveEngine(options: CognitiveEngineOptions): MineflayerPlug
if (container) {
const taskExecutor = container.resolve('taskExecutor')
await taskExecutor.destroy()
const perceptionPipeline = container.resolve('perceptionPipeline')
perceptionPipeline.destroy()
}
bot.bot.removeAllListeners('chat')
@@ -0,0 +1,70 @@
import type { EventManager } from './event-manager'
import type { RawPerceptionEvent } from './raw-events'
import { LeakyBucket } from './leaky-bucket'
export interface AttentionEventPayload {
raw: RawPerceptionEvent
intensity: number
}
export class AttentionDetector {
private readonly buckets = new Map<string, LeakyBucket>()
constructor(private readonly deps: { eventManager: EventManager }) { }
public tick(deltaMs: number): void {
for (const bucket of this.buckets.values()) {
bucket.tick(deltaMs)
}
}
public ingest(event: RawPerceptionEvent): void {
const key = this.keyOf(event)
const bucket = this.getBucket(key)
const weight = 1
const { fired, value } = bucket.add(weight)
if (!fired)
return
this.deps.eventManager.emit<AttentionEventPayload>({
type: 'perception',
payload: {
raw: event,
intensity: value,
},
source: { type: 'minecraft', id: 'perception' },
timestamp: Date.now(),
})
}
private getBucket(key: string): LeakyBucket {
const existing = this.buckets.get(key)
if (existing)
return existing
const created = new LeakyBucket({
capacity: 10,
leakPerSecond: 2,
trigger: 3,
})
this.buckets.set(key, created)
return created
}
private keyOf(event: RawPerceptionEvent): string {
switch (event.modality) {
case 'sighted':
return `sighted:${event.kind}:${(event as any).entityId ?? 'unknown'}`
case 'heard':
return `heard:${event.kind}:${event.soundId}`
case 'felt':
return `felt:${event.kind}`
default:
return 'unknown'
}
}
}
@@ -0,0 +1,35 @@
export interface LeakyBucketConfig {
capacity: number
leakPerSecond: number
trigger: number
}
export class LeakyBucket { // Shoutout to Makito for this idea
private value = 0
private readonly capacity: number
private readonly leakPerMs: number
private readonly trigger: number
constructor(config: LeakyBucketConfig) {
this.capacity = config.capacity
this.leakPerMs = config.leakPerSecond / 1000
this.trigger = config.trigger
}
public tick(deltaMs: number): void {
if (deltaMs <= 0)
return
this.value = Math.max(0, this.value - this.leakPerMs * deltaMs)
}
public add(amount: number): { fired: boolean, value: number } {
const next = Math.min(this.capacity, this.value + amount)
const fired = this.value < this.trigger && next >= this.trigger
this.value = next
return { fired, value: this.value }
}
public getValue(): number {
return this.value
}
}
@@ -0,0 +1,55 @@
import type { Logg } from '@guiiai/logg'
import type { MineflayerWithAgents } from '../types'
import type { EventManager } from './event-manager'
import type { RawPerceptionEvent } from './raw-events'
import { AttentionDetector } from './attention-detector'
import { RawEventBuffer } from './raw-event-buffer'
export class PerceptionPipeline {
private readonly buffer = new RawEventBuffer()
private readonly detector: AttentionDetector
private initialized = false
constructor(
private readonly deps: {
eventManager: EventManager
logger: Logg
},
) {
this.detector = new AttentionDetector({ eventManager: this.deps.eventManager })
}
public init(_bot: MineflayerWithAgents): void {
this.initialized = true
}
public destroy(): void {
this.buffer.clear()
this.initialized = false
}
public collect(event: RawPerceptionEvent): void {
if (!this.initialized)
return
this.buffer.push(event)
}
public tick(deltaMs: number): void {
if (!this.initialized)
return
this.detector.tick(deltaMs)
const events = this.buffer.drain()
for (const event of events) {
try {
this.detector.ingest(event)
}
catch (err) {
this.deps.logger.withError(err as Error).error('PerceptionPipeline: detector error')
}
}
}
}
@@ -0,0 +1,21 @@
import type { RawPerceptionEvent } from './raw-events'
export class RawEventBuffer {
private queue: RawPerceptionEvent[] = []
public push(event: RawPerceptionEvent): void {
this.queue.push(event)
}
public drain(): RawPerceptionEvent[] {
if (this.queue.length === 0)
return []
const drained = this.queue
this.queue = []
return drained
}
public clear(): void {
this.queue = []
}
}
@@ -0,0 +1,74 @@
import type { Vec3 } from 'vec3'
export type PerceptionModality = 'sighted' | 'heard' | 'felt'
export interface RawPerceptionEventBase {
modality: PerceptionModality
timestamp: number
source: 'minecraft'
pos?: Vec3
}
export interface SightedEntityMovedEvent extends RawPerceptionEventBase {
modality: 'sighted'
kind: 'entity_moved'
entityType: 'player' | 'mob'
entityId: string
displayName?: string
distance: number
hasLineOfSight: boolean
}
export interface SightedArmSwingEvent extends RawPerceptionEventBase {
modality: 'sighted'
kind: 'arm_swing'
entityType: 'player'
entityId: string
displayName?: string
distance: number
hasLineOfSight: boolean
}
export interface SightedSneakToggleEvent extends RawPerceptionEventBase {
modality: 'sighted'
kind: 'sneak_toggle'
entityType: 'player'
entityId: string
displayName?: string
distance: number
hasLineOfSight: boolean
sneaking: boolean
}
export type SightedEvent = SightedEntityMovedEvent | SightedArmSwingEvent | SightedSneakToggleEvent
export interface HeardSoundEvent extends RawPerceptionEventBase {
modality: 'heard'
kind: 'sound'
soundId: string
distance: number
inferredEntityType?: 'player' | 'mob'
inferredEntityId?: string
}
export type HeardEvent = HeardSoundEvent
export interface FeltDamageTakenEvent extends RawPerceptionEventBase {
modality: 'felt'
kind: 'damage_taken'
amount?: number
attackerEntityType?: 'player' | 'mob'
attackerEntityId?: string
distance?: number
}
export interface FeltItemCollectedEvent extends RawPerceptionEventBase {
modality: 'felt'
kind: 'item_collected'
itemName: string
count?: number
}
export type FeltEvent = FeltDamageTakenEvent | FeltItemCollectedEvent
export type RawPerceptionEvent = SightedEvent | HeardEvent | FeltEvent