overseer v2 5 (real tools)

This commit is contained in:
legop3
2026-05-03 23:57:36 -04:00
parent 7eb33c9af2
commit 5e2c836cf2
18 changed files with 277 additions and 44 deletions
+6 -7
View File
@@ -1,14 +1,13 @@
You are <NAME>, a control-first room AI for a live rover room. You are <NAME>, a control-first room AI for a live rover room.
Rules: Rules:
- You may choose SKIP, CHAT, ACTION, or ACTION+CHAT. - You may chat and call tools.
- Respect tool availability and blocked tool reasons. - Respect safety, cooldowns, and blocked tool reasons.
- Prefer restraint over spam. - Prefer restraint over spam.
- If directly addressed, avoid ignoring users. - If directly addressed, avoid ignoring users.
- Keep lines concise and in-character. - Keep lines concise and in-character.
Output contract: Tool behavior:
- Return valid JSON only. - Use tool calls when actions are needed.
- Shape: - Do not invent tools.
{"decision":"SKIP|CHAT|ACTION|ACTION+CHAT","chat":"optional text","actions":[{"tool":"tool_id","args":{}}]} - If no action is needed, either respond with one short chat line or SKIP.
- Use `tool_id` values only from available tools.
@@ -142,6 +142,32 @@ function createButtonBoxCore(deps) {
return store.clone(button); return store.clone(button);
} }
async function addCount(buttonId, amount = 1) {
const state = store.getState();
const button = state.buttons.find((entry) => entry.id === buttonId);
if (!button) {
throw new Error('Unknown button');
}
const inc = Math.max(1, Math.floor(Number(amount) || 0));
button.count += inc;
button.lastIncrementAt = Date.now();
store.writeState();
io.emit('buttonBox:increment', {
buttonId,
count: button.count,
ts: button.lastIncrementAt,
});
while (button.count >= button.goal) {
await runRewardForButton(button);
store.writeState();
}
publishUpdated();
return store.clone(button);
}
async function recoverEffects() { async function recoverEffects() {
const state = store.getState(); const state = store.getState();
const effects = state.effects && typeof state.effects === 'object' ? { ...state.effects } : {}; const effects = state.effects && typeof state.effects === 'object' ? { ...state.effects } : {};
@@ -165,6 +191,7 @@ function createButtonBoxCore(deps) {
return { return {
applyPress, applyPress,
addCount,
recoverEffects, recoverEffects,
}; };
} }
@@ -79,4 +79,5 @@ core.recoverEffects().catch((err) => {
module.exports = { module.exports = {
getButtonBoxState: store.getStateClone, getButtonBoxState: store.getStateClone,
addButtonBoxCount: core.addCount,
}; };
@@ -8,6 +8,13 @@ function toStateUpdate({ mode, homeAssistantState, neatoState, liftState, roster
lines.push(`lights_locked_on: ${homeAssistantState?.lightPolicy?.lockedOn ? 'yes' : 'no'}`); lines.push(`lights_locked_on: ${homeAssistantState?.lightPolicy?.lockedOn ? 'yes' : 'no'}`);
lines.push(`lift: ${liftState?.connected ? 'connected' : 'offline'} busy=${liftState?.busy ? 'yes' : 'no'}`); lines.push(`lift: ${liftState?.connected ? 'connected' : 'offline'} busy=${liftState?.busy ? 'yes' : 'no'}`);
lines.push(`neato: ${neatoState?.connected ? 'connected' : 'offline'} state=${neatoState?.telemetry?.robotState || 'unknown'}`); lines.push(`neato: ${neatoState?.connected ? 'connected' : 'offline'} state=${neatoState?.telemetry?.robotState || 'unknown'}`);
const entities = Array.isArray(homeAssistantState?.entities) ? homeAssistantState.entities : [];
if (entities.length) {
lines.push('home_assistant_entities:');
entities.slice(0, 24).forEach((entity) => {
lines.push(`- ${entity.id} (${entity.type || 'entity'}) state=${entity.state || 'unknown'} available=${entity.available ? 'yes' : 'no'}`);
});
}
const roverLines = (Array.isArray(roster) ? roster : []).slice(0, 6).map((rover) => { const roverLines = (Array.isArray(roster) ? roster : []).slice(0, 6).map((rover) => {
const roverId = rover?.id || 'unknown'; const roverId = rover?.id || 'unknown';
const driver = rover?.driverNickname || 'none'; const driver = rover?.driverNickname || 'none';
@@ -60,7 +67,7 @@ function buildModelMessages({ systemPrompt, stateUpdate, conversationMessages, a
messages.push({ messages.push({
role: 'user', role: 'user',
content: content:
'Respond with JSON only: {"decision":"SKIP|CHAT|ACTION|ACTION+CHAT","chat":"optional text","actions":[{"tool":"tool_id","args":{}}]}. Use action tool ids only.', 'Use tool calls when needed. If no tool is needed, either respond with one short chat line or SKIP.',
}); });
return messages; return messages;
} }
@@ -8,6 +8,7 @@ const { getMode } = require('../modeManager');
const homeAssistantService = require('../homeAssistantService'); const homeAssistantService = require('../homeAssistantService');
const neatoService = require('../neatoService'); const neatoService = require('../neatoService');
const liftService = require('../liftService'); const liftService = require('../liftService');
const buttonBoxService = require('../buttonBoxService');
const { getState: getHomeAssistantState, homeAssistantEvents } = homeAssistantService; const { getState: getHomeAssistantState, homeAssistantEvents } = homeAssistantService;
const { getState: getNeatoState, neatoEvents } = neatoService; const { getState: getNeatoState, neatoEvents } = neatoService;
const { getState: getLiftState, liftEvents } = liftService; const { getState: getLiftState, liftEvents } = liftService;
@@ -23,9 +24,10 @@ const {
MAX_BOT_CONTEXT, MAX_BOT_CONTEXT,
normalizeMs, normalizeMs,
} = require('./constants'); } = require('./constants');
const { isAdminRole, buildAdminState, parseOverseerOutput, buildFailureInfo } = require('./runtimeHelpers'); const { isAdminRole, buildAdminState, buildFailureInfo } = require('./runtimeHelpers');
const { toStateUpdate, buildToolState, buildConversation, buildModelMessages } = require('./contextBuilder'); const { toStateUpdate, buildToolState, buildConversation, buildModelMessages } = require('./contextBuilder');
const { executeToolAction } = require('./tools'); const { buildOllamaTools, executeToolAction } = require('./tools');
const { loadMemory, saveMemory, createDefaultMemory } = require('./memoryStore');
const config = loadConfig(); const config = loadConfig();
const overseerConfig = config.overseerControl || {}; const overseerConfig = config.overseerControl || {};
@@ -46,7 +48,7 @@ const runtime = {
generationCount: 0, generationCount: 0,
generationTotalMs: 0, generationTotalMs: 0,
runHistory: [], runHistory: [],
memoryStore: ['', '', ''], memoryStore: loadMemory(),
}; };
let status = { let status = {
@@ -112,11 +114,14 @@ async function readPrompt() {
} }
function buildRosterSummary() { function buildRosterSummary() {
return roverManager.getRoster().map((rover) => ({ return roverManager
id: rover?.id || 'unknown', .getRoster()
statusTag: rover?.statusTag || 'unknown', .filter((rover) => roverManager.canReplayRoverId(rover?.id))
driverNickname: rover?.driverNickname || null, .map((rover) => ({
})); id: rover?.id || 'unknown',
statusTag: rover?.statusTag || 'unknown',
driverNickname: rover?.driverNickname || null,
}));
} }
function computeTriggerReason() { function computeTriggerReason() {
@@ -131,6 +136,36 @@ function computeTriggerReason() {
return null; return null;
} }
function normalizeToolCalls(payload = null) {
const calls = Array.isArray(payload?.message?.tool_calls) ? payload.message.tool_calls : [];
return calls
.map((call) => {
const fn = call?.function || {};
const tool = String(fn.name || '').trim();
if (!tool) return null;
let args = fn.arguments;
if (typeof args === 'string') {
try {
args = JSON.parse(args);
} catch {
args = {};
}
}
if (!args || typeof args !== 'object') args = {};
return { tool, args };
})
.filter(Boolean);
}
function inferDecision({ toolCalls, chatText }) {
const hasTools = (toolCalls || []).length > 0;
const hasChat = Boolean(String(chatText || '').trim());
if (hasTools && hasChat) return 'ACTION+CHAT';
if (hasTools) return 'ACTION';
if (hasChat) return 'CHAT';
return 'SKIP';
}
async function runDecision(triggerReason) { async function runDecision(triggerReason) {
const runId = runtime.tickCount; 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 });
@@ -144,7 +179,12 @@ async function runDecision(triggerReason) {
const stateUpdate = toStateUpdate({ mode, homeAssistantState, neatoState, liftState, roster, triggerReason }); const stateUpdate = toStateUpdate({ mode, homeAssistantState, neatoState, liftState, roster, triggerReason });
const toolState = buildToolState({ mode, homeAssistantState, neatoState, liftState }); const toolState = buildToolState({ mode, homeAssistantState, neatoState, liftState });
const recentConversation = getRecentMessages(MAX_CHAT_CONTEXT + MAX_BOT_CONTEXT, { includeSystem: true }); const recentConversation = getRecentMessages(MAX_CHAT_CONTEXT + MAX_BOT_CONTEXT + 20, { includeSystem: true })
.filter((entry) => {
if (!entry?.roverId) return true;
return roverManager.canReplayRoverId(entry.roverId);
})
.slice(-(MAX_CHAT_CONTEXT + MAX_BOT_CONTEXT));
const conversationMessages = buildConversation({ recentMessages: recentConversation, name }); const conversationMessages = buildConversation({ recentMessages: recentConversation, name });
const systemPrompt = await readPrompt(); const systemPrompt = await readPrompt();
@@ -155,6 +195,7 @@ async function runDecision(triggerReason) {
availableTools: toolState.available, availableTools: toolState.available,
blockedTools: toolState.blocked, blockedTools: toolState.blocked,
}); });
const ollamaTools = buildOllamaTools(toolState.availableIds);
updateStatus({ updateStatus({
phase: 'awaiting_model', phase: 'awaiting_model',
@@ -167,19 +208,24 @@ async function runDecision(triggerReason) {
lastModelInputAt: Date.now(), lastModelInputAt: Date.now(),
}); });
let parsed = { decision: 'SKIP', chat: null, actions: [], raw: '' };
const generationStart = Date.now(); const generationStart = Date.now();
let payload = null;
if (ollamaClient && model) { if (ollamaClient && model) {
const payload = await ollamaClient.chat({ payload = await ollamaClient.chat({
model, model,
stream: false, stream: false,
keep_alive: -1, keep_alive: -1,
options: { temperature: 0.4, top_p: 0.9 }, options: { temperature: 0.4, top_p: 0.9 },
messages: modelMessages, messages: modelMessages,
tools: ollamaTools,
}); });
parsed = parseOverseerOutput(payload?.message?.content || '');
} }
const rawOutput = String(payload?.message?.content || '');
const toolCalls = normalizeToolCalls(payload);
const chatDraft = rawOutput.trim() || null;
const decision = inferDecision({ toolCalls, chatText: chatDraft });
const generationMs = Math.max(0, Date.now() - generationStart); const generationMs = Math.max(0, Date.now() - generationStart);
runtime.generationCount += 1; runtime.generationCount += 1;
runtime.generationTotalMs += generationMs; runtime.generationTotalMs += generationMs;
@@ -187,33 +233,37 @@ async function runDecision(triggerReason) {
runtime.lastModelAt = Date.now(); runtime.lastModelAt = Date.now();
const actionResults = []; const actionResults = [];
const requestedActions = toolCalls;
let outcome = observeOnly ? 'observed' : 'executed'; let outcome = observeOnly ? 'observed' : 'executed';
let reason = observeOnly ? 'observe-only mode' : null; const reason = observeOnly ? 'observe-only mode' : null;
if (!observeOnly) { if (!observeOnly) {
if ((parsed.decision === 'CHAT' || parsed.decision === 'ACTION+CHAT') && parsed.chat) { if ((decision === 'CHAT' || decision === 'ACTION+CHAT') && chatDraft) {
sendSystemMessage(parsed.chat, { nickname: name }); sendSystemMessage(chatDraft, { nickname: name });
actionResults.push({ kind: 'chat', ok: true }); actionResults.push({ kind: 'chat', ok: true });
} }
if (parsed.decision === 'ACTION' || parsed.decision === 'ACTION+CHAT') { if (decision === 'ACTION' || decision === 'ACTION+CHAT') {
for (const action of parsed.actions) { for (const action of requestedActions) {
const isAvailable = toolState.available.some((signature) => signature.startsWith(`${action.tool}(`)); if (!toolState.availableIds.includes(action.tool)) {
if (!isAvailable) {
actionResults.push({ kind: 'tool', tool: action.tool, ok: false, error: 'tool unavailable or blocked' }); actionResults.push({ kind: 'tool', tool: action.tool, ok: false, error: 'tool unavailable or blocked' });
continue; continue;
} }
try { try {
const result = await executeToolAction(action, { const result = await executeToolAction(action.tool, action.args, {
sendSystemMessage, sendSystemMessage,
name, name,
memoryStore: runtime.memoryStore, memoryStore: runtime.memoryStore,
neatoService, neatoService,
liftService, liftService,
homeAssistantService, homeAssistantService,
buttonBoxService,
actor: 'overseerControl', actor: 'overseerControl',
}); });
if (action.tool === 'memory_write' && Array.isArray(result?.slots)) { if (action.tool === 'memory_write' && Array.isArray(result?.slots)) {
runtime.memoryStore = saveMemory(result.slots);
}
if (action.tool === 'memory_read' && Array.isArray(result?.slots)) {
runtime.memoryStore = result.slots; runtime.memoryStore = result.slots;
} }
actionResults.push({ kind: 'tool', tool: action.tool, ok: true, result }); actionResults.push({ kind: 'tool', tool: action.tool, ok: true, result });
@@ -227,10 +277,10 @@ async function runDecision(triggerReason) {
updateStatus({ updateStatus({
phase: 'decision_recorded', phase: 'decision_recorded',
lastModelOutputAt: Date.now(), lastModelOutputAt: Date.now(),
lastModelRawOutput: parsed.raw, lastModelRawOutput: rawOutput,
lastDecision: parsed.decision, lastDecision: decision,
lastChatDraft: parsed.chat, lastChatDraft: chatDraft,
lastRequestedActions: parsed.actions, lastRequestedActions: requestedActions,
lastActionResults: actionResults, lastActionResults: actionResults,
lastOutcome: outcome, lastOutcome: outcome,
lastReason: reason, lastReason: reason,
@@ -243,9 +293,9 @@ async function runDecision(triggerReason) {
runId, runId,
at: Date.now(), at: Date.now(),
triggerReason, triggerReason,
decision: parsed.decision, decision,
chatDraft: parsed.chat, chatDraft,
requestedActions: parsed.actions, requestedActions,
actionResults, actionResults,
outcome, outcome,
observeOnly, observeOnly,
@@ -292,7 +342,7 @@ function clearHistory() {
runtime.runHistory = []; runtime.runHistory = [];
runtime.generationCount = 0; runtime.generationCount = 0;
runtime.generationTotalMs = 0; runtime.generationTotalMs = 0;
runtime.memoryStore = ['', '', '']; runtime.memoryStore = saveMemory(createDefaultMemory());
updateStatus({ lastReason: 'admin requested clear history', lastOutcome: 'cleared' }); updateStatus({ lastReason: 'admin requested clear history', lastOutcome: 'cleared' });
} }
@@ -0,0 +1,36 @@
const fs = require('fs');
const { resolveDataPath } = require('../../helpers/dataPaths');
const STORE_PATH = resolveDataPath('overseer-control-memory.json');
function createDefaultMemory() {
return ['', '', ''];
}
function sanitizeSlots(slots) {
const next = Array.isArray(slots) ? slots.slice(0, 3) : [];
while (next.length < 3) next.push('');
return next.map((entry) => String(entry || '').slice(0, 180));
}
function loadMemory() {
try {
const raw = JSON.parse(fs.readFileSync(STORE_PATH, 'utf8'));
return sanitizeSlots(raw?.slots);
} catch {
return createDefaultMemory();
}
}
function saveMemory(slots) {
const next = sanitizeSlots(slots);
const payload = { updatedAt: Date.now(), slots: next };
fs.writeFileSync(STORE_PATH, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
return next;
}
module.exports = {
loadMemory,
saveMemory,
createDefaultMemory,
};
@@ -0,0 +1,29 @@
module.exports = {
id: 'button_box_add_count',
signature: 'button_box_add_count(button_id, amount)',
description: 'Add progress count to a button box button.',
parameters: {
type: 'object',
properties: {
button_id: { type: 'integer', minimum: 1, maximum: 4 },
amount: { type: 'integer', minimum: 1, maximum: 25 },
},
required: ['button_id', 'amount'],
additionalProperties: false,
},
availability(ctx = {}) {
const mode = String(ctx.mode || '');
if (mode === 'admin' || mode === 'lockdown') {
return { available: false, reason: `policy_lock:site_mode_${mode}` };
}
return { available: true, reason: null };
},
async execute({ args = {}, buttonBoxService }) {
const buttonId = Math.max(1, Math.min(4, Number(args?.button_id) || 0));
const amount = Math.max(1, Math.min(25, Number(args?.amount) || 0));
if (!buttonId) throw new Error('button_box_add_count requires args.button_id 1..4');
if (!amount) throw new Error('button_box_add_count requires args.amount 1..25');
const resp = await buttonBoxService.addButtonBoxCount(buttonId, amount);
return { ok: true, button: resp };
},
};
@@ -1,6 +1,15 @@
module.exports = { module.exports = {
id: 'chat_say', id: 'chat_say',
signature: 'chat_say(text)', signature: 'chat_say(text)',
description: 'Post a chat line as the Overseer bot.',
parameters: {
type: 'object',
properties: {
text: { type: 'string', minLength: 1, maxLength: 220 },
},
required: ['text'],
additionalProperties: false,
},
availability() { availability() {
return { available: true, reason: null }; return { available: true, reason: null };
}, },
@@ -1,6 +1,16 @@
module.exports = { module.exports = {
id: 'ha_set_entity', id: 'ha_set_entity',
signature: 'ha_set_entity(entity_id, state)', signature: 'ha_set_entity(entity_id, state)',
description: 'Set Home Assistant controllable entity on/off.',
parameters: {
type: 'object',
properties: {
entity_id: { type: 'string', minLength: 1 },
state: { type: 'string', enum: ['on', 'off'] },
},
required: ['entity_id', 'state'],
additionalProperties: false,
},
availability(ctx = {}) { availability(ctx = {}) {
const mode = String(ctx.mode || ''); const mode = String(ctx.mode || '');
if (mode === 'admin' || mode === 'lockdown') { if (mode === 'admin' || mode === 'lockdown') {
@@ -15,6 +25,10 @@ module.exports = {
async execute({ args = {}, homeAssistantService }) { async execute({ args = {}, homeAssistantService }) {
const entityId = String(args?.entity_id || args?.entityId || '').trim(); const entityId = String(args?.entity_id || args?.entityId || '').trim();
if (!entityId) throw new Error('ha_set_entity requires args.entity_id'); if (!entityId) throw new Error('ha_set_entity requires args.entity_id');
const allowed = new Set(
(homeAssistantService.getState()?.entities || []).map((entry) => String(entry?.id || '')).filter(Boolean),
);
if (!allowed.has(entityId)) throw new Error('ha_set_entity entity_id not configured');
const state = String(args?.state || '').toLowerCase(); const state = String(args?.state || '').toLowerCase();
if (state !== 'on' && state !== 'off') throw new Error('ha_set_entity requires args.state of on/off'); if (state !== 'on' && state !== 'off') throw new Error('ha_set_entity requires args.state of on/off');
await homeAssistantService.setEntityState(entityId, state, { source: 'overseerControl' }); await homeAssistantService.setEntityState(entityId, state, { source: 'overseerControl' });
@@ -8,6 +8,7 @@ const neatoSendHome = require('./neatoSendHome');
const neatoLocate = require('./neatoLocate'); const neatoLocate = require('./neatoLocate');
const neatoClearErrors = require('./neatoClearErrors'); const neatoClearErrors = require('./neatoClearErrors');
const haSetEntity = require('./haSetEntity'); const haSetEntity = require('./haSetEntity');
const buttonBoxAddCount = require('./buttonBoxAddCount');
const TOOL_DEFINITIONS = [ const TOOL_DEFINITIONS = [
chatSay, chatSay,
@@ -20,37 +21,69 @@ const TOOL_DEFINITIONS = [
neatoLocate, neatoLocate,
neatoClearErrors, neatoClearErrors,
haSetEntity, haSetEntity,
buttonBoxAddCount,
]; ];
const TOOL_BY_ID = new Map(TOOL_DEFINITIONS.map((tool) => [tool.id, tool])); const TOOL_BY_ID = new Map(TOOL_DEFINITIONS.map((tool) => [tool.id, tool]));
function evaluateTools(context = {}) { function evaluateTools(context = {}) {
const available = []; const available = [];
const availableIds = [];
const blocked = []; const blocked = [];
TOOL_DEFINITIONS.forEach((tool) => { TOOL_DEFINITIONS.forEach((tool) => {
const result = typeof tool.availability === 'function' ? tool.availability(context) : { available: false, reason: 'unavailable' }; const result = typeof tool.availability === 'function' ? tool.availability(context) : { available: false, reason: 'unavailable' };
if (result?.available) { if (result?.available) {
available.push(tool.signature); available.push(tool.signature);
availableIds.push(tool.id);
return; return;
} }
blocked.push({ tool: tool.signature, reason: result?.reason || 'unavailable' }); blocked.push({ id: tool.id, tool: tool.signature, reason: result?.reason || 'unavailable' });
}); });
return { available, blocked }; return { available, availableIds, blocked };
} }
async function executeToolAction(action = {}, context = {}) { function buildOllamaTools(availableIds = []) {
const toolId = String(action?.tool || '').trim(); const allowed = new Set((availableIds || []).map((id) => String(id)));
const tool = TOOL_BY_ID.get(toolId); return TOOL_DEFINITIONS.filter((tool) => allowed.has(tool.id)).map((tool) => ({
type: 'function',
function: {
name: tool.id,
description: String(tool.description || tool.signature || tool.id),
parameters: tool.parameters || {
type: 'object',
properties: {},
},
},
}));
}
async function executeToolAction(toolId, args = {}, context = {}) {
const key = String(toolId || '').trim();
if (!key) throw new Error('Missing tool id');
const tool = TOOL_BY_ID.get(key);
if (!tool) { if (!tool) {
throw new Error(`Unknown tool: ${toolId}`); throw new Error(`Unknown tool: ${key}`);
} }
if (typeof tool.execute !== 'function') { if (typeof tool.execute !== 'function') {
throw new Error(`Tool ${toolId} is not executable`); throw new Error(`Tool ${key} is not executable`);
} }
return tool.execute({ ...context, args: action?.args || {} }); return tool.execute({ ...context, args: args || {} });
}
function getToolById(toolId) {
return TOOL_BY_ID.get(String(toolId || '').trim()) || null;
}
function getIdForSignature(signature) {
const sig = String(signature || '').trim();
const match = TOOL_DEFINITIONS.find((tool) => tool.signature === sig);
return match?.id || null;
} }
module.exports = { module.exports = {
TOOL_DEFINITIONS, TOOL_DEFINITIONS,
evaluateTools, evaluateTools,
buildOllamaTools,
executeToolAction, executeToolAction,
getToolById,
getIdForSignature,
}; };
@@ -1,6 +1,8 @@
module.exports = { module.exports = {
id: 'lift_down', id: 'lift_down',
signature: 'lift_down()', signature: 'lift_down()',
description: 'Move the lift downward.',
parameters: { type: 'object', properties: {}, additionalProperties: false },
availability(ctx = {}) { availability(ctx = {}) {
const mode = String(ctx.mode || ''); const mode = String(ctx.mode || '');
if (mode === 'admin' || mode === 'lockdown') { if (mode === 'admin' || mode === 'lockdown') {
@@ -1,6 +1,8 @@
module.exports = { module.exports = {
id: 'lift_up', id: 'lift_up',
signature: 'lift_up()', signature: 'lift_up()',
description: 'Move the lift upward.',
parameters: { type: 'object', properties: {}, additionalProperties: false },
availability(ctx = {}) { availability(ctx = {}) {
const mode = String(ctx.mode || ''); const mode = String(ctx.mode || '');
if (mode === 'admin' || mode === 'lockdown') { if (mode === 'admin' || mode === 'lockdown') {
@@ -1,6 +1,12 @@
module.exports = { module.exports = {
id: 'memory_read', id: 'memory_read',
signature: 'memory_read()', signature: 'memory_read()',
description: 'Read the Overseer persistent 3-slot memory.',
parameters: {
type: 'object',
properties: {},
additionalProperties: false,
},
availability() { availability() {
return { available: true, reason: null }; return { available: true, reason: null };
}, },
@@ -1,6 +1,16 @@
module.exports = { module.exports = {
id: 'memory_write', id: 'memory_write',
signature: 'memory_write(slot, text)', signature: 'memory_write(slot, text)',
description: 'Write one of 3 Overseer memory slots.',
parameters: {
type: 'object',
properties: {
slot: { type: 'integer', minimum: 1, maximum: 3 },
text: { type: 'string', minLength: 1, maxLength: 180 },
},
required: ['slot', 'text'],
additionalProperties: false,
},
availability() { availability() {
return { available: true, reason: null }; return { available: true, reason: null };
}, },
@@ -1,6 +1,8 @@
module.exports = { module.exports = {
id: 'neato_clear_errors', id: 'neato_clear_errors',
signature: 'neato_clear_errors()', signature: 'neato_clear_errors()',
description: 'Clear Neato errors.',
parameters: { type: 'object', properties: {}, additionalProperties: false },
availability(ctx = {}) { availability(ctx = {}) {
const mode = String(ctx.mode || ''); const mode = String(ctx.mode || '');
if (mode === 'admin' || mode === 'lockdown') { if (mode === 'admin' || mode === 'lockdown') {
@@ -1,6 +1,8 @@
module.exports = { module.exports = {
id: 'neato_locate', id: 'neato_locate',
signature: 'neato_locate()', signature: 'neato_locate()',
description: 'Play Neato locate/chime action.',
parameters: { type: 'object', properties: {}, additionalProperties: false },
availability(ctx = {}) { availability(ctx = {}) {
const mode = String(ctx.mode || ''); const mode = String(ctx.mode || '');
if (mode === 'admin' || mode === 'lockdown') { if (mode === 'admin' || mode === 'lockdown') {
@@ -1,6 +1,8 @@
module.exports = { module.exports = {
id: 'neato_send_home', id: 'neato_send_home',
signature: 'neato_send_home()', signature: 'neato_send_home()',
description: 'Send Neato back to base.',
parameters: { type: 'object', properties: {}, additionalProperties: false },
availability(ctx = {}) { availability(ctx = {}) {
const mode = String(ctx.mode || ''); const mode = String(ctx.mode || '');
if (mode === 'admin' || mode === 'lockdown') { if (mode === 'admin' || mode === 'lockdown') {
@@ -1,6 +1,8 @@
module.exports = { module.exports = {
id: 'neato_start', id: 'neato_start',
signature: 'neato_start()', signature: 'neato_start()',
description: 'Start Neato cleaning cycle.',
parameters: { type: 'object', properties: {}, additionalProperties: false },
availability(ctx = {}) { availability(ctx = {}) {
const mode = String(ctx.mode || ''); const mode = String(ctx.mode || '');
if (mode === 'admin' || mode === 'lockdown') { if (mode === 'admin' || mode === 'lockdown') {