refactor(minecraft): simplify EventBus by removing history and trace query features

Strips ring buffer history storage, getHistory(), getEventsByTrace(), and historySize config from EventBus. Removes logger dependency and default config object. EventBus now only handles emit/subscribe/dispatch without persistent event storage. Updates container to use zero-config createEventBus(). Moves event-bus from cognitive/os/ to cognitive/ directory.

Update services/minecraft/codex-skills/minecraft-debug-mcp/references/mcp-surface.md

Update services/minecraft/codex-skills/minecraft-debug-mcp/references/mcp-surface.md

Update services/minecraft/codex-skills/minecraft-debug-mcp/SKILL.md
This commit is contained in:
Rin
2026-02-18 11:14:46 +08:00
committed by Neko Ayaka
parent fca2f9a244
commit f3486005ea
15 changed files with 193 additions and 628 deletions
+2 -2
View File
@@ -56,7 +56,7 @@ The perception layer acts as the sensory input hub, collecting raw Mineflayer si
**Pipeline**:
- Event definitions in `events/definitions/*` bind Mineflayer events to normalized raw events.
- `EventRegistry` emits `raw:<modality>:<kind>` events to the Cognitive EventBus.
- `EventRegistry` emits `raw:<modality>:<kind>` events to the cognitive event bus.
- `RuleEngine` evaluates YAML rules and emits derived `signal:*` events consumed by Reflex/Conscious layers.
**Key files**:
@@ -149,7 +149,7 @@ src/
│ │ ├── action-registry.ts # Tool dispatch + schema validation
│ │ ├── llm-actions.ts # Tool catalog
│ │ └── types.ts
│ ├── os/ # EventBus + tracing core
│ ├── event-bus.ts # Event bus core
│ ├── container.ts # Dependency injection wiring
│ ├── index.ts # Cognitive system entrypoint
│ └── types.ts # Shared cognitive types
@@ -11,7 +11,7 @@ Use this skill to run the local bot and interact with its MCP debug interface sa
## Quick Start Workflow
1. Run `pnpm dev` from `/Users/rinshinohara/Repo/airi/services/minecraft` and keep it running.
1. Run `pnpm dev` from `/path/to/project/root/services/minecraft` and keep it running.
2. Wait for `MCP REPL server running at http://localhost:3001` in logs.
3. Connect MCP client to `http://localhost:3001/sse`.
4. Verify readiness with a read-only call:
@@ -1,6 +1,6 @@
# Minecraft Debug MCP Surface
Implementation source: `/Users/rinshinohara/Repo/airi/services/minecraft/src/debug/mcp-repl-server.ts`.
Implementation source: `/path/to/project/root/services/minecraft/src/debug/mcp-repl-server.ts`.
## Endpoint
@@ -9,7 +9,7 @@ Implementation source: `/Users/rinshinohara/Repo/airi/services/minecraft/src/deb
- SSE fallback endpoint: `GET /sse` + `POST /messages`
The bot starts this server during normal runtime from:
- `/Users/rinshinohara/Repo/airi/services/minecraft/src/cognitive/index.ts`
- `/path/to/project/root/services/minecraft/src/cognitive/index.ts`
## Resources
@@ -4,7 +4,7 @@ import type { Message } from '@xsai/shared-chat'
import type { Action } from '../../libs/mineflayer/action'
import type { TaskExecutor } from '../action/task-executor'
import type { ActionInstruction } from '../action/types'
import type { EventBus, TracedEvent } from '../os'
import type { EventBus, TracedEvent } from '../event-bus'
import type { PerceptionSignal } from '../perception/types/signals'
import type { ReflexManager } from '../reflex/reflex-manager'
import type { BotEvent, MineflayerWithAgents } from '../types'
@@ -1,6 +1,6 @@
import type { Logg } from '@guiiai/logg'
import type { EventBus } from './os'
import type { EventBus } from './event-bus'
import type { RuleEngine } from './perception/rules'
import { useLogg } from '@guiiai/logg'
@@ -10,7 +10,7 @@ import { config } from '../composables/config'
import { TaskExecutor } from './action/task-executor'
import { Brain } from './conscious/brain'
import { LLMAgent } from './conscious/llm-agent'
import { createEventBus } from './os'
import { createEventBus } from './event-bus'
import { PerceptionPipeline } from './perception/pipeline'
import { createRuleEngine } from './perception/rules'
import { ReflexManager } from './reflex/reflex-manager'
@@ -44,13 +44,8 @@ export function createAgentContainer() {
model: config.openai.model,
})).singleton(),
// Register EventBus (Cognitive OS core)
eventBus: asFunction(() =>
createEventBus({
logger: useLogg('eventBus').useGlobalConfig(),
config: { historySize: 10000 },
}),
).singleton(),
// Register EventBus (cognitive event core)
eventBus: asFunction(() => createEventBus()).singleton(),
// Register RuleEngine (YAML rules processing)
ruleEngine: asFunction(({ eventBus }) => {
@@ -1,16 +1,11 @@
import type { TracedEvent } from './types'
import type { TracedEvent } from './event-bus'
import { useLogg } from '@guiiai/logg'
import { describe, expect, it, vi } from 'vitest'
import { createEventBus } from './index'
import { createEventBus } from './event-bus'
describe('eventBus', () => {
const createTestBus = () =>
createEventBus({
logger: useLogg('test'),
config: { historySize: 100 },
})
const createTestBus = () => createEventBus()
describe('emit', () => {
it('should create an event with auto-generated id and timestamp', () => {
@@ -164,7 +159,7 @@ describe('eventBus', () => {
payload: {},
source: { component: 'test' },
})
expect(handler).toHaveBeenCalledTimes(1) // Still 1
expect(handler).toHaveBeenCalledTimes(1)
})
})
@@ -174,7 +169,6 @@ describe('eventBus', () => {
let childEvent: TracedEvent | undefined
bus.subscribe('parent:event', () => {
// Emit within handler - should inherit context
childEvent = bus.emit({
type: 'child:event',
payload: {},
@@ -193,50 +187,4 @@ describe('eventBus', () => {
expect(childEvent!.parentId).toBe(parent.id)
})
})
describe('history', () => {
it('should store events in history', () => {
const bus = createTestBus()
bus.emit({ type: 'e1', payload: {}, source: { component: 'test' } })
bus.emit({ type: 'e2', payload: {}, source: { component: 'test' } })
bus.emit({ type: 'e3', payload: {}, source: { component: 'test' } })
const history = bus.getHistory()
expect(history.length).toBe(3)
expect(history[0].type).toBe('e1')
expect(history[2].type).toBe('e3')
})
it('should respect historySize limit (ring buffer)', () => {
const bus = createEventBus({
logger: useLogg('test'),
config: { historySize: 3 },
})
bus.emit({ type: 'e1', payload: {}, source: { component: 'test' } })
bus.emit({ type: 'e2', payload: {}, source: { component: 'test' } })
bus.emit({ type: 'e3', payload: {}, source: { component: 'test' } })
bus.emit({ type: 'e4', payload: {}, source: { component: 'test' } })
const history = bus.getHistory()
expect(history.length).toBe(3)
// Oldest event (e1) should be evicted
expect(history.map(e => e.type)).toEqual(['e2', 'e3', 'e4'])
})
})
describe('getEventsByTrace', () => {
it('should filter events by traceId', () => {
const bus = createTestBus()
const e1 = bus.emit({ type: 'a', payload: {}, source: { component: 'test' } })
bus.emitChild(e1, { type: 'b', payload: {}, source: { component: 'test' } })
bus.emit({ type: 'c', payload: {}, source: { component: 'test' } }) // Different trace
const trace = bus.getEventsByTrace(e1.traceId)
expect(trace.length).toBe(2)
expect(trace.map(e => e.type)).toEqual(['a', 'b'])
})
})
})
@@ -0,0 +1,175 @@
import { AsyncLocalStorage } from 'node:async_hooks'
import { nanoid } from 'nanoid'
export type EventId = string
export type TraceId = string
export interface EventSource {
readonly component: string
readonly id?: string
}
export interface TracedEvent<T = unknown> {
readonly id: EventId
readonly traceId: TraceId
readonly parentId?: EventId
readonly type: string
readonly payload: Readonly<T>
readonly timestamp: number
readonly source: EventSource
}
export interface EventInput<T = unknown> {
readonly type: string
readonly payload: Readonly<T>
readonly source: EventSource
readonly traceId?: string
readonly parentId?: string
}
export type EventHandler<T = unknown> = (event: TracedEvent<T>) => void
export type Unsubscribe = () => void
export type EventPattern = string
interface TraceContext {
traceId: string
parentId?: string
}
interface Subscription {
pattern: EventPattern
handler: EventHandler
}
const traceStorage = new AsyncLocalStorage<TraceContext>()
function generateEventId(): string {
return nanoid(12)
}
function generateTraceId(): string {
return nanoid(16)
}
function matchesPattern(pattern: EventPattern, eventType: string): boolean {
if (pattern === '*')
return true
if (pattern.endsWith(':*')) {
const prefix = pattern.slice(0, -1)
return eventType.startsWith(prefix)
}
return pattern === eventType
}
function deepFreeze<T>(value: T): T {
if (value === null || typeof value !== 'object' || Object.isFrozen(value))
return value
if (Array.isArray(value)) {
for (const item of value)
deepFreeze(item)
return Object.freeze(value)
}
for (const child of Object.values(value as Record<string, unknown>))
deepFreeze(child)
return Object.freeze(value)
}
function resolveTraceContext(input: Pick<EventInput, 'traceId' | 'parentId'>): TraceContext {
if (input.traceId) {
return Object.freeze({
traceId: input.traceId,
parentId: input.parentId,
})
}
const inherited = traceStorage.getStore()
if (inherited) {
return Object.freeze({
traceId: inherited.traceId,
parentId: inherited.parentId,
})
}
return Object.freeze({ traceId: generateTraceId() })
}
function withTraceContext<T>(traceId: string, parentId: string, fn: () => T): T {
return traceStorage.run({ traceId, parentId }, fn)
}
export class EventBus {
private readonly subscriptions = new Map<number, Subscription>()
private nextSubId = 0
public emit<T>(input: EventInput<T>): TracedEvent<T> {
const trace = resolveTraceContext({
traceId: input.traceId,
parentId: input.parentId,
})
const event = deepFreeze({
id: generateEventId(),
traceId: trace.traceId,
parentId: trace.parentId,
type: input.type,
payload: input.payload,
timestamp: Date.now(),
source: input.source,
} satisfies TracedEvent<T>)
this.dispatch(event)
return event
}
public emitChild<T>(
parent: TracedEvent,
input: Omit<EventInput<T>, 'traceId' | 'parentId'>,
): TracedEvent<T> {
return this.emit({
...input,
traceId: parent.traceId,
parentId: parent.id,
})
}
public subscribe<T = unknown>(
pattern: EventPattern,
handler: EventHandler<T>,
): Unsubscribe {
const id = this.nextSubId++
this.subscriptions.set(id, {
pattern,
handler: handler as EventHandler,
})
return () => {
this.subscriptions.delete(id)
}
}
private dispatch(event: TracedEvent): void {
for (const sub of this.subscriptions.values()) {
if (!matchesPattern(sub.pattern, event.type))
continue
try {
withTraceContext(event.traceId, event.id, () => {
sub.handler(event)
})
}
catch {
// Keep dispatch resilient by isolating subscriber failures.
}
}
}
}
export function createEventBus(): EventBus {
return new EventBus()
}
@@ -1,273 +0,0 @@
import type { Logg } from '@guiiai/logg'
import type {
EventBusConfig,
EventBusSnapshot,
EventHandler,
EventInput,
EventPattern,
Subscription,
TraceContext,
TracedEvent,
Unsubscribe,
} from './types'
import {
deriveTraceContext,
generateEventId,
resolveTraceContext,
runWithTraceContext,
} from './tracer'
import { freezeEvent } from './types'
/**
* Default EventBus configuration
*/
const DEFAULT_CONFIG: EventBusConfig = Object.freeze({
historySize: 10000,
})
/**
* Check if an event type matches a pattern
* Supports wildcards: 'raw:*' matches 'raw:sighted:punch'
*/
function matchesPattern(pattern: EventPattern, eventType: string): boolean {
if (pattern === '*')
return true
if (pattern.endsWith(':*')) {
const prefix = pattern.slice(0, -1) // Remove the '*'
return eventType.startsWith(prefix)
}
return pattern === eventType
}
/**
* EventBus - The heart of the Cognitive OS
*
* This is the ONLY component with mutable internal state.
* All other components should be pure functions that interact
* through the EventBus.
*
* Design principles:
* - Events are immutable once created
* - Trace context automatically propagates through handlers
* - Ring buffer prevents memory leaks
* - Pattern-based subscriptions for flexible routing
*/
export class EventBus {
// Internal mutable state - isolated from the outside world
private readonly buffer: (TracedEvent | null)[] = []
private readonly subscriptions = new Map<number, Subscription>()
private nextSubId = 0
private writeIndex = 0 // Next position to write
private count = 0 // Number of events stored
constructor(
private readonly deps: {
logger: Logg
config?: Partial<EventBusConfig>
},
) {
// Pre-allocate buffer
const size = this.config.historySize
this.buffer = new Array(size).fill(null)
}
private get config(): EventBusConfig {
return { ...DEFAULT_CONFIG, ...this.deps.config }
}
/**
* Emit an event to the bus
*
* This is the ONLY side effect entry point.
* Returns the created event (immutable).
*/
public emit<T>(input: EventInput<T>): TracedEvent<T> {
// Resolve trace context (from explicit, async context, or new)
const trace = resolveTraceContext({
traceId: input.traceId,
parentId: input.parentId,
})
// Create the full event
const event = freezeEvent<T>({
id: generateEventId(),
traceId: trace.traceId,
parentId: trace.parentId,
type: input.type,
payload: input.payload,
timestamp: Date.now(),
source: input.source,
})
// Store in ring buffer
this.storeEvent(event)
// Dispatch to subscribers
this.dispatch(event)
return event
}
/**
* Emit an event as a child of another event
* Automatically sets up trace context
*/
public emitChild<T>(
parent: TracedEvent,
input: Omit<EventInput<T>, 'traceId' | 'parentId'>,
): TracedEvent<T> {
return this.emit({
...input,
traceId: parent.traceId,
parentId: parent.id,
})
}
/**
* Subscribe to events matching a pattern
* Returns an unsubscribe function
*/
public subscribe<T = unknown>(
pattern: EventPattern,
handler: EventHandler<T>,
): Unsubscribe {
const id = this.nextSubId++
this.subscriptions.set(id, {
pattern,
handler: handler as EventHandler,
})
return () => {
this.subscriptions.delete(id)
}
}
/**
* Get event history as an immutable array
* Events are returned in chronological order (oldest first)
*/
public getHistory(): readonly TracedEvent[] {
const size = this.config.historySize
if (this.count === 0) {
return []
}
const result: TracedEvent[] = []
// Calculate start position (oldest event)
// If buffer is full, oldest is at writeIndex
// If not full, oldest is at 0
const startIdx = this.count < size ? 0 : this.writeIndex
for (let i = 0; i < this.count; i++) {
const idx = (startIdx + i) % size
const event = this.buffer[idx]
if (event) {
result.push(event)
}
}
return Object.freeze(result)
}
/**
* Get debug snapshot
*/
public getSnapshot(): EventBusSnapshot {
return Object.freeze({
events: this.getHistory(),
subscriptionCount: this.subscriptions.size,
})
}
/**
* Replay a sequence of events
* Used for debugging and testing
*/
public replay(events: readonly TracedEvent[]): void {
this.deps.logger.withFields({ count: events.length }).log('EventBus: replaying events')
for (const event of events) {
// Store without re-generating IDs
this.storeEvent(event)
// Dispatch to current subscribers
this.dispatch(event)
}
}
/**
* Clear all events (for testing)
*/
public clear(): void {
this.buffer.fill(null)
this.writeIndex = 0
this.count = 0
}
/**
* Get events by trace ID
*/
public getEventsByTrace(traceId: string): readonly TracedEvent[] {
return Object.freeze(
this.getHistory().filter(e => e.traceId === traceId),
)
}
// ============================================================
// Private methods
// ============================================================
private storeEvent(event: TracedEvent): void {
const size = this.config.historySize
// Write to current position
this.buffer[this.writeIndex] = event
// Advance write position
this.writeIndex = (this.writeIndex + 1) % size
// Update count (max is buffer size)
if (this.count < size) {
this.count++
}
}
private dispatch(event: TracedEvent): void {
// Create trace context for handlers
const childContext: TraceContext = deriveTraceContext(event.traceId, event.id)
for (const sub of this.subscriptions.values()) {
if (!matchesPattern(sub.pattern, event.type))
continue
try {
// Run handler within trace context so child emissions inherit it
runWithTraceContext(childContext, () => {
sub.handler(event)
})
}
catch (err) {
this.deps.logger
.withError(err as Error)
.withFields({ eventType: event.type, pattern: sub.pattern })
.error('EventBus: handler error')
}
}
}
}
/**
* Create an EventBus instance
* Factory function for cleaner API
*/
export function createEventBus(deps: {
logger: Logg
config?: Partial<EventBusConfig>
}): EventBus {
return new EventBus(deps)
}
@@ -1,43 +0,0 @@
/**
* Cognitive OS - Event-sourced architecture for the cognitive engine
*
* Core principles:
* - All state changes go through TracedEvents
* - Events are immutable
* - Trace context propagates automatically
* - EventBus is the only mutable container
*/
// NOTE: RuleEngine and rule utilities moved to cognitive/perception/rules.
// EventBus
export { createEventBus, EventBus } from './event-bus'
// Tracer utilities
export {
createTraceContext,
deriveTraceContext,
generateEventId,
generateTraceId,
getCurrentTraceContext,
resolveTraceContext,
runWithTraceContext,
} from './tracer'
// Core types
export type {
EventBusConfig,
EventBusSnapshot,
EventHandler,
EventId,
EventInput,
EventPattern,
EventSource,
Subscription,
TraceContext,
TracedEvent,
TraceId,
Unsubscribe,
} from './types'
export { freezeEvent } from './types'
@@ -1,98 +0,0 @@
import type { EventId, TraceContext, TraceId } from './types'
/**
* AsyncLocalStorage-based trace context propagation
* This allows handlers to automatically inherit trace context
*/
import { AsyncLocalStorage } from 'node:async_hooks'
import { nanoid } from 'nanoid'
/**
* Generate a unique event ID
* Uses nanoid for compact, URL-safe IDs
*/
export function generateEventId(): EventId {
return nanoid(12)
}
/**
* Generate a unique trace ID
*/
export function generateTraceId(): TraceId {
return nanoid(16)
}
/**
* Create a new trace context for a fresh event chain
*/
export function createTraceContext(): TraceContext {
return Object.freeze({
traceId: generateTraceId(),
})
}
/**
* Derive a child trace context from a parent event
* Preserves the same traceId but sets the parentId
*/
export function deriveTraceContext(
parentTraceId: TraceId,
parentEventId: EventId,
): TraceContext {
return Object.freeze({
traceId: parentTraceId,
parentId: parentEventId,
})
}
const traceStorage = new AsyncLocalStorage<TraceContext>()
/**
* Get the current trace context from async local storage
* Returns undefined if not in a traced context
*/
export function getCurrentTraceContext(): TraceContext | undefined {
return traceStorage.getStore()
}
/**
* Run a function within a trace context
* All events emitted within this context will inherit the trace
*/
export function runWithTraceContext<T>(
context: TraceContext,
fn: () => T,
): T {
return traceStorage.run(context, fn)
}
/**
* Create or derive trace context for an event
* If we're in a traced context, derive from it; otherwise create new
*/
export function resolveTraceContext(
explicit?: Partial<TraceContext>,
): TraceContext {
const current = getCurrentTraceContext()
if (explicit?.traceId) {
// Explicit context provided
return Object.freeze({
traceId: explicit.traceId,
parentId: explicit.parentId,
})
}
if (current) {
// Derive from current async context
// Note: parentId should be set by the caller who knows the parent event
return Object.freeze({
traceId: current.traceId,
parentId: current.parentId,
})
}
// New trace
return createTraceContext()
}
@@ -1,139 +0,0 @@
/**
* Core types for the Cognitive OS event-sourced architecture.
* All types are immutable by design (Readonly).
*/
/**
* Unique identifier for events and traces
*/
export type EventId = string
export type TraceId = string
/**
* Event source identifier
*/
export interface EventSource {
readonly component: string
readonly id?: string
}
/**
* A traced event - the core unit of the event-sourced system.
* All fields are readonly to enforce immutability.
*/
export interface TracedEvent<T = unknown> {
/** Unique event ID */
readonly id: EventId
/** Trace ID shared across related events */
readonly traceId: TraceId
/** Parent event ID (if derived from another event) */
readonly parentId?: EventId
/** Event type identifier (e.g. 'raw:sighted:arm_swing') */
readonly type: string
/** Event payload - should be immutable */
readonly payload: Readonly<T>
/** Event timestamp */
readonly timestamp: number
/** Source component */
readonly source: EventSource
}
/**
* Input for creating a new event
* traceId and parentId are optional - will be auto-generated or inherited from context
* id and timestamp are always auto-generated
*/
export interface EventInput<T = unknown> {
readonly type: string
readonly payload: Readonly<T>
readonly source: EventSource
readonly traceId?: string
readonly parentId?: string
}
/**
* Event handler function - should be a pure function that may emit new events
*/
export type EventHandler<T = unknown> = (event: TracedEvent<T>) => void
/**
* Unsubscribe function returned by subscribe
*/
export type Unsubscribe = () => void
/**
* Event pattern for subscription filtering
* Supports wildcards: 'raw:*' matches 'raw:sighted:punch'
*/
export type EventPattern = string
/**
* Subscription record
*/
export interface Subscription {
readonly pattern: EventPattern
readonly handler: EventHandler
}
/**
* EventBus configuration
*/
export interface EventBusConfig {
/** Maximum events to keep in history (ring buffer) */
readonly historySize: number
}
/**
* Snapshot of EventBus state for debugging
*/
export interface EventBusSnapshot {
readonly events: readonly TracedEvent[]
readonly subscriptionCount: number
}
/**
* Trace context for propagating trace information
*/
export interface TraceContext {
readonly traceId: TraceId
readonly parentId?: EventId
}
/**
* Deep freeze an object (recursively freeze all nested objects)
* Performance note: Use sparingly on large objects
*/
export function deepFreeze<T>(obj: T): Readonly<T> {
if (obj === null || typeof obj !== 'object') {
return obj
}
// Don't freeze already frozen objects
if (Object.isFrozen(obj)) {
return obj
}
// Freeze arrays
if (Array.isArray(obj)) {
obj.forEach(item => deepFreeze(item))
return Object.freeze(obj) as Readonly<T>
}
// Freeze object properties
Object.keys(obj).forEach((key) => {
const value = (obj as Record<string, unknown>)[key]
if (value !== null && typeof value === 'object') {
deepFreeze(value)
}
})
return Object.freeze(obj)
}
/**
* Create an immutable event by deep freezing all its properties
* This ensures the entire object tree is immutable
*/
export function freezeEvent<T>(event: TracedEvent<T>): TracedEvent<T> {
return deepFreeze(event)
}
@@ -1,6 +1,6 @@
import type { Logg } from '@guiiai/logg'
import type { EventBus } from '../os'
import type { EventBus } from '../event-bus'
import type { MineflayerWithAgents } from '../types'
import { EventRegistry } from './events'
@@ -7,7 +7,7 @@
import type { Logg } from '@guiiai/logg'
import type { EventBus, TracedEvent } from '../../os'
import type { EventBus, TracedEvent } from '../../event-bus'
import type {
AccumulatorsState,
ParsedRule,
@@ -1,7 +1,7 @@
import type { Logg } from '@guiiai/logg'
import type { TaskExecutor } from '../action/task-executor'
import type { EventBus, TracedEvent } from '../os'
import type { EventBus, TracedEvent } from '../event-bus'
import type { PerceptionSignal } from '../perception/types/signals'
import type { MineflayerWithAgents } from '../types'
import type { ReflexContextState } from './context'
+1 -1
View File
@@ -66,7 +66,7 @@ export interface ReflexStateEvent {
}
/**
* Traced event from the Cognitive OS EventBus
* Traced event from the cognitive event bus
*/
export interface TraceEvent {
/** Unique event ID */