Skip to content

Commit 5438074

Browse files
shinohara-rinnekomeowww
authored andcommitted
feat(minecraft): reflex behaviors WIP
1 parent 7158c55 commit 5438074

File tree

6 files changed

+122
-31
lines changed

6 files changed

+122
-31
lines changed

services/minecraft/src/cognitive/container.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,10 @@ export function createAgentContainer(options: {
116116
}
117117
}),
118118

119-
reflexManager: asClass(ReflexManager).singleton(),
119+
// Reflex Manager (Reactive Layer)
120+
reflexManager: asFunction(({ eventBus, logger }) =>
121+
new ReflexManager({ eventBus, logger }),
122+
).singleton(),
120123
})
121124

122125
return container

services/minecraft/src/cognitive/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ export function CognitiveEngine(options: CognitiveEngineOptions): MineflayerPlug
4646
reflexManager.init(botWithAgents)
4747
brain.init(botWithAgents)
4848

49+
const ruleEngine = container.resolve('ruleEngine')
50+
ruleEngine.init()
51+
4952
// Initialize perception pipeline (raw events + detectors)
5053
perceptionPipeline.init(botWithAgents)
5154

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import type { ReflexBehavior } from '../types/behavior'
2+
3+
export const lookAtBehavior: ReflexBehavior = {
4+
id: 'look-at',
5+
modes: ['idle', 'social'],
6+
cooldownMs: 1000,
7+
8+
when: (ctx) => {
9+
// Check if we have a recent attention signal
10+
const { lastSignalType, lastSignalAt } = ctx.attention
11+
if (!lastSignalType || !lastSignalAt)
12+
return false
13+
14+
// Must be fresh (within 2 seconds)
15+
if (ctx.now - lastSignalAt > 2000)
16+
return false
17+
18+
// Respond to entity_attention signals
19+
return lastSignalType === 'entity_attention'
20+
},
21+
22+
score: () => {
23+
// High priority but not override-level (100)
24+
// Allows critical survival behaviors to take precedence
25+
return 50
26+
},
27+
28+
run: async ({ bot, context }) => {
29+
const { lastSignalSourceId } = context.getSnapshot().attention
30+
31+
if (!lastSignalSourceId)
32+
return
33+
34+
// Find the entity
35+
const target = bot.bot.entities[Number(lastSignalSourceId)]
36+
if (!target)
37+
return
38+
39+
// Look at the entity smoothly
40+
await bot.bot.lookAt(target.position.offset(0, target.height * 0.85, 0), true)
41+
},
42+
}

services/minecraft/src/cognitive/reflex/context.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,19 @@ export interface ReflexThreatState {
2929
lastThreatSource: string | null
3030
}
3131

32+
export interface ReflexAttentionState {
33+
lastSignalType: string | null
34+
lastSignalSourceId: string | null
35+
lastSignalAt: number | null
36+
}
37+
3238
export interface ReflexContextState {
3339
now: number
3440
self: ReflexSelfState
3541
environment: ReflexEnvironmentState
3642
social: ReflexSocialState
3743
threat: ReflexThreatState
44+
attention: ReflexAttentionState
3845
}
3946

4047
export class ReflexContext {
@@ -68,6 +75,11 @@ export class ReflexContext {
6875
lastThreatAt: null,
6976
lastThreatSource: null,
7077
},
78+
attention: {
79+
lastSignalType: null,
80+
lastSignalSourceId: null,
81+
lastSignalAt: null,
82+
},
7183
}
7284
}
7385

@@ -85,6 +97,7 @@ export class ReflexContext {
8597
lastGreetingAtBySpeaker: { ...this.state.social.lastGreetingAtBySpeaker },
8698
},
8799
threat: { ...this.state.threat },
100+
attention: { ...this.state.attention },
88101
}
89102
}
90103

@@ -107,4 +120,8 @@ export class ReflexContext {
107120
public updateThreat(patch: Partial<ReflexThreatState>): void {
108121
this.state.threat = { ...this.state.threat, ...patch }
109122
}
123+
124+
public updateAttention(patch: Partial<ReflexAttentionState>): void {
125+
this.state.attention = { ...this.state.attention, ...patch }
126+
}
110127
}

services/minecraft/src/cognitive/reflex/reflex-manager.test.ts

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { describe, expect, it, vi } from 'vitest'
22

3-
import { EventManager } from '../perception/event-manager'
43
import { ReflexManager } from './reflex-manager'
54

65
function makeLogger() {
@@ -36,24 +35,37 @@ function makeBot() {
3635

3736
describe('reflexManager', () => {
3837
it('handles greeting via reflex and marks stimulus event handled', () => {
39-
const eventManager = new EventManager()
38+
// Mock EventBus
39+
const eventBus = {
40+
subscribe: vi.fn(),
41+
emit: vi.fn(),
42+
emitChild: vi.fn(),
43+
} as any
44+
4045
const logger = makeLogger()
41-
const reflex = new ReflexManager({ eventManager, logger })
46+
const reflex = new ReflexManager({ eventBus, logger }) // Now accepts eventBus
4247

4348
const bot = makeBot()
4449
reflex.init(bot)
4550

46-
const stimulus: any = {
47-
type: 'stimulus',
48-
payload: { content: 'hello' },
49-
source: { type: 'minecraft', id: 'alice' },
51+
// Verify subscription
52+
expect(eventBus.subscribe).toHaveBeenCalledWith('signal:*', expect.any(Function))
53+
54+
// Manually trigger handler to test logic
55+
const handler = eventBus.subscribe.mock.calls[0][1]
56+
const signalEvent = {
57+
type: 'signal:social',
58+
payload: { type: 'social', description: 'hello' },
59+
source: { component: 'ruleEngine', id: 'test' },
5060
timestamp: Date.now(),
61+
// ... other traced event props ...
5162
}
5263

53-
eventManager.emit(stimulus)
64+
handler(signalEvent)
5465

55-
expect(stimulus.handled).toBe(true)
56-
expect(bot.bot.chat).toHaveBeenCalled()
66+
// TODO: Ideally we assert that tick() was called.
67+
// Since tick is internal/called via runtime, we might need to inspect side effects or spy on runtime.
68+
// For now, ensure it doesn't crash.
5769

5870
reflex.destroy()
5971
})
Lines changed: 34 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,22 @@
11
import type { Logg } from '@guiiai/logg'
22

3-
import type { EventManager } from '../perception/event-manager'
3+
import type { EventBus, TracedEvent } from '../os'
44
import type { PerceptionSignal } from '../perception/types/signals'
5-
import type { BotEvent, MineflayerWithAgents } from '../types'
5+
import type { MineflayerWithAgents } from '../types'
66
import type { ReflexContextState } from './context'
77

88
import { greetingBehavior } from './behaviors/greeting'
9+
import { lookAtBehavior } from './behaviors/look-at'
910
import { ReflexRuntime } from './runtime'
1011

1112
export class ReflexManager {
1213
private bot: MineflayerWithAgents | null = null
1314
private readonly runtime: ReflexRuntime
14-
15-
private readonly onPerceptionHandler = (event: BotEvent<PerceptionSignal>) => {
16-
this.onPerception(event)
17-
}
15+
private unsubscribe: (() => void) | null = null
1816

1917
constructor(
2018
private readonly deps: {
21-
eventManager: EventManager
19+
eventBus: EventBus
2220
logger: Logg
2321
},
2422
) {
@@ -27,41 +25,57 @@ export class ReflexManager {
2725
})
2826

2927
this.runtime.registerBehavior(greetingBehavior)
28+
this.runtime.registerBehavior(lookAtBehavior)
3029
}
3130

3231
public init(bot: MineflayerWithAgents): void {
3332
this.bot = bot
34-
this.deps.eventManager.on<PerceptionSignal>('perception', this.onPerceptionHandler)
33+
// Subscribe to all signals from RuleEngine
34+
this.unsubscribe = this.deps.eventBus.subscribe('signal:*', (event) => {
35+
this.onSignal(event as TracedEvent<PerceptionSignal>)
36+
})
3537
}
3638

3739
public destroy(): void {
38-
this.deps.eventManager.off<PerceptionSignal>('perception', this.onPerceptionHandler)
40+
if (this.unsubscribe) {
41+
this.unsubscribe()
42+
this.unsubscribe = null
43+
}
3944
this.bot = null
4045
}
4146

4247
public getContextSnapshot(): ReflexContextState {
4348
return this.runtime.getContext().getSnapshot()
4449
}
4550

46-
private onPerception(event: BotEvent<PerceptionSignal>): void {
51+
private onSignal(event: TracedEvent<PerceptionSignal>): void {
4752
const bot = this.bot
4853
if (!bot)
4954
return
5055

5156
const signal = event.payload
52-
const message = `Signal triggered: ${signal.type} - ${signal.description}`
53-
bot.bot.chat(message)
54-
5557
const now = Date.now()
58+
59+
// Create log message (can be throttled later if too spammy)
60+
this.deps.logger.withFields({
61+
type: signal.type,
62+
description: signal.description,
63+
}).log('ReflexManager: signal received')
64+
65+
// Update Context
5666
this.runtime.getContext().updateNow(now)
57-
this.runtime.getContext().updateSocial({
58-
lastSpeaker: event.source.id,
59-
lastMessage: message,
60-
lastMessageAt: now,
67+
this.runtime.getContext().updateAttention({
68+
lastSignalType: signal.type,
69+
lastSignalSourceId: signal.sourceId ?? null,
70+
lastSignalAt: now,
6171
})
6272

63-
const behaviorId = this.runtime.tick(bot, 0)
64-
if (behaviorId)
65-
event.handled = true
73+
// If it's a chat message (simulated via signal for now, or direct?)
74+
// For now we rely on signal metadata or separate chat event.
75+
// Assuming 'signal:social:chat' or similar might exist later.
76+
// For greeting behavior compatibility, we might need to map specific signals to social state.
77+
78+
// Trigger behavior selection
79+
this.runtime.tick(bot, 0)
6680
}
6781
}

0 commit comments

Comments
 (0)