The harness serializes every observation channel before anything reads it, so what a device produced —
a roboarm.State — reaches the policy stack as flat suffixed strings. Meaning then has to be recovered
from names.
Where this stands
Harness._build_obs calls each channel's serializer and flattens the result:
value = obs.serializer(value)
inputs.update({full: v for full, v in expand_suffixed(name, value) if v is not None})
So an arm's status arrives as the entry robot_state.status (or robot_state.{side}.status on a
bimanual rig), and StopOnFault — which runs in-process, on the rig, with the object right there —
finds it by matching that name:
def _is_robot_status(name: str) -> bool:
return name.startswith(f'{keys.ROBOT_STATE}.') and name.endswith('.status')
Consequences:
- A rig-side wrapper reads a string convention where it could read a type. The convention is spelled in
more than one place and nothing checks the two agree.
- The suffixes a serializer emits (
.q, .status) and the canonical names in positronic/keys.py are
the same contract written twice.
- Everything downstream pays the flattening cost even when no wire is involved: an in-process pipeline
serializes and never deserializes.
Decision
The harness passes domain objects. Serialization happens at the local/remote border and in the dataset
writer, and nowhere else. A wrapper that needs the arm's status reads it off the value with isinstance,
not off a name.
Consequences
eval.Observation loses its serializer: the value's domain type owns its policy- and dataset-side
encoding. (The class already carries a TODO saying so.)
_is_robot_status in positronic/policy/wrappers.py goes, and StopOnFault reads State.status.
- The offboard client serializes on the way out; a server-side stack keeps reading names, since the wire
carries no types.
keys.ROBOT_STATUS and friends stay: they name recorded signals and wire entries, which is what they
are for.
Relation to other work
Falls out of #637, which put the arm's status on the wire as data and left the name-matching behind.
The harness serializes every observation channel before anything reads it, so what a device produced —
a
roboarm.State— reaches the policy stack as flat suffixed strings. Meaning then has to be recoveredfrom names.
Where this stands
Harness._build_obscalls each channel's serializer and flattens the result:So an arm's status arrives as the entry
robot_state.status(orrobot_state.{side}.statuson abimanual rig), and
StopOnFault— which runs in-process, on the rig, with the object right there —finds it by matching that name:
Consequences:
more than one place and nothing checks the two agree.
.q,.status) and the canonical names inpositronic/keys.pyarethe same contract written twice.
serializes and never deserializes.
Decision
The harness passes domain objects. Serialization happens at the local/remote border and in the dataset
writer, and nowhere else. A wrapper that needs the arm's status reads it off the value with
isinstance,not off a name.
Consequences
eval.Observationloses its serializer: the value's domain type owns its policy- and dataset-sideencoding. (The class already carries a TODO saying so.)
_is_robot_statusinpositronic/policy/wrappers.pygoes, andStopOnFaultreadsState.status.carries no types.
keys.ROBOT_STATUSand friends stay: they name recorded signals and wire entries, which is what theyare for.
Relation to other work
Falls out of #637, which put the arm's status on the wire as data and left the name-matching behind.