feat(minecraft): expose LLM log to runtime for introspection

Add LlmLogEntry interface with id/turnId/kind/timestamp/eventType/sourceType/sourceId/tags/text/metadata fields, implement createLlmLogRuntime helper exposing query API with chainable filters (byTurn/byKind/byEventType/byTag/last/list/count/clear), track llmLogEntries array with appendLlmLog method logging turn_input/llm_attempt/planner_result/planner_error/feedback/scheduler events, inject llmLog/currentInput globals
This commit is contained in:
Rin
2026-02-18 11:14:39 +08:00
committed by Neko Ayaka
parent 034f0f30af
commit 4f382e0289
7 changed files with 487 additions and 35 deletions
@@ -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<string, unknown>,
mineflayer: this.runtimeMineflayer,
bot: this.runtimeMineflayer?.bot,
llmInput: this.lastLlmInputSnapshot,
},
this.createRuntimeGlobals(replEvent, snapshot as unknown as Record<string, unknown>),
)
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<string, unknown>,
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<string, unknown>),
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<string, unknown>,
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<string, unknown>
}): 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<string, unknown>,
mineflayer: bot,
bot: bot.bot,
llmInput: this.lastLlmInputSnapshot,
},
this.createRuntimeGlobals(event, snapshot as unknown as Record<string, unknown>, 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')
}
@@ -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)
@@ -67,6 +67,8 @@ export interface RuntimeGlobals {
snapshot: Record<string, unknown>
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<string, unknown>)?.attention,
autonomy: (globals.snapshot as Record<string, unknown>)?.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 ?? ''
@@ -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)
})
})
@@ -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<string, unknown>
}
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
},
}
}
@@ -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')
})
})
@@ -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.