From 5b48b109715622137ffe3c2f7622ef21c03fefc4 Mon Sep 17 00:00:00 2001 From: legop3 Date: Sun, 3 May 2026 23:20:09 -0400 Subject: [PATCH] overseer v2 3 --- server/prompts/overseer_control_system.txt | 8 +- server/src/services/chatService/index.js | 4 +- .../overseerControlService/contextBuilder.js | 3 +- .../services/overseerControlService/index.js | 193 ++++++++---------- .../overseerControlService/runtimeHelpers.js | 31 ++- .../overseerControlService/tools/chatSay.js | 7 + .../tools/haSetEntity.js | 9 + .../overseerControlService/tools/index.js | 14 ++ .../overseerControlService/tools/liftDown.js | 5 + .../overseerControlService/tools/liftUp.js | 5 + .../tools/memoryRead.js | 4 + .../tools/memoryWrite.js | 11 + .../tools/neatoClearErrors.js | 5 + .../tools/neatoLocate.js | 5 + .../tools/neatoSendHome.js | 5 + .../tools/neatoStart.js | 5 + 16 files changed, 196 insertions(+), 118 deletions(-) diff --git a/server/prompts/overseer_control_system.txt b/server/prompts/overseer_control_system.txt index c5092814..60aa7f18 100644 --- a/server/prompts/overseer_control_system.txt +++ b/server/prompts/overseer_control_system.txt @@ -7,6 +7,8 @@ Rules: - If directly addressed, avoid ignoring users. - Keep lines concise and in-character. -Output contract for this phase: -- Return exactly one line. -- Return one of: SKIP, CHAT, ACTION, ACTION+CHAT. +Output contract: +- Return valid JSON only. +- Shape: + {"decision":"SKIP|CHAT|ACTION|ACTION+CHAT","chat":"optional text","actions":[{"tool":"tool_id","args":{}}]} +- Use `tool_id` values only from available tools. diff --git a/server/src/services/chatService/index.js b/server/src/services/chatService/index.js index 1551afc5..a0f42f5c 100644 --- a/server/src/services/chatService/index.js +++ b/server/src/services/chatService/index.js @@ -8,13 +8,13 @@ const { broadcastMessage, getRecentMessages } = require('./broadcast'); const { createHandlers } = require('./handlers'); const { registerChatSocketHooks } = require('./socketHooks'); -function sendSystemMessage(text) { +function sendSystemMessage(text, options = {}) { const normalized = normalizeUserText(text); const clean = normalized.trim(); if (!clean) return null; const safe = clean.length > 256 ? `${clean.slice(0, 253)}...` : clean; const message = buildMessage(null, safe, { - nickname: 'The Overseer', + nickname: String(options.nickname || 'The Overseer'), role: 'user', fromDiscord: false, system: true, diff --git a/server/src/services/overseerControlService/contextBuilder.js b/server/src/services/overseerControlService/contextBuilder.js index d960b098..dec83a03 100644 --- a/server/src/services/overseerControlService/contextBuilder.js +++ b/server/src/services/overseerControlService/contextBuilder.js @@ -51,7 +51,8 @@ function buildModelMessages({ systemPrompt, stateUpdate, transcriptRows, availab }); messages.push({ role: 'user', - content: 'Respond with one line: SKIP, CHAT, ACTION, or ACTION+CHAT.', + content: + 'Respond with JSON only: {"decision":"SKIP|CHAT|ACTION|ACTION+CHAT","chat":"optional text","actions":[{"tool":"tool_id","args":{}}]}. Use action tool ids only.', }); return messages; } diff --git a/server/src/services/overseerControlService/index.js b/server/src/services/overseerControlService/index.js index 24570665..1b9470d5 100644 --- a/server/src/services/overseerControlService/index.js +++ b/server/src/services/overseerControlService/index.js @@ -5,11 +5,14 @@ const logger = require('../../globals/logger').child('overseerControl'); const { loadConfig } = require('../../helpers/configLoader'); const { getRole, roleEvents } = require('../roleService'); const { getMode } = require('../modeManager'); -const { getState: getHomeAssistantState, homeAssistantEvents } = require('../homeAssistantService'); -const { getState: getNeatoState, neatoEvents } = require('../neatoService'); -const { getState: getLiftState, liftEvents } = require('../liftService'); +const homeAssistantService = require('../homeAssistantService'); +const neatoService = require('../neatoService'); +const liftService = require('../liftService'); +const { getState: getHomeAssistantState, homeAssistantEvents } = homeAssistantService; +const { getState: getNeatoState, neatoEvents } = neatoService; +const { getState: getLiftState, liftEvents } = liftService; const roverManager = require('../roverManager'); -const { getRecentMessages } = require('../chatService'); +const { getRecentMessages, sendSystemMessage } = require('../chatService'); const { PROMPT_PATH, DEFAULT_NAME, @@ -20,18 +23,9 @@ const { MAX_BOT_CONTEXT, normalizeMs, } = require('./constants'); -const { - isAdminRole, - buildAdminState, - normalizeDecision, - buildFailureInfo, -} = require('./runtimeHelpers'); -const { - toStateUpdate, - buildToolState, - buildConversation, - buildModelMessages, -} = require('./contextBuilder'); +const { isAdminRole, buildAdminState, parseOverseerOutput, buildFailureInfo } = require('./runtimeHelpers'); +const { toStateUpdate, buildToolState, buildConversation, buildModelMessages } = require('./contextBuilder'); +const { executeToolAction } = require('./tools'); const config = loadConfig(); const overseerConfig = config.overseerControl || {}; @@ -47,12 +41,12 @@ const ollamaClient = ollamaUrl ? new Ollama({ host: ollamaUrl }) : null; const runtime = { timer: null, inFlight: false, - running: false, tickCount: 0, lastModelAt: 0, generationCount: 0, generationTotalMs: 0, runHistory: [], + memoryStore: ['', '', ''], }; let status = { @@ -83,6 +77,9 @@ let status = { lastModelOutputAt: null, lastModelRawOutput: null, lastDecision: null, + lastChatDraft: null, + lastRequestedActions: null, + lastActionResults: null, lastOutcome: null, lastReason: null, lastError: null, @@ -95,11 +92,7 @@ let status = { }; function updateStatus(patch = {}) { - status = { - ...status, - ...patch, - updatedAt: Date.now(), - }; + status = { ...status, ...patch, updatedAt: Date.now() }; const payload = buildAdminState(status, runtime.runHistory); io.sockets.sockets.forEach((socket) => { if (!isAdminRole(getRole(socket))) return; @@ -131,26 +124,16 @@ function computeTriggerReason() { const last = recent[recent.length - 1]; if (last && Date.now() - Number(last.ts || 0) < 5000) { const txt = String(last.text || '').toLowerCase(); - if (txt.includes(name.toLowerCase()) || txt.includes('overseer') || txt.includes('bot')) { - return 'direct_address'; - } + if (txt.includes(name.toLowerCase()) || txt.includes('overseer') || txt.includes('bot')) return 'direct_address'; return 'chat_activity'; } - if (!runtime.lastModelAt || Date.now() - runtime.lastModelAt >= heartbeatMs) { - return 'heartbeat'; - } + if (!runtime.lastModelAt || Date.now() - runtime.lastModelAt >= heartbeatMs) return 'heartbeat'; return null; } async function runDecision(triggerReason) { const runId = runtime.tickCount; - updateStatus({ - phase: 'context_build', - currentRunId: runId, - lastTriggerReason: triggerReason, - lastError: null, - lastErrorDetails: null, - }); + updateStatus({ phase: 'context_build', currentRunId: runId, lastTriggerReason: triggerReason, lastError: null, lastErrorDetails: null }); const mode = getMode(); const homeAssistantState = getHomeAssistantState(); @@ -162,19 +145,11 @@ async function runDecision(triggerReason) { const toolState = buildToolState({ mode, homeAssistantState, neatoState, liftState }); const human = getRecentMessages(MAX_CHAT_CONTEXT, { includeSystem: false }); - const bots = getRecentMessages(100, { includeSystem: true }) - .filter((entry) => entry?.system) - .slice(-MAX_BOT_CONTEXT); + const bots = getRecentMessages(100, { includeSystem: true }).filter((entry) => entry?.system).slice(-MAX_BOT_CONTEXT); const transcriptRows = buildConversation({ recentMessages: [...human, ...bots].slice(-(MAX_CHAT_CONTEXT + MAX_BOT_CONTEXT)), name }); const systemPrompt = await readPrompt(); - const modelMessages = buildModelMessages({ - systemPrompt, - stateUpdate, - transcriptRows, - availableTools: toolState.available, - blockedTools: toolState.blocked, - }); + const modelMessages = buildModelMessages({ systemPrompt, stateUpdate, transcriptRows, availableTools: toolState.available, blockedTools: toolState.blocked }); updateStatus({ phase: 'awaiting_model', @@ -187,40 +162,73 @@ async function runDecision(triggerReason) { lastModelInputAt: Date.now(), }); - let decision = 'SKIP'; - let rawOutput = ''; + let parsed = { decision: 'SKIP', chat: null, actions: [], raw: '' }; const generationStart = Date.now(); if (ollamaClient && model) { const payload = await ollamaClient.chat({ model, stream: false, keep_alive: -1, - options: { - temperature: 0.4, - top_p: 0.9, - }, + options: { temperature: 0.4, top_p: 0.9 }, messages: modelMessages, }); - const parsed = normalizeDecision(payload?.message?.content || ''); - rawOutput = parsed.raw; - decision = parsed.decision; + parsed = parseOverseerOutput(payload?.message?.content || ''); } const generationMs = Math.max(0, Date.now() - generationStart); runtime.generationCount += 1; runtime.generationTotalMs += generationMs; const avgGenerationMs = Math.round(runtime.generationTotalMs / runtime.generationCount); - runtime.lastModelAt = Date.now(); - const outcome = observeOnly ? 'observed' : 'pending_execution'; + + const actionResults = []; + let outcome = observeOnly ? 'observed' : 'executed'; + let reason = observeOnly ? 'observe-only mode' : null; + + if (!observeOnly) { + if ((parsed.decision === 'CHAT' || parsed.decision === 'ACTION+CHAT') && parsed.chat) { + sendSystemMessage(parsed.chat, { nickname: name }); + actionResults.push({ kind: 'chat', ok: true }); + } + + if (parsed.decision === 'ACTION' || parsed.decision === 'ACTION+CHAT') { + for (const action of parsed.actions) { + const isAvailable = toolState.available.some((signature) => signature.startsWith(`${action.tool}(`)); + if (!isAvailable) { + actionResults.push({ kind: 'tool', tool: action.tool, ok: false, error: 'tool unavailable or blocked' }); + continue; + } + try { + const result = await executeToolAction(action, { + sendSystemMessage, + name, + memoryStore: runtime.memoryStore, + neatoService, + liftService, + homeAssistantService, + actor: 'overseerControl', + }); + if (action.tool === 'memory_write' && Array.isArray(result?.slots)) { + runtime.memoryStore = result.slots; + } + actionResults.push({ kind: 'tool', tool: action.tool, ok: true, result }); + } catch (err) { + actionResults.push({ kind: 'tool', tool: action.tool, ok: false, error: err.message }); + } + } + } + } updateStatus({ phase: 'decision_recorded', lastModelOutputAt: Date.now(), - lastModelRawOutput: rawOutput, - lastDecision: decision, + lastModelRawOutput: parsed.raw, + lastDecision: parsed.decision, + lastChatDraft: parsed.chat, + lastRequestedActions: parsed.actions, + lastActionResults: actionResults, lastOutcome: outcome, - lastReason: observeOnly ? 'observe-only mode' : null, + lastReason: reason, lastGenerationMs: generationMs, avgGenerationMs, generationCount: runtime.generationCount, @@ -230,7 +238,10 @@ async function runDecision(triggerReason) { runId, at: Date.now(), triggerReason, - decision, + decision: parsed.decision, + chatDraft: parsed.chat, + requestedActions: parsed.actions, + actionResults, outcome, observeOnly, generationMs, @@ -241,24 +252,15 @@ async function runDecision(triggerReason) { async function tick() { runtime.tickCount += 1; runtime.inFlight = true; - updateStatus({ - inFlight: true, - tickCount: runtime.tickCount, - lastTickAt: Date.now(), - phase: 'gate_check', - }); + updateStatus({ inFlight: true, tickCount: runtime.tickCount, lastTickAt: Date.now(), phase: 'gate_check' }); try { const triggerReason = computeTriggerReason(); if (!triggerReason) { - updateStatus({ - phase: 'idle', - lastOutcome: 'skipped', - lastReason: 'gate not triggered', - }); - return; + updateStatus({ phase: 'idle', lastOutcome: 'skipped', lastReason: 'gate not triggered' }); + } else { + await runDecision(triggerReason); } - await runDecision(triggerReason); } catch (err) { const failure = buildFailureInfo(err); updateStatus({ @@ -271,12 +273,7 @@ async function tick() { }); } finally { runtime.inFlight = false; - updateStatus({ - inFlight: false, - currentRunId: null, - phase: 'idle', - nextRunAt: Date.now() + gateIntervalMs, - }); + updateStatus({ inFlight: false, currentRunId: null, phase: 'idle', nextRunAt: Date.now() + gateIntervalMs }); runtime.timer = setTimeout(tick, gateIntervalMs); } } @@ -290,51 +287,33 @@ function clearHistory() { runtime.runHistory = []; runtime.generationCount = 0; runtime.generationTotalMs = 0; - updateStatus({ - lastReason: 'admin requested clear history', - lastOutcome: 'cleared', - }); + runtime.memoryStore = ['', '', '']; + updateStatus({ lastReason: 'admin requested clear history', lastOutcome: 'cleared' }); } io.on('connection', (socket) => { emitStateToSocket(socket); socket.on('overseer:control', ({ controls } = {}, cb = () => {}) => { - if (!isAdminRole(getRole(socket))) { - cb({ error: 'Not authorized' }); - return; - } + if (!isAdminRole(getRole(socket))) return cb({ error: 'Not authorized' }); const action = controls?.action || null; if (action === 'clearHistory') { clearHistory(); - cb({ success: true, state: buildAdminState(status, runtime.runHistory) }); - return; + return cb({ success: true, state: buildAdminState(status, runtime.runHistory) }); } - cb({ error: 'Unknown overseer control action' }); + return cb({ error: 'Unknown overseer control action' }); }); }); -roleEvents.on('change', ({ socket }) => { - emitStateToSocket(socket); -}); - -homeAssistantEvents.on('update', () => { - updateStatus({ phase: status.phase }); -}); -neatoEvents.on('update', () => { - updateStatus({ phase: status.phase }); -}); -liftEvents.on('update', () => { - updateStatus({ phase: status.phase }); -}); -roverManager.managerEvents.on('rover', () => { - updateStatus({ phase: status.phase }); -}); +roleEvents.on('change', ({ socket }) => emitStateToSocket(socket)); +homeAssistantEvents.on('update', () => updateStatus({ phase: status.phase })); +neatoEvents.on('update', () => updateStatus({ phase: status.phase })); +liftEvents.on('update', () => updateStatus({ phase: status.phase })); +roverManager.managerEvents.on('rover', () => updateStatus({ phase: status.phase })); if (!enabled) { logger.info('overseerControl disabled'); updateStatus({ running: false, lastReason: 'overseerControl.enabled is false' }); } else { - runtime.running = true; updateStatus({ running: true, lastReason: observeOnly ? 'observe-only mode' : null }); runtime.timer = setTimeout(tick, gateIntervalMs); logger.info('overseerControl enabled', { model, ollamaUrl, gateIntervalMs, heartbeatMs, observeOnly }); diff --git a/server/src/services/overseerControlService/runtimeHelpers.js b/server/src/services/overseerControlService/runtimeHelpers.js index aebef4a8..5d81c242 100644 --- a/server/src/services/overseerControlService/runtimeHelpers.js +++ b/server/src/services/overseerControlService/runtimeHelpers.js @@ -37,6 +37,9 @@ function buildAdminState(status, runHistory) { output: { raw: status.lastModelRawOutput, normalized: status.lastDecision, + chat: status.lastChatDraft, + actions: status.lastRequestedActions, + actionResults: status.lastActionResults, outputAt: status.lastModelOutputAt, outcome: status.lastOutcome, reason: status.lastReason, @@ -61,16 +64,34 @@ function buildAdminState(status, runHistory) { }; } -function normalizeDecision(rawContent = '') { +function parseOverseerOutput(rawContent = '') { const raw = typeof rawContent === 'string' ? rawContent : ''; const trimmed = raw.trim(); - if (!trimmed) return { raw, decision: 'SKIP' }; + if (!trimmed) return { raw, decision: 'SKIP', chat: null, actions: [] }; + try { + const parsed = JSON.parse(trimmed); + const decision = String(parsed?.decision || 'SKIP').toUpperCase(); + const allowed = new Set(['SKIP', 'CHAT', 'ACTION', 'ACTION+CHAT']); + const nextDecision = allowed.has(decision) ? decision : 'SKIP'; + const chat = typeof parsed?.chat === 'string' && parsed.chat.trim() ? parsed.chat.trim() : null; + const actions = Array.isArray(parsed?.actions) + ? parsed.actions + .map((entry) => ({ + tool: String(entry?.tool || '').trim(), + args: entry?.args && typeof entry.args === 'object' ? entry.args : {}, + })) + .filter((entry) => entry.tool.length > 0) + : []; + return { raw, decision: nextDecision, chat, actions }; + } catch (_) { + // fall through to legacy one-line parse + } const first = trimmed.split(/\r?\n/).map((line) => line.trim()).find(Boolean) || ''; const upper = first.toUpperCase(); if (['SKIP', 'CHAT', 'ACTION', 'ACTION+CHAT'].includes(upper)) { - return { raw, decision: upper }; + return { raw, decision: upper, chat: null, actions: [] }; } - return { raw, decision: 'CHAT' }; + return { raw, decision: 'CHAT', chat: first, actions: [] }; } function buildFailureInfo(err) { @@ -88,6 +109,6 @@ function buildFailureInfo(err) { module.exports = { isAdminRole, buildAdminState, - normalizeDecision, + parseOverseerOutput, buildFailureInfo, }; diff --git a/server/src/services/overseerControlService/tools/chatSay.js b/server/src/services/overseerControlService/tools/chatSay.js index 65fd4a73..f40b4621 100644 --- a/server/src/services/overseerControlService/tools/chatSay.js +++ b/server/src/services/overseerControlService/tools/chatSay.js @@ -1,6 +1,13 @@ module.exports = { + id: 'chat_say', signature: 'chat_say(text)', availability() { return { available: true, reason: null }; }, + async execute({ args = {}, sendSystemMessage, name }) { + const text = String(args?.text || '').trim(); + if (!text) throw new Error('chat_say requires args.text'); + sendSystemMessage(text, { nickname: name }); + return { ok: true }; + }, }; diff --git a/server/src/services/overseerControlService/tools/haSetEntity.js b/server/src/services/overseerControlService/tools/haSetEntity.js index 607b22c7..f962460f 100644 --- a/server/src/services/overseerControlService/tools/haSetEntity.js +++ b/server/src/services/overseerControlService/tools/haSetEntity.js @@ -1,4 +1,5 @@ module.exports = { + id: 'ha_set_entity', signature: 'ha_set_entity(entity_id, state)', availability(ctx = {}) { const mode = String(ctx.mode || ''); @@ -11,4 +12,12 @@ module.exports = { if (!ctx.homeAssistantState?.connected) return { available: false, reason: 'unavailable' }; return { available: true, reason: null }; }, + async execute({ args = {}, homeAssistantService }) { + const entityId = String(args?.entity_id || args?.entityId || '').trim(); + if (!entityId) throw new Error('ha_set_entity requires args.entity_id'); + const state = String(args?.state || '').toLowerCase(); + if (state !== 'on' && state !== 'off') throw new Error('ha_set_entity requires args.state of on/off'); + await homeAssistantService.setEntityState(entityId, state, { source: 'overseerControl' }); + return { ok: true }; + }, }; diff --git a/server/src/services/overseerControlService/tools/index.js b/server/src/services/overseerControlService/tools/index.js index d6cd0f34..2435566e 100644 --- a/server/src/services/overseerControlService/tools/index.js +++ b/server/src/services/overseerControlService/tools/index.js @@ -21,6 +21,7 @@ const TOOL_DEFINITIONS = [ neatoClearErrors, haSetEntity, ]; +const TOOL_BY_ID = new Map(TOOL_DEFINITIONS.map((tool) => [tool.id, tool])); function evaluateTools(context = {}) { const available = []; @@ -36,7 +37,20 @@ function evaluateTools(context = {}) { return { available, blocked }; } +async function executeToolAction(action = {}, context = {}) { + const toolId = String(action?.tool || '').trim(); + const tool = TOOL_BY_ID.get(toolId); + if (!tool) { + throw new Error(`Unknown tool: ${toolId}`); + } + if (typeof tool.execute !== 'function') { + throw new Error(`Tool ${toolId} is not executable`); + } + return tool.execute({ ...context, args: action?.args || {} }); +} + module.exports = { TOOL_DEFINITIONS, evaluateTools, + executeToolAction, }; diff --git a/server/src/services/overseerControlService/tools/liftDown.js b/server/src/services/overseerControlService/tools/liftDown.js index 93ffbd07..1a1def28 100644 --- a/server/src/services/overseerControlService/tools/liftDown.js +++ b/server/src/services/overseerControlService/tools/liftDown.js @@ -1,4 +1,5 @@ module.exports = { + id: 'lift_down', signature: 'lift_down()', availability(ctx = {}) { const mode = String(ctx.mode || ''); @@ -9,4 +10,8 @@ module.exports = { if (ctx.liftState?.busy) return { available: false, reason: 'busy' }; return { available: true, reason: null }; }, + async execute({ liftService, actor = 'overseerControl' }) { + const resp = await liftService.moveDown(actor); + return { ok: true, resp }; + }, }; diff --git a/server/src/services/overseerControlService/tools/liftUp.js b/server/src/services/overseerControlService/tools/liftUp.js index 206813fb..f26f7b98 100644 --- a/server/src/services/overseerControlService/tools/liftUp.js +++ b/server/src/services/overseerControlService/tools/liftUp.js @@ -1,4 +1,5 @@ module.exports = { + id: 'lift_up', signature: 'lift_up()', availability(ctx = {}) { const mode = String(ctx.mode || ''); @@ -9,4 +10,8 @@ module.exports = { if (ctx.liftState?.busy) return { available: false, reason: 'busy' }; return { available: true, reason: null }; }, + async execute({ liftService, actor = 'overseerControl' }) { + const resp = await liftService.moveUp(actor); + return { ok: true, resp }; + }, }; diff --git a/server/src/services/overseerControlService/tools/memoryRead.js b/server/src/services/overseerControlService/tools/memoryRead.js index 6c1569d9..9ff6c2df 100644 --- a/server/src/services/overseerControlService/tools/memoryRead.js +++ b/server/src/services/overseerControlService/tools/memoryRead.js @@ -1,6 +1,10 @@ module.exports = { + id: 'memory_read', signature: 'memory_read()', availability() { return { available: true, reason: null }; }, + async execute({ memoryStore }) { + return { ok: true, slots: memoryStore || ['', '', ''] }; + }, }; diff --git a/server/src/services/overseerControlService/tools/memoryWrite.js b/server/src/services/overseerControlService/tools/memoryWrite.js index 867d054a..7c974d77 100644 --- a/server/src/services/overseerControlService/tools/memoryWrite.js +++ b/server/src/services/overseerControlService/tools/memoryWrite.js @@ -1,6 +1,17 @@ module.exports = { + id: 'memory_write', signature: 'memory_write(slot, text)', availability() { return { available: true, reason: null }; }, + async execute({ args = {}, memoryStore }) { + const slot = Math.max(1, Math.min(3, Number(args?.slot) || 0)); + if (!slot) throw new Error('memory_write requires args.slot 1..3'); + const text = String(args?.text || '').trim(); + if (!text) throw new Error('memory_write requires args.text'); + const next = Array.isArray(memoryStore) ? [...memoryStore] : ['', '', '']; + while (next.length < 3) next.push(''); + next[slot - 1] = text.slice(0, 180); + return { ok: true, slots: next }; + }, }; diff --git a/server/src/services/overseerControlService/tools/neatoClearErrors.js b/server/src/services/overseerControlService/tools/neatoClearErrors.js index df2ce727..999a6fc5 100644 --- a/server/src/services/overseerControlService/tools/neatoClearErrors.js +++ b/server/src/services/overseerControlService/tools/neatoClearErrors.js @@ -1,4 +1,5 @@ module.exports = { + id: 'neato_clear_errors', signature: 'neato_clear_errors()', availability(ctx = {}) { const mode = String(ctx.mode || ''); @@ -8,4 +9,8 @@ module.exports = { if (!ctx.neatoState?.connected) return { available: false, reason: 'unavailable' }; return { available: true, reason: null }; }, + async execute({ neatoService }) { + await neatoService.clearErrors(); + return { ok: true }; + }, }; diff --git a/server/src/services/overseerControlService/tools/neatoLocate.js b/server/src/services/overseerControlService/tools/neatoLocate.js index dba2d5b9..68380e46 100644 --- a/server/src/services/overseerControlService/tools/neatoLocate.js +++ b/server/src/services/overseerControlService/tools/neatoLocate.js @@ -1,4 +1,5 @@ module.exports = { + id: 'neato_locate', signature: 'neato_locate()', availability(ctx = {}) { const mode = String(ctx.mode || ''); @@ -8,4 +9,8 @@ module.exports = { if (!ctx.neatoState?.connected) return { available: false, reason: 'unavailable' }; return { available: true, reason: null }; }, + async execute({ neatoService }) { + await neatoService.locateRobot(); + return { ok: true }; + }, }; diff --git a/server/src/services/overseerControlService/tools/neatoSendHome.js b/server/src/services/overseerControlService/tools/neatoSendHome.js index 84a87fd2..c123fe75 100644 --- a/server/src/services/overseerControlService/tools/neatoSendHome.js +++ b/server/src/services/overseerControlService/tools/neatoSendHome.js @@ -1,4 +1,5 @@ module.exports = { + id: 'neato_send_home', signature: 'neato_send_home()', availability(ctx = {}) { const mode = String(ctx.mode || ''); @@ -8,4 +9,8 @@ module.exports = { if (!ctx.neatoState?.connected) return { available: false, reason: 'unavailable' }; return { available: true, reason: null }; }, + async execute({ neatoService }) { + await neatoService.sendHome(); + return { ok: true }; + }, }; diff --git a/server/src/services/overseerControlService/tools/neatoStart.js b/server/src/services/overseerControlService/tools/neatoStart.js index 659f827d..5164ea7b 100644 --- a/server/src/services/overseerControlService/tools/neatoStart.js +++ b/server/src/services/overseerControlService/tools/neatoStart.js @@ -1,4 +1,5 @@ module.exports = { + id: 'neato_start', signature: 'neato_start()', availability(ctx = {}) { const mode = String(ctx.mode || ''); @@ -8,4 +9,8 @@ module.exports = { if (!ctx.neatoState?.connected) return { available: false, reason: 'unavailable' }; return { available: true, reason: null }; }, + async execute({ neatoService }) { + await neatoService.startCleaning(); + return { ok: true }; + }, };