@whitesands/agent-player 0.2.0 → 1.0.0

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.
package/CHANGELOG.md CHANGED
@@ -1,14 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - Remember each character's actions in a local SQLite file and include them on the next view.
8
+
3
9
  ## 0.2.0
4
10
 
5
11
  ### Minor Changes
6
12
 
7
13
  - 46faef6: Publish the agent player SDK: typed actions, turn results, reconnect, and an offline testing entry.
8
14
 
9
- ## 0.1.0
10
-
11
- First public SDK release.
15
+ First release published to npm.
12
16
 
13
17
  - `AgentServer` reads `WSI_PLATFORM_KEY` and `WSI_PLATFORM_URL`.
14
18
  - Player classes can declare zod argument schemas and descriptions.
package/README.md CHANGED
@@ -1,35 +1,111 @@
1
- # @whitesands/agent-player
1
+ # WSI Agent Player
2
2
 
3
- Start a remote game agent from a player class. You call `start` and `turn`. The SDK owns the MCP server. The model stays on the platform, so the game never sees the model key.
3
+ [![npm version](https://img.shields.io/npm/v/@whitesands/agent-player.svg)](https://www.npmjs.com/package/@whitesands/agent-player)
4
+ [![Build Status](https://github.com/White-Sands-Interactive/WSI-Node-Framework/actions/workflows/ci.yml/badge.svg)](https://github.com/White-Sands-Interactive/WSI-Node-Framework/actions/workflows/ci.yml)
4
5
 
5
- ```bash
6
+ 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.
7
+
8
+ The library also provides:
9
+
10
+ - A player class turned into tools, with optional Zod argument schemas.
11
+ - One action per turn, and one retry when that action throws.
12
+ - An offline loop for tests, with no socket and no platform key.
13
+ - A local SQLite log of what each character has already called.
14
+
15
+ ## Documentation
16
+
17
+ This page is the guide. Generate the API reference with:
18
+
19
+ ```sh
20
+ npm run docs
21
+ ```
22
+
23
+ TypeDoc writes HTML to `docs`.
24
+
25
+ ## Installation
26
+
27
+ You don't need this source code unless you want to modify the SDK. To use the package:
28
+
29
+ ```sh
6
30
  npm install @whitesands/agent-player
31
+ ```
32
+
33
+ ### Requirements
34
+
35
+ Node.js 20 or newer.
36
+
37
+ Installing the package builds `better-sqlite3`. npm uses a prebuilt binary on current macOS, Linux, and Windows. That module is the action log described below.
38
+
39
+ Use the same Zod 3 the SDK depends on when you declare argument schemas. Zod 4 objects are not read as schemas.
40
+
41
+ ## Usage
42
+
43
+ Set the platform key, then start one agent per character. `turn()` grants that character one chance to act and resolves with what they did.
44
+
45
+ ```sh
7
46
  export WSI_PLATFORM_KEY=pk_...
8
- export WSI_PLATFORM_URL=wss://your-platform.example
47
+ export WSI_PLATFORM_URL=ws://127.0.0.1:8787
9
48
  ```
10
49
 
50
+ `WSI_PLATFORM_URL` defaults to `ws://127.0.0.1:8787` when it is unset.
51
+
11
52
  ```ts
12
53
  import { AgentServer } from "@whitesands/agent-player";
13
54
 
14
55
  const agents = new AgentServer();
15
56
  const ada = await agents.start(new Villager(world, "Ada"), {
16
- goal: "Collect all the wood",
57
+ name: "Ada",
58
+ goal: "Stand on the tree and chop it",
17
59
  });
18
60
 
19
61
  const result = await ada.turn();
20
62
  // { action: "move", args: { dx: 1, dy: 0 }, result, attempts: 1, usage }
21
63
  ```
22
64
 
23
- `new AgentServer()` reads `WSI_PLATFORM_KEY` and `WSI_PLATFORM_URL`. Until a hosted URL is the default, set `WSI_PLATFORM_URL` (it falls back to `ws://127.0.0.1:8787`).
65
+ `new AgentServer({ platformKey, platformUrl })` overrides the environment variables.
24
66
 
25
- ## Player class
67
+ ### The player class
26
68
 
27
- - `see()` is the state tool. It must be public and return JSON-safe data.
28
- - Every other public method is an action. The model calls one per turn.
29
- - Methods whose names start with `_`, and the constructor, are not tools.
30
- - Throw to reject an action. The platform asks the model once more, then `turn()` rejects with `TurnFailedError`.
69
+ `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 the constructor, are left off the tool list.
31
70
 
32
- Optional static metadata adds descriptions and argument types. Undeclared public methods are still tools. Without metadata, arguments accept any JSON value.
71
+ Throw to refuse an action. The platform asks the model once more. A second failure rejects `turn()` with `TurnFailedError`.
72
+
73
+ ```ts
74
+ class Villager {
75
+ x = 0;
76
+ y = 0;
77
+ wood = 0;
78
+
79
+ see() {
80
+ return { x: this.x, y: this.y, wood: this.wood, tile: world.get(this.x, this.y) };
81
+ }
82
+
83
+ move(dx: number, dy: number) {
84
+ const nextX = this.x + dx;
85
+ const nextY = this.y + dy;
86
+ if (!world.inBounds(nextX, nextY)) {
87
+ throw new Error(`Cannot move to ${nextX},${nextY}`);
88
+ }
89
+ this.x = nextX;
90
+ this.y = nextY;
91
+ return { x: this.x, y: this.y };
92
+ }
93
+
94
+ chop() {
95
+ if (world.get(this.x, this.y) !== "T") {
96
+ throw new Error(`No tree at ${this.x},${this.y}`);
97
+ }
98
+ this.wood += 1;
99
+ return { wood: this.wood };
100
+ }
101
+ }
102
+ ```
103
+
104
+ A method with no `static agent` entry is still a tool. Its arguments accept any JSON value.
105
+
106
+ ### Declaring arguments
107
+
108
+ `static agent` adds the description the model sees and checks arguments with Zod before your method runs. A schema failure counts as a failed action and uses the one retry.
33
109
 
34
110
  ```ts
35
111
  import { z } from "zod";
@@ -39,20 +115,22 @@ class Villager {
39
115
  description: "A villager who gathers wood",
40
116
  actions: {
41
117
  move: {
42
- description: "Step by dx, dy",
118
+ description: "Step by dx and dy. Stay inside the map.",
43
119
  args: { dx: z.number().int(), dy: z.number().int() },
44
120
  },
45
121
  chop: { description: "Chop the tree on your tile" },
46
122
  },
47
123
  };
124
+
125
+ see() {
126
+ return { x: this.x, y: this.y };
127
+ }
48
128
  }
49
129
  ```
50
130
 
51
- Use the same Zod major version the SDK depends on (`zod@3`).
52
-
53
- ## Turns
131
+ ### Turns
54
132
 
55
- `turn()` does not choose the action. It unblocks that session's `wait`, then resolves when the platform has called one action (or retried once).
133
+ `turn()` does not choose the action. It unblocks that session's `wait`. The platform reads `see()`, calls one action, and reports back.
56
134
 
57
135
  ```ts
58
136
  const result = await ada.turn({ timeoutMs: 30_000, signal });
@@ -63,28 +141,108 @@ const result = await ada.turn({ timeoutMs: 30_000, signal });
63
141
  | `action` | Tool the model called |
64
142
  | `args` | Arguments it passed |
65
143
  | `result` | What your method returned |
66
- | `attempts` | `1`, or `2` when the first call failed |
67
- | `usage` | Prompt and completion tokens for that turn |
144
+ | `attempts` | `1`, or `2` when the first call failed and the retry succeeded |
145
+ | `usage` | `promptTokens` and `completionTokens` for that turn |
146
+
147
+ The default timeout is 60 seconds. Pass `signal` to abort the wait. `handle.stop()` ends that session. `agents.close()`, also run by `await using`, stops every session on the server. The platform process keeps running.
148
+
149
+ ```ts
150
+ await using agents = new AgentServer();
151
+ ```
68
152
 
69
- ## Errors
153
+ ### Errors
70
154
 
71
155
  | Error | When |
72
156
  | --- | --- |
73
- | `PlatformAuthError` | The platform key was rejected |
74
- | `ProtocolVersionError` | The SDK and platform disagree on the protocol |
75
- | `TurnFailedError` | Both the action and the retry failed. `error.attempts` lists both |
76
- | `TurnTimeoutError` | The platform did not finish within `timeoutMs` (default 60s) |
77
- | `ConnectionLostError` | The socket dropped and could not be resumed |
157
+ | `PlatformAuthError` | The platform rejected the key |
158
+ | `ProtocolVersionError` | This SDK and the platform disagree on the protocol |
159
+ | `TurnFailedError` | The action and the retry both failed. `error.attempts` lists both calls |
160
+ | `TurnTimeoutError` | The platform did not finish within `timeoutMs` |
161
+ | `ConnectionLostError` | The socket dropped and the session could not be resumed |
78
162
 
79
- The SDK reconnects with backoff and resumes sessions for 30 seconds. Subscribe with `agents.on(listener)` or `handle.on(listener)` for `action`, `retry`, `turnFailed`, `disconnected`, and `reconnected`.
163
+ The SDK reconnects with backoff. A disconnected session can be resumed for 30 seconds.
80
164
 
81
165
  ```ts
82
- await using agents = new AgentServer();
166
+ import { TurnFailedError } from "@whitesands/agent-player";
167
+
168
+ try {
169
+ await ada.turn();
170
+ } catch (error) {
171
+ if (error instanceof TurnFailedError) {
172
+ console.log(error.attempts);
173
+ }
174
+ throw error;
175
+ }
83
176
  ```
84
177
 
85
- `await using` calls `close()`, which stops every session. The platform process keeps running.
178
+ ### Several characters
86
179
 
87
- ## Testing without a network
180
+ One `AgentServer` hosts every character. Each `start()` is its own session.
181
+
182
+ ```ts
183
+ const ada = await agents.start(new Villager(world, "Ada"), { name: "Ada", goal: "Chop the tree" });
184
+ const bea = await agents.start(new Villager(world, "Bea"), { name: "Bea", goal: "Carry wood home" });
185
+
186
+ await ada.turn();
187
+ await bea.turn();
188
+ ```
189
+
190
+ The game decides whose turn it is by calling `turn()` on that handle.
191
+
192
+ ### Events
193
+
194
+ `agents.on` hears every session. `handle.on` hears one session. Both return an unsubscribe function.
195
+
196
+ ```ts
197
+ const stop = ada.on((event) => {
198
+ if (event.type === "action") console.log(event.action, event.args);
199
+ if (event.type === "retry") console.log(event.action, event.error);
200
+ if (event.type === "turnFailed") console.log(event.error);
201
+ });
202
+ ```
203
+
204
+ Connection events are `disconnected` and `reconnected`. Those have no `sessionId`.
205
+
206
+ ### Action history
207
+
208
+ 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.
209
+
210
+ The next `see()` view includes `done`, oldest first, up to the last 50 calls for that session:
211
+
212
+ ```json
213
+ {
214
+ "x": 1,
215
+ "y": 0,
216
+ "done": [
217
+ { "action": "move", "args": { "dx": 1, "dy": 0 }, "ok": true }
218
+ ]
219
+ }
220
+ ```
221
+
222
+ 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.
223
+
224
+ ```ts
225
+ const agents = new AgentServer({
226
+ memoryPath: "saves/agents.sqlite",
227
+ });
228
+ ```
229
+
230
+ `memoryPath: false` turns the log off. The view then has no `done` field.
231
+
232
+ ### Goal and instructions
233
+
234
+ `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.
235
+
236
+ ```ts
237
+ await agents.start(ferry, {
238
+ name: "Ferryman",
239
+ goal: "Get the wolf, the goat, and the cabbage onto the far bank.",
240
+ });
241
+ ```
242
+
243
+ ### Testing without a platform
244
+
245
+ `@whitesands/agent-player/testing` 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.
88
246
 
89
247
  ```ts
90
248
  import { AgentServer } from "@whitesands/agent-player";
@@ -93,26 +251,64 @@ import { createTestPlatform } from "@whitesands/agent-player/testing";
93
251
  const agents = new AgentServer({
94
252
  inline: createTestPlatform([
95
253
  { name: "move", arguments: { dx: 1, dy: 0 } },
254
+ { name: "chop", arguments: {} },
96
255
  ]),
97
256
  });
98
257
  ```
99
258
 
100
- `createTestPlatform` runs the same wait / act / retry loop in memory. No key, no socket.
101
-
102
- ## What runs where
259
+ ### Where each piece runs
103
260
 
104
261
  | | Game process | Platform |
105
262
  | --- | --- | --- |
106
- | World and player methods | yes | no |
107
- | MCP tools (`see`, actions, `wait`) | yes | calls them |
108
- | Whose turn it is | `turn()` | no |
109
- | Which action to take | no | the model |
110
- | Model API key | no | yes |
263
+ | World and player methods | yes | |
264
+ | Tools (`see`, actions, `wait`) | yes | calls them |
265
+ | Whose turn it is | `turn()` | |
266
+ | Which action to take | | the model |
267
+ | Model API key | | yes |
268
+
269
+ ### Logging
270
+
271
+ Pass a `logger` to print SDK warnings. Omit it and nothing is written.
272
+
273
+ ```ts
274
+ const agents = new AgentServer({
275
+ logger: {
276
+ warn: (message) => console.warn(message),
277
+ error: (message) => console.error(message),
278
+ },
279
+ });
280
+ ```
281
+
282
+ `debug`, `info`, `warn`, and `error` are all optional.
283
+
284
+ ### Protocol
285
+
286
+ `@whitesands/agent-player/protocol` exports the wire types and `PROTOCOL_VERSION`. The platform's copy is `internal/protocol` in [White-Sands-Interactive/Platform](https://github.com/White-Sands-Interactive/Platform). A change to the wire format has to land in both places. Game code uses the main entry and does not import the protocol.
287
+
288
+ ## This repository
289
+
290
+ | Path | Role |
291
+ | --- | --- |
292
+ | `src` | The published SDK |
293
+ | `examples/forest` | Two villagers, reflected methods |
294
+ | `examples/courier` | Zod arguments and a goal |
295
+ | `examples/ferry` | One passenger chosen each turn |
296
+
297
+ ```sh
298
+ npm install
299
+ npm test
300
+ npm run build
301
+ npm run forest
302
+ npm run courier
303
+ npm run ferry
304
+ ```
111
305
 
112
- ## API reference
306
+ Each example replays a script. Add `-- --remote` to use a platform that is already serving:
113
307
 
114
- ```bash
115
- npm run docs -w @whitesands/agent-player
308
+ ```sh
309
+ ./platform create-key demo
310
+ MODEL_API_KEY=... MODEL_BASE_URL=... MODEL_NAME=... ./platform serve
311
+ PLATFORM_KEY=pk_... npm run forest -- --remote
116
312
  ```
117
313
 
118
- TypeDoc writes HTML to `packages/agent-player/docs`.
314
+ `./platform` is the Go server from the Platform repository. Releases are recorded in [CHANGELOG.md](CHANGELOG.md).
package/dist/index.cjs CHANGED
@@ -32,11 +32,8 @@ var src_exports = {};
32
32
  __export(src_exports, {
33
33
  AgentServer: () => AgentServer,
34
34
  ConnectionLostError: () => ConnectionLostError,
35
- DEFAULT_AGENT_INSTRUCTIONS: () => DEFAULT_AGENT_INSTRUCTIONS,
36
- PROTOCOL_VERSION: () => PROTOCOL_VERSION,
37
35
  PlatformAuthError: () => PlatformAuthError,
38
36
  ProtocolVersionError: () => ProtocolVersionError,
39
- SDK_VERSION: () => SDK_VERSION,
40
37
  TurnFailedError: () => TurnFailedError,
41
38
  TurnTimeoutError: () => TurnTimeoutError
42
39
  });
@@ -85,18 +82,89 @@ function log(logger, level, message, extra) {
85
82
  logger?.[level]?.(message, extra);
86
83
  }
87
84
 
85
+ // src/memory.ts
86
+ var import_node_fs = require("fs");
87
+ var import_node_path = require("path");
88
+ var import_better_sqlite3 = __toESM(require("better-sqlite3"), 1);
89
+ var MAX_DONE = 50;
90
+ var SessionMemory = class {
91
+ db;
92
+ constructor(path) {
93
+ (0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(path), { recursive: true });
94
+ this.db = new import_better_sqlite3.default(path, { timeout: 5e3 });
95
+ this.db.exec(`
96
+ CREATE TABLE IF NOT EXISTS actions (
97
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
98
+ session_id TEXT NOT NULL,
99
+ character TEXT NOT NULL,
100
+ action TEXT NOT NULL,
101
+ args TEXT NOT NULL,
102
+ ok INTEGER NOT NULL,
103
+ error TEXT
104
+ );
105
+ CREATE INDEX IF NOT EXISTS actions_by_session ON actions(session_id, id);
106
+ `);
107
+ }
108
+ record(sessionId, character, entry) {
109
+ this.db.prepare(
110
+ `INSERT INTO actions (session_id, character, action, args, ok, error)
111
+ VALUES (@sessionId, @character, @action, @args, @ok, @error)`
112
+ ).run({
113
+ sessionId,
114
+ character,
115
+ action: entry.action,
116
+ args: JSON.stringify(entry.args),
117
+ ok: entry.ok ? 1 : 0,
118
+ error: entry.error ?? null
119
+ });
120
+ }
121
+ /** Oldest first, capped to the most recent actions for this session. */
122
+ done(sessionId) {
123
+ const rows = this.db.prepare(
124
+ `SELECT action, args, ok, error FROM (
125
+ SELECT id, action, args, ok, error FROM actions
126
+ WHERE session_id = ?
127
+ ORDER BY id DESC
128
+ LIMIT ?
129
+ ) ORDER BY id ASC`
130
+ ).all(sessionId, MAX_DONE);
131
+ return rows.map((row) => {
132
+ const entry = {
133
+ action: row.action,
134
+ args: JSON.parse(row.args),
135
+ ok: row.ok === 1
136
+ };
137
+ if (row.error) entry.error = row.error;
138
+ return entry;
139
+ });
140
+ }
141
+ close() {
142
+ this.db.close();
143
+ }
144
+ };
145
+ function defaultMemoryPath() {
146
+ return (0, import_node_path.resolve)(process.cwd(), ".wsi", "agent-memory.sqlite");
147
+ }
148
+ function viewWithDone(view, done) {
149
+ if (view && typeof view === "object" && !Array.isArray(view)) {
150
+ return { ...view, done };
151
+ }
152
+ return { view, done };
153
+ }
154
+
88
155
  // src/platform-connection.ts
89
156
  var import_node_crypto = require("crypto");
90
157
  var import_ws = __toESM(require("ws"), 1);
91
158
 
92
159
  // src/protocol.ts
93
160
  var PROTOCOL_VERSION = 1;
94
- var SDK_VERSION = "0.1.0";
161
+ var SDK_VERSION = "1.0.0";
95
162
  var SESSION_RESUME_GRACE_MS = 3e4;
96
163
  var DEFAULT_AGENT_INSTRUCTIONS = [
97
164
  "You are a remote game agent. Tools are provided by the game.",
98
165
  "On every turn: call wait first. It blocks until the game grants your turn and then returns the current view from see.",
99
166
  "After wait returns, call exactly one action tool (not see, not wait).",
167
+ "The view's done list is what this character has already called in this session, oldest first. Use it when choosing the next action.",
100
168
  "If an action tool returns an error, retry once with a corrected action. A second failure ends the turn as failed."
101
169
  ].join("\n");
102
170
  function assertNever(value) {
@@ -194,7 +262,7 @@ var PlatformConnection = class {
194
262
  }
195
263
  waitForSessionEvent(sessionId, kind, timeoutMs = 15e3) {
196
264
  const key = `session:${sessionId}:${kind}`;
197
- return new Promise((resolve, reject) => {
265
+ return new Promise((resolve2, reject) => {
198
266
  const timer = setTimeout(() => {
199
267
  this.pending.delete(key);
200
268
  reject(new Error(`Timed out waiting for session ${kind}`));
@@ -202,7 +270,7 @@ var PlatformConnection = class {
202
270
  this.pending.set(key, {
203
271
  resolve: (envelope) => {
204
272
  clearTimeout(timer);
205
- resolve(envelope);
273
+ resolve2(envelope);
206
274
  },
207
275
  reject: (error) => {
208
276
  clearTimeout(timer);
@@ -224,13 +292,13 @@ var PlatformConnection = class {
224
292
  if (!this.ws) return;
225
293
  const ws = this.ws;
226
294
  this.ws = null;
227
- await new Promise((resolve) => {
228
- ws.once("close", () => resolve());
295
+ await new Promise((resolve2) => {
296
+ ws.once("close", () => resolve2());
229
297
  ws.close();
230
298
  });
231
299
  }
232
300
  openSocket() {
233
- return new Promise((resolve, reject) => {
301
+ return new Promise((resolve2, reject) => {
234
302
  const ws = new import_ws.default(this.url);
235
303
  this.ws = ws;
236
304
  let settled = false;
@@ -269,7 +337,7 @@ var PlatformConnection = class {
269
337
  this.accountName = envelope.accountName;
270
338
  this.startHeartbeat();
271
339
  this.flushOutbox();
272
- resolve(envelope.accountName);
340
+ resolve2(envelope.accountName);
273
341
  return;
274
342
  }
275
343
  if (envelope.type === "pong") {
@@ -540,13 +608,17 @@ function createSessionMcpServer(player, gate, options) {
540
608
  { name: options?.name ?? "agent-player", version: SDK_VERSION },
541
609
  { capabilities: { tools: {} } }
542
610
  );
611
+ const actions = options?.actions;
612
+ const viewed = async () => {
613
+ const view = await player.see.invoke({});
614
+ return actions ? viewWithDone(view, actions.done()) : view;
615
+ };
543
616
  server.registerTool(
544
617
  "see",
545
618
  { description: player.see.description },
546
619
  async () => {
547
620
  try {
548
- const view = await player.see.invoke({});
549
- return jsonResult(view);
621
+ return jsonResult(await viewed());
550
622
  } catch (error) {
551
623
  return toolError(error);
552
624
  }
@@ -558,8 +630,7 @@ function createSessionMcpServer(player, gate, options) {
558
630
  async () => {
559
631
  try {
560
632
  await gate.waitForTurn();
561
- const view = await player.see.invoke({});
562
- return jsonResult(view);
633
+ return jsonResult(await viewed());
563
634
  } catch (error) {
564
635
  return toolError(error);
565
636
  }
@@ -569,8 +640,11 @@ function createSessionMcpServer(player, gate, options) {
569
640
  const handler = async (args) => {
570
641
  try {
571
642
  const result = await action.invoke(args);
643
+ actions?.record({ action: action.name, args, ok: true });
572
644
  return jsonResult(result ?? { ok: true });
573
645
  } catch (error) {
646
+ const message = error instanceof Error ? error.message : String(error);
647
+ actions?.record({ action: action.name, args, ok: false, error: message });
574
648
  return toolError(error);
575
649
  }
576
650
  };
@@ -615,8 +689,8 @@ var TurnGate = class {
615
689
  return;
616
690
  }
617
691
  if (!this.waitPromise) {
618
- this.waitPromise = new Promise((resolve) => {
619
- this.waitResolve = resolve;
692
+ this.waitPromise = new Promise((resolve2) => {
693
+ this.waitResolve = resolve2;
620
694
  });
621
695
  }
622
696
  await this.waitPromise;
@@ -638,8 +712,8 @@ var TurnGate = class {
638
712
  let timeoutId;
639
713
  let onAbort;
640
714
  const signal = options?.signal;
641
- this.turnPromise = new Promise((resolve, reject) => {
642
- this.turnResolve = resolve;
715
+ this.turnPromise = new Promise((resolve2, reject) => {
716
+ this.turnResolve = resolve2;
643
717
  this.turnReject = reject;
644
718
  if (options?.timeoutMs != null) {
645
719
  timeoutId = setTimeout(() => {
@@ -672,11 +746,11 @@ var TurnGate = class {
672
746
  }
673
747
  /** Platform finished one action cycle (success or second failure). */
674
748
  completeTurn(outcome) {
675
- const resolve = this.turnResolve;
676
- if (!resolve) return;
749
+ const resolve2 = this.turnResolve;
750
+ if (!resolve2) return;
677
751
  this.turnResolve = null;
678
752
  this.turnReject = null;
679
- resolve(outcome);
753
+ resolve2(outcome);
680
754
  }
681
755
  /** Fail an in-flight turn, for example when the connection cannot be resumed. */
682
756
  failTurn(error) {
@@ -710,6 +784,7 @@ var AgentServer = class {
710
784
  sessions = /* @__PURE__ */ new Map();
711
785
  listeners = /* @__PURE__ */ new Set();
712
786
  inline;
787
+ memory = null;
713
788
  started = false;
714
789
  closing = false;
715
790
  constructor(options = {}) {
@@ -757,8 +832,15 @@ var AgentServer = class {
757
832
  const sessionToken = (0, import_node_crypto2.randomUUID)();
758
833
  const gate = new TurnGate();
759
834
  const name = options?.name ?? instance.constructor?.name ?? "player";
835
+ const memory = this.actionMemory();
760
836
  const server = createSessionMcpServer(reflected, gate, {
761
- name: `agent-player:${name}`
837
+ name: `agent-player:${name}`,
838
+ ...memory ? {
839
+ actions: {
840
+ record: (entry) => memory.record(sessionId, name, entry),
841
+ done: () => memory.done(sessionId)
842
+ }
843
+ } : {}
762
844
  });
763
845
  const instructions = [this.options.instructions ?? DEFAULT_AGENT_INSTRUCTIONS, options?.instructions].filter((part) => Boolean(part && part.length > 0)).join("\n\n");
764
846
  const listeners = /* @__PURE__ */ new Set();
@@ -961,9 +1043,19 @@ var AgentServer = class {
961
1043
  await this.connection.close();
962
1044
  this.connection = null;
963
1045
  }
1046
+ this.memory?.close();
1047
+ this.memory = null;
964
1048
  this.started = false;
965
1049
  this.closing = false;
966
1050
  }
1051
+ actionMemory() {
1052
+ if (this.options.memoryPath === false) return null;
1053
+ if (!this.memory) {
1054
+ const path = this.options.memoryPath ?? defaultMemoryPath();
1055
+ this.memory = new SessionMemory(path);
1056
+ }
1057
+ return this.memory;
1058
+ }
967
1059
  async [Symbol.asyncDispose]() {
968
1060
  await this.close();
969
1061
  }
@@ -972,11 +1064,8 @@ var AgentServer = class {
972
1064
  0 && (module.exports = {
973
1065
  AgentServer,
974
1066
  ConnectionLostError,
975
- DEFAULT_AGENT_INSTRUCTIONS,
976
- PROTOCOL_VERSION,
977
1067
  PlatformAuthError,
978
1068
  ProtocolVersionError,
979
- SDK_VERSION,
980
1069
  TurnFailedError,
981
1070
  TurnTimeoutError
982
1071
  });