mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 09:31:20 -04:00
overseer v2 5 (real tools)
This commit is contained in:
@@ -8,6 +8,7 @@ const { getMode } = require('../modeManager');
|
||||
const homeAssistantService = require('../homeAssistantService');
|
||||
const neatoService = require('../neatoService');
|
||||
const liftService = require('../liftService');
|
||||
const buttonBoxService = require('../buttonBoxService');
|
||||
const { getState: getHomeAssistantState, homeAssistantEvents } = homeAssistantService;
|
||||
const { getState: getNeatoState, neatoEvents } = neatoService;
|
||||
const { getState: getLiftState, liftEvents } = liftService;
|
||||
@@ -23,9 +24,10 @@ const {
|
||||
MAX_BOT_CONTEXT,
|
||||
normalizeMs,
|
||||
} = require('./constants');
|
||||
const { isAdminRole, buildAdminState, parseOverseerOutput, buildFailureInfo } = require('./runtimeHelpers');
|
||||
const { isAdminRole, buildAdminState, buildFailureInfo } = require('./runtimeHelpers');
|
||||
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 overseerConfig = config.overseerControl || {};
|
||||
@@ -46,7 +48,7 @@ const runtime = {
|
||||
generationCount: 0,
|
||||
generationTotalMs: 0,
|
||||
runHistory: [],
|
||||
memoryStore: ['', '', ''],
|
||||
memoryStore: loadMemory(),
|
||||
};
|
||||
|
||||
let status = {
|
||||
@@ -112,11 +114,14 @@ async function readPrompt() {
|
||||
}
|
||||
|
||||
function buildRosterSummary() {
|
||||
return roverManager.getRoster().map((rover) => ({
|
||||
id: rover?.id || 'unknown',
|
||||
statusTag: rover?.statusTag || 'unknown',
|
||||
driverNickname: rover?.driverNickname || null,
|
||||
}));
|
||||
return roverManager
|
||||
.getRoster()
|
||||
.filter((rover) => roverManager.canReplayRoverId(rover?.id))
|
||||
.map((rover) => ({
|
||||
id: rover?.id || 'unknown',
|
||||
statusTag: rover?.statusTag || 'unknown',
|
||||
driverNickname: rover?.driverNickname || null,
|
||||
}));
|
||||
}
|
||||
|
||||
function computeTriggerReason() {
|
||||
@@ -131,6 +136,36 @@ function computeTriggerReason() {
|
||||
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) {
|
||||
const runId = runtime.tickCount;
|
||||
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 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 systemPrompt = await readPrompt();
|
||||
@@ -155,6 +195,7 @@ async function runDecision(triggerReason) {
|
||||
availableTools: toolState.available,
|
||||
blockedTools: toolState.blocked,
|
||||
});
|
||||
const ollamaTools = buildOllamaTools(toolState.availableIds);
|
||||
|
||||
updateStatus({
|
||||
phase: 'awaiting_model',
|
||||
@@ -167,19 +208,24 @@ async function runDecision(triggerReason) {
|
||||
lastModelInputAt: Date.now(),
|
||||
});
|
||||
|
||||
let parsed = { decision: 'SKIP', chat: null, actions: [], raw: '' };
|
||||
const generationStart = Date.now();
|
||||
let payload = null;
|
||||
if (ollamaClient && model) {
|
||||
const payload = await ollamaClient.chat({
|
||||
payload = await ollamaClient.chat({
|
||||
model,
|
||||
stream: false,
|
||||
keep_alive: -1,
|
||||
options: { temperature: 0.4, top_p: 0.9 },
|
||||
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);
|
||||
runtime.generationCount += 1;
|
||||
runtime.generationTotalMs += generationMs;
|
||||
@@ -187,33 +233,37 @@ async function runDecision(triggerReason) {
|
||||
runtime.lastModelAt = Date.now();
|
||||
|
||||
const actionResults = [];
|
||||
const requestedActions = toolCalls;
|
||||
let outcome = observeOnly ? 'observed' : 'executed';
|
||||
let reason = observeOnly ? 'observe-only mode' : null;
|
||||
const reason = observeOnly ? 'observe-only mode' : null;
|
||||
|
||||
if (!observeOnly) {
|
||||
if ((parsed.decision === 'CHAT' || parsed.decision === 'ACTION+CHAT') && parsed.chat) {
|
||||
sendSystemMessage(parsed.chat, { nickname: name });
|
||||
if ((decision === 'CHAT' || decision === 'ACTION+CHAT') && chatDraft) {
|
||||
sendSystemMessage(chatDraft, { 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) {
|
||||
if (decision === 'ACTION' || decision === 'ACTION+CHAT') {
|
||||
for (const action of requestedActions) {
|
||||
if (!toolState.availableIds.includes(action.tool)) {
|
||||
actionResults.push({ kind: 'tool', tool: action.tool, ok: false, error: 'tool unavailable or blocked' });
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const result = await executeToolAction(action, {
|
||||
const result = await executeToolAction(action.tool, action.args, {
|
||||
sendSystemMessage,
|
||||
name,
|
||||
memoryStore: runtime.memoryStore,
|
||||
neatoService,
|
||||
liftService,
|
||||
homeAssistantService,
|
||||
buttonBoxService,
|
||||
actor: 'overseerControl',
|
||||
});
|
||||
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;
|
||||
}
|
||||
actionResults.push({ kind: 'tool', tool: action.tool, ok: true, result });
|
||||
@@ -227,10 +277,10 @@ async function runDecision(triggerReason) {
|
||||
updateStatus({
|
||||
phase: 'decision_recorded',
|
||||
lastModelOutputAt: Date.now(),
|
||||
lastModelRawOutput: parsed.raw,
|
||||
lastDecision: parsed.decision,
|
||||
lastChatDraft: parsed.chat,
|
||||
lastRequestedActions: parsed.actions,
|
||||
lastModelRawOutput: rawOutput,
|
||||
lastDecision: decision,
|
||||
lastChatDraft: chatDraft,
|
||||
lastRequestedActions: requestedActions,
|
||||
lastActionResults: actionResults,
|
||||
lastOutcome: outcome,
|
||||
lastReason: reason,
|
||||
@@ -243,9 +293,9 @@ async function runDecision(triggerReason) {
|
||||
runId,
|
||||
at: Date.now(),
|
||||
triggerReason,
|
||||
decision: parsed.decision,
|
||||
chatDraft: parsed.chat,
|
||||
requestedActions: parsed.actions,
|
||||
decision,
|
||||
chatDraft,
|
||||
requestedActions,
|
||||
actionResults,
|
||||
outcome,
|
||||
observeOnly,
|
||||
@@ -292,7 +342,7 @@ function clearHistory() {
|
||||
runtime.runHistory = [];
|
||||
runtime.generationCount = 0;
|
||||
runtime.generationTotalMs = 0;
|
||||
runtime.memoryStore = ['', '', ''];
|
||||
runtime.memoryStore = saveMemory(createDefaultMemory());
|
||||
updateStatus({ lastReason: 'admin requested clear history', lastOutcome: 'cleared' });
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user