import { History } from './history.js'; import { Coder } from './coder.js'; import { Prompter } from './prompter.js'; import { initModes } from './modes.js'; import { initBot } from '../utils/mcdata.js'; import { containsCommand, commandExists, executeCommand, truncCommandMessage, isAction } from './commands/index.js'; import { NPCContoller } from './npc/controller.js'; import { MemoryBank } from './memory_bank.js'; import { SelfPrompter } from './self_prompter.js'; export class Agent { async start(profile_fp, load_mem=false, init_message=null) { this.prompter = new Prompter(this, profile_fp); this.name = this.prompter.getName(); this.history = new History(this); this.coder = new Coder(this); this.npc = new NPCContoller(this); this.memory_bank = new MemoryBank(); this.self_prompter = new SelfPrompter(this); await this.prompter.initExamples(); console.log('Logging in...'); this.bot = initBot(this.name); initModes(this); if (load_mem) this.history.load(); this.bot.once('spawn', async () => { // wait for a bit so stats are not undefined await new Promise((resolve) => setTimeout(resolve, 1000)); console.log(`${this.name} spawned.`); this.coder.clear(); const ignore_messages = [ "Set own game mode to", "Set the time to", "Set the difficulty to", "Teleported ", "Set the weather to", "Gamerule " ]; this.bot.on('chat', (username, message) => { if (username === this.name) return; if (ignore_messages.some((m) => message.startsWith(m))) return; console.log('received message from', username, ':', message); this.handleMessage(username, message); }); // set the bot to automatically eat food when hungry this.bot.autoEat.options = { priority: 'foodPoints', startAt: 14, bannedFood: ["rotten_flesh", "spider_eye", "poisonous_potato", "pufferfish", "chicken"] }; if (init_message) { this.handleMessage('system', init_message, true); } else { this.bot.chat('Hello world! I am ' + this.name); this.bot.emit('finished_executing'); } this.startEvents(); }); } cleanChat(message) { // newlines are interpreted as separate chats, which triggers spam filters. replace them with spaces message = message.replaceAll('\n', ' '); return this.bot.chat(message); } async handleMessage(source, message, self_prompt=false) { if (!!source && !!message) await this.history.add(source, message); let used_command = false; if (source !== 'system' && source !== this.name && !self_prompt) { const user_command_name = containsCommand(message); if (user_command_name) { if (!commandExists(user_command_name)) { this.bot.chat(`Command '${user_command_name}' does not exist.`); return false; } this.bot.chat(`*${source} used ${user_command_name.substring(1)}*`); let execute_res = await executeCommand(this, message); if (user_command_name === '!newAction') { // all user initiated commands are ignored by the bot except for this one // add the preceding message to the history to give context for newAction let truncated_msg = message.substring(0, message.indexOf(user_command_name)).trim(); this.history.add(source, truncated_msg); } if (execute_res) this.cleanChat(execute_res); return true; } } let MAX_ATTEMPTS = 5; if (!self_prompt && this.self_prompter.on) MAX_ATTEMPTS = 1; // immediately respond to this message, then let self-prompting take over for (let i=0; i 0) // chat_message = `${pre_message} ${chat_message}`; this.cleanChat(res); if (self_prompt && this.self_prompter.on && this.self_prompter.interrupt) break; if (isAction(command_name) && !self_prompt && this.self_prompter.on) { this.self_prompter.stopLoop(); // so agent doesn't respond from self-prompting loop // will be automatically restarted by self-prompter } let execute_res = await executeCommand(this, res); console.log('Agent executed:', command_name, 'and got:', execute_res); used_command = true; if (execute_res) this.history.add('system', execute_res); else break; } else { // conversation response this.history.add(this.name, res); this.cleanChat(res); console.log('Purely conversational response:', res); break; } } this.history.save(); this.bot.emit('finished_executing'); return used_command; } startEvents() { // Custom events this.bot.on('time', () => { if (this.bot.time.timeOfDay == 0) this.bot.emit('sunrise'); else if (this.bot.time.timeOfDay == 6000) this.bot.emit('noon'); else if (this.bot.time.timeOfDay == 12000) this.bot.emit('sunset'); else if (this.bot.time.timeOfDay == 18000) this.bot.emit('midnight'); }); let prev_health = this.bot.health; this.bot.lastDamageTime = 0; this.bot.lastDamageTaken = 0; this.bot.on('health', () => { if (this.bot.health < prev_health) { this.bot.lastDamageTime = Date.now(); this.bot.lastDamageTaken = prev_health - this.bot.health; } prev_health = this.bot.health; }); // Logging callbacks this.bot.on('error' , (err) => { console.error('Error event!', err); }); this.bot.on('end', (reason) => { console.warn('Bot disconnected! Killing agent process.', reason) process.exit(1); }); this.bot.on('death', () => { this.coder.cancelResume(); this.coder.stop(); }); this.bot.on('kicked', (reason) => { console.warn('Bot kicked!', reason); process.exit(1); }); this.bot.on('messagestr', async (message, _, jsonMsg) => { if (jsonMsg.translate && jsonMsg.translate.startsWith('death') && message.startsWith(this.name)) { console.log('Agent died: ', message); this.handleMessage('system', `You died with the final message: '${message}'. Previous actions were stopped and you have respawned. Notify the user and perform any necessary actions.`); } }); this.bot.on('idle', () => { this.bot.clearControlStates(); this.bot.pathfinder.stop(); // clear any lingering pathfinder this.bot.modes.unPauseAll(); this.coder.executeResume(); }); // Init NPC controller this.npc.init(); // This update loop ensures that each update() is called one at a time, even if it takes longer than the interval const INTERVAL = 300; let last = Date.now(); setTimeout(async () => { while (true) { let start = Date.now(); await this.update(start - last); let remaining = INTERVAL - (Date.now() - start); if (remaining > 0) { await new Promise((resolve) => setTimeout(resolve, remaining)); } last = start; } }, INTERVAL); this.bot.emit('idle'); } async update(delta) { await this.bot.modes.update(); await this.self_prompter.update(delta); } isIdle() { return !this.coder.executing && !this.coder.generating; } }