diff --git a/services/minecraft/src/cognitive/conscious/brain.test.ts b/services/minecraft/src/cognitive/conscious/brain.test.ts index 16adb3f53..2b24ec738 100644 --- a/services/minecraft/src/cognitive/conscious/brain.test.ts +++ b/services/minecraft/src/cognitive/conscious/brain.test.ts @@ -141,7 +141,7 @@ inv; expect(result.returnValue).toContain('oak_log') }) - it('queues exactly one synthetic follow-up on no-action result', async () => { + it('queues budgeted synthetic follow-up on no-action result', async () => { const brain: any = new Brain(createDeps('1 + 1')) const enqueueSpy = vi.fn(async () => undefined) brain.enqueueEvent = enqueueSpy @@ -153,7 +153,11 @@ inv; expect(queuedEvent).toMatchObject({ type: 'system_alert', source: { type: 'system', id: 'brain:no_action_followup' }, - payload: { reason: 'no_actions', returnValue: '2' }, + payload: { + reason: 'no_actions', + returnValue: '2', + noActionBudget: { remaining: 2, default: 3, max: 8 }, + }, }) }) @@ -172,7 +176,7 @@ inv; expect(queuedEvent?.payload?.returnValue).toContain('oak_sapling') }) - it('does not chain follow-up from follow-up event source', async () => { + it('allows chained follow-up from follow-up event source while budget remains', async () => { const brain: any = new Brain(createDeps('1 + 1')) const enqueueSpy = vi.fn(async () => undefined) brain.enqueueEvent = enqueueSpy @@ -184,7 +188,46 @@ inv; timestamp: Date.now(), }) - expect(enqueueSpy).not.toHaveBeenCalled() + expect(enqueueSpy).toHaveBeenCalledTimes(1) + const queuedEvent = (enqueueSpy.mock.calls[0] as any[])?.[1] + expect(queuedEvent?.source?.id).toBe('brain:no_action_followup') + }) + + it('blocks no-action follow-up when budget is exhausted and emits budget alert', async () => { + const brain: any = new Brain(createDeps('1 + 1')) + brain.setNoActionFollowupBudget(0) + const enqueueSpy = vi.fn(async () => undefined) + brain.enqueueEvent = enqueueSpy + const bot = { bot: { chat: vi.fn() } } + + await brain.processEvent(bot as any, { + type: 'system_alert', + payload: { source: 'budget-test' }, + source: { type: 'system', id: 'budget-test' }, + timestamp: Date.now(), + }) + + expect(enqueueSpy).toHaveBeenCalledTimes(1) + const queuedEvent = (enqueueSpy.mock.calls[0] as any[])?.[1] + expect(queuedEvent).toMatchObject({ + type: 'system_alert', + source: { type: 'system', id: 'brain:no_action_budget' }, + payload: { reason: 'no_action_budget_exhausted' }, + }) + expect(bot.bot.chat).toHaveBeenCalledTimes(1) + }) + + it('resets no-action budget when player chat arrives', async () => { + const brain: any = new Brain(createDeps('await skip()')) + brain.setNoActionFollowupBudget(0) + + await brain.processEvent({} as any, createPerceptionEvent()) + + expect(brain.getNoActionBudgetState()).toEqual({ + remaining: 3, + default: 3, + max: 8, + }) }) it('does not queue follow-up when script uses skip()', async () => { diff --git a/services/minecraft/src/cognitive/conscious/brain.ts b/services/minecraft/src/cognitive/conscious/brain.ts index f12a68305..708a4ab17 100644 --- a/services/minecraft/src/cognitive/conscious/brain.ts +++ b/services/minecraft/src/cognitive/conscious/brain.ts @@ -170,6 +170,12 @@ interface ControlActionQueueEntry { error?: string } +interface NoActionBudgetState { + remaining: number + default: number + max: number +} + function truncateForPrompt(value: string, maxLength = 220): string { return value.length <= maxLength ? value : `${value.slice(0, maxLength - 1)}...` } @@ -186,6 +192,7 @@ function stringifyForLog(value: unknown): string { } const NO_ACTION_FOLLOWUP_SOURCE_ID = 'brain:no_action_followup' +const NO_ACTION_BUDGET_ALERT_SOURCE_ID = 'brain:no_action_budget' /** * Priority tiers for event scheduling (lower = higher priority). @@ -198,6 +205,9 @@ const EVENT_PRIORITY_NO_ACTION_FOLLOWUP = 3 const MAX_QUEUED_CONTROL_ACTIONS = 5 const MAX_PENDING_CONTROL_ACTIONS = 4 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 function getEventPriority(event: BotEvent): number { if (event.type === 'perception') { @@ -245,6 +255,9 @@ export class Brain { private actionQueueUpdatedAt = Date.now() private isActionWorkerRunning = false private completedControlActionsSinceLastFeedback = 0 + private noActionFollowupBudgetRemaining = NO_ACTION_FOLLOWUP_BUDGET_DEFAULT + private noActionFollowupLastSignature: string | null = null + private noActionFollowupStagnationCount = 0 constructor(private readonly deps: BrainDeps) { this.debugService = DebugService.getInstance() @@ -547,10 +560,99 @@ export class Brain { currentInput: this.currentInputEnvelope, llmLog: this.llmLogRuntime, actionQueue: this.getActionQueueSnapshot(), + noActionBudget: this.getNoActionBudgetState(), + setNoActionBudget: (value: number) => this.setNoActionFollowupBudget(value), + getNoActionBudget: () => this.getNoActionBudgetState(), forgetConversation: () => this.forgetConversation(), } } + private isPlayerChatEvent(event: BotEvent): boolean { + if (event.type !== 'perception') + return false + const signal = event.payload as PerceptionSignal + return signal.type === 'chat_message' + } + + private getNoActionBudgetState(): NoActionBudgetState { + return { + remaining: this.noActionFollowupBudgetRemaining, + default: NO_ACTION_FOLLOWUP_BUDGET_DEFAULT, + max: NO_ACTION_FOLLOWUP_BUDGET_MAX, + } + } + + private resetNoActionFollowupBudget(reason: 'player_chat' | 'manual'): NoActionBudgetState { + this.noActionFollowupBudgetRemaining = NO_ACTION_FOLLOWUP_BUDGET_DEFAULT + this.noActionFollowupLastSignature = null + this.noActionFollowupStagnationCount = 0 + this.appendLlmLog({ + turnId: this.turnCounter, + kind: 'scheduler', + eventType: 'system_alert', + sourceType: 'system', + sourceId: 'brain:no_action_budget', + tags: ['scheduler', 'no_action', 'budget_reset', reason], + text: `No-action follow-up budget reset (${reason})`, + metadata: { + budget: this.getNoActionBudgetState(), + }, + }) + return this.getNoActionBudgetState() + } + + private setNoActionFollowupBudget(value: number): { ok: true } & NoActionBudgetState { + const normalizedRaw = Number(value) + const normalized = Number.isFinite(normalizedRaw) + ? Math.floor(normalizedRaw) + : this.noActionFollowupBudgetRemaining + const clamped = Math.max(0, Math.min(NO_ACTION_FOLLOWUP_BUDGET_MAX, normalized)) + this.noActionFollowupBudgetRemaining = clamped + this.noActionFollowupLastSignature = null + this.noActionFollowupStagnationCount = 0 + + this.appendLlmLog({ + turnId: this.turnCounter, + kind: 'scheduler', + eventType: 'system_alert', + sourceType: 'system', + sourceId: 'brain:no_action_budget', + tags: ['scheduler', 'no_action', 'budget_set'], + text: `No-action follow-up budget set to ${clamped}`, + metadata: { + requested: value, + budget: this.getNoActionBudgetState(), + }, + }) + + return { + ok: true, + ...this.getNoActionBudgetState(), + } + } + + private buildNoActionSignature(returnValue: string | undefined, logs: string[]): string { + const returnPart = truncateForPrompt(returnValue ?? 'undefined', 320) + const logsPart = logs.slice(-3).map(line => truncateForPrompt(line, 140)).join('|') + return `${returnPart}||${logsPart}` + } + + private emitNoActionBudgetDebugChat( + bot: MineflayerWithAgents, + reason: 'no_action_budget_exhausted' | 'no_action_stagnated', + ): void { + const message = reason === 'no_action_budget_exhausted' + ? `[debug] no-action follow-up budget exhausted (remaining=0).` + : `[debug] no-action follow-up blocked due to stagnant eval loop.` + + try { + bot.bot.chat(message) + } + catch (err) { + this.deps.logger.withError(err as Error).warn('Brain: Failed to send no-action budget debug chat') + } + } + private appendLlmLog(entry: { turnId: number kind: LlmLogEntryKind @@ -974,26 +1076,75 @@ export class Brain { returnValue: string | undefined, logs: string[], ): void { - if (triggeringEvent.source.type === 'system' && triggeringEvent.source.id === NO_ACTION_FOLLOWUP_SOURCE_ID) { - this.deps.logger.log('INFO', 'Brain: Suppressed no-action follow-up (already in follow-up chain)') + const signature = this.buildNoActionSignature(returnValue, logs) + const budgetBefore = this.noActionFollowupBudgetRemaining + if (signature === this.noActionFollowupLastSignature) + this.noActionFollowupStagnationCount++ + else + this.noActionFollowupStagnationCount = 0 + this.noActionFollowupLastSignature = signature + + const stagnated = this.noActionFollowupStagnationCount >= NO_ACTION_STAGNATION_REPEAT_LIMIT + const exhausted = this.noActionFollowupBudgetRemaining <= 0 + if (stagnated || exhausted) { + const reason: 'no_action_budget_exhausted' | 'no_action_stagnated' = exhausted + ? 'no_action_budget_exhausted' + : 'no_action_stagnated' + this.appendLlmLog({ turnId, kind: 'scheduler', eventType: triggeringEvent.type, sourceType: triggeringEvent.source.type, sourceId: triggeringEvent.source.id, - tags: ['scheduler', 'no_action', 'suppressed'], - text: 'No-action follow-up suppressed: already follow-up source', + tags: ['scheduler', 'no_action', 'blocked', reason], + text: `Blocked no-action follow-up: ${reason}`, + metadata: { + budgetBefore, + budgetAfter: this.noActionFollowupBudgetRemaining, + stagnationCount: this.noActionFollowupStagnationCount, + signature, + returnValue: returnValue ?? 'undefined', + }, }) + + if (triggeringEvent.source.type === 'system' && triggeringEvent.source.id === NO_ACTION_BUDGET_ALERT_SOURCE_ID) { + this.deps.logger.log('INFO', `Brain: Suppressed repeated no-action budget alert (${reason})`) + return + } + + this.debugService.log('DEBUG', `No-action follow-up blocked: ${reason}`) + this.emitNoActionBudgetDebugChat(bot, reason) + + const followupEvent: BotEvent = { + type: 'system_alert', + payload: { + reason, + returnValue: returnValue ?? 'undefined', + logs: logs.slice(-3), + noActionBudget: this.getNoActionBudgetState(), + guidance: 'No-action follow-up budget exhausted. Abandon this approach or call setNoActionBudget(n) for this scenario.', + }, + source: { type: 'system', id: NO_ACTION_BUDGET_ALERT_SOURCE_ID }, + timestamp: Date.now(), + } + + void this.enqueueEvent(bot, followupEvent).catch(err => + this.deps.logger.withError(err).error('Brain: Failed to enqueue no-action budget alert'), + ) return } + this.noActionFollowupBudgetRemaining = Math.max(0, this.noActionFollowupBudgetRemaining - 1) + const budgetAfter = this.noActionFollowupBudgetRemaining + const followupEvent: BotEvent = { type: 'system_alert', payload: { reason: 'no_actions', returnValue: returnValue ?? 'undefined', logs: logs.slice(-3), + noActionBudget: this.getNoActionBudgetState(), }, source: { type: 'system', id: NO_ACTION_FOLLOWUP_SOURCE_ID }, timestamp: Date.now(), @@ -1006,12 +1157,16 @@ export class Brain { sourceType: triggeringEvent.source.type, sourceId: triggeringEvent.source.id, tags: ['scheduler', 'no_action'], - text: 'Scheduled one-hop no-action follow-up', + text: 'Scheduled budgeted no-action follow-up turn', metadata: { returnValue: returnValue ?? 'undefined', + budgetBefore, + budgetAfter, + stagnationCount: this.noActionFollowupStagnationCount, + signature, }, }) - this.debugService.log('DEBUG', 'Scheduling one-hop no-action follow-up turn') + this.debugService.log('DEBUG', 'Scheduling budgeted no-action follow-up turn') void this.enqueueEvent(bot, followupEvent).catch(err => this.deps.logger.withError(err).error('Brain: Failed to enqueue no-action follow-up'), ) @@ -1134,6 +1289,8 @@ export class Brain { this.resumeFromGiveUpIfNeeded(event) if (this.shouldSuppressDuringGiveUp(event)) return + if (this.isPlayerChatEvent(event)) + this.resetNoActionFollowupBudget('player_chat') // 0. Build Context View const snapshot = this.deps.reflexManager.getContextSnapshot() @@ -1525,8 +1682,10 @@ export class Brain { ? `${queueSnapshot.executing.tool}#${queueSnapshot.executing.id}` : 'none' 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('[RUNTIME] Globals are refreshed every turn: snapshot, self, environment, social, threat, attention, autonomy, event, now, query, bot, mineflayer, currentInput, llmLog, actionQueue, mem, lastRun, prevRun, lastAction. 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, mem, lastRun, prevRun, lastAction. Helpers: setNoActionBudget(n), getNoActionBudget(). Player gaze is available in environment.nearbyPlayersGaze when needed.') return parts.join('\n\n') } diff --git a/services/minecraft/src/cognitive/conscious/js-planner.test.ts b/services/minecraft/src/cognitive/conscious/js-planner.test.ts index 726c907e6..1cb7f70f8 100644 --- a/services/minecraft/src/cognitive/conscious/js-planner.test.ts +++ b/services/minecraft/src/cognitive/conscious/js-planner.test.ts @@ -54,6 +54,22 @@ describe('javaScriptPlanner', () => { counts: { total: 0, executing: 0, pending: 0 }, updatedAt: Date.now(), }, + noActionBudget: { + remaining: 3, + default: 3, + max: 8, + }, + setNoActionBudget: (value: number) => ({ + ok: true, + remaining: Math.max(0, Math.min(8, Math.floor(value))), + default: 3, + max: 8, + }), + getNoActionBudget: () => ({ + remaining: 3, + default: 3, + max: 8, + }), forgetConversation: () => ({ ok: true, cleared: ['conversationHistory', 'lastLlmInputSnapshot'] }), } as any @@ -197,6 +213,9 @@ describe('javaScriptPlanner', () => { expect(names).toContain('currentInput') expect(names).toContain('llmLog') expect(names).toContain('actionQueue') + expect(names).toContain('noActionBudget') + expect(names).toContain('setNoActionBudget') + expect(names).toContain('getNoActionBudget') expect(names).toContain('forget_conversation') const mem = descriptors.find(d => d.name === 'mem') @@ -211,6 +230,15 @@ describe('javaScriptPlanner', () => { expect(planned.actions).toHaveLength(0) }) + it('exposes no-action budget runtime globals to scripts', async () => { + const planner = new JavaScriptPlanner() + const executeAction = vi.fn(async action => `ok:${action.tool}`) + const planned = await planner.evaluate('return { state: getNoActionBudget(), set: setNoActionBudget(6), now: noActionBudget }', actions, globals, executeAction) + expect(planned.returnValue).toContain('remaining: 3') + expect(planned.returnValue).toContain('remaining: 6') + 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 e4402c75d..add2df481 100644 --- a/services/minecraft/src/cognitive/conscious/js-planner.ts +++ b/services/minecraft/src/cognitive/conscious/js-planner.ts @@ -68,8 +68,11 @@ export interface RuntimeGlobals { mineflayer?: Mineflayer | null bot?: unknown actionQueue?: unknown + noActionBudget?: unknown currentInput?: unknown llmLog?: unknown + setNoActionBudget?: (value: number) => { ok: true, remaining: number, default: number, max: number } + getNoActionBudget?: () => { remaining: number, default: number, max: number } forgetConversation?: () => { ok: true, cleared: string[] } llmInput?: { systemPrompt: string @@ -214,6 +217,9 @@ export class JavaScriptPlanner { { name: 'currentInput', kind: 'object', readonly: true }, { name: 'llmLog', kind: 'object', readonly: true }, { name: 'actionQueue', kind: 'object', readonly: true }, + { name: 'noActionBudget', kind: 'object', readonly: true }, + { name: 'setNoActionBudget', kind: 'function', readonly: true }, + { name: 'getNoActionBudget', kind: 'function', readonly: true }, { name: 'forget_conversation', kind: 'function', readonly: true }, { name: 'llmMessages', kind: 'object', readonly: true }, { name: 'llmSystemPrompt', kind: 'string', readonly: true }, @@ -244,6 +250,7 @@ export class JavaScriptPlanner { currentInput: globals.currentInput ?? null, llmLog: globals.llmLog ?? null, actionQueue: globals.actionQueue ?? null, + noActionBudget: globals.noActionBudget ?? null, llmMessages: globals.llmInput?.messages ?? [], llmSystemPrompt: globals.llmInput?.systemPrompt ?? '', llmUserMessage: globals.llmInput?.userMessage ?? '', @@ -261,6 +268,8 @@ export class JavaScriptPlanner { expect: this.sandbox.expect, expectMoved: this.sandbox.expectMoved, expectNear: this.sandbox.expectNear, + setNoActionBudget: this.sandbox.setNoActionBudget, + getNoActionBudget: this.sandbox.getNoActionBudget, forget_conversation: this.sandbox.forget_conversation, } @@ -406,6 +415,7 @@ export class JavaScriptPlanner { const llmInput = deepFreeze(toStructuredClone(globals.llmInput ?? null)) const currentInput = deepFreeze(toStructuredClone(globals.currentInput ?? null)) const actionQueue = deepFreeze(toStructuredClone(globals.actionQueue ?? null)) + const noActionBudget = deepFreeze(toStructuredClone(globals.noActionBudget ?? null)) const query = globals.mineflayer ? createQueryRuntime(globals.mineflayer) : undefined this.sandbox.prevRun = this.sandbox.lastRun ?? null @@ -422,6 +432,9 @@ export class JavaScriptPlanner { this.sandbox.currentInput = currentInput this.sandbox.llmLog = globals.llmLog ?? null this.sandbox.actionQueue = actionQueue + this.sandbox.noActionBudget = noActionBudget + this.sandbox.setNoActionBudget = globals.setNoActionBudget ?? null + this.sandbox.getNoActionBudget = globals.getNoActionBudget ?? null this.sandbox.forget_conversation = globals.forgetConversation ?? null this.sandbox.llmMessages = llmInput?.messages ?? [] this.sandbox.llmSystemPrompt = llmInput?.systemPrompt ?? '' diff --git a/services/minecraft/src/cognitive/conscious/prompts/brain-prompt.md b/services/minecraft/src/cognitive/conscious/prompts/brain-prompt.md index dbe21baa6..89ad44845 100644 --- a/services/minecraft/src/cognitive/conscious/prompts/brain-prompt.md +++ b/services/minecraft/src/cognitive/conscious/prompts/brain-prompt.md @@ -18,8 +18,9 @@ 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`. + - Globals refreshed every turn: `snapshot`, `self`, `environment`, `social`, `threat`, `attention`, `autonomy`, `event`, `now`, `query`, `bot`, `mineflayer`, `currentInput`, `llmLog`, `actionQueue`, `noActionBudget`. - 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. - `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). @@ -102,6 +103,7 @@ Heuristic composition examples (encouraged): - `actionQueue.pending`: FIFO queued control actions waiting to run. - `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`). Examples: - `const recentErrors = llmLog.query().errors().latest(5).list()` @@ -115,7 +117,10 @@ Silent-eval pattern (strongly encouraged): - Turn A: `let blocksToMine = someFunc(); blocksToMine` - 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. -- A `no_actions` follow-up after an eval-only turn is normal; treat it as the handoff turn for action/reporting. +- A `no_actions` follow-up after an eval-only turn is normal; follow-ups are budgeted and can chain for multi-step reasoning. +- Default no-action follow-up budget is 3 and max is 8. +- Budget auto-resets when a player chat message is received. +- If budget is exhausted, either abandon this approach or explicitly adjust it with `setNoActionBudget(n)` for the current scenario. Value-first rule (mandatory for read -> action flows): - If a request depends on observed world/query data, first run an evaluation-only turn and end with the concrete value expression. @@ -125,9 +130,10 @@ Value-first rule (mandatory for read -> action flows): - Do not re-query the same read value in the follow-up turn; use the persisted value to avoid TOCTOU drift. - Avoid acting on unresolved intermediate variables when a concrete returned value can be verified first. - For explicit user tasks (e.g. "get X", "craft Y", "go to Z"), do not stay in repeated evaluation-only turns. -- After one evaluation turn, the next turn must either: +- 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. + - call `giveUp({ reason, cooldown_seconds })` 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` - Turn B: `const inv = prevRun.returnValue; const text = Array.isArray(inv) && inv.length ? inv.map(({ name, count }) => `${count} ${name}`).join(", ") : "nothing"; await chat({ message: `I have: ${text}`, feedback: false })` 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 3bb3e1669..c79e7f1dd 100644 --- a/services/minecraft/src/cognitive/conscious/prompts/brain-prompt.test.ts +++ b/services/minecraft/src/cognitive/conscious/prompts/brain-prompt.test.ts @@ -25,8 +25,12 @@ describe('generateBrainSystemPrompt', () => { expect(prompt).toContain('Silent-eval pattern') expect(prompt).toContain('Value-first rule') expect(prompt).toContain('forget_conversation()') + expect(prompt).toContain('setNoActionBudget(n)') + expect(prompt).toContain('getNoActionBudget()') + expect(prompt).toContain('noActionBudget') 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') }) })