refactor(minecraft): further cleanup

This commit is contained in:
Rin
2026-02-18 11:14:45 +08:00
committed by Neko Ayaka
parent 13805ab7b5
commit aa2d7af17c
27 changed files with 57 additions and 988 deletions
@@ -42,20 +42,15 @@ export class ActionRegistry {
throw new Error(`Unknown action: ${step.tool}`)
}
try {
const actionFn = action.perform(this.mineflayer)
const { schema } = action
const parsedParams = schema.parse(step.params || {})
const actionFn = action.perform(this.mineflayer)
const { schema } = action
const parsedParams = schema.parse(step.params || {})
// Extract parameter values in the order defined by the schema
const paramValues = Object.keys((schema as any).shape || {}).map(key => parsedParams[key])
// Extract parameter values in the order defined by the schema
const paramValues = Object.keys((schema as any).shape || {}).map(key => parsedParams[key])
const result = await actionFn(...paramValues)
return result ?? `Action ${step.tool} completed`
}
catch (error) {
throw error
}
const result = await actionFn(...paramValues)
return result ?? `Action ${step.tool} completed`
}
/**
@@ -48,4 +48,12 @@ describe('llm-actions mineBlockAt', () => {
await expect(perform(1, 2, 3, 'torch')).rejects.toThrow(/Block type mismatch/i)
expect(breakBlockAt).not.toHaveBeenCalled()
})
it('exposes skip tool with stable return value', async () => {
const skipAction = actionsList.find(item => item.name === 'skip')
expect(skipAction).toBeDefined()
const perform = skipAction!.perform({} as any)
expect(perform()).toBe('Skipped turn')
})
})
@@ -47,23 +47,13 @@ export const actionsList: Action[] = [
}),
perform: () => (reason: string, cooldown_seconds: number): string => `Gave up for ${cooldown_seconds}s: ${reason}`,
},
// {\n // name: 'setReflexMode',
// description: 'Set (or clear) your reflex mode override. Use work/wander to disable idle-only reflex behaviors. Set override to null to return to automatic mode selection.',
// execution: 'sequential',
// schema: z.object({
// mode: z.enum(['idle', 'social', 'alert', 'work', 'wander']).nullable().describe('Mode override to set'),
// }),
// perform: mineflayer => async (mode: 'idle' | 'social' | 'alert' | 'work' | 'wander' | null) => {
// const reflexManager = (mineflayer as any).reflexManager
// if (!reflexManager || typeof reflexManager.setModeOverride !== 'function')
// throw new Error('ReflexManager is not available on this bot. Is CognitiveEngine enabled?')
// reflexManager.setModeOverride(mode)
// return mode
// ? `Reflex mode override set to '${mode}'.`
// : 'Reflex mode override cleared (automatic mode selection resumed).'
// },
// },
{
name: 'skip',
description: 'Skip this turn without performing any world action.',
execution: 'sync',
schema: z.object({}),
perform: () => (): string => 'Skipped turn',
},
{
name: 'stop',
description: 'Force stop all actions', // TODO: include name of the current action in description?
@@ -177,17 +167,6 @@ export const actionsList: Action[] = [
}
},
},
// {
// name: 'moveAway',
// description: 'Move away from the current location in any direction by a given distance.',
// schema: z.object({
// distance: z.number().describe('The distance to move away.').min(0),
// }),
// perform: mineflayer => async (distance: number) => {
// await skills.moveAway(mineflayer, distance)
// return 'Moved away'
// },
// },
{
name: 'givePlayer',
description: 'Give the specified item to the given player.',
@@ -252,15 +231,6 @@ export const actionsList: Action[] = [
return `Took [${item_name}]x${num} from chest`
},
},
// {
// name: 'viewChest',
// description: 'View the items/counts of the nearest chest.',
// schema: z.object({}),
// perform: mineflayer => async () => {
// await viewChest(mineflayer)
// return 'Viewed chest contents'
// },
// },
{
name: 'discard',
description: 'Discard the given item from the inventory.',
@@ -16,7 +16,6 @@ export class TaskExecutor extends EventEmitter {
private logger: Logger
private initialized = false
private actionRegistry: ActionRegistry
private mineflayer: Mineflayer | null = null
constructor(config: TaskExecutorConfig) {
super()
@@ -36,7 +35,6 @@ export class TaskExecutor extends EventEmitter {
* Set the mineflayer instance for action execution
*/
public setMineflayer(mineflayer: Mineflayer): void {
this.mineflayer = mineflayer
this.actionRegistry.setMineflayer(mineflayer)
}
@@ -70,34 +68,12 @@ export class TaskExecutor extends EventEmitter {
this.emit('action:started', { action })
try {
let result: unknown
if (action.tool === 'chat') {
// Handle chat action via mineflayer directly
const message = action.params.message
if (typeof message !== 'string' || message.trim().length === 0)
throw new Error('Invalid chat tool params: expected params.message to be a string')
if (!this.mineflayer) {
throw new Error('Mineflayer instance not set in TaskExecutor')
}
this.mineflayer.bot.chat(message)
result = `Sent message: "${message}"`
}
else if (action.tool === 'skip') {
result = 'Skipped turn'
}
else {
// Dispatch to ActionRegistry
const step = {
description: action.tool,
tool: action.tool,
params: action.params,
}
result = await this.actionRegistry.performAction(step)
const step = {
description: action.tool,
tool: action.tool,
params: action.params,
}
const result = await this.actionRegistry.performAction(step)
this.emit('action:completed', { action, result })
return result
@@ -7,14 +7,6 @@ export interface ActionInstruction {
params: Record<string, unknown>
}
/**
* LLM response format for the stateful agent.
* Legacy JSON response format retained for backward compatibility.
*/
export interface LLMResponse {
action: ActionInstruction
}
/**
* PlanStep for action planning - compatible with ActionInstruction
*/
@@ -23,6 +23,11 @@ const actions: Action[] = [
})),
]
const actionsWithSkip: Action[] = [
createAction('skip', z.object({})),
...actions,
]
describe('javaScriptPlanner', () => {
const globals = {
event: {
@@ -192,6 +197,21 @@ describe('javaScriptPlanner', () => {
await expect(planner.evaluate('await skip(); await chat("oops")', actions, globals, executeAction)).rejects.toThrow(/skip\(\) cannot be mixed/i)
})
it('allows evaluate when action catalog also includes skip', async () => {
const planner = new JavaScriptPlanner()
const executeAction = vi.fn(async action => `ok:${action.tool}`)
await expect(planner.evaluate('await skip()', actionsWithSkip, globals, executeAction)).resolves.toMatchObject({
actions: [
{
action: { tool: 'skip', params: {} },
ok: true,
result: 'Skipped turn',
},
],
})
})
it('returns structured validation failures without aborting the script', async () => {
const planner = new JavaScriptPlanner()
const executeAction = vi.fn(async action => `ok:${action.tool}`)
@@ -417,6 +417,10 @@ export class JavaScriptPlanner {
private installActionTools(availableActions: Action[]): void {
for (const action of availableActions) {
const existing = Object.getOwnPropertyDescriptor(this.sandbox, action.name)
if (existing && existing.configurable === false)
continue
this.defineUpdatableGlobal(action.name, async (...args: unknown[]) => {
const params = this.mapArgsToParams(action, args)
return this.runAction(action.name, params)
@@ -1,219 +0,0 @@
import type { Logg } from '@guiiai/logg'
import type { Plan } from '../../libs/mineflayer/base-agent'
import type { TaskContext, TaskStatus } from './task-state'
import { createCancellationToken } from './task-state'
export class TaskManager {
private primaryTask: TaskContext | null = null
private secondaryTasks: Map<string, TaskContext> = new Map()
private taskHistory: TaskContext[] = []
private readonly maxHistorySize = 10
constructor(private readonly logger: Logg) {}
/**
* Create a new task.
* If strictlySecondary is true, it will always be created as specific type (e.g. for forced background tasks).
* Otherwise, if no primary task exists, new task becomes primary.
*/
public createTask(goal: string): TaskContext {
const task: TaskContext = {
id: this.generateTaskId(),
goal,
status: 'idle',
startTime: Date.now(),
cancellationToken: createCancellationToken(),
}
if (!this.primaryTask) {
this.primaryTask = task
this.logger.withFields({ taskId: task.id, goal, type: 'primary' }).log('TaskManager: Created new primary task')
}
else {
this.secondaryTasks.set(task.id, task)
this.logger.withFields({ taskId: task.id, goal, type: 'secondary' }).log('TaskManager: Created new secondary task')
}
return task
}
/**
* Update the status of a specific task
*/
public updateTaskStatus(taskId: string, status: TaskStatus, currentStep?: string): void {
const task = this.getTaskById(taskId)
if (!task) {
this.logger.warn(`TaskManager: Task ${taskId} not found for update`)
return
}
task.status = status
if (currentStep) {
task.currentStep = currentStep
}
this.logger.withFields({
taskId: task.id,
status,
currentStep,
}).log('TaskManager: Updated task status')
}
/**
* Set the plan for a specific task
*/
public setTaskPlan(taskId: string, plan: Plan): void {
const task = this.getTaskById(taskId)
if (!task) {
this.logger.warn(`TaskManager: Task ${taskId} not found to set plan`)
return
}
task.plan = plan
this.logger.withFields({ taskId: task.id }).log('TaskManager: Set task plan')
}
/**
* Cancel a specific task. If no taskId provided, cancels primary task.
*/
public cancelTask(taskId: string, reason?: string): void {
const task = this.getTaskById(taskId)
if (!task) {
this.logger.warn(`TaskManager: Task ${taskId} not found to cancel`)
return
}
this.logger.withFields({
taskId: task.id,
reason,
}).log('TaskManager: Cancelling task')
task.status = 'cancelling'
task.cancellationToken.cancel()
// We don't remove it yet, we wait for completeTask to be called
this.addToHistory(task)
// Cleanup reference immediately to allow new primary tasks if this was primary?
// No, we should wait for the orchestrator to call completeTask/cleanup.
}
/**
* Cancel currently active primary task
*/
public cancelPrimaryTask(reason?: string): void {
if (this.primaryTask) {
this.cancelTask(this.primaryTask.id, reason)
}
}
/**
* Complete a task and remove it from active list
*/
public completeTask(taskId: string): void {
const task = this.getTaskById(taskId)
if (!task)
return
this.logger.withFields({ taskId: task.id }).log('TaskManager: Task completed')
this.addToHistory(task)
if (this.primaryTask?.id === taskId) {
this.primaryTask = null
}
else {
this.secondaryTasks.delete(taskId)
}
}
/**
* Get the current primary task
*/
public getPrimaryTask(): TaskContext | null {
return this.primaryTask
}
/**
* Check if there is a primary task running
*/
public hasPrimaryTask(): boolean {
return this.primaryTask !== null
}
/**
* Get a task by ID
*/
public getTaskById(taskId: string): TaskContext | null {
if (this.primaryTask?.id === taskId)
return this.primaryTask
return this.secondaryTasks.get(taskId) || null
}
/**
* Get formatted task context for LLM.
* Includes Primary Task and summary of Secondary Tasks.
*/
public getTaskContextForLLM(): string {
const lines: string[] = []
// Primary Task
if (this.primaryTask) {
const { goal, status, startTime, currentStep, plan } = this.primaryTask
const elapsedSeconds = Math.floor((Date.now() - startTime) / 1000)
lines.push(`[PRIMARY TASK] (${status.toUpperCase()}): "${goal}"`)
lines.push(`- Duration: ${elapsedSeconds}s`)
if (currentStep)
lines.push(`- Step: ${currentStep}`)
if (plan)
lines.push(`- Plan: ${plan.steps.length} steps (${plan.status})`)
}
else {
lines.push('No primary task active.')
}
// Secondary Tasks
if (this.secondaryTasks.size > 0) {
lines.push('\n[SECONDARY TASKS]')
for (const task of this.secondaryTasks.values()) {
lines.push(`- [${task.status.toUpperCase()}] "${task.goal}"`)
}
}
return lines.join('\n')
}
/**
* Get all active tasks for debugging
*/
public getAllActiveTasks(): TaskContext[] {
const tasks: TaskContext[] = []
if (this.primaryTask)
tasks.push(this.primaryTask)
tasks.push(...this.secondaryTasks.values())
return tasks
}
/**
* Get task history
*/
public getTaskHistory(): TaskContext[] {
return [...this.taskHistory]
}
private generateTaskId(): string {
return `task_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
}
private addToHistory(task: TaskContext): void {
// Only add if not already in history (simple check)
if (this.taskHistory.some(t => t.id === task.id))
return
this.taskHistory.push(task)
if (this.taskHistory.length > this.maxHistorySize) {
this.taskHistory.shift()
}
}
}
@@ -84,10 +84,9 @@ export function createAgentContainer() {
})),
// Reflex Manager (Reactive Layer)
reflexManager: asFunction(({ eventBus, perceptionPipeline, taskExecutor, logger }) =>
reflexManager: asFunction(({ eventBus, taskExecutor, logger }) =>
new ReflexManager({
eventBus,
perception: perceptionPipeline.getPerceptionAPI(),
taskExecutor,
logger,
}),
@@ -1,9 +1,7 @@
import { armSwingEvent } from './arm-swing'
import { damageTakenEvent } from './damage-taken'
import { entityMovedEvent } from './entity-moved'
import { itemCollectedEvent } from './item-collected'
import { sneakToggleEvent } from './sneak-toggle'
import { soundHeardEvent } from './sound-heard'
import { systemMessageEvent } from './system-message'
export const allEventDefinitions = [
@@ -11,17 +9,13 @@ export const allEventDefinitions = [
armSwingEvent,
sneakToggleEvent,
entityMovedEvent,
soundHeardEvent,
damageTakenEvent,
itemCollectedEvent,
]
export {
armSwingEvent,
damageTakenEvent,
entityMovedEvent,
itemCollectedEvent,
sneakToggleEvent,
soundHeardEvent,
systemMessageEvent,
}
@@ -1,24 +0,0 @@
import { definePerceptionEvent } from '..'
interface ItemCollectedExtract {
itemName: string
}
export const itemCollectedEvent = definePerceptionEvent<[any, any], ItemCollectedExtract>({
id: 'item_collected',
modality: 'felt',
kind: 'item_collected',
mineflayer: {
event: 'playerCollect',
filter: (ctx, collector, _collected) => {
if (!collector)
return false
return collector.username === ctx.selfUsername
},
extract: (_ctx, _collector, collected) => ({
itemName: String(collected?.name ?? collected?.displayName ?? collected?.type ?? 'unknown'),
}),
},
})
@@ -1,31 +0,0 @@
import type { Vec3 } from 'vec3'
import { definePerceptionEvent } from '..'
interface SoundHeardExtract {
soundId: string
distance: number
pos: Vec3
}
export const soundHeardEvent = definePerceptionEvent<[string, Vec3], SoundHeardExtract>({
id: 'sound_heard',
modality: 'heard',
kind: 'sound',
mineflayer: {
event: 'soundEffectHeard',
filter: (ctx, _soundId, pos) => {
if (!pos)
return false
const dist = ctx.distanceToPos(pos)
return dist !== null && dist <= ctx.maxDistance
},
extract: (ctx, soundId, pos) => ({
soundId,
distance: ctx.distanceToPos(pos)!,
pos,
}),
},
})
@@ -1,148 +0,0 @@
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<EntityState>): 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<BeliefEngine['register']>[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,
}
}
}
@@ -5,10 +5,8 @@ import type { MineflayerWithAgents } from '../types'
import { EventRegistry } from './events'
import { allEventDefinitions } from './events/definitions'
import { PerceptionAPI } from './perception-api'
export class PerceptionPipeline {
private readonly perception: PerceptionAPI
private readonly eventRegistry: EventRegistry
private bot: MineflayerWithAgents | null = null
@@ -18,8 +16,6 @@ export class PerceptionPipeline {
logger: Logg
},
) {
this.perception = new PerceptionAPI({ logger: this.deps.logger })
this.eventRegistry = new EventRegistry({
logger: this.deps.logger,
onRawEvent: (event) => {
@@ -51,11 +47,4 @@ export class PerceptionPipeline {
this.eventRegistry.stop()
this.bot = null
}
/**
* Get the PerceptionAPI for querying entity beliefs
*/
public getPerceptionAPI(): PerceptionAPI {
return this.perception
}
}
@@ -1,19 +0,0 @@
name: player-joined
version: 1
trigger:
modality: system
kind: player_joined
accumulator:
threshold: 1
window: 1s
signal:
type: social_presence
description: 'Player {{ displayName }} joined the server'
confidence: 1.0
metadata:
event: player_joined
playerId: '{{ playerId }}'
displayName: '{{ displayName }}'
@@ -1 +0,0 @@
export * from './types/behavior'
@@ -2,7 +2,6 @@ import type { Logg } from '@guiiai/logg'
import type { TaskExecutor } from '../action/task-executor'
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'
@@ -21,7 +20,6 @@ export class ReflexManager {
constructor(
private readonly deps: {
eventBus: EventBus
perception: PerceptionAPI
taskExecutor: TaskExecutor
logger: Logg
},
@@ -111,7 +109,7 @@ export class ReflexManager {
if (!this.bot)
return
this.runtime.tick(this.bot, 0, this.deps.perception)
this.runtime.tick(this.bot, 0)
this.emitReflexState()
}
@@ -159,7 +157,7 @@ export class ReflexManager {
// Assuming 'signal:social:chat' or similar might exist later.
// Trigger behavior selection
this.runtime.tick(bot, 0, this.deps.perception)
this.runtime.tick(bot, 0)
// Forward signals to conscious layer (Brain) ONLY when Reflex decides.
if (this.shouldForwardToConscious(signal)) {
@@ -1,6 +1,5 @@
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'
@@ -90,7 +89,7 @@ export class ReflexRuntime {
this.behaviors.push(behavior)
}
public tick(bot: MineflayerWithAgents, deltaMs: number, perception: PerceptionAPI): string | null {
public tick(bot: MineflayerWithAgents, deltaMs: number): string | null {
const now = Date.now()
this.context.updateNow(now)
@@ -167,7 +166,7 @@ export class ReflexRuntime {
this.activeBehaviorUntil = null
const ctx = this.context.getSnapshot()
const api = { bot, context: this.context, perception }
const api = { bot, context: this.context }
let best: { behavior: ReflexBehavior, score: number } | null = null
for (const behavior of this.behaviors) {
@@ -1,4 +1,3 @@
import type { PerceptionAPI } from '../../perception/perception-api'
import type { MineflayerWithAgents } from '../../types'
import type { ReflexContext } from '../context'
import type { ReflexModeId } from '../modes'
@@ -6,7 +5,6 @@ import type { ReflexModeId } from '../modes'
export interface ReflexApi {
bot: MineflayerWithAgents
context: ReflexContext
perception: PerceptionAPI
}
export interface ReflexBehavior {
@@ -1,77 +0,0 @@
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<string, PatternDefinition> = 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<string, Belief> {
const beliefs: Record<string, Belief> = {}
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 }
}
}
}
@@ -1,2 +0,0 @@
export { BeliefEngine } from './belief-engine'
export { teabagPattern } from './patterns/teabag'
@@ -1,56 +0,0 @@
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),
},
}
},
}
@@ -3,11 +3,6 @@ import type { Client } from '@proj-airi/server-sdk'
import type { Mineflayer } from '../../libs/mineflayer'
import type { ReflexManager } from '../reflex/reflex-manager'
export interface LLMResponse {
content: string
usage?: any
}
export interface MineflayerWithAgents extends Mineflayer {
reflexManager: ReflexManager
}
@@ -17,7 +12,7 @@ export interface CognitiveEngineOptions {
}
// TODO: currently stimulus is just chat events, consider renaming to 'input' or 'user_interaction'
export type EventCategory = 'perception' | 'feedback' | 'world_update' | 'system_alert'
export type EventCategory = 'perception' | 'feedback' | 'system_alert'
export interface BotEventSource {
type: 'minecraft' | 'airi' | 'system'
@@ -34,8 +29,3 @@ export interface BotEvent<T = any> {
priority?: number // Higher is more urgent
handled?: boolean // Set by Reflex layer to inhibit Conscious layer
}
export interface WorldUpdatePayload {
event: string
data: any
}
@@ -1,126 +0,0 @@
import type { Vec3 } from 'vec3'
import type { EntityState, StateChange } from './types'
/**
* Tracks the state of all known entities
*/
export class EntityStore {
private entities: Map<string, EntityState> = 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<EntityState>): 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
}
}
@@ -1,3 +0,0 @@
export { EntityStore } from './entity-store'
export { TemporalBuffer } from './temporal-buffer'
export * from './types'
@@ -1,85 +0,0 @@
import type { StateChange } from './types'
/**
* Rolling window buffer of state changes per entity
*/
export class TemporalBuffer {
private buffer: Map<string, StateChange[]> = 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()
}
}
@@ -1,72 +0,0 @@
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<string, unknown>
}
/**
* 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<string, Belief>
distanceToSelf: number
}