mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 09:31:20 -04:00
overseer v2 3
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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 };
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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 };
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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 };
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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 };
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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 || ['', '', ''] };
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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 };
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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 };
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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 };
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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 };
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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 };
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user