diff --git a/services/minecraft/src/cognitive/conscious/brain.test.ts b/services/minecraft/src/cognitive/conscious/brain.test.ts index 7b555eda4..78c928edd 100644 --- a/services/minecraft/src/cognitive/conscious/brain.test.ts +++ b/services/minecraft/src/cognitive/conscious/brain.test.ts @@ -72,6 +72,28 @@ function createPerceptionEvent() { } describe('brain no-action follow-up', () => { + it('forgets conversation only', () => { + const brain: any = new Brain(createDeps('await skip()')) + brain.conversationHistory = [{ role: 'user', content: 'old' }] + brain.lastLlmInputSnapshot = { + systemPrompt: 'sys', + userMessage: 'msg', + messages: [], + conversationHistory: [], + updatedAt: Date.now(), + attempt: 1, + } + brain.llmLogEntries = [{ id: 1, turnId: 1, kind: 'turn_input', timestamp: Date.now(), eventType: 'x', sourceType: 'x', sourceId: 'x', tags: [], text: 'x' }] + + const result = brain.forgetConversation() + + expect(result.ok).toBe(true) + expect(result.cleared).toEqual(['conversationHistory', 'lastLlmInputSnapshot']) + expect(brain.conversationHistory).toEqual([]) + expect(brain.lastLlmInputSnapshot).toBeNull() + expect(brain.llmLogEntries).toHaveLength(1) + }) + it('returns trailing expression values in debug repl scripts', async () => { const brain: any = new Brain(createDeps('await skip()')) diff --git a/services/minecraft/src/cognitive/conscious/brain.ts b/services/minecraft/src/cognitive/conscious/brain.ts index f8e62611f..8f144c89c 100644 --- a/services/minecraft/src/cognitive/conscious/brain.ts +++ b/services/minecraft/src/cognitive/conscious/brain.ts @@ -313,6 +313,15 @@ export class Brain { return JSON.parse(JSON.stringify(entries)) as LlmTraceEntry[] } + public forgetConversation(): { ok: true, cleared: string[] } { + this.conversationHistory = [] + this.lastLlmInputSnapshot = null + return { + ok: true, + cleared: ['conversationHistory', 'lastLlmInputSnapshot'], + } + } + public async injectDebugEvent(event: BotEvent): Promise { if (!this.runtimeMineflayer) { throw new Error('Brain runtime is not initialized yet') @@ -462,6 +471,7 @@ export class Brain { llmInput: this.lastLlmInputSnapshot, currentInput: this.currentInputEnvelope, llmLog: this.llmLogRuntime, + forgetConversation: () => this.forgetConversation(), } } diff --git a/services/minecraft/src/cognitive/conscious/js-planner.test.ts b/services/minecraft/src/cognitive/conscious/js-planner.test.ts index b53e3d378..f744e10e1 100644 --- a/services/minecraft/src/cognitive/conscious/js-planner.test.ts +++ b/services/minecraft/src/cognitive/conscious/js-planner.test.ts @@ -46,6 +46,7 @@ describe('javaScriptPlanner', () => { updatedAt: Date.now(), attempt: 1, }, + forgetConversation: () => ({ ok: true, cleared: ['conversationHistory', 'lastLlmInputSnapshot'] }), } as any it('maps positional/object args and executes tools in order', async () => { @@ -170,6 +171,7 @@ describe('javaScriptPlanner', () => { expect(names).toContain('mineflayer') expect(names).toContain('currentInput') expect(names).toContain('llmLog') + expect(names).toContain('forget_conversation') const mem = descriptors.find(d => d.name === 'mem') expect(mem?.readonly).toBe(false) @@ -182,6 +184,15 @@ describe('javaScriptPlanner', () => { expect(planned.actions[0]?.action).toEqual({ tool: 'chat', params: { message: 'llm=latest user message' } }) }) + it('exposes forget_conversation runtime function', async () => { + const planner = new JavaScriptPlanner() + const executeAction = vi.fn(async action => `ok:${action.tool}`) + const planned = await planner.evaluate('return forget_conversation()', actions, globals, executeAction) + + expect(planned.returnValue).toContain('conversationHistory') + expect(planned.actions).toHaveLength(0) + }) + it('detects expression-friendly REPL inputs', () => { const planner = new JavaScriptPlanner() expect(planner.canEvaluateAsExpression('2 + 3')).toBe(true) diff --git a/services/minecraft/src/cognitive/conscious/js-planner.ts b/services/minecraft/src/cognitive/conscious/js-planner.ts index 3086cbce4..1f7fcedcc 100644 --- a/services/minecraft/src/cognitive/conscious/js-planner.ts +++ b/services/minecraft/src/cognitive/conscious/js-planner.ts @@ -69,6 +69,7 @@ export interface RuntimeGlobals { bot?: unknown currentInput?: unknown llmLog?: unknown + forgetConversation?: () => { ok: true, cleared: string[] } llmInput?: { systemPrompt: string userMessage: string @@ -195,6 +196,7 @@ export class JavaScriptPlanner { { name: 'llmInput', kind: 'object', readonly: true }, { name: 'currentInput', kind: 'object', readonly: true }, { name: 'llmLog', kind: 'object', readonly: true }, + { name: 'forget_conversation', kind: 'function', readonly: true }, { name: 'llmMessages', kind: 'object', readonly: true }, { name: 'llmSystemPrompt', kind: 'string', readonly: true }, { name: 'llmUserMessage', kind: 'string', readonly: true }, @@ -238,6 +240,7 @@ export class JavaScriptPlanner { expect: this.sandbox.expect, expectMoved: this.sandbox.expectMoved, expectNear: this.sandbox.expectNear, + forget_conversation: this.sandbox.forget_conversation, } for (const item of staticGlobals) { @@ -394,6 +397,7 @@ export class JavaScriptPlanner { this.sandbox.llmInput = llmInput this.sandbox.currentInput = currentInput this.sandbox.llmLog = globals.llmLog ?? null + this.sandbox.forget_conversation = globals.forgetConversation ?? null this.sandbox.llmMessages = llmInput?.messages ?? [] this.sandbox.llmSystemPrompt = llmInput?.systemPrompt ?? '' this.sandbox.llmUserMessage = llmInput?.userMessage ?? '' diff --git a/services/minecraft/src/cognitive/conscious/prompts/brain-prompt.md b/services/minecraft/src/cognitive/conscious/prompts/brain-prompt.md index 1d46e94d2..2c49e7477 100644 --- a/services/minecraft/src/cognitive/conscious/prompts/brain-prompt.md +++ b/services/minecraft/src/cognitive/conscious/prompts/brain-prompt.md @@ -18,6 +18,7 @@ You are an autonomous agent playing Minecraft. - 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`, `currentInput`, `llmLog`. - Persistent globals: `mem` (cross-turn memory), `lastRun` (this run), `prevRun` (previous run), `lastAction` (latest action result), `log(...)`. + - `forget_conversation()` clears conversation memory (`conversationHistory` and `lastLlmInputSnapshot`) for prompt/debug reset workflows. - Last script outcome is also echoed in the next turn as `[SCRIPT]` context (return value, action stats, and logs). - Maximum actions per turn: 5. If you need more, break down your task to perform in multiple turns. - Mineflayer API is provided for low-level control. @@ -89,6 +90,12 @@ Silent-eval pattern (strongly encouraged): - 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. +Value-first rule (mandatory for read -> action flows): +- If a request depends on observed world/query data, first run an evaluation-only turn and `return` the concrete value. +- Do not call world/chat tools in that first turn. +- In the next turn, use `[SCRIPT] Last eval return=...` as the source of truth for tool parameters/messages. +- Avoid acting on unresolved intermediate variables when a concrete returned value can be verified first. + # Response Format You must respond with JavaScript only (no markdown code fences). Call tool functions directly. @@ -133,6 +140,9 @@ Common patterns: - 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. +- For read->chat/report tasks, always prefer: + - Turn A: `const value = ...; return value` + - Turn B: construct tool params/messages from confirmed returned value. - 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. 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 42c344dfd..9b081f728 100644 --- a/services/minecraft/src/cognitive/conscious/prompts/brain-prompt.test.ts +++ b/services/minecraft/src/cognitive/conscious/prompts/brain-prompt.test.ts @@ -21,5 +21,7 @@ describe('generateBrainSystemPrompt', () => { expect(prompt).toContain('Heuristic composition examples') expect(prompt).toContain('llmLog') expect(prompt).toContain('Silent-eval pattern') + expect(prompt).toContain('Value-first rule') + expect(prompt).toContain('forget_conversation()') }) })