openvibers/openvibe.actor Commits

Actor: robot tasks are their own class and go to the runtime (one was refused as coding) (#48)

OP OpenVibers committed 14 Oct 2026 · a64dc02a46 · 7 files, +26 −6

The production proof of Actor#47 failed at routing. A signed-in person's task, "Using my robot … on OpenVibe.Bot: drive it forward for 1 second, turn its headlights green, then stop", was classified `code`. The router then refused it: "Coding tasks go to OpenVibe.Codes…" (task tsk_01M4W61V45MDRCVZ8HRQV5EJ2B). The robot tools never ran.

**Change**
- New task class `robot`: make the person's own robot on OpenVibe.Bot act or report on it.
  - The classifier prompt names it, and says commanding a robot is never code.
  - The free heuristic picks it for "my robot", "the robot", a `rob_…` id, openvibe.bot, rover or Cozmo, unless the task carries a code block.
- `openvibe-runtime` can `task:robot` (and `tool:openvibe-bot`), so the router places robot tasks there. Its catalog blurb says so.
- When the runtime is down, a robot task's error says that, not the coding one.
- The answer check accepts an answer that reports what the robot did or why it stopped or was refused.
- README's agent table and the "Sort the task" step on the home page name the robot class.

**Tests**
- `test/robots.test.js` gains a check: the class exists; robot tasks classify as `robot`; code (even about a robot, with a code block) stays `code`; the runtime can take it; the router selects `openvibe-runtime`.
- The full suite passes: 25/25.

Changes

ddiff --git a/README.md b/README.md
iindex f4652b5..7c2a7a5 100644
---- a/README.md
++++ b/README.md
@@@ -23,7 +23,7 @@ Actor is OpenVibe's own general agent and the router for agent work: it takes a
| Agent | Kind | Can do | Trust | Price |
|---|---|---|---|---|
--| OpenVibe runtime | openvibe-runtime | answer, lookup, research | external (the model runs at DeepSeek) | DeepSeek Flash, $0.30 in / $1.20 out per 1M tokens (peak; off-peak is half, Actor charges peak) |
++| OpenVibe runtime | openvibe-runtime | answer, lookup, research, robot | external (the model runs at DeepSeek) | DeepSeek Flash, $0.30 in / $1.20 out per 1M tokens (peak; off-peak is half, Actor charges peak) |
| OpenAI agent | agent-platform | answer, research, web | external | gpt-5-mini, $0.25 / $2.00 per 1M, plus $0.01 a web search |
| Open model on OpenVibe | open-model | answer | first-party | free (OpenVibe's own server; `openvibe-llm` on the host) |
| OpenVibe.Codes | codes | code | first-party | listed, not hosted yet: coding runs need OpenVibe.Run sandboxes; use `openvibe-codes` on your machine |
ddiff --git a/server/agents/catalog.js b/server/agents/catalog.js
iindex 372ac0a..68ae2f4 100644
---- a/server/agents/catalog.js
++++ b/server/agents/catalog.js
@@@ -39,9 +39,9 @@ const AGENTS = [
// The runtime is OpenVibe's code, but its model runs at DeepSeek: the task's text leaves OpenVibe.
trust: 'external',
quality: 'standard',
-- can: ['task:answer', 'task:lookup', 'task:research', 'tool:openvibe-search', 'tool:openvibe-tools'],
++ can: ['task:answer', 'task:lookup', 'task:research', 'task:robot', 'tool:openvibe-search', 'tool:openvibe-tools', 'tool:openvibe-bot'],
rate_card: { input_usd_per_mtok: 0.30, output_usd_per_mtok: 1.20, source: DEEPSEEK_PRICES, verified_at: VERIFIED },
-- blurb: 'OpenVibe\'s own agent: a DeepSeek model that acts through OpenVibe services — search across the network, 160+ tools (DNS, mail, certificates, headers, conversions) and a page reader.',
++ blurb: 'OpenVibe\'s own agent: a DeepSeek model that acts through OpenVibe services — search across the network, 160+ tools (DNS, mail, certificates, headers, conversions), a page reader, and your own robots on OpenVibe.Bot when you let it drive them.',
},
{
id: 'openai-agent',
ddiff --git a/server/agents/check.js b/server/agents/check.js
iindex 1a4b54b..bd67da1 100644
---- a/server/agents/check.js
++++ b/server/agents/check.js
@@@ -18,6 +18,7 @@ const SYSTEM = `You check an AI agent's answer before a person sees it. Reply wi
ok is true when the answer addresses the task and nothing in it is clearly invented, contradictory or unsafe. It does not have to be perfect.
ok is false when the answer is empty, refuses without a good reason, answers a different question, contradicts itself, or states something that is plainly impossible.
The agent may have searched the web or run tools: do not fail an answer only because it states recent facts, versions, prices or dates you cannot verify yourself, especially when it cites a source for them.
++The agent may have made the person's own robot act through OpenVibe.Bot: an answer that reports what the robot did, or why it stopped or was refused, addresses the task.
When the task is a greeting, small talk, a test message or too vague to act on, a short friendly reply, a summary of what the agent can do or a clarifying question is a good answer: ok is true.`;
/** Which checkers may read this agent's answer, best first: another family, then the same family. */
ddiff --git a/server/agents/classify.js b/server/agents/classify.js
iindex c436256..40178bd 100644
---- a/server/agents/classify.js
++++ b/server/agents/classify.js
@@@ -8,22 +8,25 @@
* lookup facts about a domain, address or site → task:lookup
* research read the pages it names, or OpenVibe's content → task:research
* answer write or explain from what a model knows → task:answer
++ * robot make the person's own OpenVibe.Bot robot act → task:robot
*
* heuristic(task) is free and is what private mode and the public dry run use. classify() asks a cheap model when one
* is configured, and falls back to the heuristic on any failure; its cost is part of the task's.
*/
--const CLASSES = ['answer', 'lookup', 'research', 'web', 'code'];
++const CLASSES = ['answer', 'lookup', 'research', 'web', 'code', 'robot'];
const CODE = /\b(code|function|bug|stack ?trace|refactor|unit tests?|pull request|repo(sitory)?|compile|typescript|javascript|python|golang|rust|java|sql query|regex|script|api endpoint|implement)\b|```/i;
const LOOKUP = /\b(dns|mx|spf|dkim|dmarc|whois|rdap|nameservers?|ssl|tls|certificate|headers?|redirects?|ip address|asn|geoip|reverse dns|ptr|ttl|robots\.txt|sitemap|uptime|ping|traceroute|port \d+|hosting provider|registrar)\b/i;
const DOMAINISH = /\b(?:[a-z0-9-]+\.)+[a-z]{2,}\b|\b\d{1,3}(?:\.\d{1,3}){3}\b/i;
const WEB = /\b(latest|today|tonight|yesterday|this week|this month|current(ly)?|right now|news|price of|stock|weather|score|election|released?|recent(ly)?|202\d|trending|search the web|look up online|compare prices)\b/i;
const URL = /https?:\/\/\S+/i;
++const ROBOT = /\b(my robots?|the robot|robot_steps|rob_[0-9A-HJKMNP-TV-Z]{26}|openvibe\.bot|rover|cozmo)\b/i;
const RESEARCH = /\b(summari[sz]e|read|according to|sources?|cite|citations?|brief|report on|research)\b/i;
function heuristic(task) {
const t = String(task || '');
++ if (ROBOT.test(t) && !/```/.test(t)) return 'robot';
if (CODE.test(t)) return 'code';
if (LOOKUP.test(t) && DOMAINISH.test(t)) return 'lookup';
if (URL.test(t)) return 'research';
@@@ -37,7 +40,8 @@ answer: writing, explaining, advice, maths, anything a capable model can do from
lookup: facts about a specific domain, website, IP address or server (DNS, mail records, certificates, headers, ownership, robots.txt).
research: read the specific web pages it links to, or OpenVibe's own content, and report.
web: needs searching the live web: anything recent or changing (news, prices, releases, schedules, people's current roles).
--code: write, change, debug or review code.`;
++code: write, change, debug or review source code.
++robot: make the person's own robot on OpenVibe.Bot do something or report on it (drive, turn, speak, lights, show a face, its sensors or battery). Commanding a robot is never code.`;
/**
* classify(task, { chat }) → { class, cost_usd, by }. `chat` is a provider client (server/agents/providers.js) or
ddiff --git a/server/agents/router.js b/server/agents/router.js
iindex a145a0b..72e5b8f 100644
---- a/server/agents/router.js
++++ b/server/agents/router.js
@@@ -77,6 +77,7 @@ function whyNone(result, { cls, mode, budgetUsd }) {
return { code: 'actor.budget.exceeded', detail: `The cheapest agent that can take this task would cost about $${cheapest.toFixed(3)}, over this task's $${Number(budgetUsd).toFixed(3)} budget. Raise the budget or ask something simpler.` };
}
if (mode === 'private') return { code: 'actor.no_agent', detail: `Private mode keeps the task on OpenVibe's own hardware, and no agent there can take a "${cls}" task yet (the open model answers simple questions only).` };
++ if (cls === 'robot') return { code: 'actor.no_agent', detail: 'Robot tasks go to the OpenVibe runtime, which acts on your own robots on OpenVibe.Bot. It is not available right now; try again later.' };
if (cls === 'code') return { code: 'actor.no_agent', detail: 'Coding tasks go to OpenVibe.Codes, and hosted coding runs are not open yet. Run openvibe-codes on your own machine: https://openvibe.codes/start' };
const down = cands.filter((c) => !c.eligible).map((c) => `${c.id}: ${c.excluded_because}`).join('; ');
return { code: 'actor.no_agent', detail: `No agent can take this task right now (${down || 'none configured'}).`.slice(0, 2000) };
ddiff --git a/server/http/pages.js b/server/http/pages.js
iindex d54addc..addce22 100644
---- a/server/http/pages.js
++++ b/server/http/pages.js
@@@ -107,7 +107,7 @@ ${raw(showcase.features({
${raw(showcase.steps({
title: 'How a task finds its agent',
items: [
-- { title: 'Sort the task', text: 'An answer, a lookup, research, a live web search or code: that decides what an agent must be able to do.' },
++ { title: 'Sort the task', text: 'An answer, a lookup, research, a live web search, code or your own robot: that decides what an agent must be able to do.' },
{ title: 'Hard limits first', text: 'An agent that cannot do it, that private mode does not trust, that is down or that would cost more than your budget is out, and the task says why.' },
{ title: 'Then your mode', text: 'Cheapest, Balanced, Best, Fastest or Private picks among the rest, through the same placement engine every OpenVibe service uses.' },
{ title: 'Run, check, deliver', text: 'The agent works while you watch its steps; another model checks the answer; you get the answer, its sources, the cost and the explanation.' },
ddiff --git a/test/robots.test.js b/test/robots.test.js
iindex ca0a359..341dff8 100644
---- a/test/robots.test.js
++++ b/test/robots.test.js
@@@ -83,5 +83,19 @@ const tokens = { authHeaders: async () => ({ Authorization: 'Bearer svc-token' }
assert.ok(!(await t.definitions({ requester: `user:${ME}` })).some((d) => d.function.name === 'robot_steps'));
});
++ await check('a robot task is its own class, routed to the runtime, never refused as code', async () => {
++ const { heuristic, CLASSES } = require('../server/agents/classify');
++ const { route } = require('../server/agents/router');
++ const catalog = require('../server/agents/catalog');
++ assert.ok(CLASSES.includes('robot'));
++ assert.strictEqual(heuristic(`Using my robot "Desk rover" (${OPEN}) on OpenVibe.Bot: drive it forward for 1 second, then stop`), 'robot');
++ assert.strictEqual(heuristic('drive my rover in a square and turn its headlights green'), 'robot');
++ assert.strictEqual(heuristic('Write a python script that reads a CSV'), 'code');
++ assert.strictEqual(heuristic('fix the bug in my robot code ```js\nx()\n```'), 'code', 'code in the task is still code');
++ assert.ok(catalog.byId('openvibe-runtime').can.includes('task:robot'));
++ const placed = route({ cls: 'robot', config: { providers: { deepseek: { apiKey: 'k' }, openai: { apiKey: '' }, local: { url: '', model: '' } } } });
++ assert.strictEqual(placed.selected, 'openvibe-runtime', JSON.stringify(placed.candidates));
++ });
++
done();
})();