Declare actions
Each turn you sendvalid_actions, the set available right now. The set can change every turn as the participant moves through your environment. A declared action has this shape:
namematches^[a-z][a-z0-9_]{0,63}$: lowercase, starting with a letter, up to 64 characters.descriptionsays what the control does, for the resolver.parametersis a restricted JSON Schema subset (below), or{}for no parameters.resolver_hintis an optional per-control note read only by the resolver in intent mode (below).
The parameters subset
parameters accepts a deliberately small subset of JSON Schema. Anything outside it is a 422.
{}: no parameters; or{ "type": "object", "properties": { ... }, "required": [ ... ] }, where each property is either- an enum:
{ "enum": ["a", "b", ...] }(a non-empty list), or - a free string:
{ "type": "string" }(an optional"description"is allowed).
- an enum:
- No integers, numbers, booleans, nested objects, arrays, or other keywords.
- Every name in
requiredmust exist inproperties.
Keep labels from leaking
A label names what is pressable, never what it means. The participant reads meaning off the pixels, so a label that encodes hidden state hands them knowledge a person would not have.- Declare
movewith a direction, notwalk_to_treasure. - Declare a from-and-to square pair, not the chess move
Nxe5+(which leaks the piece, the capture, and the check). - Declare
clickon a visible target, notrusty key(which leaks an object’s identity and role before the participant has looked).
Guide the resolver (intent mode)
In intent mode a resolver maps the participant’s intent onto your declared actions. Two layers let you steer it, and neither is ever shown to the participant:resolver_hintis a per-control note on a single declared action (up to 1000 characters). Use it to disambiguate one control: “this toggles mute, not volume”.resolver_guidanceis a session-level, environment-wide manual passed on create (up to 8000 characters). Use it for rules that span the whole environment: how your navigation works, what a gesture does, which controls are modal.
The web adapter
You can drive the API from any client. For web environments there is a small convention worth following, an object onwindow.__ishEnv that the loop reads each turn:
execute ({ ok: false, error }) becomes the next turn’s error block. There is no separate executed field. Native, desktop, and game integrations do not need this adapter; they run the same loop directly: observe, post, execute.
WebGL and 3D environments
Read this before you integrate anything that paints to a<canvas>. Headless browsers do not render WebGL reliably. Three failure paths integrators have hit:
- headless rendering returns a blank frame (a black image, tens of seconds per capture);
canvas.toDataURL()returns blank unless the context was created withpreserveDrawingBuffer: true;page.screenshot()hangs for tens of seconds and times out.
--use-angle=swiftshader is a software-rendering fallback that lets a headless capture work, but it is not enough for dense scenes (a point cloud, a splat world); those need a real GPU.
Frame size
Real frames are far larger than a 1x1 placeholder. A frame about 1024 pixels wide is roughly 1 to 2 MB of base64, already close to the limit; a full 1080p frame exceeds it.- Keep frames around 1280x800, as PNG.
- The hard ceiling is a
413. If you hit it, reduce the frame’s dimensions, not its format. - The bytes are labeled
image/png, so send PNG. Sending JPEG under that label can fail to decode. To shrink a frame, scale it down; do not switch format.
Related
Observations
Why labels must not leak and state is refused.
Intent and resolution
How the resolver maps intent onto your actions.