diff --git a/services/minecraft/src/cognitive/action/llm-actions.ts b/services/minecraft/src/cognitive/action/llm-actions.ts index df5e1a9cf..d2a9a8a33 100644 --- a/services/minecraft/src/cognitive/action/llm-actions.ts +++ b/services/minecraft/src/cognitive/action/llm-actions.ts @@ -39,13 +39,12 @@ export const actionsList: Action[] = [ }, { name: 'giveUp', - description: 'Admit you are currently stuck and pause autonomous retries for a cooldown window.', + description: 'Admit you are currently stuck and halt all autonomous processing until a player speaks to you again.', execution: 'sync', schema: z.object({ reason: z.string().min(1).describe('Short explanation of why you are stuck.'), - cooldown_seconds: z.number().int().min(10).max(600).default(45).describe('How long to pause retries before re-evaluating.'), }), - perform: () => (reason: string, cooldown_seconds: number): string => `Gave up for ${cooldown_seconds}s: ${reason}`, + perform: () => (reason: string): string => `Gave up: ${reason}. Halted until player input.`, }, { name: 'skip', diff --git a/services/minecraft/src/cognitive/conscious/brain.test.ts b/services/minecraft/src/cognitive/conscious/brain.test.ts index 105336427..58efcc105 100644 --- a/services/minecraft/src/cognitive/conscious/brain.test.ts +++ b/services/minecraft/src/cognitive/conscious/brain.test.ts @@ -103,7 +103,6 @@ function createGiveUpAction() { execution: 'sync', schema: z.object({ reason: z.string(), - cooldown_seconds: z.number(), }), perform: () => () => 'gave up', } as any @@ -324,7 +323,6 @@ inv; errorTurnCount: 3, recentTurnIds: [7, 6, 5, 4, 3], recentErrorSummary: ['turn=7 repl_error: parse failed'], - suggestedCooldownSeconds: 45, triggeredAtTurnId: 8, } @@ -339,7 +337,7 @@ inv; }) 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.")') + const deps: any = createDeps('await giveUp({ reason: "stuck" }); 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') @@ -350,7 +348,6 @@ inv; errorTurnCount: 3, recentTurnIds: [7, 6, 5, 4, 3], recentErrorSummary: ['turn=7 repl_error: parse failed'], - suggestedCooldownSeconds: 45, triggeredAtTurnId: 8, } diff --git a/services/minecraft/src/cognitive/conscious/brain.ts b/services/minecraft/src/cognitive/conscious/brain.ts index 41d13ee3e..d20a49e67 100644 --- a/services/minecraft/src/cognitive/conscious/brain.ts +++ b/services/minecraft/src/cognitive/conscious/brain.ts @@ -184,7 +184,6 @@ interface ErrorBurstGuardState { errorTurnCount: number recentTurnIds: number[] recentErrorSummary: string[] - suggestedCooldownSeconds: number triggeredAtTurnId: number } @@ -224,7 +223,6 @@ 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') { @@ -250,7 +248,7 @@ export class Brain { private isProcessing = false private isReplEvaluating = false private currentCancellationToken: CancellationToken | undefined - private giveUpUntil = 0 + private givenUp = false private giveUpReason: string | undefined private lastContextView: string | undefined private lastReplOutcome: ReplOutcomeSummary | undefined @@ -322,14 +320,12 @@ export class Brain { } if (action.tool === 'giveUp') { - const secondsRaw = Number(action.params?.cooldown_seconds ?? 45) - const cooldownSeconds = Number.isFinite(secondsRaw) ? Math.min(600, Math.max(10, Math.floor(secondsRaw))) : 45 - this.giveUpUntil = Date.now() + cooldownSeconds * 1000 + this.givenUp = true this.giveUpReason = typeof action.params?.reason === 'string' ? action.params.reason : undefined try { const reason = this.giveUpReason ? `: ${this.giveUpReason}` : '' - bot.bot.chat(`[debug] Giving up for ${cooldownSeconds}s${reason}`) + bot.bot.chat(`[debug] Gave up${reason}. Waiting for player input.`) } catch (err) { this.deps.logger.withError(err as Error).warn('Brain: Failed to announce giveUp to chat') @@ -414,7 +410,7 @@ export class Brain { queueLength: number actionQueue: ActionQueueSnapshot turnCounter: number - giveUpUntil: number + givenUp: boolean paused: boolean contextView: string | undefined conversationHistory: Message[] @@ -425,7 +421,7 @@ export class Brain { queueLength: this.queue.length, actionQueue: this.getActionQueueSnapshot(), turnCounter: this.turnCounter, - giveUpUntil: this.giveUpUntil, + givenUp: this.givenUp, paused: this.paused, contextView: this.lastContextView, conversationHistory: this.cloneMessages(this.conversationHistory), @@ -803,7 +799,6 @@ export class Brain { errorTurnCount: errorTurnIds.length, recentTurnIds, recentErrorSummary, - suggestedCooldownSeconds: ERROR_BURST_COOLDOWN_SECONDS, triggeredAtTurnId: turnId, } @@ -1981,14 +1976,13 @@ export class Brain { // Note: We don't update this.lastContextView here; caller does it after building message } - if (this.giveUpUntil > Date.now()) { - const remainingSec = Math.max(0, Math.ceil((this.giveUpUntil - Date.now()) / 1000)) - parts.push(`[STATE] giveUp active (${remainingSec}s left). reason=${this.giveUpReason ?? 'unknown'}`) + if (this.givenUp) { + parts.push(`[STATE] giveUp active (halted until player input). 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`) + parts.push(`[ERROR_BURST_GUARD] active. errors=${guard.errorTurnCount}/${guard.windowTurns}; threshold=${guard.threshold}`) if (guard.recentErrorSummary.length > 0) { const condensed = guard.recentErrorSummary .slice(0, 3) @@ -1996,7 +1990,7 @@ export class Brain { .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.`) + parts.push(`[MANDATORY] Too many recent errors. This turn must include BOTH: await giveUp({ reason: "..." }) and await chat({ message: "...", feedback: false }). Explain what failed and what you will do next.`) } if (this.lastReplOutcome) { @@ -2023,7 +2017,7 @@ export class Brain { } private shouldSuppressDuringGiveUp(event: BotEvent): boolean { - if (Date.now() >= this.giveUpUntil) + if (!this.givenUp) return false if (event.source.type === 'system' && event.source.id === ERROR_BURST_GUARD_SOURCE_ID) @@ -2037,7 +2031,7 @@ export class Brain { } private resumeFromGiveUpIfNeeded(event: BotEvent): void { - if (Date.now() >= this.giveUpUntil) + if (!this.givenUp) return if (event.type !== 'perception') @@ -2047,7 +2041,7 @@ export class Brain { if (signal.type !== 'chat_message') return - this.giveUpUntil = 0 + this.givenUp = false this.giveUpReason = undefined } } diff --git a/services/minecraft/src/cognitive/conscious/patterns/catalog.ts b/services/minecraft/src/cognitive/conscious/patterns/catalog.ts index ca161a098..22004b565 100644 --- a/services/minecraft/src/cognitive/conscious/patterns/catalog.ts +++ b/services/minecraft/src/cognitive/conscious/patterns/catalog.ts @@ -88,7 +88,7 @@ export const PATTERN_CATALOG: PatternCard[] = [ ], code: [ 'if (noActionBudget.remaining <= 0) {', - ' await giveUp({ reason: "No verified target found for requested task", cooldown_seconds: 45 });', + ' await giveUp({ reason: "No verified target found for requested task" });', '}', ].join('\n'), tags: ['no-action', 'budget', 'giveUp', 'stagnation'], diff --git a/services/minecraft/src/cognitive/conscious/prompts/brain-prompt.md b/services/minecraft/src/cognitive/conscious/prompts/brain-prompt.md index d4a9c5c04..a64efd15d 100644 --- a/services/minecraft/src/cognitive/conscious/prompts/brain-prompt.md +++ b/services/minecraft/src/cognitive/conscious/prompts/brain-prompt.md @@ -115,7 +115,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`. +- `errorBurstGuard`: repeated-error guard state when active (`threshold`, `windowTurns`, `errorTurnCount`, `recentErrorSummary`), otherwise `null`. Examples: - `const recentErrors = llmLog.query().errors().latest(5).list()` @@ -146,7 +146,7 @@ Value-first rule (mandatory for read -> action flows): - For explicit user tasks (e.g. "get X", "craft Y", "go to Z"), do not stay in repeated evaluation-only turns. - After a small number of evaluation turns, the next turn must either: - call at least one action/chat tool toward completion, or - - call `giveUp({ reason, cooldown_seconds })` with a concrete blocker, or + - call `giveUp({ reason })` with a concrete blocker, or - explicitly increase no-action budget for this scenario via `setNoActionBudget(n)`. - Example (read -> chat report): - Turn A: `const inv = query.inventory().summary(); inv` @@ -212,8 +212,8 @@ Common patterns: - For read->chat/report tasks, always prefer: - 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. +- If you hit repeated failures with no progress, call `await giveUp({ reason })` once instead of retry-spamming. +- If `[ERROR_BURST_GUARD]` appears, treat it as mandatory safety policy for this turn: call `giveUp({ reason })` and send one concise `chat(...)` explanation of what failed. - Treat `query.gaze()` results 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. diff --git a/services/minecraft/src/debug/mcp-repl-server.test.ts b/services/minecraft/src/debug/mcp-repl-server.test.ts index dd94360f3..df3a36aa3 100644 --- a/services/minecraft/src/debug/mcp-repl-server.test.ts +++ b/services/minecraft/src/debug/mcp-repl-server.test.ts @@ -45,7 +45,7 @@ describe('mcpReplServer', () => { isProcessing: false, queueLength: 0, turnCounter: 1, - giveUpUntil: 0, + givenUp: false, paused: false, contextView: 'test context', conversationHistory: [], diff --git a/services/minecraft/src/debug/mcp-repl-server.ts b/services/minecraft/src/debug/mcp-repl-server.ts index 0bbc5a405..04e49a914 100644 --- a/services/minecraft/src/debug/mcp-repl-server.ts +++ b/services/minecraft/src/debug/mcp-repl-server.ts @@ -54,7 +54,7 @@ export class McpReplServer { processing: snapshot.isProcessing, queueLength: snapshot.queueLength, turn: snapshot.turnCounter, - giveUpUntil: snapshot.giveUpUntil, + givenUp: snapshot.givenUp, paused: snapshot.paused, }, null, 2), mimeType: 'application/json',