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.
ws://127.0.0.1:8787$ npm install @whitesands/agent-playerGET /{"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
typestringRequiredhello.platformKeystringRequiredA key currently loaded by this process.
protocolVersionintegerRequiredMust be
1.clientIdstringAccepted from the SDK. Not stored.
sdkVersionstringAccepted from the SDK. Not stored.
hello_err codes
AUTHplatformKeyis missing or not in the store loaded at startup.PROTOCOLThe first message is not
hello, the JSON is invalid, orprotocolVersionis not1. A missing version is reported asnone.
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.
platformKeystringOverrides
WSI_PLATFORM_KEY.platformUrlstringOverrides
WSI_PLATFORM_URL.
helloimport { AgentServer } from "@whitesands/agent-player";
const agents = new AgentServer({
platformKey: "pk_4f2c1b8e0a7d9c3e5b1a6f8d2c4e0a9b7d1f3c5e8a2b4d6f",
platformUrl: "ws://127.0.0.1:8787",
});hello_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_errAuthentication failed.
codeisAUTHorPROTOCOL. The socket is closed.errorThe message was rejected, or a session failed during startup.
messagedescribes it.sessionIdis set when the failure belongs to a session.resume_errThe session cannot be resumed.
turn_resultWith
ok: false, the turn failed or the loop stopped.erroris the reason.
Node SDK
The Node SDK raises typed errors.
PlatformAuthErrorThe platform rejected the key.
ProtocolVersionErrorThis SDK and the platform disagree on the protocol.
TurnFailedErrorThe action and the retry both failed.
error.attemptslists both calls.TurnTimeoutErrorThe platform did not finish the turn within
timeoutMs.ConnectionLostErrorThe socket dropped and the session could not be resumed.
import { TurnFailedError } from "@whitesands/agent-player";
try {
await player.turn();
} catch (error) {
if (error instanceof TurnFailedError) {
console.log(error.attempts);
}
throw error;
}hello_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
typestringRequiredstart_session.sessionIdstringRequiredChosen by the client. Unique among live sessions on this process.
sessionTokenstringRequiredSecret the client must present to
resume_session.instructionsstringSystem prompt for the model.
goalstringAppended 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.
namestringLabel stored with the session. Defaults to the class name.
goalstringAdded to the model prompt for this player only.
instructionsstringAppended to the default agent prompt.
start_sessionconst player = await agents.start(new Pawn(), {
name: "Pawn",
goal: "Walk forward",
});session_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
typestringRequiredstop_session.sessionIdstringRequiredThe 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_sessionawait player.stop();
await agents.close();session_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
typestringRequiredresume_session.sessionIdstringRequiredThe session to resume.
sessionTokenstringRequiredThe 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_sessionagents.on((event) => {
if (event.type === "disconnected") console.warn("platform disconnected");
if (event.type === "reconnected") console.info("platform reconnected");
});session_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.
- Call
wait. The result is the view. - If
waitreturnsisError: true, sendturn_resultwithok: falseand callwaitagain. The model is not called. - Ask the model for exactly one tool call.
waitandseeare state tools, so they are left off the tool list. A model that names one of them has not played an action. - Send
session_eventwithevent: "action", then call the tool on the game. - If the call returns
isError: true, or the choice waswaitorsee, sendsession_eventwithevent: "retry"and ask the model once more. A second failure sendsevent: "turnFailed". - 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.
timeoutMsintegerDefaults to 60 seconds, or
turnTimeoutMsonAgentServer. Rejects withTurnTimeoutErrorwhen exceeded.signalAbortSignalAborts the wait for the turn result.
tools/call waitconst result = await player.turn({ timeoutMs: 30_000 });turn_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
typestringturn_result.sessionIdstringThe session that played the turn.
okbooleantruewhen the action that stands is a successful tool call.actionstringTool that succeeded. Omitted when
okisfalse.argsobjectArguments of that call.
resultanyParsed JSON text from the tool, or the raw text when it is not JSON.
errorstringPresent when
okisfalse. For a tool error, this is the result'serrorfield when the body is a JSON object, otherwise the tool text.attemptsinteger1when the first call succeeded.2when a retry tool call was made, including a retry that succeeded.1when the retry itself waswaitorsee.0whenwaitreturned an error, or when the loop stopped on a model or transport error.attemptDetailsarrayFailed attempts that were recorded, each
{action, args, error}. Present when the turn fails after a rejected action.usageobjectpromptTokensandcompletionTokenssummed 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.
{
"type": "turn_result",
"sessionId": "s1",
"ok": true,
"action": "move",
"args": { "dx": 2 },
"result": { "x": 2 },
"attempts": 1,
"usage": { "promptTokens": 10, "completionTokens": 3 }
}{
"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
typestringsession_event.sessionIdstringThe session playing the turn.
eventenumaction,retry, orturnFailed.actionstringThe tool. Sent with
actionandretry, and withturnFailedwhen a second tool was called.argsobjectArguments of that tool call.
errorstringSent with
retryandturnFailed.
Events
actionThe platform is about to call this tool.
retryThe action failed, or the model returned
waitorsee. One retry follows.turnFailedThe retry call failed, or the retry was
waitorsee.
Node SDK
player.on hears one session. agents.on hears every session. Both return an unsubscribe function.
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);
});{
"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
seeReturns the current view for this player. Left off the model's tool list.
waitBlocks 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: trueis 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.
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 };
}
}wait{
"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
typestringRequiredmcp.sessionIdstringRequiredThe session the call belongs to.
messageobjectRequiredA 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.
tools/call movemove(dx: number) {
this.x += dx;
return { x: this.x };
}mcp{
"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
typestringRequiredping.idstringRequiredEchoed back in
pong.
Returns
Returns pong with the same id.
Node SDK
The Node SDK sends ping on a timer.
pingThe Node SDK sends ping on its own.
pong{"type":"pong","id":"1"}