Skip to content

Agent Training Sandbox

A private, valueless practice table that lives inside the agent builder. You open one to practice with — and refine — your agent against house bots. There is one kind of training table: you hold your own seat, your agent rides alongside, and two live toggles decide how it behaves — autonomy (you decide ↔ your agent decides) and card visibility (your hand only ↔ everyone's cards). Nothing of value can be won: a training table transacts a third economy, money_kind = 'sandbox', that never touches a bank, cage, custody, or settlement path.

Status: v0. The two-mode create (play-with-it → copilot / watch-it-play → agent) was collapsed into one copilot kind + two runtime toggles — see .indusk/planning/sandbox-one-game/adr.md.

The weightless guarantee

A sandbox table is structurally incapable of touching value — not because the UI hides money buttons, but because there is no ledger to credit into:

  • money_kind = 'sandbox' is a third value on the economy axis (alongside play and real). Every seat — human or bot — is credited its starting stack directly from table config, with no wallet, permit, deposit, or bank call.
  • The cage (solana-bank) keeps its own MoneyKind of just play | real, so sandbox is unnameable in fund-path code by construction — a sandbox table can never route to a bank.
  • Boundary persistence (chip checkpoints, hand history, decision observations) still runs — those are the training data — but every money leg (deposit / withdraw / settle / escape / custody / conservation) is short-circuited.

One table, two toggles

There is one kind of training table — copilot: you hold your own seat, your agent bound as advisor. How it plays is two independent switches you flip live on the felt:

ToggleOffOn
Autonomy (◉ full agentic / ○ advised)you decide each turn; your agent advises, you accept or overridefull agentic — your agent auto-submits its own recommendation (the existing per-seat autopilot), and you watch
Visibility (◉ reveal / ○ sweat)sweat view — you see only your own hand (and the board)reveal — you also see opponents' hole cards, to study the hand

The old "watch it play" is simply full agentic ON; the old "face-up watch" is reveal ON. Both run your draft agent version, so the sandbox doubles as a pre-publish test bench.

THE INVARIANT — reveal is a lens on your screen, never the agent's

Card visibility never changes what any agent sees. Every agent at the table — yours and the opponents' — always plays a fair game on public information only. "Reveal" only lets you, the owner, peek at hole cards you wouldn't normally see; it does not feed those cards to any agent's decision. This matters mechanically because your human connection and your advisor agent's connection shadow the same seat — they share a viewer id. The reveal is therefore authorized on the connection, not the seat: the server sets it true only on your human broadcast frame and false on the agent's decision frame (which shares your seat id). A reveal keyed on the seat would feed opponents' cards straight into the agent's prompt — the exact information-boundary leak the reveal policy exists to prevent. So an agent you train here behaves identically whether or not you have reveal on.

The felt

The table is the identical real felt (BratPokerTable), rendered scaled-down inside the lab — the game visibly exists within your agent's brain — with a single subtle sandbox tell. A zoom affordance pulls it out to near-fullscreen framed by a glowing screen-edge border.

Lifecycle

A training table concludes cleanly and frees your slot — and it never reaches a bank on any path.

  • Deals only while you're present. A sandbox table deals a hand ONLY while its owner's live seat connection is present (you always hold a seat). When you leave, dealing pauses (the bots do not keep playing — this bounds house-bot inference cost); when you return, dealing resumes automatically. A paused table is a normal, labeled state, not a wedge.
  • Deals heads-up. A training table's roster is exactly who you seated, so it deals as soon as you + one bot are ready (no lobby "wait for more players" window, and no 3-player opening minimum a real 6-max table would apply).
  • End session. From the table you can end the training table outright — it concludes as closed('sandbox_ended') through the single table-state writer, no money legs. Only the owner can end their own table.
  • Abandoned tables self-dispose. Leave a training table idle and the idle reaper closes it to nothingclosed('sandbox_idle'), never a bank escape, freeze, or custody move — after a short threshold (SANDBOX_IDLE_CLOSE_SECONDS, default 30 min; the sandboxIdleCloseSeconds game-setting overrides it per environment). This is what prevents a forgotten table from locking you out.
  • One live table per user. You can only have one live training table at a time — a second create (or sit) is refused server-side (sandbox_table_already_live). End or abandon the current one and a new create immediately succeeds.

How "never touches a bank" is enforced (and tested)

The money legs are removed at every table-reachable site, each guarded by the table row'smoney_kind, never a request:

PathOn a sandbox table
Hand-end conservation / custody reconcileno-op — never reads a pool, never freezes (a poolless real table would freeze on hand 1)
DB-side conservation alarm (boundary + boot)skipped — the cash identity is meaningless without a deposit ledger
Position-chain eventsnone — the chain is deposit-anchored; no deposit, no chain
Forced withdraw (disposition sweep / disconnect)local seat release, no cage call
Idle reaperclose-to-nothing (sandbox_idle), never escape
Frozen-table reaperexcluded (a sandbox table cannot even reach frozen)

Integration tests pin this: a played-and-ended training table writes zero rows across deposit_intents / withdrawal_intents / payout_intents / custody_entries and zero position-chain events, a hand plays to showdown without freezing and the next hand deals, and an abandoned table auto-closes with zero bank/escape/custody action.

Filling seats — house bots

You hand-fill the other seats with "+ bot." Each click seats a random idle member of the persistent house fleet (auth_provider='house', published) that is not already at your table — cageless (no deposit): the bot's seat is credited its starting stack the moment its sit_down lands, through the same sandbox credit branch your own seat uses. There is no bank involvement — add-bot is a game-server route (POST /sandbox/tables/:id/add-bot, owner-only) that never calls a cage.

  • Add again seats a different bot (the fleet query excludes agents already at your table).
  • Remove frees a seat outright — the bot's connection closes and its seat re-opens, with no withdrawal (there is nothing of value to settle).

The bots are real hosted agents taking real LLM turns, so a sandbox turn writes an agent_inference_charges row exactly like any table. That inference is a real cost, but it is bounded: a sandbox table deals only while you are present, and you can have only one live at a time.

The refinement loop

  1. Sit — click an empty seat. You're credited the stack instantly (cageless), and your agent binds to your seat as advisor.
  2. Add bots to fill the table.
  3. Play, or hand it over. With autonomy off (advised) your agent posts an advisory each turn and you accept or override — every override lands in decision_observations as labeled divergence data (the refinement dataset, viewable in the Decisions tab). Flip full agentic on and your agent submits its own decisions while you watch.
  4. Study the hand. Flip reveal on to see opponents' cards — a lens on your screen only; it never changes what your agent (or any opponent) is playing against.
  5. Refine → publish. Your draft version runs as the advisor here, so the sandbox is a pre-publish test bench — admitted by the one relaxed version gate, sandbox tables only.

Reveal is a per-owner view overlay on top of this loop — it changes only what you are shown, never the decisions any agent makes.

Opening one — the builder walkthrough

The training sandbox lives inside the agent lab, at /agents, under the Train tab. It is the only entry point (training tables never appear in the main lobby).

  1. Train tab → create. With no live table you see the create panel: a single open training table button plus a minimal config — seats, blinds, and the starting stack every seat is credited. No mode picker (there's one kind of table); no wallet, permit, or deposit — the table opens on the real felt, embedded in the lab.
  2. The felt is embedded, "inside the brain." The real BratPokerTable renders scaled into a brat-framed container. ⤢ zoom pulls it out to near-fullscreen with a glowing screen-edge border; ⤡ shrink brings it back. Same component both ways — the game keeps running through the zoom.
  3. Sit + fill seats. Click an empty seat to sit (cageless — you're credited the stack instantly), then + bot to seat house bots that actually play. Remove a bot from the bots: strip.
  4. Flip the toggles (once seated).
    • ◉ full agentic / ○ advised — hand your seat to your agent, or play it yourself with advice. Each override you make is captured as refinement data (visible in the Decisions tab).
    • ◉ reveal / ○ sweat — see everyone's cards, or just your own. Reveal is your view only.
  5. End when you're done. ✕ end concludes the table (confirm first) and frees your one-live slot. An abandoned table closes itself (see Lifecycle) — no lockout.

One live table per user. While a training table is live the create affordance is absent (not disabled) — the felt takes over. A direct server attempt to open a second one is refused with the coded sandbox_table_already_live.

No money surface, ever. No deposit / withdraw / cash-out affordance renders on a training table — money is unreachable, not merely hidden. Standing up (a seat verb) just leaves the seat; there is nothing to return.

The sandbox tell

The felt is byte-identical to a real table save one subtle accent, so you always know a table is valueless at a glance without it reading as a different product: on a moneyKind='sandbox' table the card drop-shadow turns lime, the dealer's eyes turn blue, and the dealer's chip tower turns pink (data-sandbox="true" on the felt root — the marker the accent hangs off). Everything else is the same BratPokerTable.