@whitesands/agent-player 0.2.0 → 1.1.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 +13 -3
- package/README.md +234 -41
- package/dist/index.cjs +116 -27
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -4
- package/dist/index.d.ts +6 -4
- package/dist/index.js +116 -24
- package/dist/index.js.map +1 -1
- package/dist/protocol.cjs +2 -1
- package/dist/protocol.cjs.map +1 -1
- package/dist/protocol.d.cts +4 -2
- package/dist/protocol.d.ts +4 -2
- package/dist/protocol.js +2 -1
- package/dist/protocol.js.map +1 -1
- package/dist/testing.cjs +19 -20
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.d.cts +3 -2
- package/dist/testing.d.ts +3 -2
- package/dist/testing.js +19 -20
- package/dist/testing.js.map +1 -1
- package/dist/{types-DCIs4-gP.d.cts → types-D0p6GrVs.d.cts} +9 -2
- package/dist/{types-DIF8LgGI.d.ts → types-DXM_HZHu.d.ts} +9 -2
- package/package.json +27 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,14 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- The SDK connects to `wss://api.whitesandsint.com`. The platform URL is no longer read from the environment.
|
|
8
|
+
|
|
9
|
+
## 1.0.0
|
|
10
|
+
|
|
11
|
+
### Major Changes
|
|
12
|
+
|
|
13
|
+
- Remember each character's actions in a local SQLite file and include them on the next view.
|
|
14
|
+
|
|
3
15
|
## 0.2.0
|
|
4
16
|
|
|
5
17
|
### Minor Changes
|
|
6
18
|
|
|
7
19
|
- 46faef6: Publish the agent player SDK: typed actions, turn results, reconnect, and an offline testing entry.
|
|
8
20
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
First public SDK release.
|
|
21
|
+
First release published to npm.
|
|
12
22
|
|
|
13
23
|
- `AgentServer` reads `WSI_PLATFORM_KEY` and `WSI_PLATFORM_URL`.
|
|
14
24
|
- Player classes can declare zod argument schemas and descriptions.
|
package/README.md
CHANGED
|
@@ -1,35 +1,110 @@
|
|
|
1
|
-
#
|
|
1
|
+
# WSI Agent Player
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@whitesands/agent-player)
|
|
4
|
+
[](https://github.com/White-Sands-Interactive/WSI-Node/actions/workflows/ci.yml)
|
|
4
5
|
|
|
5
|
-
|
|
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, 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
|
|
9
47
|
```
|
|
10
48
|
|
|
49
|
+
The SDK connects to `wss://api.whitesandsint.com`.
|
|
50
|
+
|
|
11
51
|
```ts
|
|
12
52
|
import { AgentServer } from "@whitesands/agent-player";
|
|
13
53
|
|
|
14
54
|
const agents = new AgentServer();
|
|
15
55
|
const ada = await agents.start(new Villager(world, "Ada"), {
|
|
16
|
-
|
|
56
|
+
name: "Ada",
|
|
57
|
+
goal: "Stand on the tree and chop it",
|
|
17
58
|
});
|
|
18
59
|
|
|
19
60
|
const result = await ada.turn();
|
|
20
61
|
// { action: "move", args: { dx: 1, dy: 0 }, result, attempts: 1, usage }
|
|
21
62
|
```
|
|
22
63
|
|
|
23
|
-
`new AgentServer()`
|
|
64
|
+
`new AgentServer({ platformKey })` overrides `WSI_PLATFORM_KEY`.
|
|
24
65
|
|
|
25
|
-
|
|
66
|
+
### The player class
|
|
26
67
|
|
|
27
|
-
|
|
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`.
|
|
68
|
+
`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
69
|
|
|
32
|
-
|
|
70
|
+
Throw to refuse an action. The platform asks the model once more. A second failure rejects `turn()` with `TurnFailedError`.
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
class Villager {
|
|
74
|
+
x = 0;
|
|
75
|
+
y = 0;
|
|
76
|
+
wood = 0;
|
|
77
|
+
|
|
78
|
+
see() {
|
|
79
|
+
return { x: this.x, y: this.y, wood: this.wood, tile: world.get(this.x, this.y) };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
move(dx: number, dy: number) {
|
|
83
|
+
const nextX = this.x + dx;
|
|
84
|
+
const nextY = this.y + dy;
|
|
85
|
+
if (!world.inBounds(nextX, nextY)) {
|
|
86
|
+
throw new Error(`Cannot move to ${nextX},${nextY}`);
|
|
87
|
+
}
|
|
88
|
+
this.x = nextX;
|
|
89
|
+
this.y = nextY;
|
|
90
|
+
return { x: this.x, y: this.y };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
chop() {
|
|
94
|
+
if (world.get(this.x, this.y) !== "T") {
|
|
95
|
+
throw new Error(`No tree at ${this.x},${this.y}`);
|
|
96
|
+
}
|
|
97
|
+
this.wood += 1;
|
|
98
|
+
return { wood: this.wood };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
A method with no `static agent` entry is still a tool. Its arguments accept any JSON value.
|
|
104
|
+
|
|
105
|
+
### Declaring arguments
|
|
106
|
+
|
|
107
|
+
`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
108
|
|
|
34
109
|
```ts
|
|
35
110
|
import { z } from "zod";
|
|
@@ -39,20 +114,22 @@ class Villager {
|
|
|
39
114
|
description: "A villager who gathers wood",
|
|
40
115
|
actions: {
|
|
41
116
|
move: {
|
|
42
|
-
description: "Step by dx
|
|
117
|
+
description: "Step by dx and dy. Stay inside the map.",
|
|
43
118
|
args: { dx: z.number().int(), dy: z.number().int() },
|
|
44
119
|
},
|
|
45
120
|
chop: { description: "Chop the tree on your tile" },
|
|
46
121
|
},
|
|
47
122
|
};
|
|
123
|
+
|
|
124
|
+
see() {
|
|
125
|
+
return { x: this.x, y: this.y };
|
|
126
|
+
}
|
|
48
127
|
}
|
|
49
128
|
```
|
|
50
129
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
## Turns
|
|
130
|
+
### Turns
|
|
54
131
|
|
|
55
|
-
`turn()` does not choose the action. It unblocks that session's `wait
|
|
132
|
+
`turn()` does not choose the action. It unblocks that session's `wait`. The platform reads `see()`, calls one action, and reports back.
|
|
56
133
|
|
|
57
134
|
```ts
|
|
58
135
|
const result = await ada.turn({ timeoutMs: 30_000, signal });
|
|
@@ -63,28 +140,108 @@ const result = await ada.turn({ timeoutMs: 30_000, signal });
|
|
|
63
140
|
| `action` | Tool the model called |
|
|
64
141
|
| `args` | Arguments it passed |
|
|
65
142
|
| `result` | What your method returned |
|
|
66
|
-
| `attempts` | `1`, or `2` when the first call failed |
|
|
67
|
-
| `usage` |
|
|
143
|
+
| `attempts` | `1`, or `2` when the first call failed and the retry succeeded |
|
|
144
|
+
| `usage` | `promptTokens` and `completionTokens` for that turn |
|
|
145
|
+
|
|
146
|
+
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.
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
await using agents = new AgentServer();
|
|
150
|
+
```
|
|
68
151
|
|
|
69
|
-
|
|
152
|
+
### Errors
|
|
70
153
|
|
|
71
154
|
| Error | When |
|
|
72
155
|
| --- | --- |
|
|
73
|
-
| `PlatformAuthError` | The platform
|
|
74
|
-
| `ProtocolVersionError` |
|
|
75
|
-
| `TurnFailedError` |
|
|
76
|
-
| `TurnTimeoutError` | The platform did not finish within `timeoutMs`
|
|
77
|
-
| `ConnectionLostError` | The socket dropped and could not be resumed |
|
|
156
|
+
| `PlatformAuthError` | The platform rejected the key |
|
|
157
|
+
| `ProtocolVersionError` | This SDK and the platform disagree on the protocol |
|
|
158
|
+
| `TurnFailedError` | The action and the retry both failed. `error.attempts` lists both calls |
|
|
159
|
+
| `TurnTimeoutError` | The platform did not finish within `timeoutMs` |
|
|
160
|
+
| `ConnectionLostError` | The socket dropped and the session could not be resumed |
|
|
78
161
|
|
|
79
|
-
The SDK reconnects with backoff
|
|
162
|
+
The SDK reconnects with backoff. A disconnected session can be resumed for 30 seconds.
|
|
80
163
|
|
|
81
164
|
```ts
|
|
82
|
-
|
|
165
|
+
import { TurnFailedError } from "@whitesands/agent-player";
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
await ada.turn();
|
|
169
|
+
} catch (error) {
|
|
170
|
+
if (error instanceof TurnFailedError) {
|
|
171
|
+
console.log(error.attempts);
|
|
172
|
+
}
|
|
173
|
+
throw error;
|
|
174
|
+
}
|
|
83
175
|
```
|
|
84
176
|
|
|
85
|
-
|
|
177
|
+
### Several characters
|
|
86
178
|
|
|
87
|
-
|
|
179
|
+
One `AgentServer` hosts every character. Each `start()` is its own session.
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
const ada = await agents.start(new Villager(world, "Ada"), { name: "Ada", goal: "Chop the tree" });
|
|
183
|
+
const bea = await agents.start(new Villager(world, "Bea"), { name: "Bea", goal: "Carry wood home" });
|
|
184
|
+
|
|
185
|
+
await ada.turn();
|
|
186
|
+
await bea.turn();
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
The game decides whose turn it is by calling `turn()` on that handle.
|
|
190
|
+
|
|
191
|
+
### Events
|
|
192
|
+
|
|
193
|
+
`agents.on` hears every session. `handle.on` hears one session. Both return an unsubscribe function.
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
const stop = ada.on((event) => {
|
|
197
|
+
if (event.type === "action") console.log(event.action, event.args);
|
|
198
|
+
if (event.type === "retry") console.log(event.action, event.error);
|
|
199
|
+
if (event.type === "turnFailed") console.log(event.error);
|
|
200
|
+
});
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Connection events are `disconnected` and `reconnected`. Those have no `sessionId`.
|
|
204
|
+
|
|
205
|
+
### Action history
|
|
206
|
+
|
|
207
|
+
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.
|
|
208
|
+
|
|
209
|
+
The next `see()` view includes `done`, oldest first, up to the last 50 calls for that session:
|
|
210
|
+
|
|
211
|
+
```json
|
|
212
|
+
{
|
|
213
|
+
"x": 1,
|
|
214
|
+
"y": 0,
|
|
215
|
+
"done": [
|
|
216
|
+
{ "action": "move", "args": { "dx": 1, "dy": 0 }, "ok": true }
|
|
217
|
+
]
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
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.
|
|
222
|
+
|
|
223
|
+
```ts
|
|
224
|
+
const agents = new AgentServer({
|
|
225
|
+
memoryPath: "saves/agents.sqlite",
|
|
226
|
+
});
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
`memoryPath: false` turns the log off. The view then has no `done` field.
|
|
230
|
+
|
|
231
|
+
### Goal and instructions
|
|
232
|
+
|
|
233
|
+
`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.
|
|
234
|
+
|
|
235
|
+
```ts
|
|
236
|
+
await agents.start(ferry, {
|
|
237
|
+
name: "Ferryman",
|
|
238
|
+
goal: "Get the wolf, the goat, and the cabbage onto the far bank.",
|
|
239
|
+
});
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### Testing without a platform
|
|
243
|
+
|
|
244
|
+
`@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
245
|
|
|
89
246
|
```ts
|
|
90
247
|
import { AgentServer } from "@whitesands/agent-player";
|
|
@@ -93,26 +250,62 @@ import { createTestPlatform } from "@whitesands/agent-player/testing";
|
|
|
93
250
|
const agents = new AgentServer({
|
|
94
251
|
inline: createTestPlatform([
|
|
95
252
|
{ name: "move", arguments: { dx: 1, dy: 0 } },
|
|
253
|
+
{ name: "chop", arguments: {} },
|
|
96
254
|
]),
|
|
97
255
|
});
|
|
98
256
|
```
|
|
99
257
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
## What runs where
|
|
258
|
+
### Where each piece runs
|
|
103
259
|
|
|
104
260
|
| | Game process | Platform |
|
|
105
261
|
| --- | --- | --- |
|
|
106
|
-
| World and player methods | yes |
|
|
107
|
-
|
|
|
108
|
-
| Whose turn it is | `turn()` |
|
|
109
|
-
| Which action to take |
|
|
110
|
-
| Model API key |
|
|
262
|
+
| World and player methods | yes | |
|
|
263
|
+
| Tools (`see`, actions, `wait`) | yes | calls them |
|
|
264
|
+
| Whose turn it is | `turn()` | |
|
|
265
|
+
| Which action to take | | the model |
|
|
266
|
+
| Model API key | | yes |
|
|
267
|
+
|
|
268
|
+
### Logging
|
|
269
|
+
|
|
270
|
+
Pass a `logger` to print SDK warnings. Omit it and nothing is written.
|
|
271
|
+
|
|
272
|
+
```ts
|
|
273
|
+
const agents = new AgentServer({
|
|
274
|
+
logger: {
|
|
275
|
+
warn: (message) => console.warn(message),
|
|
276
|
+
error: (message) => console.error(message),
|
|
277
|
+
},
|
|
278
|
+
});
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
`debug`, `info`, `warn`, and `error` are all optional.
|
|
282
|
+
|
|
283
|
+
### Protocol
|
|
284
|
+
|
|
285
|
+
`@whitesands/agent-player/protocol` exports the wire types and `PROTOCOL_VERSION`. Game code uses the main entry and does not import the protocol.
|
|
286
|
+
|
|
287
|
+
## This repository
|
|
288
|
+
|
|
289
|
+
| Path | Role |
|
|
290
|
+
| --- | --- |
|
|
291
|
+
| `src` | The published SDK |
|
|
292
|
+
| `examples/forest` | Two villagers, reflected methods |
|
|
293
|
+
| `examples/courier` | Zod arguments and a goal |
|
|
294
|
+
| `examples/ferry` | One passenger chosen each turn |
|
|
295
|
+
|
|
296
|
+
```sh
|
|
297
|
+
npm install
|
|
298
|
+
npm test
|
|
299
|
+
npm run build
|
|
300
|
+
npm run forest
|
|
301
|
+
npm run courier
|
|
302
|
+
npm run ferry
|
|
303
|
+
```
|
|
111
304
|
|
|
112
|
-
|
|
305
|
+
Each example replays a script. Forest and ferry can also call the hosted platform:
|
|
113
306
|
|
|
114
|
-
```
|
|
115
|
-
npm run
|
|
307
|
+
```sh
|
|
308
|
+
PLATFORM_KEY=pk_... npm run forest -- --remote
|
|
116
309
|
```
|
|
117
310
|
|
|
118
|
-
|
|
311
|
+
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 = "
|
|
161
|
+
var SDK_VERSION = "1.1.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((
|
|
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
|
-
|
|
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((
|
|
228
|
-
ws.once("close", () =>
|
|
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((
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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((
|
|
619
|
-
this.waitResolve =
|
|
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((
|
|
642
|
-
this.turnResolve =
|
|
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
|
|
676
|
-
if (!
|
|
749
|
+
const resolve2 = this.turnResolve;
|
|
750
|
+
if (!resolve2) return;
|
|
677
751
|
this.turnResolve = null;
|
|
678
752
|
this.turnReject = null;
|
|
679
|
-
|
|
753
|
+
resolve2(outcome);
|
|
680
754
|
}
|
|
681
755
|
/** Fail an in-flight turn, for example when the connection cannot be resumed. */
|
|
682
756
|
failTurn(error) {
|
|
@@ -700,7 +774,7 @@ var TurnGate = class {
|
|
|
700
774
|
|
|
701
775
|
// src/agent-server.ts
|
|
702
776
|
var DEFAULT_TURN_TIMEOUT_MS = 6e4;
|
|
703
|
-
var
|
|
777
|
+
var PLATFORM_URL = "wss://api.whitesandsint.com";
|
|
704
778
|
var AgentServer = class {
|
|
705
779
|
options;
|
|
706
780
|
platformKey;
|
|
@@ -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 = {}) {
|
|
@@ -719,7 +794,7 @@ var AgentServer = class {
|
|
|
719
794
|
}
|
|
720
795
|
this.options = options;
|
|
721
796
|
this.platformKey = platformKey || "inline";
|
|
722
|
-
this.platformUrl = options.platformUrl ??
|
|
797
|
+
this.platformUrl = options.platformUrl ?? PLATFORM_URL;
|
|
723
798
|
this.logger = options.logger;
|
|
724
799
|
this.inline = options.inline;
|
|
725
800
|
if (options.onEvent) this.listeners.add(options.onEvent);
|
|
@@ -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
|
});
|