agent-player 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,346 @@
1
+ Metadata-Version: 2.4
2
+ Name: agent-player
3
+ Version: 1.0.0
4
+ Summary: Start a remote game agent from a player class. The SDK owns the MCP server; the model stays on the platform.
5
+ Author: White Sands Interactive
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 White Sands Interactive
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://github.com/White-Sands-Interactive/WSI-Python
29
+ Project-URL: Repository, https://github.com/White-Sands-Interactive/WSI-Python
30
+ Project-URL: Issues, https://github.com/White-Sands-Interactive/WSI-Python/issues
31
+ Keywords: whitesands,agent,mcp,game
32
+ Requires-Python: >=3.11
33
+ Description-Content-Type: text/markdown
34
+ License-File: LICENSE
35
+ Requires-Dist: websockets>=13
36
+ Provides-Extra: dev
37
+ Requires-Dist: pytest>=8; extra == "dev"
38
+ Dynamic: license-file
39
+
40
+ # WSI Agent Player for Python
41
+
42
+ The library starts a remote game agent from a player class. The game calls `start` and `turn`. The SDK publishes that class as tools. The model runs on the [platform](https://github.com/White-Sands-Interactive/Platform), so the game process never holds the model key.
43
+
44
+ This is the Python port of [`@whitesands/agent-player`](https://github.com/White-Sands-Interactive/WSI-Node) and `WhiteSands.AgentPlayer`. All three speak protocol version 1.
45
+
46
+ The library also provides:
47
+
48
+ - A player class turned into tools, with optional argument schemas.
49
+ - One action per turn, and one retry when that action throws.
50
+ - An offline loop for tests, with no socket and no platform key.
51
+ - A local SQLite log of what each character has already called.
52
+
53
+ ## Installation
54
+
55
+ ```sh
56
+ pip install agent-player
57
+ ```
58
+
59
+ Python 3.11 or newer. The action log uses the standard-library `sqlite3` module. A connection to the platform uses `websockets`, which this package depends on. An offline `inline` platform does not open a socket.
60
+
61
+ ## Usage
62
+
63
+ Set the platform key, then start one agent per character. `turn()` grants that character one chance to act and returns what they did.
64
+
65
+ ```sh
66
+ export WSI_PLATFORM_KEY=pk_...
67
+ export WSI_PLATFORM_URL=ws://127.0.0.1:8787
68
+ ```
69
+
70
+ `WSI_PLATFORM_URL` defaults to `ws://127.0.0.1:8787` when it is unset.
71
+
72
+ ```python
73
+ from whitesands.agent_player import AgentServer
74
+
75
+ agents = AgentServer()
76
+ ada = await agents.start(Villager(world, "Ada"), name="Ada", goal="Stand on the tree and chop it")
77
+
78
+ result = await ada.turn()
79
+ # result.action, result.args, result.result, result.attempts, result.usage
80
+ ```
81
+
82
+ `AgentServer(platform_key="...", platform_url="...")` overrides the environment variables.
83
+
84
+ ### The player class
85
+
86
+ `see` is required. It returns the JSON the model reads at the start of the turn. Every other public method is an action the model may call. Names that start with `_`, and `wait` (the SDK provides that tool), are left off the tool list.
87
+
88
+ Throw to refuse an action. The platform asks the model once more. A second failure raises `TurnFailedError` from `turn()`.
89
+
90
+ ```python
91
+ class Villager:
92
+ def __init__(self, world, name):
93
+ self.world = world
94
+ self.name = name
95
+ self.x = 0
96
+ self.y = 0
97
+ self.wood = 0
98
+
99
+ def see(self):
100
+ return {"x": self.x, "y": self.y, "wood": self.wood, "tile": self.world.get(self.x, self.y)}
101
+
102
+ def move(self, dx, dy):
103
+ next_x = self.x + dx
104
+ next_y = self.y + dy
105
+ if not self.world.in_bounds(next_x, next_y):
106
+ raise RuntimeError(f"Cannot move to {next_x},{next_y}")
107
+ self.x = next_x
108
+ self.y = next_y
109
+ return {"x": self.x, "y": self.y}
110
+
111
+ def chop(self):
112
+ if self.world.get(self.x, self.y) != "T":
113
+ raise RuntimeError(f"No tree at {self.x},{self.y}")
114
+ self.wood += 1
115
+ return {"wood": self.wood}
116
+ ```
117
+
118
+ A method with no `agent` entry is still a tool. Its arguments accept any JSON value. `async def` methods are awaited. A method that returns `None` is reported as `{ "ok": true }`.
119
+
120
+ ### Declaring arguments
121
+
122
+ `agent` on the class adds the description the model sees and checks arguments before your method runs. A schema failure counts as a failed action and uses the one retry.
123
+
124
+ ```python
125
+ class Villager:
126
+ agent = {
127
+ "description": "A villager who gathers wood",
128
+ "actions": {
129
+ "move": {
130
+ "description": "Step by dx and dy. Stay inside the map.",
131
+ "args": {"dx": int, "dy": int},
132
+ },
133
+ "chop": {"description": "Chop the tree on your tile"},
134
+ },
135
+ }
136
+
137
+ def see(self):
138
+ return {"x": self.x, "y": self.y}
139
+ ```
140
+
141
+ | Annotation | JSON Schema | Accepted values |
142
+ | --- | --- | --- |
143
+ | `int` | integer | integers. `True` and `False` are rejected |
144
+ | `float` | number | integers and floats |
145
+ | `str` | string | strings |
146
+ | `bool` | boolean | `True` and `False` |
147
+ | `list` or `list[int]` | array | lists, and each item when the item type is set |
148
+ | `dict` | object | dicts |
149
+ | `Literal["wolf", "goat"]` | string enum | one of those values |
150
+ | `int \| None` | integer, not required | omitted, `None`, or an integer |
151
+
152
+ Argument names must match the method parameters. Keys in `args` are the names passed into the method. A missing required argument fails before the method runs. Extra keys are ignored.
153
+
154
+ ### Turns
155
+
156
+ `turn()` does not choose the action. It unblocks that session's `wait`. The platform reads `see()`, calls one action, and reports back.
157
+
158
+ ```python
159
+ result = await ada.turn(timeout_ms=30_000, signal=signal)
160
+ ```
161
+
162
+ | Field | Meaning |
163
+ | --- | --- |
164
+ | `action` | Tool the model called |
165
+ | `args` | Arguments it passed |
166
+ | `result` | What your method returned, after a JSON round trip |
167
+ | `attempts` | `1`, or `2` when the first call failed and the retry succeeded |
168
+ | `usage` | `prompt_tokens` and `completion_tokens` for that turn |
169
+
170
+ The default timeout is 60 seconds. Pass `signal` to abort the wait. `AbortSignal` is the built-in signal: call `signal.abort()` and `turn()` raises. Any object with `aborted`, `reason`, and `add_abort_listener` works the same way.
171
+
172
+ `handle.stop()` ends that session. `agents.close()`, also run by `async with`, stops every session on the server. The platform process keeps running.
173
+
174
+ ```python
175
+ async with AgentServer() as agents:
176
+ ada = await agents.start(villager, name="Ada")
177
+ await ada.turn()
178
+ ```
179
+
180
+ ### Errors
181
+
182
+ | Error | When |
183
+ | --- | --- |
184
+ | `PlatformAuthError` | The platform rejected the key |
185
+ | `ProtocolVersionError` | This SDK and the platform disagree on the protocol |
186
+ | `TurnFailedError` | The action and the retry both failed. `error.attempts` lists both calls |
187
+ | `TurnTimeoutError` | The platform did not finish within `timeout_ms` |
188
+ | `ConnectionLostError` | The socket dropped and the session could not be resumed |
189
+
190
+ The SDK reconnects with backoff. A disconnected session can be resumed for 30 seconds.
191
+
192
+ ```python
193
+ from whitesands.agent_player import TurnFailedError
194
+
195
+ try:
196
+ await ada.turn()
197
+ except TurnFailedError as error:
198
+ print(error.attempts)
199
+ raise
200
+ ```
201
+
202
+ ### Several characters
203
+
204
+ One `AgentServer` hosts every character. Each `start()` is its own session.
205
+
206
+ ```python
207
+ ada = await agents.start(Villager(world, "Ada"), name="Ada", goal="Chop the tree")
208
+ bea = await agents.start(Villager(world, "Bea"), name="Bea", goal="Carry wood home")
209
+
210
+ await ada.turn()
211
+ await bea.turn()
212
+ ```
213
+
214
+ The game decides whose turn it is by calling `turn()` on that handle.
215
+
216
+ ### Events
217
+
218
+ `agents.on` hears every session. `handle.on` hears one session. Both return an unsubscribe function.
219
+
220
+ ```python
221
+ stop = ada.on(lambda event: print(event.type, event.action))
222
+ ```
223
+
224
+ | `event.type` | Fields |
225
+ | --- | --- |
226
+ | `action` | `session_id`, `action`, `args` |
227
+ | `retry` | `session_id`, `action`, `args`, `error` |
228
+ | `turnFailed` | `session_id`, `error`, and `action` / `args` when the retry was a real tool |
229
+ | `disconnected` | none. No `session_id` |
230
+ | `reconnected` | none. No `session_id` |
231
+
232
+ `disconnected` and `reconnected` are connection events. They are delivered to `agents.on` only.
233
+
234
+ ### Action history
235
+
236
+ Every `start()` records that character's calls in one SQLite file, `.wsi/agent-memory.sqlite` in the process's current directory. The directory is created on the first `start()`. Deleting it is safe. The next start creates it again.
237
+
238
+ The next `see()` view includes `done`, oldest first, up to the last 50 calls for that session:
239
+
240
+ ```json
241
+ {
242
+ "x": 1,
243
+ "y": 0,
244
+ "done": [
245
+ { "action": "move", "args": { "dx": 1, "dy": 0 }, "ok": true }
246
+ ]
247
+ }
248
+ ```
249
+
250
+ A refused call is `{ "ok": false, "error": "..." }`. A new `start()` has an empty `done` list. Older rows stay in the file. Two characters on one server share the file and see only their own rows. The name on each row is the `name` passed to `start()`, or the class name.
251
+
252
+ ```python
253
+ agents = AgentServer(memory_path="saves/agents.sqlite")
254
+ ```
255
+
256
+ `memory_path=False` turns the log off. The view then has no `done` field.
257
+
258
+ ### Goal and instructions
259
+
260
+ `goal` is added to the model prompt for that character. `instructions` on `start()` is appended to the default prompt. `instructions` on `AgentServer` replaces the default prompt for every character on that server.
261
+
262
+ ```python
263
+ await agents.start(ferry, name="Ferryman", goal="Get the wolf, the goat, and the cabbage onto the far bank.")
264
+ ```
265
+
266
+ ### Testing without a platform
267
+
268
+ `whitesands.agent_player.testing.create_test_platform` runs the same wait, act, and retry loop in memory. The script is the list of tool calls, in order. No key and no socket. Every session on that server consumes the script from one shared queue.
269
+
270
+ ```python
271
+ from whitesands.agent_player import AgentServer
272
+ from whitesands.agent_player.testing import create_test_platform
273
+
274
+ agents = AgentServer(
275
+ inline=create_test_platform(
276
+ [
277
+ {"name": "move", "arguments": {"dx": 1, "dy": 0}},
278
+ {"name": "chop", "arguments": {}},
279
+ ]
280
+ )
281
+ )
282
+ ```
283
+
284
+ When the script runs out, the stand-in model calls `wait`, which is not an action, so the turn fails.
285
+
286
+ ### Where each piece runs
287
+
288
+ | | Game process | Platform |
289
+ | --- | --- | --- |
290
+ | World and player methods | yes | |
291
+ | Tools (`see`, actions, `wait`) | yes | calls them |
292
+ | Whose turn it is | `turn()` | |
293
+ | Which action to take | | the model |
294
+ | Model API key | | yes |
295
+
296
+ ### Logging
297
+
298
+ Pass a `logger` to print SDK warnings. Omit it and nothing is written. `debug`, `info`, `warn`, and `error` are all optional. `warning` is accepted in place of `warn`.
299
+
300
+ ```python
301
+ class Log:
302
+ def warn(self, message, extra=None):
303
+ print(message)
304
+
305
+ def error(self, message, extra=None):
306
+ print(message)
307
+
308
+ agents = AgentServer(logger=Log())
309
+ ```
310
+
311
+ A standard-library logger works for `error`. `warning` is accepted in place of `warn`.
312
+
313
+ ### Protocol
314
+
315
+ `whitesands.agent_player.protocol` exports the wire constants and `PROTOCOL_VERSION`. The platform's copy is `internal/protocol` in [White-Sands-Interactive/Platform](https://github.com/White-Sands-Interactive/Platform). The Node SDK's copy is `src/protocol.ts`. The C# SDK's copy is `WhiteSands.AgentPlayer.Protocol`. A change to the wire format has to land in every place. Game code uses `AgentServer` and does not import the protocol.
316
+
317
+ Messages on the socket use the same camelCase fields as the other SDKs (`sessionId`, `promptTokens`, `inputSchema`). The Python API uses snake_case (`session_id`, `prompt_tokens`).
318
+
319
+ ## This repository
320
+
321
+ | Path | Role |
322
+ | --- | --- |
323
+ | `src/whitesands/agent_player` | The SDK |
324
+ | `examples/forest.py` | Two villagers, reflected methods |
325
+ | `examples/courier.py` | An integer argument and a goal |
326
+ | `examples/ferry.py` | One passenger chosen each turn |
327
+
328
+ ```sh
329
+ python -m venv .venv
330
+ source .venv/bin/activate
331
+ pip install -e ".[dev]"
332
+ pytest
333
+ python examples/forest.py
334
+ python examples/courier.py
335
+ python examples/ferry.py
336
+ ```
337
+
338
+ Each example replays a script. Add `--remote` to forest or ferry to use a platform that is already serving:
339
+
340
+ ```sh
341
+ ./platform create-key demo
342
+ MODEL_API_KEY=... MODEL_BASE_URL=... MODEL_NAME=... ./platform serve
343
+ PLATFORM_KEY=pk_... python examples/forest.py --remote
344
+ ```
345
+
346
+ `./platform` is the Go server from the Platform repository. Releases are recorded in [CHANGELOG.md](CHANGELOG.md).
@@ -0,0 +1,20 @@
1
+ agent_player-1.0.0.dist-info/licenses/LICENSE,sha256=08M09dtvK2Z85eR9Ua86KVDKX9LpKvnoutWl8DjQwsI,1080
2
+ whitesands/__init__.py,sha256=2xpj-D0jv_0NG6sOjFmMj55hZ9D3uiakZqZuBDVyCnM,41
3
+ whitesands/agent_player/__init__.py,sha256=ejkPV3voHcIK04g9B80FvCxsesGCfb6zF_3FAdMBtbA,891
4
+ whitesands/agent_player/agent_server.py,sha256=3Yx1XlkvDJIr7K9etwJ44OaKlIRZkqkOBqQqjbf_MAk,14967
5
+ whitesands/agent_player/errors.py,sha256=TF2TsZzTaq80B5vMVhUF3xC6L7H77OG3a-dmP021Vww,949
6
+ whitesands/agent_player/events.py,sha256=5s-_XIeqiZwBnb63m-_CC85eavPLYg80x02Sfxw643E,615
7
+ whitesands/agent_player/logger.py,sha256=AhthX_V1CoNY5cppzkQo92_TwCv_2GFQjtBEBDxys00,543
8
+ whitesands/agent_player/mcp.py,sha256=iFPQ2fZvCLQG5binVwIwE2iYWdp9T1WbVcJJCGSaviQ,16673
9
+ whitesands/agent_player/memory.py,sha256=y6kDC3VNbpML8fNtLJXQZWzgotGI4snkCschZIs0BGs,3356
10
+ whitesands/agent_player/platform_connection.py,sha256=OCyE2u5hISzaA2LUk3tuLlrnhhf1VIvwi2vrXkLuxc8,15269
11
+ whitesands/agent_player/protocol.py,sha256=NS6DKWe2-F5Ox1PnT3C0q0_xTYg_aKBfUl0OZTbSd-c,3364
12
+ whitesands/agent_player/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
+ whitesands/agent_player/reflect.py,sha256=XWBWBJDYqrn1gyEPGyRqSAN061b-FktiZNwDAsRWbBo,10845
14
+ whitesands/agent_player/session_loop.py,sha256=5mRaeQS-v_E3nMGUPiJn0fTVETzti_HLjRNbHgcB07o,11776
15
+ whitesands/agent_player/testing.py,sha256=aD-PgrN_2TqEA_bKN6o5St7IBJ8E5V2NPdxB2z8weN4,1995
16
+ whitesands/agent_player/turn_gate.py,sha256=7FvkCWqyxR-W7L-VCepmSJsM8vuSETl6_wpeuHyGURU,5025
17
+ agent_player-1.0.0.dist-info/METADATA,sha256=4oiu3GpBCnlBFzmJkge-gOthtJclanNoE7LQ2AKeNd8,13192
18
+ agent_player-1.0.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
19
+ agent_player-1.0.0.dist-info/top_level.txt,sha256=vcGfaDz4aQib-mMWjfZaqhOvVKlExFwf-z8i0Xm2hP4,11
20
+ agent_player-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 White Sands Interactive
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ whitesands
whitesands/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """White Sands Interactive libraries."""
@@ -0,0 +1,34 @@
1
+ """Start a remote game agent from a player class.
2
+
3
+ The SDK publishes the class as tools. The model runs on the platform, so the
4
+ game process never holds the model key.
5
+ """
6
+
7
+ from whitesands.agent_player.agent_server import AgentHandle, AgentServer, TurnResult
8
+ from whitesands.agent_player.errors import (
9
+ ConnectionLostError,
10
+ PlatformAuthError,
11
+ ProtocolVersionError,
12
+ TurnFailedError,
13
+ TurnTimeoutError,
14
+ )
15
+ from whitesands.agent_player.events import AgentEvent
16
+ from whitesands.agent_player.protocol import Attempt, TokenUsage
17
+ from whitesands.agent_player.turn_gate import AbortSignal
18
+
19
+ __version__ = "1.0.0"
20
+
21
+ __all__ = [
22
+ "AbortSignal",
23
+ "AgentEvent",
24
+ "AgentHandle",
25
+ "AgentServer",
26
+ "Attempt",
27
+ "ConnectionLostError",
28
+ "PlatformAuthError",
29
+ "ProtocolVersionError",
30
+ "TokenUsage",
31
+ "TurnFailedError",
32
+ "TurnResult",
33
+ "TurnTimeoutError",
34
+ ]