diff --git a/src/agent/agent.js b/src/agent/agent.js index 5be2385..30715d8 100644 --- a/src/agent/agent.js +++ b/src/agent/agent.js @@ -4,6 +4,7 @@ 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 { TaskManager } from './tasks.js'; import { NPCContoller } from './npc/controller.js'; import { MemoryBank } from './memory_bank.js'; import { SelfPrompter } from './self_prompter.js'; @@ -13,6 +14,7 @@ import settings from '../../settings.js'; export class Agent { async start(profile_fp, load_mem=false, init_message=null, count_id=0) { + this.tasks = new TaskManager(this); this.prompter = new Prompter(this, profile_fp); this.name = this.prompter.getName(); this.history = new History(this); @@ -40,7 +42,7 @@ export class Agent { await new Promise((resolve) => setTimeout(resolve, 1000)); console.log(`${this.name} spawned.`); - this.coder.clear(); + this.clearBotLogs(); const ignore_messages = [ "Set own game mode to", @@ -91,6 +93,17 @@ export class Agent { }); } + interruptBot() { + this.bot.interrupt_code = true; + this.bot.collectBlock.cancelTask(); + this.bot.pathfinder.stop(); + this.bot.pvp.stop(); + } + + clearBotLogs() { + this.bot.output = ''; + this.bot.interrupt_code = false; + } async cleanChat(message, translate_up_to=-1) { let to_translate = message; @@ -250,8 +263,8 @@ export class Agent { this.cleanKill('Bot disconnected! Killing agent process.'); }); this.bot.on('death', () => { - this.coder.cancelResume(); - this.coder.stop(); + this.tasks.cancelResume(); + this.tasks.stop(); }); this.bot.on('kicked', (reason) => { console.warn('Bot kicked!', reason); @@ -267,7 +280,7 @@ export class Agent { this.bot.clearControlStates(); this.bot.pathfinder.stop(); // clear any lingering pathfinder this.bot.modes.unPauseAll(); - this.coder.executeResume(); + this.tasks.resumeTask(); }); // Init NPC controller @@ -297,7 +310,7 @@ export class Agent { } isIdle() { - return !this.coder.executing && !this.coder.generating; + return !this.tasks.executing && !this.coder.generating; } cleanKill(msg='Killing agent process...') { diff --git a/src/agent/coder.js b/src/agent/coder.js index d312387..e1179d3 100644 --- a/src/agent/coder.js +++ b/src/agent/coder.js @@ -7,11 +7,8 @@ export class Coder { this.agent = agent; this.file_counter = 0; this.fp = '/bots/'+agent.name+'/action-code/'; - this.executing = false; this.generating = false; this.code_template = ''; - this.timedout = false; - this.cur_action_name = ''; readFile('./bots/template.js', 'utf8', (err, data) => { if (err) throw err; @@ -83,7 +80,7 @@ export class Coder { async generateCode(agent_history) { // wrapper to prevent overlapping code generation loops - await this.stop(); + await this.agent.tasks.stop(); this.generating = true; let res = await this.generateCodeLoop(agent_history); this.generating = false; @@ -119,7 +116,7 @@ export class Coder { } if (failures >= 3) { - return {success: false, message: 'Action failed, agent would not write code.', interrupted: false, timedout: false}; + return { success: false, message: 'Action failed, agent would not write code.', interrupted: false, timedout: false }; } messages.push({ role: 'system', @@ -137,22 +134,22 @@ export class Coder { continue; } - const execution_file = await this.stageCode(code); - if (!execution_file) { + const executionModuleExports = await this.stageCode(code); + if (!executionModuleExports) { agent_history.add('system', 'Failed to stage code, something is wrong.'); return {success: false, message: null, interrupted: false, timedout: false}; } - code_return = await this.execute(async ()=>{ - return await execution_file.main(this.agent.bot); - }, settings.code_timeout_mins); + code_return = await this.agent.tasks.runTask('newAction', async () => { + return await executionModuleExports.main(this.agent.bot); + }, { timeout: settings.code_timeout_mins }); if (code_return.interrupted && !code_return.timedout) - return {success: false, message: null, interrupted: true, timedout: false}; + return { success: false, message: null, interrupted: true, timedout: false }; console.log("Code generation result:", code_return.success, code_return.message); if (code_return.success) { const summary = "Summary of newAction\nAgent wrote this code: \n```" + this.sanitizeCode(code) + "```\nCode Output:\n" + code_return.message; - return {success: true, message: summary, interrupted: false, timedout: false}; + return { success: true, message: summary, interrupted: false, timedout: false }; } messages.push({ @@ -164,114 +161,7 @@ export class Coder { content: code_return.message + '\nCode failed. Please try again:' }); } - return {success: false, message: null, interrupted: false, timedout: true}; + return { success: false, message: null, interrupted: false, timedout: true }; } - async executeResume(func=null, timeout=10) { - const new_resume = func != null; - if (new_resume) { // start new resume - this.resume_func = func; - this.resume_name = this.cur_action_name; - } - if (this.resume_func != null && this.agent.isIdle() && (!this.agent.self_prompter.on || new_resume)) { - this.cur_action_name = this.resume_name; - let res = await this.execute(this.resume_func, timeout); - this.cur_action_name = ''; - return res; - } else { - return {success: false, message: null, interrupted: false, timedout: false}; - } - } - - cancelResume() { - this.resume_func = null; - this.resume_name = null; - } - - setCurActionName(name) { - this.cur_action_name = name.replace(/!/g, ''); - } - - // returns {success: bool, message: string, interrupted: bool, timedout: false} - async execute(func, timeout=10) { - if (!this.code_template) return {success: false, message: "Code template not loaded.", interrupted: false, timedout: false}; - - let TIMEOUT; - try { - console.log('executing code...\n'); - await this.stop(); - this.clear(); - - this.executing = true; - if (timeout > 0) - TIMEOUT = this._startTimeout(timeout); - await func(); // open fire - this.executing = false; - clearTimeout(TIMEOUT); - - let output = this.formatOutput(this.agent.bot); - let interrupted = this.agent.bot.interrupt_code; - let timedout = this.timedout; - this.clear(); - if (!interrupted && !this.generating) this.agent.bot.emit('idle'); - return {success:true, message: output, interrupted, timedout}; - } catch (err) { - this.executing = false; - clearTimeout(TIMEOUT); - this.cancelResume(); - console.error("Code execution triggered catch: " + err); - await this.stop(); - - let message = this.formatOutput(this.agent.bot) + '!!Code threw exception!! Error: ' + err; - let interrupted = this.agent.bot.interrupt_code; - this.clear(); - if (!interrupted && !this.generating) this.agent.bot.emit('idle'); - return {success: false, message, interrupted, timedout: false}; - } - } - - formatOutput(bot) { - if (bot.interrupt_code && !this.timedout) return ''; - let output = bot.output; - const MAX_OUT = 500; - if (output.length > MAX_OUT) { - output = `Code output is very long (${output.length} chars) and has been shortened.\n - First outputs:\n${output.substring(0, MAX_OUT/2)}\n...skipping many lines.\nFinal outputs:\n ${output.substring(output.length - MAX_OUT/2)}`; - } - else { - output = 'Code output:\n' + output; - } - return output; - } - - async stop() { - if (!this.executing) return; - const start = Date.now(); - while (this.executing) { - this.agent.bot.interrupt_code = true; - this.agent.bot.collectBlock.cancelTask(); - this.agent.bot.pathfinder.stop(); - this.agent.bot.pvp.stop(); - console.log('waiting for code to finish executing...'); - await new Promise(resolve => setTimeout(resolve, 1000)); - if (Date.now() - start > 10 * 1000) { - this.agent.cleanKill('Code execution refused stop after 10 seconds. Killing process.'); - } - } - } - - clear() { - 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.history.add('system', `Code execution timed out after ${TIMEOUT_MINS} minutes. Attempting force stop.`); - await this.stop(); // last attempt to stop - }, TIMEOUT_MINS*60*1000); - } } \ No newline at end of file diff --git a/src/agent/commands/actions.js b/src/agent/commands/actions.js index a80ad29..d8e0cfe 100644 --- a/src/agent/commands/actions.js +++ b/src/agent/commands/actions.js @@ -1,17 +1,12 @@ import * as skills from '../library/skills.js'; import settings from '../../../settings.js'; -function wrapExecution(func, resume=false, timeout=-1) { +function runAsTask (taskLabel, taskFn, resume = false, timeout = -1) { return async function (agent, ...args) { - let code_return; - const wrappedFunction = async () => { - await func(agent, ...args); + const taskFnWithAgent = async () => { + await taskFn(agent, ...args); }; - if (resume) { - code_return = await agent.coder.executeResume(wrappedFunction, timeout); - } else { - code_return = await agent.coder.execute(wrappedFunction, timeout); - } + const code_return = await agent.tasks.runTask(`action:${taskLabel}`, taskFnWithAgent, { timeout, resume }); if (code_return.interrupted && !code_return.timedout) return; return code_return.message; @@ -36,9 +31,9 @@ export const actionsList = [ name: '!stop', description: 'Force stop all actions and commands that are currently executing.', perform: async function (agent) { - await agent.coder.stop(); - agent.coder.clear(); - agent.coder.cancelResume(); + await agent.tasks.stop(); + agent.clearBotLogs(); + agent.tasks.cancelResume(); agent.bot.emit('idle'); let msg = 'Agent stopped.'; if (agent.self_prompter.on) @@ -78,7 +73,7 @@ export const actionsList = [ 'player_name': {type: 'string', description: 'The name of the player to go to.'}, 'closeness': {type: 'float', description: 'How close to get to the player.', domain: [0, Infinity]} }, - perform: wrapExecution(async (agent, player_name, closeness) => { + perform: runAsTask('goToPlayer', async (agent, player_name, closeness) => { return await skills.goToPlayer(agent.bot, player_name, closeness); }) }, @@ -89,7 +84,7 @@ export const actionsList = [ 'player_name': {type: 'string', description: 'name of the player to follow.'}, 'follow_dist': {type: 'float', description: 'The distance to follow from.', domain: [0, Infinity]} }, - perform: wrapExecution(async (agent, player_name, follow_dist) => { + perform: runAsTask('followPlayer', async (agent, player_name, follow_dist) => { await skills.followPlayer(agent.bot, player_name, follow_dist); }, true) }, @@ -101,7 +96,7 @@ export const actionsList = [ 'closeness': { type: 'float', description: 'How close to get to the block.', domain: [0, Infinity] }, 'search_range': { type: 'float', description: 'The distance to search for the block.', domain: [0, Infinity] } }, - perform: wrapExecution(async (agent, type, closeness, range) => { + perform: runAsTask('goToBlock', async (agent, type, closeness, range) => { await skills.goToNearestBlock(agent.bot, type, closeness, range); }) }, @@ -109,7 +104,7 @@ export const actionsList = [ name: '!moveAway', description: 'Move away from the current location in any direction by a given distance.', params: {'distance': { type: 'float', description: 'The distance to move away.', domain: [0, Infinity] }}, - perform: wrapExecution(async (agent, distance) => { + perform: runAsTask('moveAway', async (agent, distance) => { await skills.moveAway(agent.bot, distance); }) }, @@ -127,11 +122,11 @@ export const actionsList = [ name: '!goToPlace', description: 'Go to a saved location.', params: {'name': { type: 'string', description: 'The name of the location to go to.' }}, - perform: wrapExecution(async (agent, name) => { + perform: runAsTask('goToPlace', async (agent, name) => { const pos = agent.memory_bank.recallPlace(name); if (!pos) { - skills.log(agent.bot, `No location named "${name}" saved.`); - return; + skills.log(agent.bot, `No location named "${name}" saved.`); + return; } await skills.goToPosition(agent.bot, pos[0], pos[1], pos[2], 1); }) @@ -144,7 +139,7 @@ export const actionsList = [ 'item_name': { type: 'ItemName', description: 'The name of the item to give.' }, 'num': { type: 'int', description: 'The number of items to give.', domain: [1, Number.MAX_SAFE_INTEGER] } }, - perform: wrapExecution(async (agent, player_name, item_name, num) => { + perform: runAsTask('givePlayer', async (agent, player_name, item_name, num) => { await skills.giveToPlayer(agent.bot, item_name, player_name, num); }) }, @@ -152,7 +147,7 @@ export const actionsList = [ name: '!consume', description: 'Eat/drink the given item.', params: {'item_name': { type: 'ItemName', description: 'The name of the item to consume.' }}, - perform: wrapExecution(async (agent, item_name) => { + perform: runAsTask('consume', async (agent, item_name) => { await agent.bot.consume(item_name); skills.log(agent.bot, `Consumed ${item_name}.`); }) @@ -161,7 +156,7 @@ export const actionsList = [ name: '!equip', description: 'Equip the given item.', params: {'item_name': { type: 'ItemName', description: 'The name of the item to equip.' }}, - perform: wrapExecution(async (agent, item_name) => { + perform: runAsTask('equip', async (agent, item_name) => { await skills.equip(agent.bot, item_name); }) }, @@ -172,7 +167,7 @@ export const actionsList = [ 'item_name': { type: 'ItemName', description: 'The name of the item to put in the chest.' }, 'num': { type: 'int', description: 'The number of items to put in the chest.', domain: [1, Number.MAX_SAFE_INTEGER] } }, - perform: wrapExecution(async (agent, item_name, num) => { + perform: runAsTask('putInChest', async (agent, item_name, num) => { await skills.putInChest(agent.bot, item_name, num); }) }, @@ -183,7 +178,7 @@ export const actionsList = [ 'item_name': { type: 'ItemName', description: 'The name of the item to take.' }, 'num': { type: 'int', description: 'The number of items to take.', domain: [1, Number.MAX_SAFE_INTEGER] } }, - perform: wrapExecution(async (agent, item_name, num) => { + perform: runAsTask('takeFromChest', async (agent, item_name, num) => { await skills.takeFromChest(agent.bot, item_name, num); }) }, @@ -191,7 +186,7 @@ export const actionsList = [ name: '!viewChest', description: 'View the items/counts of the nearest chest.', params: { }, - perform: wrapExecution(async (agent) => { + perform: runAsTask('viewChest', async (agent) => { await skills.viewChest(agent.bot); }) }, @@ -202,7 +197,7 @@ export const actionsList = [ 'item_name': { type: 'ItemName', description: 'The name of the item to discard.' }, 'num': { type: 'int', description: 'The number of items to discard.', domain: [1, Number.MAX_SAFE_INTEGER] } }, - perform: wrapExecution(async (agent, item_name, num) => { + perform: runAsTask('discard', async (agent, item_name, num) => { const start_loc = agent.bot.entity.position; await skills.moveAway(agent.bot, 5); await skills.discard(agent.bot, item_name, num); @@ -216,7 +211,7 @@ export const actionsList = [ 'type': { type: 'BlockName', description: 'The block type to collect.' }, 'num': { type: 'int', description: 'The number of blocks to collect.', domain: [1, Number.MAX_SAFE_INTEGER] } }, - perform: wrapExecution(async (agent, type, num) => { + perform: runAsTask('collectBlocks', async (agent, type, num) => { await skills.collectBlock(agent.bot, type, num); }, false, 10) // 10 minute timeout }, @@ -226,10 +221,10 @@ export const actionsList = [ params: { 'type': { type: 'BlockName', description: 'The block type to collect.' } }, - perform: wrapExecution(async (agent, type) => { + perform: runAsTask('collectAllBlocks', async (agent, type) => { let success = await skills.collectBlock(agent.bot, type, 1); if (!success) - agent.coder.cancelResume(); + agent.tasks.cancelResume(); }, true, 3) // 3 minute timeout }, { @@ -239,7 +234,7 @@ export const actionsList = [ 'recipe_name': { type: 'ItemName', description: 'The name of the output item to craft.' }, 'num': { type: 'int', description: 'The number of times to craft the recipe. This is NOT the number of output items, as it may craft many more items depending on the recipe.', domain: [1, Number.MAX_SAFE_INTEGER] } }, - perform: wrapExecution(async (agent, recipe_name, num) => { + perform: runAsTask('craftRecipe', async (agent, recipe_name, num) => { await skills.craftRecipe(agent.bot, recipe_name, num); }) }, @@ -250,32 +245,29 @@ export const actionsList = [ 'item_name': { type: 'ItemName', description: 'The name of the input item to smelt.' }, 'num': { type: 'int', description: 'The number of times to smelt the item.', domain: [1, Number.MAX_SAFE_INTEGER] } }, - perform: async function (agent, item_name, num) { - let response = await wrapExecution(async (agent) => { - console.log('smelting item'); - return await skills.smeltItem(agent.bot, item_name, num); - })(agent); + perform: runAsTask('smeltItem', async (agent, item_name, num) => { + let response = await skills.smeltItem(agent.bot, item_name, num); if (response.indexOf('Successfully') !== -1) { - // there is a bug where the bot's inventory is not updated after smelting - // only updates after a restart - agent.cleanKill(response + ' Safely restarting to update inventory.'); + // there is a bug where the bot's inventory is not updated after smelting + // only updates after a restart + agent.cleanKill(response + ' Safely restarting to update inventory.'); } return response; - } - }, - { - name: '!clearFurnace', - description: 'Tak all items out of the nearest furnace.', - params: { }, - perform: wrapExecution(async (agent) => { - await skills.clearNearestFurnace(agent.bot); }) }, { + name: '!clearFurnace', + description: 'Take all items out of the nearest furnace.', + params: { }, + perform: runAsTask('clearFurnace', async (agent) => { + await skills.clearNearestFurnace(agent.bot); + }) + }, + { name: '!placeHere', description: 'Place a given block in the current location. Do NOT use to build structures, only use for single blocks/torches.', params: {'type': { type: 'BlockName', description: 'The block type to place.' }}, - perform: wrapExecution(async (agent, type) => { + perform: runAsTask('placeHere', async (agent, type) => { let pos = agent.bot.entity.position; await skills.placeBlock(agent.bot, type, pos.x, pos.y, pos.z); }) @@ -284,14 +276,14 @@ export const actionsList = [ name: '!attack', description: 'Attack and kill the nearest entity of a given type.', params: {'type': { type: 'string', description: 'The type of entity to attack.'}}, - perform: wrapExecution(async (agent, type) => { + perform: runAsTask('attack', async (agent, type) => { await skills.attackNearest(agent.bot, type, true); }) }, { name: '!goToBed', description: 'Go to the nearest bed and sleep.', - perform: wrapExecution(async (agent) => { + perform: runAsTask('goToBed', async (agent) => { await skills.goToBed(agent.bot); }) }, @@ -299,7 +291,7 @@ export const actionsList = [ name: '!activate', description: 'Activate the nearest object of a given type.', params: {'type': { type: 'BlockName', description: 'The type of object to activate.' }}, - perform: wrapExecution(async (agent, type) => { + perform: runAsTask('activate', async (agent, type) => { await skills.activateNearestBlock(agent.bot, type); }) }, @@ -307,7 +299,7 @@ export const actionsList = [ name: '!stay', description: 'Stay in the current location no matter what. Pauses all modes.', params: {'type': { type: 'int', description: 'The number of seconds to stay. -1 for forever.', domain: [-1, Number.MAX_SAFE_INTEGER] }}, - perform: wrapExecution(async (agent, seconds) => { + perform: runAsTask('stay', async (agent, seconds) => { await skills.stay(agent.bot, seconds); }) }, @@ -321,9 +313,9 @@ export const actionsList = [ perform: async function (agent, mode_name, on) { const modes = agent.bot.modes; if (!modes.exists(mode_name)) - return `Mode ${mode_name} does not exist.` + modes.getDocs(); + return `Mode ${mode_name} does not exist.` + modes.getDocs(); if (modes.isOn(mode_name) === on) - return `Mode ${mode_name} is already ${on ? 'on' : 'off'}.`; + return `Mode ${mode_name} is already ${on ? 'on' : 'off'}.`; modes.setOn(mode_name, on); return `Mode ${mode_name} is now ${on ? 'on' : 'off'}.`; } diff --git a/src/agent/commands/index.js b/src/agent/commands/index.js index 622808e..a5c0f42 100644 --- a/src/agent/commands/index.js +++ b/src/agent/commands/index.js @@ -207,7 +207,6 @@ export async function executeCommand(agent, message) { else { console.log('parsed command:', parsed); const command = getCommand(parsed.commandName); - const is_action = isAction(command.name); let numArgs = 0; if (parsed.args) { numArgs = parsed.args.length; @@ -215,11 +214,7 @@ export async function executeCommand(agent, message) { if (numArgs !== numParams(command)) return `Command ${command.name} was given ${numArgs} args, but requires ${numParams(command)} args.`; else { - if (is_action) - agent.coder.setCurActionName(command.name); const result = await command.perform(agent, ...parsed.args); - if (is_action) - agent.coder.setCurActionName(''); return result; } } diff --git a/src/agent/modes.js b/src/agent/modes.js index 2e47fb3..977d01d 100644 --- a/src/agent/modes.js +++ b/src/agent/modes.js @@ -260,9 +260,9 @@ async function execute(mode, agent, func, timeout=-1) { if (agent.self_prompter.on) agent.self_prompter.stopLoop(); mode.active = true; - let code_return = await agent.coder.execute(async () => { + let code_return = await agent.tasks.runTask(`mode:${mode.name}`, async () => { await func(); - }, timeout); + }, { timeout }); mode.active = false; console.log(`Mode ${mode.name} finished executing, code_return: ${code_return.message}`); } @@ -328,7 +328,7 @@ class ModeController { this.unPauseAll(); } for (let mode of this.modes_list) { - let interruptible = mode.interrupts.some(i => i === 'all') || mode.interrupts.some(i => i === this.agent.coder.cur_action_name); + let interruptible = mode.interrupts.some(i => i === 'all') || mode.interrupts.some(i => `action:${i}` === this.agent.tasks.currentTaskLabel); if (mode.on && !mode.paused && !mode.active && (this.agent.isIdle() || interruptible)) { await mode.update(this.agent); } diff --git a/src/agent/npc/build_goal.js b/src/agent/npc/build_goal.js index 3af1e88..e7199ee 100644 --- a/src/agent/npc/build_goal.js +++ b/src/agent/npc/build_goal.js @@ -13,7 +13,7 @@ export class BuildGoal { async wrapSkill(func) { if (!this.agent.isIdle()) return false; - let res = await this.agent.coder.execute(func); + let res = await this.agent.tasks.runTask('BuildGoal', func); return !res.interrupted; } diff --git a/src/agent/npc/controller.js b/src/agent/npc/controller.js index 18658b6..d7f213c 100644 --- a/src/agent/npc/controller.js +++ b/src/agent/npc/controller.js @@ -72,7 +72,7 @@ export class NPCContoller { if (!this.agent.isIdle()) return; // Persue goal - if (!this.agent.coder.resume_func) { + if (!this.agent.tasks.resume_func) { this.executeNext(); this.agent.history.save(); } @@ -104,7 +104,7 @@ export class NPCContoller { async executeNext() { if (!this.agent.isIdle()) return; - await this.agent.coder.execute(async () => { + await this.agent.tasks.runTask('npc:moveAway', async () => { await skills.moveAway(this.agent.bot, 2); }); @@ -114,7 +114,7 @@ export class NPCContoller { if (building == this.data.home) { let door_pos = this.getBuildingDoor(building); if (door_pos) { - await this.agent.coder.execute(async () => { + await this.agent.tasks.runTask('npc:exitBuilding', async () => { await skills.useDoor(this.agent.bot, door_pos); await skills.moveAway(this.agent.bot, 2); // If the bot is too close to the building it will try to enter again }); @@ -132,13 +132,13 @@ export class NPCContoller { let building = this.currentBuilding(); if (this.data.home !== null && (building === null || building != this.data.home)) { let door_pos = this.getBuildingDoor(this.data.home); - await this.agent.coder.execute(async () => { + await this.agent.tasks.runTask('npc:returnHome', async () => { await skills.useDoor(this.agent.bot, door_pos); }); } // Go to bed - await this.agent.coder.execute(async () => { + await this.agent.tasks.runTask('npc:bed', async () => { await skills.goToBed(this.agent.bot); }); } diff --git a/src/agent/npc/item_goal.js b/src/agent/npc/item_goal.js index 0550657..9d67e7d 100644 --- a/src/agent/npc/item_goal.js +++ b/src/agent/npc/item_goal.js @@ -322,7 +322,7 @@ export class ItemGoal { // If the bot has failed to obtain the block before, explore if (this.failed.includes(next.name)) { this.failed = this.failed.filter((item) => item !== next.name); - await this.agent.coder.execute(async () => { + await this.agent.tasks.runTask('itemGoal:explore', async () => { await skills.moveAway(this.agent.bot, 8); }); } else { @@ -339,7 +339,7 @@ export class ItemGoal { // Execute the next goal let init_quantity = world.getInventoryCounts(this.agent.bot)[next.name] || 0; - await this.agent.coder.execute(async () => { + await this.agent.tasks.runTask('itemGoal:next', async () => { await next.execute(quantity); }); let final_quantity = world.getInventoryCounts(this.agent.bot)[next.name] || 0; diff --git a/src/agent/self_prompter.js b/src/agent/self_prompter.js index a6eedfe..4f9d885 100644 --- a/src/agent/self_prompter.js +++ b/src/agent/self_prompter.js @@ -87,7 +87,7 @@ export class SelfPrompter { async stop(stop_action=true) { this.interrupt = true; if (stop_action) - await this.agent.coder.stop(); + await this.agent.tasks.stop(); await this.stopLoop(); this.on = false; } diff --git a/src/agent/tasks.js b/src/agent/tasks.js new file mode 100644 index 0000000..ab02bd3 --- /dev/null +++ b/src/agent/tasks.js @@ -0,0 +1,148 @@ +export class TaskManager { + constructor(agent) { + this.agent = agent; + this.executing = false; + this.currentTaskLabel = ''; + this.currentTaskFn = null; + this.timedout = false; + this.resume_func = null; + this.resume_name = ''; + } + + async resumeTask(taskFn, timeout) { + return this._executeResume(taskFn, timeout); + } + + async runTask(taskLabel, taskFn, { timeout, resume = false } = {}) { + if (resume) { + return this._executeResume(taskFn, timeout); + } else { + return this._executeTask(taskLabel, taskFn, timeout); + } + } + + async stop() { + if (!this.executing) return; + console.trace(); + const start = Date.now(); + while (this.executing) { + this.agent.interruptBot(); + console.log('waiting for code to finish executing...'); + await new Promise(resolve => setTimeout(resolve, 1000)); + if (Date.now() - start > 10 * 1000) { + this.agent.cleanKill('Code execution refused stop after 10 seconds. Killing process.'); + } + } + } + + cancelResume() { + this.resume_func = null; + this.resume_name = null; + } + + async _executeResume(taskFn = null, timeout = 10) { + const new_resume = taskFn != null; + if (new_resume) { // start new resume + this.resume_func = taskFn; + this.resume_name = this.currentTaskLabel; + } + if (this.resume_func != null && this.agent.isIdle() && (!this.agent.self_prompter.on || new_resume)) { + this.currentTaskLabel = this.resume_name; + let res = await this._executeTask(this.resume_name, this.resume_func, timeout); + this.currentTaskLabel = ''; + return res; + } else { + return { success: false, message: null, interrupted: false, timedout: false }; + } + } + + async _executeTask(taskLabel, taskFn, timeout = 10) { + let TIMEOUT; + try { + console.log('executing code...\n'); + + // await current task to finish (executing=false), with 10 seconds timeout + // also tell agent.bot to stop various actions + if (this.executing) { + console.log(`new task "${taskLabel}" trying to interrupt current task "${this.currentTaskLabel}"`); + } + await this.stop(); + + // clear bot logs and reset interrupt code + this.agent.clearBotLogs(); + + this.executing = true; + this.currentTaskLabel = taskLabel; + this.currentTaskFn = taskFn; + + // timeout in minutes + if (timeout > 0) { + TIMEOUT = this._startTimeout(timeout); + } + + // start the task + await taskFn(); + + // mark task as finished + cleanup + this.executing = false; + this.currentTaskLabel = ''; + this.currentTaskFn = null; + clearTimeout(TIMEOUT); + + // get bot activity summary + let output = this._getBotOutputSummary(); + let interrupted = this.agent.bot.interrupt_code; + let timedout = this.timedout; + this.agent.clearBotLogs(); + + // if not interrupted and not generating, emit idle event + if (!interrupted && !this.agent.coder.generating) { + this.agent.bot.emit('idle'); + } + + // return task status report + return { success: true, message: output, interrupted, timedout }; + } catch (err) { + this.executing = false; + this.currentTaskLabel = ''; + this.currentTaskFn = null; + clearTimeout(TIMEOUT); + this.cancelResume(); + console.error("Code execution triggered catch: " + err); + await this.stop(); + + let message = this._getBotOutputSummary() + '!!Code threw exception!! Error: ' + err; + let interrupted = this.agent.bot.interrupt_code; + this.agent.clearBotLogs(); + if (!interrupted && !this.agent.coder.generating) { + this.agent.bot.emit('idle'); + } + return { success: false, message, interrupted, timedout: false }; + } + } + + _getBotOutputSummary() { + const { bot } = this.agent; + if (bot.interrupt_code && !this.timedout) return ''; + let output = bot.output; + const MAX_OUT = 500; + if (output.length > MAX_OUT) { + output = `Code output is very long (${output.length} chars) and has been shortened.\n + First outputs:\n${output.substring(0, MAX_OUT / 2)}\n...skipping many lines.\nFinal outputs:\n ${output.substring(output.length - MAX_OUT / 2)}`; + } + else { + output = 'Code output:\n' + output; + } + return output; + } + + _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.history.add('system', `Code execution timed out after ${TIMEOUT_MINS} minutes. Attempting force stop.`); + await this.stop(); // last attempt to stop + }, TIMEOUT_MINS * 60 * 1000); + } + +} \ No newline at end of file