refactor(minecraft): simplify giveUp action to halt until player input instead of timed cooldown

Removes cooldown_seconds parameter and timer-based resumption from giveUp action. Brain now uses boolean givenUp flag instead of giveUpUntil timestamp. Action halts all autonomous processing until next player chat message. Updates error-burst guard to remove suggestedCooldownSeconds field. Adjusts prompts, tests, and MCP server to reflect halt-until-input semantics.
This commit is contained in:
Rin
2026-02-19 00:25:06 +08:00
parent f176aecaee
commit 4ce0c427bc
7 changed files with 22 additions and 32 deletions
@@ -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',
@@ -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,
}
@@ -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
}
}
@@ -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'],
@@ -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.
@@ -45,7 +45,7 @@ describe('mcpReplServer', () => {
isProcessing: false,
queueLength: 0,
turnCounter: 1,
giveUpUntil: 0,
givenUp: false,
paused: false,
contextView: 'test context',
conversationHistory: [],
@@ -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',