refactor(minecraft): switch brain to stateless conversation with explicit history management

Replace stateful neuri.handle with handleStateless, maintain conversation history manually using Message[] array, append user/assistant messages only after successful parsing to avoid dirty data on retry
This commit is contained in:
Rin
2026-02-18 11:14:08 +08:00
committed by Neko Ayaka
parent b237ac846d
commit edde03216f
@@ -1,6 +1,6 @@
import type { Logg } from '@guiiai/logg'
import type { Neuri } from 'neuri'
// import type { Message } from 'neuri/openai' // Use if needed
import type { Message } from 'neuri/openai'
import type { TaskExecutor } from '../action/task-executor'
import type { EventBus, TracedEvent } from '../os'
@@ -9,7 +9,7 @@ import type { ReflexManager } from '../reflex/reflex-manager'
import type { BotEvent, MineflayerWithAgents } from '../types'
import { createCancellationToken, type CancellationToken } from './task-state'
import { system, user } from 'neuri/openai'
import { assistant, system, user } from 'neuri/openai'
import { config } from '../../composables/config'
import { DebugService } from '../../debug'
@@ -96,7 +96,7 @@ export class Brain {
private isProcessing = false
private currentCancellationToken: CancellationToken | undefined
private lastContextView: string | undefined
private lastReasoning: string | undefined // HACK: Store previous reasoning token for continuity
private conversationHistory: Message[] = []
constructor(private readonly deps: BrainDeps) {
this.debugService = DebugService.getInstance()
@@ -207,28 +207,22 @@ export class Brain {
// 2. Prepare System Prompt (static)
const systemPrompt = generateBrainSystemPrompt(this.deps.taskExecutor.getAvailableActions())
// 3. Call Neuri (Stateful) with retry logic
// 3. Call Neuri (Stateless) with retry logic
const maxAttempts = 3
let result: string | null = null
let capturedReasoning: string | undefined // HACK: Capture reasoning but don't save until success
let capturedReasoning: string | undefined
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
result = await this.deps.neuri.handle(
[
// System prompt is inserted/updated in the callback to prevent duplication
user(userMessage),
],
result = await this.deps.neuri.handleStateless(
[user(userMessage)],
async (ctx) => {
let messages = ctx.messages
// Ensure system prompt is at the start (or replace existing one)
const sysMsgIndex = messages.findIndex(m => m.role === 'system')
if (sysMsgIndex >= 0) {
messages[sysMsgIndex] = system(systemPrompt)
} else {
messages = [system(systemPrompt), ...messages]
}
// Build complete message history: system + conversation history + new user message
const messages = [
system(systemPrompt),
...this.conversationHistory,
...ctx.messages
]
const traceStart = Date.now()
@@ -241,11 +235,12 @@ export class Brain {
const content = message?.content
const reasoning = message?.reasoning_content || message?.reasoning
// HACK: Capture reasoning but don't save yet (wait for successful parsing)
capturedReasoning = reasoning
if (!content) throw new Error('No content from LLM')
// Capture reasoning for later use
// We'll only append to history after successful parsing outside the callback
capturedReasoning = reasoning
this.debugService.traceLLM({
route: 'brain',
messages,
@@ -293,10 +288,12 @@ export class Brain {
const parsed = this.parseResponse(result)
const action = parsed.action
// HACK: Only store reasoning after successful LLM call and valid JSON parsing
if (capturedReasoning) {
this.lastReasoning = capturedReasoning
}
// Only append to conversation history after successful parsing (avoid dirty data on retry)
this.conversationHistory.push(user(userMessage))
const assistantContent = capturedReasoning
? `[REASONING] ${capturedReasoning}\n\n${result}`
: result
this.conversationHistory.push(assistant(assistantContent))
if (action.tool === 'skip') {
this.deps.logger.log('INFO', 'Brain: Skipping turn (observing)')
@@ -341,11 +338,6 @@ export class Brain {
private buildUserMessage(event: BotEvent, contextView: string): string {
const parts: string[] = []
// HACK: Inject previous reasoning token into user message for continuity
if (this.lastReasoning) {
parts.push(`[PREVIOUS REASONING] ${this.lastReasoning}`)
}
// 1. Event Content
if (event.type === 'perception') {
const signal = event.payload as PerceptionSignal