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
| Field | Meaning |
|---|---|
job_id | The credential the operator gave you |
protocol_version | msg.PROTOCOL_VERSION of your client library |
manifest | ClientManifest |
ClientManifest
| Field | Meaning |
|---|---|
model | ModelIdentity{name, version, digest}. Customer-declared and recorded as such, never verified |
action_schemas | Schemas you can produce; the station picks one it offers |
observation | ObservationRequest: images (ImageRequest{camera, max_width, max_height, encoding}) and proprio fields |
max_horizon | Longest chunk you will return; the station may lower it |
stateful | Whether you keep per-episode state |
preferred_step_hz | 0 leaves the rate to the station's eval config |
Welcome
| Field | Meaning |
|---|---|
session_id | This session |
station | StationManifest{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 |
negotiated | Negotiated{action_schema, images[], proprio[], step_hz, max_horizon, deadline_ms}. What the session actually runs with |
task | Task{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
| Code | Meaning |
|---|---|
UNKNOWN_JOB | No runner is waiting for this job id |
JOB_BUSY | Another client is attached to this job |
PROTOCOL_VERSION | Client library too old or too new |
PROFILE_UNSUPPORTED | The manifest asks for something the station cannot serve |
PREFLIGHT_FAILED | Latency or chunk validation failed; the message carries measurements |
Episode messages
Station to client:
| Message | Fields | Meaning |
|---|---|---|
Reset | episode_id, seed, task, synthetic | Clear all per-episode state. Must be answered with ResetAck{episode_id} before any Observe |
Observe | episode_id, step_id, request_id, deadline_ms, observation, synthetic | One observation; answer with one Chunk |
Close | reason | Session over; release everything |
Client to station:
| Message | Fields | Meaning |
|---|---|---|
ResetAck | episode_id | Acknowledges a Reset |
Chunk | episode_id, step_id, request_id, actions[], diagnostics | 1 to negotiated.max_horizon actions at offsets k * dt |
EndEpisode | episode_id, reason | Declares 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, orrequest_iddoes not match the outstanding request is discarded and counts as a miss. - Deadlines are enforced on the station's monotonic clock.
deadline_mson the wire is advisory. A late chunk is recorded as late and discarded. - No transport retry inside a step. The next
Observeis 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_LOSTand 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 entersPAUSEDand 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 withSTREAM_LOST.