From 8beac18ced41b7c0b98f9cde15fc686f02133752 Mon Sep 17 00:00:00 2001 From: Rin Date: Tue, 13 Jan 2026 01:10:39 +0800 Subject: [PATCH] feat(minecraft): semantic percetion, belief and state pulling --- .../src/cognitive/conscious/brain.ts | 15 +- services/minecraft/src/cognitive/container.ts | 8 +- .../cognitive/perception/perception-api.ts | 148 ++++++++++++++++++ .../src/cognitive/perception/pipeline.ts | 36 +++++ .../src/cognitive/reflex/behaviors/teabag.ts | 30 ++-- .../cognitive/reflex/reflex-manager.test.ts | 9 +- .../src/cognitive/reflex/reflex-manager.ts | 4 +- .../minecraft/src/cognitive/reflex/runtime.ts | 10 +- .../src/cognitive/reflex/types/behavior.ts | 6 +- .../src/cognitive/semantics/belief-engine.ts | 77 +++++++++ .../src/cognitive/semantics/index.ts | 2 + .../cognitive/semantics/patterns/teabag.ts | 56 +++++++ .../src/cognitive/world/entity-store.ts | 126 +++++++++++++++ .../minecraft/src/cognitive/world/index.ts | 3 + .../src/cognitive/world/temporal-buffer.ts | 85 ++++++++++ .../minecraft/src/cognitive/world/types.ts | 72 +++++++++ 16 files changed, 658 insertions(+), 29 deletions(-) create mode 100644 services/minecraft/src/cognitive/perception/perception-api.ts create mode 100644 services/minecraft/src/cognitive/semantics/belief-engine.ts create mode 100644 services/minecraft/src/cognitive/semantics/index.ts create mode 100644 services/minecraft/src/cognitive/semantics/patterns/teabag.ts create mode 100644 services/minecraft/src/cognitive/world/entity-store.ts create mode 100644 services/minecraft/src/cognitive/world/index.ts create mode 100644 services/minecraft/src/cognitive/world/temporal-buffer.ts create mode 100644 services/minecraft/src/cognitive/world/types.ts diff --git a/services/minecraft/src/cognitive/conscious/brain.ts b/services/minecraft/src/cognitive/conscious/brain.ts index 4a6449376..c63e6e3c0 100644 --- a/services/minecraft/src/cognitive/conscious/brain.ts +++ b/services/minecraft/src/cognitive/conscious/brain.ts @@ -56,11 +56,16 @@ export class Brain { public init(bot: MineflayerWithAgents): void { this.log('INFO', 'Brain: Initializing...') - // Unified Perception Signal Handler - // this.deps.eventManager.on('perception', async (event) => { - // this.log('INFO', `Brain: Received perception signal: ${event.payload.type} - ${event.payload.description}`) - // await this.enqueueEvent(bot, event) - // }) + // Perception Signal Handler - Only process chat messages for now + this.deps.eventManager.on('perception', async (event) => { + const signal = event.payload + // Only handle chat messages in the deliberative layer + if (signal.type !== 'chat_message') + return + + this.log('INFO', `Brain: Received chat: ${signal.description}`) + await this.enqueueEvent(bot, event) + }) // Listen to Task Execution Events (Action Feedback) this.deps.taskExecutor.on('action:completed', async ({ action, result }) => { diff --git a/services/minecraft/src/cognitive/container.ts b/services/minecraft/src/cognitive/container.ts index bde89a575..8638818d5 100644 --- a/services/minecraft/src/cognitive/container.ts +++ b/services/minecraft/src/cognitive/container.ts @@ -117,8 +117,12 @@ export function createAgentContainer(options: { }), // Reflex Manager (Reactive Layer) - reflexManager: asFunction(({ eventBus, logger }) => - new ReflexManager({ eventBus, logger }), + reflexManager: asFunction(({ eventBus, perceptionPipeline, logger }) => + new ReflexManager({ + eventBus, + perception: perceptionPipeline.getPerceptionAPI(), + logger, + }), ).singleton(), }) diff --git a/services/minecraft/src/cognitive/perception/perception-api.ts b/services/minecraft/src/cognitive/perception/perception-api.ts new file mode 100644 index 000000000..ce5bb0aaa --- /dev/null +++ b/services/minecraft/src/cognitive/perception/perception-api.ts @@ -0,0 +1,148 @@ +import type { Logg } from '@guiiai/logg' + +import type { Belief, EntityState, EntityView } from '../world/types' + +import { BeliefEngine } from '../semantics/belief-engine' +import { teabagPattern } from '../semantics/patterns/teabag' +import { EntityStore } from '../world/entity-store' +import { TemporalBuffer } from '../world/temporal-buffer' + +/** + * Unified perception API for upper layers + * Provides entity queries and belief computations + */ +export class PerceptionAPI { + private store: EntityStore + private buffer: TemporalBuffer + private engine: BeliefEngine + + constructor( + private readonly deps: { + logger: Logg + }, + ) { + this.deps.logger.log('PerceptionAPI: initialized') + this.store = new EntityStore() + this.buffer = new TemporalBuffer(5000) // 5s history + this.engine = new BeliefEngine() + + // Register default patterns + this.engine.register(teabagPattern) + } + + // ============ Entity Updates (from Mineflayer) ============ + + /** + * Update an entity's state (called by perception collector) + */ + updateEntity(id: string, partial: Partial): void { + const changes = this.store.update(id, partial) + this.buffer.recordAll(changes) + } + + /** + * Remove an entity + */ + removeEntity(id: string): void { + this.store.remove(id) + this.buffer.clearEntity(id) + } + + /** + * Update self position (for distance calculations) + */ + updateSelfPosition(x: number, y: number, z: number): void { + this.store.updateSelfPosition({ x, y, z } as any) + } + + // ============ Entity Queries ============ + + /** + * Get all player entities with computed beliefs + */ + getPlayers(): EntityView[] { + return this.store.getPlayers().map(e => this.buildEntityView(e)) + } + + /** + * Get a specific entity by ID + */ + getEntity(id: string): EntityView | null { + const state = this.store.get(id) + if (!state) + return null + return this.buildEntityView(state) + } + + // ============ Belief Queries ============ + + /** + * Find entities with high confidence of a pattern + */ + entitiesWithBelief(pattern: string, minConfidence: number = 0.5): EntityView[] { + return this.getPlayers().filter(e => (e.beliefs[pattern]?.confidence ?? 0) >= minConfidence) + } + + /** + * Get the top belief for an entity + */ + getTopBelief(entityId: string): { pattern: string, belief: Belief } | null { + const entity = this.getEntity(entityId) + if (!entity) + return null + + let top: { pattern: string, belief: Belief } | null = null + for (const [pattern, belief] of Object.entries(entity.beliefs)) { + if (!top || belief.confidence > top.belief.confidence) { + top = { pattern, belief } + } + } + return top + } + + // ============ Pattern Management ============ + + /** + * Register a custom pattern + */ + registerPattern(pattern: Parameters[0]): void { + this.engine.register(pattern) + } + + // ============ Maintenance ============ + + /** + * Prune old history entries + */ + prune(): void { + this.buffer.prune() + } + + /** + * Clear all state + */ + clear(): void { + this.store.clear() + this.buffer.clear() + } + + // ============ Internal ============ + + private buildEntityView(state: EntityState): EntityView { + const beliefs = this.engine.computeBeliefs( + state.id, + id => this.store.get(id), + (id, since) => this.buffer.query(id, since), + this.store.getSelfPosition(), + ) + + return { + id: state.id, + name: state.name ?? state.id, + type: state.type, + state, + beliefs, + distanceToSelf: this.store.distanceToSelf(state.id) ?? Infinity, + } + } +} diff --git a/services/minecraft/src/cognitive/perception/pipeline.ts b/services/minecraft/src/cognitive/perception/pipeline.ts index 46e448814..2fdb6b036 100644 --- a/services/minecraft/src/cognitive/perception/pipeline.ts +++ b/services/minecraft/src/cognitive/perception/pipeline.ts @@ -11,10 +11,12 @@ import type { PerceptionStage } from './types/stage' import { DebugService } from '../../debug' import { createPerceptionFrameFromRawEvent } from './frame' import { MineflayerPerceptionCollector } from './mineflayer-perception-collector' +import { PerceptionAPI } from './perception-api' import { SaliencyDetector } from './saliency-detector' export class PerceptionPipeline { private readonly detector: SaliencyDetector + private readonly perception: PerceptionAPI private collector: MineflayerPerceptionCollector | null = null private initialized = false @@ -31,6 +33,8 @@ export class PerceptionPipeline { logger: Logg }, ) { + this.perception = new PerceptionAPI({ logger: this.deps.logger }) + this.detector = new SaliencyDetector({ logger: this.deps.logger, onAttention: (signal) => { @@ -44,6 +48,31 @@ export class PerceptionPipeline { }) this.stages = [ + { + name: 'entity_update', + handle: (frame) => { + if (frame.kind !== 'world_raw') + return frame + + const raw = frame.raw as RawPerceptionEvent + + // Feed entity updates to PerceptionAPI + if ('entityId' in raw && 'entityType' in raw) { + const entityRaw = raw as RawPerceptionEvent & { entityId: string, entityType: string, displayName?: string, pos?: { x: number, y: number, z: number } } + if (entityRaw.entityType === 'player') { + this.perception.updateEntity(entityRaw.entityId, { + id: entityRaw.entityId, + type: 'player', + name: entityRaw.displayName, + position: entityRaw.pos as any, + isSneaking: 'sneaking' in entityRaw ? (entityRaw as any).sneaking : undefined, + }) + } + } + + return frame + }, + }, { name: 'attention', handle: (frame) => { @@ -152,6 +181,13 @@ export class PerceptionPipeline { this.initialized = false } + /** + * Get the PerceptionAPI for querying entity beliefs + */ + public getPerceptionAPI(): PerceptionAPI { + return this.perception + } + public ingest(frame: PerceptionFrame): void { if (!this.initialized) return diff --git a/services/minecraft/src/cognitive/reflex/behaviors/teabag.ts b/services/minecraft/src/cognitive/reflex/behaviors/teabag.ts index ef236b7ee..c6ed6af80 100644 --- a/services/minecraft/src/cognitive/reflex/behaviors/teabag.ts +++ b/services/minecraft/src/cognitive/reflex/behaviors/teabag.ts @@ -5,22 +5,24 @@ export const teabagBehavior: ReflexBehavior = { modes: ['social', 'idle'], cooldownMs: 5000, - when: (ctx) => { - // Check if we recently received a teabag signal - // Check if we recently received a teabag signal - if (ctx.social.lastGesture === 'teabag') { - const now = Date.now() - const signalAge = now - (ctx.social.lastGestureAt || 0) - - // Only respond if signal is fresh (< 2s) - return signalAge < 2000 - } - return false + when: (_ctx, api) => { + // Check if any player is teabagging with high confidence + if (!api?.perception) + return false + const teabaggers = api.perception.entitiesWithBelief('teabag', 0.6) + return teabaggers.length > 0 }, - score: () => { - // Higher priority than LookAt (50) - return 60 + score: (_ctx, api) => { + // Higher priority than LookAt (50), scaled by confidence + if (!api?.perception) + return 0 + const teabaggers = api.perception.entitiesWithBelief('teabag', 0.6) + if (teabaggers.length === 0) + return 0 + // Use highest confidence as score boost + const maxConfidence = Math.max(...teabaggers.map(e => e.beliefs.teabag?.confidence ?? 0)) + return 60 + (maxConfidence * 20) }, run: async ({ bot }) => { diff --git a/services/minecraft/src/cognitive/reflex/reflex-manager.test.ts b/services/minecraft/src/cognitive/reflex/reflex-manager.test.ts index f3972ce49..a5871f922 100644 --- a/services/minecraft/src/cognitive/reflex/reflex-manager.test.ts +++ b/services/minecraft/src/cognitive/reflex/reflex-manager.test.ts @@ -43,7 +43,14 @@ describe('reflexManager', () => { } as any const logger = makeLogger() - const reflex = new ReflexManager({ eventBus, logger }) // Now accepts eventBus + const perception = { + getPlayers: vi.fn(() => []), + getEntity: vi.fn(() => null), + entitiesWithBelief: vi.fn(() => []), + updateEntity: vi.fn(), + updateSelfPosition: vi.fn(), + } as any + const reflex = new ReflexManager({ eventBus, perception, logger }) const bot = makeBot() reflex.init(bot) diff --git a/services/minecraft/src/cognitive/reflex/reflex-manager.ts b/services/minecraft/src/cognitive/reflex/reflex-manager.ts index f69bcb375..db7593888 100644 --- a/services/minecraft/src/cognitive/reflex/reflex-manager.ts +++ b/services/minecraft/src/cognitive/reflex/reflex-manager.ts @@ -1,6 +1,7 @@ import type { Logg } from '@guiiai/logg' 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' @@ -19,6 +20,7 @@ export class ReflexManager { constructor( private readonly deps: { eventBus: EventBus + perception: PerceptionAPI logger: Logg }, ) { @@ -86,7 +88,7 @@ export class ReflexManager { // For greeting behavior compatibility, we might need to map specific signals to social state. // Trigger behavior selection - this.runtime.tick(bot, 0) + this.runtime.tick(bot, 0, this.deps.perception) // Emit reflex state for observability DebugService.getInstance().emitReflexState({ diff --git a/services/minecraft/src/cognitive/reflex/runtime.ts b/services/minecraft/src/cognitive/reflex/runtime.ts index 5a88aabe2..9f6b3ce18 100644 --- a/services/minecraft/src/cognitive/reflex/runtime.ts +++ b/services/minecraft/src/cognitive/reflex/runtime.ts @@ -1,5 +1,6 @@ import type { Logg } from '@guiiai/logg' +import type { PerceptionAPI } from '../perception/perception-api' import type { MineflayerWithAgents } from '../types' import type { ReflexModeId } from './modes' import type { ReflexBehavior } from './types/behavior' @@ -38,7 +39,7 @@ export class ReflexRuntime { this.behaviors.push(behavior) } - public tick(bot: MineflayerWithAgents, deltaMs: number): string | null { + public tick(bot: MineflayerWithAgents, deltaMs: number, perception: PerceptionAPI): string | null { const now = Date.now() this.context.updateNow(now) @@ -73,16 +74,17 @@ export class ReflexRuntime { this.activeBehaviorUntil = null const ctx = this.context.getSnapshot() + const api = { bot, context: this.context, perception } let best: { behavior: ReflexBehavior, score: number } | null = null for (const behavior of this.behaviors) { if (!behavior.modes.includes(this.mode)) continue - if (!behavior.when(ctx)) + if (!behavior.when(ctx, api)) continue - const score = behavior.score(ctx) + const score = behavior.score(ctx, api) if (score <= 0) continue @@ -102,7 +104,7 @@ export class ReflexRuntime { this.runHistory.set(best.behavior.id, { lastRunAt: now }) try { - const maybePromise = best.behavior.run({ bot, context: this.context }) + const maybePromise = best.behavior.run(api) if (maybePromise && typeof (maybePromise as any).then === 'function') { this.activeBehaviorUntil = now + Math.max(deltaMs, 50) void (maybePromise as Promise).finally(() => { diff --git a/services/minecraft/src/cognitive/reflex/types/behavior.ts b/services/minecraft/src/cognitive/reflex/types/behavior.ts index e820b2fc6..5032586c9 100644 --- a/services/minecraft/src/cognitive/reflex/types/behavior.ts +++ b/services/minecraft/src/cognitive/reflex/types/behavior.ts @@ -1,3 +1,4 @@ +import type { PerceptionAPI } from '../../perception/perception-api' import type { MineflayerWithAgents } from '../../types' import type { ReflexContext } from '../context' import type { ReflexModeId } from '../modes' @@ -5,14 +6,15 @@ import type { ReflexModeId } from '../modes' export interface ReflexApi { bot: MineflayerWithAgents context: ReflexContext + perception: PerceptionAPI } export interface ReflexBehavior { id: string modes: ReflexModeId[] cooldownMs?: number - when: (ctx: ReturnType) => boolean - score: (ctx: ReturnType) => number + when: (ctx: ReturnType, api?: ReflexApi) => boolean + score: (ctx: ReturnType, api?: ReflexApi) => number run: (api: ReflexApi) => Promise | void } diff --git a/services/minecraft/src/cognitive/semantics/belief-engine.ts b/services/minecraft/src/cognitive/semantics/belief-engine.ts new file mode 100644 index 000000000..0b4c0252a --- /dev/null +++ b/services/minecraft/src/cognitive/semantics/belief-engine.ts @@ -0,0 +1,77 @@ +import type { Vec3 } from 'vec3' + +import type { Belief, EntityState, PatternDefinition, StateChange } from '../world/types' + +/** + * Computes beliefs about entity behaviors based on registered patterns + */ +export class BeliefEngine { + private patterns: Map = new Map() + + /** + * Register a pattern + */ + register(pattern: PatternDefinition): void { + this.patterns.set(pattern.id, pattern) + } + + /** + * Unregister a pattern + */ + unregister(id: string): void { + this.patterns.delete(id) + } + + /** + * Get all registered pattern IDs + */ + getPatternIds(): string[] { + return Array.from(this.patterns.keys()) + } + + /** + * Compute all beliefs for an entity + */ + computeBeliefs( + entityId: string, + getState: (id: string) => EntityState | null, + getHistory: (id: string, since: number) => StateChange[], + selfPosition: Vec3 | null, + ): Record { + const beliefs: Record = {} + + for (const [id, pattern] of this.patterns) { + try { + beliefs[id] = pattern.compute(entityId, getState, getHistory, selfPosition) + } + catch { + // Pattern threw an error, treat as no belief + beliefs[id] = { confidence: 0 } + } + } + + return beliefs + } + + /** + * Compute a single belief for an entity + */ + computeBelief( + patternId: string, + entityId: string, + getState: (id: string) => EntityState | null, + getHistory: (id: string, since: number) => StateChange[], + selfPosition: Vec3 | null, + ): Belief | null { + const pattern = this.patterns.get(patternId) + if (!pattern) + return null + + try { + return pattern.compute(entityId, getState, getHistory, selfPosition) + } + catch { + return { confidence: 0 } + } + } +} diff --git a/services/minecraft/src/cognitive/semantics/index.ts b/services/minecraft/src/cognitive/semantics/index.ts new file mode 100644 index 000000000..f4f074b30 --- /dev/null +++ b/services/minecraft/src/cognitive/semantics/index.ts @@ -0,0 +1,2 @@ +export { BeliefEngine } from './belief-engine' +export { teabagPattern } from './patterns/teabag' diff --git a/services/minecraft/src/cognitive/semantics/patterns/teabag.ts b/services/minecraft/src/cognitive/semantics/patterns/teabag.ts new file mode 100644 index 000000000..d4052e6b0 --- /dev/null +++ b/services/minecraft/src/cognitive/semantics/patterns/teabag.ts @@ -0,0 +1,56 @@ +import type { PatternDefinition } from '../../world/types' + +/** + * Teabag pattern: detects rapid crouching behavior + */ +export const teabagPattern: PatternDefinition = { + id: 'teabag', + category: 'social', + description: 'Rapid crouching, typically a greeting or taunt', + + compute(entityId, getState, getHistory, selfPosition) { + // Get sneaking state changes in the last 2 seconds + const since = Date.now() - 2000 + const changes = getHistory(entityId, since).filter(c => c.field === 'isSneaking') + + // Need at least 4 toggles (2 full crouch cycles) + if (changes.length < 4) { + return { confidence: 0 } + } + + // Check distance if we have self position + const entity = getState(entityId) + if (entity && selfPosition) { + const dx = entity.position.x - selfPosition.x + const dy = entity.position.y - selfPosition.y + const dz = entity.position.z - selfPosition.z + const distance = Math.sqrt(dx * dx + dy * dy + dz * dz) + + // Too far away, reduce confidence + if (distance > 15) { + return { confidence: 0, data: { toggles: changes.length, distance } } + } + } + + // Calculate frequency (toggles per second) + const duration = (changes[changes.length - 1].timestamp - changes[0].timestamp) / 1000 + const frequency = duration > 0 ? changes.length / duration : 0 + + // Confidence based on: + // - Number of toggles (more = more confident, up to 8) + // - Frequency (faster = more confident, up to 4 Hz) + const countFactor = Math.min(1, changes.length / 8) + const frequencyFactor = Math.min(1, frequency / 4) + + const confidence = (countFactor * 0.4) + (frequencyFactor * 0.6) + + return { + confidence, + data: { + toggles: changes.length, + frequency: Math.round(frequency * 100) / 100, + duration: Math.round(duration * 1000), + }, + } + }, +} diff --git a/services/minecraft/src/cognitive/world/entity-store.ts b/services/minecraft/src/cognitive/world/entity-store.ts new file mode 100644 index 000000000..332511ddf --- /dev/null +++ b/services/minecraft/src/cognitive/world/entity-store.ts @@ -0,0 +1,126 @@ +import type { Vec3 } from 'vec3' + +import type { EntityState, StateChange } from './types' + +/** + * Tracks the state of all known entities + */ +export class EntityStore { + private entities: Map = new Map() + private selfPosition: Vec3 | null = null + + /** + * Update or create an entity's state + * Returns list of state changes that occurred + */ + update(id: string, partial: Partial): StateChange[] { + const now = Date.now() + const changes: StateChange[] = [] + const existing = this.entities.get(id) + + if (!existing) { + // New entity + const newState: EntityState = { + id, + type: partial.type ?? 'player', + name: partial.name, + position: partial.position ?? { x: 0, y: 0, z: 0 } as Vec3, + velocity: partial.velocity ?? { x: 0, y: 0, z: 0 } as Vec3, + yaw: partial.yaw ?? 0, + pitch: partial.pitch ?? 0, + isSneaking: partial.isSneaking ?? false, + isSprinting: partial.isSprinting ?? false, + onGround: partial.onGround ?? true, + firstSeen: now, + lastUpdate: now, + } + this.entities.set(id, newState) + return changes + } + + // Track changes to relevant fields + const trackedFields: (keyof EntityState)[] = ['isSneaking', 'isSprinting', 'onGround'] + + for (const field of trackedFields) { + if (field in partial && partial[field] !== existing[field]) { + changes.push({ + entityId: id, + field, + from: existing[field], + to: partial[field], + timestamp: now, + }) + } + } + + // Apply updates + Object.assign(existing, partial, { lastUpdate: now }) + + return changes + } + + /** + * Get entity by ID + */ + get(id: string): EntityState | null { + return this.entities.get(id) ?? null + } + + /** + * Get all player entities + */ + getPlayers(): EntityState[] { + return Array.from(this.entities.values()).filter(e => e.type === 'player') + } + + /** + * Get all entity IDs + */ + getAllIds(): string[] { + return Array.from(this.entities.keys()) + } + + /** + * Remove an entity + */ + remove(id: string): void { + this.entities.delete(id) + } + + /** + * Update self (bot) position for distance calculations + */ + updateSelfPosition(pos: Vec3): void { + this.selfPosition = pos + } + + /** + * Get self position + */ + getSelfPosition(): Vec3 | null { + return this.selfPosition + } + + /** + * Calculate distance from self to entity + */ + distanceToSelf(id: string): number | null { + if (!this.selfPosition) + return null + const entity = this.entities.get(id) + if (!entity) + return null + const dx = entity.position.x - this.selfPosition.x + const dy = entity.position.y - this.selfPosition.y + const dz = entity.position.z - this.selfPosition.z + return Math.sqrt(dx * dx + dy * dy + dz * dz) + } + + /** + * Clear all entities + */ + clear(): void { + this.entities.clear() + this.selfPosition = null + } +} diff --git a/services/minecraft/src/cognitive/world/index.ts b/services/minecraft/src/cognitive/world/index.ts new file mode 100644 index 000000000..7b31f369a --- /dev/null +++ b/services/minecraft/src/cognitive/world/index.ts @@ -0,0 +1,3 @@ +export { EntityStore } from './entity-store' +export { TemporalBuffer } from './temporal-buffer' +export * from './types' diff --git a/services/minecraft/src/cognitive/world/temporal-buffer.ts b/services/minecraft/src/cognitive/world/temporal-buffer.ts new file mode 100644 index 000000000..23ddba0d1 --- /dev/null +++ b/services/minecraft/src/cognitive/world/temporal-buffer.ts @@ -0,0 +1,85 @@ +import type { StateChange } from './types' + +/** + * Rolling window buffer of state changes per entity + */ +export class TemporalBuffer { + private buffer: Map = new Map() + private maxAge: number + + constructor(maxAgeMs: number = 5000) { + this.maxAge = maxAgeMs + } + + /** + * Record a state change + */ + record(change: StateChange): void { + const list = this.buffer.get(change.entityId) ?? [] + list.push(change) + this.buffer.set(change.entityId, list) + } + + /** + * Record multiple state changes + */ + recordAll(changes: StateChange[]): void { + for (const change of changes) { + this.record(change) + } + } + + /** + * Query changes for an entity since a given timestamp + */ + query(entityId: string, since: number): StateChange[] { + const list = this.buffer.get(entityId) ?? [] + return list.filter(c => c.timestamp >= since) + } + + /** + * Query changes for an entity by field + */ + queryField(entityId: string, field: string, since: number): StateChange[] { + return this.query(entityId, since).filter(c => c.field === field) + } + + /** + * Get all changes for an entity (within max age) + */ + getAll(entityId: string): StateChange[] { + const cutoff = Date.now() - this.maxAge + return this.query(entityId, cutoff) + } + + /** + * Prune old entries from all buffers + */ + prune(): void { + const cutoff = Date.now() - this.maxAge + + for (const [entityId, list] of this.buffer.entries()) { + const filtered = list.filter(c => c.timestamp >= cutoff) + if (filtered.length === 0) { + this.buffer.delete(entityId) + } + else { + this.buffer.set(entityId, filtered) + } + } + } + + /** + * Clear all history for an entity + */ + clearEntity(entityId: string): void { + this.buffer.delete(entityId) + } + + /** + * Clear all buffers + */ + clear(): void { + this.buffer.clear() + } +} diff --git a/services/minecraft/src/cognitive/world/types.ts b/services/minecraft/src/cognitive/world/types.ts new file mode 100644 index 000000000..1309627bd --- /dev/null +++ b/services/minecraft/src/cognitive/world/types.ts @@ -0,0 +1,72 @@ +import type { Vec3 } from 'vec3' + +/** + * State of an entity at a point in time + */ +export interface EntityState { + id: string + type: 'player' | 'mob' | 'item' + name?: string + + // Position & Movement + position: Vec3 + velocity: Vec3 + yaw: number + pitch: number + + // Status flags + isSneaking: boolean + isSprinting: boolean + onGround: boolean + + // Timestamps + firstSeen: number + lastUpdate: number +} + +/** + * A change in entity state + */ +export interface StateChange { + entityId: string + field: keyof EntityState + from: unknown + to: unknown + timestamp: number +} + +/** + * Belief about an entity's behavior + */ +export interface Belief { + confidence: number // 0-1 + data?: Record +} + +/** + * Definition of a pattern that computes beliefs + */ +export interface PatternDefinition { + id: string + category: 'social' | 'spatial' | 'threat' | 'neutral' + description: string + + compute: ( + entityId: string, + getState: (id: string) => EntityState | null, + getHistory: (id: string, since: number) => StateChange[], + selfPosition: Vec3 | null, + ) => Belief +} + +/** + * View of an entity for upper layers + */ +export interface EntityView { + id: string + name: string + type: EntityState['type'] + state: EntityState + beliefs: Record + distanceToSelf: number +}