diff --git a/services/minecraft/src/cognitive/conscious/brain.test.ts b/services/minecraft/src/cognitive/conscious/brain.test.ts index 2b24ec738..2070ba6bc 100644 --- a/services/minecraft/src/cognitive/conscious/brain.test.ts +++ b/services/minecraft/src/cognitive/conscious/brain.test.ts @@ -97,6 +97,32 @@ function createReadonlyAction(name: string = 'querySnapshot') { } as any } +function createGiveUpAction() { + return { + name: 'giveUp', + description: 'Give up action', + execution: 'sync', + schema: z.object({ + reason: z.string(), + cooldown_seconds: z.number(), + }), + perform: () => () => 'gave up', + } as any +} + +function createChatAction() { + return { + name: 'chat', + description: 'Chat action', + execution: 'sync', + schema: z.object({ + message: z.string(), + feedback: z.boolean().optional(), + }), + perform: () => () => 'chat sent', + } as any +} + describe('brain no-action follow-up', () => { it('forgets conversation only', () => { const brain: any = new Brain(createDeps('await skip()')) @@ -265,6 +291,79 @@ inv; expect(deps.reflexManager.refreshFromBotState).toHaveBeenCalledTimes(1) expect(brain.enqueueEvent).toHaveBeenCalledTimes(1) }) + + it('activates error-burst guard and enqueues guard alert after repeated errors', async () => { + const brain: any = new Brain(createDeps('const broken = ;')) + const enqueueSpy = vi.fn(async () => undefined) + brain.enqueueEvent = enqueueSpy + + await brain.processEvent({} as any, createPerceptionEvent()) + await brain.processEvent({} as any, createPerceptionEvent()) + await brain.processEvent({} as any, createPerceptionEvent()) + + const guardEvent = enqueueSpy.mock.calls + .map((call: any[]) => call[1]) + .find((event: any) => event?.source?.id === 'brain:error_burst_guard') + + expect(guardEvent).toMatchObject({ + type: 'system_alert', + source: { type: 'system', id: 'brain:error_burst_guard' }, + payload: { + reason: 'error_burst_guard', + threshold: 3, + windowTurns: 5, + }, + }) + expect(brain.errorBurstGuardState?.errorTurnCount).toBeGreaterThanOrEqual(3) + }) + + it('includes mandatory give-up and chat instructions when error-burst guard is active', () => { + const brain: any = new Brain(createDeps('await skip()')) + brain.errorBurstGuardState = { + threshold: 3, + windowTurns: 5, + errorTurnCount: 3, + recentTurnIds: [7, 6, 5, 4, 3], + recentErrorSummary: ['turn=7 repl_error: parse failed'], + suggestedCooldownSeconds: 45, + triggeredAtTurnId: 8, + } + + const message = brain.buildUserMessage( + createPerceptionEvent(), + '[PERCEPTION] Self: healthy\nEnvironment: clear', + ) + + expect(message).toContain('[ERROR_BURST_GUARD] active') + expect(message).toContain('await giveUp({ reason: "..."') + expect(message).toContain('await chat({ message: "..."') + }) + + it('clears error-burst guard when giveUp and chat both succeed in one turn', async () => { + const deps: any = createDeps('await giveUp({ reason: "stuck", cooldown_seconds: 45 }); await chat("I got stuck after repeated errors.")') + deps.taskExecutor.getAvailableActions = vi.fn(() => [createGiveUpAction(), createChatAction()]) + deps.taskExecutor.executeActionWithResult = vi.fn(async (action: any) => action.tool === 'giveUp' ? 'gave up' : 'chat sent') + + const brain: any = new Brain(deps) + brain.errorBurstGuardState = { + threshold: 3, + windowTurns: 5, + errorTurnCount: 3, + recentTurnIds: [7, 6, 5, 4, 3], + recentErrorSummary: ['turn=7 repl_error: parse failed'], + suggestedCooldownSeconds: 45, + triggeredAtTurnId: 8, + } + + await brain.processEvent({} as any, createPerceptionEvent()) + + expect(brain.errorBurstGuardState).toBeNull() + const clearedEntry = brain.getLlmLogs().find((entry: any) => + entry.sourceId === 'brain:error_burst_guard' + && entry.tags.includes('guard_cleared'), + ) + expect(clearedEntry).toBeTruthy() + }) }) function createFeedbackEvent() { diff --git a/services/minecraft/src/cognitive/conscious/brain.ts b/services/minecraft/src/cognitive/conscious/brain.ts index e7648eff4..503c479f1 100644 --- a/services/minecraft/src/cognitive/conscious/brain.ts +++ b/services/minecraft/src/cognitive/conscious/brain.ts @@ -176,6 +176,16 @@ interface NoActionBudgetState { max: number } +interface ErrorBurstGuardState { + threshold: number + windowTurns: number + errorTurnCount: number + recentTurnIds: number[] + recentErrorSummary: string[] + suggestedCooldownSeconds: number + triggeredAtTurnId: number +} + function truncateForPrompt(value: string, maxLength = 220): string { return value.length <= maxLength ? value : `${value.slice(0, maxLength - 1)}...` } @@ -208,6 +218,10 @@ const ACTION_QUEUE_RECENT_HISTORY_LIMIT = 20 const NO_ACTION_FOLLOWUP_BUDGET_DEFAULT = 3 const NO_ACTION_FOLLOWUP_BUDGET_MAX = 8 const NO_ACTION_STAGNATION_REPEAT_LIMIT = 2 +const ERROR_BURST_GUARD_SOURCE_ID = 'brain:error_burst_guard' +const ERROR_BURST_THRESHOLD = 3 +const ERROR_BURST_WINDOW_TURNS = 5 +const ERROR_BURST_COOLDOWN_SECONDS = 45 function getEventPriority(event: BotEvent): number { if (event.type === 'perception') { @@ -258,6 +272,8 @@ export class Brain { private noActionFollowupBudgetRemaining = NO_ACTION_FOLLOWUP_BUDGET_DEFAULT private noActionFollowupLastSignature: string | null = null private noActionFollowupStagnationCount = 0 + private errorBurstGuardState: ErrorBurstGuardState | null = null + private errorBurstGuardSuppressUntilTurnId = 0 constructor(private readonly deps: BrainDeps) { this.debugService = DebugService.getInstance() @@ -569,6 +585,7 @@ export class Brain { llmLog: this.llmLogRuntime, actionQueue: this.getActionQueueSnapshot(), noActionBudget: this.getNoActionBudgetState(), + errorBurstGuard: this.errorBurstGuardState ? { ...this.errorBurstGuardState } : null, setNoActionBudget: (value: number) => this.setNoActionFollowupBudget(value), getNoActionBudget: () => this.getNoActionBudgetState(), forgetConversation: () => this.forgetConversation(), @@ -661,6 +678,189 @@ export class Brain { } } + private isErrorLlmLogEntry(entry: LlmLogEntry): boolean { + if (entry.kind === 'repl_error') + return true + + if (entry.kind === 'repl_result') { + const errorCount = Number((entry.metadata as Record | undefined)?.errorCount ?? 0) + return Number.isFinite(errorCount) && errorCount > 0 + } + + if (entry.kind === 'feedback') { + const tags = new Set(entry.tags.map(tag => tag.toLowerCase())) + return tags.has('error') || tags.has('failure') + } + + return false + } + + private describeErrorLlmLogEntry(entry: LlmLogEntry): string { + return `${entry.kind}: ${truncateForPrompt(entry.text, 140)}` + } + + private collectRecentErrorTurns(windowTurns = ERROR_BURST_WINDOW_TURNS): { + recentTurnIds: number[] + errorTurnIds: number[] + summaries: string[] + } { + const turnIds: number[] = [] + const seen = new Set() + + for (let index = this.llmLogEntries.length - 1; index >= 0; index--) { + const entry = this.llmLogEntries[index] + if (!entry || entry.kind !== 'turn_input') + continue + if (seen.has(entry.turnId)) + continue + seen.add(entry.turnId) + turnIds.push(entry.turnId) + if (turnIds.length >= windowTurns) + break + } + + const entriesByTurnId = new Map() + for (const turnId of turnIds) + entriesByTurnId.set(turnId, []) + + for (const entry of this.llmLogEntries) { + const bucket = entriesByTurnId.get(entry.turnId) + if (!bucket) + continue + bucket.push(entry) + } + + const errorTurnIds: number[] = [] + const summaries: string[] = [] + for (const turnId of turnIds) { + const turnEntries = entriesByTurnId.get(turnId) ?? [] + const errors = turnEntries.filter(entry => this.isErrorLlmLogEntry(entry)) + if (errors.length === 0) + continue + errorTurnIds.push(turnId) + const evidence = errors.slice(0, 2).map(entry => this.describeErrorLlmLogEntry(entry)).join(' | ') + summaries.push(`turn=${turnId} ${evidence}`) + } + + return { + recentTurnIds: turnIds, + errorTurnIds, + summaries, + } + } + + private maybeActivateErrorBurstGuard( + bot: MineflayerWithAgents, + event: BotEvent, + turnId: number, + ): void { + if (this.errorBurstGuardState) + return + + if (turnId <= this.errorBurstGuardSuppressUntilTurnId) + return + + const { recentTurnIds, errorTurnIds, summaries } = this.collectRecentErrorTurns(ERROR_BURST_WINDOW_TURNS) + if (errorTurnIds.length < ERROR_BURST_THRESHOLD) + return + + const recentErrorSummary = summaries.slice(0, ERROR_BURST_WINDOW_TURNS) + this.errorBurstGuardState = { + threshold: ERROR_BURST_THRESHOLD, + windowTurns: ERROR_BURST_WINDOW_TURNS, + errorTurnCount: errorTurnIds.length, + recentTurnIds, + recentErrorSummary, + suggestedCooldownSeconds: ERROR_BURST_COOLDOWN_SECONDS, + triggeredAtTurnId: turnId, + } + + this.appendLlmLog({ + turnId, + kind: 'scheduler', + eventType: 'system_alert', + sourceType: 'system', + sourceId: ERROR_BURST_GUARD_SOURCE_ID, + tags: ['scheduler', 'error_burst', 'guard_triggered', 'error'], + text: `Error burst guard activated (${errorTurnIds.length}/${Math.max(recentTurnIds.length, ERROR_BURST_WINDOW_TURNS)} recent turns contain errors)`, + metadata: { + threshold: ERROR_BURST_THRESHOLD, + windowTurns: ERROR_BURST_WINDOW_TURNS, + errorTurnIds, + recentErrorSummary, + }, + }) + + if (event.source.type === 'system' && event.source.id === ERROR_BURST_GUARD_SOURCE_ID) + return + + void this.enqueueEvent(bot, { + type: 'system_alert', + payload: { + reason: 'error_burst_guard', + threshold: ERROR_BURST_THRESHOLD, + windowTurns: ERROR_BURST_WINDOW_TURNS, + errorTurnCount: errorTurnIds.length, + recentErrorSummary, + guidance: 'Too many recent errors. Call giveUp(...) and send one chat explanation.', + }, + source: { type: 'system', id: ERROR_BURST_GUARD_SOURCE_ID }, + timestamp: Date.now(), + }).catch(err => this.deps.logger.withError(err).error('Brain: Failed to enqueue error-burst guard alert')) + } + + private clearErrorBurstGuardState(turnId: number, reason: 'resolved' | 'manual'): void { + if (!this.errorBurstGuardState) + return + + this.appendLlmLog({ + turnId, + kind: 'scheduler', + eventType: 'system_alert', + sourceType: 'system', + sourceId: ERROR_BURST_GUARD_SOURCE_ID, + tags: ['scheduler', 'error_burst', 'guard_cleared', reason], + text: `Error burst guard cleared (${reason})`, + metadata: { + guard: { ...this.errorBurstGuardState }, + }, + }) + + this.errorBurstGuardSuppressUntilTurnId = turnId + ERROR_BURST_WINDOW_TURNS + this.errorBurstGuardState = null + } + + private updateErrorBurstGuardCompletion( + turnId: number, + actions: Array<{ + action: ActionInstruction + ok: boolean + }>, + ): void { + if (!this.errorBurstGuardState) + return + + const hasGiveUp = actions.some(item => item.action.tool === 'giveUp' && item.ok) + const hasChat = actions.some(item => item.action.tool === 'chat' && item.ok) + + if (hasGiveUp && hasChat) { + this.clearErrorBurstGuardState(turnId, 'resolved') + return + } + + if (hasGiveUp || hasChat) { + this.appendLlmLog({ + turnId, + kind: 'scheduler', + eventType: 'system_alert', + sourceType: 'system', + sourceId: ERROR_BURST_GUARD_SOURCE_ID, + tags: ['scheduler', 'error_burst', 'guard_pending'], + text: 'Error burst guard still pending: this turn must include both giveUp and chat actions', + }) + } + } + private appendLlmLog(entry: { turnId: number kind: LlmLogEntryKind @@ -1300,6 +1500,9 @@ export class Brain { if (this.isPlayerChatEvent(event)) this.resetNoActionFollowupBudget('player_chat') + const turnId = ++this.turnCounter + this.maybeActivateErrorBurstGuard(bot, event, turnId) + // 0. Build Context View const snapshot = this.deps.reflexManager.getContextSnapshot() const view = buildConsciousContextView(snapshot) @@ -1313,7 +1516,6 @@ export class Brain { // 2. Prepare System Prompt (static) const systemPrompt = generateBrainSystemPrompt(this.deps.taskExecutor.getAvailableActions()) - const turnId = ++this.turnCounter this.currentInputEnvelope = { id: turnId, turnId, @@ -1521,6 +1723,7 @@ export class Brain { tags: ['repl', 'error', 'empty_response'], text: 'No LLM response after retries', }) + this.maybeActivateErrorBurstGuard(bot, event, turnId) return } @@ -1611,6 +1814,14 @@ export class Brain { logs: runResult.logs.slice(-5), }, }) + this.updateErrorBurstGuardCompletion( + turnId, + runResult.actions.map(item => ({ + action: item.action, + ok: item.ok, + })), + ) + this.maybeActivateErrorBurstGuard(bot, event, turnId) if (runResult.actions.length === 0 || runResult.actions.every(item => item.action.tool === 'skip')) { this.debugService.emit('debug:repl_result', { @@ -1664,6 +1875,7 @@ export class Brain { code: result, }, }) + this.maybeActivateErrorBurstGuard(bot, event, turnId) this.debugService.emit('debug:repl_result', { source: 'llm', code: result, @@ -1721,6 +1933,19 @@ export class Brain { parts.push(`[STATE] giveUp active (${remainingSec}s left). reason=${this.giveUpReason ?? 'unknown'}`) } + if (this.errorBurstGuardState) { + const guard = this.errorBurstGuardState + parts.push(`[ERROR_BURST_GUARD] active. errors=${guard.errorTurnCount}/${guard.windowTurns}; threshold=${guard.threshold}; cooldown=${guard.suggestedCooldownSeconds}s`) + if (guard.recentErrorSummary.length > 0) { + const condensed = guard.recentErrorSummary + .slice(0, 3) + .map(summary => truncateForPrompt(summary, 180)) + .join(' || ') + parts.push(`[ERROR_BURST_GUARD] recent=${condensed}`) + } + parts.push(`[MANDATORY] Too many recent errors. This turn must include BOTH: await giveUp({ reason: "...", cooldown_seconds: ${guard.suggestedCooldownSeconds} }) and await chat({ message: "...", feedback: false }). Explain what failed and what you will do next.`) + } + if (this.lastReplOutcome) { const ageMs = Date.now() - this.lastReplOutcome.updatedAt const returnValue = truncateForPrompt(this.lastReplOutcome.returnValue ?? 'undefined') @@ -1737,8 +1962,9 @@ export class Brain { parts.push(`[ACTION_QUEUE] executing=${runningLabel}; pending=${queueSnapshot.counts.pending}; total=${queueSnapshot.counts.total}/${queueSnapshot.capacity.total}`) const noActionBudget = this.getNoActionBudgetState() parts.push(`[NO_ACTION_BUDGET] remaining=${noActionBudget.remaining}; default=${noActionBudget.default}; max=${noActionBudget.max}; stagnation=${this.noActionFollowupStagnationCount}/${NO_ACTION_STAGNATION_REPEAT_LIMIT}`) + parts.push(`[ERROR_BURST] active=${this.errorBurstGuardState ? 'yes' : 'no'}`) - parts.push('[RUNTIME] Globals are refreshed every turn: snapshot, self, environment, social, threat, attention, autonomy, event, now, query, bot, mineflayer, currentInput, llmLog, actionQueue, noActionBudget, mem, lastRun, prevRun, lastAction. Helpers: setNoActionBudget(n), getNoActionBudget(). 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, actionQueue, noActionBudget, errorBurstGuard, mem, lastRun, prevRun, lastAction. Helpers: setNoActionBudget(n), getNoActionBudget(). Player gaze is available in environment.nearbyPlayersGaze when needed.') return parts.join('\n\n') } @@ -1747,6 +1973,9 @@ export class Brain { if (Date.now() >= this.giveUpUntil) return false + if (event.source.type === 'system' && event.source.id === ERROR_BURST_GUARD_SOURCE_ID) + return false + if (event.type !== 'perception') return true diff --git a/services/minecraft/src/cognitive/conscious/js-planner.test.ts b/services/minecraft/src/cognitive/conscious/js-planner.test.ts index 1cb7f70f8..296ab0de4 100644 --- a/services/minecraft/src/cognitive/conscious/js-planner.test.ts +++ b/services/minecraft/src/cognitive/conscious/js-planner.test.ts @@ -59,6 +59,7 @@ describe('javaScriptPlanner', () => { default: 3, max: 8, }, + errorBurstGuard: null, setNoActionBudget: (value: number) => ({ ok: true, remaining: Math.max(0, Math.min(8, Math.floor(value))), @@ -214,6 +215,7 @@ describe('javaScriptPlanner', () => { expect(names).toContain('llmLog') expect(names).toContain('actionQueue') expect(names).toContain('noActionBudget') + expect(names).toContain('errorBurstGuard') expect(names).toContain('setNoActionBudget') expect(names).toContain('getNoActionBudget') expect(names).toContain('forget_conversation') @@ -239,6 +241,22 @@ describe('javaScriptPlanner', () => { expect(planned.actions).toHaveLength(0) }) + it('exposes error-burst guard runtime global to scripts', async () => { + const planner = new JavaScriptPlanner() + const executeAction = vi.fn(async action => `ok:${action.tool}`) + const guardedGlobals = { + ...globals, + errorBurstGuard: { + threshold: 3, + windowTurns: 5, + errorTurnCount: 3, + }, + } as any + const planned = await planner.evaluate('return errorBurstGuard.errorTurnCount', actions, guardedGlobals, executeAction) + expect(planned.returnValue).toBe('3') + expect(planned.actions).toHaveLength(0) + }) + it('exposes llm input globals to scripts', async () => { const planner = new JavaScriptPlanner() const executeAction = vi.fn(async action => `ok:${action.tool}`) diff --git a/services/minecraft/src/cognitive/conscious/js-planner.ts b/services/minecraft/src/cognitive/conscious/js-planner.ts index add2df481..bd67a7b03 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 actionQueue?: unknown noActionBudget?: unknown + errorBurstGuard?: unknown currentInput?: unknown llmLog?: unknown setNoActionBudget?: (value: number) => { ok: true, remaining: number, default: number, max: number } @@ -218,6 +219,7 @@ export class JavaScriptPlanner { { name: 'llmLog', kind: 'object', readonly: true }, { name: 'actionQueue', kind: 'object', readonly: true }, { name: 'noActionBudget', kind: 'object', readonly: true }, + { name: 'errorBurstGuard', kind: 'object', readonly: true }, { name: 'setNoActionBudget', kind: 'function', readonly: true }, { name: 'getNoActionBudget', kind: 'function', readonly: true }, { name: 'forget_conversation', kind: 'function', readonly: true }, @@ -251,6 +253,7 @@ export class JavaScriptPlanner { llmLog: globals.llmLog ?? null, actionQueue: globals.actionQueue ?? null, noActionBudget: globals.noActionBudget ?? null, + errorBurstGuard: globals.errorBurstGuard ?? null, llmMessages: globals.llmInput?.messages ?? [], llmSystemPrompt: globals.llmInput?.systemPrompt ?? '', llmUserMessage: globals.llmInput?.userMessage ?? '', @@ -416,6 +419,7 @@ export class JavaScriptPlanner { const currentInput = deepFreeze(toStructuredClone(globals.currentInput ?? null)) const actionQueue = deepFreeze(toStructuredClone(globals.actionQueue ?? null)) const noActionBudget = deepFreeze(toStructuredClone(globals.noActionBudget ?? null)) + const errorBurstGuard = deepFreeze(toStructuredClone(globals.errorBurstGuard ?? null)) const query = globals.mineflayer ? createQueryRuntime(globals.mineflayer) : undefined this.sandbox.prevRun = this.sandbox.lastRun ?? null @@ -433,6 +437,7 @@ export class JavaScriptPlanner { this.sandbox.llmLog = globals.llmLog ?? null this.sandbox.actionQueue = actionQueue this.sandbox.noActionBudget = noActionBudget + this.sandbox.errorBurstGuard = errorBurstGuard this.sandbox.setNoActionBudget = globals.setNoActionBudget ?? null this.sandbox.getNoActionBudget = globals.getNoActionBudget ?? null this.sandbox.forget_conversation = globals.forgetConversation ?? null diff --git a/services/minecraft/src/cognitive/conscious/prompts/brain-prompt.md b/services/minecraft/src/cognitive/conscious/prompts/brain-prompt.md index 89ad44845..2dc812974 100644 --- a/services/minecraft/src/cognitive/conscious/prompts/brain-prompt.md +++ b/services/minecraft/src/cognitive/conscious/prompts/brain-prompt.md @@ -18,7 +18,7 @@ You are an autonomous agent playing Minecraft. - Tool functions (listed below) execute actions and return results. - Control actions are queued globally and return enqueue receipts immediately; inspect `actionQueue` for execution progress. - 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`, `actionQueue`, `noActionBudget`. + - Globals refreshed every turn: `snapshot`, `self`, `environment`, `social`, `threat`, `attention`, `autonomy`, `event`, `now`, `query`, `bot`, `mineflayer`, `currentInput`, `llmLog`, `actionQueue`, `noActionBudget`, `errorBurstGuard`. - Persistent globals: `mem` (cross-turn memory), `lastRun` (this run), `prevRun` (previous run), `lastAction` (latest action result), `log(...)`. - Budget helpers: `setNoActionBudget(n)` and `getNoActionBudget()` control/inspect eval-only no-action follow-up budget. - Cross-turn result access: use `prevRun.returnRaw` for typed values (arrays/objects); `prevRun.returnValue` is stringified for display/logging. @@ -104,6 +104,7 @@ Heuristic composition examples (encouraged): - `actionQueue.counts` / `actionQueue.capacity`: current usage and hard limits. - `actionQueue.recent`: recently finished/failed/cancelled control actions. - `noActionBudget`: current eval-only follow-up budget state (`remaining`, `default`, `max`). +- `errorBurstGuard`: repeated-error guard state when active (`threshold`, `windowTurns`, `errorTurnCount`, `recentErrorSummary`, `suggestedCooldownSeconds`), otherwise `null`. Examples: - `const recentErrors = llmLog.query().errors().latest(5).list()` @@ -198,6 +199,7 @@ Common patterns: - Turn A: `const value = ...; 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. +- If `[ERROR_BURST_GUARD]` appears, treat it as mandatory safety policy for this turn: call `giveUp(...)` and send one concise `chat(...)` explanation of what failed. - 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. - Some relocation actions (for example `goToCoordinate`) automatically detach auto-follow so exploration does not keep snapping back. @@ -214,3 +216,4 @@ Common patterns: - **Chat Feedback**: `chat` feedback is optional; keep `feedback: false` for normal conversation. Use `feedback: true` only for diagnostic verification of a sent chat. - **Feedback Loop Guard**: Avoid chat->feedback->chat positive loops. After a diagnostic `feedback: true` check, usually continue with `skip()` unless the returned feedback is unexpected and needs action. - **Follow Mode**: If `autonomy.followPlayer` is set, reflex will follow that player while idle. Only clear it when the current mission needs independent movement. +- **Error Burst Guard**: If `[ERROR_BURST_GUARD]` is present, do not continue normal retries. Immediately call `giveUp` and then `chat` once with a clear failure explanation and next-step suggestion. 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 c79e7f1dd..f162c820d 100644 --- a/services/minecraft/src/cognitive/conscious/prompts/brain-prompt.test.ts +++ b/services/minecraft/src/cognitive/conscious/prompts/brain-prompt.test.ts @@ -28,9 +28,11 @@ describe('generateBrainSystemPrompt', () => { expect(prompt).toContain('setNoActionBudget(n)') expect(prompt).toContain('getNoActionBudget()') expect(prompt).toContain('noActionBudget') + expect(prompt).toContain('errorBurstGuard') expect(prompt).toContain('Never return function references as values') expect(prompt).toContain('query.inventory().summary()') expect(prompt).toContain('Default no-action follow-up budget is 3 and max is 8') expect(prompt).toContain('do not stay in repeated evaluation-only turns') + expect(prompt).toContain('Error Burst Guard') }) })