From 07097d1f786c88795eee5fe52d2df5f0d9b8fa13 Mon Sep 17 00:00:00 2001 From: Rin Date: Tue, 13 Jan 2026 22:44:51 +0800 Subject: [PATCH] feat(minecraft): execute tools from dashboard --- .../mineflayer-perception-collector.ts | 410 +++++++++--------- .../cognitive/rules/attention/movement.yaml | 2 +- services/minecraft/src/debug/debug-service.ts | 12 + services/minecraft/src/debug/tool-executor.ts | 189 ++++++++ services/minecraft/src/debug/types.ts | 56 ++- services/minecraft/src/debug/web/app.js | 235 +++++++++- services/minecraft/src/debug/web/index.html | 15 + services/minecraft/src/debug/web/tools.css | 167 +++++++ services/minecraft/src/main.ts | 4 + 9 files changed, 883 insertions(+), 207 deletions(-) create mode 100644 services/minecraft/src/debug/tool-executor.ts create mode 100644 services/minecraft/src/debug/web/tools.css diff --git a/services/minecraft/src/cognitive/perception/mineflayer-perception-collector.ts b/services/minecraft/src/cognitive/perception/mineflayer-perception-collector.ts index a60babdd8..ccf787379 100644 --- a/services/minecraft/src/cognitive/perception/mineflayer-perception-collector.ts +++ b/services/minecraft/src/cognitive/perception/mineflayer-perception-collector.ts @@ -20,7 +20,6 @@ export class MineflayerPerceptionCollector { }> = [] private lastSelfHealth: number | null = null - private lastStatsAt = 0 private stats: Record = {} private sneakingState: Map = new Map() @@ -36,214 +35,12 @@ export class MineflayerPerceptionCollector { public init(bot: MineflayerWithAgents): void { this.bot = bot this.lastSelfHealth = bot.bot.health - this.lastStatsAt = Date.now() this.stats = {} this.deps.logger.withFields({ maxDistance: this.deps.maxDistance }).log('MineflayerPerceptionCollector: init') - this.onBot('entityMoved', (entity: any) => { - const now = Date.now() - const dist = this.distanceTo(entity) - if (dist === null || dist > this.deps.maxDistance) - return - - // Ignore self - if (entity.username === this.bot?.bot.username) - return - - const entityId = this.entityId(entity) - - const event: SightedEntityMovedEvent = { - modality: 'sighted', - kind: 'entity_moved', - entityType: entity?.type === 'player' ? 'player' : 'mob', - entityId, - displayName: entity?.username, - distance: dist, - hasLineOfSight: true, - timestamp: now, - source: 'minecraft', - pos: entity?.position, - } - - this.deps.emitRaw(event) - this.bumpStat('sighted.entity_moved') - this.maybeLogStats() - }) - - this.onBot('entitySwingArm', (entity: any) => { - const now = Date.now() - const dist = this.distanceTo(entity) - if (dist === null || dist > this.deps.maxDistance) - return - - // Ignore self - if (entity.username === this.bot?.bot.username) - return - - const event: SightedArmSwingEvent = { - modality: 'sighted', - kind: 'arm_swing', - entityType: 'player', - entityId: this.entityId(entity), - displayName: entity?.username, - distance: dist, - hasLineOfSight: true, - timestamp: now, - source: 'minecraft', - pos: entity?.position, - } - - this.deps.emitRaw(event) - this.bumpStat('sighted.arm_swing') - this.maybeLogStats() - }) - - this.onBot('entityUpdate', (entity: any) => { - if (!entity || entity.type !== 'player') - return - - // Ignore self - if (entity.username === this.bot?.bot.username) - return - - const entityId = this.entityId(entity) - const flags = entity?.metadata?.[0] - // Bit 1 (0x02) is sneaking - const isSneaking = typeof flags === 'number' ? !!(flags & 0x02) : false - - // Check if state actually changed - const lastState = this.sneakingState.get(entityId) - if (lastState === isSneaking) { - return - } - this.sneakingState.set(entityId, isSneaking) - - const now = Date.now() - const dist = this.distanceTo(entity) - if (dist === null || dist > this.deps.maxDistance) - return - - const event: SightedSneakToggleEvent = { - modality: 'sighted', - kind: 'sneak_toggle', - entityType: 'player', - entityId, - displayName: entity?.username, - distance: dist, - hasLineOfSight: true, - sneaking: isSneaking, - timestamp: now, - source: 'minecraft', - pos: entity?.position, - } - - this.deps.logger.withFields({ entity: entity.username, sneaking: isSneaking }).log('MineflayerPerceptionCollector: sneak_toggle') - - this.deps.emitRaw(event) - this.bumpStat('sighted.sneak_toggle') - this.maybeLogStats() - }) - - this.onBot('soundEffectHeard', (soundId: string, pos: Vec3) => { - const now = Date.now() - if (!pos) - return - - const dist = this.distanceToPos(pos) - if (dist === null || dist > this.deps.maxDistance) - return - - const event: HeardSoundEvent = { - modality: 'heard', - kind: 'sound', - soundId, - distance: dist, - timestamp: now, - source: 'minecraft', - pos, - } - - this.deps.emitRaw(event) - }) - - // Felt: damage taken (self health decreased) - this.onBot('health', () => { - if (!this.bot) - return - - const now = Date.now() - const current = this.bot.bot.health - const prev = this.lastSelfHealth - this.lastSelfHealth = current - if (typeof prev !== 'number') - return - - if (current >= prev) - return - - const event: FeltDamageTakenEvent = { - modality: 'felt', - kind: 'damage_taken', - amount: prev - current, - timestamp: now, - source: 'minecraft', - } - - this.deps.emitRaw(event) - this.bumpStat('felt.damage_taken') - this.maybeLogStats() - }) - - // Felt: item collected (best-effort; depends on mineflayer version/events) - this.onBot('playerCollect', (collector: any, collected: any) => { - if (!this.bot) - return - if (!collector) - return - if (collector.username !== this.bot.bot.username) - return - - const now = Date.now() - const itemName = String(collected?.name ?? collected?.displayName ?? collected?.type ?? 'unknown') - - const event: FeltItemCollectedEvent = { - modality: 'felt', - kind: 'item_collected', - itemName, - timestamp: now, - source: 'minecraft', - } - - this.deps.emitRaw(event) - this.bumpStat('felt.item_collected') - this.maybeLogStats() - }) - - this.onBot('entityCollect', (collector: any, collected: any) => { - if (!this.bot) - return - if (!collector) - return - if (collector.username !== this.bot.bot.username) - return - - const now = Date.now() - const itemName = String(collected?.name ?? collected?.displayName ?? collected?.type ?? 'unknown') - - const event: FeltItemCollectedEvent = { - modality: 'felt', - kind: 'item_collected', - itemName, - timestamp: now, - source: 'minecraft', - } - - this.deps.emitRaw(event) - this.bumpStat('felt.item_collected') - this.maybeLogStats() - }) + this.registerEventHandlers() } public destroy(): void { @@ -268,6 +65,211 @@ export class MineflayerPerceptionCollector { this.bot = null } + // ======================================== + // Event Handler Registration + // ======================================== + + private registerEventHandlers(): void { + this.onBot('entityMoved', entity => this.handleEntityMoved(entity)) + this.onBot('entitySwingArm', entity => this.handleEntitySwingArm(entity)) + this.onBot('entityUpdate', entity => this.handleEntityUpdate(entity)) + this.onBot('soundEffectHeard', (soundId, pos) => this.handleSoundHeard(soundId, pos)) + this.onBot('health', () => this.handleHealthChange()) + this.onBot('playerCollect', (collector, collected) => this.handleItemCollected(collector, collected)) + this.onBot('entityCollect', (collector, collected) => this.handleItemCollected(collector, collected)) + } + + // ======================================== + // Sighted Event Handlers + // ======================================== + + private handleEntityMoved(entity: any): void { + if (!this.isValidEntityInRange(entity)) + return + + const event: SightedEntityMovedEvent = { + modality: 'sighted', + kind: 'entity_moved', + entityType: entity?.type === 'player' ? 'player' : 'mob', + entityId: this.entityId(entity), + displayName: entity?.username, + distance: this.distanceTo(entity)!, + hasLineOfSight: true, + timestamp: Date.now(), + source: 'minecraft', + pos: entity?.position, + } + + this.emitEvent(event, 'sighted.entity_moved') + } + + private handleEntitySwingArm(entity: any): void { + if (!this.isValidEntityInRange(entity)) + return + + const event: SightedArmSwingEvent = { + modality: 'sighted', + kind: 'arm_swing', + entityType: 'player', + entityId: this.entityId(entity), + displayName: entity?.username, + distance: this.distanceTo(entity)!, + hasLineOfSight: true, + timestamp: Date.now(), + source: 'minecraft', + pos: entity?.position, + } + + this.emitEvent(event, 'sighted.arm_swing') + } + + private handleEntityUpdate(entity: any): void { + if (!entity || entity.type !== 'player') + return + + if (this.isSelfEntity(entity)) + return + + const entityId = this.entityId(entity) + const isSneaking = this.extractSneakingState(entity) + + if (!this.hasSneakingStateChanged(entityId, isSneaking)) + return + + this.sneakingState.set(entityId, isSneaking) + + const dist = this.distanceTo(entity) + if (dist === null || dist > this.deps.maxDistance) + return + + const event: SightedSneakToggleEvent = { + modality: 'sighted', + kind: 'sneak_toggle', + entityType: 'player', + entityId, + displayName: entity?.username, + distance: dist, + hasLineOfSight: true, + sneaking: isSneaking, + timestamp: Date.now(), + source: 'minecraft', + pos: entity?.position, + } + + this.emitEvent(event, 'sighted.sneak_toggle') + } + + // ======================================== + // Heard Event Handlers + // ======================================== + + private handleSoundHeard(soundId: string, pos: Vec3): void { + if (!pos) + return + + const dist = this.distanceToPos(pos) + if (dist === null || dist > this.deps.maxDistance) + return + + const event: HeardSoundEvent = { + modality: 'heard', + kind: 'sound', + soundId, + distance: dist, + timestamp: Date.now(), + source: 'minecraft', + pos, + } + + this.deps.emitRaw(event) + } + + // ======================================== + // Felt Event Handlers + // ======================================== + + private handleHealthChange(): void { + if (!this.bot) + return + + const current = this.bot.bot.health + const prev = this.lastSelfHealth + this.lastSelfHealth = current + + if (typeof prev !== 'number' || current >= prev) + return + + const event: FeltDamageTakenEvent = { + modality: 'felt', + kind: 'damage_taken', + amount: prev - current, + timestamp: Date.now(), + source: 'minecraft', + } + + this.emitEvent(event, 'felt.damage_taken') + } + + private handleItemCollected(collector: any, collected: any): void { + if (!this.bot || !collector) + return + + if (collector.username !== this.bot.bot.username) + return + + const itemName = String(collected?.name ?? collected?.displayName ?? collected?.type ?? 'unknown') + + const event: FeltItemCollectedEvent = { + modality: 'felt', + kind: 'item_collected', + itemName, + timestamp: Date.now(), + source: 'minecraft', + } + + this.emitEvent(event, 'felt.item_collected') + } + + // ======================================== + // Validation Helpers + // ======================================== + + private isValidEntityInRange(entity: any): boolean { + const dist = this.distanceTo(entity) + if (dist === null || dist > this.deps.maxDistance) + return false + + if (this.isSelfEntity(entity)) + return false + + return true + } + + private isSelfEntity(entity: any): boolean { + return entity.username === this.bot?.bot.username + } + + private extractSneakingState(entity: any): boolean { + const flags = entity?.metadata?.[0] + // Bit 1 (0x02) is sneaking + return typeof flags === 'number' ? !!(flags & 0x02) : false + } + + private hasSneakingStateChanged(entityId: string, isSneaking: boolean): boolean { + const lastState = this.sneakingState.get(entityId) + return lastState !== isSneaking + } + + // ======================================== + // Utilities + // ======================================== + + private emitEvent(event: RawPerceptionEvent, statKey: string): void { + this.deps.emitRaw(event) + this.bumpStat(statKey) + this.maybeLogStats() + } + private bumpStat(key: string): void { this.stats[key] = (this.stats[key] ?? 0) + 1 } diff --git a/services/minecraft/src/cognitive/rules/attention/movement.yaml b/services/minecraft/src/cognitive/rules/attention/movement.yaml index e86d5339c..065b89832 100644 --- a/services/minecraft/src/cognitive/rules/attention/movement.yaml +++ b/services/minecraft/src/cognitive/rules/attention/movement.yaml @@ -8,7 +8,7 @@ trigger: entityType: player accumulator: - threshold: 5 + threshold: 14 window: 2s signal: diff --git a/services/minecraft/src/debug/debug-service.ts b/services/minecraft/src/debug/debug-service.ts index 5f6b564fb..028c5ca26 100644 --- a/services/minecraft/src/debug/debug-service.ts +++ b/services/minecraft/src/debug/debug-service.ts @@ -200,6 +200,18 @@ export class DebugService { case 'reflex': this.emitReflexState(payload as Omit) break + case 'debug:tools_list': + this.server.broadcast({ + type: 'debug:tools_list', + payload: payload as { tools: any[] }, + }) + break + case 'debug:tool_result': + this.server.broadcast({ + type: 'debug:tool_result', + payload: payload as any, + }) + break default: // For unknown types, emit as log this.log('DEBUG', `Unknown event type: ${type}`, { payload }) diff --git a/services/minecraft/src/debug/tool-executor.ts b/services/minecraft/src/debug/tool-executor.ts new file mode 100644 index 000000000..c94e43067 --- /dev/null +++ b/services/minecraft/src/debug/tool-executor.ts @@ -0,0 +1,189 @@ +import type { ZodObject, ZodType } from 'zod' + +import type { Mineflayer } from '../libs/mineflayer' +import type { ToolDefinition, ToolParameter } from './types' + +import { actionsList } from '../agents/action/tools' +import { DebugService } from './debug-service' + +export class ToolExecutor { + private mineflayer: Mineflayer + private debugService: DebugService + + constructor(mineflayer: Mineflayer) { + this.mineflayer = mineflayer + this.debugService = DebugService.getInstance() + console.log('[ToolExecutor] Initializing ToolExecutor') + this.setupHandlers() + } + + private setupHandlers(): void { + console.log('[ToolExecutor] Key registered for request_tools') + + // Handle tool list request + this.debugService.onCommand('request_tools', () => { + console.log('[ToolExecutor] Received request_tools command') + this.sendToolsList() + }) + + // Handle tool execution + this.debugService.onCommand('execute_tool', (cmd) => { + if (cmd.type === 'execute_tool') { + console.log(`[ToolExecutor] Executing tool: ${cmd.payload.toolName}`) + this.executeTool(cmd.payload.toolName, cmd.payload.params) + } + }) + } + + private sendToolsList(): void { + try { + const tools = this.extractToolDefinitions() + console.log(`[ToolExecutor] Sending ${tools.length} tools`) + this.debugService.emit('debug:tools_list', { tools }) + } + catch (err) { + console.error('[ToolExecutor] Error sending tool list:', err) + } + } + + private async executeTool(toolName: string, params: Record): Promise { + try { + // Check if action is blocked + // TODO: Add check for running agent if needed + + const action = actionsList.find(a => a.name === toolName) + if (!action) { + throw new Error(`Tool '${toolName}' not found`) + } + + // Validate params + const validated = action.schema.parse(params) + + // Execute + // The perform function in existing tools often returns a function that returns a Promise (or value) + // perform: (mineflayer) => async (args) => result + const performer = action.perform(this.mineflayer) + + const args: any[] = [] + const shape = (action.schema as any).shape + for (const key in shape) { + if (Object.prototype.hasOwnProperty.call(validated, key)) { + args.push((validated as any)[key]) + } + } + + const result = await performer(...args) + + this.debugService.emit('debug:tool_result', { + toolName, + params, + result: typeof result === 'string' ? result : JSON.stringify(result), + timestamp: Date.now(), + }) + } + catch (err: unknown) { + this.debugService.emit('debug:tool_result', { + toolName, + params, + error: err instanceof Error ? err.message : String(err), + timestamp: Date.now(), + }) + } + } + + private extractToolDefinitions(): ToolDefinition[] { + return actionsList.map(action => ({ + name: action.name, + description: action.description, + params: this.extractParamsFromSchema(action.schema), + })) + } + + private extractParamsFromSchema(schema: ZodObject): ToolParameter[] { + if (!schema || !schema.shape) + return [] + + const shape = schema.shape + const params: ToolParameter[] = [] + + for (const [name, zodType] of Object.entries(shape)) { + const def = this.getZodDef(zodType as ZodType) + + params.push({ + name, + type: def.typeName, + description: def.description, + min: def.min, + max: def.max, + default: def.defaultValue, + }) + } + + return params + } + + // Helper to extract metadata from Zod types + private getZodDef(zodType: ZodType): { typeName: 'string' | 'number' | 'boolean', description?: string, min?: number, max?: number, defaultValue?: any } { + let typeName: 'string' | 'number' | 'boolean' = 'string' + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let curr: any = zodType + const description = curr.description + + let min: number | undefined + let max: number | undefined + let defaultValue: any + + // Helper to get type identifier + const getTypeId = (t: any) => t.constructor.name || t._def?.typeName + + // Unwrap effects/optional/nullable/default to get inner type + let infiniteLoopGuard = 0 + while (infiniteLoopGuard++ < 10) { + const typeId = getTypeId(curr) + if (typeId === 'ZodOptional' || typeId === 'ZodNullable') { + curr = curr._def.innerType + } + else if (typeId === 'ZodEffects') { + curr = curr._def.schema + } + else if (typeId === 'ZodDefault') { + defaultValue = curr._def.defaultValue() + curr = curr._def.innerType + } + else { + break + } + } + + const typeId = getTypeId(curr) + + // Debug logging for type checking + // console.log(`[ToolExecutor] Type check: ${description} -> ${typeId}`) + + if (typeId === 'ZodString') { + typeName = 'string' + } + else if (typeId === 'ZodNumber') { + typeName = 'number' + // Try to extract min/max from checks + // eslint-disable-next-line @typescript-eslint/no-explicit-any + if (curr._def.checks) { + for (const check of (curr as any)._def.checks) { + if (check.kind === 'min') + min = check.value + if (check.kind === 'max') + max = check.value + } + } + } + else if (typeId === 'ZodBoolean') { + typeName = 'boolean' + } + + return { typeName, description, min, max, defaultValue } + } +} + +export function setupToolExecutor(mineflayer: Mineflayer): ToolExecutor { + return new ToolExecutor(mineflayer) +} diff --git a/services/minecraft/src/debug/types.ts b/services/minecraft/src/debug/types.ts index 70bd678f7..ec47121de 100644 --- a/services/minecraft/src/debug/types.ts +++ b/services/minecraft/src/debug/types.ts @@ -101,6 +101,41 @@ export interface TraceBatchEvent { } // Union type for all server events + +// ============================================================ +// Tool types +// ============================================================ + +export interface ToolParameter { + name: string + type: 'string' | 'number' | 'boolean' + description?: string + required?: boolean + min?: number + max?: number + default?: unknown +} + +export interface ToolDefinition { + name: string + description: string + params: ToolParameter[] +} + +export interface ToolExecutionResultEvent { + toolName: string + params: Record + result?: string + error?: string + timestamp: number +} + +// ============================================================ +// Server Events Extension +// ============================================================ + +// ... (previous events) + export type ServerEvent = | { type: 'log', payload: LogEvent } | { type: 'llm', payload: LLMTraceEvent } @@ -112,6 +147,8 @@ export type ServerEvent | { type: 'trace_batch', payload: TraceBatchEvent } | { type: 'history', payload: ServerEvent[] } | { type: 'pong', payload: { timestamp: number } } + | { type: 'debug:tools_list', payload: { tools: ToolDefinition[] } } + | { type: 'debug:tool_result', payload: ToolExecutionResultEvent } // ============================================================ // Client -> Server commands @@ -146,13 +183,30 @@ export interface RequestHistoryCommand { type: 'request_history' } -// Union type for all client commands +// ============================================================ +// Client Commands Extension +// ============================================================ + +export interface ExecuteToolCommand { + type: 'execute_tool' + payload: { + toolName: string + params: Record + } +} + +export interface RequestToolsCommand { + type: 'request_tools' +} + export type ClientCommand = | ClearLogsCommand | SetFilterCommand | InjectEventCommand | PingCommand | RequestHistoryCommand + | ExecuteToolCommand + | RequestToolsCommand // ============================================================ // Wire format diff --git a/services/minecraft/src/debug/web/app.js b/services/minecraft/src/debug/web/app.js index 6cb92b71c..d2e19ed8b 100644 --- a/services/minecraft/src/debug/web/app.js +++ b/services/minecraft/src/debug/web/app.js @@ -679,9 +679,240 @@ class SaliencyPanel { } // ============================================================================= -// Timeline Panel (Event Tracing) +// Tools Panel // ============================================================================= +class ToolsPanel { + constructor(client) { + this.client = client + this.tools = [] + this.filter = '' + this.executingTools = new Set() + this.elements = { + grid: document.getElementById('tools-grid'), + search: document.getElementById('tools-search'), + } + } + + init() { + this.client.on('debug:tools_list', data => this.updateTools(data)) + this.client.on('debug:tool_result', data => this.handleResult(data)) + this.client.on('connected', () => this.requestTools()) + + this.elements.search?.addEventListener('input', (e) => { + this.filter = e.target.value.toLowerCase() + this.render() + }) + } + + requestTools() { + console.log('[ToolsPanel] Requesting tools...') + // Check if we already have tools to avoid re-rendering on reconnect if not needed + // But re-requesting ensures we are in sync with server capabilities + this.client.send({ type: 'request_tools' }) + } + + updateTools(data) { + if (data && data.tools) { + this.tools = data.tools + this.render() + } + } + + render() { + if (!this.tools || this.tools.length === 0) { + this.elements.grid.innerHTML = '
Loading tools...
' + return + } + + const filtered = this.tools.filter((tool) => { + if (!this.filter) + return true + return tool.name.toLowerCase().includes(this.filter) + || (tool.description && tool.description.toLowerCase().includes(this.filter)) + }) + + if (filtered.length === 0) { + this.elements.grid.innerHTML = '
No tools match filter
' + return + } + + // Don't nuke usage of existing DOM elements if possible to preserve form state? + // For simplicity, re-render is fine for this debug tool. + + this.elements.grid.innerHTML = filtered.map(tool => this.renderCard(tool)).join('') + + // Attach event listeners + filtered.forEach((tool) => { + const card = document.getElementById(`tool-card-${tool.name}`) + const executeBtn = card?.querySelector('.btn-execute') + + executeBtn?.addEventListener('click', () => this.executeTool(tool)) + }) + } + + renderCard(tool) { + const isExecuting = this.executingTools.has(tool.name) + const cardState = isExecuting ? 'executing' : '' + const paramCount = tool.params.length + + return ` +
+
+ ${escapeHtml(tool.name)} + ${paramCount > 0 ? `${paramCount} param${paramCount > 1 ? 's' : ''}` : ''} +
+
${escapeHtml(tool.description || '')}
+ ${this.renderParams(tool)} +
+ +
+ +
+ ` + } + + renderParams(tool) { + if (tool.params.length === 0) + return '' + + return ` +
+ ${tool.params.map(param => ` +
+ + +
+ `).join('')} +
+ ` + } + + executeTool(tool) { + const card = document.getElementById(`tool-card-${tool.name}`) + if (!card) + return + + // Collect parameter values + const params = {} + const inputs = card.querySelectorAll('.param-input') + + for (const input of inputs) { + const paramName = input.dataset.param + let value = input.value + + // Convert to appropriate type based on definition + const paramDef = tool.params.find(p => p.name === paramName) + if (paramDef) { + if (paramDef.type === 'number') { + if (value === '') { + // Handle empty number input if needed? + } + else { + value = Number.parseFloat(value) + if (isNaN(value)) { + this.showResult(tool.name, { error: `Invalid number for ${paramName}` }, true) + return + } + } + } + } + + // Simple type conversion could be improved but sufficient for now + params[paramName] = value + } + + // Mark as executing + this.executingTools.add(tool.name) + this.updateCardState(tool.name, 'executing') + this.hideResult(tool.name) + + // Send command to server + this.client.send({ + type: 'execute_tool', + payload: { + toolName: tool.name, + params, + }, + }) + } + + handleResult(data) { + const { toolName, result, error } = data + + this.executingTools.delete(toolName) + this.updateCardState(toolName, error ? 'error' : 'success') + this.showResult(toolName, data, !!error) + + if (!error) { + setTimeout(() => { + // Reset state visual but keep result visible for a bit? + // Or remove success styling + const card = document.getElementById(`tool-card-${toolName}`) + if (card) { + card.classList.remove('success') + // Don't auto-hide result immediately, maybe user wants to read it + } + }, 3000) + } + } + + updateCardState(toolName, state) { + const card = document.getElementById(`tool-card-${toolName}`) + if (!card) + return + + card.classList.remove('executing', 'success', 'error') + if (state) + card.classList.add(state) + + const btn = card.querySelector('.btn-execute') + if (btn) { + if (state === 'executing') { + btn.disabled = true + btn.textContent = 'Executing...' + } + else { + btn.disabled = false + btn.textContent = 'Execute' + } + } + } + + showResult(toolName, data, isError) { + const resultEl = document.getElementById(`result-${toolName}`) + if (!resultEl) + return + + resultEl.classList.remove('hidden') + + const label = isError ? 'Error' : 'Result' + const content = isError ? data.error : data.result + + resultEl.innerHTML = ` + ${label} +
${escapeHtml(content || 'No result')}
+ ` + } + + hideResult(toolName) { + const resultEl = document.getElementById(`result-${toolName}`) + if (resultEl) { + resultEl.classList.add('hidden') + } + } +} + class TimelinePanel { constructor(client) { this.client = client @@ -991,6 +1222,7 @@ class DebugApp { this.llmPanel = new LLMPanel(this.client) this.saliencyPanel = new SaliencyPanel(this.client) this.timelinePanel = new TimelinePanel(this.client) + this.toolsPanel = new ToolsPanel(this.client) this.panels = { queue: this.queuePanel, @@ -1000,6 +1232,7 @@ class DebugApp { llm: this.llmPanel, saliency: this.saliencyPanel, timeline: this.timelinePanel, + tools: this.toolsPanel, } this.paused = false } diff --git a/services/minecraft/src/debug/web/index.html b/services/minecraft/src/debug/web/index.html index bda72f5df..60ca7c2d0 100644 --- a/services/minecraft/src/debug/web/index.html +++ b/services/minecraft/src/debug/web/index.html @@ -6,6 +6,7 @@ Debug Dashboard + @@ -108,6 +109,20 @@
{}
+ + +
+
+

Tools

+
+ + +
+
+
+
+
+
diff --git a/services/minecraft/src/debug/web/tools.css b/services/minecraft/src/debug/web/tools.css new file mode 100644 index 000000000..a91f8dca8 --- /dev/null +++ b/services/minecraft/src/debug/web/tools.css @@ -0,0 +1,167 @@ +/* Tools Panel */ +.tools-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 12px; + max-height: 100%; + overflow-y: auto; + padding-right: 4px; +} + +.tool-card { + background: var(--bg-secondary); + border: 1px solid var(--border); + border-radius: 6px; + padding: 12px; + transition: all 0.2s; + display: flex; + flex-direction: column; +} + +.tool-card:hover { + border-color: var(--accent); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); +} + +.tool-card.executing { + border-color: var(--accent); + background: rgba(102, 126, 234, 0.05); +} + +.tool-card.success { + border-color: var(--success); + background: rgba(46, 160, 67, 0.05); +} + +.tool-card.error { + border-color: var(--error); + background: rgba(248, 81, 73, 0.05); +} + +.tool-card-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; +} + +.tool-name { + font-weight: 600; + font-size: 14px; + color: var(--text-primary); + font-family: monospace; +} + +.tool-badge { + font-size: 10px; + padding: 2px 6px; + background: var(--bg-primary); + border-radius: 3px; + color: var(--text-muted); +} + +.tool-description { + font-size: 11px; + color: var(--text-secondary); + margin-bottom: 12px; + line-height: 1.4; + flex-grow: 1; +} + +.tool-params { + margin-bottom: 10px; +} + +.param-group { + margin-bottom: 8px; +} + +.param-label { + display: block; + margin-bottom: 3px; + font-size: 11px; + font-weight: 500; + color: var(--text-secondary); +} + +.param-input { + width: 100%; + padding: 5px 7px; + background: var(--bg-primary); + border: 1px solid var(--border); + border-radius: 3px; + color: var(--text-primary); + font-size: 11px; + font-family: monospace; +} + +.param-input:focus { + outline: none; + border-color: var(--accent); +} + +.param-description { + margin-top: 2px; + font-size: 10px; + color: var(--text-muted); + font-style: italic; +} + +.tool-actions { + display: flex; + gap: 6px; + margin-top: auto; +} + +.btn-execute { + flex: 1; + padding: 7px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border: none; + border-radius: 4px; + color: white; + font-weight: 600; + font-size: 11px; + cursor: pointer; + transition: opacity 0.2s; +} + +.btn-execute:hover:not(:disabled) { + opacity: 0.9; +} + +.btn-execute:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.tool-result { + margin-top: 8px; + padding: 8px; + background: var(--bg-primary); + border: 1px solid var(--border); + border-radius: 4px; + font-family: monospace; + font-size: 10px; + line-height: 1.4; + max-height: 100px; + overflow-y: auto; +} + +.tool-result.hidden { + display: none; +} + +.result-label { + font-weight: 600; + margin-bottom: 4px; + display: block; + text-transform: uppercase; + font-size: 9px; + letter-spacing: 0.5px; +} + +.result-content { + white-space: pre-wrap; + word-break: break-word; +} \ No newline at end of file diff --git a/services/minecraft/src/main.ts b/services/minecraft/src/main.ts index 2b329e59a..6deac933b 100644 --- a/services/minecraft/src/main.ts +++ b/services/minecraft/src/main.ts @@ -48,6 +48,10 @@ async function main() { const agent = await createNeuriAgent(bot) await bot.loadPlugin(CognitiveEngine({ agent, airiClient })) + // Setup Tool Executor for Debug Dashboard + const { setupToolExecutor } = await import('./debug/tool-executor') + setupToolExecutor(bot) + process.on('SIGINT', () => { bot.stop() exit(0)