Reasoner (S2) Reference
The reasoner is OpenRAL's slow, deliberative control layer — the S2 half
of the dual-system architecture. Where an rSkill (S1) is a fast visuomotor
policy running at 30–200 Hz, the reasoner is an event-driven LLM supervisor
that closes the loop context → LLM → one typed tool call at a slow cadence.
It decides what to do next; it never drives motors itself.
- Core (transport-agnostic):
openral_reasoner.ReasonerCore - ROS 2 lifecycle node:
openral_reasoner_ros.reasoner_node— full contract in its README - Design: covers the supervisor graph + tool-dispatch, active search, and the read-only query tools
- Design narrative (how it thinks): Reasoner Design & Decisions — the connective story across the decisions below, organized by logic problem (tick loop → grounding → decomposition → completion verdict → reward pairing → replanning → memory → LLM choice).
Authority boundary. The reasoner never publishes
openral_msgs/ActionChunk. Actuation lives behind the S1 skill runner (/openral/execute_rskillaction server) and the F5 safety boundary. The reasoner proposes; the C++ safety kernel disposes (see the safety hazard log — private OpenRAL/management repo).
Cadence & event model
As of the 2026-05-25 amendment, the reasoner is event-driven with a slow heartbeat:
- Heartbeat — a periodic timer ticks at
tick_hz(default 0.2 Hz, one tick every 5 s). A heartbeat tick that sees no new event since the last successful tick is short-circuited insideReasonerCorewithsuppressed_reason="heartbeat_idle"(no LLM call, no span). - Event preemption is the primary trigger, subject to a hard 100 ms min-interval between ticks:
| Tier | Source | Preempts on |
|---|---|---|
| A — safety | /openral/failure/safety |
severity ≥ SEVERITY_WARN |
| B — execution | /openral/failure/{hal,sensor,rskill,wam} |
severity ≥ SEVERITY_FAIL |
| C — critic | /openral/failure/critic |
severity ≥ SEVERITY_FAIL |
| D — operator | /openral/prompt |
always |
/openral/perception/{motion,objects,ocr,scene_change} events are informational
context (not preemptive on their own).
The tool-call contract
Each tick the LLM emits exactly one variant of the
ReasonerToolCall
discriminated union (discriminator field: tool). Output is structured —
the provider's tool-use API returns a Pydantic-validated object, never free-form
JSON. Extending the palette requires a new variant in openral_core and the
matching dispatch in reasoner_node (CLAUDE.md §3).
Effect tools
Tool (tool=) |
Dispatch | Notes |
|---|---|---|
ExecuteRskillTool (execute_rskill) |
action goal on /openral/execute_rskill |
rskill_id, prompt, goal_params_json, deadline_s. Emits a FailureTrigger on rejection/abort/timeout. |
LifecycleTransitionTool (lifecycle_transition) |
<node>/change_state service |
configure / activate / deactivate / cleanup only — shutdown is reserved for the safety supervisor (CLAUDE.md §6). |
EmitPromptTool (emit_prompt) |
publish on a PromptStamped topic |
Stamps the active OTel traceparent into metadata_json. Used to stage multi-step plans / cascade prompts. |
ReloadGstPipelineTool (reload_gst_pipeline) |
/openral/sensors/<id>/reload_pipeline service |
⚠️ log-and-acknowledge stub today — the F6 sensor-service IDL is not yet on disk (GH-126). |
Read-only query tools
These hold no actuation authority — they read state and feed the result back
to the LLM as a re-prompt. Each is gated by a ToolPalette flag and only offered
when the corresponding service is present.
Tool (tool=) |
Reads | Gate |
|---|---|---|
RecallObjectTool (recall_object) |
spatial-memory scene graph — "where did I last see X?" | spatial_memory_available |
ResolvePlaceTool (resolve_place) |
spatial memory → navigation goal pose + path | spatial_memory_available |
LocateInViewTool (locate_in_view) |
on-demand open-vocab detector — "where is X right now?" | detector_available |
QuerySceneTool (query_scene) |
scene VLM (Qwen3.5-4B) — free-text "did the grasp succeed?" | scene_query_available |
QueryTaskProgressTool (query_task_progress) |
reward monitor (Robometer-4B) — windowed progress_now / success_now / trends / stalled |
task_progress_available |
locate_in_view carries an optional detector selector — omdet-turbo-locator
(fast, in-process) for simple "find X", locateanything-3b for complex referring
expressions. recall_object remembers; locate_in_view looks now.
Memory & mission tools
These edit the reasoner's own state — its MEMORY.md file and its task ledger —
never the robot. They are advisory and hold no actuation authority.
Tool (tool=) |
Effect |
|---|---|
MemoryWriteTool (memory_write) |
the reasoner's first write-capable variant — add / update / supersede / delete an entry in the self-maintained MEMORY.md |
MemorySearchTool (memory_search) |
read-only query over the archival memory log |
DecomposeMissionTool (decompose_mission) |
write the deterministic MissionState task queue — populate/replace it, or flat-splice a blocked task into finer subtasks (subdivide_active) |
Playbooks, memory & missions
Three S2 capabilities layer on top of the tool surface:
- Playbooks (
kind: playbook). At palette-seed time the reasoner gathers installed, capability-matched playbook rSkills, reads theirPLAYBOOK.mdbodies, and appends a## PLAYBOOKSsection to the system prompt — so the LLM follows the relevant authored decision procedure when its trigger matches the goal. Playbooks arerole: s2content, never in the ExecuteRskill palette; every motion still crossesexecute_rskill+ the C++ safety kernel. Six ship in-tree:decompose-mission,verify-outcome,clarify-ambiguity,preflight-reach,stage-for-manipulation,find-object. - Self-maintained
MEMORY.md. A persistent semantic memory (MemoryStore/MemoryEntry) the reasoner reads each tick and edits throughmemory_write, withconsolidate()(drop duplicates) and ato_context_block(cap=N)render that bounds the always-on## MEMORYblock on a long-running robot. Loaded at deploy time viaopenral deploy sim/run --memory-dir(alongsidescene_graph.jsonand the 2D nav map). - Sequential missions. The operator goal seeds a single-task
MissionState; the LLM decomposes it into the ordered queue viadecompose_missionwith at most oneactive(orverifying)TaskState. The queue advances only when the active task passes the reward/critic gate, rendered as a## MISSIONledger each tick.DecomposeMissionTool+MissionState.subdivide_activeflat-splice a blocked task into finer subtasks on replan, bounded byDEFAULT_MAX_SUBDIVIDE_DEPTHbefore human-handoff.
Tool palette & gating
The palette (openral_reasoner.palette.ToolPalette)
is built at on_configure and rebuilt on /openral/skill_registry_changed
(fired by openral rskill install|remove). build_tool_palette() filters
installed rSkills by:
RobotCapabilitiesflags (capabilities_required⊆ robot capabilities),- embodiment-tag intersection (
embodiment_tags), role == "s1"(S0/S2 excluded from the actuation palette),- license posture (commercial-deployment gate).
The LLM sees one tool per skill (execute_rskill__<slug>) with
a real description + action/object/scene discriminators, not one opaque tool with
an enum. Continuous detectors are surfaced as continuous_detectors so the LLM is
told what is already tracked for free and only reaches for locate_in_view on
something outside that coverage.
The palette is closed: the LLM cannot dispatch a skill that isn't installed, capability-matched, and licensed.
Reasoner model selection
Selection is model-first (ADR-0088). OPENRAL_REASONER_MODEL names a curated
openral_core.REASONER_MODELS entry; registry membership means the model has
passed OpenRAL's robotics tool-calling contract. The entry resolves the client
dialect, served model id, endpoint, auth, hosting mode, and local-compute floor.
The library has no default; openral deploy sim defaults to gpt-5.5.
| Registry key | Served model | Hosting |
|---|---|---|
claude-opus-4-8 |
claude-opus-4-8 |
Anthropic cloud |
gpt-5.5 |
openai/gpt-5.5 |
OpenRouter cloud |
gpt-5.6 |
openai/gpt-5.6 |
OpenRouter cloud |
cosmos3-edge |
nvidia/Cosmos3-Edge |
managed local vLLM |
Other env: OPENRAL_REASONER_ENDPOINT (optional location override),
OPENRAL_REASONER_API_KEY (conditional), and
OPENRAL_REASONER_{MAX_TOKENS,TIMEOUT_S}. A raw uncurated model id also needs
OPENRAL_REASONER_ENDPOINT; doctor reports it as unverified.
ENDPOINT takes a named endpoint as well as a URL:
| name | base URL | dialect | key | first-call timeout |
|---|---|---|---|---|
anthropic |
SDK default | anthropic | required | 10 s |
openrouter |
https://openrouter.ai/api/v1 |
openai | required | 10 s |
gemini |
…/v1beta/openai/ |
openai | required | 10 s |
xai |
https://api.x.ai/v1 |
openai | required | 10 s |
deepseek |
https://api.deepseek.com |
openai | required | 10 s |
huggingface |
https://router.huggingface.co/v1 |
openai | required | 60 s |
ollama |
http://localhost:11434/v1 |
openai | optional | 60 s |
vllm |
http://localhost:8000/v1 |
openai | optional | 60 s |
A name carries its own dialect, so OPENRAL_REASONER_DIALECT=anthropic|openai
is needed only for a bare URL — nothing can classify one. Set it anyway to
override a preset sitting behind a translating proxy. The 60 s rows are the
endpoints that materialise a model on the first call (a cold Ollama/vLLM
daemon, the HF serverless router); huggingface additionally downgrades
tool_choice to auto, which is the only value its router accepts.
# Curated cloud
export OPENRAL_REASONER_MODEL=gpt-5.5
export OPENRAL_REASONER_API_KEY=sk-or-...
# Curated managed local
export OPENRAL_REASONER_MODEL=cosmos3-edge
# Uncurated on a named endpoint — no dialect needed
export OPENRAL_REASONER_MODEL=qwen3:8b
export OPENRAL_REASONER_ENDPOINT=ollama
# Uncurated on a bare URL — dialect required
export OPENRAL_REASONER_MODEL=qwen3:8b
export OPENRAL_REASONER_ENDPOINT=http://10.0.0.5:11434/v1
export OPENRAL_REASONER_DIALECT=openai
The old OPENRAL_REASONER_LLM_* provider-first contract was removed in 0.3.0;
see the migration table in
packages/openral_reasoner_ros/README.md.
openral doctor resolves the model registry directly,
checks auth, and probes loopback endpoints; a down managed-local endpoint is
informational only while autostart is enabled. Tests use the deterministic
FakeToolUseClient process-boundary double (CLAUDE.md §1.11).
System prompt & context
At on_configure, resolve_reasoner_system_prompt(...) composes the prompt in
two parts:
- Base brief —
DEFAULT_SYSTEM_PROMPT(robot-agnostic operating brief: one-tool-per-tick, faithful goal adherence, locate-before-manipulate, navigate-to-approach, observe-but-never-bypass safety), overridable viaOPENRAL_REASONER_SYSTEM_PROMPT. ## THIS ROBOTblock — rendered from the activeRobotCapabilities(embodiment tags, whether it can locomote — which gates the navigate-to-approach rule — manipulation/sensing hardware, payload, control modes).
Each tick the ContextRenderer assembles the per-tick situation report from the
subscribed topics: world state (/openral/world_state_slow, 5 Hz),
failures (the /openral/failure/* bus), perception events, and pending
operator prompts. The reasoner does not read pixels directly — vision
reaches it through the perception tools (query_scene, query_task_progress,
locate_in_view) and the GStreamer perception bus.
Bounded replanning
ReasonerCore enforces a per-kind retry cap (retry_cap_per_kind, default 3):
consecutive selections of the same tool kind beyond the cap are suppressed for one
tick with suppressed_reason="retry_cap". The streak resets when context shifts
materially (new operator prompt, palette refresh). This is the concrete gate that
ships; the broader ladder (retry → param-tweak → substitute-skill → goal-replan →
human-handoff, CLAUDE.md §3) is partially realized — the substitute/replan rungs
are still being built out.
Observability
Every ReasonerCore.tick opens an OTel span reasoner.tick (via
openral_observability.reasoner_span) with attributes including reasoner.tick.idx,
reasoner.model, reasoner.force, reasoner.tool, reasoner.rskill_id,
reasoner.tier (A/B/C/D/heartbeat), and reasoner.suppressed_reason
(palette_empty / retry_cap / heartbeat_idle). The captured traceparent is
threaded onto outbound emit_prompt payloads so the bag↔OTel correlator can join a
published prompt back to the producing tick. Watch it live on openral dashboard.
Running it
just ros2-build
source install/setup.bash
export OPENRAL_REASONER_MODEL=claude-opus-4-8
export OPENRAL_REASONER_API_KEY=sk-ant-...
ros2 run openral_reasoner_ros reasoner_node
ros2 lifecycle set /openral_reasoner configure
ros2 lifecycle set /openral_reasoner activate
In practice the reasoner comes up as part of the deploy graph
(openral deploy sim / openral deploy run), which wires it alongside the HAL,
safety, world-state, and perception nodes.
In development
The reasoner core, playbooks, the self-maintained MEMORY.md, and the sequential
mission task-queue have all landed on this integration branch.
Still in flight:
- Dashboard mission card — surfacing the
MissionStateledger + the reward gate and attempts/cap ladder on the liveopenral dashboard(PR #122).
See also
- Reasoner Design & Decisions — the why behind every mechanism on this page, organized by logic problem.
openral_reasoner_rosREADME — full ROS wrapper contract, provider presets, baseline LLM configs.- The reasoner/supervisor graph + tool-dispatch design.
- Reasoner-managed SLAM/Nav2 background services.
- LLM task planning & active search.
- Playbooks + self-maintained MEMORY.md.
- Success-gating + sequential mission task queue.
- rSkills reference — the
kind: detector/vlm/reward/ros_action/playbookskills the reasoner reads and dispatches.