@whitesands/agent-player 1.1.0 → 1.2.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 +7 -0
- package/README.md +37 -5
- package/dist/index.cjs +141 -45
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +12 -4
- package/dist/index.d.ts +12 -4
- package/dist/index.js +139 -44
- package/dist/index.js.map +1 -1
- package/dist/protocol.cjs +16 -3
- package/dist/protocol.cjs.map +1 -1
- package/dist/protocol.d.cts +6 -2
- package/dist/protocol.d.ts +6 -2
- package/dist/protocol.js +14 -2
- package/dist/protocol.js.map +1 -1
- package/dist/testing.cjs +1 -1
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.d.cts +2 -2
- package/dist/testing.d.ts +2 -2
- package/dist/testing.js +1 -1
- package/dist/testing.js.map +1 -1
- package/dist/{types-DXM_HZHu.d.ts → types-DXqkV7YA.d.ts} +6 -0
- package/dist/{types-D0p6GrVs.d.cts → types-Zi-2YmF2.d.cts} +6 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Register a tool from a name, description, Zod schema, and callback with `bindDynamic`.
|
|
8
|
+
- `start()` requires `playerId`, the character UUID from the web app. One platform key serves the game, and each session names the character to play.
|
|
9
|
+
|
|
3
10
|
## 1.1.0
|
|
4
11
|
|
|
5
12
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -40,7 +40,7 @@ Use the same Zod 3 the SDK depends on when you declare argument schemas. Zod 4 o
|
|
|
40
40
|
|
|
41
41
|
## Usage
|
|
42
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.
|
|
43
|
+
Set the platform key, then start one agent per character. The key belongs to the game. `playerId` is the character id from the Players page, and every `start()` sends it. `turn()` grants that character one chance to act and resolves with what they did.
|
|
44
44
|
|
|
45
45
|
```sh
|
|
46
46
|
export WSI_PLATFORM_KEY=pk_...
|
|
@@ -53,6 +53,7 @@ import { AgentServer } from "@whitesands/agent-player";
|
|
|
53
53
|
|
|
54
54
|
const agents = new AgentServer();
|
|
55
55
|
const ada = await agents.start(new Villager(world, "Ada"), {
|
|
56
|
+
playerId: "3f6c2d1e-8b7a-4c5d-9e0f-1a2b3c4d5e6f",
|
|
56
57
|
name: "Ada",
|
|
57
58
|
goal: "Stand on the tree and chop it",
|
|
58
59
|
});
|
|
@@ -127,6 +128,28 @@ class Villager {
|
|
|
127
128
|
}
|
|
128
129
|
```
|
|
129
130
|
|
|
131
|
+
### Tools from data
|
|
132
|
+
|
|
133
|
+
`bindDynamic` registers one tool when the action is data rather than a method. Call it on the player before `start()`. `see` is allowed, including when the player has no `see()` method. `wait`, an empty name, a name starting with `_`, and a missing callback are rejected. An empty description becomes `Action: <name>`. Registering the same name again replaces that tool, including a reflected method of the same name.
|
|
134
|
+
|
|
135
|
+
Arguments use the same Zod shape as `static agent`. A schema failure throws before the callback runs and uses the one retry. Extra keys are ignored.
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
import { AgentServer, bindDynamic } from "@whitesands/agent-player";
|
|
139
|
+
import { z } from "zod";
|
|
140
|
+
|
|
141
|
+
const scout = { n: 1 };
|
|
142
|
+
const playerId = "3f6c2d1e-8b7a-4c5d-9e0f-1a2b3c4d5e6f";
|
|
143
|
+
bindDynamic(scout, "see", "", undefined, () => ({ n: scout.n }));
|
|
144
|
+
bindDynamic(scout, "add", "Add dx", { dx: z.number().int() }, (args) => {
|
|
145
|
+
scout.n += args.dx as number;
|
|
146
|
+
return { n: scout.n };
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const agents = new AgentServer();
|
|
150
|
+
const agent = await agents.start(scout, { playerId });
|
|
151
|
+
```
|
|
152
|
+
|
|
130
153
|
### Turns
|
|
131
154
|
|
|
132
155
|
`turn()` does not choose the action. It unblocks that session's `wait`. The platform reads `see()`, calls one action, and reports back.
|
|
@@ -176,11 +199,19 @@ try {
|
|
|
176
199
|
|
|
177
200
|
### Several characters
|
|
178
201
|
|
|
179
|
-
One `AgentServer`
|
|
202
|
+
One `AgentServer` and one platform key host every character in the game. Each `start()` is its own session and names that character with `playerId`. The id is a UUID, and it stays the same when the character is renamed. A missing id, a value that is not a UUID, or an id from another game is rejected and no session is created.
|
|
180
203
|
|
|
181
204
|
```ts
|
|
182
|
-
const ada = await agents.start(new Villager(world, "Ada"), {
|
|
183
|
-
|
|
205
|
+
const ada = await agents.start(new Villager(world, "Ada"), {
|
|
206
|
+
playerId: "3f6c2d1e-8b7a-4c5d-9e0f-1a2b3c4d5e6f",
|
|
207
|
+
name: "Ada",
|
|
208
|
+
goal: "Chop the tree",
|
|
209
|
+
});
|
|
210
|
+
const bea = await agents.start(new Villager(world, "Bea"), {
|
|
211
|
+
playerId: "6b1e2a30-0c1d-4e5f-8a9b-0c1d2e3f4a5b",
|
|
212
|
+
name: "Bea",
|
|
213
|
+
goal: "Carry wood home",
|
|
214
|
+
});
|
|
184
215
|
|
|
185
216
|
await ada.turn();
|
|
186
217
|
await bea.turn();
|
|
@@ -234,6 +265,7 @@ const agents = new AgentServer({
|
|
|
234
265
|
|
|
235
266
|
```ts
|
|
236
267
|
await agents.start(ferry, {
|
|
268
|
+
playerId: "3f6c2d1e-8b7a-4c5d-9e0f-1a2b3c4d5e6f",
|
|
237
269
|
name: "Ferryman",
|
|
238
270
|
goal: "Get the wolf, the goat, and the cabbage onto the far bank.",
|
|
239
271
|
});
|
|
@@ -241,7 +273,7 @@ await agents.start(ferry, {
|
|
|
241
273
|
|
|
242
274
|
### Testing without a platform
|
|
243
275
|
|
|
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.
|
|
276
|
+
`@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. `start()` still requires `playerId`.
|
|
245
277
|
|
|
246
278
|
```ts
|
|
247
279
|
import { AgentServer } from "@whitesands/agent-player";
|
package/dist/index.cjs
CHANGED
|
@@ -35,7 +35,8 @@ __export(src_exports, {
|
|
|
35
35
|
PlatformAuthError: () => PlatformAuthError,
|
|
36
36
|
ProtocolVersionError: () => ProtocolVersionError,
|
|
37
37
|
TurnFailedError: () => TurnFailedError,
|
|
38
|
-
TurnTimeoutError: () => TurnTimeoutError
|
|
38
|
+
TurnTimeoutError: () => TurnTimeoutError,
|
|
39
|
+
bindDynamic: () => bindDynamic
|
|
39
40
|
});
|
|
40
41
|
module.exports = __toCommonJS(src_exports);
|
|
41
42
|
|
|
@@ -158,7 +159,7 @@ var import_ws = __toESM(require("ws"), 1);
|
|
|
158
159
|
|
|
159
160
|
// src/protocol.ts
|
|
160
161
|
var PROTOCOL_VERSION = 1;
|
|
161
|
-
var SDK_VERSION = "1.
|
|
162
|
+
var SDK_VERSION = "1.2.0";
|
|
162
163
|
var SESSION_RESUME_GRACE_MS = 3e4;
|
|
163
164
|
var DEFAULT_AGENT_INSTRUCTIONS = [
|
|
164
165
|
"You are a remote game agent. Tools are provided by the game.",
|
|
@@ -167,6 +168,17 @@ var DEFAULT_AGENT_INSTRUCTIONS = [
|
|
|
167
168
|
"The view's done list is what this character has already called in this session, oldest first. Use it when choosing the next action.",
|
|
168
169
|
"If an action tool returns an error, retry once with a corrected action. A second failure ends the turn as failed."
|
|
169
170
|
].join("\n");
|
|
171
|
+
var PLAYER_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
172
|
+
function requirePlayerId(value) {
|
|
173
|
+
const id = value?.trim() ?? "";
|
|
174
|
+
if (id.length === 0) {
|
|
175
|
+
throw new Error("playerId is required. Copy the character ID from the web app");
|
|
176
|
+
}
|
|
177
|
+
if (!PLAYER_ID.test(id)) {
|
|
178
|
+
throw new Error(`playerId "${id}" is not a character ID. Copy the ID from the character in the web app`);
|
|
179
|
+
}
|
|
180
|
+
return id;
|
|
181
|
+
}
|
|
170
182
|
function assertNever(value) {
|
|
171
183
|
throw new Error(`Unhandled protocol message: ${JSON.stringify(value)}`);
|
|
172
184
|
}
|
|
@@ -279,6 +291,14 @@ var PlatformConnection = class {
|
|
|
279
291
|
});
|
|
280
292
|
});
|
|
281
293
|
}
|
|
294
|
+
/** Reject a start, stop, or resume wait. No-op when nothing is waiting. */
|
|
295
|
+
failSessionEvent(sessionId, kind, error) {
|
|
296
|
+
const key = `session:${sessionId}:${kind}`;
|
|
297
|
+
const waiter = this.pending.get(key);
|
|
298
|
+
if (!waiter) return;
|
|
299
|
+
this.pending.delete(key);
|
|
300
|
+
waiter.reject(error);
|
|
301
|
+
}
|
|
282
302
|
createTransport(sessionId) {
|
|
283
303
|
return new SessionMuxTransport(sessionId, (envelope) => this.send(envelope));
|
|
284
304
|
}
|
|
@@ -482,6 +502,7 @@ var PlatformConnection = class {
|
|
|
482
502
|
var import_zod = require("zod");
|
|
483
503
|
var import_zod_to_json_schema = require("zod-to-json-schema");
|
|
484
504
|
var SKIP = /* @__PURE__ */ new Set(["constructor"]);
|
|
505
|
+
var dynamicTools = /* @__PURE__ */ new WeakMap();
|
|
485
506
|
var SEE_DESCRIPTION = "Read the current game view for this player. Prefer wait, which returns this after the game grants a turn.";
|
|
486
507
|
var WAIT_DESCRIPTION = "Block until the game calls turn() for this session, then return the current see() view.";
|
|
487
508
|
function getParamNames(fn) {
|
|
@@ -525,6 +546,56 @@ function formatZodError(error) {
|
|
|
525
546
|
return `${path}: ${issue.message}`;
|
|
526
547
|
}).join("; ");
|
|
527
548
|
}
|
|
549
|
+
function rejectDynamicName(name) {
|
|
550
|
+
if (name.length === 0 || name === "wait" || name.startsWith("_")) {
|
|
551
|
+
throw new Error(`cannot publish action "${name}"`);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
function dynamicBindings(player) {
|
|
555
|
+
return dynamicTools.get(player) ?? /* @__PURE__ */ new Map();
|
|
556
|
+
}
|
|
557
|
+
function bindDynamic(player, name, description, args, callback) {
|
|
558
|
+
rejectDynamicName(name);
|
|
559
|
+
if (typeof callback !== "function") {
|
|
560
|
+
throw new Error(`cannot publish action "${name}"`);
|
|
561
|
+
}
|
|
562
|
+
const shape = args && Object.keys(args).length > 0 ? { ...args } : void 0;
|
|
563
|
+
let tools = dynamicTools.get(player);
|
|
564
|
+
if (!tools) {
|
|
565
|
+
tools = /* @__PURE__ */ new Map();
|
|
566
|
+
dynamicTools.set(player, tools);
|
|
567
|
+
}
|
|
568
|
+
tools.set(name, {
|
|
569
|
+
name,
|
|
570
|
+
description: description.length > 0 ? description : `Action: ${name}`,
|
|
571
|
+
...shape ? { args: shape } : {},
|
|
572
|
+
callback
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
function buildDynamicTool(binding) {
|
|
576
|
+
const shape = binding.args;
|
|
577
|
+
const paramNames = shape ? Object.keys(shape) : [];
|
|
578
|
+
const argsSchema = shape ? import_zod.z.object(shape) : void 0;
|
|
579
|
+
const inputSchema = argsSchema ? toJsonSchema(argsSchema) : { type: "object", properties: {} };
|
|
580
|
+
return {
|
|
581
|
+
name: binding.name,
|
|
582
|
+
description: binding.description,
|
|
583
|
+
paramNames,
|
|
584
|
+
...argsSchema ? { argsSchema } : {},
|
|
585
|
+
descriptor: { name: binding.name, description: binding.description, inputSchema },
|
|
586
|
+
async invoke(args) {
|
|
587
|
+
let parsed = {};
|
|
588
|
+
if (argsSchema) {
|
|
589
|
+
const result = argsSchema.safeParse(args ?? {});
|
|
590
|
+
if (!result.success) {
|
|
591
|
+
throw new Error(formatZodError(result.error));
|
|
592
|
+
}
|
|
593
|
+
parsed = result.data;
|
|
594
|
+
}
|
|
595
|
+
return await Promise.resolve(binding.callback(parsed));
|
|
596
|
+
}
|
|
597
|
+
};
|
|
598
|
+
}
|
|
528
599
|
function buildTool(instance, name, description, spec) {
|
|
529
600
|
const fn = instance[name];
|
|
530
601
|
if (typeof fn !== "function") {
|
|
@@ -560,24 +631,31 @@ function buildTool(instance, name, description, spec) {
|
|
|
560
631
|
}
|
|
561
632
|
function reflectPlayer(instance) {
|
|
562
633
|
const names = collectMethodNames(instance);
|
|
563
|
-
|
|
634
|
+
const dynamic = dynamicBindings(instance);
|
|
635
|
+
if (!names.includes("see") && !dynamic.has("see")) {
|
|
564
636
|
throw new Error("Player must implement a public see() method");
|
|
565
637
|
}
|
|
566
638
|
const metadata = readMetadata(instance);
|
|
567
639
|
const declared = metadata?.actions ?? {};
|
|
568
640
|
const className = instance.constructor?.name || "Player";
|
|
569
641
|
for (const name of Object.keys(declared)) {
|
|
570
|
-
if (!names.includes(name)
|
|
642
|
+
if (name === "see" || !names.includes(name) && !dynamic.has(name)) {
|
|
571
643
|
throw new Error(`Declared action "${name}" is not a public method on ${className}`);
|
|
572
644
|
}
|
|
573
645
|
}
|
|
574
|
-
const
|
|
575
|
-
const
|
|
576
|
-
const actions =
|
|
646
|
+
const dynamicSee = dynamic.get("see");
|
|
647
|
+
const see = dynamicSee ? buildDynamicTool(dynamicSee) : buildTool(instance, "see", SEE_DESCRIPTION);
|
|
648
|
+
const actions = [];
|
|
649
|
+
for (const name of names) {
|
|
650
|
+
if (name === "see" || dynamic.has(name)) continue;
|
|
577
651
|
const spec = declared[name];
|
|
578
652
|
const description = spec?.description ?? `Action: ${name}`;
|
|
579
|
-
|
|
580
|
-
}
|
|
653
|
+
actions.push(buildTool(instance, name, description, spec));
|
|
654
|
+
}
|
|
655
|
+
for (const [name, binding] of dynamic) {
|
|
656
|
+
if (name === "see") continue;
|
|
657
|
+
actions.push(buildDynamicTool(binding));
|
|
658
|
+
}
|
|
581
659
|
const wait = {
|
|
582
660
|
name: "wait",
|
|
583
661
|
description: WAIT_DESCRIPTION,
|
|
@@ -826,6 +904,7 @@ var AgentServer = class {
|
|
|
826
904
|
* `turn()` on that handle lets the remote model act once.
|
|
827
905
|
*/
|
|
828
906
|
async start(instance, options) {
|
|
907
|
+
const playerId = requirePlayerId(options.playerId);
|
|
829
908
|
await this.connect();
|
|
830
909
|
const reflected = reflectPlayer(instance);
|
|
831
910
|
const sessionId = (0, import_node_crypto2.randomUUID)();
|
|
@@ -857,41 +936,54 @@ var AgentServer = class {
|
|
|
857
936
|
gate.completeTurn(result);
|
|
858
937
|
};
|
|
859
938
|
const onEvent = (event) => this.emit(event);
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
939
|
+
this.sessions.set(sessionId, record);
|
|
940
|
+
try {
|
|
941
|
+
if (this.inline) {
|
|
942
|
+
const [serverTransport, clientTransport] = import_inMemory.InMemoryTransport.createLinkedPair();
|
|
943
|
+
await server.connect(serverTransport);
|
|
944
|
+
record.transport = serverTransport;
|
|
945
|
+
await this.inline.startSession({
|
|
946
|
+
sessionId,
|
|
947
|
+
sessionToken,
|
|
948
|
+
playerId,
|
|
949
|
+
name,
|
|
950
|
+
tools: reflected.tools,
|
|
951
|
+
instructions,
|
|
952
|
+
...options.goal ? { goal: options.goal } : {},
|
|
953
|
+
...this.logger ? { logger: this.logger } : {},
|
|
954
|
+
transport: clientTransport,
|
|
955
|
+
onTurnResult,
|
|
956
|
+
onEvent
|
|
957
|
+
});
|
|
958
|
+
} else {
|
|
959
|
+
if (!this.connection) throw new Error("Not connected");
|
|
960
|
+
const mux = this.connection.createTransport(sessionId);
|
|
961
|
+
await server.connect(mux);
|
|
962
|
+
record.transport = mux;
|
|
963
|
+
record.mux = mux;
|
|
964
|
+
const started = this.connection.waitForSessionEvent(sessionId, "start");
|
|
965
|
+
try {
|
|
966
|
+
this.connection.send({
|
|
967
|
+
type: "start_session",
|
|
968
|
+
sessionId,
|
|
969
|
+
sessionToken,
|
|
970
|
+
playerId,
|
|
971
|
+
name,
|
|
972
|
+
tools: reflected.tools,
|
|
973
|
+
instructions,
|
|
974
|
+
...options.goal ? { goal: options.goal } : {}
|
|
975
|
+
});
|
|
976
|
+
await started;
|
|
977
|
+
} catch (error) {
|
|
978
|
+
const wrapped = error instanceof Error ? error : new Error(String(error));
|
|
979
|
+
this.connection.failSessionEvent(sessionId, "start", wrapped);
|
|
980
|
+
await started.catch(() => void 0);
|
|
981
|
+
throw wrapped;
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
} catch (error) {
|
|
985
|
+
await this.stopSession(sessionId);
|
|
986
|
+
throw error;
|
|
895
987
|
}
|
|
896
988
|
return {
|
|
897
989
|
sessionId,
|
|
@@ -991,6 +1083,9 @@ var AgentServer = class {
|
|
|
991
1083
|
}
|
|
992
1084
|
case "error":
|
|
993
1085
|
log(this.logger, "error", envelope.message);
|
|
1086
|
+
if (envelope.sessionId) {
|
|
1087
|
+
this.connection?.failSessionEvent(envelope.sessionId, "start", new Error(envelope.message));
|
|
1088
|
+
}
|
|
994
1089
|
return;
|
|
995
1090
|
case "session_started":
|
|
996
1091
|
case "session_stopped":
|
|
@@ -1067,6 +1162,7 @@ var AgentServer = class {
|
|
|
1067
1162
|
PlatformAuthError,
|
|
1068
1163
|
ProtocolVersionError,
|
|
1069
1164
|
TurnFailedError,
|
|
1070
|
-
TurnTimeoutError
|
|
1165
|
+
TurnTimeoutError,
|
|
1166
|
+
bindDynamic
|
|
1071
1167
|
});
|
|
1072
1168
|
//# sourceMappingURL=index.cjs.map
|