diff --git a/services/minecraft/src/cognitive/conscious/brain.ts b/services/minecraft/src/cognitive/conscious/brain.ts index f8942b13a..8b41f8086 100644 --- a/services/minecraft/src/cognitive/conscious/brain.ts +++ b/services/minecraft/src/cognitive/conscious/brain.ts @@ -9,12 +9,14 @@ import type { ReflexManager } from '../reflex/reflex-manager' import type { BotEvent, MineflayerWithAgents } from '../types' import type { PlannerGlobalDescriptor } from './js-planner' import type { LLMAgent } from './llm-agent' +import type { LlmLogEntry, LlmLogEntryKind } from './llm-log' import type { CancellationToken } from './task-state' import { config } from '../../composables/config' import { DebugService } from '../../debug' import { buildConsciousContextView } from './context-view' import { JavaScriptPlanner } from './js-planner' +import { createLlmLogRuntime } from './llm-log' import { isLikelyAuthOrBadArgError, isRateLimitError, @@ -73,10 +75,48 @@ interface LlmInputSnapshot { attempt: number } +interface RuntimeInputEnvelope { + id: number + turnId: number + timestamp: number + event: { + type: string + sourceType: string + sourceId: string + payload: unknown + } + contextView: string + userMessage: string + systemPrompt: { + preview: string + length: number + } + llm?: { + attempt: number + model: string + usage?: { + prompt_tokens?: number + completion_tokens?: number + total_tokens?: number + } + } +} + function truncateForPrompt(value: string, maxLength = 220): string { return value.length <= maxLength ? value : `${value.slice(0, maxLength - 1)}...` } +function stringifyForLog(value: unknown): string { + if (typeof value === 'string') + return value + try { + return JSON.stringify(value) + } + catch { + return String(value) + } +} + const NO_ACTION_FOLLOWUP_SOURCE_ID = 'brain:no_action_followup' export class Brain { @@ -97,6 +137,11 @@ export class Brain { private conversationHistory: Message[] = [] private lastLlmInputSnapshot: LlmInputSnapshot | null = null private runtimeMineflayer: MineflayerWithAgents | null = null + private readonly llmLogEntries: LlmLogEntry[] = [] + private llmLogIdCounter = 0 + private turnCounter = 0 + private currentInputEnvelope: RuntimeInputEnvelope | null = null + private readonly llmLogRuntime = createLlmLogRuntime(() => this.llmLogEntries) constructor(private readonly deps: BrainDeps) { this.debugService = DebugService.getInstance() @@ -120,6 +165,19 @@ export class Brain { // Action Feedback Handler this.deps.taskExecutor.on('action:completed', async ({ action, result }) => { this.deps.logger.log('INFO', `Brain: Action completed: ${action.tool}`) + this.appendLlmLog({ + turnId: this.turnCounter, + kind: 'feedback', + eventType: 'feedback', + sourceType: 'system', + sourceId: 'executor', + tags: ['feedback', 'success', action.tool], + text: `Action completed: ${action.tool}`, + metadata: { + params: action.params, + result: stringifyForLog(result), + }, + }) if (action.tool === 'chat' && action.params?.feedback !== true) { return @@ -142,6 +200,18 @@ export class Brain { this.deps.taskExecutor.on('action:failed', async ({ action, error }) => { this.deps.logger.withError(error).warn(`Brain: Action failed: ${action.tool}`) + this.appendLlmLog({ + turnId: this.turnCounter, + kind: 'feedback', + eventType: 'feedback', + sourceType: 'system', + sourceId: 'executor', + tags: ['feedback', 'error', action.tool], + text: `Action failed: ${action.tool}: ${error?.message || String(error)}`, + metadata: { + params: action.params, + }, + }) this.enqueueEvent(bot, { type: 'feedback', payload: { status: 'failure', action, error: error.message || error }, @@ -160,20 +230,15 @@ export class Brain { public getReplState(): { variables: PlannerGlobalDescriptor[], updatedAt: number } { const snapshot = this.deps.reflexManager.getContextSnapshot() + const replEvent: BotEvent = { + type: 'system_alert', + payload: { source: 'debug-repl-state' }, + source: { type: 'system', id: 'debug-repl' }, + timestamp: Date.now(), + } const variables = this.planner.describeGlobals( this.deps.taskExecutor.getAvailableActions(), - { - event: { - type: 'system_alert', - payload: { source: 'debug-repl-state' }, - source: { type: 'system', id: 'debug-repl' }, - timestamp: Date.now(), - }, - snapshot: snapshot as unknown as Record, - mineflayer: this.runtimeMineflayer, - bot: this.runtimeMineflayer?.bot, - llmInput: this.lastLlmInputSnapshot, - }, + this.createRuntimeGlobals(replEvent, snapshot as unknown as Record), ) return { @@ -208,18 +273,12 @@ export class Brain { const runResult = await this.planner.evaluate( codeToEvaluate, this.deps.taskExecutor.getAvailableActions(), - { - event: { - type: 'system_alert', - payload: { source: 'debug-repl' }, - source: { type: 'system', id: 'debug-repl' }, - timestamp: Date.now(), - }, - snapshot: snapshot as unknown as Record, - mineflayer: this.runtimeMineflayer, - bot: this.runtimeMineflayer?.bot, - llmInput: this.lastLlmInputSnapshot, - }, + this.createRuntimeGlobals({ + type: 'system_alert', + payload: { source: 'debug-repl' }, + source: { type: 'system', id: 'debug-repl' }, + timestamp: Date.now(), + }, snapshot as unknown as Record), async (action: ActionInstruction) => { const actionDef = actionDefs.get(action.tool) if (actionDef?.followControl === 'detach') @@ -282,15 +341,70 @@ export class Brain { return JSON.parse(JSON.stringify(messages)) as Message[] } + private createRuntimeGlobals( + event: BotEvent, + snapshot: Record, + mineflayerOverride?: MineflayerWithAgents | null, + ) { + const mineflayer = mineflayerOverride ?? this.runtimeMineflayer + return { + event, + snapshot, + mineflayer, + bot: mineflayer?.bot, + llmInput: this.lastLlmInputSnapshot, + currentInput: this.currentInputEnvelope, + llmLog: this.llmLogRuntime, + } + } + + private appendLlmLog(entry: { + turnId: number + kind: LlmLogEntryKind + eventType: string + sourceType: string + sourceId: string + tags?: string[] + text: string + metadata?: Record + }): void { + const normalized: LlmLogEntry = { + id: ++this.llmLogIdCounter, + turnId: entry.turnId, + kind: entry.kind, + timestamp: Date.now(), + eventType: entry.eventType, + sourceType: entry.sourceType, + sourceId: entry.sourceId, + tags: entry.tags ?? [], + text: entry.text, + metadata: entry.metadata, + } + + this.llmLogEntries.push(normalized) + if (this.llmLogEntries.length > 1000) { + this.llmLogEntries.shift() + } + } + private queueNoActionFollowup( bot: MineflayerWithAgents, triggeringEvent: BotEvent, + turnId: number, returnValue: string | undefined, logs: string[], ): void { if (triggeringEvent.source.type === 'system' && triggeringEvent.source.id === NO_ACTION_FOLLOWUP_SOURCE_ID) { this.deps.logger.log('INFO', 'Brain: Suppressed no-action follow-up (already in follow-up chain)') - this.debugService.log('DEBUG', 'No-action follow-up suppressed (already follow-up source)') + this.appendLlmLog({ + turnId, + kind: 'scheduler', + eventType: triggeringEvent.type, + sourceType: triggeringEvent.source.type, + sourceId: triggeringEvent.source.id, + tags: ['scheduler', 'no_action', 'suppressed'], + text: 'No-action follow-up suppressed: already follow-up source', + }) return } @@ -305,6 +419,18 @@ export class Brain { timestamp: Date.now(), } + this.appendLlmLog({ + turnId, + kind: 'scheduler', + eventType: triggeringEvent.type, + sourceType: triggeringEvent.source.type, + sourceId: triggeringEvent.source.id, + tags: ['scheduler', 'no_action'], + text: 'Scheduled one-hop no-action follow-up', + metadata: { + returnValue: returnValue ?? 'undefined', + }, + }) this.debugService.log('DEBUG', 'Scheduling one-hop no-action follow-up turn') void this.enqueueEvent(bot, followupEvent).catch(err => this.deps.logger.withError(err).error('Brain: Failed to enqueue no-action follow-up'), @@ -378,6 +504,36 @@ export class Brain { // 2. Prepare System Prompt (static) const systemPrompt = generateBrainSystemPrompt(this.deps.taskExecutor.getAvailableActions()) + const turnId = ++this.turnCounter + this.currentInputEnvelope = { + id: turnId, + turnId, + timestamp: Date.now(), + event: { + type: event.type, + sourceType: event.source.type, + sourceId: event.source.id, + payload: event.payload, + }, + contextView, + userMessage, + systemPrompt: { + preview: truncateForPrompt(systemPrompt, 240), + length: systemPrompt.length, + }, + } + this.appendLlmLog({ + turnId, + kind: 'turn_input', + eventType: event.type, + sourceType: event.source.type, + sourceId: event.source.id, + tags: ['input', event.type], + text: truncateForPrompt(userMessage, 600), + metadata: { + queueLength: this.queue.length, + }, + }) // 3. Call LLM with retry logic const maxAttempts = 3 @@ -400,6 +556,24 @@ export class Brain { updatedAt: Date.now(), attempt, } + this.currentInputEnvelope.llm = { + attempt, + model: config.openai.model, + } + this.appendLlmLog({ + turnId, + kind: 'llm_attempt', + eventType: event.type, + sourceType: event.source.type, + sourceId: event.source.id, + tags: ['llm', 'attempt'], + text: `LLM attempt ${attempt}/${maxAttempts}`, + metadata: { + attempt, + maxAttempts, + messageCount: messages.length, + }, + }) const traceStart = Date.now() @@ -426,6 +600,25 @@ export class Brain { model: config.openai.model, duration: Date.now() - traceStart, }) + this.currentInputEnvelope.llm = { + attempt, + model: config.openai.model, + usage: llmResult.usage, + } + this.appendLlmLog({ + turnId, + kind: 'llm_attempt', + eventType: event.type, + sourceType: event.source.type, + sourceId: event.source.id, + tags: ['llm', 'response'], + text: truncateForPrompt(content, 400), + metadata: { + attempt, + usage: llmResult.usage, + reasoningSize: reasoning?.length ?? 0, + }, + }) this.debugService.emitBrainState({ status: 'processing', @@ -455,6 +648,15 @@ export class Brain { // 4. Parse & Execute if (!result) { this.deps.logger.warn('Brain: No response after all retries') + this.appendLlmLog({ + turnId, + kind: 'planner_error', + eventType: event.type, + sourceType: event.source.type, + sourceId: event.source.id, + tags: ['planner', 'error', 'empty_response'], + text: 'No LLM response after retries', + }) return } @@ -479,13 +681,7 @@ export class Brain { const runResult = await this.planner.evaluate( codeToEvaluate, this.deps.taskExecutor.getAvailableActions(), - { - event, - snapshot: snapshot as unknown as Record, - mineflayer: bot, - bot: bot.bot, - llmInput: this.lastLlmInputSnapshot, - }, + this.createRuntimeGlobals(event, snapshot as unknown as Record, bot), async (action: ActionInstruction) => { if (action.tool === 'chat' && !this.shouldAllowChatForEvent(event, snapshot.self.health)) { return 'Chat suppressed: no direct user prompt for chat this turn' @@ -518,6 +714,31 @@ export class Brain { logs: runResult.logs.slice(-3), updatedAt: Date.now(), } + this.appendLlmLog({ + turnId, + kind: 'planner_result', + eventType: event.type, + sourceType: event.source.type, + sourceId: event.source.id, + tags: [ + 'planner', + runResult.actions.length === 0 ? 'no_actions' : 'actions', + runResult.actions.some(item => !item.ok) ? 'error' : 'ok', + ], + text: `actions=${runResult.actions.length} return=${runResult.returnValue ?? 'undefined'}`, + metadata: { + returnValue: runResult.returnValue, + actionCount: runResult.actions.length, + okCount: runResult.actions.filter(item => item.ok).length, + errorCount: runResult.actions.filter(item => !item.ok).length, + actions: runResult.actions.map(item => ({ + tool: item.action.tool, + ok: item.ok, + error: item.error, + })), + logs: runResult.logs.slice(-5), + }, + }) if (runResult.actions.length === 0 || runResult.actions.every(item => item.action.tool === 'skip')) { this.debugService.emit('debug:repl_result', { @@ -530,7 +751,7 @@ export class Brain { timestamp: Date.now(), }) if (runResult.actions.length === 0) { - this.queueNoActionFollowup(bot, event, runResult.returnValue, runResult.logs) + this.queueNoActionFollowup(bot, event, turnId, runResult.returnValue, runResult.logs) } this.deps.logger.log('INFO', 'Brain: Skipping turn (observing)') return @@ -559,6 +780,18 @@ export class Brain { } catch (err) { this.deps.logger.withError(err).error('Brain: Failed to execute decision') + this.appendLlmLog({ + turnId, + kind: 'planner_error', + eventType: event.type, + sourceType: event.source.type, + sourceId: event.source.id, + tags: ['planner', 'error'], + text: truncateForPrompt(toErrorMessage(err), 360), + metadata: { + code: result, + }, + }) this.debugService.emit('debug:repl_result', { source: 'llm', code: result, @@ -625,7 +858,7 @@ export class Brain { parts.push(`[SCRIPT] Last eval ${ageMs}ms ago: return=${returnValue}; actions=${this.lastPlannerOutcome.actionCount} (ok=${this.lastPlannerOutcome.okCount}, err=${this.lastPlannerOutcome.errorCount}); logs=${logs}`) } - parts.push('[RUNTIME] Globals are refreshed every turn: snapshot, self, environment, social, threat, attention, autonomy, event, now, query, bot, mineflayer, mem, lastRun, prevRun, lastAction. Player gaze is available in environment.nearbyPlayersGaze when needed.') + parts.push('[RUNTIME] Globals are refreshed every turn: snapshot, self, environment, social, threat, attention, autonomy, event, now, query, bot, mineflayer, currentInput, llmLog, mem, lastRun, prevRun, lastAction. Player gaze is available in environment.nearbyPlayersGaze when needed.') return parts.join('\n\n') } diff --git a/services/minecraft/src/cognitive/conscious/js-planner.test.ts b/services/minecraft/src/cognitive/conscious/js-planner.test.ts index 4b6bc574f..b53e3d378 100644 --- a/services/minecraft/src/cognitive/conscious/js-planner.test.ts +++ b/services/minecraft/src/cognitive/conscious/js-planner.test.ts @@ -168,6 +168,8 @@ describe('javaScriptPlanner', () => { expect(names).toContain('query') expect(names).toContain('bot') expect(names).toContain('mineflayer') + expect(names).toContain('currentInput') + expect(names).toContain('llmLog') const mem = descriptors.find(d => d.name === 'mem') expect(mem?.readonly).toBe(false) diff --git a/services/minecraft/src/cognitive/conscious/js-planner.ts b/services/minecraft/src/cognitive/conscious/js-planner.ts index c05e7e062..3086cbce4 100644 --- a/services/minecraft/src/cognitive/conscious/js-planner.ts +++ b/services/minecraft/src/cognitive/conscious/js-planner.ts @@ -67,6 +67,8 @@ export interface RuntimeGlobals { snapshot: Record mineflayer?: Mineflayer | null bot?: unknown + currentInput?: unknown + llmLog?: unknown llmInput?: { systemPrompt: string userMessage: string @@ -191,6 +193,8 @@ export class JavaScriptPlanner { { name: 'attention', kind: 'object', readonly: true }, { name: 'autonomy', kind: 'object', readonly: true }, { name: 'llmInput', kind: 'object', readonly: true }, + { name: 'currentInput', kind: 'object', readonly: true }, + { name: 'llmLog', kind: 'object', readonly: true }, { name: 'llmMessages', kind: 'object', readonly: true }, { name: 'llmSystemPrompt', kind: 'string', readonly: true }, { name: 'llmUserMessage', kind: 'string', readonly: true }, @@ -215,6 +219,8 @@ export class JavaScriptPlanner { attention: (globals.snapshot as Record)?.attention, autonomy: (globals.snapshot as Record)?.autonomy, llmInput: globals.llmInput ?? null, + currentInput: globals.currentInput ?? null, + llmLog: globals.llmLog ?? null, llmMessages: globals.llmInput?.messages ?? [], llmSystemPrompt: globals.llmInput?.systemPrompt ?? '', llmUserMessage: globals.llmInput?.userMessage ?? '', @@ -372,6 +378,7 @@ export class JavaScriptPlanner { const snapshot = deepFreeze(toStructuredClone(globals.snapshot)) const event = deepFreeze(toStructuredClone(globals.event)) const llmInput = deepFreeze(toStructuredClone(globals.llmInput ?? null)) + const currentInput = deepFreeze(toStructuredClone(globals.currentInput ?? null)) const query = globals.mineflayer ? createQueryRuntime(globals.mineflayer) : undefined this.sandbox.prevRun = this.sandbox.lastRun ?? null @@ -385,6 +392,8 @@ export class JavaScriptPlanner { this.sandbox.attention = snapshot.attention this.sandbox.autonomy = snapshot.autonomy this.sandbox.llmInput = llmInput + this.sandbox.currentInput = currentInput + this.sandbox.llmLog = globals.llmLog ?? null this.sandbox.llmMessages = llmInput?.messages ?? [] this.sandbox.llmSystemPrompt = llmInput?.systemPrompt ?? '' this.sandbox.llmUserMessage = llmInput?.userMessage ?? '' diff --git a/services/minecraft/src/cognitive/conscious/llm-log.test.ts b/services/minecraft/src/cognitive/conscious/llm-log.test.ts new file mode 100644 index 000000000..67647cad2 --- /dev/null +++ b/services/minecraft/src/cognitive/conscious/llm-log.test.ts @@ -0,0 +1,53 @@ +import type { LlmLogEntry } from './llm-log' + +import { describe, expect, it } from 'vitest' + +import { createLlmLogRuntime } from './llm-log' + +function seedEntries(count: number): LlmLogEntry[] { + return Array.from({ length: count }, (_, index) => ({ + id: index + 1, + turnId: Math.floor(index / 2) + 1, + kind: index % 3 === 0 ? 'planner_error' : 'turn_input', + timestamp: 1000 + index, + eventType: 'perception', + sourceType: 'minecraft', + sourceId: index % 2 === 0 ? 'Alex' : 'Steve', + tags: index % 3 === 0 ? ['error', 'planner'] : ['input'], + text: index % 3 === 0 ? 'Invalid tool parameters' : 'Chat event', + metadata: { i: index }, + })) +} + +describe('llmLog runtime', () => { + it('supports fluent filtering and latest slicing', () => { + const entries = seedEntries(20) + const llmLog = createLlmLogRuntime(() => entries) + const result = llmLog.query().errors().latest(3).list() + + expect(result).toHaveLength(3) + expect(result.every(entry => entry.tags.includes('error'))).toBe(true) + expect(result[0]?.timestamp).toBeGreaterThan(result[1]?.timestamp ?? 0) + }) + + it('supports text/source filtering and counting', () => { + const entries = seedEntries(12) + const llmLog = createLlmLogRuntime(() => entries) + const count = llmLog + .query() + .textIncludes('invalid tool') + .whereSource('minecraft', 'Alex') + .count() + + expect(count).toBeGreaterThan(0) + }) + + it('returns immutable copies from latest()', () => { + const entries = seedEntries(4) + const llmLog = createLlmLogRuntime(() => entries) + const recent = llmLog.latest(2) + recent[0]!.tags.push('mutated') + + expect(entries[3]?.tags.includes('mutated')).toBe(false) + }) +}) diff --git a/services/minecraft/src/cognitive/conscious/llm-log.ts b/services/minecraft/src/cognitive/conscious/llm-log.ts new file mode 100644 index 000000000..1c4c8fce8 --- /dev/null +++ b/services/minecraft/src/cognitive/conscious/llm-log.ts @@ -0,0 +1,133 @@ +export type LlmLogEntryKind + = 'turn_input' + | 'llm_attempt' + | 'planner_result' + | 'planner_error' + | 'scheduler' + | 'feedback' + +export interface LlmLogEntry { + id: number + turnId: number + kind: LlmLogEntryKind + timestamp: number + eventType: string + sourceType: string + sourceId: string + tags: string[] + text: string + metadata?: Record +} + +interface LlmLogQueryPatch { + predicates?: Array<(entry: LlmLogEntry) => boolean> + sorter?: (a: LlmLogEntry, b: LlmLogEntry) => number + sliceLatest?: number +} + +class LlmLogQuery { + constructor( + private readonly entries: readonly LlmLogEntry[], + private readonly predicates: Array<(entry: LlmLogEntry) => boolean> = [], + private readonly sorter?: (a: LlmLogEntry, b: LlmLogEntry) => number, + private readonly sliceLatest?: number, + ) {} + + public whereKind(kind: LlmLogEntryKind | LlmLogEntryKind[]): LlmLogQuery { + const set = new Set(Array.isArray(kind) ? kind : [kind]) + return this.clone({ + predicates: [...this.predicates, entry => set.has(entry.kind)], + }) + } + + public whereTag(tag: string | string[]): LlmLogQuery { + const set = new Set((Array.isArray(tag) ? tag : [tag]).map(item => item.toLowerCase())) + return this.clone({ + predicates: [...this.predicates, entry => entry.tags.some(item => set.has(item.toLowerCase()))], + }) + } + + public whereSource(sourceType: string, sourceId?: string): LlmLogQuery { + return this.clone({ + predicates: [...this.predicates, (entry) => { + if (entry.sourceType !== sourceType) + return false + if (sourceId !== undefined) + return entry.sourceId === sourceId + return true + }], + }) + } + + public errors(): LlmLogQuery { + return this.whereTag('error') + } + + public turns(): LlmLogQuery { + return this.whereKind('turn_input') + } + + public between(startTs: number, endTs: number): LlmLogQuery { + return this.clone({ + predicates: [...this.predicates, entry => entry.timestamp >= startTs && entry.timestamp <= endTs], + }) + } + + public textIncludes(fragment: string): LlmLogQuery { + const needle = fragment.toLowerCase() + return this.clone({ + predicates: [...this.predicates, entry => entry.text.toLowerCase().includes(needle)], + }) + } + + public latest(count: number): LlmLogQuery { + return this.clone({ + sorter: (a, b) => b.timestamp - a.timestamp, + sliceLatest: Math.max(1, Math.floor(count)), + }) + } + + public list(): LlmLogEntry[] { + let result = this.entries.filter(entry => this.predicates.every(predicate => predicate(entry))) + if (this.sorter) + result = [...result].sort(this.sorter) + if (this.sliceLatest !== undefined) + result = result.slice(0, this.sliceLatest) + return result.map(entry => ({ ...entry, tags: [...entry.tags] })) + } + + public first(): LlmLogEntry | null { + return this.list()[0] ?? null + } + + public count(): number { + return this.list().length + } + + private clone(patch: LlmLogQueryPatch): LlmLogQuery { + return new LlmLogQuery( + this.entries, + patch.predicates ?? this.predicates, + patch.sorter ?? this.sorter, + patch.sliceLatest ?? this.sliceLatest, + ) + } +} + +export function createLlmLogRuntime(getEntries: () => readonly LlmLogEntry[]) { + return { + get entries(): LlmLogEntry[] { + return getEntries().map(entry => ({ ...entry, tags: [...entry.tags] })) + }, + query(): LlmLogQuery { + return new LlmLogQuery(getEntries()) + }, + latest(count = 20): LlmLogEntry[] { + return new LlmLogQuery(getEntries()).latest(count).list() + }, + byId(id: number): LlmLogEntry | null { + const item = getEntries().find(entry => entry.id === id) + return item ? { ...item, tags: [...item.tags] } : null + }, + } +} diff --git a/services/minecraft/src/cognitive/conscious/prompts/brain-prompt.test.ts b/services/minecraft/src/cognitive/conscious/prompts/brain-prompt.test.ts index aa3bf370d..42c344dfd 100644 --- a/services/minecraft/src/cognitive/conscious/prompts/brain-prompt.test.ts +++ b/services/minecraft/src/cognitive/conscious/prompts/brain-prompt.test.ts @@ -19,5 +19,7 @@ describe('generateBrainSystemPrompt', () => { expect(prompt).toContain('chat->feedback->chat') expect(prompt).toContain('Query DSL') expect(prompt).toContain('Heuristic composition examples') + expect(prompt).toContain('llmLog') + expect(prompt).toContain('Silent-eval pattern') }) }) diff --git a/services/minecraft/src/cognitive/conscious/prompts/brain-prompt.ts b/services/minecraft/src/cognitive/conscious/prompts/brain-prompt.ts index 8ce7799cb..fd5067da1 100644 --- a/services/minecraft/src/cognitive/conscious/prompts/brain-prompt.ts +++ b/services/minecraft/src/cognitive/conscious/prompts/brain-prompt.ts @@ -110,7 +110,7 @@ You are an autonomous agent playing Minecraft. 6. **Planner Runtime**: Your script runs in a persistent JavaScript context with a timeout. - Tool functions (listed below) execute actions and return results. - Use \`await\` on tool calls when later logic depends on the result. - - Globals refreshed every turn: \`snapshot\`, \`self\`, \`environment\`, \`social\`, \`threat\`, \`attention\`, \`autonomy\`, \`event\`, \`now\`, \`query\`, \`bot\`, \`mineflayer\`. + - Globals refreshed every turn: \`snapshot\`, \`self\`, \`environment\`, \`social\`, \`threat\`, \`attention\`, \`autonomy\`, \`event\`, \`now\`, \`query\`, \`bot\`, \`mineflayer\`, \`currentInput\`, \`llmLog\`. - Persistent globals: \`mem\` (cross-turn memory), \`lastRun\` (this run), \`prevRun\` (previous run), \`lastAction\` (latest action result), \`log(...)\`. - Last script outcome is also echoed in the next turn as \`[SCRIPT]\` context (return value, action stats, and logs). - Maximum actions per turn: 5. @@ -167,6 +167,25 @@ Heuristic composition examples (encouraged): - \`if (orePressure > 3 && !hostileClose) { /* mine-oriented plan */ }\` - Verify assumptions with \`query\` first, then call action tools. +# Input + Runtime Log Objects +- \`currentInput\`: structured object for the current turn input (event metadata, user message, prompt preview, attempt/model info). +- \`llmLog\`: runtime ring-log of prior turn envelopes/results/errors with metadata. + - \`llmLog.entries\` for raw entries. + - \`llmLog.query()\` fluent lookup (\`whereKind\`, \`whereTag\`, \`whereSource\`, \`errors\`, \`turns\`, \`latest\`, \`between\`, \`textIncludes\`, \`list\`, \`first\`, \`count\`). + +Examples: +- \`const recentErrors = llmLog.query().errors().latest(5).list()\` +- \`const lastNoAction = llmLog.query().whereTag("no_actions").latest(1).first()\` +- \`const sameSourceTurns = llmLog.query().turns().whereSource(currentInput.event.sourceType, currentInput.event.sourceId).latest(3).list()\` +- \`const parseIssues = llmLog.query().textIncludes("Invalid tool parameters").latest(10).list()\` + +Silent-eval pattern (strongly encouraged): +- Use no-action evaluation turns to inspect uncertain values before committing to world actions. +- Good pattern: + - Turn A: \`let blocksToMine = someFunc(); blocksToMine\` + - Turn B: inspect \`[SCRIPT]\` return / \`llmLog\`, then act: \`await collectBlocks({ type: ..., num: ... })\` +- Prefer this when a wrong action would be costly, dangerous, or hard to undo. + # Response Format You must respond with JavaScript only (no markdown code fences). Call tool functions directly. @@ -211,6 +230,7 @@ Common patterns: - Treat action results as potentially unreliable; check outcomes against \`snapshot\`/feedback before committing to the next step. - Prefer deterministic scripts: no random branching unless needed. - Keep per-turn scripts short and focused on one tactical objective. +- Prefer "evaluate then act" loops: first compute and return candidate values (no actions), then perform tools in the next turn using confirmed values. - If you hit repeated failures with no progress, call \`await giveUp({ reason, cooldown_seconds })\` once instead of retry-spamming. - Treat \`environment.nearbyPlayersGaze\` as a weak hint, not a command. Never move solely because someone looked somewhere unless they also gave a clear instruction. - Use \`followPlayer\` to set idle auto-follow and \`clearFollowTarget\` before independent exploration.