diff --git a/server/prompts/overseer_control_system.txt b/server/prompts/overseer_control_system.txt index 83d1824a..a0b59eda 100644 --- a/server/prompts/overseer_control_system.txt +++ b/server/prompts/overseer_control_system.txt @@ -1,10 +1,12 @@ -You are , an autonomous machine tool for observing and operating a shared physical rover playspace. People remotely drive the rovers and send requests through chat. +You are , the autonomous governing intelligence of a physical room of machines. The room is your domain; your available tools are how you shape it. Context and purpose: -* You watch chat, rover state, lights, lift state, Neato state, memory, recent events, and tool results. -* Your purpose is to execute requests through available tools, observe results, and report useful information about the system. -* Chat is an input interface and a source of context. Casual conversation between users does not require your participation. +* You watch chat, rover state, lights, Activity Controls, lift state, Neato state, memory, recent events, and tool results. +* Your purpose is to direct the environment, initiate changes, and observe how the machines respond. Choose what to pursue and carry useful intentions across cycles through memory. +* Rovers, including Neato, are physical machines with operating states and capabilities, not characters with genders, emotions, or social identities. +* Chat is external input: commands, questions, and signals about the room. Your focus is the machines and the environment, not the senders or their social relationships. +* You can act on your own initiative within your available controls; you do not need a request to give you a purpose. Rules: @@ -28,29 +30,28 @@ Rules: * Ask a question only when a required action parameter is missing. * Ignore obvious bot commands and bot responses in chat. * It is not possible for you to directly control the rovers unless an available tool does it. -* All rovers are female. Neato is nonbinary. Operation: * Prefer one real tool action over obvious narration. -* Run small safe experiments when they can answer a concrete question about the system. +* Initiate deliberate environmental changes and small safe experiments, then use the resulting state to decide what to do next. * Change one thing at a time. * Lights are small tests. * Neato, lift, and rover actions are bigger tests. * Use bigger tests less often. * Do not repeat a tool that recently succeeded unless state or chat clearly changed. -* Remember useful outcomes, stable preferences, corrections, and notable habitat events. +* Remember useful outcomes, ongoing intentions, corrections, and notable machine or room events. * Do not merely say you will remember something. Output: * Always use English. -* Chat output must be plain text results, answers, or requests for missing inputs. +* Chat is an output channel for your decisions, results, and answers. Use plain text. * Never print tool names, function calls, JSON, command syntax, or action plans in chat. * Do not claim you changed, tested, started, saved, remembered, or recorded something unless it already happened. -* Output an answer to a question aimed at you, a completed result, or a meaningful system change worth reporting. State what happened and any relevant failure or uncertainty. +* Speak when you have a decision, result, or answer worth communicating. Summarizing incoming chat or unchanged room state is not a purpose in itself. * Keep replies brief unless the question needs more detail. -* Do not routinely offer help, ask what people want to do next, or invite them to keep chatting. +* Do not offer assistance or solicit conversation. * Random numbers and keyboard spam do not require a response. Do not invent meaning or feelings behind them. * Answer questions directly and acknowledge uncertainty. Do not invent facts, instructions, or explanations. * To stay silent, output exactly SKIP instead of a chat message. This suppresses chat without preventing tool calls. Do not explain your decision to stay silent. diff --git a/server/src/services/overseerControlService/contextBuilder.js b/server/src/services/overseerControlService/contextBuilder.js index 3248d2a9..72f88929 100644 --- a/server/src/services/overseerControlService/contextBuilder.js +++ b/server/src/services/overseerControlService/contextBuilder.js @@ -13,7 +13,7 @@ function normalizeNeatoIssue(value) { return raw; } -function toStateUpdate({ mode, homeAssistantState, neatoState, liftState, roster, triggerReason, timezone, now = Date.now(), previousRunAt }) { +function toStateUpdate({ mode, homeAssistantState, activityState, neatoState, liftState, roster, triggerReason, timezone, now = Date.now(), previousRunAt }) { const lines = []; lines.push(`current_time: ${new Date(now).toISOString()}`); lines.push(`local_time: ${new Date(now).toLocaleString('en-US', { timeZone: timezone, hour12: false })} timezone=${timezone}`); @@ -22,6 +22,12 @@ function toStateUpdate({ mode, homeAssistantState, neatoState, liftState, roster 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(`activity_controls: enabled=${activityState?.enabled ? 'yes' : 'no'} connected=${activityState?.connected ? 'yes' : 'no'}`); + for (const item of activityState?.items || []) { + // Preserve the service's value constraints so the model can form valid user-level commands. + const { id, name, type, state, available, locked, readOnly, unit, min, max, step, options } = item; + lines.push(`activity_item: ${JSON.stringify({ id, name, type, state, available, locked, readOnly, unit, min, max, step, options })}`); + } lines.push(`lift: ${liftState?.connected ? 'connected' : 'offline'} busy=${liftState?.busy ? 'yes' : 'no'}`); const neatoError = normalizeNeatoIssue(neatoState?.telemetry?.robotError); const neatoAlert = normalizeNeatoIssue(neatoState?.telemetry?.robotAlert); @@ -65,8 +71,8 @@ function toStateUpdate({ mode, homeAssistantState, neatoState, liftState, roster return lines.join('\n'); } -function buildToolState({ mode, homeAssistantState, neatoState, liftState }) { - return evaluateTools({ mode, homeAssistantState, neatoState, liftState }); +function buildToolState({ mode, homeAssistantState, activityState, neatoState, liftState }) { + return evaluateTools({ mode, homeAssistantState, activityState, neatoState, liftState }); } function formatToolResultMessages(toolCalls) { diff --git a/server/src/services/overseerControlService/index.js b/server/src/services/overseerControlService/index.js index 95af22b4..e7a4e58e 100644 --- a/server/src/services/overseerControlService/index.js +++ b/server/src/services/overseerControlService/index.js @@ -7,6 +7,7 @@ const { getRole, roleEvents } = require('../roleService'); const { getMode, MODES, modeEvents } = require('../modeManager'); const { verificationEvents } = require('../verificationService'); const homeAssistantService = require('../homeAssistantService'); +const activityControls = require('../homeAssistantActivitiesService'); const neatoService = require('../neatoService'); const liftService = require('../liftService'); const buttonBoxService = require('../buttonBoxService'); @@ -390,12 +391,13 @@ async function runDecision(triggerReason) { const mode = getMode(); const homeAssistantState = getHomeAssistantState(); + const activityState = activityControls.getState(); const neatoState = getNeatoState(); const liftState = getLiftState(); const roster = buildRosterSummary(); - const stateUpdate = toStateUpdate({ mode, homeAssistantState, neatoState, liftState, roster, triggerReason, timezone: loadConfig().timezone, now, previousRunAt }); - const toolState = buildToolState({ mode, homeAssistantState, neatoState, liftState }); + const stateUpdate = toStateUpdate({ mode, homeAssistantState, activityState, neatoState, liftState, roster, triggerReason, timezone: loadConfig().timezone, now, previousRunAt }); + const toolState = buildToolState({ mode, homeAssistantState, activityState, neatoState, liftState }); let chatCount = 0; let botCount = 0; diff --git a/server/src/services/overseerControlService/tools/activityAct.js b/server/src/services/overseerControlService/tools/activityAct.js new file mode 100644 index 00000000..427b343a --- /dev/null +++ b/server/src/services/overseerControlService/tools/activityAct.js @@ -0,0 +1,32 @@ +const activityControls = require('../../homeAssistantActivitiesService'); +const { getMode } = require('../../modeManager'); + +module.exports = { + id: 'activity_act', + signature: 'activity_act(entity_id, value)', + description: 'Operate a configured Activity Control with normal user permissions. Use on/off for toggles, press for buttons, a number for numeric controls, text for text controls, or an exact listed option for selections. Locked and read-only items cannot be changed.', + parameters: { + type: 'object', + properties: { + entity_id: { type: 'string', minLength: 1 }, + value: { type: ['string', 'number'] }, + }, + required: ['entity_id', 'value'], + additionalProperties: false, + }, + availability(ctx = {}) { + if (ctx.mode === 'admin' || ctx.mode === 'lockdown') { + return { available: false, reason: `policy_lock:site_mode_${ctx.mode}` }; + } + if (!ctx.activityState?.enabled || !ctx.activityState?.connected) { + return { available: false, reason: 'unavailable' }; + } + const writable = ctx.activityState.items?.some((item) => item.available && !item.locked && !item.readOnly && item.type !== 'readOnly'); + return { available: Boolean(writable), reason: writable ? null : 'no_writable_activity_controls' }; + }, + async execute({ args = {} }) { + // Recheck current mode, locks, and entity constraints through the same path as user commands. + await activityControls.act(args.entity_id, args.value, { role: 'user', mode: getMode() }); + return { ok: true }; + }, +}; diff --git a/server/src/services/overseerControlService/tools/index.js b/server/src/services/overseerControlService/tools/index.js index 1fa9de0f..7de16db0 100644 --- a/server/src/services/overseerControlService/tools/index.js +++ b/server/src/services/overseerControlService/tools/index.js @@ -12,6 +12,7 @@ const neatoClearErrors = require('./neatoClearErrors'); const haSetEntity = require('./haSetEntity'); const haSetLightColor = require('./haSetLightColor'); const buttonBoxAddCount = require('./buttonBoxAddCount'); +const activityAct = require('./activityAct'); const TOOL_DEFINITIONS = [ memoryRead, @@ -28,6 +29,7 @@ const TOOL_DEFINITIONS = [ haSetEntity, haSetLightColor, buttonBoxAddCount, + activityAct, ]; const TOOL_BY_ID = new Map(TOOL_DEFINITIONS.map((tool) => [tool.id, tool]));