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:
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user