Session protocol¶
The session protocol is how any front end plays the games: the web
showcase's browser worker, our own service over WebSocket and HTTP, and
the command line, which runs on the same session layer. This page lists the messages, the
fields of a frame, how play --view json relates to frames, and the
versioning rules; recorded sessions are stored in the
replay format. The machine-readable form is the JSON Schema
src/grounded_decision/session/protocol.schema.json (generated from
PROTOCOL_VERSION), generated from the Python dataclasses; the plan behind it is
Web showcase: project plan, phase 1. How
the browser runs it is The engine in the browser.
The session layer in Python¶
grounded_decision.session opens a match from a spec and steps it:
from grounded_decision.session import Session, SessionSpec
session = Session.open(SessionSpec("tictactoe", agents=("you", "grounded")))
[move.notation for move in session.legal_moves()] # ['1', '2', ..., '9']
frame = session.submit("5") # a person's move, in the game's notation
frame = session.step() # the agent to move decides and plays
frame.decision.verdict # Verdict.ACT: every check that ran passed
frames = session.run(10) # up to 10 moves; stops when a person must move
Session.describe() # every game, agent, check, world and variant
| Name | What it does |
|---|---|
SessionSpec(game, agents, world, seed, size, players, until_length, rules, you, you_play, guarded, max_steps) |
Everything that fixes a match. Built and validated by the same code as grounded-decision play (GameModule in games/registry.py), so it refuses exactly what play refuses, with the same message. |
Session.open(spec) |
A session at the starting position. |
session.step() |
The agent to move decides and plays; returns the new Frame. |
session.submit(text) |
A person's move; with guarded=True the gate checks it and may play its safe fallback. |
session.run(count) |
Up to count moves, stopping when a person must move or the game ends. |
session.legal_moves() |
The mover's legal moves, each with the notation that submit accepts. |
session.select(cell) |
A person picks the piece on cell: a PieceSelection with highlights (selected, target_near, target_far) and one legal move per target. Games with select and cell_name hooks (Hexxagon) mark their own targets, as the terminal's selection prompt does; otherwise the targets are the legal moves that start on the cell. |
session.frame |
The current frame. |
session.info |
What stays fixed: seats, world, the panel's rows. |
Session.describe() |
The catalogue: games, agents, regimes, checks, worlds, variants and sizes. |
The layer is pure: it does no I/O, starts no threads, handles no
signals and reads no clock, so it runs unchanged in Pyodide. A test
runs every game with signal, subprocess and multiprocessing
blocked and the clock and Thread.start replaced by functions that
fail. The command line injects time.perf_counter for its own timing.
Messages¶
Every message is a JSON object with a type. A request may carry an
integer id; the reply repeats it. An Endpoint answers each request
with exactly one reply and never raises; Endpoint.handle_text does the
same for JSON text (what the browser's worker passes between JavaScript
and Python).
| Request | Fields | Reply |
|---|---|---|
describe |
(none) | describe: version (the protocol version) and catalogue: the games and, from 1.2, the regimes (each name, label and description, the words of play GAME --explain) |
open |
spec: a SessionSpec |
frames: session and the first frame |
step |
count (1 to 10,000, default 1) |
frames: one frame per move played |
submit |
move: a legal move's notation |
frames: the frame after the move |
select (1.1) |
cell: a {row, column} of the board |
selection: the piece, its overlays, a hint and one legal move per target (moves, origin first and target last in cells) |
compare (1.3) |
spec: a CompareSpec (compare's options: game, agents, seeds, first_seed, opening_plies, size, players, rules, world, max_steps); games: indices to play now (at most 100); results: games other engines finished |
comparison: the plan (format, seatings, seeds and openings, number of games, the identical command), the games played, and the table of every game this engine knows |
audit (1.3) |
spec: an AuditSpec (audit's options: game, agent, opponent, oracle, oracle_depth, seed, size, seat, players, world, max_steps); moves: how many to play now (default 100) |
audit: the info (agent, seat, line-up, unit, the session's seats and panel, the command), the decisions judged in these moves, and the summary so far |
analyse (1.3) |
(none) | analysis: the open session's position analysed by the game's own solvers (index, and an Analysis or null) |
learn (1.4) |
max_risk; sample (how_to or authorisations) or cases (each LearnCase: the evidence fields, proposed_action, action_was_correct, optional support, was_acted_on, case_id); confidence, measure, procedure, seed, grid (a LearnGrid, or null for the default), learn_decision_line |
learned: status, the dataset, target, chosen settings (as Limits names them), the risk and holdout parts, counts of combinations, explain (learned.explain()), warnings, the fingerprint, parameters_json (what learn --output writes) and every combination's test in candidates |
monitor (1.4) |
scenario (authorisations or outcomes); max_risk, confidence, window; the scenario's settings (authorisations: batches, batch_size, drift_from, seed; outcomes: before, before_rate, after, after_rate, seed) |
monitored: the target, window, decisions, the monitor after every recorded outcome (steps: counts, bounds, status), the first alarm and drift_step, and the report's lines |
runtime (1.4) |
events (each entity_id, event, payload, candidate_model_score, idempotency_key); enforce (false is shadow mode) |
decisions: the policy_version, parameters_version, valid_events, the demo data's accounts, and one step per event (state before and after, the evidence, the six checks, verdict, routing, review and outbox numbers, the DecisionRecord as JSON) |
An error reply has a code and a one-sentence message, and leaves
any open session unchanged:
| Code | Meaning |
|---|---|
message |
The request is malformed: unknown type, unknown field, wrong type, count out of range. |
spec |
The game, seats, size, rules or world are not allowed (the command line's message), or a demo cannot run with these settings or data. |
session |
Not now: no session is open, a person must move, the move is not understood or not legal, the game is over, no move starts on the selected cell. |
internal |
A bug; the message names the exception. |
A spec in JSON uses the field names above; size is text ("20x20"
or a named size such as "expert"), rules an object of the options
the game's configure takes (mines, preview, board, start,
layout, holes, stall_plies …). Unset fields take the defaults of
play.
Comparisons and audits (1.3)¶
compare and audit run the command line's own code
(Tournament in games/compare.py, AuditSetup and AuditRun in
games/audit.py; design decisions D59 to D61). A comparison's games are
numbered: game i is seed seeds[i // len(seatings)] with seating
seatings[i % len(seatings)]. A compare request with the same spec as
the open comparison continues it: the engine plays the games asked for
(a game already known is not played again), takes in the results
another engine sent (a result that contradicts a known one is a
session error), and replies with the table of every game it knows.
The table is tallied in index order, so it depends only on which games
it holds, and once it holds them all it equals compare --json: each
row's games, wins, draws, losses, cut_off, points,
mean_moves, mean_score, overrides, win_rate and its exact
Clopper–Pearson interval (low, high), rounded as the command line
rounds them. A browser spreads the games over several engines this way.
An audit request with the same spec as the open audit plays on;
nothing is played once the match is over. Each judged decision has the
move played and the oracle's (chosen, best), their values and the
regret in the oracle's unit; a disagreement (positive regret) also has
its position: the before board, the chosen_cells and best_cells,
and the frame after the move, whose decision the inspector reads. The
summary is the command line's report so far (agreement with its
interval, total and mean regret, overrides, the worst decisions, the
oracle's name, the outcome); at the end it equals the audit object of
audit --view json --no-regimes. The regime price table stays in the
command line. The catalogue's games list their audit oracles (1.3).
Analyses and lessons (1.3)¶
An Analysis is one position analysed by the package's own solvers:
values (a number per cell, read on its scale: probability,
signed, count or none), tags (conclusions about cells, named in
tag_names), routes (ordered cells, closed for a cycle) and facts
(numbers about the whole position), with a title and a one-sentence
summary. Each game with a teaching page defines one
(grounded_decision.session.lessons.GAME.analyse): the snake's cycle and
certificate, Minesweeper's mine probabilities, the Connect Four solver's
column values and so on. A Lesson is a document, not a message: the
scenes (real sessions) and positions a teaching page explains, each with
its frame and analysis; make lessons writes one per game into the
site, and the schema defines it so a front end validates it.
The demos beyond games (1.4)¶
learn, monitor and runtime run the library's other documented
examples for the web showcase's /parameters, /monitor and /runtime
pages (grounded_decision.session.demos; design decisions D64 to D68).
Each request is complete in itself and its reply has everything to draw;
none touches the open game session. They need numpy, which the browser's
engine loads only for the pages that send them. The numbers are the
command line's: learn runs learning_run (the code of
learn_parameters, which also reports every combination's exact test),
the authorisations drift is the parameter_learning example's live
traffic and prints its report's lines, the outcomes stream is the
how-to's example (evenly spaced errors, or seeded draws), and runtime
runs the reference service's pure core (examples/reference_core.py)
over the demo data, giving POST /decide's routing, versions, review and
outbox numbers. A setting or data a demo cannot use is an error with
code spec; tests/test_session_demos.py compares every demo with the
command line, the reference service and the docs' numbers.
Board sizes (1.9)¶
A catalogue game's size_offers say which sizes its board may take, so a
front end offers only those: none for a fixed size (tic-tac-toe), which
it does not offer to change. Each offer's rule is the game's SIZES
(games/sizes.py): a width range (low, high or null for no limit,
step: checkers' sides are even), a height range or null for one
number, the side (NxN: Go's lines, a hexagonal board's side),
one_even (the width or the height must be even: snake), the unit a
side counts, the default size and the menu of usual sizes, each with
an optional label (standard, expert); its description is the rule
in one line. The first offer holds unless a later one's option (a rule
option, such as board) has that offer's value: a Hexxagon donut needs
side 4, and a procedural Breakout layout fits a narrower field than the
built-in levels. A spec's size stays WxH (or a named size of
sizes); the engine checks it with the same rule, and what no rule can
say (a layout read from a file must fit) it still refuses with a reason.
Lapsed guarantees and rule options (1.10, 1.11)¶
A frame's decision has a lapse (1.10) when the gate cannot certify
the move played because no move meets what its checks protect (Breakout:
no spin-proof bounce exists; Minesweeper: no cell is proved safe; Connect
Four: the opponent forces a win within two whatever is played); empty
while the gate's guarantee holds. A front end shows it rather than the
regime's promise.
A catalogue game's rules (1.11) are the options its configure takes
that a command line sets: each with its name (the keyword), flag
(also the URL parameter), label, kind (int, text, switch,
choice, or file: a path a browser cannot offer), its choices or
span, its values in words (1 to 9), its default, its help, the
option values it requires (Tetris's level needs mode realtime) and
whether 0 turns it off (zero_is_off). They come from the flag table
and the game's OPTIONS (games/options.py).
Transports¶
The protocol is the same whatever carries it; each transport moves JSON
text and nothing else. Every one answers with Endpoint.handle_text's
exact text for the same request (the conformance tests compare them
byte for byte, tests/test_service.py and web/tests/e2e/service.spec.ts).
| Transport | Session | How a message travels | Where |
|---|---|---|---|
| In-process | one Endpoint |
Endpoint.handle(message) or handle_text(text) |
the command line, tests |
| Web Worker | one per worker | postMessage of the text; the worker calls handle_text in Pyodide |
WorkerTransport (The engine in the browser) |
WebSocket /ws/ |
one per connection | one text message per request and per reply, in request order; requests may be sent before the replies come | our own service, WebSocketTransport |
HTTP POST /api/ |
a fresh one per request | the request as the body, the reply as the body (application/json) |
our own service |
Our own service (grounded-decision serve,
Run our own service) adds limits a
browser does not need. A request over one is not run; it gets an
error reply of the protocol whose message names the limit:
| Limit | Code | id |
Then |
|---|---|---|---|
Message larger than GD_SERVICE_MAX_MESSAGE_BYTES (HTTP 413) |
message |
null | the session stays open |
| A binary WebSocket message | message |
null | the session stays open |
step count, audit moves or compare games above the service's maximum |
message |
the request's | the session stays open |
| A comparison with more seeds than the service allows | spec |
the request's | the session stays open |
| More messages than the connection's rate | session |
the request's | the session stays open |
| More sessions than the process allows (HTTP 503) | session |
null | the socket closes with 1013 |
A request slower than GD_SERVICE_REQUEST_TIMEOUT (HTTP 504) |
session |
the request's | the socket closes with 1013: the session is lost |
A socket silent for GD_SERVICE_IDLE_TIMEOUT closes with 1001. An
error with a null id belongs to the oldest request not yet
answered, which the engine could not read; the TypeScript Engine
fails that request with it. No new message or code was needed, so the
protocol's version did not change (design decision D78).
Frames¶
A frame is one position with everything a front end draws for it. It
is semantic: cells are CellKind values and highlights Overlay
values, never glyphs or colours.
| Field | Contents |
|---|---|
index |
Moves played to reach it (0 at the start). |
to_move, awaiting_input |
The seat to move (null once over), and whether a person must submit. |
decision |
The move and how it was chosen: seat, player, agent, move, proposal, verdict (act, degrade, abstain or null), checks (each name, passed, explanation: the evidence), notes, metrics, regime. Null at the start. |
board |
layout (square, pointy, flat), rows, columns, cells, hex offsets, details (per-cell order, connections, heading, label, tint), overlays (row, column, overlay), coordinate labels, row_labels, column_labels, focus, and (1.2) paths: ordered cell lists, each with a kind, whose cells' details are derived (below). |
transition |
In-between pictures of the move (a board each, and a weight), for animation. |
move_cells, proposal_cells |
The cells the move touched, origin first; on an override, the rejected proposal's cells. |
arrows (1.7) |
The move as arrows: each a role (played, or proposed for an override's rejected proposal), the seat that moved and its cells, origin first. Defined over the previous frame's board, where the piece still stands: the origin holds a piece of that seat and every later cell is free (ArrowRule, session/arrows.py). Empty for placements and drops. |
event |
What the move did in a few words (text, size, big), or null. |
panel |
counters (moves, acts, overrides, ungated), the game's stats, standings with recent history, recent events, the sparkline. |
outcome |
finished, winner, reason, scores, cut_off (the move budget ran out), placings. |
legal_moves |
When a person is to move: every legal move's notation, description, cells and, for a guarded seat, certified. |
assumption |
Outside the random world, why probabilistic statements do not apply; else empty. |
A board's paths (protocol 1.2) are ordered lists of cells with a
semantic kind; today one kind, body, a connected body head first
(the snake's). A reader derives each path cell's note instead of finding
it in details, exactly as the terminal views do
(CellPath.details in views.py, pathNotes in web/packages/board):
cell i of n has order i / (n - 1) (0 for a one-cell path),
connections to cells i - 1 and i + 1 when they are neighbours
(diagonals included), and the first cell has the path's own heading
(1.8: where the head faces, the snake's last move, which a one-cell
snake cannot show by its neck) or else the direction from the second
cell to it; a note listed in details for the same cell
replaces the derived one. A move then changes the path by one cell at
each end, where a note per cell changed every cell's order: a 20 × 20
snake frame is built and encoded about 1.6 times faster, and a recorded
full snake game is a quarter of the size. A board without paths
(older frames) has none.
The panel's fixed rows (checks, note_rows, stats_keys,
standing_rows, event_rows, sparkline) come once per session in
session.panel, so the web panel keeps the same rows on every frame as
the terminal panel does.
--view json and frames¶
grounded-decision play GAME --view json prints one JSON object per
line, in the format it always had, and that format is a documented
projection of the frames. The canonical form is:
- the
startline:game,players,agents, andworldfor games with worlds; - one
stepline per frame after a move:indexis the frame'sindex - 1,player,agent,move,proposal,verdict,checks,notes,metricsandregimeare the frame'sdecision, andstatsis the frame'spanel.stats; - the
endline: the match summary (moves,overrides,cut_off,outcome);
each serialised by json.dumps(..., sort_keys=True). A golden test
plays every game in the snapshot set on several seeds both ways, through
the command line and through Session.step, and compares the output
byte for byte.
Generated files¶
make protocol writes, from the Python source:
| File | Contents |
|---|---|
src/grounded_decision/session/protocol.schema.json |
The JSON Schema (draft 2020-12), shipped in the package. |
web/packages/protocol/generated/protocol.schema.json |
The same schema, for the TypeScript package. |
web/packages/protocol/generated/tokens.json |
Design tokens: a colour and glyph for every cell kind and overlay, dark and light themes, from the terminal palette in views.py. |
web/packages/protocol/fixtures/*.json |
Golden conversations (requests and replies) for several games. |
web/apps/site/static/demos/*.json |
The demo pages' previews (the engine's replies for the documented examples) and the how-to's history; floats to ten significant digits (D66). |
web/packages/protocol/src/generated/types.ts |
The TypeScript types, generated from the schema (needs make web-setup). |
Tests fail when any of them is stale. In TypeScript,
Protocol.default().reply(json) validates a message against the schema
and returns it typed; Vitest checks every golden fixture with it.
Versioning¶
The protocol has its own semantic version, PROTOCOL_VERSION in
grounded_decision/session/protocol.py, independent of the package's.
src/grounded_decision/session/protocol.baseline.schema.json records
the schema of the last released protocol version, and a test compares
it with the current schema:
| Change | Kind | Version bump |
|---|---|---|
| Remove a type, a field, an enumeration value or a union member; change a field's type | Breaking | Major |
| A request field becomes required, or a reply field stops being always sent | Breaking | Major |
| Add a type, an optional field, a reply field, an enumeration value or a message | Additive | Minor |
| Change only descriptions | None | Patch, or none |
Readers must ignore fields they do not know, so additions never break
an older peer; the TypeScript validator does this with
{ strict: false } (the tests validate strictly). At a protocol
release, make protocol-baseline records the new baseline.