overseer v2 1

This commit is contained in:
legop3
2026-05-03 22:51:50 -04:00
parent 32e4afdbe8
commit 850f89c8ed
26 changed files with 929 additions and 128 deletions
@@ -107,6 +107,7 @@ function buildMessage(socket, text, meta = {}) {
text,
tts: meta.tts || null,
system: Boolean(meta.system),
bot: Boolean(meta.bot),
};
}
+1
View File
@@ -18,6 +18,7 @@ function sendSystemMessage(text) {
role: 'user',
fromDiscord: false,
system: true,
bot: true,
});
broadcastMessage(message);
return message;
@@ -0,0 +1,26 @@
const path = require('path');
const PROMPT_PATH = path.join(__dirname, '..', '..', '..', 'prompts', 'overseer_control_system.txt');
const DEFAULT_NAME = 'The Overseer';
const DEFAULT_GATE_INTERVAL_MS = 2000;
const DEFAULT_HEARTBEAT_MS = 30000;
const MIN_INTERVAL_MS = 250;
const MAX_RUN_HISTORY = 100;
const MAX_CHAT_CONTEXT = 12;
const MAX_BOT_CONTEXT = 2;
function normalizeMs(value, fallback) {
if (!Number.isFinite(value)) return fallback;
return Math.max(MIN_INTERVAL_MS, Math.floor(value));
}
module.exports = {
PROMPT_PATH,
DEFAULT_NAME,
DEFAULT_GATE_INTERVAL_MS,
DEFAULT_HEARTBEAT_MS,
MAX_RUN_HISTORY,
MAX_CHAT_CONTEXT,
MAX_BOT_CONTEXT,
normalizeMs,
};
@@ -0,0 +1,64 @@
const { evaluateTools } = require('./tools');
function toStateUpdate({ mode, homeAssistantState, neatoState, liftState, roster, triggerReason }) {
const lines = [];
lines.push(`trigger: ${triggerReason || 'heartbeat'}`);
lines.push(`mode: ${mode || 'unknown'}`);
lines.push(`home_assistant_connected: ${homeAssistantState?.connected ? '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(`neato: ${neatoState?.connected ? 'connected' : 'offline'} state=${neatoState?.telemetry?.robotState || 'unknown'}`);
const roverLines = (Array.isArray(roster) ? roster : []).slice(0, 6).map((rover) => {
const roverId = rover?.id || 'unknown';
const driver = rover?.driverNickname || 'none';
const status = rover?.statusTag || 'unknown';
return `- ${roverId} status=${status} driver=${driver}`;
});
if (roverLines.length) {
lines.push('rovers:');
lines.push(...roverLines);
}
return lines.join('\n');
}
function buildToolState({ mode, homeAssistantState, neatoState, liftState }) {
return evaluateTools({ mode, homeAssistantState, neatoState, liftState });
}
function buildConversation({ recentMessages, name }) {
const rows = [];
(recentMessages || []).forEach((entry) => {
const text = String(entry?.text || '').trim();
if (!text) return;
const who = entry?.system ? (entry?.nickname || name || 'Overseer') : (entry?.nickname || 'user');
rows.push(`${who}: ${text}`);
});
return rows;
}
function buildModelMessages({ systemPrompt, stateUpdate, transcriptRows, availableTools, blockedTools }) {
const messages = [];
messages.push({ role: 'system', content: systemPrompt });
messages.push({ role: 'user', content: `STATE_UPDATE\n${stateUpdate}` });
transcriptRows.forEach((row) => messages.push({ role: 'user', content: row }));
messages.push({
role: 'user',
content: `available_tools:\n${availableTools.map((tool) => `- ${tool}`).join('\n') || '- none'}`,
});
messages.push({
role: 'user',
content: `blocked_tools:\n${blockedTools.map((entry) => `- ${entry.tool} reason=${entry.reason}`).join('\n') || '- none'}`,
});
messages.push({
role: 'user',
content: 'Respond with one line: SKIP, CHAT, ACTION, or ACTION+CHAT.',
});
return messages;
}
module.exports = {
toStateUpdate,
buildToolState,
buildConversation,
buildModelMessages,
};
@@ -0,0 +1,343 @@
const fsp = require('fs/promises');
const { Ollama } = require('ollama');
const io = require('../../globals/io');
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 roverManager = require('../roverManager');
const { getRecentMessages } = require('../chatService');
const {
PROMPT_PATH,
DEFAULT_NAME,
DEFAULT_GATE_INTERVAL_MS,
DEFAULT_HEARTBEAT_MS,
MAX_RUN_HISTORY,
MAX_CHAT_CONTEXT,
MAX_BOT_CONTEXT,
normalizeMs,
} = require('./constants');
const {
isAdminRole,
buildAdminState,
normalizeDecision,
buildFailureInfo,
} = require('./runtimeHelpers');
const {
toStateUpdate,
buildToolState,
buildConversation,
buildModelMessages,
} = require('./contextBuilder');
const config = loadConfig();
const overseerConfig = config.overseerControl || {};
const enabled = Boolean(overseerConfig.enabled);
const observeOnly = overseerConfig.observeOnly !== false;
const name = String(overseerConfig.name || DEFAULT_NAME).trim() || DEFAULT_NAME;
const model = String(overseerConfig.model || '').trim();
const ollamaUrl = String(overseerConfig.ollamaUrl || overseerConfig.ollamaServer || '').trim();
const gateIntervalMs = normalizeMs(Number(overseerConfig.gateIntervalMs), DEFAULT_GATE_INTERVAL_MS);
const heartbeatMs = normalizeMs(Number(overseerConfig.heartbeatMs), DEFAULT_HEARTBEAT_MS);
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: [],
};
let status = {
enabled,
observeOnly,
name,
model,
ollamaUrl,
promptPath: PROMPT_PATH,
gateIntervalMs,
heartbeatMs,
running: false,
inFlight: false,
phase: 'idle',
phaseAt: Date.now(),
tickCount: 0,
currentRunId: null,
nextRunAt: null,
lastTickAt: null,
lastTriggerReason: null,
lastSystemPrompt: null,
lastStateUpdate: null,
lastTranscript: null,
lastAvailableTools: null,
lastBlockedTools: null,
lastModelMessages: null,
lastModelInputAt: null,
lastModelOutputAt: null,
lastModelRawOutput: null,
lastDecision: null,
lastOutcome: null,
lastReason: null,
lastError: null,
lastErrorDetails: null,
lastFailedAt: null,
lastGenerationMs: null,
avgGenerationMs: null,
generationCount: 0,
updatedAt: Date.now(),
};
function updateStatus(patch = {}) {
status = {
...status,
...patch,
updatedAt: Date.now(),
};
const payload = buildAdminState(status, runtime.runHistory);
io.sockets.sockets.forEach((socket) => {
if (!isAdminRole(getRole(socket))) return;
socket.emit('overseer:state', payload);
});
}
function pushRun(run = {}) {
runtime.runHistory = [...runtime.runHistory.slice(-(MAX_RUN_HISTORY - 1)), run];
}
async function readPrompt() {
const raw = await fsp.readFile(PROMPT_PATH, 'utf8');
const prompt = String(raw || '').replace(/<NAME>/g, name).trim();
if (!prompt) throw new Error(`Prompt file empty: ${PROMPT_PATH}`);
return prompt;
}
function buildRosterSummary() {
return roverManager.getRoster().map((rover) => ({
id: rover?.id || 'unknown',
statusTag: rover?.statusTag || 'unknown',
driverNickname: rover?.driverNickname || null,
}));
}
function computeTriggerReason() {
const recent = getRecentMessages(1, { includeSystem: false });
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';
}
return 'chat_activity';
}
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,
});
const mode = getMode();
const homeAssistantState = getHomeAssistantState();
const neatoState = getNeatoState();
const liftState = getLiftState();
const roster = buildRosterSummary();
const stateUpdate = toStateUpdate({ mode, homeAssistantState, neatoState, liftState, roster, 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 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,
});
updateStatus({
phase: 'awaiting_model',
lastSystemPrompt: systemPrompt,
lastStateUpdate: stateUpdate,
lastTranscript: transcriptRows,
lastAvailableTools: toolState.available,
lastBlockedTools: toolState.blocked,
lastModelMessages: modelMessages,
lastModelInputAt: Date.now(),
});
let decision = 'SKIP';
let rawOutput = '';
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,
},
messages: modelMessages,
});
const parsed = normalizeDecision(payload?.message?.content || '');
rawOutput = parsed.raw;
decision = parsed.decision;
}
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';
updateStatus({
phase: 'decision_recorded',
lastModelOutputAt: Date.now(),
lastModelRawOutput: rawOutput,
lastDecision: decision,
lastOutcome: outcome,
lastReason: observeOnly ? 'observe-only mode' : null,
lastGenerationMs: generationMs,
avgGenerationMs,
generationCount: runtime.generationCount,
});
pushRun({
runId,
at: Date.now(),
triggerReason,
decision,
outcome,
observeOnly,
generationMs,
blockedTools: toolState.blocked,
});
}
async function tick() {
runtime.tickCount += 1;
runtime.inFlight = true;
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;
}
await runDecision(triggerReason);
} catch (err) {
const failure = buildFailureInfo(err);
updateStatus({
phase: 'failed',
lastError: failure.message,
lastErrorDetails: failure.details,
lastFailedAt: Date.now(),
lastOutcome: 'failed',
lastReason: 'exception',
});
} finally {
runtime.inFlight = false;
updateStatus({
inFlight: false,
currentRunId: null,
phase: 'idle',
nextRunAt: Date.now() + gateIntervalMs,
});
runtime.timer = setTimeout(tick, gateIntervalMs);
}
}
function emitStateToSocket(socket) {
if (!socket || !isAdminRole(getRole(socket))) return;
socket.emit('overseer:state', buildAdminState(status, runtime.runHistory));
}
function clearHistory() {
runtime.runHistory = [];
runtime.generationCount = 0;
runtime.generationTotalMs = 0;
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;
}
const action = controls?.action || null;
if (action === 'clearHistory') {
clearHistory();
cb({ success: true, state: buildAdminState(status, runtime.runHistory) });
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 });
});
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 });
}
module.exports = {};
@@ -0,0 +1,93 @@
function isAdminRole(role) {
return role === 'admin' || role === 'lockdown' || role === 'lockdown-admin';
}
function buildAdminState(status, runHistory) {
return {
runtime: {
running: status.running,
phase: status.phase,
phaseAt: status.phaseAt,
inFlight: status.inFlight,
tickCount: status.tickCount,
currentRunId: status.currentRunId,
lastTriggerReason: status.lastTriggerReason,
nextRunAt: status.nextRunAt,
lastTickAt: status.lastTickAt,
},
config: {
enabled: status.enabled,
name: status.name,
model: status.model,
ollamaUrl: status.ollamaUrl,
gateIntervalMs: status.gateIntervalMs,
heartbeatMs: status.heartbeatMs,
observeOnly: status.observeOnly,
promptPath: status.promptPath,
},
input: {
systemPrompt: status.lastSystemPrompt,
stateUpdate: status.lastStateUpdate,
transcript: status.lastTranscript,
availableTools: status.lastAvailableTools,
blockedTools: status.lastBlockedTools,
modelMessages: status.lastModelMessages,
modelInputAt: status.lastModelInputAt,
},
output: {
raw: status.lastModelRawOutput,
normalized: status.lastDecision,
outputAt: status.lastModelOutputAt,
outcome: status.lastOutcome,
reason: status.lastReason,
},
timings: {
lastGenerationMs: status.lastGenerationMs,
avgGenerationMs: status.avgGenerationMs,
generationCount: status.generationCount,
},
errors: {
message: status.lastError,
details: status.lastErrorDetails,
failedAt: status.lastFailedAt,
},
history: runHistory,
debug: {
status,
},
controls: {
supportedActions: ['clearHistory'],
},
};
}
function normalizeDecision(rawContent = '') {
const raw = typeof rawContent === 'string' ? rawContent : '';
const trimmed = raw.trim();
if (!trimmed) return { raw, decision: 'SKIP' };
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: 'CHAT' };
}
function buildFailureInfo(err) {
const message = err?.message || String(err || 'Unknown error');
const details = {
name: err?.name || null,
code: err?.code || null,
};
return {
message,
details,
};
}
module.exports = {
isAdminRole,
buildAdminState,
normalizeDecision,
buildFailureInfo,
};
@@ -0,0 +1,6 @@
module.exports = {
signature: 'chat_say(text)',
availability() {
return { available: true, reason: null };
},
};
@@ -0,0 +1,14 @@
module.exports = {
signature: 'ha_set_entity(entity_id, state)',
availability(ctx = {}) {
const mode = String(ctx.mode || '');
if (mode === 'admin' || mode === 'lockdown') {
return { available: false, reason: `policy_lock:site_mode_${mode}` };
}
if (ctx.homeAssistantState?.lightPolicy?.lockedOn) {
return { available: false, reason: 'policy_lock:lights_locked_on' };
}
if (!ctx.homeAssistantState?.connected) return { available: false, reason: 'unavailable' };
return { available: true, reason: null };
},
};
@@ -0,0 +1,42 @@
const chatSay = require('./chatSay');
const memoryRead = require('./memoryRead');
const memoryWrite = require('./memoryWrite');
const liftUp = require('./liftUp');
const liftDown = require('./liftDown');
const neatoStart = require('./neatoStart');
const neatoSendHome = require('./neatoSendHome');
const neatoLocate = require('./neatoLocate');
const neatoClearErrors = require('./neatoClearErrors');
const haSetEntity = require('./haSetEntity');
const TOOL_DEFINITIONS = [
chatSay,
memoryRead,
memoryWrite,
liftUp,
liftDown,
neatoStart,
neatoSendHome,
neatoLocate,
neatoClearErrors,
haSetEntity,
];
function evaluateTools(context = {}) {
const available = [];
const blocked = [];
TOOL_DEFINITIONS.forEach((tool) => {
const result = typeof tool.availability === 'function' ? tool.availability(context) : { available: false, reason: 'unavailable' };
if (result?.available) {
available.push(tool.signature);
return;
}
blocked.push({ tool: tool.signature, reason: result?.reason || 'unavailable' });
});
return { available, blocked };
}
module.exports = {
TOOL_DEFINITIONS,
evaluateTools,
};
@@ -0,0 +1,12 @@
module.exports = {
signature: 'lift_down()',
availability(ctx = {}) {
const mode = String(ctx.mode || '');
if (mode === 'admin' || mode === 'lockdown') {
return { available: false, reason: `policy_lock:site_mode_${mode}` };
}
if (!ctx.liftState?.connected) return { available: false, reason: 'unavailable' };
if (ctx.liftState?.busy) return { available: false, reason: 'busy' };
return { available: true, reason: null };
},
};
@@ -0,0 +1,12 @@
module.exports = {
signature: 'lift_up()',
availability(ctx = {}) {
const mode = String(ctx.mode || '');
if (mode === 'admin' || mode === 'lockdown') {
return { available: false, reason: `policy_lock:site_mode_${mode}` };
}
if (!ctx.liftState?.connected) return { available: false, reason: 'unavailable' };
if (ctx.liftState?.busy) return { available: false, reason: 'busy' };
return { available: true, reason: null };
},
};
@@ -0,0 +1,6 @@
module.exports = {
signature: 'memory_read()',
availability() {
return { available: true, reason: null };
},
};
@@ -0,0 +1,6 @@
module.exports = {
signature: 'memory_write(slot, text)',
availability() {
return { available: true, reason: null };
},
};
@@ -0,0 +1,11 @@
module.exports = {
signature: 'neato_clear_errors()',
availability(ctx = {}) {
const mode = String(ctx.mode || '');
if (mode === 'admin' || mode === 'lockdown') {
return { available: false, reason: `policy_lock:site_mode_${mode}` };
}
if (!ctx.neatoState?.connected) return { available: false, reason: 'unavailable' };
return { available: true, reason: null };
},
};
@@ -0,0 +1,11 @@
module.exports = {
signature: 'neato_locate()',
availability(ctx = {}) {
const mode = String(ctx.mode || '');
if (mode === 'admin' || mode === 'lockdown') {
return { available: false, reason: `policy_lock:site_mode_${mode}` };
}
if (!ctx.neatoState?.connected) return { available: false, reason: 'unavailable' };
return { available: true, reason: null };
},
};
@@ -0,0 +1,11 @@
module.exports = {
signature: 'neato_send_home()',
availability(ctx = {}) {
const mode = String(ctx.mode || '');
if (mode === 'admin' || mode === 'lockdown') {
return { available: false, reason: `policy_lock:site_mode_${mode}` };
}
if (!ctx.neatoState?.connected) return { available: false, reason: 'unavailable' };
return { available: true, reason: null };
},
};
@@ -0,0 +1,11 @@
module.exports = {
signature: 'neato_start()',
availability(ctx = {}) {
const mode = String(ctx.mode || '');
if (mode === 'admin' || mode === 'lockdown') {
return { available: false, reason: `policy_lock:site_mode_${mode}` };
}
if (!ctx.neatoState?.connected) return { available: false, reason: 'unavailable' };
return { available: true, reason: null };
},
};