Skip to main content

Protocol

The policy client and the station communicate over gRPC using a single bidirectional stream. The message schema is defined in Protocol Buffers (proto3) under the package name se3labs.eval.policy.v1. The se3labs Python package exposes every message and enumeration of that schema as se3labs.eval.msg, so no code generation is required on the client side. If you are using the policy.serve_policy helper described in getting started, the exchange below is handled for you; this page specifies the protocol for readers who need the message-level detail or who are implementing a client in another language.

The service exposes one method. Run opens the stream for a session, and every message in either direction is a wrapper carrying exactly one of the message types listed here:

service PolicySession {
rpc Run(stream ClientMsg) returns (stream ServerMsg);
}

// Client → station
message ClientMsg {
oneof msg {
Hello hello = 1; // opens the session
ResetAck reset_ack = 2; // acknowledges a Reset
Chunk chunk = 3; // answers an Observe
EndEpisode end_episode = 4; // declares the running episode finished
}
}

// Station → client
message ServerMsg {
oneof msg {
Welcome welcome = 1; // session accepted; carries the negotiated profile
Reject reject = 2; // session refused; carries the reason
Reset reset = 3; // start of an episode
Observe observe = 4; // one observation, to be answered with a Chunk
Close close = 5; // end of the session
}
}

The station end of the stream is the evaluation runner. When the client connects through the relay, the relay forwards messages between the client and the runner without inspecting or modifying them.

Handshake

Hello

FieldMeaning
job_idThe credential the operator gave you
protocol_versionmsg.PROTOCOL_VERSION of your client library
manifestClientManifest

ClientManifest

FieldMeaning
modelModelIdentity{name, version, digest}. Customer-declared and recorded as such, never verified
action_schemasSchemas you can produce; the station picks one it offers
observationObservationRequest: images (ImageRequest{camera, max_width, max_height, encoding}) and proprio fields
max_horizonLongest chunk you will return; the station may lower it
statefulWhether you keep per-episode state
preferred_step_hz0 leaves the rate to the station's eval config

Welcome

FieldMeaning
session_idThis session
stationStationManifest{station_id, arms[], cameras[], software_version}. Each ArmSpec has name, num_joints, joint_lower_rad, joint_upper_rad, has_gripper; each CameraSpec has name, width, height, fps, mount
negotiatedNegotiated{action_schema, images[], proprio[], step_hz, max_horizon, deadline_ms}. What the session actually runs with
taskTask{id, version, instruction, parameters}

Resolution rule: the station's caps bound your request. Anything you need that the station cannot offer rejects with PROFILE_UNSUPPORTED.

Reject

CodeMeaning
UNKNOWN_JOBNo runner is waiting for this job id
JOB_BUSYAnother client is attached to this job
PROTOCOL_VERSIONClient library too old or too new
PROFILE_UNSUPPORTEDThe manifest asks for something the station cannot serve
PREFLIGHT_FAILEDLatency or chunk validation failed; the message carries measurements

Episode messages

Station to client:

MessageFieldsMeaning
Resetepisode_id, seed, task, syntheticClear all per-episode state. Must be answered with ResetAck{episode_id} before any Observe
Observeepisode_id, step_id, request_id, deadline_ms, observation, syntheticOne observation; answer with one Chunk
ClosereasonSession over; release everything

Client to station:

MessageFieldsMeaning
ResetAckepisode_idAcknowledges a Reset
Chunkepisode_id, step_id, request_id, actions[], diagnostics1 to negotiated.max_horizon actions at offsets k * dt
EndEpisodeepisode_id, reasonDeclares the running episode finished; see below

Diagnostics{inference_ms, model_version, extra} is untrusted annotation: recorded, never acted on.

Ending an episode from the policy

A policy may end the running episode before the station's own end conditions (step limit, timeout, or operator) apply, for example when it determines that the task is complete or that it cannot proceed. To do so it sends EndEpisode with the current episode_id. The message may be sent in answer to an Observe, in place of a Chunk, or at any other point during the episode.

On receipt the station stops sending Observe messages, holds the arms, and prompts the operator to score the episode as it stands. The station does not treat EndEpisode as a verdict: the outcome is still assigned by the operator. reason is free text recorded in the episode manifest as end_reason, alongside end_condition = "policy"; it is not interpreted.

An EndEpisode whose episode_id does not name the running episode is ignored, as is any EndEpisode received during preflight. The step in which EndEpisode replaces a Chunk is not counted as a missed step.

Rules

  • The station generates every id; the client echoes them. A chunk whose episode_id, step_id, or request_id does not match the outstanding request is discarded and counts as a miss.
  • Deadlines are enforced on the station's monotonic clock. deadline_ms on the wire is advisory. A late chunk is recorded as late and discarded.
  • No transport retry inside a step. The next Observe is the retry; chunk buffering covers the gap.
  • All numbers must be finite. Any NaN or infinity discards the chunk.
  • A chunk for step k supersedes the unexecuted tail of the chunk for step k−1 from the moment it is accepted. Overlap is resolved by time, not index.
  • If the stream is lost during an episode, the episode is invalidated with STREAM_LOST and the arms walk to the home pose, as after every episode. The episode is retried according to the job's retry policy. Before the next episode begins, the job enters PAUSED and waits up to 120 seconds for a client to reconnect with the same job id; the relay pairs the new connection to the waiting runner. If no client reconnects within that period, the job fails with STREAM_LOST.