feat(minecraft): scaffold raw event pipeline with leaky bucket + DI wiring
This commit is contained in:
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user