Skip to content
White SandsInteractive

API reference

The White Sands platform runs the AI player for your game. Your game process opens one WebSocket, authenticates with a platform key, and serves its player methods as MCP tools. The platform holds the model and the model API key, plays one action per turn, and reports what happened.

Messages are JSON text frames. A single frame can be at most 4 MiB. Upgrades are accepted on any path and from any origin. This reference describes protocol version 1.

Games connect with the Node SDK, @whitesands/agent-player, the Python SDK, agent-player, or the C# SDK, WhiteSands.AgentPlayer. Choose a language on any code sample to see the SDK call for that message.

Default URL
ws://127.0.0.1:8787
Client libraries
$ npm install @whitesands/agent-player
Health checkGET /
{"ok":true,"service":"agent-platform"}

Authentication

hello

Every connection authenticates with a platform key. The key is a secret that identifies a game account. Send it in hello, the first message on the socket. It must arrive within 10 seconds, or the socket is closed.

The model API key is a different secret. It is set on the platform process. Game processes send only the platform key.

Parameters

  • typestringRequired

    hello.

  • platformKeystringRequired

    A key currently loaded by this process.

  • protocolVersionintegerRequired

    Must be 1.

  • clientIdstring

    Accepted from the SDK. Not stored.

  • sdkVersionstring

    Accepted from the SDK. Not stored.

hello_err codes

  • AUTH

    platformKey is missing or not in the store loaded at startup.

  • PROTOCOL

    The first message is not hello, the JSON is invalid, or protocolVersion is not 1. A missing version is reported as none.

Returns

Returns hello_ok with the account name and the protocol version. The socket stays open.

Any other first message returns hello_err, and the server closes the socket. A second hello on an open socket returns error with already authenticated.

Node SDK

AgentServer reads the key from WSI_PLATFORM_KEY and the URL from WSI_PLATFORM_URL. The URL defaults to ws://127.0.0.1:8787.

The SDK sends hello on the first start(). hello_err rejects that call with PlatformAuthError or ProtocolVersionError.

  • platformKeystring

    Overrides WSI_PLATFORM_KEY.

  • platformUrlstring

    Overrides WSI_PLATFORM_URL.

Authenticatehello
import { AgentServer } from "@whitesands/agent-player";

const agents = new AgentServer({
  platformKey: "pk_4f2c1b8e0a7d9c3e5b1a6f8d2c4e0a9b7d1f3c5e8a2b4d6f",
  platformUrl: "ws://127.0.0.1:8787",
});
Responsehello_ok
{"type":"hello_ok","accountName":"demo","protocolVersion":1}

Errors

Failed requests return one of these messages. Unknown message types and invalid JSON return error. Message types the server sends, such as hello_ok and turn_result, are ignored if a client sends them.

Error messages

  • hello_err

    Authentication failed. code is AUTH or PROTOCOL. The socket is closed.

  • error

    The message was rejected, or a session failed during startup. message describes it. sessionId is set when the failure belongs to a session.

  • resume_err

    The session cannot be resumed.

  • turn_result

    With ok: false, the turn failed or the loop stopped. error is the reason.

Node SDK

The Node SDK raises typed errors.

  • PlatformAuthError

    The platform rejected the key.

  • ProtocolVersionError

    This SDK and the platform disagree on the protocol.

  • TurnFailedError

    The action and the retry both failed. error.attempts lists both calls.

  • TurnTimeoutError

    The platform did not finish the turn within timeoutMs.

  • ConnectionLostError

    The socket dropped and the session could not be resumed.

Handle errors
import { TurnFailedError } from "@whitesands/agent-player";

try {
  await player.turn();
} catch (error) {
  if (error instanceof TurnFailedError) {
    console.log(error.attempts);
  }
  throw error;
}
Responsehello_err
{"type":"hello_err","code":"PROTOCOL","message":"Unsupported protocol version 2; this platform speaks version 1. Upgrade @whitesands/agent-player."}

Start a session

start_session

A session is one AI player bound to one platform key. start_session is accepted only after hello_ok. One connection can own several sessions.

The platform sends MCP initialize and notifications/initialized to the game. When that handshake succeeds, it replies session_started. The platform reads the player's tools from tools/list.

Parameters

  • typestringRequired

    start_session.

  • sessionIdstringRequired

    Chosen by the client. Unique among live sessions on this process.

  • sessionTokenstringRequired

    Secret the client must present to resume_session.

  • instructionsstring

    System prompt for the model.

  • goalstring

    Appended to the system prompt as Your goal: ....

Returns

Returns session_started with the session id.

A duplicate id returns error with Session already exists. A missing sessionId or sessionToken returns error. If initialize fails, the platform sends error with the session id and drops the session.

Node SDK

agents.start(player, options) turns the player into tools, picks a random sessionId and sessionToken, and sends start_session. It resolves with a handle after session_started.

  • namestring

    Label stored with the session. Defaults to the class name.

  • goalstring

    Added to the model prompt for this player only.

  • instructionsstring

    Appended to the default agent prompt.

Start a sessionstart_session
const player = await agents.start(new Pawn(), {
  name: "Pawn",
  goal: "Walk forward",
});
Responsesession_started
{"type":"session_started","sessionId":"s1"}

Stop a session

stop_session

Ends a session. stop_session is accepted from any connection that authenticated with the same platform key.

Parameters

  • typestringRequired

    stop_session.

  • sessionIdstringRequired

    The session to stop.

Returns

Every stop_session is answered with session_stopped. The loop is cancelled only when this platform key owns that session.

Node SDK

player.stop() ends one session. agents.close() stops every session on that server. The platform process keeps running.

Stop a sessionstop_session
await player.stop();
await agents.close();
Responsesession_stopped
{"type":"session_stopped","sessionId":"s1"}

Resume a session

resume_session

When the socket drops, every session attached to it stays alive for 30 seconds. Messages the loop produces in that window are queued.

A new socket must send hello with the same platform key, then resume_session.

Parameters

  • typestringRequired

    resume_session.

  • sessionIdstringRequired

    The session to resume.

  • sessionTokenstringRequired

    The token sent in start_session.

Returns

Queued messages are written first, then session_resumed.

A wrong token, a different platform key, an unknown id, or a session whose grace period has elapsed returns resume_err. A rejected resume leaves the session as it was.

Node SDK

The SDK reconnects with backoff and resumes every session on its own. agents.on reports disconnected and reconnected. When a session cannot be resumed, turn() rejects with ConnectionLostError.

Resume a sessionresume_session
agents.on((event) => {
  if (event.type === "disconnected") console.warn("platform disconnected");
  if (event.type === "reconnected") console.info("platform reconnected");
});
Responsesession_resumed
{"type":"session_resumed","sessionId":"s1"}

Grant a turn

wait

After session_started, the platform calls the game's wait tool and blocks. wait has no platform-side timeout. The game grants a turn by letting wait return. Its text is the view. JSON text is parsed, and other text stays a string.

  1. Call wait. The result is the view.
  2. If wait returns isError: true, send turn_result with ok: false and call wait again. The model is not called.
  3. Ask the model for exactly one tool call. wait and see are state tools, so they are left off the tool list. A model that names one of them has not played an action.
  4. Send session_event with event: "action", then call the tool on the game.
  5. If the call returns isError: true, or the choice was wait or see, send session_event with event: "retry" and ask the model once more. A second failure sends event: "turnFailed".
  6. Send one turn_result.

Returns

Every turn ends with one turn_result.

If listing tools, a tool call, or the model request fails, the loop stops and sends turn_result with ok: false. The session stays allocated until stop_session, shutdown, or 30 seconds after its socket drops, and it plays no further turns.

Node SDK

player.turn() unblocks that session's wait. It does not choose the action. It resolves with the turn_result as action, args, result, attempts, and usage, or rejects with TurnFailedError after the second failure.

  • timeoutMsinteger

    Defaults to 60 seconds, or turnTimeoutMs on AgentServer. Rejects with TurnTimeoutError when exceeded.

  • signalAbortSignal

    Aborts the wait for the turn result.

Grant a turntools/call wait
const result = await player.turn({ timeoutMs: 30_000 });
Responseturn_result
{
  "type": "turn_result",
  "sessionId": "s1",
  "ok": true,
  "action": "move",
  "args": { "dx": 2 },
  "result": { "x": 2 },
  "attempts": 1,
  "usage": { "promptTokens": 10, "completionTokens": 3 }
}

The turn_result object

The platform sends one turn_result at the end of every turn.

Attributes

  • typestring

    turn_result.

  • sessionIdstring

    The session that played the turn.

  • okboolean

    true when the action that stands is a successful tool call.

  • actionstring

    Tool that succeeded. Omitted when ok is false.

  • argsobject

    Arguments of that call.

  • resultany

    Parsed JSON text from the tool, or the raw text when it is not JSON.

  • errorstring

    Present when ok is false. For a tool error, this is the result's error field when the body is a JSON object, otherwise the tool text.

  • attemptsinteger

    1 when the first call succeeded. 2 when a retry tool call was made, including a retry that succeeded. 1 when the retry itself was wait or see. 0 when wait returned an error, or when the loop stopped on a model or transport error.

  • attemptDetailsarray

    Failed attempts that were recorded, each {action, args, error}. Present when the turn fails after a rejected action.

  • usageobject

    promptTokens and completionTokens summed across the model calls that finished in this turn. Omitted when the loop ends on a later transport or model error.

Node SDK

turn() resolves with action, args, result, attempts, and usage. When ok is false, it rejects with TurnFailedError, and error.attempts holds attemptDetails.

The turn_result object
{
  "type": "turn_result",
  "sessionId": "s1",
  "ok": true,
  "action": "move",
  "args": { "dx": 2 },
  "result": { "x": 2 },
  "attempts": 1,
  "usage": { "promptTokens": 10, "completionTokens": 3 }
}
Failed turn
{
  "type": "turn_result",
  "sessionId": "s1",
  "ok": false,
  "error": "cannot move backwards",
  "attempts": 2,
  "attemptDetails": [
    { "action": "move", "args": { "dx": -5 }, "error": "cannot move backwards" },
    { "action": "move", "args": { "dx": -5 }, "error": "cannot move backwards" }
  ],
  "usage": { "promptTokens": 20, "completionTokens": 6 }
}

The session_event object

The platform sends session_event while a turn is in progress.

Attributes

  • typestring

    session_event.

  • sessionIdstring

    The session playing the turn.

  • eventenum

    action, retry, or turnFailed.

  • actionstring

    The tool. Sent with action and retry, and with turnFailed when a second tool was called.

  • argsobject

    Arguments of that tool call.

  • errorstring

    Sent with retry and turnFailed.

Events

  • action

    The platform is about to call this tool.

  • retry

    The action failed, or the model returned wait or see. One retry follows.

  • turnFailed

    The retry call failed, or the retry was wait or see.

Node SDK

player.on hears one session. agents.on hears every session. Both return an unsubscribe function.

Listen
const unsubscribe = player.on((event) => {
  if (event.type === "action") console.log(event.action, event.args);
  if (event.type === "retry") console.log(event.action, event.error);
  if (event.type === "turnFailed") console.log(event.error);
});
The session_event object
{
  "type": "session_event",
  "sessionId": "s1",
  "event": "retry",
  "action": "move",
  "args": { "dx": -5 },
  "error": "cannot move backwards"
}

The player tools

The game serves its player as MCP tools over the session. Two are state tools. The rest are actions the model may call.

Tools

  • see

    Returns the current view for this player. Left off the model's tool list.

  • wait

    Blocks until the game grants a turn, then returns the current view. Left off the model's tool list.

  • <action>

    Any other tool. A result with isError: true is a failed action.

Node SDK

The SDK turns a player class into these tools. see() is required. Every other public method is an action. Names that start with _, and the constructor, are left off. Throw to refuse an action.

static agent adds the description the model sees and checks arguments with Zod 3 before the method runs. A schema failure counts as a failed action and uses the one retry.

Each start() records that player's calls in .wsi/agent-memory.sqlite. The next view includes done, up to the last 50 calls. memoryPath: false turns it off.

Player class
import { z } from "zod";

class Pawn {
  static agent = {
    description: "A pawn on a line",
    actions: {
      move: {
        description: "Step by dx",
        args: { dx: z.number().int() },
      },
    },
  };

  x = 0;

  see() {
    return { x: this.x };
  }

  move(dx: number) {
    if (dx < 0) throw new Error("cannot move backwards");
    this.x += dx;
    return { x: this.x };
  }
}
Viewwait
{
  "x": 2,
  "done": [
    { "action": "move", "args": { "dx": 2 }, "ok": true }
  ]
}

Call a tool

mcp

Tool calls travel as JSON-RPC 2.0 inside mcp messages. The platform sends requests, and the game answers with the same sessionId.

The platform calls initialize, notifications/initialized, tools/list, and tools/call. Request ids are JSON numbers. Echo the same id in the result. A tool result's text blocks are joined with newlines. isError: true marks the call failed.

If the game sends a request, the platform answers ping with an empty result and anything else with JSON-RPC -32601. Notifications from the game are ignored. An mcp message for a session this key does not own is discarded.

Parameters

  • typestringRequired

    mcp.

  • sessionIdstringRequired

    The session the call belongs to.

  • messageobjectRequired

    A JSON-RPC 2.0 request or result.

Returns

The game returns an mcp message with the JSON-RPC result for the same id.

Node SDK

The SDK runs the MCP server for each session. It answers tools/call by calling the player method and returning what it returns.

Call a tooltools/call move
move(dx: number) {
  this.x += dx;
  return { x: this.x };
}
Responsemcp
{
  "type": "mcp",
  "sessionId": "s1",
  "message": {
    "jsonrpc": "2.0",
    "id": 2,
    "result": {
      "isError": false,
      "content": [{ "type": "text", "text": "{\"x\":2}" }]
    }
  }
}

Keep the socket alive

ping

The platform closes a socket that receives nothing for 45 seconds. Send ping to keep it open.

Shutdown on SIGINT or SIGTERM ends every session within 10 seconds.

Parameters

  • typestringRequired

    ping.

  • idstringRequired

    Echoed back in pong.

Returns

Returns pong with the same id.

Node SDK

The Node SDK sends ping on a timer.

Pingping

The Node SDK sends ping on its own.

Responsepong
{"type":"pong","id":"1"}