Skip to content

conformance: grade a training server (SPEC 8.2), with a reference server written against SPEC - #10

Merged
tactino merged 6 commits into
mainfrom
feat/server-conformance
Oct 2, 2026
Merged

tactino merged 6 commits into
mainfrom
feat/server-conformance

Conversation

@tactino

@tactino tactino commented Oct 1, 2026

Copy link
Copy Markdown
Member

The checker graded clients only. This grades a training server, so both ends of the protocol can be checked: plugrl-server, the RLinf bridge to come, or anyone else's. Stacked on #9 (and #8): merge those first.

plugrl-conformance-server (new SPEC 8.2) is a client that drives a server the way SPEC lets a client behave, and checks what comes back. It runs in phases:

  • exchange:
    • the server speaks first, with metadata;
    • every action is [H, n, *da], with env_ids equal to env_indices in order, and agrees with the declared action_horizon and action_dim;
    • the server keeps the connection open through ragged batches, a feedback env set unlike the infer's, terminated episodes and a 2.25 MiB frame.
  • errors: a malformed infer, two infers in a row, and an info that cannot be split per env. Each must close that connection with 1001 and plugrl-server-resync, and a new connection must still get metadata afterwards.
  • scoping: two connections at once, both using env 0.
  • stop (--until-stop): the run must end with 1001 and plugrl-server-stop.

It sends a states-only observation, --state-dim values under --state-key (default obs), and can add an image with --image-key. The phases never leave a connection idle while another waits, because plugrl-server waits for every connected client before it infers.

examples/reference_server.py is a server written against SPEC that trains nothing. It answers with random chunks, validates every message, closes for a resync on anything malformed, survives a bad client, and stops after --steps frames. It passes every phase. Its --bug option breaks one server clause at a time: no-metadata, env-major, no-env-ids, wrong-horizon, lenient, crash-on-info and plain-stop. tests/test_server_conformance.py checks that the grader names each one.

Against plugrl-server (dummy policy, CPU, on this laptop):

  • main (835f691): no violations, in all four phases.
  • The commit before #108 (33079ed) fails two checks, which is a real defect this would have caught:
    • an info it could not split closed the connection with 1011;
    • the server then refused new connections with HTTP 503 and exited.

Local run: 50 passed.

Next:

  • plugrl-server's own CI runs this against WebSocketAgentServer, once it pins a plugrl-protocol that has it.
  • The RLinf bridge is graded with it.

tactino and others added 6 commits October 1, 2026 16:34
…the checker sees what a client does with each action and how it handles closes

The passive checker watched one connection and could not tell chunk-summed
reward, terminal observations, time-major order, metadata-first, the close
reasons or text frames from their violations. With --probe the client runs a
probe env whose observation counts steps and echoes the applied action, and
the checker drives the connection: it speaks late, sends a 2 MiB metadata
frame, closes for a resync, stops the run, and answers with a text frame.
raw_client.py implements the probe env, and its --bug option breaks each
clause in turn; tests/test_conformance_probe.py checks that each is named.
…fter

Found by the probe checker itself, on Linux, in about one run in two of the
resync scenario: when the server's resync close arrived while the feedback
was being sent, the send raised, the reset after it never ran, and env 0
carried its finished episode onto the next connection (t = 7, 11, 15 for an
episode of 3). The packed feedback already holds the terminal observation,
so resetting first changes nothing the server sees.
…ed in CI in every scenario

`plugrl_client --probe host port batch` runs the probe environment, executes
each chunk time-major and flushes it at the terminal step, and handles the
section 7 close reasons: it reconnects after plugrl-server-resync with
nothing resent, exits 0 on plugrl-server-stop, and exits 2 on a text frame.
A close now raises ServerClosed with its code and reason, and a text frame
TextFrame, instead of one generic error. Passive mode is unchanged.
…s a 7.6 violation; raw_client --bug stale-chunk

plugrl-env-client failed the resync scenario this way: after reconnecting it
fed back chunks answered on the old connection, which the checker reported
under 4.3. On a reconnected connection, feedback for an env that was never
given an action there is now named for the clause it breaks. The new
stale-chunk bug does exactly that, and the test expects the 7.6 clause.
… 8.2), and examples/reference_server.py is one written against SPEC alone

A client that drives a server the way SPEC lets a client behave - ragged
batches, a feedback set unlike the infer's, large frames, three kinds of
malformed message, two connections sharing env 0, and the end of the run -
and checks the action layout and env_ids, the resync closes, that the server
survives a bad client, and the stop reason. The reference server trains
nothing and passes; its --bug option breaks each server clause in turn.
plugrl-server main passes; its version before #108 fails two checks.
@tactino
tactino merged commit d021e07 into main Oct 2, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant