@whitesands/agent-player 0.0.0-stage → 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/dist/index.cjs ADDED
@@ -0,0 +1,1072 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var src_exports = {};
32
+ __export(src_exports, {
33
+ AgentServer: () => AgentServer,
34
+ ConnectionLostError: () => ConnectionLostError,
35
+ PlatformAuthError: () => PlatformAuthError,
36
+ ProtocolVersionError: () => ProtocolVersionError,
37
+ TurnFailedError: () => TurnFailedError,
38
+ TurnTimeoutError: () => TurnTimeoutError
39
+ });
40
+ module.exports = __toCommonJS(src_exports);
41
+
42
+ // src/agent-server.ts
43
+ var import_node_crypto2 = require("crypto");
44
+ var import_inMemory = require("@modelcontextprotocol/sdk/inMemory.js");
45
+
46
+ // src/errors.ts
47
+ var PlatformAuthError = class extends Error {
48
+ constructor(message) {
49
+ super(message);
50
+ this.name = "PlatformAuthError";
51
+ }
52
+ };
53
+ var ProtocolVersionError = class extends Error {
54
+ constructor(message) {
55
+ super(message);
56
+ this.name = "ProtocolVersionError";
57
+ }
58
+ };
59
+ var TurnFailedError = class extends Error {
60
+ attempts;
61
+ constructor(message, attempts = []) {
62
+ super(message);
63
+ this.name = "TurnFailedError";
64
+ this.attempts = attempts;
65
+ }
66
+ };
67
+ var TurnTimeoutError = class extends Error {
68
+ constructor(message) {
69
+ super(message);
70
+ this.name = "TurnTimeoutError";
71
+ }
72
+ };
73
+ var ConnectionLostError = class extends Error {
74
+ constructor(message = "Connection to the platform was lost") {
75
+ super(message);
76
+ this.name = "ConnectionLostError";
77
+ }
78
+ };
79
+
80
+ // src/logger.ts
81
+ function log(logger, level, message, extra) {
82
+ logger?.[level]?.(message, extra);
83
+ }
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
+
155
+ // src/platform-connection.ts
156
+ var import_node_crypto = require("crypto");
157
+ var import_ws = __toESM(require("ws"), 1);
158
+
159
+ // src/protocol.ts
160
+ var PROTOCOL_VERSION = 1;
161
+ var SDK_VERSION = "1.0.0";
162
+ var SESSION_RESUME_GRACE_MS = 3e4;
163
+ var DEFAULT_AGENT_INSTRUCTIONS = [
164
+ "You are a remote game agent. Tools are provided by the game.",
165
+ "On every turn: call wait first. It blocks until the game grants your turn and then returns the current view from see.",
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.",
168
+ "If an action tool returns an error, retry once with a corrected action. A second failure ends the turn as failed."
169
+ ].join("\n");
170
+ function assertNever(value) {
171
+ throw new Error(`Unhandled protocol message: ${JSON.stringify(value)}`);
172
+ }
173
+
174
+ // src/ws-transport.ts
175
+ var SessionMuxTransport = class {
176
+ onclose;
177
+ onerror;
178
+ onmessage;
179
+ sessionId;
180
+ sid;
181
+ sendEnvelope;
182
+ started = false;
183
+ closed = false;
184
+ constructor(sessionId, sendEnvelope) {
185
+ this.sid = sessionId;
186
+ this.sessionId = sessionId;
187
+ this.sendEnvelope = sendEnvelope;
188
+ }
189
+ async start() {
190
+ if (this.closed) throw new Error("Transport already closed");
191
+ this.started = true;
192
+ }
193
+ async close() {
194
+ if (this.closed) return;
195
+ this.closed = true;
196
+ this.onclose?.();
197
+ }
198
+ async send(message) {
199
+ if (this.closed) throw new Error("Transport closed");
200
+ this.sendEnvelope({ type: "mcp", sessionId: this.sid, message });
201
+ }
202
+ /** Deliver an inbound MCP message from the platform. */
203
+ receive(message) {
204
+ if (this.closed || !this.started) return;
205
+ try {
206
+ this.onmessage?.(message);
207
+ } catch (error) {
208
+ this.onerror?.(error instanceof Error ? error : new Error(String(error)));
209
+ }
210
+ }
211
+ };
212
+
213
+ // src/platform-connection.ts
214
+ var HEARTBEAT_MS = 15e3;
215
+ var PONG_TIMEOUT_MS = 1e4;
216
+ var MAX_RECONNECT_ATTEMPTS = 8;
217
+ var PlatformConnection = class {
218
+ ws = null;
219
+ url;
220
+ platformKey;
221
+ clientId = (0, import_node_crypto.randomUUID)();
222
+ handlers;
223
+ logger;
224
+ pending = /* @__PURE__ */ new Map();
225
+ outbox = [];
226
+ accountName = null;
227
+ closed = false;
228
+ everConnected = false;
229
+ reconnecting = false;
230
+ reconnectAttempt = 0;
231
+ reconnectTimer;
232
+ heartbeat;
233
+ pongTimer;
234
+ pendingPong = null;
235
+ constructor(url, platformKey, handlers, logger) {
236
+ this.url = url;
237
+ this.platformKey = platformKey;
238
+ this.handlers = handlers;
239
+ this.logger = logger;
240
+ }
241
+ get connected() {
242
+ return this.ws?.readyState === import_ws.default.OPEN;
243
+ }
244
+ async connect() {
245
+ if (this.closed) throw new Error("Platform connection is closed");
246
+ if (this.connected && this.accountName) return this.accountName;
247
+ const account = await this.openSocket();
248
+ this.everConnected = true;
249
+ this.reconnectAttempt = 0;
250
+ return account;
251
+ }
252
+ send(envelope) {
253
+ if (this.connected && this.ws) {
254
+ this.ws.send(JSON.stringify(envelope));
255
+ return;
256
+ }
257
+ if (envelope.type === "mcp" || envelope.type === "ping") {
258
+ this.outbox.push(envelope);
259
+ return;
260
+ }
261
+ throw new Error("Not connected to platform");
262
+ }
263
+ waitForSessionEvent(sessionId, kind, timeoutMs = 15e3) {
264
+ const key = `session:${sessionId}:${kind}`;
265
+ return new Promise((resolve2, reject) => {
266
+ const timer = setTimeout(() => {
267
+ this.pending.delete(key);
268
+ reject(new Error(`Timed out waiting for session ${kind}`));
269
+ }, timeoutMs);
270
+ this.pending.set(key, {
271
+ resolve: (envelope) => {
272
+ clearTimeout(timer);
273
+ resolve2(envelope);
274
+ },
275
+ reject: (error) => {
276
+ clearTimeout(timer);
277
+ reject(error);
278
+ }
279
+ });
280
+ });
281
+ }
282
+ createTransport(sessionId) {
283
+ return new SessionMuxTransport(sessionId, (envelope) => this.send(envelope));
284
+ }
285
+ async close() {
286
+ this.closed = true;
287
+ this.stopHeartbeat();
288
+ if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
289
+ this.reconnectTimer = void 0;
290
+ this.reconnecting = false;
291
+ this.failWaiters(new Error("platform connection closed"));
292
+ if (!this.ws) return;
293
+ const ws = this.ws;
294
+ this.ws = null;
295
+ await new Promise((resolve2) => {
296
+ ws.once("close", () => resolve2());
297
+ ws.close();
298
+ });
299
+ }
300
+ openSocket() {
301
+ return new Promise((resolve2, reject) => {
302
+ const ws = new import_ws.default(this.url);
303
+ this.ws = ws;
304
+ let settled = false;
305
+ const fail = (error) => {
306
+ if (settled) return;
307
+ settled = true;
308
+ reject(error);
309
+ };
310
+ ws.on("open", () => {
311
+ ws.send(JSON.stringify({
312
+ type: "hello",
313
+ platformKey: this.platformKey,
314
+ clientId: this.clientId,
315
+ protocolVersion: PROTOCOL_VERSION,
316
+ sdkVersion: SDK_VERSION
317
+ }));
318
+ });
319
+ ws.on("message", (data) => {
320
+ let envelope;
321
+ try {
322
+ envelope = JSON.parse(data.toString());
323
+ } catch (error) {
324
+ const parsed = error instanceof Error ? error : new Error("Invalid platform message");
325
+ this.handlers.onError?.(parsed);
326
+ return;
327
+ }
328
+ if (!settled && (envelope.type === "hello_ok" || envelope.type === "hello_err")) {
329
+ settled = true;
330
+ if (envelope.type === "hello_err") {
331
+ const error = envelope.code === "PROTOCOL" ? new ProtocolVersionError(envelope.message) : new PlatformAuthError(envelope.message);
332
+ this.closed = true;
333
+ reject(error);
334
+ ws.close();
335
+ return;
336
+ }
337
+ this.accountName = envelope.accountName;
338
+ this.startHeartbeat();
339
+ this.flushOutbox();
340
+ resolve2(envelope.accountName);
341
+ return;
342
+ }
343
+ if (envelope.type === "pong") {
344
+ if (this.pendingPong === envelope.id) {
345
+ this.pendingPong = null;
346
+ if (this.pongTimer) clearTimeout(this.pongTimer);
347
+ this.pongTimer = void 0;
348
+ }
349
+ return;
350
+ }
351
+ this.resolveSessionWaiter(envelope);
352
+ this.handlers.onEnvelope(envelope);
353
+ });
354
+ ws.on("error", (error) => {
355
+ this.handlers.onError?.(error);
356
+ fail(error);
357
+ });
358
+ ws.on("close", () => {
359
+ this.stopHeartbeat();
360
+ if (this.ws === ws) this.ws = null;
361
+ if (!settled) fail(new Error("platform connection closed before hello"));
362
+ if (this.closed || !this.everConnected) {
363
+ this.failWaiters(new Error("platform connection closed"));
364
+ return;
365
+ }
366
+ this.handlers.onDisconnect?.();
367
+ this.scheduleReconnect();
368
+ });
369
+ });
370
+ }
371
+ resolveSessionWaiter(envelope) {
372
+ if (envelope.type === "session_started" || envelope.type === "session_stopped") {
373
+ const kind = envelope.type === "session_started" ? "start" : "stop";
374
+ const waiter = this.pending.get(`session:${envelope.sessionId}:${kind}`);
375
+ if (!waiter) return;
376
+ this.pending.delete(`session:${envelope.sessionId}:${kind}`);
377
+ waiter.resolve(envelope);
378
+ return;
379
+ }
380
+ if (envelope.type === "session_resumed" || envelope.type === "resume_err") {
381
+ const waiter = this.pending.get(`session:${envelope.sessionId}:resume`);
382
+ if (!waiter) return;
383
+ this.pending.delete(`session:${envelope.sessionId}:resume`);
384
+ if (envelope.type === "resume_err") {
385
+ waiter.reject(new ConnectionLostError(envelope.message));
386
+ return;
387
+ }
388
+ waiter.resolve(envelope);
389
+ }
390
+ }
391
+ scheduleReconnect() {
392
+ if (this.closed || this.reconnecting) return;
393
+ if (this.reconnectAttempt >= MAX_RECONNECT_ATTEMPTS) {
394
+ this.closed = true;
395
+ const error = new ConnectionLostError("Could not reconnect to the platform");
396
+ this.handlers.onError?.(error);
397
+ this.failWaiters(error);
398
+ return;
399
+ }
400
+ const delay = Math.min(SESSION_RESUME_GRACE_MS, 250 * 2 ** this.reconnectAttempt);
401
+ this.reconnectAttempt += 1;
402
+ this.reconnecting = true;
403
+ log(this.logger, "warn", `platform connection dropped; retrying in ${delay}ms`);
404
+ this.reconnectTimer = setTimeout(() => {
405
+ this.reconnectTimer = void 0;
406
+ void this.reconnect();
407
+ }, delay);
408
+ this.reconnectTimer.unref?.();
409
+ }
410
+ async reconnect() {
411
+ try {
412
+ await this.openSocket();
413
+ this.everConnected = true;
414
+ this.reconnectAttempt = 0;
415
+ this.reconnecting = false;
416
+ const sessions = this.handlers.sessions?.() ?? [];
417
+ for (const session of sessions) {
418
+ const resumed = this.waitForSessionEvent(session.sessionId, "resume");
419
+ this.send({
420
+ type: "resume_session",
421
+ sessionId: session.sessionId,
422
+ sessionToken: session.sessionToken
423
+ });
424
+ await resumed;
425
+ }
426
+ this.handlers.onReconnect?.();
427
+ } catch (error) {
428
+ this.reconnecting = false;
429
+ if (this.closed) return;
430
+ const wrapped = error instanceof Error ? error : new Error(String(error));
431
+ if (wrapped instanceof PlatformAuthError || wrapped instanceof ProtocolVersionError) {
432
+ this.closed = true;
433
+ this.handlers.onError?.(wrapped);
434
+ this.failWaiters(wrapped);
435
+ return;
436
+ }
437
+ this.scheduleReconnect();
438
+ }
439
+ }
440
+ flushOutbox() {
441
+ if (!this.ws || !this.connected) return;
442
+ const pending = this.outbox.splice(0, this.outbox.length);
443
+ for (const envelope of pending) {
444
+ this.ws.send(JSON.stringify(envelope));
445
+ }
446
+ }
447
+ startHeartbeat() {
448
+ this.stopHeartbeat();
449
+ this.heartbeat = setInterval(() => {
450
+ if (!this.connected) return;
451
+ const id = (0, import_node_crypto.randomUUID)();
452
+ this.pendingPong = id;
453
+ try {
454
+ this.send({ type: "ping", id });
455
+ } catch {
456
+ return;
457
+ }
458
+ this.pongTimer = setTimeout(() => {
459
+ if (this.pendingPong === id) {
460
+ log(this.logger, "warn", "platform heartbeat timed out");
461
+ this.ws?.terminate();
462
+ }
463
+ }, PONG_TIMEOUT_MS);
464
+ this.pongTimer.unref?.();
465
+ }, HEARTBEAT_MS);
466
+ this.heartbeat.unref?.();
467
+ }
468
+ stopHeartbeat() {
469
+ if (this.heartbeat) clearInterval(this.heartbeat);
470
+ if (this.pongTimer) clearTimeout(this.pongTimer);
471
+ this.heartbeat = void 0;
472
+ this.pongTimer = void 0;
473
+ this.pendingPong = null;
474
+ }
475
+ failWaiters(error) {
476
+ for (const [, waiter] of this.pending) waiter.reject(error);
477
+ this.pending.clear();
478
+ }
479
+ };
480
+
481
+ // src/reflect.ts
482
+ var import_zod = require("zod");
483
+ var import_zod_to_json_schema = require("zod-to-json-schema");
484
+ var SKIP = /* @__PURE__ */ new Set(["constructor"]);
485
+ var SEE_DESCRIPTION = "Read the current game view for this player. Prefer wait, which returns this after the game grants a turn.";
486
+ var WAIT_DESCRIPTION = "Block until the game calls turn() for this session, then return the current see() view.";
487
+ function getParamNames(fn) {
488
+ const src = fn.toString().replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/.*$/gm, "");
489
+ const header = src.match(/(?:async\s+)?(?:function\s*)?(?:\w*)\s*\(([^)]*)\)/) ?? src.match(/^(?:async\s*)?\(?([^)=]*)\)?\s*=>/);
490
+ if (!header) return [];
491
+ const raw = header[1]?.trim() ?? "";
492
+ if (!raw) return [];
493
+ return raw.split(",").map((part) => part.trim()).filter(Boolean).map((part) => {
494
+ const cleaned = part.replace(/\s*=\s*[^,]+$/, "").trim();
495
+ const name = cleaned.split(/[:\s]/)[0]?.replace(/^\.\.\./, "") ?? "";
496
+ return name;
497
+ }).filter((name) => name.length > 0 && name !== "this");
498
+ }
499
+ function collectMethodNames(instance) {
500
+ const names = /* @__PURE__ */ new Set();
501
+ let proto = Object.getPrototypeOf(instance);
502
+ while (proto && proto !== Object.prototype) {
503
+ for (const name of Object.getOwnPropertyNames(proto)) {
504
+ if (SKIP.has(name) || name.startsWith("_")) continue;
505
+ const desc = Object.getOwnPropertyDescriptor(proto, name);
506
+ if (!desc || typeof desc.value !== "function") continue;
507
+ names.add(name);
508
+ }
509
+ proto = Object.getPrototypeOf(proto);
510
+ }
511
+ return [...names].sort();
512
+ }
513
+ function readMetadata(instance) {
514
+ const ctor = instance.constructor;
515
+ return ctor.agent;
516
+ }
517
+ function toJsonSchema(schema) {
518
+ const json = (0, import_zod_to_json_schema.zodToJsonSchema)(schema, { target: "jsonSchema7", $refStrategy: "none" });
519
+ delete json.$schema;
520
+ return json;
521
+ }
522
+ function formatZodError(error) {
523
+ return error.issues.map((issue) => {
524
+ const path = issue.path.length > 0 ? issue.path.join(".") : "arguments";
525
+ return `${path}: ${issue.message}`;
526
+ }).join("; ");
527
+ }
528
+ function buildTool(instance, name, description, spec) {
529
+ const fn = instance[name];
530
+ if (typeof fn !== "function") {
531
+ throw new Error(`Expected method ${name}`);
532
+ }
533
+ const paramNames = spec?.args ? Object.keys(spec.args) : getParamNames(fn);
534
+ const shape = spec?.args ?? Object.fromEntries(paramNames.map((param) => [param, import_zod.z.any()]));
535
+ const argsSchema = paramNames.length > 0 || spec?.args ? import_zod.z.object(shape) : void 0;
536
+ const inputSchema = spec?.args && argsSchema ? toJsonSchema(argsSchema) : {
537
+ type: "object",
538
+ properties: Object.fromEntries(paramNames.map((param) => [param, {}])),
539
+ ...paramNames.length > 0 ? { required: paramNames } : {}
540
+ };
541
+ return {
542
+ name,
543
+ description,
544
+ paramNames,
545
+ ...argsSchema ? { argsSchema } : {},
546
+ descriptor: { name, description, inputSchema },
547
+ async invoke(args) {
548
+ let parsed = args ?? {};
549
+ if (argsSchema) {
550
+ const result = argsSchema.safeParse(args ?? {});
551
+ if (!result.success) {
552
+ throw new Error(formatZodError(result.error));
553
+ }
554
+ parsed = result.data;
555
+ }
556
+ const ordered = paramNames.map((param) => parsed[param]);
557
+ return await Promise.resolve(fn.apply(instance, ordered));
558
+ }
559
+ };
560
+ }
561
+ function reflectPlayer(instance) {
562
+ const names = collectMethodNames(instance);
563
+ if (!names.includes("see")) {
564
+ throw new Error("Player must implement a public see() method");
565
+ }
566
+ const metadata = readMetadata(instance);
567
+ const declared = metadata?.actions ?? {};
568
+ const className = instance.constructor?.name || "Player";
569
+ for (const name of Object.keys(declared)) {
570
+ if (!names.includes(name) || name === "see") {
571
+ throw new Error(`Declared action "${name}" is not a public method on ${className}`);
572
+ }
573
+ }
574
+ const see = buildTool(instance, "see", SEE_DESCRIPTION);
575
+ const actionNames = names.filter((name) => name !== "see");
576
+ const actions = actionNames.map((name) => {
577
+ const spec = declared[name];
578
+ const description = spec?.description ?? `Action: ${name}`;
579
+ return buildTool(instance, name, description, spec);
580
+ });
581
+ const wait = {
582
+ name: "wait",
583
+ description: WAIT_DESCRIPTION,
584
+ inputSchema: { type: "object", properties: {} }
585
+ };
586
+ return {
587
+ ...metadata?.description ? { description: metadata.description } : {},
588
+ see,
589
+ actions,
590
+ tools: [see.descriptor, ...actions.map((action) => action.descriptor), wait]
591
+ };
592
+ }
593
+
594
+ // src/session-mcp.ts
595
+ var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
596
+ function jsonResult(payload, isError = false) {
597
+ return {
598
+ isError,
599
+ content: [{ type: "text", text: JSON.stringify(payload) }]
600
+ };
601
+ }
602
+ function toolError(error) {
603
+ const message = error instanceof Error ? error.message : String(error);
604
+ return jsonResult({ error: message }, true);
605
+ }
606
+ function createSessionMcpServer(player, gate, options) {
607
+ const server = new import_mcp.McpServer(
608
+ { name: options?.name ?? "agent-player", version: SDK_VERSION },
609
+ { capabilities: { tools: {} } }
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
+ };
616
+ server.registerTool(
617
+ "see",
618
+ { description: player.see.description },
619
+ async () => {
620
+ try {
621
+ return jsonResult(await viewed());
622
+ } catch (error) {
623
+ return toolError(error);
624
+ }
625
+ }
626
+ );
627
+ server.registerTool(
628
+ "wait",
629
+ { description: WAIT_DESCRIPTION },
630
+ async () => {
631
+ try {
632
+ await gate.waitForTurn();
633
+ return jsonResult(await viewed());
634
+ } catch (error) {
635
+ return toolError(error);
636
+ }
637
+ }
638
+ );
639
+ for (const action of player.actions) {
640
+ const handler = async (args) => {
641
+ try {
642
+ const result = await action.invoke(args);
643
+ actions?.record({ action: action.name, args, ok: true });
644
+ return jsonResult(result ?? { ok: true });
645
+ } catch (error) {
646
+ const message = error instanceof Error ? error.message : String(error);
647
+ actions?.record({ action: action.name, args, ok: false, error: message });
648
+ return toolError(error);
649
+ }
650
+ };
651
+ if (action.argsSchema) {
652
+ server.registerTool(
653
+ action.name,
654
+ {
655
+ description: action.description,
656
+ inputSchema: action.argsSchema.shape
657
+ },
658
+ async (args) => handler(args)
659
+ );
660
+ } else {
661
+ server.registerTool(
662
+ action.name,
663
+ { description: action.description },
664
+ async () => handler({})
665
+ );
666
+ }
667
+ }
668
+ return server;
669
+ }
670
+
671
+ // src/turn-gate.ts
672
+ function abortError(signal) {
673
+ const reason = signal.reason;
674
+ if (reason instanceof Error) return reason;
675
+ if (typeof reason === "string" && reason.length > 0) return new Error(reason);
676
+ return new Error("turn aborted");
677
+ }
678
+ var TurnGate = class {
679
+ waitResolve = null;
680
+ waitPromise = null;
681
+ turnResolve = null;
682
+ turnReject = null;
683
+ turnPromise = null;
684
+ released = false;
685
+ /** Called by the wait tool. Blocks until turn() releases this session. */
686
+ async waitForTurn() {
687
+ if (this.released) {
688
+ this.released = false;
689
+ return;
690
+ }
691
+ if (!this.waitPromise) {
692
+ this.waitPromise = new Promise((resolve2) => {
693
+ this.waitResolve = resolve2;
694
+ });
695
+ }
696
+ await this.waitPromise;
697
+ this.waitPromise = null;
698
+ this.waitResolve = null;
699
+ this.released = false;
700
+ }
701
+ /**
702
+ * Unblock wait and wait for the platform's turn report.
703
+ * Rejects on timeout or abort; those do not produce a TurnOutcome.
704
+ */
705
+ async runTurn(options) {
706
+ if (this.turnPromise) {
707
+ throw new Error("turn() already in progress for this session");
708
+ }
709
+ if (options?.signal?.aborted) {
710
+ throw abortError(options.signal);
711
+ }
712
+ let timeoutId;
713
+ let onAbort;
714
+ const signal = options?.signal;
715
+ this.turnPromise = new Promise((resolve2, reject) => {
716
+ this.turnResolve = resolve2;
717
+ this.turnReject = reject;
718
+ if (options?.timeoutMs != null) {
719
+ timeoutId = setTimeout(() => {
720
+ this.rejectTurn(new TurnTimeoutError(`turn() timed out after ${options.timeoutMs}ms`));
721
+ }, options.timeoutMs);
722
+ }
723
+ if (signal) {
724
+ if (signal.aborted) {
725
+ this.rejectTurn(abortError(signal));
726
+ return;
727
+ }
728
+ onAbort = () => this.rejectTurn(abortError(signal));
729
+ signal.addEventListener("abort", onAbort, { once: true });
730
+ }
731
+ });
732
+ this.released = true;
733
+ const release = this.waitResolve;
734
+ this.waitResolve = null;
735
+ this.waitPromise = null;
736
+ release?.();
737
+ try {
738
+ return await this.turnPromise;
739
+ } finally {
740
+ if (timeoutId) clearTimeout(timeoutId);
741
+ if (signal && onAbort) signal.removeEventListener("abort", onAbort);
742
+ this.turnPromise = null;
743
+ this.turnResolve = null;
744
+ this.turnReject = null;
745
+ }
746
+ }
747
+ /** Platform finished one action cycle (success or second failure). */
748
+ completeTurn(outcome) {
749
+ const resolve2 = this.turnResolve;
750
+ if (!resolve2) return;
751
+ this.turnResolve = null;
752
+ this.turnReject = null;
753
+ resolve2(outcome);
754
+ }
755
+ /** Fail an in-flight turn, for example when the connection cannot be resumed. */
756
+ failTurn(error) {
757
+ this.rejectTurn(error);
758
+ }
759
+ reset() {
760
+ this.waitResolve?.();
761
+ this.completeTurn({ ok: false, error: "session stopped" });
762
+ this.waitResolve = null;
763
+ this.waitPromise = null;
764
+ this.released = false;
765
+ }
766
+ rejectTurn(error) {
767
+ const reject = this.turnReject;
768
+ if (!reject) return;
769
+ this.turnResolve = null;
770
+ this.turnReject = null;
771
+ reject(error);
772
+ }
773
+ };
774
+
775
+ // src/agent-server.ts
776
+ var DEFAULT_TURN_TIMEOUT_MS = 6e4;
777
+ var DEFAULT_PLATFORM_URL = "ws://127.0.0.1:8787";
778
+ var AgentServer = class {
779
+ options;
780
+ platformKey;
781
+ platformUrl;
782
+ logger;
783
+ connection = null;
784
+ sessions = /* @__PURE__ */ new Map();
785
+ listeners = /* @__PURE__ */ new Set();
786
+ inline;
787
+ memory = null;
788
+ started = false;
789
+ closing = false;
790
+ constructor(options = {}) {
791
+ const platformKey = options.platformKey ?? process.env.WSI_PLATFORM_KEY;
792
+ if (!options.inline && !platformKey) {
793
+ throw new Error("Set WSI_PLATFORM_KEY or pass platformKey, unless an inline platform is provided");
794
+ }
795
+ this.options = options;
796
+ this.platformKey = platformKey || "inline";
797
+ this.platformUrl = options.platformUrl ?? process.env.WSI_PLATFORM_URL ?? DEFAULT_PLATFORM_URL;
798
+ this.logger = options.logger;
799
+ this.inline = options.inline;
800
+ if (options.onEvent) this.listeners.add(options.onEvent);
801
+ }
802
+ /** Ensure the outbound platform connection. No-op for an inline platform. */
803
+ async connect() {
804
+ if (this.inline || this.started) {
805
+ this.started = true;
806
+ return;
807
+ }
808
+ this.connection = new PlatformConnection(this.platformUrl, this.platformKey, {
809
+ onEnvelope: (envelope) => this.handleEnvelope(envelope),
810
+ onDisconnect: () => this.emit({ type: "disconnected" }),
811
+ onReconnect: () => this.emit({ type: "reconnected" }),
812
+ onError: (error) => {
813
+ log(this.logger, "error", error.message);
814
+ if (error instanceof ConnectionLostError) this.failInflight(error);
815
+ },
816
+ sessions: () => [...this.sessions.values()].map((session) => ({
817
+ sessionId: session.id,
818
+ sessionToken: session.token
819
+ }))
820
+ }, this.logger);
821
+ await this.connection.connect();
822
+ this.started = true;
823
+ }
824
+ /**
825
+ * Reflect the player into tools, register a session, and return a handle.
826
+ * `turn()` on that handle lets the remote model act once.
827
+ */
828
+ async start(instance, options) {
829
+ await this.connect();
830
+ const reflected = reflectPlayer(instance);
831
+ const sessionId = (0, import_node_crypto2.randomUUID)();
832
+ const sessionToken = (0, import_node_crypto2.randomUUID)();
833
+ const gate = new TurnGate();
834
+ const name = options?.name ?? instance.constructor?.name ?? "player";
835
+ const memory = this.actionMemory();
836
+ const server = createSessionMcpServer(reflected, gate, {
837
+ name: `agent-player:${name}`,
838
+ ...memory ? {
839
+ actions: {
840
+ record: (entry) => memory.record(sessionId, name, entry),
841
+ done: () => memory.done(sessionId)
842
+ }
843
+ } : {}
844
+ });
845
+ const instructions = [this.options.instructions ?? DEFAULT_AGENT_INSTRUCTIONS, options?.instructions].filter((part) => Boolean(part && part.length > 0)).join("\n\n");
846
+ const listeners = /* @__PURE__ */ new Set();
847
+ const record = {
848
+ id: sessionId,
849
+ token: sessionToken,
850
+ name,
851
+ gate,
852
+ server,
853
+ transport: void 0,
854
+ listeners
855
+ };
856
+ const onTurnResult = (result) => {
857
+ gate.completeTurn(result);
858
+ };
859
+ const onEvent = (event) => this.emit(event);
860
+ if (this.inline) {
861
+ const [serverTransport, clientTransport] = import_inMemory.InMemoryTransport.createLinkedPair();
862
+ await server.connect(serverTransport);
863
+ record.transport = serverTransport;
864
+ this.sessions.set(sessionId, record);
865
+ await this.inline.startSession({
866
+ sessionId,
867
+ sessionToken,
868
+ name,
869
+ tools: reflected.tools,
870
+ instructions,
871
+ ...options?.goal ? { goal: options.goal } : {},
872
+ ...this.logger ? { logger: this.logger } : {},
873
+ transport: clientTransport,
874
+ onTurnResult,
875
+ onEvent
876
+ });
877
+ } else {
878
+ if (!this.connection) throw new Error("Not connected");
879
+ const mux = this.connection.createTransport(sessionId);
880
+ await server.connect(mux);
881
+ record.transport = mux;
882
+ record.mux = mux;
883
+ this.sessions.set(sessionId, record);
884
+ const started = this.connection.waitForSessionEvent(sessionId, "start");
885
+ this.connection.send({
886
+ type: "start_session",
887
+ sessionId,
888
+ sessionToken,
889
+ name,
890
+ tools: reflected.tools,
891
+ instructions,
892
+ ...options?.goal ? { goal: options.goal } : {}
893
+ });
894
+ await started;
895
+ }
896
+ return {
897
+ sessionId,
898
+ turn: (turnOptions) => this.turn(sessionId, turnOptions),
899
+ stop: () => this.stopSession(sessionId),
900
+ on: (listener) => {
901
+ listeners.add(listener);
902
+ return () => listeners.delete(listener);
903
+ }
904
+ };
905
+ }
906
+ /** Subscribe to every session plus connection events. Returns unsubscribe. */
907
+ on(listener) {
908
+ this.listeners.add(listener);
909
+ return () => this.listeners.delete(listener);
910
+ }
911
+ async turn(sessionId, options) {
912
+ const session = this.sessions.get(sessionId);
913
+ if (!session) throw new Error(`Unknown session ${sessionId}`);
914
+ const timeoutMs = options?.timeoutMs ?? this.options.turnTimeoutMs ?? DEFAULT_TURN_TIMEOUT_MS;
915
+ let outcome;
916
+ try {
917
+ outcome = await session.gate.runTurn({
918
+ timeoutMs,
919
+ ...options?.signal ? { signal: options.signal } : {}
920
+ });
921
+ } catch (error) {
922
+ if (error instanceof ConnectionLostError) throw error;
923
+ throw error;
924
+ }
925
+ if (!outcome.ok) {
926
+ if (outcome.connectionLost) {
927
+ throw new ConnectionLostError(outcome.error ?? "Connection to the platform was lost");
928
+ }
929
+ throw new TurnFailedError(outcome.error ?? "turn failed", outcome.attemptDetails ?? []);
930
+ }
931
+ return {
932
+ action: outcome.action ?? "unknown",
933
+ args: outcome.args ?? {},
934
+ result: outcome.result,
935
+ attempts: outcome.attempts ?? 1,
936
+ usage: outcome.usage ?? { promptTokens: 0, completionTokens: 0 }
937
+ };
938
+ }
939
+ emit(event) {
940
+ for (const listener of this.listeners) listener(event);
941
+ if (event.type === "disconnected" || event.type === "reconnected") return;
942
+ const session = this.sessions.get(event.sessionId);
943
+ if (!session) return;
944
+ for (const listener of session.listeners) listener(event);
945
+ }
946
+ failInflight(error) {
947
+ for (const session of this.sessions.values()) {
948
+ session.gate.failTurn(error);
949
+ }
950
+ }
951
+ handleEnvelope(envelope) {
952
+ switch (envelope.type) {
953
+ case "mcp": {
954
+ const session = this.sessions.get(envelope.sessionId);
955
+ session?.mux?.receive(envelope.message);
956
+ return;
957
+ }
958
+ case "turn_result": {
959
+ const session = this.sessions.get(envelope.sessionId);
960
+ session?.gate.completeTurn(envelope);
961
+ return;
962
+ }
963
+ case "session_event": {
964
+ if (envelope.event === "action") {
965
+ this.emit({
966
+ type: "action",
967
+ sessionId: envelope.sessionId,
968
+ action: envelope.action ?? "unknown",
969
+ args: envelope.args ?? {}
970
+ });
971
+ return;
972
+ }
973
+ if (envelope.event === "retry") {
974
+ this.emit({
975
+ type: "retry",
976
+ sessionId: envelope.sessionId,
977
+ action: envelope.action ?? "unknown",
978
+ args: envelope.args ?? {},
979
+ error: envelope.error ?? "action failed"
980
+ });
981
+ return;
982
+ }
983
+ this.emit({
984
+ type: "turnFailed",
985
+ sessionId: envelope.sessionId,
986
+ error: envelope.error ?? "turn failed",
987
+ ...envelope.action ? { action: envelope.action } : {},
988
+ ...envelope.args ? { args: envelope.args } : {}
989
+ });
990
+ return;
991
+ }
992
+ case "error":
993
+ log(this.logger, "error", envelope.message);
994
+ return;
995
+ case "session_started":
996
+ case "session_stopped":
997
+ case "session_resumed":
998
+ case "resume_err":
999
+ case "hello_ok":
1000
+ case "hello_err":
1001
+ case "hello":
1002
+ case "start_session":
1003
+ case "stop_session":
1004
+ case "ping":
1005
+ case "pong":
1006
+ case "resume_session":
1007
+ return;
1008
+ default: {
1009
+ assertNever(envelope);
1010
+ }
1011
+ }
1012
+ }
1013
+ async stopSession(sessionId) {
1014
+ const session = this.sessions.get(sessionId);
1015
+ if (!session) return;
1016
+ this.sessions.delete(sessionId);
1017
+ session.gate.reset();
1018
+ if (this.inline) {
1019
+ await this.inline.stopSession(sessionId);
1020
+ } else if (this.connection) {
1021
+ const stopped = this.connection.waitForSessionEvent(sessionId, "stop").catch(() => null);
1022
+ try {
1023
+ this.connection.send({ type: "stop_session", sessionId });
1024
+ } catch {
1025
+ }
1026
+ await stopped;
1027
+ }
1028
+ try {
1029
+ await session.server.close();
1030
+ } catch {
1031
+ }
1032
+ }
1033
+ /** Stop every session and close the platform connection. */
1034
+ async close() {
1035
+ if (this.closing) return;
1036
+ this.closing = true;
1037
+ const ids = [...this.sessions.keys()];
1038
+ for (const id of ids) {
1039
+ await this.stopSession(id);
1040
+ }
1041
+ if (this.inline?.close) await this.inline.close();
1042
+ if (this.connection) {
1043
+ await this.connection.close();
1044
+ this.connection = null;
1045
+ }
1046
+ this.memory?.close();
1047
+ this.memory = null;
1048
+ this.started = false;
1049
+ this.closing = false;
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
+ }
1059
+ async [Symbol.asyncDispose]() {
1060
+ await this.close();
1061
+ }
1062
+ };
1063
+ // Annotate the CommonJS export names for ESM import in node:
1064
+ 0 && (module.exports = {
1065
+ AgentServer,
1066
+ ConnectionLostError,
1067
+ PlatformAuthError,
1068
+ ProtocolVersionError,
1069
+ TurnFailedError,
1070
+ TurnTimeoutError
1071
+ });
1072
+ //# sourceMappingURL=index.cjs.map