refactor(minecraft): further cleanup
This commit is contained in:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user