diff --git a/agent.js b/agent.js index 672463d..5dda00a 100644 --- a/agent.js +++ b/agent.js @@ -3,18 +3,19 @@ import { sendRequest } from './utils/gpt.js'; import { History } from './utils/history.js'; import { Coder } from './utils/coder.js'; import { getQuery, containsQuery } from './utils/queries.js'; -import { containsCodeBlock } from './utils/skill_library.js'; +import { containsCodeBlock } from './utils/skill-library.js'; import { Events } from './utils/events.js'; export class Agent { - constructor(name, save_path, restart_memory=false) { + constructor(name, save_path, clear_memory=false, autostart=false) { this.name = name; this.bot = initBot(name); this.history = new History(this, save_path); + this.history.loadExamples(); this.coder = new Coder(this); - if (!restart_memory) { + if (!clear_memory) { this.history.load(); } @@ -23,25 +24,21 @@ export class Agent { this.bot.on('login', () => { this.bot.chat('Hello world! I am ' + this.name); console.log(`${this.name} logged in.`); + + this.bot.on('chat', (username, message) => { + if (username === this.name) return; + console.log('received message from', username, ':', message); + + this.history.add(username, message); + this.handleMessage(); + }); + + if (autostart) + this.respond('system', 'Agent process restarted. Notify the user and decide what to do.'); + }); } - async start() { - await this.history.loadExamples(); - - this.bot.on('chat', (username, message) => { - if (username === this.name) return; - console.log('received message from', username, ':', message); - - this.history.add(username, message); - this.handleMessage(); - }); - - if (this.history.default) { - this.executeCode(this.history.default); - } - } - async executeCode(code, triesRemaining=5) { if (code == 'default') code = this.history.default; diff --git a/controller/agent-process.js b/controller/agent-process.js new file mode 100644 index 0000000..53fdf1b --- /dev/null +++ b/controller/agent-process.js @@ -0,0 +1,39 @@ +import { spawn } from 'child_process'; + +export class AgentProcess { + constructor(name) { + this.name = name; + } + start(clear_memory=false, autostart=false) { + let args = ['controller/init-agent.js', this.name]; + if (clear_memory) + args.push('-c'); + if (autostart) + args.push('-a'); + + const agentProcess = spawn('node', args, { + stdio: 'inherit', + stderr: 'inherit', + }); + + let last_restart = Date.now(); + agentProcess.on('exit', (code, signal) => { + console.log(`Agent process exited with code ${code} and signal ${signal}`); + + if (code !== 0) { + // agent must run for at least 30 seconds before restarting + if (Date.now() - last_restart < 30 * 1000) { + console.error('Agent process exited too quickly. Killing entire process. Goodbye.'); + process.exit(1); + } + console.log('Restarting agent...'); + this.start(false, true); + last_restart = Date.now(); + } + }); + + agentProcess.on('error', (err) => { + console.error('Failed to start agent process:', err); + }); + } +} \ No newline at end of file diff --git a/controller/init-agent.js b/controller/init-agent.js new file mode 100644 index 0000000..e632064 --- /dev/null +++ b/controller/init-agent.js @@ -0,0 +1,27 @@ +import { Agent } from '../agent.js'; +import yargs from 'yargs'; + +const args = process.argv.slice(2); +if (args.length < 1) { + console.log('Usage: node init_agent.js [-c] [-a]'); + process.exit(1); +} + +const argv = yargs(args) + .option('clear_memory', { + alias: 'c', + type: 'boolean', + description: 'restart memory from scratch' + }) + .option('autostart', { + alias: 'a', + type: 'boolean', + description: 'automatically prompt the agent on startup' + }).argv + +const name = argv._[0]; +const clear_memory = !!argv.clear_memory; +const autostart = !!argv.autostart; +const save_path = './bots/'+name+'.json'; + +new Agent(name, save_path, clear_memory, autostart); diff --git a/main.js b/main.js index 7d56f81..ea78372 100644 --- a/main.js +++ b/main.js @@ -1,4 +1,3 @@ -import { Agent } from './agent.js'; +import { AgentProcess } from './controller/agent-process.js'; -let agent = new Agent('andy', './bots/andy.json', true); -agent.start(); +new AgentProcess('andy').start(true, false); \ No newline at end of file diff --git a/package.json b/package.json index 401e918..39ea3ce 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "mineflayer-pathfinder": "^2.4.4", "mineflayer-pvp": "^1.3.2", "openai": "^4.4.0", - "patch-package": "^8.0.0" + "patch-package": "^8.0.0", + "yargs": "^17.7.2" } } diff --git a/utils/coder.js b/utils/coder.js index b80c026..f6040d1 100644 --- a/utils/coder.js +++ b/utils/coder.js @@ -11,6 +11,7 @@ export class Coder { this.executing = false; this.agent.bot.output = ''; this.code_template = ''; + this.timedout = false; readFile(this.fp+'template.js', 'utf8', (err, data) => { if (err) throw err; @@ -47,10 +48,10 @@ export class Coder { } - // returns {success: bool, message: string, interrupted: bool} + // returns {success: bool, message: string, interrupted: bool, timedout: false} async execute() { - if (!this.queued_code) return {success: false, message: "No code to execute.", interrupted: false}; - if (!this.code_template) return {success: false, message: "Code template not loaded.", interrupted: false}; + if (!this.queued_code) return {success: false, message: "No code to execute.", interrupted: false, timedout: false}; + if (!this.code_template) return {success: false, message: "Code template not loaded.", interrupted: false, timedout: false}; let src = ''; let code = this.queued_code; @@ -80,9 +81,9 @@ export class Coder { if (write_result) { console.error('Error writing code execution file: ' + result); - return {success: false, message: result, interrupted: false}; + return {success: false, message: result, interrupted: false, timedout: false}; } - + let TIMEOUT; try { console.log('executing code...\n'); let execution_file = await import('.'+filename); @@ -90,28 +91,33 @@ export class Coder { this.current_code = this.queued_code; this.executing = true; - await execution_file.main(this.agent.bot); + TIMEOUT = this._startTimeout(10); + await execution_file.main(this.agent.bot); // open fire this.executing = false; + clearTimeout(TIMEOUT); this.agent.bot.emit('finished_executing'); let output = this.formatOutput(this.agent.bot); let interrupted = this.agent.bot.interrupt_code; + let timedout = this.timedout; this.clear(); - return {success:true, message: output, interrupted}; + return {success:true, message: output, interrupted, timedout}; } catch (err) { this.executing = false; + clearTimeout(TIMEOUT); + this.agent.bot.emit('finished_executing'); - console.error("Code execution triggered catch:" + err); + console.error("Code execution triggered catch: " + err); let message = this.formatOutput(this.agent.bot); message += '!!Code threw exception!! Error: ' + err; let interrupted = this.agent.bot.interrupt_code; await this.stop(); - return {success: false, message, interrupted}; + return {success: false, message, interrupted, timedout: false}; } } formatOutput(bot) { - if (bot.interrupt_code) return ''; + if (bot.interrupt_code && !this.timedout) return ''; let output = bot.output; const MAX_OUT = 1000; if (output.length > MAX_OUT) { @@ -125,6 +131,7 @@ export class Coder { } async stop() { + if (!this.executing) return; while (this.executing) { this.agent.bot.interrupt_code = true; this.agent.bot.collectBlock.cancelTask(); @@ -140,5 +147,26 @@ export class Coder { this.current_code = ''; this.agent.bot.output = ''; this.agent.bot.interrupt_code = false; + this.timedout = false; + } + + _startTimeout(TIMEOUT_MINS=10) { + return setTimeout(async () => { + console.warn(`Code execution timed out after ${TIMEOUT_MINS} minutes. Attempting force stop.`); + this.timedout = true; + this.agent.bot.output += `\nAction performed for ${TIMEOUT_MINS} minutes and then timed out and stopped. You may want to continue or do something else.`; + this.stop(); // last attempt to stop + await new Promise(resolve => setTimeout(resolve, 5 * 1000)); // wait 5 seconds + if (this.executing) { + console.error(`Failed to stop. Killing process. Goodbye.`); + this.agent.bot.output += `\nForce stop failed! Process was killed and will be restarted. Goodbye world.`; + this.agent.bot.chat('Goodbye world.'); + let output = this.formatOutput(this.agent.bot); + this.agent.history.add('system', output); + this.agent.history.save(); + process.exit(1); // force exit program + } + console.log('Code execution stopped successfully.'); + }, TIMEOUT_MINS*60*1000); } } \ No newline at end of file diff --git a/utils/history.js b/utils/history.js index b155a83..f1d4d60 100644 --- a/utils/history.js +++ b/utils/history.js @@ -1,6 +1,6 @@ import { writeFileSync, readFileSync, mkdirSync } from 'fs'; import { getQueryDocs } from './queries.js'; -import { getSkillDocs } from './skill_library.js'; +import { getSkillDocs } from './skill-library.js'; import { sendRequest, embed, cosineSimilarity } from './gpt.js'; diff --git a/utils/skill_library.js b/utils/skill-library.js similarity index 100% rename from utils/skill_library.js rename to utils/skill-library.js diff --git a/utils/skills.js b/utils/skills.js index c024b0d..2b119bc 100644 --- a/utils/skills.js +++ b/utils/skills.js @@ -57,13 +57,12 @@ export async function smeltItem(bot, itemName, num=1) { let furnaceBlock = undefined; furnaceBlock = getNearestBlock(bot, 'furnace', 6); - if (furnaceBlock === null){ + if (!furnaceBlock){ log(bot, `There is no furnace nearby.`) return false; } await bot.lookAt(furnaceBlock.position); - console.log('smelting...'); const furnace = await bot.openFurnace(furnaceBlock); // check if the furnace is already smelting something @@ -227,6 +226,10 @@ export async function collectBlock(bot, blockType, num=1) { * @example * await skills.collectBlock(bot, "oak_log"); **/ + if (num < 1) { + log(bot, `Invalid number of blocks to collect: ${num}.`); + return false; + } let collected = 0; const blocks = getNearestBlocks(bot, blockType, 64, num); if (blocks.length === 0) { @@ -255,7 +258,9 @@ export async function collectBlock(bot, blockType, num=1) { log(bot, `Failed to collect ${blockType}: ${err}.`); continue; } - } + } + if (bot.interrupt_code) + break; } log(bot, `Collected ${collected} ${blockType}.`); return true; @@ -565,7 +570,6 @@ export async function followPlayer(bot, username) { log(bot, `You are now actively following player ${username}.`); while (!bot.interrupt_code) { - console.log('followPlayer waiting for interrupt...', bot.interrupt_code); await new Promise(resolve => setTimeout(resolve, 1000)); }