From a3a29ae21aa97c15bd2a53cb4606ab685ffca764 Mon Sep 17 00:00:00 2001 From: Rin Date: Fri, 9 Jan 2026 06:04:22 +0800 Subject: [PATCH] feat(minecraft): scaffold raw event pipeline with leaky bucket + DI wiring --- services/minecraft/src/cognitive/container.ts | 4 + services/minecraft/src/cognitive/index.ts | 11 +++ .../perception/attention-detector.ts | 70 ++++++++++++++++++ .../src/cognitive/perception/leaky-bucket.ts | 35 +++++++++ .../src/cognitive/perception/pipeline.ts | 55 ++++++++++++++ .../cognitive/perception/raw-event-buffer.ts | 21 ++++++ .../src/cognitive/perception/raw-events.ts | 74 +++++++++++++++++++ 7 files changed, 270 insertions(+) create mode 100644 services/minecraft/src/cognitive/perception/attention-detector.ts create mode 100644 services/minecraft/src/cognitive/perception/leaky-bucket.ts create mode 100644 services/minecraft/src/cognitive/perception/pipeline.ts create mode 100644 services/minecraft/src/cognitive/perception/raw-event-buffer.ts create mode 100644 services/minecraft/src/cognitive/perception/raw-events.ts diff --git a/services/minecraft/src/cognitive/container.ts b/services/minecraft/src/cognitive/container.ts index ab5600a21..7f259d448 100644 --- a/services/minecraft/src/cognitive/container.ts +++ b/services/minecraft/src/cognitive/container.ts @@ -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(), diff --git a/services/minecraft/src/cognitive/index.ts b/services/minecraft/src/cognitive/index.ts index 01c424234..df9f83ba8 100644 --- a/services/minecraft/src/cognitive/index.ts +++ b/services/minecraft/src/cognitive/index.ts @@ -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') diff --git a/services/minecraft/src/cognitive/perception/attention-detector.ts b/services/minecraft/src/cognitive/perception/attention-detector.ts new file mode 100644 index 000000000..177bc098b --- /dev/null +++ b/services/minecraft/src/cognitive/perception/attention-detector.ts @@ -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() + + 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({ + 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' + } + } +} diff --git a/services/minecraft/src/cognitive/perception/leaky-bucket.ts b/services/minecraft/src/cognitive/perception/leaky-bucket.ts new file mode 100644 index 000000000..af8dfe619 --- /dev/null +++ b/services/minecraft/src/cognitive/perception/leaky-bucket.ts @@ -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 + } +} diff --git a/services/minecraft/src/cognitive/perception/pipeline.ts b/services/minecraft/src/cognitive/perception/pipeline.ts new file mode 100644 index 000000000..1bbdb4211 --- /dev/null +++ b/services/minecraft/src/cognitive/perception/pipeline.ts @@ -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') + } + } + } +} diff --git a/services/minecraft/src/cognitive/perception/raw-event-buffer.ts b/services/minecraft/src/cognitive/perception/raw-event-buffer.ts new file mode 100644 index 000000000..19cc83f9c --- /dev/null +++ b/services/minecraft/src/cognitive/perception/raw-event-buffer.ts @@ -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 = [] + } +} diff --git a/services/minecraft/src/cognitive/perception/raw-events.ts b/services/minecraft/src/cognitive/perception/raw-events.ts new file mode 100644 index 000000000..70ddd4c18 --- /dev/null +++ b/services/minecraft/src/cognitive/perception/raw-events.ts @@ -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