major refactor: use mindserver to init settings

This commit is contained in:
MaxRobinsonTheGreat 2025-06-10 17:52:30 -05:00
parent 6f2bf41e6e
commit 8162fc1ab1
22 changed files with 423 additions and 376 deletions

120
api.js
View file

@ -1,43 +1,101 @@
import * as Mindcraft from './mindcraft.js';
import { readFileSync } from 'fs';
await Mindcraft.init('localhost', 8080); // starts server locally
await Mindcraft.connect('ip', 'port') // connects to remote server
// await Mindcraft.connect('ip', 'port') // connects to remote server
// ^ must do one of these before calling anything else
Mindcraft.addWorld(
let profile = JSON.parse(readFileSync('./profiles/gemini.json', 'utf8'));
Mindcraft.createAgent(
{
name: 'test',
minecraft_version: "1.21.1",
host: 'localhost',
port: 55916,
auth: 'offline',
render_bot_views: false, // show bot's view in browser at localhost:3000, 3001...
allow_insecure_coding: true, // allows newAction command and model can write/run code on server. enable at own risk
code_timeout_mins: -1, // minutes code is allowed to run. -1 for no timeout
verbose_commands: true, // show full command syntax
chat_bot_messages: true, // publicly chat bot-to-bot messages
}
)
// add world for easy reuse. not super necessary, easy for user to copy world def object around. remove?
Mindcraft.addAgent(
{
world: 'test',
world: {
minecraft_version: '',
host: '',
port: '',
auth: 'offline'
"minecraft_version": "1.21.1", // supports up to 1.21.1
"host": "127.0.0.1", // or "localhost", "your.ip.address.here"
"port": 55916,
"auth": "offline", // or "microsoft"
},
profile: './profiles/test.json',
// profile: {
// name: 'test',
// prompt: 'test',
profile,
"base_profile": "survival", // survival | creative | god_mode
"load_memory": false, // load memory from previous session
"init_message": "Respond with hello world and your name", // sends to all on spawn
"only_chat_with": [], // users that the bots listen to and send general messages to. if empty it will chat publicly
"speak": false, // allows all bots to speak through system text-to-speech. works on windows, mac, on linux you need to `apt install espeak`
"language": "en", // translate to/from this language. Supports these language names: https://cloud.google.com/translate/docs/languages
"allow_vision": false, // allows vision model to interpret screenshots as inputs
"blocked_actions" : ["!checkBlueprint", "!checkBlueprintLevel", "!getBlueprint", "!getBlueprintLevel"] , // commands to disable and remove from docs. Ex: ["!setMode"]
"relevant_docs_count": 5, // number of relevant code function docs to select for prompting. -1 for all
"max_messages": 15, // max number of messages to keep in context
"num_examples": 2, // number of examples to give to the model
"max_commands": -1, // max number of commands that can be used in consecutive responses. -1 for no limit
"narrate_behavior": true, // chat simple automatic actions ('Picking up item!')
"log_all_prompts": false, // log ALL prompts to file
// "task": {
// "task_id": "multiagent_crafting_pink_wool_full_plan__depth_0",
// "goal": "Collaborate with other agents to craft an pink_wool",
// "conversation": "Let's work together to craft an pink_wool.",
// "initial_inventory": {
// "0": {
// "pink_dye": 1
// }
// },
task: './tasks/test.json'
// "agent_count": 1,
// "target": "pink_wool",
// "number_of_target": 1,
// "type": "techtree",
// "max_depth": 1,
// "depth": 0,
// "timeout": 300,
// "blocked_actions": {
// "0": [],
// },
// "missing_items": [],
// "requires_ctable": false
// },
"verbose_commands": true, // show full command syntax
"chat_bot_messages": true, // publicly chat bot-to-bot messages
// mindserver settings
"render_bot_view": false, // show bot's view in browser at localhost:3000, 3001...
"allow_insecure_coding": true, // allows newAction command and model can write/run code on your computer. enable at own risk
"code_timeout_mins": -1, // minutes code is allowed to run. -1 for no timeout
}
)
Mindcraft.removeAgent()
// profile = JSON.parse(readFileSync('./andy.json', 'utf8'));
// Mindcraft.createAgent(
// {
// world: {
// "minecraft_version": "1.21.1", // supports up to 1.21.1
// "host": "127.0.0.1", // or "localhost", "your.ip.address.here"
// "port": 55916,
// "auth": "offline", // or "microsoft"
// },
// profile,
// "base_profile": "survival", // also see creative.json, god_mode.json
// "load_memory": false, // load memory from previous session
// "init_message": "Respond with hello world and your name", // sends to all on spawn
// "only_chat_with": [], // users that the bots listen to and send general messages to. if empty it will chat publicly
// "speak": false, // allows all bots to speak through system text-to-speech. works on windows, mac, on linux you need to `apt install espeak`
// "language": "en", // translate to/from this language. Supports these language names: https://cloud.google.com/translate/docs/languages
// "allow_vision": false, // allows vision model to interpret screenshots as inputs
// "blocked_actions" : ["!checkBlueprint", "!checkBlueprintLevel", "!getBlueprint", "!getBlueprintLevel"] , // commands to disable and remove from docs. Ex: ["!setMode"]
// "relevant_docs_count": 5, // number of relevant code function docs to select for prompting. -1 for all
// "max_messages": 15, // max number of messages to keep in context
// "num_examples": 2, // number of examples to give to the model
// "max_commands": -1, // max number of commands that can be used in consecutive responses. -1 for no limit
// "narrate_behavior": true, // chat simple automatic actions ('Picking up item!')
// "log_all_prompts": false, // log ALL prompts to file
// "task_file": "",
// "task_name": "",
// "verbose_commands": true, // show full command syntax
// "chat_bot_messages": true, // publicly chat bot-to-bot messages
// // mindserver settings
// "render_bot_view": false, // show bot's view in browser at localhost:3000, 3001...
// "allow_insecure_coding": true, // allows newAction command and model can write/run code on your computer. enable at own risk
// "code_timeout_mins": -1, // minutes code is allowed to run. -1 for no timeout
// }
// )

View file

@ -3,7 +3,7 @@ import settings from './settings.js';
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
import { createMindServer } from './src/server/mindserver.js';
import { mindserverProxy } from './src/process/mindserver_proxy.js.js';
import { mindserverProxy } from './src/process/mindserver_proxy.js';
import { readFileSync } from 'fs';
function parseArguments() {

View file

@ -1,10 +1,12 @@
import { createMindServer, registerAgent } from './src/server/mindserver.js';
import { AgentProcess } from './src/process/agent_process.js';
import { createMindServer } from './src/server/mindserver.js';
import { mindserverProxy } from './src/process/mindserver_proxy.js.js';
import { readFileSync } from 'fs';
let mindserver;
let connected = false;
let agent_processes = {};
let agent_count = 0;
let host = 'localhost';
let port = 8080;
export async function init(host='localhost', port=8080) {
if (connected) {
@ -12,21 +14,55 @@ export async function init(host='localhost', port=8080) {
return;
}
mindserver = createMindServer(host, port);
mindserverProxy.connect(host, port);
host = host;
port = port;
connected = true;
}
export async function connect() {
if (connected) {
console.error('Already connected!');
export async function createAgent(settings) {
if (!settings.profile.name) {
console.error('Agent name is required in profile');
return;
}
let agent_name = settings.profile.name;
registerAgent(settings);
let load_memory = settings.load_memory || false;
let init_message = settings.init_message || null;
const agentProcess = new AgentProcess(agent_name);
agentProcess.start(load_memory, init_message, agent_count, host, port);
agent_count++;
agent_processes[settings.profile.name] = agentProcess;
}
export function getAgentProcess(agentName) {
return agent_processes[agentName];
}
export function startAgent(agentName) {
if (this.agent_processes[agentName]) {
this.agent_processes[agentName].continue();
}
else {
console.error(`Cannot start agent ${agentName}; not found`);
}
}
export function addWorld(settings) {
export function stopAgent(agentName) {
if (this.agent_processes[agentName]) {
this.agent_processes[agentName].stop();
}
}
export async function addAgent(settings) {
export function shutdown() {
console.log('Shutting down');
for (let agentName in this.agent_processes) {
this.agent_processes[agentName].stop();
}
setTimeout(() => {
process.exit(0);
}, 2000);
}
export function logoutAgent(agentName) {
this.socket.emit('logout-agent', agentName);
}

View file

@ -12,26 +12,21 @@ import { SelfPrompter } from './self_prompter.js';
import convoManager from './conversation.js';
import { handleTranslation, handleEnglishTranslation } from '../utils/translator.js';
import { addBrowserViewer } from './vision/browser_viewer.js';
import settings from '../../settings.js';
import { serverProxy } from './agent_proxy.js';
import { serverProxy } from './mindserver_proxy.js';
import settings from './settings.js';
import { Task } from './tasks/tasks.js';
import { say } from './speak.js';
export class Agent {
async start(profile_fp, load_mem=false, init_message=null, count_id=0, task_path=null, task_id=null) {
async start(load_mem=false, init_message=null, count_id=0) {
this.last_sender = null;
this.count_id = count_id;
if (!profile_fp) {
throw new Error('No profile filepath provided');
}
console.log('Starting agent initialization with profile:', profile_fp);
// Initialize components with more detailed error handling
console.log('Initializing action manager...');
this.actions = new ActionManager(this);
console.log('Initializing prompter...');
this.prompter = new Prompter(this, profile_fp);
this.prompter = new Prompter(this, settings.profile);
this.name = this.prompter.getName();
console.log('Initializing history...');
this.history = new History(this);
@ -59,19 +54,15 @@ export class Agent {
} else {
taskStart = Date.now();
}
this.task = new Task(this, task_path, task_id, taskStart);
this.task = new Task(this, settings.task, taskStart);
this.blocked_actions = settings.blocked_actions.concat(this.task.blocked_actions || []);
blacklistCommands(this.blocked_actions);
serverProxy.connect(this);
console.log(this.name, 'logging into minecraft...');
this.bot = initBot(this.name);
initModes(this);
this.bot.on('login', () => {
console.log(this.name, 'logged in!');
serverProxy.login();
@ -90,6 +81,8 @@ export class Agent {
try {
clearTimeout(spawnTimeout);
addBrowserViewer(this.bot, count_id);
console.log('Initializing vision intepreter...');
this.vision_interpreter = new VisionInterpreter(this, settings.allow_vision);
// wait for a bit so stats are not undefined
await new Promise((resolve) => setTimeout(resolve, 1000));
@ -101,13 +94,13 @@ export class Agent {
this.startEvents();
if (!load_mem) {
if (task_path !== null) {
if (settings.task) {
this.task.initBotTask();
this.task.setAgentGoal();
}
} else {
// set the goal without initializing the rest of the task
if (task_path !== null) {
if (settings.task) {
this.task.setAgentGoal();
}
}
@ -115,9 +108,6 @@ export class Agent {
await new Promise((resolve) => setTimeout(resolve, 10000));
this.checkAllPlayersPresent();
console.log('Initializing vision intepreter...');
this.vision_interpreter = new VisionInterpreter(this, settings.allow_vision);
} catch (error) {
console.error('Error in spawn event:', error);
process.exit(0);
@ -160,8 +150,12 @@ export class Agent {
this.respondFunc = respondFunc;
this.bot.on('whisper', respondFunc);
if (settings.profiles.length === 1)
this.bot.on('chat', respondFunc);
this.bot.on('chat', (username, message) => {
if (serverProxy.getNumOtherAgents() > 0) return;
// only respond to open chat messages when there are no other agents
respondFunc(username, message);
});
// Set up auto-eat
this.bot.autoEat.options = {

View file

@ -1,73 +0,0 @@
import { io } from 'socket.io-client';
import convoManager from './conversation.js';
import settings from '../../settings.js';
class AgentServerProxy {
constructor() {
if (AgentServerProxy.instance) {
return AgentServerProxy.instance;
}
this.socket = null;
this.connected = false;
AgentServerProxy.instance = this;
}
connect(agent) {
if (this.connected) return;
this.agent = agent;
this.socket = io(`http://${settings.mindserver_host}:${settings.mindserver_port}`);
this.connected = true;
this.socket.on('connect', () => {
console.log('Connected to MindServer');
});
this.socket.on('disconnect', () => {
console.log('Disconnected from MindServer');
this.connected = false;
});
this.socket.on('chat-message', (agentName, json) => {
convoManager.receiveFromBot(agentName, json);
});
this.socket.on('agents-update', (agents) => {
convoManager.updateAgents(agents);
});
this.socket.on('restart-agent', (agentName) => {
console.log(`Restarting agent: ${agentName}`);
this.agent.cleanKill();
});
this.socket.on('send-message', (agentName, message) => {
try {
this.agent.respondFunc("NO USERNAME", message);
} catch (error) {
console.error('Error: ', JSON.stringify(error, Object.getOwnPropertyNames(error)));
}
});
}
login() {
this.socket.emit('login-agent', this.agent.name);
}
shutdown() {
this.socket.emit('shutdown');
}
getSocket() {
return this.socket;
}
}
// Create and export a singleton instance
export const serverProxy = new AgentServerProxy();
export function sendBotChatToServer(agentName, json) {
serverProxy.getSocket().emit('chat-message', agentName, json);
}

View file

@ -1,6 +1,5 @@
import { writeFile, readFile, mkdirSync } from 'fs';
import settings from '../../settings.js';
import { makeCompartment } from './library/lockdown.js';
import { makeCompartment, lockdown } from './library/lockdown.js';
import * as skills from './library/skills.js';
import * as world from './library/world.js';
import { Vec3 } from 'vec3';
@ -27,6 +26,7 @@ export class Coder {
async generateCode(agent_history) {
this.agent.bot.modes.pause('unstuck');
lockdown();
// this message history is transient and only maintained in this function
let messages = agent_history.getHistory();
messages.push({role: 'system', content: 'Code generation started. Write code in codeblock in your response:'});

View file

@ -1,5 +1,5 @@
import * as skills from '../library/skills.js';
import settings from '../../../settings.js';
import settings from '../settings.js';
import convoManager from '../conversation.js';
@ -46,7 +46,7 @@ export const actionsList = [
result = 'Error generating code: ' + e.toString();
}
};
await agent.actions.runAction('action:newAction', actionFn);
await agent.actions.runAction('action:newAction', actionFn, {timeout: settings.code_timeout_mins});
return result;
}
},

View file

@ -1,10 +1,9 @@
import settings from '../../settings.js';
import { readFileSync } from 'fs';
import settings from './settings.js';
import { containsCommand } from './commands/index.js';
import { sendBotChatToServer } from './agent_proxy.js';
import { sendBotChatToServer } from './mindserver_proxy.js';
let agent;
let agent_names = settings.profiles.map((p) => JSON.parse(readFileSync(p, 'utf8')).name);
let agent_names = [];
let agents_in_game = [];
class Conversation {

View file

@ -1,6 +1,6 @@
import { writeFileSync, readFileSync, mkdirSync, existsSync } from 'fs';
import { NPCData } from './npc/data.js';
import settings from '../../settings.js';
import settings from './settings.js';
export class History {

View file

@ -4,7 +4,12 @@ import 'ses';
// We disable some of the taming to allow for more flexibility
// For configuration, see https://github.com/endojs/endo/blob/master/packages/ses/docs/lockdown.md
lockdown({
let lockeddown = false;
export function lockdown() {
if (lockeddown) return;
lockeddown = true;
lockdown({
// basic devex and quality of life improvements
localeTaming: 'unsafe',
consoleTaming: 'unsafe',
@ -13,7 +18,8 @@ lockdown({
// allow eval outside of created compartments
// (mineflayer dep "protodef" uses eval)
evalTaming: 'unsafeEval',
});
});
}
export const makeCompartment = (endowments = {}) => {
return new Compartment({

View file

@ -0,0 +1,107 @@
import { io } from 'socket.io-client';
import convoManager from './conversation.js';
import { setSettings } from './settings.js';
class MindServerProxy {
constructor() {
if (MindServerProxy.instance) {
return MindServerProxy.instance;
}
this.socket = null;
this.connected = false;
this.agents = [];
MindServerProxy.instance = this;
}
async connect(name, host, port) {
if (this.connected) return;
this.name = name;
this.socket = io(`http://${host}:${port}`);
this.connected = true;
this.socket.on('connect', () => {
console.log(name, 'connected to MindServer');
});
this.socket.on('disconnect', () => {
console.log('Disconnected from MindServer');
this.connected = false;
});
this.socket.on('chat-message', (agentName, json) => {
convoManager.receiveFromBot(agentName, json);
});
this.socket.on('agents-update', (agents) => {
this.agents = agents;
convoManager.updateAgents(agents);
if (this.agent?.task) {
console.log(this.agent.name, 'updating available agents');
this.agent.task.updateAvailableAgents(agents);
}
});
this.socket.on('restart-agent', (agentName) => {
console.log(`Restarting agent: ${agentName}`);
this.agent.cleanKill();
});
this.socket.on('send-message', (agentName, message) => {
try {
this.agent.respondFunc("NO USERNAME", message);
} catch (error) {
console.error('Error: ', JSON.stringify(error, Object.getOwnPropertyNames(error)));
}
});
// Request settings and wait for response
await new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error('Settings request timed out after 10 seconds'));
}, 10000);
this.socket.emit('get-settings', name, (response) => {
clearTimeout(timeout);
if (response.error) {
return reject(new Error(response.error));
}
setSettings(response.settings);
resolve();
});
});
}
setAgent(agent) {
this.agent = agent;
}
getAgents() {
return this.agents;
}
getNumOtherAgents() {
return this.agents.length - 1;
}
login() {
this.socket.emit('login-agent', this.agent.name);
}
shutdown() {
this.socket.emit('shutdown');
}
getSocket() {
return this.socket;
}
}
// Create and export a singleton instance
export const serverProxy = new MindServerProxy();
export function sendBotChatToServer(agentName, json) {
serverProxy.getSocket().emit('chat-message', agentName, json);
}

View file

@ -1,7 +1,7 @@
import * as skills from './library/skills.js';
import * as world from './library/world.js';
import * as mc from '../utils/mcdata.js';
import settings from '../../settings.js'
import settings from './settings.js'
import convoManager from './conversation.js';
async function say(agent, message) {

View file

@ -0,0 +1,7 @@
// extremely lightweight obj that can be imported/modified by any file
let settings = {};
export default settings;
export function setSettings(new_settings) {
Object.keys(settings).forEach(key => delete settings[key]);
Object.assign(settings, new_settings);
}

View file

@ -1,7 +1,6 @@
import { readFileSync , writeFileSync, existsSync} from 'fs';
import { executeCommand } from '../commands/index.js';
import { getPosition } from '../library/world.js';
import settings from '../../../settings.js';
import { ConstructionTaskValidator, Blueprint } from './construction_tasks.js';
import { CookingTaskInitiator } from './cooking_tasks.js';
@ -233,27 +232,26 @@ class CookingCraftingTaskValidator {
}
export class Task {
constructor(agent, task_path, task_id, taskStartTime = null) {
constructor(agent, task_data, taskStartTime = null) {
this.agent = agent;
this.data = null;
if (taskStartTime !== null)
this.taskStartTime = taskStartTime;
else
this.taskStartTime = Date.now();
console.log("Task start time set to", this.taskStartTime);
this.validator = null;
this.reset_function = null;
this.blocked_actions = [];
this.task_id = task_id;
if (task_path && task_id) {
console.log('Starting task', task_id);
if (task_id.endsWith('hells_kitchen')) {
this.task_data = task_data;
if (task_data) {
console.log('Starting task', task_data.task_id);
console.log("Task start time set to", this.taskStartTime);
if (task_data.task_id.endsWith('hells_kitchen')) {
// Reset hells_kitchen progress when a new task starts
hellsKitchenProgressManager.resetTask(task_id);
hellsKitchenProgressManager.resetTask(task_data.task_id);
console.log('Reset Hells Kitchen progress for new task');
}
this.data = this.loadTask(task_path, task_id);
this.data = task_data;
this.task_type = this.data.type;
if (this.task_type === 'construction' && this.data.blueprint) {
this.blueprint = new Blueprint(this.data.blueprint);
@ -300,7 +298,11 @@ export class Task {
}
this.name = this.agent.name;
this.available_agents = settings.profiles.map((p) => JSON.parse(readFileSync(p, 'utf8')).name);
this.available_agents = []
}
updateAvailableAgents(agents) {
this.available_agents = agents
}
// Add this method if you want to manually reset the hells_kitchen progress
@ -360,28 +362,6 @@ export class Task {
return null;
}
loadTask(task_path, task_id) {
try {
const tasksFile = readFileSync(task_path, 'utf8');
const tasks = JSON.parse(tasksFile);
let task = tasks[task_id];
task['task_id'] = task_id;
console.log(task);
console.log(this.agent.count_id);
if (!task) {
throw new Error(`Task ${task_id} not found`);
}
// if ((!task.agent_count || task.agent_count <= 1) && this.agent.count_id > 0) {
// task = null;
// }
return task;
} catch (error) {
console.error('Error loading task:', error);
process.exit(1);
}
}
isDone() {
let res = null;
if (this.validator)

View file

@ -1,8 +1,8 @@
import settings from '../../../settings.js';
import settings from '../settings.js';
import prismarineViewer from 'prismarine-viewer';
const mineflayerViewer = prismarineViewer.mineflayer;
export function addBrowserViewer(bot, count_id) {
if (settings.show_bot_views)
if (settings.render_bot_view)
mineflayerViewer(bot, { port: 3000+count_id, firstPerson: true, });
}

View file

@ -4,7 +4,7 @@ import { getCommandDocs } from '../agent/commands/index.js';
import { SkillLibrary } from "../agent/library/skill_library.js";
import { stringifyTurns } from '../utils/text.js';
import { getCommand } from '../agent/commands/index.js';
import settings from '../../settings.js';
import settings from '../agent/settings.js';
import { Gemini } from './gemini.js';
import { GPT } from './gpt.js';
@ -30,11 +30,18 @@ const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export class Prompter {
constructor(agent, fp) {
constructor(agent, profile) {
this.agent = agent;
this.profile = JSON.parse(readFileSync(fp, 'utf8'));
this.profile = profile;
let default_profile = JSON.parse(readFileSync('./profiles/defaults/_default.json', 'utf8'));
let base_fp = settings.base_profile;
let base_fp = '';
if (settings.base_profile === 'survival') {
base_fp = './profiles/defaults/survival.json';
} else if (settings.base_profile === 'creative') {
base_fp = './profiles/defaults/creative.json';
} else if (settings.base_profile === 'god_mode') {
base_fp = './profiles/defaults/god_mode.json';
}
let base_profile = JSON.parse(readFileSync(base_fp, 'utf8'));
// first use defaults to fill in missing values in the base profile

View file

@ -1,23 +1,24 @@
import { spawn } from 'child_process';
import { mindserverProxy } from './mindserver_proxy.js.js';
import { logoutAgent } from '../server/mindserver.js';
export class AgentProcess {
start(profile, load_memory=false, init_message=null, count_id=0, task_path=null, task_id=null) {
this.profile = profile;
constructor(name) {
this.name = name;
}
start(load_memory=false, init_message=null, count_id=0, host, port) {
this.count_id = count_id;
this.running = true;
let args = ['src/process/init_agent.js', this.name];
args.push('-p', profile);
args.push('-n', this.name);
args.push('-c', count_id);
if (load_memory)
args.push('-l', load_memory);
if (init_message)
args.push('-m', init_message);
if (task_path)
args.push('-t', task_path);
if (task_id)
args.push('-i', task_id);
args.push('-h', host);
args.push('-p', port);
const agentProcess = spawn('node', args, {
stdio: 'inherit',
@ -28,7 +29,7 @@ export class AgentProcess {
agentProcess.on('exit', (code, signal) => {
console.log(`Agent process exited with code ${code} and signal ${signal}`);
this.running = false;
mindserverProxy.logoutAgent(this.name);
logoutAgent(this.name);
if (code > 1) {
console.log(`Ending task`);
@ -38,11 +39,11 @@ export class AgentProcess {
if (code !== 0 && signal !== 'SIGINT') {
// agent must run for at least 10 seconds before restarting
if (Date.now() - last_restart < 10000) {
console.error(`Agent process ${profile} exited too quickly and will not be restarted.`);
console.error(`Agent process exited too quickly and will not be restarted.`);
return;
}
console.log('Restarting agent...');
this.start(profile, true, 'Agent process restarted.', count_id, task_path, task_id);
this.start(true, 'Agent process restarted.', count_id, host, port);
last_restart = Date.now();
}
});
@ -61,7 +62,7 @@ export class AgentProcess {
continue() {
if (!this.running) {
this.start(this.profile, true, 'Agent process restarted.', this.count_id);
this.start(true, 'Agent process restarted.', this.count_id);
}
}
}

View file

@ -1,16 +1,7 @@
import { Agent } from '../agent/agent.js';
import { serverProxy } from '../agent/mindserver_proxy.js';
import yargs from 'yargs';
// Add global unhandled rejection handler
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Rejection at:', {
promise: promise,
reason: reason,
stack: reason?.stack || 'No stack trace'
});
process.exit(1);
});
const args = process.argv.slice(2);
if (args.length < 1) {
console.log('Usage: node init_agent.js <agent_name> [profile] [load_memory] [init_message]');
@ -18,10 +9,10 @@ if (args.length < 1) {
}
const argv = yargs(args)
.option('profile', {
alias: 'p',
.option('name', {
alias: 'n',
type: 'string',
description: 'profile filepath to use for agent'
description: 'name of agent'
})
.option('load_memory', {
alias: 'l',
@ -33,29 +24,32 @@ const argv = yargs(args)
type: 'string',
description: 'automatically prompt the agent on startup'
})
.option('task_path', {
alias: 't',
type: 'string',
description: 'task filepath to use for agent'
})
.option('task_id', {
alias: 'i',
type: 'string',
description: 'task ID to execute'
})
.option('count_id', {
alias: 'c',
type: 'number',
default: 0,
description: 'identifying count for multi-agent scenarios',
}).argv;
})
.option('host', {
alias: 'h',
type: 'string',
description: 'host of mindserver'
})
.option('port', {
alias: 'p',
type: 'number',
description: 'port of mindserver'
})
.argv;
// Wrap agent start in async IIFE with proper error handling
(async () => {
try {
console.log('Starting agent with profile:', argv.profile);
console.log('Connecting to MindServer');
await serverProxy.connect(argv.name, argv.host, argv.port);
console.log('Starting agent');
const agent = new Agent();
await agent.start(argv.profile, argv.load_memory, argv.init_message, argv.count_id, argv.task_path, argv.task_id);
serverProxy.setAgent(agent);
await agent.start(argv.load_memory, argv.init_message, argv.count_id);
} catch (error) {
console.error('Failed to start agent process:');
console.error(error.message);

View file

@ -1,64 +0,0 @@
import { io } from 'socket.io-client';
// Singleton mindserver proxy for the main process
// recieves commands from mindserver
class MindserverProxy {
constructor() {
if (MindserverProxy.instance) {
return MindserverProxy.instance;
}
this.socket = null;
this.connected = false;
this.agent_processes = {};
MindserverProxy.instance = this;
}
connect(host, port) {
if (this.connected) return;
this.socket = io(`http://${host}:${port}`);
this.connected = true;
this.socket.on('stop-agent', (agentName) => {
if (this.agent_processes[agentName]) {
this.agent_processes[agentName].stop();
}
});
this.socket.on('start-agent', (agentName) => {
if (this.agent_processes[agentName]) {
this.agent_processes[agentName].continue();
}
});
this.socket.on('register-agents-success', () => {
console.log('Agents registered');
});
this.socket.on('shutdown', () => {
console.log('Shutting down');
for (let agentName in this.agent_processes) {
this.agent_processes[agentName].stop();
}
setTimeout(() => {
process.exit(0);
}, 2000);
});
}
addAgent(agent) {
this.agent_processes.push(agent);
}
logoutAgent(agentName) {
this.socket.emit('logout-agent', agentName);
}
registerAgent(name, process) {
this.socket.emit('register-agents', [name]);
this.agent_processes[name] = process;
}
}
export const mindserverProxy = new MindserverProxy();

View file

@ -3,21 +3,41 @@ import express from 'express';
import http from 'http';
import path from 'path';
import { fileURLToPath } from 'url';
import * as mindcraft from '../../mindcraft.js';
// Mindserver purposes:
// - central hub for inter-process communication between all agent processes
// Mindserver is:
// - central hub for communication between all agent processes
// - api to control from other languages and remote users
// - host for webapp
// Module-level variables
let io;
let server;
const registeredAgents = new Set();
const inGameAgents = {};
const agentManagers = {}; // socket for main process that registers/controls agents
const agent_connections = {};
class AgentConnection {
constructor(settings) {
this.socket = null;
this.settings = settings;
this.in_game = false;
}
}
export function registerAgent(settings) {
let agentConnection = new AgentConnection(settings);
agent_connections[settings.profile.name] = agentConnection;
}
export function logoutAgent(agentName) {
if (agent_connections[agentName]) {
agent_connections[agentName].in_game = false;
agentsUpdate();
}
}
// Initialize the server
export function createMindServer(port = 8080) {
export function createMindServer(host = 'localhost', port = 8080) {
const app = express();
server = http.createServer(app);
io = new Server(server);
@ -33,109 +53,90 @@ export function createMindServer(port = 8080) {
agentsUpdate(socket);
socket.on('register-agents', (agentNames) => {
console.log(`Registering agents: ${agentNames}`);
agentNames.forEach(name => registeredAgents.add(name));
for (let name of agentNames) {
agentManagers[name] = socket;
socket.on('get-settings', (agentName, callback) => {
if (agent_connections[agentName]) {
callback({ settings: agent_connections[agentName].settings });
} else {
callback({ error: `Agent '${agentName}' not found.` });
}
socket.emit('register-agents-success');
agentsUpdate();
});
socket.on('login-agent', (agentName) => {
if (curAgentName && curAgentName !== agentName) {
console.warn(`Agent ${agentName} already logged in as ${curAgentName}`);
return;
}
if (registeredAgents.has(agentName)) {
if (agent_connections[agentName]) {
agent_connections[agentName].socket = socket;
agent_connections[agentName].in_game = true;
curAgentName = agentName;
inGameAgents[agentName] = socket;
agentsUpdate();
} else {
console.warn(`Agent ${agentName} not registered`);
}
else {
console.warn(`Unregistered agent ${agentName} tried to login`);
}
});
socket.on('logout-agent', (agentName) => {
if (inGameAgents[agentName]) {
delete inGameAgents[agentName];
if (agent_connections[agentName]) {
agent_connections[agentName].in_game = false;
agentsUpdate();
}
});
socket.on('disconnect', () => {
console.log('Client disconnected');
if (inGameAgents[curAgentName]) {
delete inGameAgents[curAgentName];
if (agent_connections[curAgentName]) {
agent_connections[curAgentName].in_game = false;
agentsUpdate();
}
});
socket.on('chat-message', (agentName, json) => {
if (!inGameAgents[agentName]) {
if (!agent_connections[agentName]) {
console.warn(`Agent ${agentName} tried to send a message but is not logged in`);
return;
}
console.log(`${curAgentName} sending message to ${agentName}: ${json.message}`);
inGameAgents[agentName].emit('chat-message', curAgentName, json);
agent_connections[agentName].socket.emit('chat-message', curAgentName, json);
});
socket.on('restart-agent', (agentName) => {
console.log(`Restarting agent: ${agentName}`);
inGameAgents[agentName].emit('restart-agent');
agent_connections[agentName].socket.emit('restart-agent');
});
socket.on('stop-agent', (agentName) => {
let manager = agentManagers[agentName];
if (manager) {
manager.emit('stop-agent', agentName);
}
else {
console.warn(`Stopping unregisterd agent ${agentName}`);
}
mindcraft.stopAgent(agentName);
});
socket.on('start-agent', (agentName) => {
let manager = agentManagers[agentName];
if (manager) {
manager.emit('start-agent', agentName);
}
else {
console.warn(`Starting unregisterd agent ${agentName}`);
}
mindcraft.startAgent(agentName);
});
socket.on('stop-all-agents', () => {
console.log('Killing all agents');
stopAllAgents();
for (let agentName in agent_connections) {
mindcraft.stopAgent(agentName);
}
});
socket.on('shutdown', () => {
console.log('Shutting down');
for (let manager of Object.values(agentManagers)) {
manager.emit('shutdown');
}
setTimeout(() => {
process.exit(0);
}, 2000);
});
socket.on('send-message', (agentName, message) => {
if (!inGameAgents[agentName]) {
console.warn(`Agent ${agentName} not logged in, cannot send message via MindServer.`);
if (!agent_connections[agentName]) {
console.warn(`Agent ${agentName} not in game, cannot send message via MindServer.`);
return
}
try {
console.log(`Sending message to agent ${agentName}: ${message}`);
inGameAgents[agentName].emit('send-message', agentName, message)
agent_connections[agentName].socket.emit('send-message', agentName, message)
} catch (error) {
console.error('Error: ', error);
}
});
});
server.listen(port, 'localhost', () => {
server.listen(port, host, () => {
console.log(`MindServer running on port ${port}`);
});
@ -147,22 +148,12 @@ function agentsUpdate(socket) {
socket = io;
}
let agents = [];
registeredAgents.forEach(name => {
agents.push({name, in_game: !!inGameAgents[name]});
});
for (let agentName in agent_connections) {
agents.push({name: agentName, in_game: agent_connections[agentName].in_game});
};
socket.emit('agents-update', agents);
}
function stopAllAgents() {
for (const agentName in inGameAgents) {
let manager = agentManagers[agentName];
if (manager) {
manager.emit('stop-agent', agentName);
}
}
}
// Optional: export these if you need access to them from other files
export const getIO = () => io;
export const getServer = () => server;
export const getConnectedAgents = () => connectedAgents;

View file

@ -1,5 +1,5 @@
import minecraftData from 'minecraft-data';
import settings from '../../settings.js';
import settings from '../agent/settings.js';
import { createBot } from 'mineflayer';
import prismarine_items from 'prismarine-item';
import { pathfinder } from 'mineflayer-pathfinder';
@ -8,10 +8,9 @@ import { plugin as collectblock } from 'mineflayer-collectblock';
import { plugin as autoEat } from 'mineflayer-auto-eat';
import plugin from 'mineflayer-armor-manager';
const armorManager = plugin;
const mc_version = settings.minecraft_version;
const mcdata = minecraftData(mc_version);
const Item = prismarine_items(mc_version);
let mc_version = null;
let mcdata = null;
let Item = null;
/**
* @typedef {string} ItemName
@ -54,12 +53,15 @@ export const WOOL_COLORS = [
export function initBot(username) {
mc_version = settings.world.minecraft_version;
mcdata = minecraftData(mc_version);
Item = prismarine_items(mc_version);
let bot = createBot({
username: username,
host: settings.host,
port: settings.port,
auth: settings.auth,
host: settings.world.host,
port: settings.world.port,
auth: settings.world.auth,
version: mc_version,
});

View file

@ -1,10 +1,11 @@
import translate from 'google-translate-api-x';
import settings from '../../settings.js';
import settings from '../agent/settings.js';
const preferred_lang = String(settings.language).toLowerCase();
export async function handleTranslation(message) {
if (preferred_lang === 'en' || preferred_lang === 'english')
let preferred_lang = String(settings.language).toLowerCase();
if (!preferred_lang || preferred_lang === 'en' || preferred_lang === 'english')
return message;
try {
const translation = await translate(message, { to: preferred_lang });
@ -16,7 +17,8 @@ export async function handleTranslation(message) {
}
export async function handleEnglishTranslation(message) {
if (preferred_lang === 'en' || preferred_lang === 'english')
let preferred_lang = String(settings.language).toLowerCase();
if (!preferred_lang || preferred_lang === 'en' || preferred_lang === 'english')
return message;
try {
const translation = await translate(message, { to: 'english' });