refactor(minecraft): dead code cleanup
Strips saliency thresholds, window counters, signal generation, and route filtering from all perception event definitions and EventRegistry. Events now emit raw data directly via onRawEvent callback without conscious/reflex/debug routing or threshold-based filtering. Removes WindowCounter, SaliencySnapshot, and related timer/slot logic.
This commit is contained in:
@@ -34,22 +34,4 @@ export const armSwingEvent = definePerceptionEvent<[any], ArmSwingExtract>({
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
|
||||||
saliency: {
|
|
||||||
threshold: 5,
|
|
||||||
key: 'punch:player',
|
|
||||||
},
|
|
||||||
|
|
||||||
signal: {
|
|
||||||
type: 'entity_attention',
|
|
||||||
description: extracted => `Player ${extracted.displayName || 'unknown'} is punching nearby`,
|
|
||||||
metadata: extracted => ({
|
|
||||||
kind: 'player',
|
|
||||||
action: 'punch',
|
|
||||||
distance: extracted.distance,
|
|
||||||
hasLineOfSight: extracted.hasLineOfSight,
|
|
||||||
displayName: extracted.displayName,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
|
|
||||||
routes: ['conscious', 'reflex', 'debug'],
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -86,23 +86,6 @@ function inferCauseFromName(name: string): DamageSourceCause {
|
|||||||
return 'unknown'
|
return 'unknown'
|
||||||
}
|
}
|
||||||
|
|
||||||
function describeDamageTaken(extracted: DamageTakenExtract): string {
|
|
||||||
const amount = extracted.amount
|
|
||||||
const ds = extracted.damageSource
|
|
||||||
const cause = ds?.cause ?? 'unknown'
|
|
||||||
const name = ds?.name
|
|
||||||
const distance = ds?.distance
|
|
||||||
|
|
||||||
const details = [
|
|
||||||
`amount=${amount}`,
|
|
||||||
`cause=${cause}`,
|
|
||||||
typeof name === 'string' && name.length > 0 ? `name=${name}` : null,
|
|
||||||
typeof distance === 'number' ? `distance=${distance.toFixed(1)}` : null,
|
|
||||||
].filter(Boolean).join(', ')
|
|
||||||
|
|
||||||
return `Taken damage (${details}).`
|
|
||||||
}
|
|
||||||
|
|
||||||
export const damageTakenEvent = definePerceptionEvent<[], DamageTakenExtract>({
|
export const damageTakenEvent = definePerceptionEvent<[], DamageTakenExtract>({
|
||||||
id: 'damage_taken',
|
id: 'damage_taken',
|
||||||
modality: 'felt',
|
modality: 'felt',
|
||||||
@@ -139,21 +122,4 @@ export const damageTakenEvent = definePerceptionEvent<[], DamageTakenExtract>({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
saliency: {
|
|
||||||
threshold: 1,
|
|
||||||
key: 'felt:damage',
|
|
||||||
},
|
|
||||||
|
|
||||||
signal: {
|
|
||||||
type: 'saliency_high',
|
|
||||||
description: extracted => describeDamageTaken(extracted),
|
|
||||||
metadata: extracted => ({
|
|
||||||
kind: 'felt',
|
|
||||||
action: 'damage',
|
|
||||||
amount: extracted.amount,
|
|
||||||
damageSource: extracted.damageSource,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
|
|
||||||
routes: ['conscious', 'reflex', 'debug'],
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -34,22 +34,4 @@ export const entityMovedEvent = definePerceptionEvent<[any], EntityMovedExtract>
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
|
||||||
saliency: {
|
|
||||||
threshold: 5,
|
|
||||||
key: 'move:player',
|
|
||||||
},
|
|
||||||
|
|
||||||
signal: {
|
|
||||||
type: 'entity_attention',
|
|
||||||
description: extracted => `Player ${extracted.displayName || 'unknown'} is moving nearby`,
|
|
||||||
metadata: extracted => ({
|
|
||||||
kind: 'player',
|
|
||||||
action: 'move',
|
|
||||||
distance: extracted.distance,
|
|
||||||
hasLineOfSight: extracted.hasLineOfSight,
|
|
||||||
displayName: extracted.displayName,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
|
|
||||||
routes: ['reflex', 'debug'],
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -21,56 +21,4 @@ export const itemCollectedEvent = definePerceptionEvent<[any, any], ItemCollecte
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
|
||||||
saliency: {
|
|
||||||
threshold: 3,
|
|
||||||
key: 'felt:pickup',
|
|
||||||
},
|
|
||||||
|
|
||||||
signal: {
|
|
||||||
type: 'entity_attention',
|
|
||||||
description: () => 'Picked up an item',
|
|
||||||
metadata: extracted => ({
|
|
||||||
kind: 'felt',
|
|
||||||
action: 'pickup',
|
|
||||||
itemName: extracted.itemName,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
|
|
||||||
routes: ['conscious', 'debug'],
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// TODO: find out if this is needed. Some servers might use entityCollect instead of playerCollect?
|
|
||||||
// export const itemCollectedEntityCollectEvent = definePerceptionEvent<[any, any], ItemCollectedExtract>({
|
|
||||||
// id: 'item_collected_entity_collect',
|
|
||||||
// modality: 'felt',
|
|
||||||
// kind: 'item_collected',
|
|
||||||
|
|
||||||
// mineflayer: {
|
|
||||||
// event: 'entityCollect',
|
|
||||||
// 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'),
|
|
||||||
// }),
|
|
||||||
// },
|
|
||||||
|
|
||||||
// saliency: {
|
|
||||||
// threshold: 3,
|
|
||||||
// key: 'felt:pickup',
|
|
||||||
// },
|
|
||||||
|
|
||||||
// signal: {
|
|
||||||
// type: 'entity_attention',
|
|
||||||
// description: () => 'Picked up an item',
|
|
||||||
// metadata: extracted => ({
|
|
||||||
// kind: 'felt',
|
|
||||||
// action: 'pickup',
|
|
||||||
// itemName: extracted.itemName,
|
|
||||||
// }),
|
|
||||||
// },
|
|
||||||
|
|
||||||
// routes: ['conscious', 'debug'],
|
|
||||||
// })
|
|
||||||
|
|||||||
@@ -59,22 +59,4 @@ export const sneakToggleEvent = definePerceptionEvent<[any], SneakToggleExtract>
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
|
||||||
saliency: {
|
|
||||||
threshold: 5,
|
|
||||||
key: 'teabag:player',
|
|
||||||
},
|
|
||||||
|
|
||||||
signal: {
|
|
||||||
type: 'entity_attention',
|
|
||||||
description: extracted => `Player ${extracted.displayName || 'unknown'} is teabagging (rapid sneaking)`,
|
|
||||||
metadata: extracted => ({
|
|
||||||
kind: 'player',
|
|
||||||
action: 'teabag',
|
|
||||||
distance: extracted.distance,
|
|
||||||
hasLineOfSight: extracted.hasLineOfSight,
|
|
||||||
displayName: extracted.displayName,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
|
|
||||||
routes: ['conscious', 'reflex', 'debug'],
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -28,21 +28,4 @@ export const soundHeardEvent = definePerceptionEvent<[string, Vec3], SoundHeardE
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
|
||||||
saliency: {
|
|
||||||
threshold: 5,
|
|
||||||
key: 'sound:ambient',
|
|
||||||
},
|
|
||||||
|
|
||||||
signal: {
|
|
||||||
type: 'environmental_anomaly',
|
|
||||||
description: extracted => `Heard sound: ${extracted.soundId}`,
|
|
||||||
metadata: extracted => ({
|
|
||||||
kind: 'sound',
|
|
||||||
action: 'sound',
|
|
||||||
soundId: extracted.soundId,
|
|
||||||
distance: extracted.distance,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
|
|
||||||
routes: ['conscious', 'debug'],
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -11,19 +11,4 @@ export const systemMessageEvent = definePerceptionEvent<[string, string], { mess
|
|||||||
extract: (_ctx, message, position) => ({ message, position }),
|
extract: (_ctx, message, position) => ({ message, position }),
|
||||||
},
|
},
|
||||||
|
|
||||||
saliency: {
|
|
||||||
threshold: 1,
|
|
||||||
key: 'system:message',
|
|
||||||
},
|
|
||||||
|
|
||||||
signal: {
|
|
||||||
type: 'system_message',
|
|
||||||
description: extracted => extracted.message,
|
|
||||||
metadata: extracted => ({
|
|
||||||
message: extracted.message,
|
|
||||||
position: extracted.position,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
|
|
||||||
routes: ['reflex'],
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import type { Vec3 } from 'vec3'
|
|||||||
import type {
|
import type {
|
||||||
PerceptionContext,
|
PerceptionContext,
|
||||||
PerceptionEventDefinition,
|
PerceptionEventDefinition,
|
||||||
PerceptionSignal,
|
|
||||||
RawPerceptionEventBase,
|
RawPerceptionEventBase,
|
||||||
} from './types'
|
} from './types'
|
||||||
|
|
||||||
@@ -15,17 +14,6 @@ export function definePerceptionEvent<TArgs extends any[], TExtract>(
|
|||||||
return definition
|
return definition
|
||||||
}
|
}
|
||||||
|
|
||||||
interface WindowCounter {
|
|
||||||
head: number
|
|
||||||
windowSize: number
|
|
||||||
counts: number[]
|
|
||||||
triggers: number[]
|
|
||||||
total: number
|
|
||||||
lastEventSlot: number
|
|
||||||
lastFireSlot: number | null
|
|
||||||
lastFireTotal: number
|
|
||||||
}
|
|
||||||
|
|
||||||
interface RegisteredListener {
|
interface RegisteredListener {
|
||||||
event: string
|
event: string
|
||||||
handler: (...args: any[]) => void
|
handler: (...args: any[]) => void
|
||||||
@@ -33,33 +21,12 @@ interface RegisteredListener {
|
|||||||
|
|
||||||
export interface EventRegistryDeps {
|
export interface EventRegistryDeps {
|
||||||
logger: Logg
|
logger: Logg
|
||||||
onSignal: (signal: PerceptionSignal) => void
|
onRawEvent: (event: RawPerceptionEventBase & Record<string, any>) => void
|
||||||
onRawEvent?: (event: RawPerceptionEventBase & Record<string, any>) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const SLOT_MS = 20
|
|
||||||
const DEFAULT_WINDOW_MS = 2000
|
|
||||||
|
|
||||||
export interface CounterSnapshot {
|
|
||||||
key: string
|
|
||||||
total: number
|
|
||||||
window: number[]
|
|
||||||
triggers: number[]
|
|
||||||
lastFireSlot: number | null
|
|
||||||
lastFireTotal: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SaliencySnapshot {
|
|
||||||
slot: number
|
|
||||||
counters: CounterSnapshot[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class EventRegistry {
|
export class EventRegistry {
|
||||||
private definitions: Map<string, PerceptionEventDefinition> = new Map()
|
private definitions: Map<string, PerceptionEventDefinition> = new Map()
|
||||||
private counters: Map<string, WindowCounter> = new Map()
|
|
||||||
private listeners: RegisteredListener[] = []
|
private listeners: RegisteredListener[] = []
|
||||||
private currentSlot = 0
|
|
||||||
private timer: ReturnType<typeof setInterval> | null = null
|
|
||||||
private context: PerceptionContext | null = null
|
private context: PerceptionContext | null = null
|
||||||
private maxDistance = 32
|
private maxDistance = 32
|
||||||
|
|
||||||
@@ -67,12 +34,6 @@ export class EventRegistry {
|
|||||||
|
|
||||||
public register(definition: PerceptionEventDefinition): void {
|
public register(definition: PerceptionEventDefinition): void {
|
||||||
this.definitions.set(definition.id, definition)
|
this.definitions.set(definition.id, definition)
|
||||||
|
|
||||||
if (definition.saliency) {
|
|
||||||
const windowMs = definition.saliency.windowMs ?? DEFAULT_WINDOW_MS
|
|
||||||
const windowSize = Math.max(1, Math.round(windowMs / SLOT_MS))
|
|
||||||
this.counters.set(definition.saliency.key, this.createCounter(windowSize))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public registerAll(definitions: PerceptionEventDefinition[]): void {
|
public registerAll(definitions: PerceptionEventDefinition[]): void {
|
||||||
@@ -81,47 +42,13 @@ export class EventRegistry {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public start(): void {
|
|
||||||
if (this.timer)
|
|
||||||
return
|
|
||||||
|
|
||||||
this.timer = setInterval(() => {
|
|
||||||
this.currentSlot += 1
|
|
||||||
this.advanceWindows()
|
|
||||||
}, SLOT_MS)
|
|
||||||
}
|
|
||||||
|
|
||||||
public stop(): void {
|
public stop(): void {
|
||||||
if (this.timer) {
|
|
||||||
clearInterval(this.timer)
|
|
||||||
this.timer = null
|
|
||||||
}
|
|
||||||
// NOTICE: Nullify context so that any stale mineflayer listeners that fire
|
// NOTICE: Nullify context so that any stale mineflayer listeners that fire
|
||||||
// after stop() (but before detachFromBot()) are silently ignored by the
|
// after stop() (but before detachFromBot()) are silently ignored by the
|
||||||
// guard at the top of handleMineflayerEvent.
|
// guard at the top of handleMineflayerEvent.
|
||||||
this.context = null
|
this.context = null
|
||||||
}
|
}
|
||||||
|
|
||||||
public getDebugSnapshot(): SaliencySnapshot {
|
|
||||||
const counters: CounterSnapshot[] = []
|
|
||||||
|
|
||||||
for (const [key, counter] of this.counters.entries()) {
|
|
||||||
counters.push({
|
|
||||||
key,
|
|
||||||
total: counter.total,
|
|
||||||
window: this.exportWindow(counter),
|
|
||||||
triggers: this.exportTriggers(counter),
|
|
||||||
lastFireSlot: counter.lastFireSlot,
|
|
||||||
lastFireTotal: counter.lastFireTotal,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
slot: this.currentSlot,
|
|
||||||
counters,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public attachToBot(bot: Bot, maxDistance = 32): void {
|
public attachToBot(bot: Bot, maxDistance = 32): void {
|
||||||
this.maxDistance = maxDistance
|
this.maxDistance = maxDistance
|
||||||
this.context = this.createContext(bot)
|
this.context = this.createContext(bot)
|
||||||
@@ -175,22 +102,10 @@ export class EventRegistry {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public getSignalTypes(): string[] {
|
|
||||||
const types = new Set<string>()
|
|
||||||
for (const def of this.definitions.values()) {
|
|
||||||
types.add(def.signal.type)
|
|
||||||
}
|
|
||||||
return Array.from(types)
|
|
||||||
}
|
|
||||||
|
|
||||||
public getDefinitions(): PerceptionEventDefinition[] {
|
public getDefinitions(): PerceptionEventDefinition[] {
|
||||||
return Array.from(this.definitions.values())
|
return Array.from(this.definitions.values())
|
||||||
}
|
}
|
||||||
|
|
||||||
public getSaliencyKeys(): string[] {
|
|
||||||
return Array.from(this.counters.keys())
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleMineflayerEvent(def: PerceptionEventDefinition, args: any[]): void {
|
private handleMineflayerEvent(def: PerceptionEventDefinition, args: any[]): void {
|
||||||
if (!this.context)
|
if (!this.context)
|
||||||
return
|
return
|
||||||
@@ -210,95 +125,7 @@ export class EventRegistry {
|
|||||||
...extracted,
|
...extracted,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (def.routes.includes('reflex'))
|
this.deps.onRawEvent(rawEvent)
|
||||||
this.deps.onRawEvent?.(rawEvent)
|
|
||||||
|
|
||||||
if (def.saliency) {
|
|
||||||
const shouldEmit = this.incrementAndCheck(def.saliency.key, def.saliency.threshold)
|
|
||||||
if (!shouldEmit)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const signal: PerceptionSignal = {
|
|
||||||
type: def.signal.type,
|
|
||||||
description: def.signal.description(extracted, timestamp),
|
|
||||||
sourceId: def.id,
|
|
||||||
confidence: 1.0,
|
|
||||||
timestamp,
|
|
||||||
metadata: def.signal.metadata?.(extracted) ?? extracted,
|
|
||||||
}
|
|
||||||
|
|
||||||
if (def.routes.includes('conscious'))
|
|
||||||
this.deps.onSignal(signal)
|
|
||||||
}
|
|
||||||
|
|
||||||
private incrementAndCheck(key: string, threshold: number): boolean {
|
|
||||||
const counter = this.counters.get(key)
|
|
||||||
if (!counter)
|
|
||||||
return true
|
|
||||||
|
|
||||||
counter.counts[counter.head] = (counter.counts[counter.head] ?? 0) + 1
|
|
||||||
counter.total += 1
|
|
||||||
counter.lastEventSlot = this.currentSlot
|
|
||||||
|
|
||||||
if (counter.total >= threshold) {
|
|
||||||
counter.lastFireSlot = this.currentSlot
|
|
||||||
counter.lastFireTotal = counter.total
|
|
||||||
counter.triggers[counter.head] = 1
|
|
||||||
this.resetCounter(counter)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
private createCounter(windowSize: number): WindowCounter {
|
|
||||||
return {
|
|
||||||
head: 0,
|
|
||||||
windowSize,
|
|
||||||
counts: new Array(windowSize).fill(0),
|
|
||||||
triggers: new Array(windowSize).fill(0),
|
|
||||||
total: 0,
|
|
||||||
lastEventSlot: 0,
|
|
||||||
lastFireSlot: null,
|
|
||||||
lastFireTotal: 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private resetCounter(counter: WindowCounter): void {
|
|
||||||
counter.total = 0
|
|
||||||
counter.counts.fill(0)
|
|
||||||
// do not reset triggers; they are historical markers for debug
|
|
||||||
}
|
|
||||||
|
|
||||||
private advanceWindows(): void {
|
|
||||||
for (const counter of this.counters.values()) {
|
|
||||||
counter.head = (counter.head + 1) % counter.windowSize
|
|
||||||
const expired = counter.counts[counter.head] ?? 0
|
|
||||||
if (expired > 0) {
|
|
||||||
counter.total = Math.max(0, counter.total - expired)
|
|
||||||
}
|
|
||||||
counter.counts[counter.head] = 0
|
|
||||||
counter.triggers[counter.head] = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private exportWindow(counter: WindowCounter): number[] {
|
|
||||||
const out = new Array<number>(counter.windowSize)
|
|
||||||
for (let i = 0; i < counter.windowSize; i++) {
|
|
||||||
const idx = (counter.head + 1 + i) % counter.windowSize
|
|
||||||
out[i] = counter.counts[idx] ?? 0
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
private exportTriggers(counter: WindowCounter): number[] {
|
|
||||||
const out = new Array<number>(counter.windowSize)
|
|
||||||
for (let i = 0; i < counter.windowSize; i++) {
|
|
||||||
const idx = (counter.head + 1 + i) % counter.windowSize
|
|
||||||
out[i] = counter.triggers[idx] ?? 0
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ import type { Vec3 } from 'vec3'
|
|||||||
|
|
||||||
export type PerceptionModality = 'sighted' | 'heard' | 'felt' | 'system'
|
export type PerceptionModality = 'sighted' | 'heard' | 'felt' | 'system'
|
||||||
|
|
||||||
export type PerceptionRoute = 'reflex' | 'conscious' | 'debug'
|
|
||||||
|
|
||||||
export interface PerceptionContext {
|
export interface PerceptionContext {
|
||||||
bot: Bot
|
bot: Bot
|
||||||
selfUsername: string
|
selfUsername: string
|
||||||
@@ -21,30 +19,12 @@ export interface MineflayerBinding<TArgs extends any[] = any[], TExtract = any>
|
|||||||
extract: (ctx: PerceptionContext, ...args: TArgs) => TExtract
|
extract: (ctx: PerceptionContext, ...args: TArgs) => TExtract
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SaliencyConfig {
|
|
||||||
threshold: number
|
|
||||||
key: string
|
|
||||||
windowMs?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SignalConfig<TExtract> {
|
|
||||||
type: string
|
|
||||||
description: (extracted: TExtract, timestamp: number) => string
|
|
||||||
metadata?: (extracted: TExtract) => Record<string, any>
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PerceptionEventDefinition<TArgs extends any[] = any[], TExtract = any> {
|
export interface PerceptionEventDefinition<TArgs extends any[] = any[], TExtract = any> {
|
||||||
id: string
|
id: string
|
||||||
modality: PerceptionModality
|
modality: PerceptionModality
|
||||||
kind: string
|
kind: string
|
||||||
|
|
||||||
mineflayer: MineflayerBinding<TArgs, TExtract>
|
mineflayer: MineflayerBinding<TArgs, TExtract>
|
||||||
|
|
||||||
saliency?: SaliencyConfig
|
|
||||||
|
|
||||||
signal: SignalConfig<TExtract>
|
|
||||||
|
|
||||||
routes: PerceptionRoute[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RawPerceptionEventBase {
|
export interface RawPerceptionEventBase {
|
||||||
@@ -53,12 +33,3 @@ export interface RawPerceptionEventBase {
|
|||||||
timestamp: number
|
timestamp: number
|
||||||
source: string
|
source: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PerceptionSignal {
|
|
||||||
type: string
|
|
||||||
description: string
|
|
||||||
sourceId?: string
|
|
||||||
confidence: number
|
|
||||||
timestamp: number
|
|
||||||
metadata: Record<string, any>
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
import type { RawPerceptionEvent } from './types/raw-events'
|
|
||||||
|
|
||||||
export type PerceptionFrameSource = 'minecraft'
|
|
||||||
|
|
||||||
export type PerceptionFrameKind
|
|
||||||
= | 'world_raw'
|
|
||||||
| 'chat_raw'
|
|
||||||
|
|
||||||
export interface PerceptionFrame {
|
|
||||||
id: string
|
|
||||||
ts: number
|
|
||||||
source: PerceptionFrameSource
|
|
||||||
kind: PerceptionFrameKind
|
|
||||||
raw: RawPerceptionEvent | { username: string, message: string }
|
|
||||||
signals: Array<{ type: string, payload: any }>
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createPerceptionFrameFromRawEvent(event: RawPerceptionEvent): PerceptionFrame {
|
|
||||||
return {
|
|
||||||
id: `p_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`,
|
|
||||||
ts: event.timestamp,
|
|
||||||
source: 'minecraft',
|
|
||||||
kind: 'world_raw',
|
|
||||||
raw: event,
|
|
||||||
signals: [],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createPerceptionFrameFromChat(username: string, message: string): PerceptionFrame {
|
|
||||||
return {
|
|
||||||
id: `p_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`,
|
|
||||||
ts: Date.now(),
|
|
||||||
source: 'minecraft',
|
|
||||||
kind: 'chat_raw',
|
|
||||||
raw: {
|
|
||||||
username,
|
|
||||||
message,
|
|
||||||
},
|
|
||||||
signals: [],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
export interface LeakyBucketConfig {
|
|
||||||
capacity: number
|
|
||||||
leakPerSecond: number
|
|
||||||
trigger: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export class LeakyBucket { // Shoutout to Makito for this idea
|
|
||||||
private value = 0
|
|
||||||
private readonly capacity: number
|
|
||||||
private readonly leakPerMs: number
|
|
||||||
private readonly trigger: number
|
|
||||||
|
|
||||||
constructor(config: LeakyBucketConfig) {
|
|
||||||
this.capacity = config.capacity
|
|
||||||
this.leakPerMs = config.leakPerSecond / 1000
|
|
||||||
this.trigger = config.trigger
|
|
||||||
}
|
|
||||||
|
|
||||||
public tick(deltaMs: number): void {
|
|
||||||
if (deltaMs <= 0)
|
|
||||||
return
|
|
||||||
this.value = Math.max(0, this.value - this.leakPerMs * deltaMs)
|
|
||||||
}
|
|
||||||
|
|
||||||
public add(amount: number): { fired: boolean, value: number } {
|
|
||||||
const next = Math.min(this.capacity, this.value + amount)
|
|
||||||
const fired = this.value < this.trigger && next >= this.trigger
|
|
||||||
this.value = next
|
|
||||||
return { fired, value: this.value }
|
|
||||||
}
|
|
||||||
|
|
||||||
public getValue(): number {
|
|
||||||
return this.value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,442 +0,0 @@
|
|||||||
import type { Logg } from '@guiiai/logg'
|
|
||||||
import type { Vec3 } from 'vec3'
|
|
||||||
|
|
||||||
import type { MineflayerWithAgents } from '../types'
|
|
||||||
import type {
|
|
||||||
FeltDamageTakenEvent,
|
|
||||||
FeltItemCollectedEvent,
|
|
||||||
HeardSoundEvent,
|
|
||||||
PlayerJoinedEvent,
|
|
||||||
RawPerceptionEvent,
|
|
||||||
SightedArmSwingEvent,
|
|
||||||
SightedEntityMovedEvent,
|
|
||||||
SightedSneakToggleEvent,
|
|
||||||
SystemMessageEvent,
|
|
||||||
} from './types/raw-events'
|
|
||||||
|
|
||||||
export class MineflayerPerceptionCollector {
|
|
||||||
private bot: MineflayerWithAgents | null = null
|
|
||||||
private readonly listeners: Array<{
|
|
||||||
event: string
|
|
||||||
handler: (...args: any[]) => void
|
|
||||||
}> = []
|
|
||||||
|
|
||||||
private knownPlayerIds: Set<string> = new Set()
|
|
||||||
|
|
||||||
private lastSelfHealth: number | null = null
|
|
||||||
private lastStatsAt = 0
|
|
||||||
private stats: Record<string, number> = {}
|
|
||||||
private sneakingState: Map<string, boolean> = new Map()
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private readonly deps: {
|
|
||||||
logger: Logg
|
|
||||||
emitRaw: (event: RawPerceptionEvent) => void
|
|
||||||
maxDistance: number
|
|
||||||
},
|
|
||||||
) { }
|
|
||||||
|
|
||||||
public init(bot: MineflayerWithAgents): void {
|
|
||||||
this.bot = bot
|
|
||||||
this.lastSelfHealth = bot.bot.health
|
|
||||||
this.lastStatsAt = Date.now()
|
|
||||||
this.stats = {}
|
|
||||||
this.knownPlayerIds = this.snapshotKnownPlayers(bot)
|
|
||||||
|
|
||||||
this.deps.logger.withFields({ maxDistance: this.deps.maxDistance }).log('MineflayerPerceptionCollector: init')
|
|
||||||
|
|
||||||
this.registerEventHandlers()
|
|
||||||
}
|
|
||||||
|
|
||||||
public destroy(): void {
|
|
||||||
if (!this.bot)
|
|
||||||
return
|
|
||||||
|
|
||||||
this.deps.logger.withFields({ listeners: this.listeners.length }).log('MineflayerPerceptionCollector: destroy')
|
|
||||||
|
|
||||||
for (const { event, handler } of this.listeners) {
|
|
||||||
try {
|
|
||||||
const b = this.bot.bot as any
|
|
||||||
b.off?.(event, handler)
|
|
||||||
b.removeListener?.(event, handler)
|
|
||||||
}
|
|
||||||
catch (err) {
|
|
||||||
this.deps.logger.withError(err as Error).error('MineflayerPerceptionCollector: failed to remove listener')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.listeners.length = 0
|
|
||||||
this.lastSelfHealth = null
|
|
||||||
this.bot = null
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========================================
|
|
||||||
// Event Handler Registration
|
|
||||||
// ========================================
|
|
||||||
|
|
||||||
private registerEventHandlers(): void {
|
|
||||||
// NOTICE: Sighted events (entityMoved, entitySwingArm, entityUpdate) now handled by EventRegistry
|
|
||||||
// See perception/events/definitions/arm-swing.ts, sneak-toggle.ts, entity-moved.ts
|
|
||||||
// NOTICE: Heard/Felt events (soundEffectHeard, health, playerCollect) now handled by EventRegistry
|
|
||||||
// See perception/events/definitions/sound-heard.ts, damage-taken.ts, item-collected.ts
|
|
||||||
// NOTICE: playerJoined hook disconnected - system messages now cover join/leave events
|
|
||||||
// this.onBot('playerJoined', player => this.handlePlayerJoined(player))
|
|
||||||
// this.onBot('playerUpdated', () => this.handlePlayersMaybeChanged())
|
|
||||||
// NOTICE: messagestr now handled by EventRegistry in perception/events/definitions/system-message.ts
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========================================
|
|
||||||
// Sighted Event Handlers
|
|
||||||
// NOTICE: These handlers are now handled by EventRegistry
|
|
||||||
// Kept for reference during migration
|
|
||||||
// ========================================
|
|
||||||
|
|
||||||
// @ts-expect-error Intentionally unused - migrated to EventRegistry
|
|
||||||
private handleEntityMoved(entity: any): void {
|
|
||||||
if (!this.isValidEntityInRange(entity))
|
|
||||||
return
|
|
||||||
|
|
||||||
const event: SightedEntityMovedEvent = {
|
|
||||||
modality: 'sighted',
|
|
||||||
kind: 'entity_moved',
|
|
||||||
entityType: entity?.type === 'player' ? 'player' : 'mob',
|
|
||||||
entityId: this.entityId(entity),
|
|
||||||
displayName: entity?.username,
|
|
||||||
distance: this.distanceTo(entity)!,
|
|
||||||
hasLineOfSight: true,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
source: 'minecraft',
|
|
||||||
pos: entity?.position,
|
|
||||||
}
|
|
||||||
|
|
||||||
this.emitEvent(event, 'sighted.entity_moved')
|
|
||||||
}
|
|
||||||
|
|
||||||
// @ts-expect-error Intentionally unused - migrated to EventRegistry
|
|
||||||
private handleEntitySwingArm(entity: any): void {
|
|
||||||
if (!this.isValidEntityInRange(entity))
|
|
||||||
return
|
|
||||||
|
|
||||||
const event: SightedArmSwingEvent = {
|
|
||||||
modality: 'sighted',
|
|
||||||
kind: 'arm_swing',
|
|
||||||
entityType: 'player',
|
|
||||||
entityId: this.entityId(entity),
|
|
||||||
displayName: entity?.username,
|
|
||||||
distance: this.distanceTo(entity)!,
|
|
||||||
hasLineOfSight: true,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
source: 'minecraft',
|
|
||||||
pos: entity?.position,
|
|
||||||
}
|
|
||||||
|
|
||||||
this.emitEvent(event, 'sighted.arm_swing')
|
|
||||||
}
|
|
||||||
|
|
||||||
// @ts-expect-error Intentionally unused - migrated to EventRegistry
|
|
||||||
private handleEntityUpdate(entity: any): void {
|
|
||||||
if (!entity || entity.type !== 'player')
|
|
||||||
return
|
|
||||||
|
|
||||||
if (this.isSelfEntity(entity))
|
|
||||||
return
|
|
||||||
|
|
||||||
const entityId = this.entityId(entity)
|
|
||||||
const isSneaking = this.extractSneakingState(entity)
|
|
||||||
|
|
||||||
if (!this.hasSneakingStateChanged(entityId, isSneaking))
|
|
||||||
return
|
|
||||||
|
|
||||||
this.sneakingState.set(entityId, isSneaking)
|
|
||||||
|
|
||||||
const dist = this.distanceTo(entity)
|
|
||||||
if (dist === null || dist > this.deps.maxDistance)
|
|
||||||
return
|
|
||||||
|
|
||||||
const event: SightedSneakToggleEvent = {
|
|
||||||
modality: 'sighted',
|
|
||||||
kind: 'sneak_toggle',
|
|
||||||
entityType: 'player',
|
|
||||||
entityId,
|
|
||||||
displayName: entity?.username,
|
|
||||||
distance: dist,
|
|
||||||
hasLineOfSight: true,
|
|
||||||
sneaking: isSneaking,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
source: 'minecraft',
|
|
||||||
pos: entity?.position,
|
|
||||||
}
|
|
||||||
|
|
||||||
this.emitEvent(event, 'sighted.sneak_toggle')
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========================================
|
|
||||||
// Heard Event Handlers
|
|
||||||
// NOTICE: These handlers are now handled by EventRegistry
|
|
||||||
// ========================================
|
|
||||||
|
|
||||||
// @ts-expect-error Intentionally unused - migrated to EventRegistry
|
|
||||||
private handleSoundHeard(soundId: string, pos: Vec3): void {
|
|
||||||
if (!pos)
|
|
||||||
return
|
|
||||||
|
|
||||||
const dist = this.distanceToPos(pos)
|
|
||||||
if (dist === null || dist > this.deps.maxDistance)
|
|
||||||
return
|
|
||||||
|
|
||||||
const event: HeardSoundEvent = {
|
|
||||||
modality: 'heard',
|
|
||||||
kind: 'sound',
|
|
||||||
soundId,
|
|
||||||
distance: dist,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
source: 'minecraft',
|
|
||||||
pos,
|
|
||||||
}
|
|
||||||
|
|
||||||
this.deps.emitRaw(event)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========================================
|
|
||||||
// Felt Event Handlers
|
|
||||||
// NOTICE: These handlers are now handled by EventRegistry
|
|
||||||
// ========================================
|
|
||||||
|
|
||||||
// @ts-expect-error Intentionally unused - migrated to EventRegistry
|
|
||||||
private handleHealthChange(): void {
|
|
||||||
if (!this.bot)
|
|
||||||
return
|
|
||||||
|
|
||||||
const current = this.bot.bot.health
|
|
||||||
const prev = this.lastSelfHealth
|
|
||||||
this.lastSelfHealth = current
|
|
||||||
|
|
||||||
if (typeof prev !== 'number' || current >= prev)
|
|
||||||
return
|
|
||||||
|
|
||||||
const event: FeltDamageTakenEvent = {
|
|
||||||
modality: 'felt',
|
|
||||||
kind: 'damage_taken',
|
|
||||||
amount: prev - current,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
source: 'minecraft',
|
|
||||||
}
|
|
||||||
|
|
||||||
this.emitEvent(event, 'felt.damage_taken')
|
|
||||||
}
|
|
||||||
|
|
||||||
// @ts-expect-error Intentionally unused - migrated to EventRegistry
|
|
||||||
private handleItemCollected(collector: any, collected: any): void {
|
|
||||||
if (!this.bot || !collector)
|
|
||||||
return
|
|
||||||
|
|
||||||
if (collector.username !== this.bot.bot.username)
|
|
||||||
return
|
|
||||||
|
|
||||||
const itemName = String(collected?.name ?? collected?.displayName ?? collected?.type ?? 'unknown')
|
|
||||||
|
|
||||||
const event: FeltItemCollectedEvent = {
|
|
||||||
modality: 'felt',
|
|
||||||
kind: 'item_collected',
|
|
||||||
itemName,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
source: 'minecraft',
|
|
||||||
}
|
|
||||||
|
|
||||||
this.emitEvent(event, 'felt.item_collected')
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========================================
|
|
||||||
// System Event Handlers
|
|
||||||
// ========================================
|
|
||||||
|
|
||||||
// NOTICE: handleSystemMessage now handled by EventRegistry
|
|
||||||
// @ts-expect-error Intentionally unused - kept for reference during migration
|
|
||||||
private handleSystemMessage(message: string, messagePosition: string): void {
|
|
||||||
// messagePosition: 'chat' = player chat, 'system' = system message, 'game_info' = action bar
|
|
||||||
if (messagePosition !== 'system')
|
|
||||||
return
|
|
||||||
|
|
||||||
const event: SystemMessageEvent = {
|
|
||||||
modality: 'system',
|
|
||||||
kind: 'system_message',
|
|
||||||
message,
|
|
||||||
position: messagePosition,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
source: 'minecraft',
|
|
||||||
}
|
|
||||||
|
|
||||||
this.emitEvent(event, 'system.system_message')
|
|
||||||
}
|
|
||||||
|
|
||||||
// NOTICE: handlePlayerJoined and handlePlayersMaybeChanged are disconnected
|
|
||||||
// System messages now cover join/leave events via messagestr
|
|
||||||
// @ts-expect-error Intentionally unused - kept for potential future reconnection
|
|
||||||
private handlePlayerJoined(player: any): void {
|
|
||||||
if (!player)
|
|
||||||
return
|
|
||||||
|
|
||||||
if (player.username === this.bot?.bot.username)
|
|
||||||
return
|
|
||||||
|
|
||||||
const playerId = String(player.uuid ?? player.id ?? player.username ?? 'unknown')
|
|
||||||
if (this.knownPlayerIds.has(playerId))
|
|
||||||
return
|
|
||||||
|
|
||||||
this.knownPlayerIds.add(playerId)
|
|
||||||
|
|
||||||
const event: PlayerJoinedEvent = {
|
|
||||||
modality: 'system',
|
|
||||||
kind: 'player_joined',
|
|
||||||
playerId,
|
|
||||||
displayName: player.username,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
source: 'minecraft',
|
|
||||||
}
|
|
||||||
|
|
||||||
this.emitEvent(event, 'system.player_joined')
|
|
||||||
}
|
|
||||||
|
|
||||||
// @ts-expect-error Intentionally unused - kept for potential future reconnection
|
|
||||||
private handlePlayersMaybeChanged(): void {
|
|
||||||
const bot = this.bot
|
|
||||||
if (!bot)
|
|
||||||
return
|
|
||||||
|
|
||||||
const current = this.snapshotKnownPlayers(bot)
|
|
||||||
|
|
||||||
for (const playerId of current) {
|
|
||||||
if (this.knownPlayerIds.has(playerId))
|
|
||||||
continue
|
|
||||||
|
|
||||||
this.knownPlayerIds.add(playerId)
|
|
||||||
|
|
||||||
const player = bot.bot.players?.[playerId]
|
|
||||||
const username = player?.username
|
|
||||||
|
|
||||||
const event: PlayerJoinedEvent = {
|
|
||||||
modality: 'system',
|
|
||||||
kind: 'player_joined',
|
|
||||||
playerId,
|
|
||||||
displayName: typeof username === 'string' ? username : undefined,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
source: 'minecraft',
|
|
||||||
}
|
|
||||||
|
|
||||||
this.emitEvent(event, 'system.player_joined')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private snapshotKnownPlayers(bot: MineflayerWithAgents): Set<string> {
|
|
||||||
const out = new Set<string>()
|
|
||||||
const players = bot.bot.players as Record<string, any> | undefined
|
|
||||||
if (!players)
|
|
||||||
return out
|
|
||||||
|
|
||||||
const selfUsername = bot.bot.username
|
|
||||||
|
|
||||||
for (const [id, player] of Object.entries(players)) {
|
|
||||||
if (!id)
|
|
||||||
continue
|
|
||||||
|
|
||||||
const username = player?.username
|
|
||||||
if (username && username === selfUsername)
|
|
||||||
continue
|
|
||||||
|
|
||||||
out.add(String(id))
|
|
||||||
}
|
|
||||||
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========================================
|
|
||||||
// Validation Helpers
|
|
||||||
// ========================================
|
|
||||||
|
|
||||||
private isValidEntityInRange(entity: any): boolean {
|
|
||||||
const dist = this.distanceTo(entity)
|
|
||||||
if (dist === null || dist > this.deps.maxDistance)
|
|
||||||
return false
|
|
||||||
|
|
||||||
if (this.isSelfEntity(entity))
|
|
||||||
return false
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private isSelfEntity(entity: any): boolean {
|
|
||||||
return entity.username === this.bot?.bot.username
|
|
||||||
}
|
|
||||||
|
|
||||||
private extractSneakingState(entity: any): boolean {
|
|
||||||
const flags = entity?.metadata?.[0]
|
|
||||||
// Bit 1 (0x02) is sneaking
|
|
||||||
return typeof flags === 'number' ? !!(flags & 0x02) : false
|
|
||||||
}
|
|
||||||
|
|
||||||
private hasSneakingStateChanged(entityId: string, isSneaking: boolean): boolean {
|
|
||||||
const lastState = this.sneakingState.get(entityId)
|
|
||||||
return lastState !== isSneaking
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========================================
|
|
||||||
// Utilities
|
|
||||||
// ========================================
|
|
||||||
|
|
||||||
private emitEvent(event: RawPerceptionEvent, statKey: string): void {
|
|
||||||
this.deps.emitRaw(event)
|
|
||||||
this.bumpStat(statKey)
|
|
||||||
this.maybeLogStats()
|
|
||||||
}
|
|
||||||
|
|
||||||
private bumpStat(key: string): void {
|
|
||||||
this.stats[key] = (this.stats[key] ?? 0) + 1
|
|
||||||
}
|
|
||||||
|
|
||||||
private maybeLogStats(): void {
|
|
||||||
const now = Date.now()
|
|
||||||
if (now - this.lastStatsAt < 2000)
|
|
||||||
return
|
|
||||||
|
|
||||||
// this.deps.logger.withFields({
|
|
||||||
// ...this.stats,
|
|
||||||
// }).log('MineflayerPerceptionCollector: stats')
|
|
||||||
|
|
||||||
this.lastStatsAt = now
|
|
||||||
this.stats = {}
|
|
||||||
}
|
|
||||||
|
|
||||||
// @ts-expect-error Intentionally unused - all events migrated to EventRegistry
|
|
||||||
private onBot(event: string, handler: (...args: any[]) => void): void {
|
|
||||||
if (!this.bot)
|
|
||||||
return
|
|
||||||
|
|
||||||
(this.bot.bot as any).on(event, handler)
|
|
||||||
this.listeners.push({ event, handler })
|
|
||||||
}
|
|
||||||
|
|
||||||
private entityId(entity: any): string {
|
|
||||||
return String(entity?.id ?? entity?.uuid ?? entity?.username ?? 'unknown')
|
|
||||||
}
|
|
||||||
|
|
||||||
private distanceTo(entity: any): number | null {
|
|
||||||
const pos = entity?.position
|
|
||||||
if (!pos)
|
|
||||||
return null
|
|
||||||
return this.distanceToPos(pos)
|
|
||||||
}
|
|
||||||
|
|
||||||
private distanceToPos(pos: Vec3): number | null {
|
|
||||||
if (!this.bot)
|
|
||||||
return null
|
|
||||||
const selfPos = this.bot.bot.entity?.position
|
|
||||||
if (!selfPos)
|
|
||||||
return null
|
|
||||||
try {
|
|
||||||
return selfPos.distanceTo(pos)
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,11 +2,7 @@ import type { Logg } from '@guiiai/logg'
|
|||||||
|
|
||||||
import type { EventBus } from '../os'
|
import type { EventBus } from '../os'
|
||||||
import type { MineflayerWithAgents } from '../types'
|
import type { MineflayerWithAgents } from '../types'
|
||||||
import type { PerceptionFrame } from './frame'
|
|
||||||
import type { RawPerceptionEvent } from './types/raw-events'
|
|
||||||
import type { PerceptionStage } from './types/stage'
|
|
||||||
|
|
||||||
import { DebugService } from '../../debug'
|
|
||||||
import { EventRegistry } from './events'
|
import { EventRegistry } from './events'
|
||||||
import { allEventDefinitions } from './events/definitions'
|
import { allEventDefinitions } from './events/definitions'
|
||||||
import { PerceptionAPI } from './perception-api'
|
import { PerceptionAPI } from './perception-api'
|
||||||
@@ -15,11 +11,6 @@ export class PerceptionPipeline {
|
|||||||
private readonly perception: PerceptionAPI
|
private readonly perception: PerceptionAPI
|
||||||
private readonly eventRegistry: EventRegistry
|
private readonly eventRegistry: EventRegistry
|
||||||
private bot: MineflayerWithAgents | null = null
|
private bot: MineflayerWithAgents | null = null
|
||||||
private initialized = false
|
|
||||||
|
|
||||||
private readonly stages: PerceptionStage[]
|
|
||||||
|
|
||||||
private saliencyEmitTimer: ReturnType<typeof setInterval> | null = null
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly deps: {
|
private readonly deps: {
|
||||||
@@ -31,12 +22,6 @@ export class PerceptionPipeline {
|
|||||||
|
|
||||||
this.eventRegistry = new EventRegistry({
|
this.eventRegistry = new EventRegistry({
|
||||||
logger: this.deps.logger,
|
logger: this.deps.logger,
|
||||||
onSignal: (signal) => {
|
|
||||||
// NOTICE: In the linear architecture, Perception does not emit signal:* directly.
|
|
||||||
// Raw events are emitted and the rules layer derives signals. Reflex then forwards
|
|
||||||
// selected signals to conscious.
|
|
||||||
void signal
|
|
||||||
},
|
|
||||||
onRawEvent: (event) => {
|
onRawEvent: (event) => {
|
||||||
const eventType = `raw:${event.modality}:${event.kind}`
|
const eventType = `raw:${event.modality}:${event.kind}`
|
||||||
this.deps.eventBus.emit({
|
this.deps.eventBus.emit({
|
||||||
@@ -47,63 +32,14 @@ export class PerceptionPipeline {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
this.eventRegistry.registerAll(allEventDefinitions)
|
this.eventRegistry.registerAll(allEventDefinitions)
|
||||||
|
|
||||||
this.stages = [
|
|
||||||
{
|
|
||||||
name: 'entity_update',
|
|
||||||
handle: (frame) => {
|
|
||||||
if (frame.kind !== 'world_raw')
|
|
||||||
return frame
|
|
||||||
|
|
||||||
const raw = frame.raw as RawPerceptionEvent
|
|
||||||
|
|
||||||
// Feed entity updates to PerceptionAPI
|
|
||||||
if ('entityId' in raw && 'entityType' in raw) {
|
|
||||||
const entityRaw = raw as RawPerceptionEvent & { entityId: string, entityType: string, displayName?: string, pos?: { x: number, y: number, z: number } }
|
|
||||||
if (entityRaw.entityType === 'player') {
|
|
||||||
this.perception.updateEntity(entityRaw.entityId, {
|
|
||||||
id: entityRaw.entityId,
|
|
||||||
type: 'player',
|
|
||||||
name: entityRaw.displayName,
|
|
||||||
position: entityRaw.pos as any,
|
|
||||||
isSneaking: 'sneaking' in entityRaw ? (entityRaw as any).sneaking : undefined,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return frame
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'attention',
|
|
||||||
handle: (frame) => {
|
|
||||||
if (frame.kind !== 'world_raw')
|
|
||||||
return frame
|
|
||||||
|
|
||||||
const raw = frame.raw as RawPerceptionEvent
|
|
||||||
// Legacy pipeline saliency disabled; EventRegistry is the source of truth.
|
|
||||||
// Keep raw emission for now in case some other component still ingests frames.
|
|
||||||
this.emitRawToEventBus(raw)
|
|
||||||
return frame
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public init(bot: MineflayerWithAgents): void {
|
public init(bot: MineflayerWithAgents): void {
|
||||||
this.initialized = true
|
|
||||||
this.bot = bot
|
this.bot = bot
|
||||||
|
|
||||||
this.deps.logger.withFields({ maxDistance: 32 }).log('PerceptionPipeline: init')
|
this.deps.logger.withFields({ maxDistance: 32 }).log('PerceptionPipeline: init')
|
||||||
|
|
||||||
this.eventRegistry.start()
|
|
||||||
this.eventRegistry.attachToBot(bot.bot, 32)
|
this.eventRegistry.attachToBot(bot.bot, 32)
|
||||||
|
|
||||||
this.saliencyEmitTimer = setInterval(() => {
|
|
||||||
if (!this.initialized)
|
|
||||||
return
|
|
||||||
DebugService.getInstance().emit('saliency', this.eventRegistry.getDebugSnapshot())
|
|
||||||
}, 100)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public destroy(): void {
|
public destroy(): void {
|
||||||
@@ -114,12 +50,6 @@ export class PerceptionPipeline {
|
|||||||
}
|
}
|
||||||
this.eventRegistry.stop()
|
this.eventRegistry.stop()
|
||||||
this.bot = null
|
this.bot = null
|
||||||
|
|
||||||
if (this.saliencyEmitTimer) {
|
|
||||||
clearInterval(this.saliencyEmitTimer)
|
|
||||||
this.saliencyEmitTimer = null
|
|
||||||
}
|
|
||||||
this.initialized = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -128,47 +58,4 @@ export class PerceptionPipeline {
|
|||||||
public getPerceptionAPI(): PerceptionAPI {
|
public getPerceptionAPI(): PerceptionAPI {
|
||||||
return this.perception
|
return this.perception
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all registered signal types from the EventRegistry
|
|
||||||
* Used by consumers (e.g., Brain) to dynamically subscribe to signals
|
|
||||||
*/
|
|
||||||
public getSignalTypes(): string[] {
|
|
||||||
return this.eventRegistry.getSignalTypes()
|
|
||||||
}
|
|
||||||
|
|
||||||
public ingest(frame: PerceptionFrame): void {
|
|
||||||
if (!this.initialized)
|
|
||||||
return
|
|
||||||
|
|
||||||
let current: PerceptionFrame | null = frame
|
|
||||||
for (const stage of this.stages) {
|
|
||||||
if (!current)
|
|
||||||
break
|
|
||||||
try {
|
|
||||||
current = stage.handle(current)
|
|
||||||
}
|
|
||||||
catch (err) {
|
|
||||||
this.deps.logger.withError(err as Error).error('PerceptionPipeline: stage error')
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Emit a raw perception event to the EventBus
|
|
||||||
* This bridges the perception system to the rules layer
|
|
||||||
*/
|
|
||||||
private emitRawToEventBus(raw: RawPerceptionEvent): void {
|
|
||||||
const eventType = `raw:${raw.modality}:${raw.kind}`
|
|
||||||
|
|
||||||
this.deps.eventBus.emit({
|
|
||||||
type: eventType,
|
|
||||||
payload: Object.freeze(raw),
|
|
||||||
source: {
|
|
||||||
component: 'perception',
|
|
||||||
id: raw.source,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
import type { PerceptionFrame } from './frame'
|
|
||||||
|
|
||||||
export class RawEventBuffer {
|
|
||||||
private queue: PerceptionFrame[] = []
|
|
||||||
|
|
||||||
public push(event: PerceptionFrame): void {
|
|
||||||
this.queue.push(event)
|
|
||||||
}
|
|
||||||
|
|
||||||
public size(): number {
|
|
||||||
return this.queue.length
|
|
||||||
}
|
|
||||||
|
|
||||||
public drain(): PerceptionFrame[] {
|
|
||||||
if (this.queue.length === 0)
|
|
||||||
return []
|
|
||||||
const drained = this.queue
|
|
||||||
this.queue = []
|
|
||||||
return drained
|
|
||||||
}
|
|
||||||
|
|
||||||
public clear(): void {
|
|
||||||
this.queue = []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from './types/raw-events'
|
|
||||||
@@ -1,243 +0,0 @@
|
|||||||
import type { Logg } from '@guiiai/logg'
|
|
||||||
|
|
||||||
import type { SaliencyRuleBook } from './saliency-rules'
|
|
||||||
import type { RawPerceptionEvent } from './types/raw-events'
|
|
||||||
import type { PerceptionSignal } from './types/signals'
|
|
||||||
|
|
||||||
import { EVENT_KEYS, SALIENCY_RULES, WINDOW_SIZE } from './saliency-rules'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Circular buffer counter for a single event type
|
|
||||||
*/
|
|
||||||
interface WindowCounter {
|
|
||||||
/** Current head position in circular buffer */
|
|
||||||
head: number
|
|
||||||
/** Event counts per slot */
|
|
||||||
counts: number[]
|
|
||||||
/** Trigger markers per slot (1 = fired in this slot) */
|
|
||||||
triggers: number[]
|
|
||||||
/** Running total of counts in window */
|
|
||||||
total: number
|
|
||||||
/** Slot when last event was received */
|
|
||||||
lastEventSlot: number
|
|
||||||
/** Slot when threshold was last triggered */
|
|
||||||
lastFireSlot: number | null
|
|
||||||
/** Total count when last fired */
|
|
||||||
lastFireTotal: number
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Debug snapshot data for a single counter
|
|
||||||
*/
|
|
||||||
export interface CounterSnapshot {
|
|
||||||
key: string
|
|
||||||
total: number
|
|
||||||
window: number[]
|
|
||||||
triggers: number[]
|
|
||||||
lastFireSlot: number | null
|
|
||||||
lastFireTotal: number
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Debug snapshot for the entire saliency system
|
|
||||||
*/
|
|
||||||
export interface SaliencySnapshot {
|
|
||||||
slot: number
|
|
||||||
counters: CounterSnapshot[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export class SaliencyDetector {
|
|
||||||
/** Fixed set of counters - one per event key, never deleted */
|
|
||||||
private readonly counters: Map<string, WindowCounter> = new Map()
|
|
||||||
|
|
||||||
/** Current slot (advances every slotMs) */
|
|
||||||
private currentSlot = 0
|
|
||||||
|
|
||||||
/** Timer for advancing slots */
|
|
||||||
private timer: ReturnType<typeof setInterval> | null = null
|
|
||||||
|
|
||||||
/** Milliseconds per slot */
|
|
||||||
private readonly slotMs = 20
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private readonly deps: {
|
|
||||||
logger: Logg
|
|
||||||
onAttention: (signal: PerceptionSignal) => void
|
|
||||||
rules?: SaliencyRuleBook
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
// Initialize all counters at construction (fixed set)
|
|
||||||
for (const key of EVENT_KEYS) {
|
|
||||||
this.counters.set(key, this.createCounter())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public start(): void {
|
|
||||||
if (this.timer)
|
|
||||||
return
|
|
||||||
|
|
||||||
this.timer = setInterval(() => {
|
|
||||||
this.currentSlot += 1
|
|
||||||
this.advanceWindows()
|
|
||||||
}, this.slotMs)
|
|
||||||
}
|
|
||||||
|
|
||||||
public stop(): void {
|
|
||||||
if (!this.timer)
|
|
||||||
return
|
|
||||||
clearInterval(this.timer)
|
|
||||||
this.timer = null
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Process an incoming perception event
|
|
||||||
*/
|
|
||||||
public ingest(event: RawPerceptionEvent): void {
|
|
||||||
const rule = this.lookupRule(event)
|
|
||||||
if (!rule)
|
|
||||||
return
|
|
||||||
|
|
||||||
if (rule.predicate && !rule.predicate(event))
|
|
||||||
return
|
|
||||||
|
|
||||||
const counter = this.counters.get(rule.key)
|
|
||||||
if (!counter) {
|
|
||||||
// Unknown key - should not happen with fixed key set
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Increment count in current slot
|
|
||||||
counter.counts[counter.head] = (counter.counts[counter.head] ?? 0) + 1
|
|
||||||
counter.total += 1
|
|
||||||
counter.lastEventSlot = this.currentSlot
|
|
||||||
|
|
||||||
// Check threshold
|
|
||||||
if (counter.total >= rule.threshold) {
|
|
||||||
counter.lastFireSlot = this.currentSlot
|
|
||||||
counter.lastFireTotal = counter.total
|
|
||||||
counter.triggers[counter.head] = 1
|
|
||||||
this.resetCounter(counter)
|
|
||||||
this.emitSignal(rule.buildSignal(event))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get debug snapshot for visualization
|
|
||||||
* Returns all counters in fixed order (always same keys, same order)
|
|
||||||
*/
|
|
||||||
public getDebugSnapshot(): SaliencySnapshot {
|
|
||||||
const counters: CounterSnapshot[] = []
|
|
||||||
|
|
||||||
for (const key of EVENT_KEYS) {
|
|
||||||
const counter = this.counters.get(key)
|
|
||||||
if (!counter)
|
|
||||||
continue
|
|
||||||
|
|
||||||
counters.push({
|
|
||||||
key,
|
|
||||||
total: counter.total,
|
|
||||||
window: this.exportWindow(counter),
|
|
||||||
triggers: this.exportTriggers(counter),
|
|
||||||
lastFireSlot: counter.lastFireSlot,
|
|
||||||
lastFireTotal: counter.lastFireTotal,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
slot: this.currentSlot,
|
|
||||||
counters,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Advance all windows by one slot
|
|
||||||
* Never deletes counters - they persist with zeroed values
|
|
||||||
*/
|
|
||||||
private advanceWindows(): void {
|
|
||||||
for (const counter of this.counters.values()) {
|
|
||||||
// Move head forward in circular buffer
|
|
||||||
counter.head = (counter.head + 1) % WINDOW_SIZE
|
|
||||||
|
|
||||||
// Subtract expired slot from total
|
|
||||||
const expired = counter.counts[counter.head] ?? 0
|
|
||||||
if (expired > 0) {
|
|
||||||
counter.total = Math.max(0, counter.total - expired)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear the slot for new data
|
|
||||||
counter.counts[counter.head] = 0
|
|
||||||
counter.triggers[counter.head] = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new counter with zeroed circular buffer
|
|
||||||
*/
|
|
||||||
private createCounter(): WindowCounter {
|
|
||||||
return {
|
|
||||||
head: 0,
|
|
||||||
counts: new Array(WINDOW_SIZE).fill(0),
|
|
||||||
triggers: new Array(WINDOW_SIZE).fill(0),
|
|
||||||
total: 0,
|
|
||||||
lastEventSlot: 0,
|
|
||||||
lastFireSlot: null,
|
|
||||||
lastFireTotal: 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reset counter counts (after threshold triggered)
|
|
||||||
* Note: We only reset the counts, not triggers - triggers are visual markers
|
|
||||||
* that should persist until they naturally expire in the circular buffer
|
|
||||||
*/
|
|
||||||
private resetCounter(counter: WindowCounter): void {
|
|
||||||
counter.total = 0
|
|
||||||
counter.counts.fill(0)
|
|
||||||
// Don't reset triggers - they're historical markers for visualization
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Export window data in chronological order (oldest -> newest)
|
|
||||||
*/
|
|
||||||
private exportWindow(counter: WindowCounter): number[] {
|
|
||||||
const out = new Array<number>(WINDOW_SIZE)
|
|
||||||
for (let i = 0; i < WINDOW_SIZE; i++) {
|
|
||||||
const idx = (counter.head + 1 + i) % WINDOW_SIZE
|
|
||||||
out[i] = counter.counts[idx] ?? 0
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Export trigger markers in chronological order (oldest -> newest)
|
|
||||||
*/
|
|
||||||
private exportTriggers(counter: WindowCounter): number[] {
|
|
||||||
const out = new Array<number>(WINDOW_SIZE)
|
|
||||||
for (let i = 0; i < WINDOW_SIZE; i++) {
|
|
||||||
const idx = (counter.head + 1 + i) % WINDOW_SIZE
|
|
||||||
out[i] = counter.triggers[idx] ?? 0
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find matching rule for an event
|
|
||||||
*/
|
|
||||||
private lookupRule(event: RawPerceptionEvent) {
|
|
||||||
const rules = this.deps.rules ?? SALIENCY_RULES
|
|
||||||
return rules[event.modality]?.[event.kind]
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Emit a perception signal via callback
|
|
||||||
*/
|
|
||||||
private emitSignal(signal: PerceptionSignal): void {
|
|
||||||
// this.deps.logger.withFields({
|
|
||||||
// type: signal.type,
|
|
||||||
// desc: signal.description,
|
|
||||||
// meta: signal.metadata,
|
|
||||||
// }).log('SaliencyDetector: emit')
|
|
||||||
|
|
||||||
this.deps.onAttention(signal)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,211 +0,0 @@
|
|||||||
import type { RawPerceptionEvent } from './types/raw-events'
|
|
||||||
import type { PerceptionSignal } from './types/signals'
|
|
||||||
|
|
||||||
export interface SaliencyRule<E extends RawPerceptionEvent = RawPerceptionEvent> {
|
|
||||||
/**
|
|
||||||
* How many occurrences within the window are required before emitting.
|
|
||||||
*/
|
|
||||||
threshold: number
|
|
||||||
/**
|
|
||||||
* Optional predicate to gate the rule.
|
|
||||||
*/
|
|
||||||
predicate?: (event: E) => boolean
|
|
||||||
/**
|
|
||||||
* Counter key - should be one of the predefined EVENT_KEYS
|
|
||||||
*/
|
|
||||||
key: string
|
|
||||||
/**
|
|
||||||
* Builds the PerceptionSignal when the rule fires.
|
|
||||||
*/
|
|
||||||
buildSignal: (event: E) => PerceptionSignal
|
|
||||||
}
|
|
||||||
|
|
||||||
export type SaliencyRuleBook = Partial<Record<
|
|
||||||
RawPerceptionEvent['modality'],
|
|
||||||
Record<string, SaliencyRule>
|
|
||||||
>>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fixed event keys for stable heatmap visualization.
|
|
||||||
* All counters are initialized with these keys at startup.
|
|
||||||
*/
|
|
||||||
export const EVENT_KEYS = [
|
|
||||||
'punch:player',
|
|
||||||
'teabag:player',
|
|
||||||
'move:player',
|
|
||||||
'sound:ambient',
|
|
||||||
'felt:damage',
|
|
||||||
'felt:pickup',
|
|
||||||
'system:message',
|
|
||||||
] as const
|
|
||||||
|
|
||||||
export type EventKey = typeof EVENT_KEYS[number]
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unified window size for all event types (100 slots = ~2 seconds at 20ms/slot)
|
|
||||||
*/
|
|
||||||
export const WINDOW_SIZE = 100
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default threshold for attention triggering
|
|
||||||
*/
|
|
||||||
export const DEFAULT_THRESHOLD = 5
|
|
||||||
|
|
||||||
export const SALIENCY_RULES: SaliencyRuleBook = {
|
|
||||||
sighted: {
|
|
||||||
arm_swing: {
|
|
||||||
threshold: 5,
|
|
||||||
key: 'punch:player',
|
|
||||||
buildSignal: (event) => {
|
|
||||||
const e = event as Extract<RawPerceptionEvent, { modality: 'sighted', kind: 'arm_swing' }>
|
|
||||||
return {
|
|
||||||
type: 'entity_attention',
|
|
||||||
description: `Player ${e.displayName || 'unknown'} is punching nearby`,
|
|
||||||
sourceId: e.entityId,
|
|
||||||
confidence: 1.0,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
metadata: {
|
|
||||||
kind: 'player',
|
|
||||||
action: 'punch',
|
|
||||||
distance: e.distance,
|
|
||||||
hasLineOfSight: e.hasLineOfSight,
|
|
||||||
displayName: e.displayName,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
sneak_toggle: {
|
|
||||||
threshold: 5,
|
|
||||||
key: 'teabag:player',
|
|
||||||
buildSignal: (event) => {
|
|
||||||
const e = event as Extract<RawPerceptionEvent, { modality: 'sighted', kind: 'sneak_toggle' }>
|
|
||||||
return {
|
|
||||||
type: 'entity_attention',
|
|
||||||
description: `Player ${e.displayName || 'unknown'} is teabagging (rapid sneaking)`,
|
|
||||||
sourceId: e.entityId,
|
|
||||||
confidence: 1.0,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
metadata: {
|
|
||||||
kind: 'player',
|
|
||||||
action: 'teabag',
|
|
||||||
distance: e.distance,
|
|
||||||
hasLineOfSight: e.hasLineOfSight,
|
|
||||||
displayName: e.displayName,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
entity_moved: {
|
|
||||||
threshold: 5,
|
|
||||||
predicate: event => (event as Extract<RawPerceptionEvent, { modality: 'sighted', kind: 'entity_moved' }>).entityType === 'player',
|
|
||||||
key: 'move:player',
|
|
||||||
buildSignal: (event) => {
|
|
||||||
const e = event as Extract<RawPerceptionEvent, { modality: 'sighted', kind: 'entity_moved' }>
|
|
||||||
return {
|
|
||||||
type: 'entity_attention',
|
|
||||||
description: `Player ${e.displayName || 'unknown'} is moving nearby`,
|
|
||||||
sourceId: e.entityId,
|
|
||||||
confidence: 0.8,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
metadata: {
|
|
||||||
kind: 'player',
|
|
||||||
action: 'move',
|
|
||||||
distance: e.distance,
|
|
||||||
hasLineOfSight: e.hasLineOfSight,
|
|
||||||
displayName: e.displayName,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
heard: {
|
|
||||||
sound: {
|
|
||||||
threshold: 5,
|
|
||||||
key: 'sound:ambient',
|
|
||||||
buildSignal: (event) => {
|
|
||||||
const e = event as Extract<RawPerceptionEvent, { modality: 'heard', kind: 'sound' }>
|
|
||||||
return {
|
|
||||||
type: 'environmental_anomaly',
|
|
||||||
description: `Heard sound: ${e.soundId}`,
|
|
||||||
sourceId: e.soundId,
|
|
||||||
confidence: 1.0,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
metadata: {
|
|
||||||
kind: 'sound',
|
|
||||||
action: 'sound',
|
|
||||||
soundId: e.soundId,
|
|
||||||
distance: e.distance,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
felt: {
|
|
||||||
damage_taken: {
|
|
||||||
threshold: 1, // Damage is immediately salient
|
|
||||||
key: 'felt:damage',
|
|
||||||
buildSignal: (event) => {
|
|
||||||
const e = event as Extract<RawPerceptionEvent, { modality: 'felt', kind: 'damage_taken' }>
|
|
||||||
const ds = (e as any).damageSource as any
|
|
||||||
const cause = typeof ds?.cause === 'string' ? ds.cause : 'unknown'
|
|
||||||
const name = typeof ds?.name === 'string' && ds.name.length > 0 ? ds.name : undefined
|
|
||||||
const distance = typeof ds?.distance === 'number' ? ds.distance : undefined
|
|
||||||
const amount = typeof (e as any).amount === 'number' ? (e as any).amount : undefined
|
|
||||||
|
|
||||||
const details = [
|
|
||||||
amount !== undefined ? `amount=${amount}` : null,
|
|
||||||
`cause=${cause}`,
|
|
||||||
name ? `name=${name}` : null,
|
|
||||||
distance !== undefined ? `distance=${distance.toFixed(1)}` : null,
|
|
||||||
].filter(Boolean).join(', ')
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: 'saliency_high',
|
|
||||||
description: `Taken damage (${details}).`,
|
|
||||||
confidence: 1.0,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
metadata: {
|
|
||||||
kind: 'felt',
|
|
||||||
action: 'damage',
|
|
||||||
amount: e.amount,
|
|
||||||
damageSource: e.damageSource,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
item_collected: {
|
|
||||||
threshold: 3,
|
|
||||||
key: 'felt:pickup',
|
|
||||||
buildSignal: () => ({
|
|
||||||
type: 'entity_attention',
|
|
||||||
description: 'Picked up an item',
|
|
||||||
confidence: 1.0,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
metadata: {
|
|
||||||
kind: 'felt',
|
|
||||||
action: 'pickup',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
system: {
|
|
||||||
system_message: {
|
|
||||||
threshold: 1, // System messages are immediately salient
|
|
||||||
key: 'system:message',
|
|
||||||
buildSignal: (event) => {
|
|
||||||
const e = event as Extract<RawPerceptionEvent, { modality: 'system', kind: 'system_message' }>
|
|
||||||
return {
|
|
||||||
type: 'system_message',
|
|
||||||
description: e.message,
|
|
||||||
sourceId: 'system',
|
|
||||||
confidence: 1.0,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
metadata: {
|
|
||||||
message: e.message,
|
|
||||||
position: e.position,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from './types/stage'
|
|
||||||
@@ -1,65 +1,3 @@
|
|||||||
import type { Vec3 } from 'vec3'
|
|
||||||
|
|
||||||
export type PerceptionModality = 'sighted' | 'heard' | 'felt' | 'system'
|
|
||||||
|
|
||||||
export interface RawPerceptionEventBase {
|
|
||||||
modality: PerceptionModality
|
|
||||||
timestamp: number
|
|
||||||
source: 'minecraft'
|
|
||||||
pos?: Vec3
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SightedEntityMovedEvent extends RawPerceptionEventBase {
|
|
||||||
modality: 'sighted'
|
|
||||||
kind: 'entity_moved'
|
|
||||||
entityType: 'player' | 'mob'
|
|
||||||
entityId: string
|
|
||||||
displayName?: string
|
|
||||||
distance: number
|
|
||||||
hasLineOfSight: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SightedArmSwingEvent extends RawPerceptionEventBase {
|
|
||||||
modality: 'sighted'
|
|
||||||
kind: 'arm_swing'
|
|
||||||
entityType: 'player'
|
|
||||||
entityId: string
|
|
||||||
displayName?: string
|
|
||||||
distance: number
|
|
||||||
hasLineOfSight: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SightedSneakToggleEvent extends RawPerceptionEventBase {
|
|
||||||
modality: 'sighted'
|
|
||||||
kind: 'sneak_toggle'
|
|
||||||
entityType: 'player'
|
|
||||||
entityId: string
|
|
||||||
displayName?: string
|
|
||||||
distance: number
|
|
||||||
hasLineOfSight: boolean
|
|
||||||
sneaking: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export type SightedEvent = SightedEntityMovedEvent | SightedArmSwingEvent | SightedSneakToggleEvent
|
|
||||||
|
|
||||||
export interface HeardSoundEvent extends RawPerceptionEventBase {
|
|
||||||
modality: 'heard'
|
|
||||||
kind: 'sound'
|
|
||||||
soundId: string
|
|
||||||
distance: number
|
|
||||||
inferredEntityType?: 'player' | 'mob'
|
|
||||||
inferredEntityId?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type HeardEvent = HeardSoundEvent
|
|
||||||
|
|
||||||
export interface FeltDamageTakenEvent extends RawPerceptionEventBase {
|
|
||||||
modality: 'felt'
|
|
||||||
kind: 'damage_taken'
|
|
||||||
amount?: number
|
|
||||||
damageSource?: DamageSourceMetadata
|
|
||||||
}
|
|
||||||
|
|
||||||
export type DamageSourceCause
|
export type DamageSourceCause
|
||||||
= | 'player'
|
= | 'player'
|
||||||
| 'mob'
|
| 'mob'
|
||||||
@@ -79,30 +17,3 @@ export interface DamageSourceMetadata {
|
|||||||
entityId?: string
|
entityId?: string
|
||||||
distance?: number
|
distance?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FeltItemCollectedEvent extends RawPerceptionEventBase {
|
|
||||||
modality: 'felt'
|
|
||||||
kind: 'item_collected'
|
|
||||||
itemName: string
|
|
||||||
count?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export type FeltEvent = FeltDamageTakenEvent | FeltItemCollectedEvent
|
|
||||||
|
|
||||||
export interface PlayerJoinedEvent extends RawPerceptionEventBase {
|
|
||||||
modality: 'system'
|
|
||||||
kind: 'player_joined'
|
|
||||||
playerId: string
|
|
||||||
displayName?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SystemMessageEvent extends RawPerceptionEventBase {
|
|
||||||
modality: 'system'
|
|
||||||
kind: 'system_message'
|
|
||||||
message: string
|
|
||||||
position: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type SystemEvent = PlayerJoinedEvent | SystemMessageEvent
|
|
||||||
|
|
||||||
export type RawPerceptionEvent = SightedEvent | HeardEvent | FeltEvent | SystemEvent
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
export type PerceptionSignalType
|
export type PerceptionSignalType
|
||||||
= | 'chat_message'
|
= | 'chat_message'
|
||||||
| 'entity_attention' // e.g. someone waving, teabagging
|
| 'entity_attention' // e.g. someone waving, punching
|
||||||
| 'environmental_anomaly' // e.g. sudden loud sound
|
| 'environmental_anomaly' // e.g. sudden loud sound
|
||||||
| 'saliency_high' // generic high saliency event
|
| 'saliency_high' // generic high-priority event (e.g. damage)
|
||||||
| 'social_gesture' // e.g. teabagging, waving
|
| 'social_gesture' // e.g. waving
|
||||||
| 'social_presence'
|
| 'social_presence'
|
||||||
| 'system_message' // e.g. death messages, join/leave
|
| 'system_message' // e.g. death messages, join/leave
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
import type { PerceptionFrame } from '../frame'
|
|
||||||
|
|
||||||
export interface PerceptionStage {
|
|
||||||
name: string
|
|
||||||
tick?: (deltaMs: number) => void
|
|
||||||
handle: (frame: PerceptionFrame) => PerceptionFrame | null
|
|
||||||
}
|
|
||||||
@@ -9,7 +9,6 @@ import type {
|
|||||||
ReflexStateEvent,
|
ReflexStateEvent,
|
||||||
ReplExecutionResultEvent,
|
ReplExecutionResultEvent,
|
||||||
ReplStateEvent,
|
ReplStateEvent,
|
||||||
SaliencyEvent,
|
|
||||||
ServerEvent,
|
ServerEvent,
|
||||||
TraceEvent,
|
TraceEvent,
|
||||||
} from './types'
|
} from './types'
|
||||||
@@ -116,24 +115,6 @@ export class DebugService {
|
|||||||
this.server.broadcast(event)
|
this.server.broadcast(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Emit a saliency snapshot
|
|
||||||
*/
|
|
||||||
public emitSaliency(
|
|
||||||
slot: number,
|
|
||||||
counters: SaliencyEvent['counters'],
|
|
||||||
): void {
|
|
||||||
const event: ServerEvent = {
|
|
||||||
type: 'saliency',
|
|
||||||
payload: {
|
|
||||||
slot,
|
|
||||||
counters,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
this.server.broadcast(event)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emit a single trace event from the EventBus
|
* Emit a single trace event from the EventBus
|
||||||
*/
|
*/
|
||||||
@@ -212,19 +193,6 @@ export class DebugService {
|
|||||||
case 'queue':
|
case 'queue':
|
||||||
this.server.broadcast({ type: 'queue', payload: payload as QueueEvent })
|
this.server.broadcast({ type: 'queue', payload: payload as QueueEvent })
|
||||||
break
|
break
|
||||||
case 'saliency': {
|
|
||||||
// Transform the saliency data to match expected format
|
|
||||||
const data = payload as { slot?: number, counters?: SaliencyEvent['counters'] }
|
|
||||||
this.server.broadcast({
|
|
||||||
type: 'saliency',
|
|
||||||
payload: {
|
|
||||||
slot: data.slot || 0,
|
|
||||||
counters: data.counters || [],
|
|
||||||
timestamp: Date.now(),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'reflex':
|
case 'reflex':
|
||||||
this.emitReflexState(payload as Omit<ReflexStateEvent, 'timestamp'>)
|
this.emitReflexState(payload as Omit<ReflexStateEvent, 'timestamp'>)
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -8,6 +8,5 @@ export type {
|
|||||||
LLMTraceEvent,
|
LLMTraceEvent,
|
||||||
LogEvent,
|
LogEvent,
|
||||||
QueueEvent,
|
QueueEvent,
|
||||||
SaliencyEvent,
|
|
||||||
ServerEvent,
|
ServerEvent,
|
||||||
} from './types'
|
} from './types'
|
||||||
|
|||||||
@@ -55,19 +55,6 @@ export interface QueueEvent {
|
|||||||
timestamp: number
|
timestamp: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SaliencyEvent {
|
|
||||||
slot: number
|
|
||||||
counters: Array<{
|
|
||||||
key: string
|
|
||||||
total: number
|
|
||||||
window: number[]
|
|
||||||
triggers: number[]
|
|
||||||
lastFireSlot: number | null
|
|
||||||
lastFireTotal: number
|
|
||||||
}>
|
|
||||||
timestamp: number
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reflex system state update
|
* Reflex system state update
|
||||||
*/
|
*/
|
||||||
@@ -189,7 +176,6 @@ export type ServerEvent
|
|||||||
| { type: 'llm', payload: LLMTraceEvent }
|
| { type: 'llm', payload: LLMTraceEvent }
|
||||||
| { type: 'blackboard', payload: BlackboardEvent }
|
| { type: 'blackboard', payload: BlackboardEvent }
|
||||||
| { type: 'queue', payload: QueueEvent }
|
| { type: 'queue', payload: QueueEvent }
|
||||||
| { type: 'saliency', payload: SaliencyEvent }
|
|
||||||
| { type: 'reflex', payload: ReflexStateEvent }
|
| { type: 'reflex', payload: ReflexStateEvent }
|
||||||
| { type: 'trace', payload: TraceEvent }
|
| { type: 'trace', payload: TraceEvent }
|
||||||
| { type: 'trace_batch', payload: TraceBatchEvent }
|
| { type: 'trace_batch', payload: TraceBatchEvent }
|
||||||
|
|||||||
@@ -731,107 +731,6 @@ class ConversationPanel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// Saliency Panel
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
class SaliencyPanel {
|
|
||||||
constructor(client) {
|
|
||||||
this.client = client
|
|
||||||
this.data = null
|
|
||||||
this.elements = {
|
|
||||||
canvas: document.getElementById('saliency-canvas'),
|
|
||||||
labels: document.getElementById('saliency-labels'),
|
|
||||||
slot: document.getElementById('saliency-slot'),
|
|
||||||
}
|
|
||||||
this.ctx = this.elements.canvas.getContext('2d')
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
|
||||||
this.client.on('saliency', data => this.update(data))
|
|
||||||
this.client.on('connected', () => this.reset())
|
|
||||||
}
|
|
||||||
|
|
||||||
update(data) {
|
|
||||||
this.data = data
|
|
||||||
this.render()
|
|
||||||
}
|
|
||||||
|
|
||||||
reset() {
|
|
||||||
this.data = null
|
|
||||||
this.render()
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
if (!this.data || !this.data.counters) {
|
|
||||||
this.elements.labels.innerHTML = '<div class="empty-state">No data</div>'
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this.elements.slot.textContent = this.data.slot || 0
|
|
||||||
|
|
||||||
// Use all counters directly from server (fixed order)
|
|
||||||
const counters = this.data.counters
|
|
||||||
|
|
||||||
// Render labels
|
|
||||||
this.elements.labels.innerHTML = counters.map(c => `
|
|
||||||
<div class="saliency-label">
|
|
||||||
<span class="saliency-key">${escapeHtml(c.key || '')}</span>
|
|
||||||
<span class="saliency-total">(${c.total || 0})</span>
|
|
||||||
</div>
|
|
||||||
`).join('')
|
|
||||||
|
|
||||||
// Render canvas
|
|
||||||
const cols = 100
|
|
||||||
const rows = counters.length
|
|
||||||
const cellW = 6
|
|
||||||
const cellH = 16
|
|
||||||
|
|
||||||
this.elements.canvas.width = cols * cellW
|
|
||||||
this.elements.canvas.height = Math.max(1, rows) * cellH
|
|
||||||
|
|
||||||
this.ctx.fillStyle = '#0d1117'
|
|
||||||
this.ctx.fillRect(0, 0, this.elements.canvas.width, this.elements.canvas.height)
|
|
||||||
|
|
||||||
let maxCell = 0
|
|
||||||
for (const counter of counters) {
|
|
||||||
for (let i = 0; i < Math.min(cols, counter.window?.length || 0); i++) {
|
|
||||||
maxCell = Math.max(maxCell, counter.window[i] || 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let y = 0; y < rows; y++) {
|
|
||||||
const counter = counters[y]
|
|
||||||
const w = counter.window || []
|
|
||||||
const t = counter.triggers || []
|
|
||||||
|
|
||||||
for (let x = 0; x < cols; x++) {
|
|
||||||
const v = x < w.length ? (w[x] || 0) : 0
|
|
||||||
const fired = x < t.length ? (t[x] || 0) : 0
|
|
||||||
|
|
||||||
this.ctx.fillStyle = fired
|
|
||||||
? 'rgba(248, 81, 73, 0.9)'
|
|
||||||
: this.colorFor(v, maxCell)
|
|
||||||
this.ctx.fillRect(x * cellW, y * cellH, cellW - 1, cellH - 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
colorFor(value, maxValue) {
|
|
||||||
// Always show a subtle color for empty cells, never fully transparent
|
|
||||||
if (value === 0) {
|
|
||||||
return 'rgba(30, 38, 50, 1)' // Dark background for empty cells
|
|
||||||
}
|
|
||||||
// Normalize value when we have data
|
|
||||||
const max = maxValue || 1
|
|
||||||
const t = Math.min(1, value / max)
|
|
||||||
const r = Math.round(88 + 80 * t)
|
|
||||||
const g = Math.round(166 + 80 * t)
|
|
||||||
const b = Math.round(255 * t)
|
|
||||||
return `rgba(${r},${g},${b},${0.3 + 0.7 * t})`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Tools Panel
|
// Tools Panel
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
@@ -1491,7 +1390,6 @@ class LayoutManager {
|
|||||||
{ id: 'splitter-v1', var: '--col-left', type: 'v' },
|
{ id: 'splitter-v1', var: '--col-left', type: 'v' },
|
||||||
{ id: 'splitter-h1', var: '--row-1', type: 'h' },
|
{ id: 'splitter-h1', var: '--row-1', type: 'h' },
|
||||||
{ id: 'splitter-h2', var: '--row-2', type: 'h' },
|
{ id: 'splitter-h2', var: '--row-2', type: 'h' },
|
||||||
{ id: 'splitter-h3', var: '--row-3', type: 'h' },
|
|
||||||
]
|
]
|
||||||
|
|
||||||
splitters.forEach((config) => {
|
splitters.forEach((config) => {
|
||||||
@@ -1578,7 +1476,6 @@ class DebugApp {
|
|||||||
this.brainPanel = new BrainPanel(this.client)
|
this.brainPanel = new BrainPanel(this.client)
|
||||||
this.logsPanel = new LogsPanel(this.client)
|
this.logsPanel = new LogsPanel(this.client)
|
||||||
this.conversationPanel = new ConversationPanel(this.client)
|
this.conversationPanel = new ConversationPanel(this.client)
|
||||||
this.saliencyPanel = new SaliencyPanel(this.client)
|
|
||||||
this.timelinePanel = new TimelinePanel(this.client)
|
this.timelinePanel = new TimelinePanel(this.client)
|
||||||
this.toolsPanel = new ToolsPanel(this.client)
|
this.toolsPanel = new ToolsPanel(this.client)
|
||||||
this.replPanel = new ReplPanel(this.client)
|
this.replPanel = new ReplPanel(this.client)
|
||||||
@@ -1589,7 +1486,6 @@ class DebugApp {
|
|||||||
brain: this.brainPanel,
|
brain: this.brainPanel,
|
||||||
logs: this.logsPanel,
|
logs: this.logsPanel,
|
||||||
conversation: this.conversationPanel,
|
conversation: this.conversationPanel,
|
||||||
saliency: this.saliencyPanel,
|
|
||||||
timeline: this.timelinePanel,
|
timeline: this.timelinePanel,
|
||||||
tools: this.toolsPanel,
|
tools: this.toolsPanel,
|
||||||
repl: this.replPanel,
|
repl: this.replPanel,
|
||||||
|
|||||||
@@ -207,25 +207,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Splitter H3 (LLM vs Saliency) -->
|
|
||||||
<div class="splitter-h" id="splitter-h3"></div>
|
|
||||||
|
|
||||||
<!-- Bottom Row: Saliency -->
|
|
||||||
<section id="saliency-section" class="panel">
|
|
||||||
<div class="panel-header">
|
|
||||||
<h2>Saliency Heatmap</h2>
|
|
||||||
<div class="panel-controls">
|
|
||||||
<span>Slot: <span id="saliency-slot" class="panel-badge">0</span></span>
|
|
||||||
<button class="icon-btn maximize-btn" title="Toggle maximize">⤢</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="panel-content">
|
|
||||||
<div class="saliency-container">
|
|
||||||
<canvas id="saliency-canvas"></canvas>
|
|
||||||
<div id="saliency-labels" class="saliency-labels"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -182,9 +182,9 @@ button:hover,
|
|||||||
display: grid;
|
display: grid;
|
||||||
/* Columns: Left Panel | Splitter | Right Content */
|
/* Columns: Left Panel | Splitter | Right Content */
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
/* Rows: Logs | Splitter | Timeline | Splitter | LLM | Splitter | Saliency */
|
/* Rows: Logs | Splitter | Timeline | Splitter | Conversation */
|
||||||
grid-template-rows:
|
grid-template-rows:
|
||||||
minmax(var(--row-1), 1fr) 8px minmax(var(--row-2), 1fr) 8px minmax(var(--row-3), 1fr) 8px 1fr;
|
minmax(var(--row-1), 1fr) 8px minmax(var(--row-2), 1fr) 8px minmax(var(--row-3), 1fr);
|
||||||
gap: 0;
|
gap: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -455,17 +455,6 @@ button:hover,
|
|||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#splitter-h3 {
|
|
||||||
grid-column: 3 / 4;
|
|
||||||
grid-row: 6 / 7;
|
|
||||||
}
|
|
||||||
|
|
||||||
#saliency-section {
|
|
||||||
grid-column: 3 / 4;
|
|
||||||
grid-row: 7 / 8;
|
|
||||||
min-height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Panel Styles */
|
/* Panel Styles */
|
||||||
.panel {
|
.panel {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -834,42 +823,6 @@ button:hover,
|
|||||||
40% { transform: translateY(-3px); opacity: 0.9; }
|
40% { transform: translateY(-3px); opacity: 0.9; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Saliency */
|
|
||||||
.saliency-container {
|
|
||||||
display: flex;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#saliency-canvas {
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
background-color: var(--bg-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.saliency-labels {
|
|
||||||
flex: 0 0 300px;
|
|
||||||
font-family: var(--font-mono);
|
|
||||||
font-size: 11px;
|
|
||||||
line-height: 16px;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.saliency-label {
|
|
||||||
height: 16px;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
border-bottom: 1px solid var(--bg-tertiary);
|
|
||||||
color: var(--text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.saliency-key {
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.saliency-total {
|
|
||||||
color: var(--text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Utilities */
|
/* Utilities */
|
||||||
.empty-state {
|
.empty-state {
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
|
|||||||
Reference in New Issue
Block a user