Published in openvibe-contracts v0.107.0 (docs/adr/ADR-044-actor-runtime.md), rendered as is.

ADR-044: The Actor runtime — the task contract, adapter interface, modes, signals, verification and trust

Status: Proposed 2026-10-05 (plan track T17). This is the build-order item immediately after the engine: plan T17's build order puts "the engine first, where it already runs — harness adapter interface in openvibe-agents with the first new adapters (OpenCode, Antigravity CLI), capability-aware routing, vision inputs and a local sandbox (headless browser + virtual desktop, screenshots)" ahead of "ADR-044 (task contract, adapter interface, modes, signals, verification, trust)", and the engine's harness work is paused where it stands. Contract: platform.task@1. Builds on ADR-034 §2 (projects and resource names), §5 (control and data plane) and §9 (metering), on ADR-046 (the placer, its objectives and the five trust classes), on ADR-012 (one Billing authority) and on ADR-043 (Node control levels and the local kill switch). For the owner's review.

Context and current evidence

Decision

1. One task contract: platform.task@1

contracts/platform/task.v1.json (PLANNED, owner network) is one task at the router: the resource POST /v1/tasks creates and GET /v1/tasks/{id} reads. It carries id (tsk_<ULID>), project_id, requester (a person or an agent subject), the task text, the mode, the budget, the state, created_at/finished_at, the result, the cost, the explanation (one or more platform.placement-result@1, oldest first, an escalation appending one), the progress descriptor, the cancel record and the registered webhooks.

2. The adapter interface

An adapter is how Actor reaches one agent system, and the plan fixes what it declares: "Each adapter declares capabilities, limits, a revisioned rate card (never AI-written) and trust class; BYO keys for any backend." So an adapter has:

The plan puts the first adapters in the engine (OpenCode, Antigravity CLI) and demands that "a new agent platform is one adapter". The concrete interface — its field names, whether it is one manifest or a set of declared contracts, and how it is registered — is not fixed by the plan and is left open; this ADR fixes only what an adapter must declare, not the wire shape.

3. Modes are planner objectives

mode is one of cheapest, balanced, best, fastest, private; balanced is the default. The plan is explicit that "a mode is a planner objective, not a hard-coded list", so a mode is stated to the placer, not translated into a backend by hand. Four modes map onto ADR-046's platform.placement-result@1 objectives directly: cheapest → cheapest, balanced → balanced, fastest → lowest-latency, private → private. best does not map to correctness: the SDK's correctness objective only preserves req.authority or the current placement, so for a new task with neither it ranks like balanced, and high-reliability has no branch either — openvibe-sdk/placement has no quality ranking that expresses "the highest chance of a right answer first time". What best maps to is therefore left as an open question below; the placer may refine the rest, and the exact tie-breaks inside a mode are left to openvibe-sdk/placement.

4. Signals and auto-balancing

Auto-balancing goes "through openvibe-sdk/placement over live signals per backend × task class; hysteresis keeps a task class unless another is clearly better; failover is immediate; every decision is stored and explained." The signals are the ones the Fabric already contracts: rate cards price a backend, platform.telemetry-sample@1 reports its latency, health and status, and platform.cost-snapshot@1 measures what a class actually cost against its alternatives. A capability the task needs is a hard filter before ranking, as excluded() is in the placer. The explanation is platform.placement-result@1, and it is part of the task, so "which agent ran, why it won, what else was considered and what it cost" survives the request.

5. Verification and cascades

"Cheap first, escalate on failure; cross-family checks; the task's own tests when it has them (code: tests; research: citations; actions: postconditions)." The task therefore has a verifying state between running and succeeded: a result that exists is checked before it is delivered, by a model family other than the one that produced it, and only then becomes result. A failed check moves the task to a stronger backend and appends a placement result; the plan puts the full cascade build after the router itself, so the first router may ship with a single check. A task that never passes ends failed; it never returns an unchecked result as succeeded.

6. Trust

ADR-046 §4's five classes apply unchanged: first-party, user-owned, partner, community, external, with user-owned eligible only when the task names it. private mode narrows the eligible set to backends that keep the data on OpenVibe — first-party, and a user-owned node only when the deployment allows it — which the plan describes as "only agents that keep your data on OpenVibe: its own runtime and open models on its hardware". Local execution on a person's own machine goes through OpenVibe.Node under ADR-043's control levels (Observe / Ask / Trusted / Full control), a local indicator and a kill switch the owner holds; Node's local policy always overrides a cloud command (plan T14).

7. Progress, cancel and webhooks

8. Cost and budgets

budget.per_task_usd and budget.per_day_usd are hard limits the router never exceeds; a task that cannot be run inside per_task_usd fails (actor.budget.exceeded) instead of running, and per_day_usd caps the project's tasks in one UTC day. cost.usd is what the task was billed, to the cent, across every attempt, and the plan's money rule is "one key, one bill (Billing, T5), per-task cost reported to the cent; free tier from the bounded allowance". The metering is platform.usage-sample@1, written through Billing as Run's job seconds are.

Authority boundaries

Actor plans and routes; it does not become an authority. The plan's own list of what it calls:

| What Actor needs | Whose authority | |---|---| | Coding work | Codes (the one coding-harness router) | | Persistent conditions | Watch | | Cloud execution | Run (platform.job@1 / platform.job-frame@1) | | Local execution and computer control | Node (ADR-043 control levels, kill switch) | | Resource control | Services (common.resource-control-request@1, ADR-048) | | Artifacts | Media | | Robots | Bot | | Progress and triggers | Events (ADR-042) | | Models | AI |

Out of scope

Open questions for the owner

  1. The adapter interface's shape. One contract with a discriminator, or one small contract per adapter kind? The plan fixes what an adapter declares, not the wire shape.
  2. Per-class success as a signal. ADR-046 contracts telemetry and cost, but not a per-backend × task-class success rate. Whether that is a new signal contract or an aggregation of run.job.*-style outcomes is open.
  3. Whether a user-owned node may take other people's tasks (ADR-046's own open question), which private mode depends on.
  4. Webhook delivery guarantees — retry count, backoff and whether an exhausted webhook surfaces on the task.
  5. Where an escalated task's partial results live before it succeeds.
  6. What best maps to. No objective in openvibe-sdk/placement ranks quality: correctness only preserves req.authority or the current placement and otherwise ranks like balanced, and high-reliability has no branch. Until the placer grows a real quality ranking, best → <objective> is undecided.

Consequences

What this ADR does not claim