diff --git a/services/minecraft/src/cognitive/conscious/js-planner.ts b/services/minecraft/src/cognitive/conscious/js-planner.ts index cc29c6b81..48a7c9d12 100644 --- a/services/minecraft/src/cognitive/conscious/js-planner.ts +++ b/services/minecraft/src/cognitive/conscious/js-planner.ts @@ -207,6 +207,8 @@ export class JavaScriptPlanner { { name: 'llmUserMessage', kind: 'string', readonly: true }, { name: 'llmConversationHistory', kind: 'object', readonly: true }, { name: 'query', kind: 'object', readonly: true }, + { name: 'query.self', kind: 'function', readonly: true }, + { name: 'query.snapshot', kind: 'function', readonly: true }, { name: 'bot', kind: 'object', readonly: true }, { name: 'mineflayer', kind: 'object', readonly: true }, { name: 'mem', kind: 'object', readonly: false }, diff --git a/services/minecraft/src/cognitive/conscious/query-dsl.test.ts b/services/minecraft/src/cognitive/conscious/query-dsl.test.ts index df425d6bd..233a098f5 100644 --- a/services/minecraft/src/cognitive/conscious/query-dsl.test.ts +++ b/services/minecraft/src/cognitive/conscious/query-dsl.test.ts @@ -73,4 +73,29 @@ describe('query DSL', () => { const pickaxes = query.craftable().whereIncludes('pickaxe').uniq().list() expect(pickaxes).toEqual(['wooden_pickaxe', 'stone_pickaxe']) }) + + it('supports inventory helper methods for count/has/summary', () => { + const query = createQueryRuntime(createMineflayerStub()) + expect(query.inventory().count('bread')).toBe(5) + expect(query.inventory().has('bread', 5)).toBe(true) + expect(query.inventory().has('bread', 6)).toBe(false) + expect(query.inventory().summary()).toEqual([ + { name: 'cobblestone', count: 16 }, + { name: 'bread', count: 5 }, + ]) + }) + + it('returns self and snapshot views for one-shot state reads', () => { + const query = createQueryRuntime(createMineflayerStub()) + const self = query.self() + expect(self.pos).toEqual({ x: 0, y: 64, z: 0 }) + expect(self.heldItem).toBeNull() + + const snap = query.snapshot(12) + expect(snap.inventory.counts).toEqual({ + bread: 5, + cobblestone: 16, + }) + expect(snap.nearby.ores.map(b => b.name)).toEqual(['coal_ore', 'coal_ore', 'ancient_debris']) + }) }) diff --git a/services/minecraft/src/cognitive/conscious/query-dsl.ts b/services/minecraft/src/cognitive/conscious/query-dsl.ts index 03a4a765c..33ab8a8a2 100644 --- a/services/minecraft/src/cognitive/conscious/query-dsl.ts +++ b/services/minecraft/src/cognitive/conscious/query-dsl.ts @@ -31,6 +31,21 @@ interface InventoryRecord { displayName?: string } +interface InventorySummaryRecord { + name: string + count: number +} + +interface SelfQueryRecord { + pos: { x: number, y: number, z: number } + health: number + food: number + heldItem: string | null + gameMode: string + isRaining: boolean + timeOfDay: number | null +} + type NamePredicate = (value: string) => boolean class NameQueryChain { @@ -199,6 +214,28 @@ class InventoryQueryChain { }, {} as Record) } + public count(name: string): number { + const needle = name.toLowerCase() + return this.list() + .filter(item => item.name.toLowerCase() === needle) + .reduce((sum, item) => sum + item.count, 0) + } + + public has(name: string, atLeast = 1): boolean { + return this.count(name) >= Math.max(1, Math.floor(atLeast)) + } + + public summary(): InventorySummaryRecord[] { + const counts = this.countByName() + return Object.entries(counts) + .map(([name, count]) => ({ name, count })) + .sort((a, b) => { + if (b.count !== a.count) + return b.count - a.count + return a.name.localeCompare(b.name) + }) + } + public list(): InventoryRecord[] { return this.mineflayer.bot.inventory .items() @@ -296,8 +333,41 @@ function clamp(value: number, min: number, max: number): number { return Math.min(max, Math.max(min, value)) } +function toSelfRecord(mineflayer: Mineflayer): SelfQueryRecord { + return { + pos: toPos(mineflayer.bot.entity.position), + health: mineflayer.bot.health, + food: mineflayer.bot.food, + heldItem: mineflayer.bot.heldItem?.name ?? null, + gameMode: mineflayer.bot.game?.gameMode ?? 'unknown', + isRaining: Boolean(mineflayer.bot.isRaining), + timeOfDay: typeof mineflayer.bot.time?.timeOfDay === 'number' ? mineflayer.bot.time.timeOfDay : null, + } +} + export function createQueryRuntime(mineflayer: Mineflayer) { return { + self: () => toSelfRecord(mineflayer), + snapshot: (range = 16) => { + const normalizedRange = clamp(Math.floor(range), 1, 64) + const inventory = new InventoryQueryChain(mineflayer) + return { + self: toSelfRecord(mineflayer), + inventory: { + counts: inventory.countByName(), + summary: inventory.summary(), + emptySlots: typeof mineflayer.bot.inventory.emptySlotCount === 'function' + ? mineflayer.bot.inventory.emptySlotCount() + : Math.max(0, 36 - mineflayer.bot.inventory.items().length), + totalStacks: mineflayer.bot.inventory.items().length, + }, + nearby: { + blocks: new BlockQueryChain(mineflayer).within(normalizedRange).limit(20).list(), + entities: new EntityQueryChain(mineflayer).within(normalizedRange).limit(20).list(), + ores: new BlockQueryChain(mineflayer).within(normalizedRange).isOre().limit(20).list(), + }, + } + }, blocks: () => new BlockQueryChain(mineflayer), blockAt: ({ x, y, z }: { x: number, y: number, z: number }) => { const block = mineflayer.bot.blockAt(new Vec3(Math.floor(x), Math.floor(y), Math.floor(z)))