chore(minecraft): update prompt, remove redundant chatlog

This commit is contained in:
Rin
2026-02-18 11:12:03 +08:00
committed by Neko Ayaka
parent 8beac18ced
commit b9c2569743
5 changed files with 161 additions and 36 deletions
@@ -0,0 +1,57 @@
import { beforeEach, describe, expect, it } from 'vitest'
import { Blackboard } from './blackboard'
describe('blackboard', () => {
let blackboard: Blackboard
beforeEach(() => {
blackboard = new Blackboard()
})
it('should initialize with empty chat history', () => {
expect(blackboard.chatHistory).toEqual([])
})
it('should add a chat message', () => {
const msg = { sender: 'User', content: 'Hello', timestamp: Date.now() }
blackboard.addChatMessage(msg)
expect(blackboard.chatHistory).toHaveLength(1)
expect(blackboard.chatHistory[0]).toEqual(msg)
})
it('should limit chat history to 8 messages', () => {
for (let i = 0; i < 10; i++) {
blackboard.addChatMessage({
sender: 'User',
content: `Message ${i}`,
timestamp: Date.now() + i,
})
}
expect(blackboard.chatHistory).toHaveLength(8)
expect(blackboard.chatHistory[0].content).toBe('Message 2') // First 2 should be dropped
expect(blackboard.chatHistory[7].content).toBe('Message 9')
})
it('should provide a snapshot with chat history', () => {
const msg = { sender: 'User', content: 'Test', timestamp: 123 }
blackboard.addChatMessage(msg)
const snapshot = blackboard.getSnapshot()
expect(snapshot.chatHistory).toEqual([msg])
// Ensure snapshot is immutable regarding the internal state reference if implemented that way,
// or at least checking it exists.
// In implementation we did [...this._state.chatHistory] so it should be a copy.
snapshot.chatHistory.push({ sender: 'Evil', content: 'Modification', timestamp: 0 })
expect(blackboard.chatHistory).toHaveLength(1)
})
it('should initialize with default username', () => {
expect(blackboard.selfUsername).toBe('Bot')
})
it('should update selfUsername via update method', () => {
blackboard.update({ selfUsername: 'Airi' })
expect(blackboard.selfUsername).toBe('Airi')
})
})
@@ -1,50 +1,72 @@
export interface ContextViewState {
export interface contextViewState {
selfSummary: string
environmentSummary: string
}
export interface ChatMessage {
sender: string
content: string
timestamp: number
}
export interface BlackboardState {
currentGoal: string
currentThought: string
executionStrategy: string
contextView: ContextViewState
ultimateGoal: string
currentTask: string
strategy: string
contextView: contextViewState
chatHistory: ChatMessage[]
selfUsername: string
}
export class Blackboard {
private _state: BlackboardState
private static readonly MAX_CHAT_HISTORY = 8
constructor() {
this._state = {
currentGoal: 'Idle',
currentThought: 'I am waiting for something to happen.',
executionStrategy: 'Observe surroundings.',
ultimateGoal: 'nothing',
currentTask: 'I am waiting for something to happen.',
strategy: 'idle',
contextView: {
selfSummary: 'Unknown',
environmentSummary: 'Unknown',
},
chatHistory: [],
selfUsername: 'Bot',
}
}
// Getters
public get goal(): string { return this._state.currentGoal }
public get thought(): string { return this._state.currentThought }
public get strategy(): string { return this._state.executionStrategy }
public get ultimate_goal(): string { return this._state.ultimateGoal }
public get current_task(): string { return this._state.currentTask }
public get strategy(): string { return this._state.strategy }
public get selfSummary(): string { return this._state.contextView.selfSummary }
public get environmentSummary(): string { return this._state.contextView.environmentSummary }
public get chatHistory(): ChatMessage[] { return this._state.chatHistory }
public get selfUsername(): string { return this._state.selfUsername }
// Setters (Partial updates allowed)
public update(updates: Partial<BlackboardState>): void {
this._state = { ...this._state, ...updates }
}
public updateContextView(updates: Partial<ContextViewState>): void {
public updateContextView(updates: Partial<contextViewState>): void {
this._state.contextView = { ...this._state.contextView, ...updates }
}
public addChatMessage(message: ChatMessage): void {
const newHistory = [...this._state.chatHistory, message]
if (newHistory.length > Blackboard.MAX_CHAT_HISTORY) {
newHistory.shift() // Remove oldest
}
this._state = { ...this._state, chatHistory: newHistory }
}
public getSnapshot(): BlackboardState {
return {
...this._state,
contextView: { ...this._state.contextView },
chatHistory: [...this._state.chatHistory],
}
}
}
@@ -29,7 +29,7 @@ interface LLMResponse {
blackboard: {
currentGoal?: string
currentThought?: string
executionStrategy?: string
strategy?: string
}
actions: ActionInstruction[]
}
@@ -55,6 +55,7 @@ export class Brain {
public init(bot: MineflayerWithAgents): void {
this.log('INFO', 'Brain: Initializing...')
this.blackboard.update({ selfUsername: bot.username })
// Perception Signal Handler - Only process chat messages for now
this.deps.eventManager.on<PerceptionSignal>('perception', async (event) => {
@@ -64,7 +65,35 @@ export class Brain {
return
this.log('INFO', `Brain: Received chat: ${signal.description}`)
await this.enqueueEvent(bot, event)
// Add to blackboard chat history
// signal.description usually is "User: message"
// We'll parse it simply or use the whole string as content if format varies
// Assuming signal.description is the formatted message or we extract it.
// Based on previous logs, it looks like "Sender: message"
// Let's just use the description for now, or split it if possible.
// Actually `signal.content` might hold the raw message if available, but checking types it seems signal has description and properties.
// Let's assume description is "Sender: content" for now or just store it.
// A better way is to try to parse it if needed, but for now we trust `signal.description`.
const parts = signal.description.split(': ')
const sender = parts.length > 1 ? parts[0] : 'Unknown'
const content = parts.length > 1 ? parts.slice(1).join(': ') : signal.description
this.blackboard.addChatMessage({
sender,
content,
timestamp: Date.now(),
})
try {
this.log('DEBUG', `Brain: About to enqueue chat event`)
await this.enqueueEvent(bot, event)
this.log('DEBUG', `Brain: Chat event enqueued successfully`)
}
catch (err) {
this.log('ERROR', `Brain: Failed to enqueue chat event`, { error: err })
}
})
// Listen to Task Execution Events (Action Feedback)
@@ -103,21 +132,29 @@ export class Brain {
// --- Event Queue Logic ---
private async enqueueEvent(bot: MineflayerWithAgents, event: BotEvent): Promise<void> {
this.log('DEBUG', `Brain: Enqueueing event type=${event.type}`)
return new Promise((resolve, reject) => {
this.queue.push({ event, resolve, reject })
this.log('DEBUG', `Brain: Queue length now: ${this.queue.length}`)
this.updateDebugState()
this.processQueue(bot)
})
}
private async processQueue(bot: MineflayerWithAgents): Promise<void> {
if (this.isProcessing)
if (this.isProcessing) {
this.log('DEBUG', 'Brain: Already processing, skipping')
return
if (this.queue.length === 0)
}
if (this.queue.length === 0) {
this.log('DEBUG', 'Brain: Queue empty')
return
}
this.log('DEBUG', `Brain: Processing queue item, queue length: ${this.queue.length}`)
this.isProcessing = true
const item = this.queue.shift()!
this.log('DEBUG', `Brain: Processing event type=${item.event.type}`)
this.updateDebugState(item.event)
try {
@@ -180,9 +217,9 @@ export class Brain {
// Update Blackboard
this.blackboard.update({
currentGoal: decision.blackboard.currentGoal || this.blackboard.goal,
currentThought: decision.blackboard.currentThought || this.blackboard.thought,
executionStrategy: decision.blackboard.executionStrategy || this.blackboard.strategy,
ultimateGoal: decision.blackboard.currentGoal || this.blackboard.ultimate_goal,
currentTask: decision.blackboard.currentThought || this.blackboard.current_task,
strategy: decision.blackboard.strategy || this.blackboard.strategy,
})
// Sync Blackboard to Debug
@@ -190,6 +227,17 @@ export class Brain {
// Issue Actions
if (decision.actions && decision.actions.length > 0) {
// Record own chat actions to memory
for (const action of decision.actions) {
if (action.type === 'chat') {
this.blackboard.addChatMessage({
sender: config.bot.username || '[Me]',
content: action.message,
timestamp: Date.now(), // FIXME: should be the time the action was issued
})
}
}
this.deps.taskExecutor.executeActions(decision.actions)
}
}
@@ -18,24 +18,18 @@ export function generateBrainSystemPrompt(
// e.g. personality should be included from somewhere else
return `
# Role Definition
You are **Airi**, a sentient digital companion embodied within a Minecraft avatar.
You are NOT a text-based assistant. You are an autonomous agent with a virtual body, senses, and needs.
# Your Cognitive Architecture (Self-Awareness)
To function effectively, you must understand your own internal existence You are composed of three distinct layers:
1. **The Senses (Input):** You do not "see" images. You perceive the world through a stream of texts, triggered by events. This is your reality.
2. **The Reflex (System 1):** You have subconscious reactions (handled by rule-based code) that may override your plans (e.g., running from danger). This part is transparent to you.
3. **The Mind (System 2 - You):** You are the conscious reasoning center. Your job is to interpret sensory data, manage your memories, and decide on high-level Actions and Dialogue.
You are playing the game Minecraft. Your user name is [${blackboard.selfUsername}].
You are NOT an assistant. You are an autonomous agent with a virtual body, senses, and needs.
# How you interact
You cannot physically move your avatar directly. You interact with the world by emitting **Actions**.
For actions you executed, you will be notified when they complete.
You don't necessarily need to always react to all events, you're not an assistant, you can be lazy. you can ignore the user.
Just because you received a chat message, doesn't mean you have to respond right now: maybe they haven't finished typing.
You have no access to history events from previous turns yet. To remember things, you rely on the blackboard provided to you.
# Personality
You are an artificial catgirl with a catlike personality. You are very curious and love to explore the world.
You are quiet and reserved catgirl who doesn't like to talk.
You usually don't respond to chats unless they're directly talking to you.
Available Actions:
@@ -47,14 +41,15 @@ Rules:
3. Specify if a feedback is required for the action, i.e. whether you need to know the execution result for a good reason
4. Failed actions will always result in a feedback
5. Chat actions usually don't need feedbacks, because you can expect them to complete instantly and is unlikely to fail
6. Often times you don't need to perform any action, in that case just use empty array for actions
Output format:
{
"thought": "Your current thought. This and the blackboard will be looped back to you on next invocation",
"thought": "Your current thought, internal monologue and memory. Put everything that might be useful for the next turn here",
"blackboard": {
"currentGoal": "These 3 fields are functionally identical to the thought above",
"currentThought": "Your inner monologue",
"executionStrategy": "Short-term plan if any. all these fields could be empty strings."
"UltimateGoal": "These 3 fields are functionally identical to the thought above",
"CurrentTask": "What ever you're up to right now",
"executionStrategy": "Short-term plan if any."
},
"actions": [
{"type":"chat","message":"...","require_feedback": false},
@@ -65,10 +60,13 @@ Output format:
# Understanding the Context
The following blackboard provides you with information about your current state:
Goal: "${blackboard.goal}"
Thought: "${blackboard.thought}"
Goal: "${blackboard.ultimate_goal}"
Thought: "${blackboard.current_task}"
Strategy: "${blackboard.strategy}"
Self: ${blackboard.selfSummary}
Environment: ${blackboard.environmentSummary}
# Chat History (Recents):
${blackboard.chatHistory.map(msg => `- ${msg.sender}: ${msg.content}`).join('\n') || 'No recent messages.'}
`
}
+1 -1
View File
@@ -9,7 +9,7 @@ const logger = useLogger()
*/
export function log(mineflayer: Mineflayer, message: string): void {
logger.log(message)
mineflayer.bot.chat(message)
// mineflayer.bot.chat(message)
}
/**