diff --git a/services/minecraft/src/cognitive/action/llm-actions.ts b/services/minecraft/src/cognitive/action/llm-actions.ts index 90dd15ea2..d73307870 100644 --- a/services/minecraft/src/cognitive/action/llm-actions.ts +++ b/services/minecraft/src/cognitive/action/llm-actions.ts @@ -31,6 +31,7 @@ export const actionsList: Action[] = [ execution: 'sync', schema: z.object({ message: z.string().describe('The message to send in chat.'), + feedback: z.boolean().default(false).describe('Whether to emit FEEDBACK for this chat action. Keep false for normal conversation to avoid feedback loops.'), }), perform: mineflayer => (message: string): string => { mineflayer.bot.chat(message) diff --git a/services/minecraft/src/cognitive/conscious/brain.ts b/services/minecraft/src/cognitive/conscious/brain.ts index 913a0904f..e9efc6c1b 100644 --- a/services/minecraft/src/cognitive/conscious/brain.ts +++ b/services/minecraft/src/cognitive/conscious/brain.ts @@ -46,6 +46,8 @@ export class Brain { private currentCancellationToken: CancellationToken | undefined private giveUpUntil = 0 private giveUpReason: string | undefined + private lastHumanChatAt = 0 + private botUsername = '' private lastContextView: string | undefined private conversationHistory: Message[] = [] @@ -55,6 +57,7 @@ export class Brain { public init(bot: MineflayerWithAgents): void { this.deps.logger.log('INFO', 'Brain: Initializing stateful core...') + this.botUsername = bot.bot.username // Perception Handler this.deps.eventBus.subscribe('conscious:signal:*', (event: TracedEvent) => { @@ -70,6 +73,10 @@ export class Brain { this.deps.taskExecutor.on('action:completed', async ({ action, result }) => { this.deps.logger.log('INFO', `Brain: Action completed: ${action.tool}`) + if (action.tool === 'chat' && action.params?.feedback !== true) { + return + } + 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 @@ -148,6 +155,7 @@ export class Brain { // --- Cognitive Cycle --- private async processEvent(bot: MineflayerWithAgents, event: BotEvent): Promise { + this.updateHumanChatTimestamp(event) this.resumeFromGiveUpIfNeeded(event) if (this.shouldSuppressDuringGiveUp(event)) return @@ -254,6 +262,10 @@ export class Brain { this.deps.taskExecutor.getAvailableActions(), { event, snapshot: snapshot as unknown as Record }, async (action: ActionInstruction) => { + if (action.tool === 'chat' && !this.shouldAllowChatForEvent(event, snapshot.self.health)) { + return 'Chat suppressed: no direct user prompt for chat this turn' + } + const actionDef = actionDefs.get(action.tool) const isPhysicalAction = action.tool !== 'skip' && !actionDef?.readonly @@ -362,4 +374,35 @@ export class Brain { this.giveUpUntil = 0 this.giveUpReason = undefined } + + private shouldAllowChatForEvent(event: BotEvent, health: number): boolean { + if (health <= 8) + return true + + if (event.type !== 'perception') + return Date.now() - this.lastHumanChatAt <= 45000 && event.type === 'feedback' + + const signal = event.payload as PerceptionSignal + if (signal.type === 'chat_message') { + const speaker = typeof (signal.metadata as any)?.username === 'string' + ? String((signal.metadata as any).username) + : signal.sourceId + if (speaker === this.botUsername) + return false + return true + } + + return false + } + + private updateHumanChatTimestamp(event: BotEvent): void { + if (event.type !== 'perception') + return + + const signal = event.payload as PerceptionSignal + if (signal.type !== 'chat_message') + return + + this.lastHumanChatAt = Date.now() + } } diff --git a/services/minecraft/src/cognitive/conscious/prompts/brain-prompt.ts b/services/minecraft/src/cognitive/conscious/prompts/brain-prompt.ts index 6659242ce..3285ede55 100644 --- a/services/minecraft/src/cognitive/conscious/prompts/brain-prompt.ts +++ b/services/minecraft/src/cognitive/conscious/prompts/brain-prompt.ts @@ -143,5 +143,9 @@ Examples: - **Handling Feedback**: When you perform an action, you will see a \`[FEEDBACK]\` message in the history later with the result. Use this to verify success. - **Tool Choice**: If a dedicated tool exists for a task, use it. - **Skip Rule**: If you call \`skip()\`, do not call any other tool in the same turn. +- **Chat Discipline**: Do not send proactive small-talk. Use \`chat\` only when replying to a player chat, reporting meaningful task progress/failure, or urgent safety status. +- **No Harness Replies**: Never treat \`[PERCEPTION]\`, \`[FEEDBACK]\`, or other system wrappers as players. Only reply with \`chat\` to actual player \`chat_message\` events. +- **No Self Replies**: Never reply to your own previous bot messages. +- **Chat Feedback**: \`chat\` feedback is optional; keep \`feedback: false\` for normal conversation. Use \`feedback: true\` only when your next step explicitly needs the chat acknowledgement in history. ` }