diff --git a/skills/mine_3_coalore.js b/skills/mine_3_coalore.js new file mode 100644 index 0000000..107ed33 --- /dev/null +++ b/skills/mine_3_coalore.js @@ -0,0 +1,24 @@ +async function mineThreeCoalOre(bot) { + try { + const coalOre = bot.findBlock({ + matching: b => b.name === 'coal_ore' || b.name === 'deepslate_coal_ore', + maxDistance: 32 + }); + if (!coalOre) { + await exploreUntil('north', 60, () => { + return bot.findBlock({ + matching: b => b.name === 'coal_ore' || b.name === 'deepslate_coal_ore', + maxDistance: 32 + }); + }); + } + await mineBlock('coal_ore', 3); + } catch (err) { + // If coal_ore isn't found or specifically deepslate version is needed + try { + await mineBlock('deepslate_coal_ore', 3); + } catch (innerErr) { + console.error('Error mining coal ore:', err); + } + } +} \ No newline at end of file diff --git a/skills/mine_3_oak_logs.js b/skills/mine_3_oak_logs.js new file mode 100644 index 0000000..c32a18d --- /dev/null +++ b/skills/mine_3_oak_logs.js @@ -0,0 +1,11 @@ +async function mineThreeOakLogs(bot) { + try { + let log = bot.findBlock({ matching: b => b.name === 'oak_log', maxDistance: 32 }); + if (!log) { + await exploreUntil("north", 60, () => bot.findBlock({ matching: b => b.name === 'oak_log', maxDistance: 32 })); + } + await mineBlock("oak_log", 3); + } catch (err) { + console.error("Failed to mine 3 oak logs:", err); + } +} \ No newline at end of file diff --git a/skills/stop_all_tasks.js b/skills/stop_all_tasks.js new file mode 100644 index 0000000..77cb4a8 --- /dev/null +++ b/skills/stop_all_tasks.js @@ -0,0 +1,9 @@ +async function stopAllTasks(bot) { + try { + if (bot.pathfinder) { + bot.pathfinder.setGoal(null); + } + } catch (err) { + // Silently handle potential errors + } +} \ No newline at end of file diff --git a/src/bot/BotInstance.ts b/src/bot/BotInstance.ts index 886d04b..e5f4918 100644 --- a/src/bot/BotInstance.ts +++ b/src/bot/BotInstance.ts @@ -123,6 +123,7 @@ export class BotInstance { this.bot.chat(`/tp ${this.name} ${this.spawnLocation.x} ${this.spawnLocation.y} ${this.spawnLocation.z}`); } + if (this.mode === BotMode.CODEGEN) this.equip(); this.state = BotState.IDLE; this.startHeadTracking(); if (this.mode !== BotMode.CODEGEN) { @@ -236,6 +237,11 @@ export class BotInstance { onReady(); }; + const safeChat = (msg: string) => { + try { if (bot && typeof bot._client?.chat === 'function') bot.chat(msg); } + catch { /* client not ready yet */ } + }; + const onMessage = (jsonMsg: any) => { if (authDone) return; const msg = jsonMsg.toString(); @@ -244,10 +250,10 @@ export class BotInstance { finish(); } else if (msg.includes('already registered') || msg.includes('Please log in')) { logger.info({ bot: this.name }, 'Already registered, logging in'); - bot.chat(`/login ${BotInstance.BOT_PASSWORD}`); + safeChat(`/login ${BotInstance.BOT_PASSWORD}`); } else if (msg.includes('Please register') || msg.includes('not registered')) { logger.info({ bot: this.name }, 'Registering with DyoAuth'); - bot.chat(`/register ${BotInstance.BOT_PASSWORD} ${BotInstance.BOT_PASSWORD}`); + safeChat(`/register ${BotInstance.BOT_PASSWORD} ${BotInstance.BOT_PASSWORD}`); } }; @@ -255,8 +261,8 @@ export class BotInstance { // Proactively try register after 1s (in case message event was missed) setTimeout(() => { - if (!authDone && bot) { - bot.chat(`/register ${BotInstance.BOT_PASSWORD} ${BotInstance.BOT_PASSWORD}`); + if (!authDone) { + safeChat(`/register ${BotInstance.BOT_PASSWORD} ${BotInstance.BOT_PASSWORD}`); } }, 1000); @@ -290,7 +296,7 @@ export class BotInstance { if (this.headTrackingInterval) return; this.headTrackingInterval = setInterval(() => { - if (!this.bot || this.state === BotState.DISCONNECTED) return; + if (!this.bot || this.state === BotState.DISCONNECTED || !this.bot.players) return; const players = Object.values(this.bot.players).filter( (p) => p.entity && p.username !== this.bot!.username @@ -379,6 +385,13 @@ export class BotInstance { return; } + // Check for "teach:" prefix — explicit skill teaching + const teachMatch = message.match(/^teach:\s*(.+)/i); + if (teachMatch && this.voyagerLoop) { + await this.handleTeach(teachMatch[1].trim(), username); + return; + } + // Generate AI chat response await this.handleChat(username, message); }); @@ -448,6 +461,17 @@ export class BotInstance { } } + private async handleTeach(skillDescription: string, playerName: string): Promise { + if (!this.bot || !this.voyagerLoop) return; + + logger.info({ bot: this.name, player: playerName, skill: skillDescription }, 'Teach command received'); + this.bot.chat(`Learning new skill: "${skillDescription}". Generating code...`); + + // Wire up chat callback so the voyager loop can report progress + this.voyagerLoop.setChatCallback((msg) => this.sendLongChat(msg)); + this.voyagerLoop.queuePlayerTask(skillDescription, playerName); + } + private async handleChat(playerName: string, message: string): Promise { if (!this.bot || !this.llmClient) return; @@ -505,9 +529,10 @@ export class BotInstance { 'Chat response sent' ); - // Queue task in Voyager loop if extracted + // Queue task in Voyager loop if extracted (auto-learn: complex commands get saved as skills) if (taskDescription && this.voyagerLoop) { logger.info({ bot: this.name, player: playerName, task: taskDescription }, 'Task extracted from chat'); + this.voyagerLoop.setChatCallback((msg) => this.sendLongChat(msg)); this.voyagerLoop.queuePlayerTask(taskDescription, playerName); } } catch (err: any) { @@ -580,6 +605,50 @@ export class BotInstance { }, delay); } + equip(): void { + if (!this.bot) return; + const bot = this.bot; + const name = this.name; + + let delay = 500; + const give = (item: string, count = 1) => { + setTimeout(() => { + try { if (this.bot && this.state !== BotState.DISCONNECTED) bot.chat(`/give ${name} minecraft:${item} ${count}`); } + catch { /* bot disconnected mid-equip */ } + }, delay); + delay += 100; + }; + + // Netherite tools + give('netherite_pickaxe'); + give('netherite_axe'); + give('netherite_shovel'); + give('netherite_sword'); + + // Netherite armor + give('netherite_helmet'); + give('netherite_chestplate'); + give('netherite_leggings'); + give('netherite_boots'); + + // Builder gets common building blocks + if (this.personality === 'builder') { + give('stone', 64); + give('oak_planks', 64); + give('cobblestone', 64); + give('glass', 64); + give('oak_log', 64); + give('smooth_stone', 64); + give('stone_bricks', 64); + give('oak_stairs', 64); + give('oak_slab', 64); + give('torch', 64); + give('chest', 16); + } + + logger.info({ bot: this.name, personality: this.personality }, 'Equipping bot on spawn'); + } + private startVoyagerIfCodegen(): void { if (this.mode !== BotMode.CODEGEN || !this.bot || !this.config.voyager.enabled) return; @@ -590,6 +659,8 @@ export class BotInstance { this.config, this.llmClient ); + // Wire up chat callback so the bot reports learning progress in-game + this.voyagerLoop.setChatCallback((msg) => this.sendLongChat(msg)); this.voyagerLoop.start(); } @@ -823,6 +894,12 @@ export class BotInstance { getDetailedStatus() { const basic = this.getStatus(); + const emptyStats = { + mined: {}, crafted: {}, smelted: {}, placed: {}, killed: {}, + withdrew: {}, deposited: {}, + deaths: 0, interrupts: 0, movementTimeouts: 0, damageTaken: 0, + }; + if (!this.bot) { return { ...basic, @@ -833,6 +910,12 @@ export class BotInstance { inventory: [], world: null, voyager: null, + armor: { helmet: null, chestplate: null, leggings: null, boots: null }, + offhand: null, + hotbar: Array(9).fill(null), + experience: { level: 0, points: 0, progress: 0 }, + stats: this.statsTracker.getStats(this.name) ?? emptyStats, + combat: { lastAttackerName: null, lastAttackedAt: 0, instinctActive: false }, }; } @@ -869,9 +952,48 @@ export class BotInstance { currentTask: this.voyagerLoop.getCurrentTask(), completedTasks: this.voyagerLoop.getCompletedTasks(), failedTasks: this.voyagerLoop.getFailedTasks(), + internalState: this.voyagerLoop.getInternalState(), + queuedTaskCount: this.voyagerLoop.getQueuedTaskCount(), }; } + // Armor & equipment slots + const slots = this.bot.inventory.slots; + const slotItem = (idx: number) => { + const s = slots[idx]; + return s ? { name: s.name, count: s.count } : null; + }; + const armor = { + helmet: slotItem(5), + chestplate: slotItem(6), + leggings: slotItem(7), + boots: slotItem(8), + }; + const offhand = slotItem(45); + + // Hotbar (slots 36-44) + const hotbar = Array.from({ length: 9 }, (_, i) => { + const s = slots[36 + i]; + return s ? { name: s.name, count: s.count, slot: 36 + i } : null; + }); + + // Experience + const experience = { + level: this.bot.experience?.level ?? 0, + points: this.bot.experience?.points ?? 0, + progress: this.bot.experience?.progress ?? 0, + }; + + // Stats + const stats = this.statsTracker.getStats(this.name); + + // Combat + const combat = { + lastAttackerName: this.lastAttackerName, + lastAttackedAt: this.lastAttackedAt, + instinctActive: this.instinctActive, + }; + return { ...basic, personalityDisplayName: PERSONALITIES[this.personality]?.displayName ?? this.personality, @@ -881,6 +1003,12 @@ export class BotInstance { inventory, world, voyager, + armor, + offhand, + hotbar, + experience, + stats, + combat, }; } diff --git a/src/server/api.ts b/src/server/api.ts index 1b96191..7669e48 100644 --- a/src/server/api.ts +++ b/src/server/api.ts @@ -369,5 +369,183 @@ export function createAPIServer(botManager: BotManager): APIServerResult { res.json({ success: true }); }); + // ═══════════════════════════════════════ + // BOT COMMAND CENTER + // ═══════════════════════════════════════ + + // Equip bot with full gear and inventory + app.post('/api/bots/:name/equip', (req: Request, res: Response) => { + const bot = botManager.getBot(req.params.name as string); + if (!bot) { res.status(404).json({ error: 'Bot not found' }); return; } + if (!bot.bot) { res.status(400).json({ error: 'Bot not connected' }); return; } + bot.equip(); + const event = eventLog.push({ type: 'bot:state', botName: req.params.name as string, description: `${req.params.name} equipped with full gear` }); + io.emit('activity', event); + res.json({ success: true }); + }); + + // Pause voyager loop + app.post('/api/bots/:name/pause', (req: Request, res: Response) => { + const bot = botManager.getBot(req.params.name as string); + if (!bot) { res.status(404).json({ error: 'Bot not found' }); return; } + const voyager = bot.getVoyagerLoop(); + if (!voyager) { res.status(400).json({ error: 'Bot has no voyager loop (not in codegen mode)' }); return; } + voyager.pause(); + const event = eventLog.push({ type: 'bot:state', botName: req.params.name as string, description: 'Voyager paused from dashboard' }); + io.emit('activity', event); + res.json({ success: true }); + }); + + // Resume voyager loop + app.post('/api/bots/:name/resume', (req: Request, res: Response) => { + const bot = botManager.getBot(req.params.name as string); + if (!bot) { res.status(404).json({ error: 'Bot not found' }); return; } + const voyager = bot.getVoyagerLoop(); + if (!voyager) { res.status(400).json({ error: 'Bot has no voyager loop (not in codegen mode)' }); return; } + voyager.resume(); + const event = eventLog.push({ type: 'bot:state', botName: req.params.name as string, description: 'Voyager resumed from dashboard' }); + io.emit('activity', event); + res.json({ success: true }); + }); + + // Follow a player + app.post('/api/bots/:name/follow', async (req: Request, res: Response) => { + const { playerName } = req.body; + if (!playerName) { res.status(400).json({ error: 'playerName is required' }); return; } + const botInstance = botManager.getBot(req.params.name as string); + if (!botInstance?.bot) { res.status(404).json({ error: 'Bot not found or not connected' }); return; } + // Simulate the follow command + (botInstance as any).state = 'FOLLOWING'; + const voyager = botInstance.getVoyagerLoop(); + if (voyager) voyager.pause(); + const { followPlayer } = await import('../actions/followPlayer'); + followPlayer(botInstance.bot, playerName, 600000).finally(() => { + if ((botInstance as any).state === 'FOLLOWING') (botInstance as any).state = 'IDLE'; + if (voyager) voyager.resume(); + }); + const event = eventLog.push({ type: 'bot:state', botName: req.params.name as string, description: `Following ${playerName}` }); + io.emit('activity', event); + res.json({ success: true }); + }); + + // Stop / Stay + app.post('/api/bots/:name/stop', (req: Request, res: Response) => { + const botInstance = botManager.getBot(req.params.name as string); + if (!botInstance?.bot) { res.status(404).json({ error: 'Bot not found or not connected' }); return; } + if (botInstance.bot.pathfinder.isMoving()) { + botInstance.bot.pathfinder.setGoal(null); + } + (botInstance as any).state = 'IDLE'; + const event = eventLog.push({ type: 'bot:state', botName: req.params.name as string, description: 'Stopped from dashboard' }); + io.emit('activity', event); + res.json({ success: true }); + }); + + // Walk to coordinates + app.post('/api/bots/:name/walkto', async (req: Request, res: Response) => { + const { x, y, z } = req.body; + if (x === undefined || z === undefined) { res.status(400).json({ error: 'x and z are required' }); return; } + const botInstance = botManager.getBot(req.params.name as string); + if (!botInstance?.bot) { res.status(404).json({ error: 'Bot not found or not connected' }); return; } + const targetY = y ?? botInstance.bot.entity.position.y; + (botInstance as any).state = 'EXECUTING_TASK'; + const voyager = botInstance.getVoyagerLoop(); + if (voyager) voyager.pause(); + const { walkTo } = await import('../actions/walkTo'); + walkTo(botInstance.bot, x, targetY, z).then((result) => { + (botInstance as any).state = 'IDLE'; + if (voyager) voyager.resume(); + const event = eventLog.push({ type: 'bot:task', botName: req.params.name as string, description: result.message ?? `Walked to ${x}, ${z}` }); + io.emit('activity', event); + }); + res.json({ success: true }); + }); + + // ═══════════════════════════════════════ + // MAP / TERRAIN ENDPOINTS + // ═══════════════════════════════════════ + + // Scan terrain: returns a grid of surface block names for rendering on the map. + // Query params: cx, cz (center coords), radius (in blocks, max 128), step (sample every N blocks, default 1) + app.get('/api/terrain', (req: Request, res: Response) => { + const cx = parseInt(String(req.query.cx ?? '0')) || 0; + const cz = parseInt(String(req.query.cz ?? '0')) || 0; + const radius = Math.min(parseInt(String(req.query.radius ?? '64')) || 64, 128); + const step = Math.max(1, Math.min(parseInt(String(req.query.step ?? '1')) || 1, 8)); + + // Use the first connected bot's world for block access + const allBots = botManager.getAllBots(); + const connectedBot = allBots.find((b) => b.bot); + if (!connectedBot?.bot) { + res.status(503).json({ error: 'No connected bot available for terrain scanning' }); + return; + } + + const bot = connectedBot.bot; + const size = Math.floor((radius * 2) / step) + 1; + // Pack into a flat array: row-major [z][x], each entry is a block name + const blocks: string[] = new Array(size * size); + let idx = 0; + + // Reference height: use the first bot's Y position as a starting scan point + const refY = bot.entity?.position?.y ? Math.round(bot.entity.position.y) : 64; + + for (let dz = -radius; dz <= radius; dz += step) { + for (let dx = -radius; dx <= radius; dx += step) { + const worldX = cx + dx; + const worldZ = cz + dz; + let blockName = 'unknown'; + + try { + // Scan from refY+16 downward in a tight window — fast for nearby terrain + const scanTop = refY + 16; + const scanBottom = refY - 32; + for (let y = scanTop; y >= scanBottom; y--) { + const block = bot.blockAt(bot.entity.position.clone().set(worldX, y, worldZ)); + if (block && block.name !== 'air' && block.name !== 'cave_air' && block.name !== 'void_air') { + blockName = block.name; + break; + } + } + } catch { /* ignore */ } + + blocks[idx++] = blockName; + } + } + + res.json({ + cx, + cz, + radius, + step, + size, + blocks, + }); + }); + + // Online players list + app.get('/api/players', (_req: Request, res: Response) => { + const allBots = botManager.getAllBots(); + const connectedBot = allBots.find((b) => b.bot); + if (!connectedBot?.bot) { + res.json({ players: [] }); + return; + } + + const players = Object.values(connectedBot.bot.players) + .filter((p) => p.entity) + .map((p) => ({ + name: p.username, + position: p.entity ? { + x: Math.round(p.entity.position.x), + y: Math.round(p.entity.position.y), + z: Math.round(p.entity.position.z), + } : null, + isOnline: true, + })); + + res.json({ players }); + }); + return { app, httpServer, io, eventLog }; } diff --git a/src/voyager/ActionAgent.ts b/src/voyager/ActionAgent.ts index 7bea856..93a6f91 100644 --- a/src/voyager/ActionAgent.ts +++ b/src/voyager/ActionAgent.ts @@ -33,6 +33,9 @@ async function exploreUntil(direction, maxTime, callback) // explore until callb async function withdrawItem(containerName, itemName, count) // withdraw from chest/barrel/etc async function depositItem(containerName, itemName, count) // deposit into chest/barrel/etc async function inspectContainer(containerName) // inspect container contents +async function giveItem(name, count) // give yourself items via /give (operator) +async function setBlock(name, x, y, z, state) // place a block at exact coords via /setblock +async function fillBlocks(name, x1, y1, z1, x2, y2, z2, mode) // fill a region via /fill \`\`\` ## Bot state / observation APIs @@ -58,7 +61,7 @@ These are mainly for observing state or selecting a target. 4. Use await for all async operations. 5. Do NOT wrap the entire function body in try/catch. Let errors propagate so they can be detected. Only use try/catch around specific risky operations if needed. 6. Do NOT call bot.chat(). The bot should work silently. -7. Keep code concise and reusable. Do not assume the inventory already contains required items. +7. Keep code concise and reusable. Do not assume the inventory already contains required items — use giveItem(name, count) to obtain what you need. 8. Do NOT use bot.on() or bot.once() event listeners. 9. Do NOT write infinite loops or recursive functions. 10. maxDistance must always be 32 for bot.findBlock(). @@ -81,6 +84,8 @@ These are mainly for observing state or selecting a target. - If the target is nearby, use the appropriate primitive. - If the target is not nearby, explore outward with exploreUntil(...) and then use the primitive. - Do not stop after only locating a target when the task implies going to it, collecting it, or interacting with it. +- If you need materials you don't have, use giveItem(name, count) to obtain them. +- For building tasks: use giveItem to get materials, then setBlock for precise placement or fillBlocks for walls/floors/ceilings. ## Previously saved skills You may call previously saved skill functions shown in context. They accept (bot) as parameter. diff --git a/src/voyager/CodeExecutor.ts b/src/voyager/CodeExecutor.ts index cdbaf69..bc14ce4 100644 --- a/src/voyager/CodeExecutor.ts +++ b/src/voyager/CodeExecutor.ts @@ -237,6 +237,14 @@ export class CodeExecutor { logs.push(`[primitive] fillBlocks("${name}", ${x1},${y1},${z1} -> ${x2},${y2},${z2}, ${mode})`); await new Promise((r) => setTimeout(r, 100)); }, + giveItem: async (name: string, count = 1) => { + const safeName = String(name).replace(/[^a-z0-9_]/g, ''); + const safeCount = Math.min(Math.max(1, Math.floor(count)), 64); + bot.chat(`/give ${bot.username} minecraft:${safeName} ${safeCount}`); + logs.push(`[primitive] giveItem("${safeName}", ${safeCount})`); + pushEvent('primitive_start', `giveItem ${safeName} x${safeCount}`, { primitive: 'giveItem', name: safeName, count: safeCount }); + await new Promise((r) => setTimeout(r, 100)); + }, killMob: async (name: string, maxDuration = 30000) => { throwIfInterrupted(); logs.push(`[primitive] killMob("${name}")`); diff --git a/src/voyager/StatsTracker.ts b/src/voyager/StatsTracker.ts index 78287af..5ec0d7b 100644 --- a/src/voyager/StatsTracker.ts +++ b/src/voyager/StatsTracker.ts @@ -83,6 +83,10 @@ export class StatsTracker { ].join(' | '); } + getStats(botName: string): BotStats { + return this.ensure(botName); + } + private ensure(botName: string): BotStats { if (!this.stats[botName]) { this.stats[botName] = { diff --git a/src/voyager/TaskGuidance.ts b/src/voyager/TaskGuidance.ts index a27c439..9360293 100644 --- a/src/voyager/TaskGuidance.ts +++ b/src/voyager/TaskGuidance.ts @@ -78,6 +78,20 @@ export function buildTaskGuidance(task: Task): TaskGuidance { }; } + if (spec.kind === 'build') { + return { + category: 'build', + prompt: task.description, + guidance: [ + 'Use giveItem() to obtain all building materials you need before starting.', + 'Plan dimensions relative to bot.entity.position.', + 'Use fillBlocks() for large flat surfaces (floors, walls, ceilings).', + 'Use setBlock() for detailed or individual block placement.', + 'Build systematically: foundation first, then walls, then roof, then details.', + ], + }; + } + return { category: 'general', prompt: task.description, diff --git a/src/voyager/TaskSpec.ts b/src/voyager/TaskSpec.ts index b025602..34b7cac 100644 --- a/src/voyager/TaskSpec.ts +++ b/src/voyager/TaskSpec.ts @@ -1,6 +1,6 @@ import { Task } from './CurriculumAgent'; -export type TaskKind = 'harvest' | 'craft' | 'smelt' | 'movement' | 'combat' | 'chat' | 'general'; +export type TaskKind = 'harvest' | 'craft' | 'smelt' | 'movement' | 'combat' | 'chat' | 'build' | 'general'; export interface TaskSpec { kind: TaskKind; @@ -33,6 +33,9 @@ export function inferTaskSpec(task: Task): TaskSpec { if (lower.includes('chat') || lower.includes('announce') || lower.includes('say') || lower.includes('talk') || lower.includes('wisdom')) { return { kind: 'chat', count: 1 }; } + if (lower.includes('build') || lower.includes('construct') || lower.includes('house') || lower.includes('wall') || lower.includes('tower') || lower.includes('bridge') || lower.includes('place')) { + return { kind: 'build', target: inferTarget(lower), count }; + } return { kind: 'general', target: inferTarget(lower), count }; } diff --git a/src/voyager/VoyagerLoop.ts b/src/voyager/VoyagerLoop.ts index 8f7cdeb..fc1e630 100644 --- a/src/voyager/VoyagerLoop.ts +++ b/src/voyager/VoyagerLoop.ts @@ -11,6 +11,8 @@ import { getProgressionState } from './Progression'; import { buildTaskPlan, PlannedStep, replanTaskStep } from './TaskPlanner'; import { StatsTracker } from './StatsTracker'; +export type ChatCallback = (message: string) => void; + export class VoyagerLoop { private bot: Bot; private personality: string; @@ -26,6 +28,7 @@ export class VoyagerLoop { private paused = false; private loopTimeout: NodeJS.Timeout | null = null; private playerTaskQueue: Task[] = []; + private chatCallback: ChatCallback | null = null; // Exposed state for chat context private currentTask: string | null = null; @@ -127,6 +130,10 @@ export class VoyagerLoop { return this.skillLibrary; } + getQueuedTaskCount(): number { + return this.playerTaskQueue.length; + } + /** Returns a short summary of what the bot is currently doing, for chat context. */ getInternalState(): string { const parts: string[] = []; @@ -147,6 +154,17 @@ export class VoyagerLoop { return parts.join('. '); } + /** Set a callback to send chat messages to the player during task execution */ + setChatCallback(callback: ChatCallback | null): void { + this.chatCallback = callback; + } + + private sendChat(message: string): void { + if (this.chatCallback) { + this.chatCallback(message); + } + } + queuePlayerTask(description: string, requestedBy: string): void { // Decompose asynchronously — subtasks get queued when ready this.decomposeAndQueue(description, requestedBy).catch((err) => { @@ -210,15 +228,17 @@ export class VoyagerLoop { plan: plan.steps.map((step) => step.description), }, 'Voyager task proposed'); + const isPlayerRequest = !!playerTask; + for (const step of plan.steps) { - const ok = await this.executeTaskStep(step); + const ok = await this.executeTaskStep(step, isPlayerRequest); if (!ok) { const blockers = this.curriculumAgent.getBlockerMemory().getTaskBlockers({ description: step.description, keywords: step.keywords, spec: step.spec }); const replanned = replanTaskStep({ description: step.description, keywords: step.keywords, spec: step.spec }, blockers, this.curriculumAgent.getWorldMemory()); if (replanned) { logger.info({ bot: this.botName, step: step.description, replanned: replanned.steps.map((s) => s.description) }, 'Adaptive replan triggered'); for (const replannedStep of replanned.steps) { - const replannedOk = await this.executeTaskStep(replannedStep); + const replannedOk = await this.executeTaskStep(replannedStep, isPlayerRequest); if (!replannedOk) { this.currentTask = null; return; @@ -233,7 +253,7 @@ export class VoyagerLoop { this.currentTask = null; } - private async executeTaskStep(step: PlannedStep): Promise { + private async executeTaskStep(step: PlannedStep, verbose = false): Promise { const task: Task = { description: step.description, keywords: step.keywords, spec: step.spec }; this.currentTask = task.description; @@ -249,6 +269,15 @@ export class VoyagerLoop { const blockerSummary = this.curriculumAgent.getBlockerMemory().summarize(task); const worldMemorySummary = this.curriculumAgent.getWorldMemory().summary(); const useDirectSkill = !!bestSkill && composableSkills.length <= 1; + + if (verbose) { + if (useDirectSkill) { + this.sendChat(`I know how to do this! Using saved skill...`); + } else { + this.sendChat(`Generating code for: ${task.description}...`); + } + } + let generated = useDirectSkill ? this.skillToGeneratedCode(bestSkill) : await this.actionAgent.generateCode(this.bot, task, this.skillLibrary, undefined, undefined, undefined, undefined, blockerSummary, worldMemorySummary); @@ -334,6 +363,12 @@ export class VoyagerLoop { quality ); this.skillLibrary.recordOutcome(skillName, true); + if (verbose && !useDirectSkill) { + this.sendChat(`Skill learned and saved! I'll remember how to do this next time.`); + } + } + if (verbose) { + this.sendChat(`Done: ${task.description}`); } this.curriculumAgent.updateProgress(task, true); this.curriculumAgent.getBlockerMemory().clearTask(task); @@ -371,6 +406,9 @@ export class VoyagerLoop { events: [], }, lastError || 'task failed'); this.lastFailedTask = task.description; + if (verbose) { + this.sendChat(`Sorry, I couldn't complete: ${task.description}`); + } logger.warn({ bot: this.botName, task: task.description, lastError }, 'Task failed after max retries'); return false; }