The loop order
Each turn hands you a decision and a session status. Act on them in this order:- If
action_nameis non-null, execute it first, even if this turn is terminal. A non-null action is always yours to run. If executing fails, add anerrorblock to your next turn. - Then honor
session_status. If it is anything other thanopen(completed,gave_up,closed,max_turns,spend_cap_reached,balance_exhausted), stop, after executing any action from step 1. - Otherwise (
open), take the next turn. If you executed an action, capture the new frame and submit atturn_index + 1. Ifaction_namewas null (an intentno_matchornone_needed), execute nothing, the world is unchanged, and submit the next turn anyway. The outcome feeds back to the participant.
continue turn, then done with a null action_name on the next turn. Both bundled and split are valid, and the ordering above handles both. Execute a non-null action, then check the status.
Errors are observations
There is no separate “executed” field on a turn. When an action you executed fails, you report it as anerror observation block on the next turn, alongside the image:
error block is additional, never a second image.
Turn indexing
Theturn_index on a turn must equal the session’s current turn count. The first turn is 0, and every recorded turn increments it by one.
Every returned turn is recorded, including no_match and none_needed turns. Drive the next index off the response’s turn_index, plus one, not off whether you executed anything. Two cases move differently:
- A
502 decision_invalidrecords nothing. Retry the sameturn_indexwith a fresh frame. - A
409 turn_index_mismatchhands you the index the server expected. Resubmit withturn_indexset to thatexpected_turn_index.
Reading the trace
Read a session back to replay its decisions. The trace is a cursor page: passafter_index to get turns strictly after an index and limit to size the page (default 50, max 200). Page again with after_index set to the last index you received while has_more is true.
Idempotency and replay
The create call takes an optionalIdempotency-Key header. The same key with the same body returns the existing session (200, not 201); the same key with a different body is a 409 with error_kind: idempotency_conflict.
Every create on the API takes the header on the same terms, so a retried registry write is as safe as a retried session: sessions, environments, environment versions, and tasks. Each create looks its key up in its own scope, the workspace for sessions, environments, and tasks, and the parent environment for a version, so the same string on two different creates never collides.
Turns have a related safety net. An exact-duplicate replay of your final turn, the same turn_index, the same observation, the same valid_actions, returns the stored decision even after the session has ended. Replay is checked before the “is the session open?” guard, so a lost response to the last turn is always safe to retry. A different body at that index is a turn_index_mismatch.
Handling errors
These are the errors the session loop itself produces. Branch ondetail.error_kind, not on the status. Every kind the API returns, including the registry ones this table leaves out, is in the error reference.
Two shapes of 422
Validation errors come back in two shapes, so parse defensively:-
A malformed request (wrong types, a missing field, an invalid observation
type, a bad enum value,parametersoutside the allowed subset) returns a structured envelope with anerrorsarray of field locations: -
A business-rule rejection raised inside the endpoint (for example “exactly one image observation block is required”, or a declared-action that is not enum-or-string) returns a plain
detail:
Billing and limits
Two limits can end or block a session for cost or capacity reasons, separate from the participant’s own termination. Running out of budget. Each turn accrues credits against the organization’s credit pool, and the charge settles when the session ends. A turn that cannot be paid for is refused with a402 and is not applied; the session becomes terminal. Three refusals, three remedies. Branch on detail.error_kind, not on the status, because the first two share one status:
The session also records the reason as
ended_reason, so a session you read back later still says which of the two balance_exhausted cases it was. Treat any terminal status as “stop the loop”; to continue, resolve the cause and open a new session.
Session-create quota (per key). An API key has a cap on how many sessions it can open per day. A create that exceeds it returns a 429. Unlike the spend cap, this is transient: back off and retry the create later. It applies to API keys only, not to user tokens.
Open unions: tolerate unknown values
sentiment, session_status, resolution, and the observation block type are open unions. New values may be added over time. Tolerate a value you do not recognize rather than hard-failing on it. spend_cap_reached is a live example: it joined session_status after the first statuses shipped. The 11 sentiments today are Delighted, Excited, Confident, Satisfied, Relieved, Neutral, Confused, Uncertain, Anxious, Frustrated, and Disengaged, and felt_intensity is one of mild, moderate, or strong.
The reference loop
The whole contract in one place, in any language:Related
Author your environment
Declare actions, write labels, size frames.
Score outcomes
Judge success from your state, not the participant’s words.