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 (the why): Reasoner Design & Decisions, 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
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 (
min_interval_s):
| 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. |
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
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 offered only when its 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 and task ledger), never the
robot; 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
- 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 matching decision procedure. 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
(std_msgs/Empty; the deploy launch fires it once a late action server such as
Nav2's /navigate_to_pose appears). 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 knows what is already
tracked and reaches for locate_in_view only 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
cleared OpenRAL's robotics tool-calling contract), and the orthogonal
OPENRAL_REASONER_ENDPOINT overrides where it runs. The library has no
default.
export OPENRAL_REASONER_MODEL=gpt-5.5
export OPENRAL_REASONER_API_KEY=sk-or-...
Full model/endpoint matrix, named endpoints, the dialect rule, the legacy
migration table, and what openral doctor's Reasoner LLM row checks:
openral_reasoner_ros README §Reasoner model
registry.
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
(min_interval / retry_cap / retry_cap_hold / mission_finished /
heartbeat_idle / palette_empty). 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.
See also
- Reasoner Design & Decisions — the why behind every mechanism on this page.
openral_reasoner_rosREADME — full ROS wrapper contract, provider presets, baseline LLM configs.- Private decision log (
OpenRAL/management): ADR-0018 (reasoner + supervisor graph), ADR-0025 (reasoner-managed background services), ADR-0039 (LLM task planning & active search), ADR-0072 (playbooks + self-maintained memory), ADR-0073 (success gating + task queue), ADR-0088 (model registry). - rSkills reference — the
kind: detector/vlm/reward/ros_action/playbookskills the reasoner reads and dispatches.