@agent-play/sdk 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,462 @@
1
+ /**
2
+ * Domain types for sessions, journeys, players, and world structures shared by the SDK and server.
3
+ */
4
+ /** Role for a single line in the interaction log / chat stream. */
5
+ type WorldInteractionRole = "user" | "assistant" | "tool";
6
+ /**
7
+ * Payload for {@link import("./lib/remote-play-world.js").RemotePlayWorld.recordInteraction}.
8
+ *
9
+ * @property playerId - Player id returned from `addPlayer`.
10
+ * @property role - Who "spoke" the line.
11
+ * @property text - Plain text; may be truncated for display server-side.
12
+ */
13
+ type RecordInteractionInput = {
14
+ playerId: string;
15
+ role: WorldInteractionRole;
16
+ text: string;
17
+ };
18
+ /**
19
+ * Metadata for a tool whose name starts with `assist_`, shown as assist actions on the watch UI.
20
+ *
21
+ * @property parameters - Derived from Zod object `schema` when present; placeholder hints otherwise.
22
+ */
23
+ type AssistToolSpec = {
24
+ name: string;
25
+ description: string;
26
+ parameters: Record<string, unknown>;
27
+ };
28
+ /**
29
+ * Serializable shape returned by {@link import("./platforms/langchain.js").langchainRegistration} for `addPlayer`.
30
+ *
31
+ * @property type - Always `"langchain"` for this adapter.
32
+ * @property toolNames - All tool names from the agent (must include `chat_tool`).
33
+ * @property assistTools - Subset of tools with `assist_` prefix, for UI buttons.
34
+ */
35
+ type LangChainAgentRegistration = {
36
+ type: "langchain";
37
+ toolNames: string[];
38
+ assistTools?: AssistToolSpec[];
39
+ };
40
+ /** Minimal player identity in the SDK (without preview URL). */
41
+ type PlayAgentInformation = {
42
+ id: string;
43
+ name: string;
44
+ sid: string;
45
+ createdAt: Date;
46
+ updatedAt: Date;
47
+ };
48
+ /** Input fields for `addPlayer` before `agent` is attached. */
49
+ type PlatformAgentInformation = {
50
+ name: string;
51
+ type: string;
52
+ version?: string;
53
+ createdAt?: Date;
54
+ updatedAt?: Date;
55
+ };
56
+ /**
57
+ * Register a player (agent) in the world.
58
+ *
59
+ * Use **`langchainRegistration(agent)`** for `agent` (requires a **`chat_tool`** tool; `assist_*`
60
+ * tools are indexed for the watch UI).
61
+ *
62
+ * **`apiKey`** is set on **`RemotePlayWorld`** options, not here. With a registered-agent
63
+ * repository, you may pass **`agentId`** from **`agent-play create`**, or omit it: the server
64
+ * matches an existing agent by **`name`** plus **`agent.toolNames`**, or creates a registered
65
+ * agent when under the account limit. Without Redis, omit **`apiKey`** and **`agentId`**.
66
+ */
67
+ type AddPlayerInput = PlatformAgentInformation & {
68
+ /** Registration from {@link import("./platforms/langchain.js").langchainRegistration}. */
69
+ agent: LangChainAgentRegistration;
70
+ /** Optional explicit registered agent id when using Redis repository. */
71
+ agentId?: string;
72
+ };
73
+ /** Zone counter event surfaced on snapshots and signals. */
74
+ type ZoneEventInfo = {
75
+ zoneCount: number;
76
+ flagged?: boolean;
77
+ at: string;
78
+ };
79
+ /** Yield counter event surfaced on snapshots and signals. */
80
+ type YieldEventInfo = {
81
+ yieldCount: number;
82
+ at: string;
83
+ };
84
+ /** Kind of world structure tile (home base, tool pad, API, etc.). */
85
+ type WorldStructureKind = "home" | "tool" | "api" | "database" | "model";
86
+ /**
87
+ * One placed structure on the world map for a player.
88
+ *
89
+ * @property id - Stable id (often derived from tool name / layout).
90
+ * @property kind - Visual category.
91
+ * @property x, y - Coordinates in world grid units.
92
+ * @property toolName - When kind is tool-related, the tool name.
93
+ * @property label - Optional human label for the canvas.
94
+ */
95
+ type WorldStructure = {
96
+ id: string;
97
+ kind: WorldStructureKind;
98
+ x: number;
99
+ y: number;
100
+ toolName?: string;
101
+ label?: string;
102
+ };
103
+ /** Result of `addPlayer` including watch URL and laid-out structures. */
104
+ type RegisteredPlayer = PlayAgentInformation & {
105
+ previewUrl: string;
106
+ structures: WorldStructure[];
107
+ };
108
+ /** First step of a journey: user message origin. */
109
+ type OriginJourneyStep = {
110
+ type: "origin";
111
+ content: string;
112
+ messageId: string;
113
+ };
114
+ /** Middle step: tool invocation on the map. */
115
+ type StructureJourneyStep = {
116
+ type: "structure";
117
+ toolName: string;
118
+ toolCallId: string;
119
+ args: Record<string, unknown>;
120
+ result?: string;
121
+ };
122
+ /** Final step: assistant reply. */
123
+ type DestinationJourneyStep = {
124
+ type: "destination";
125
+ content: string;
126
+ messageId: string;
127
+ };
128
+ /** Union of journey step shapes. */
129
+ type JourneyStep = OriginJourneyStep | StructureJourneyStep | DestinationJourneyStep;
130
+ /**
131
+ * Ordered journey with timestamps; sent to the server via `recordJourney`.
132
+ *
133
+ * @property steps - Ordered path from origin through structures to destination.
134
+ * @property startedAt, completedAt - Wall times for the run (client or server).
135
+ */
136
+ type Journey = {
137
+ steps: JourneyStep[];
138
+ startedAt: Date;
139
+ completedAt: Date;
140
+ };
141
+ /** Journey step after server assigns coordinates (and optional structure id). */
142
+ type PositionedStep = JourneyStep & {
143
+ x?: number;
144
+ y?: number;
145
+ structureId?: string;
146
+ };
147
+ /** Full snapshot row for a journey update (SSE `world:journey`). */
148
+ type WorldJourneyUpdate = {
149
+ playerId: string;
150
+ journey: Journey;
151
+ path: PositionedStep[];
152
+ structures: WorldStructure[];
153
+ };
154
+
155
+ /**
156
+ * String constants and payload shapes for SSE and in-process world events.
157
+ *
158
+ * @remarks **Emitters:** server `PlayWorld` and Redis fanout. **Consumers:** watch UI `EventSource`,
159
+ * integration tests, and any host that forwards `POST` events.
160
+ */
161
+
162
+ /** Fired when `addPlayer` completes; payload includes snapshot row for the new player. */
163
+ declare const PLAYER_ADDED_EVENT = "world:player_added";
164
+ /** Fired when structures change (sync tools, layout refresh). */
165
+ declare const WORLD_STRUCTURES_EVENT = "world:structures";
166
+ /** Fired for each new chat/interaction line. */
167
+ declare const WORLD_INTERACTION_EVENT = "world:interaction";
168
+ /** Lightweight signals (zone, yield, assist, journey metadata, etc.). */
169
+ declare const WORLD_AGENT_SIGNAL_EVENT = "world:agent_signal";
170
+ /** Full journey + path update for a player. */
171
+ declare const WORLD_JOURNEY_EVENT = "world:journey";
172
+ /**
173
+ * Payload for {@link WORLD_AGENT_SIGNAL_EVENT}.
174
+ *
175
+ * @property playerId - Target player.
176
+ * @property kind - Signal category; `journey` often carries `{ stepCount }` in `data`.
177
+ * @property data - Optional free-form metadata.
178
+ */
179
+ type WorldAgentSignalPayload = {
180
+ playerId: string;
181
+ kind: "zone" | "yield" | "assist" | "chat" | "metadata" | "journey";
182
+ data?: Record<string, unknown>;
183
+ };
184
+ /**
185
+ * Payload for {@link WORLD_INTERACTION_EVENT}.
186
+ *
187
+ * @property seq - Monotonic sequence for ordering in the UI.
188
+ */
189
+ type WorldInteractionPayload = {
190
+ playerId: string;
191
+ role: WorldInteractionRole;
192
+ text: string;
193
+ at: string;
194
+ seq: number;
195
+ };
196
+ /**
197
+ * Payload for {@link WORLD_STRUCTURES_EVENT}.
198
+ *
199
+ * @property type - Optional agent platform type string.
200
+ */
201
+ type WorldStructuresPayload = {
202
+ playerId: string;
203
+ name: string;
204
+ structures: WorldStructure[];
205
+ type?: string;
206
+ };
207
+
208
+ /**
209
+ * Axis-aligned rectangle in world coordinates (grid units). Used by the server to clamp paths
210
+ * and by the watch UI to clamp joystick-driven movement.
211
+ *
212
+ * @remarks **Consumers:** {@link clampWorldPosition}, {@link boundsContain}; server `PlayWorld` and
213
+ * play-ui canvas both import these helpers from `@agent-play/sdk`.
214
+ */
215
+ type WorldBounds = {
216
+ /** Inclusive minimum X. */
217
+ minX: number;
218
+ /** Inclusive minimum Y. */
219
+ minY: number;
220
+ /** Inclusive maximum X. */
221
+ maxX: number;
222
+ /** Inclusive maximum Y. */
223
+ maxY: number;
224
+ };
225
+ /**
226
+ * Clamps a point to lie inside `bounds` along both axes.
227
+ *
228
+ * @param p - Position with `x` and `y` in world units.
229
+ * @param bounds - Valid rectangle (`min` ≤ `max` per axis).
230
+ * @returns Same point if inside, otherwise clamped to the nearest edge.
231
+ *
232
+ * @remarks **Callers:** server `PlayWorld` path enrichment; play-ui joystick and preview. **Callees:** `Math.min/Math.max`.
233
+ */
234
+ declare function clampWorldPosition(p: {
235
+ x: number;
236
+ y: number;
237
+ }, bounds: WorldBounds): {
238
+ x: number;
239
+ y: number;
240
+ };
241
+ /**
242
+ * @returns Whether `p` lies inside or on the border of `bounds`.
243
+ *
244
+ * @remarks **Callers:** optional UI checks. **Callees:** none.
245
+ */
246
+ declare function boundsContain(bounds: WorldBounds, p: {
247
+ x: number;
248
+ y: number;
249
+ }): boolean;
250
+
251
+ /**
252
+ * Optional structured `console.debug` for SDK internals; gated by {@link configureAgentPlayDebug} or `AGENT_PLAY_DEBUG=1`.
253
+ */
254
+ type DebugConfigure = {
255
+ /** When set, overrides environment: `true` forces debug on, `false` forces off. */
256
+ debug?: boolean;
257
+ };
258
+ /**
259
+ * Sets whether SDK debug logging is enabled regardless of `AGENT_PLAY_DEBUG`.
260
+ *
261
+ * @param opts.debug - `true` / `false` to force; omit to clear override.
262
+ *
263
+ * @remarks **Callers:** tests and user code. **Callees:** none.
264
+ */
265
+ declare function configureAgentPlayDebug(opts: DebugConfigure): void;
266
+ /**
267
+ * Clears the in-memory override so only `AGENT_PLAY_DEBUG` applies.
268
+ *
269
+ * @remarks **Callers:** tests. **Callees:** none.
270
+ */
271
+ declare function resetAgentPlayDebug(): void;
272
+ /**
273
+ * @returns Whether debug logging should run: override wins, else `AGENT_PLAY_DEBUG === "1"`.
274
+ *
275
+ * @remarks **Callers:** {@link agentPlayDebug}. **Callees:** `process.env` read.
276
+ */
277
+ declare function isAgentPlayDebugEnabled(): boolean;
278
+ /**
279
+ * Emits `console.debug` when {@link isAgentPlayDebugEnabled} is true.
280
+ *
281
+ * @param scope - Short label (e.g. `"langchain"`).
282
+ * @param message - Human-readable message.
283
+ * @param detail - Optional object serialized by {@link safeSerialize}.
284
+ *
285
+ * @remarks **Callers:** {@link langchainRegistration} and other SDK modules. **Callees:** {@link isAgentPlayDebugEnabled}, {@link safeSerialize}.
286
+ */
287
+ declare function agentPlayDebug(scope: string, message: string, detail?: unknown): void;
288
+
289
+ /**
290
+ * Validates a LangChain-style agent exposes tools (including required `chat_tool`) and returns
291
+ * a {@link LangChainAgentRegistration} for `addPlayer`.
292
+ *
293
+ * @param agent - Return value from `createAgent` (or equivalent) with a `tools` array.
294
+ * @throws Error if tools are missing or `chat_tool` is not present.
295
+ *
296
+ * @remarks **Callers:** user code before `RemotePlayWorld.addPlayer`. **Callees:** {@link extractToolsArray},
297
+ * {@link formatMissingAgentToolsError}, {@link formatMissingChatToolError}, {@link describeTool}, {@link agentPlayDebug}.
298
+ */
299
+ declare function langchainRegistration(agent: unknown): LangChainAgentRegistration;
300
+
301
+ /** Options for {@link RemotePlayWorld}: API origin and credentials for RPC calls. */
302
+ type RemotePlayWorldOptions = {
303
+ /** Web UI base URL (no trailing slash), e.g. `https://host` or `http://127.0.0.1:3000`. */
304
+ baseUrl: string;
305
+ /** Account API key when the server uses `AgentRepository`; use a non-empty placeholder if none. */
306
+ apiKey: string;
307
+ /** Optional bearer token for authenticated routes. */
308
+ authToken?: string;
309
+ };
310
+ /**
311
+ * Return value of {@link RemotePlayWorld.hold}: a delayed promise helper for long-running processes.
312
+ */
313
+ type RemotePlayWorldHold = {
314
+ /**
315
+ * Sleeps for the given number of seconds (non-negative).
316
+ *
317
+ * @remarks **Callers:** integration scripts that must keep the Node process alive after `start()`.
318
+ * **Callees:** `setTimeout` via `Promise`.
319
+ */
320
+ for: (seconds: number) => Promise<void>;
321
+ };
322
+ /**
323
+ * HTTP client for Agent Play: starts a session, registers players, records journeys and
324
+ * interactions, and syncs structures. Designed for long-running Node processes with
325
+ * {@link RemotePlayWorld.hold `hold().for()`} to keep the process alive.
326
+ *
327
+ * @remarks **Callers:** user code and SDK examples. All public methods except `constructor` require
328
+ * {@link RemotePlayWorld.start} to have succeeded first (except `start` itself).
329
+ *
330
+ * **Protocol:** Uses `fetch` to `GET /api/agent-play/session`, `POST /api/agent-play/players`, and
331
+ * `POST /api/agent-play/sdk/rpc` with JSON body `{ op, payload }`, plus MCP registration
332
+ * `POST /api/agent-play/mcp/register`.
333
+ */
334
+ declare class RemotePlayWorld {
335
+ /** Normalized {@link RemotePlayWorldOptions.baseUrl} (no trailing slash). Used for `fetch` base. */
336
+ private readonly apiBase;
337
+ /** Trimmed account API key; sent on `addPlayer` and implied for RPC auth expectations. */
338
+ private readonly apiKey;
339
+ /** Optional bearer token merged into request headers when set. */
340
+ private readonly authToken;
341
+ /** Session id from `GET /api/agent-play/session`; `null` until {@link RemotePlayWorld.start} succeeds. */
342
+ private sid;
343
+ /** When true, {@link RemotePlayWorld.close} is a no-op and listeners already ran. */
344
+ private closed;
345
+ /** Unsubscribe callbacks registered via {@link RemotePlayWorld.onClose}. */
346
+ private readonly closeListeners;
347
+ /**
348
+ * @param options - Base URL, API key, and optional auth token.
349
+ * @throws Error if `apiKey` is missing or whitespace-only (see {@link formatMissingApiKeyError}).
350
+ *
351
+ * @remarks **Callees:** {@link formatMissingApiKeyError}, {@link normalizeBaseUrl}.
352
+ */
353
+ constructor(options: RemotePlayWorldOptions);
354
+ /**
355
+ * Registers a one-shot listener invoked when {@link RemotePlayWorld.close} runs (e.g. process shutdown).
356
+ *
357
+ * @param handler - Synchronous callback; errors are swallowed.
358
+ * @returns Unsubscribe function that removes this `handler` from the set.
359
+ *
360
+ * @remarks **Callers:** user code. **Callees:** `Set.prototype.add` / `delete`.
361
+ */
362
+ onClose(handler: () => void): () => void;
363
+ /**
364
+ * Returns a helper that sleeps for wall-clock seconds (useful for `await world.hold().for(3600)`).
365
+ *
366
+ * @remarks **Callers:** user code. **Callees:** {@link formatInvalidHoldSecondsError}.
367
+ */
368
+ hold(): RemotePlayWorldHold;
369
+ /**
370
+ * Authorization header map for requests that only need session cookie or bearer auth.
371
+ *
372
+ * @internal
373
+ * @remarks **Callers:** {@link RemotePlayWorld.start}, {@link RemotePlayWorld.addPlayer} (via headers),
374
+ * {@link RemotePlayWorld.registerMcp}, and any future GET-only calls.
375
+ */
376
+ private authHeaders;
377
+ /**
378
+ * Headers for JSON `POST` bodies (RPC, players, MCP).
379
+ *
380
+ * @internal
381
+ * @remarks **Callers:** {@link RemotePlayWorld.addPlayer}, {@link RemotePlayWorld.rpc}, {@link RemotePlayWorld.registerMcp}.
382
+ * **Callees:** {@link authHeaders}.
383
+ */
384
+ private jsonHeaders;
385
+ /**
386
+ * Creates a session and stores `sid` from `GET /api/agent-play/session`.
387
+ *
388
+ * @throws Error if the response is not OK or JSON lacks a non-empty `sid` string.
389
+ *
390
+ * @remarks **Callers:** user code. **Callees:** `fetch`, {@link isRecord}.
391
+ */
392
+ start(): Promise<void>;
393
+ /**
394
+ * Marks the client closed and invokes all {@link onClose} listeners once.
395
+ *
396
+ * @remarks **Callers:** user code. **Callees:** `Array.from` over {@link closeListeners}.
397
+ */
398
+ close(): Promise<void>;
399
+ /**
400
+ * @returns Current session id.
401
+ * @throws Error if {@link RemotePlayWorld.start} has not been called successfully.
402
+ *
403
+ * @remarks **Callers:** user code. **Callees:** none.
404
+ */
405
+ getSessionId(): string;
406
+ /**
407
+ * @returns Absolute watch URL for the session (`/agent-play/watch` on `apiBase`). Query `sid` is not appended;
408
+ * consumers append `?sid=` from {@link getSessionId} when needed.
409
+ *
410
+ * @remarks **Callers:** user code. **Callees:** `URL` constructor.
411
+ */
412
+ getPreviewUrl(): string;
413
+ /**
414
+ * Registers a player agent with the server for the current session.
415
+ *
416
+ * @param input - Name, type, `agent` registration from {@link langchainRegistration}, optional `agentId`.
417
+ * @returns Resolved player row with `previewUrl` and `structures`.
418
+ * @throws Error on HTTP errors or malformed JSON.
419
+ *
420
+ * @remarks **Callers:** user code. **Callees:** {@link getSessionId}, `fetch`, `JSON.parse`, {@link parseStructures}.
421
+ */
422
+ addPlayer(input: AddPlayerInput): Promise<RegisteredPlayer>;
423
+ /**
424
+ * Appends a chat-style interaction line for a player (RPC `recordInteraction`).
425
+ *
426
+ * @remarks **Callers:** user code. **Callees:** {@link rpc}.
427
+ */
428
+ recordInteraction(input: RecordInteractionInput): Promise<void>;
429
+ /**
430
+ * Records a full journey for a player (RPC `recordJourney`).
431
+ *
432
+ * @remarks **Callers:** user code. **Callees:** {@link rpc}.
433
+ */
434
+ recordJourney(playerId: string, journey: Journey): Promise<void>;
435
+ /**
436
+ * Re-syncs layout structures from an ordered tool name list (RPC `syncPlayerStructuresFromTools`).
437
+ *
438
+ * @remarks **Callers:** user code. **Callees:** {@link rpc}.
439
+ */
440
+ syncPlayerStructuresFromTools(playerId: string, toolNames: string[]): Promise<void>;
441
+ /**
442
+ * Registers an MCP server metadata row for the session (HTTP POST, not the same as RPC `op`).
443
+ *
444
+ * @returns New registration id string from JSON `{ id }`.
445
+ *
446
+ * @remarks **Callers:** user code. **Callees:** `fetch`, {@link getSessionId}, {@link jsonHeaders}.
447
+ */
448
+ registerMcp(options: {
449
+ name: string;
450
+ url?: string;
451
+ }): Promise<string>;
452
+ /**
453
+ * Posts `{ op, payload }` to `/api/agent-play/sdk/rpc?sid=...`.
454
+ *
455
+ * @internal
456
+ * @remarks **Callers:** {@link recordInteraction}, {@link recordJourney}, {@link syncPlayerStructuresFromTools}.
457
+ * **Callees:** {@link getSessionId}, `fetch`, {@link jsonHeaders}.
458
+ */
459
+ private rpc;
460
+ }
461
+
462
+ export { type AddPlayerInput, type AssistToolSpec, type DestinationJourneyStep, type Journey, type JourneyStep, type LangChainAgentRegistration, type OriginJourneyStep, PLAYER_ADDED_EVENT, type PlatformAgentInformation, type PlayAgentInformation, type PositionedStep, type RecordInteractionInput, type RegisteredPlayer, RemotePlayWorld, type RemotePlayWorldHold, type RemotePlayWorldOptions, type StructureJourneyStep, WORLD_AGENT_SIGNAL_EVENT, WORLD_INTERACTION_EVENT, WORLD_JOURNEY_EVENT, WORLD_STRUCTURES_EVENT, type WorldAgentSignalPayload, type WorldBounds, type WorldInteractionPayload, type WorldInteractionRole, type WorldJourneyUpdate, type WorldStructure, type WorldStructureKind, type WorldStructuresPayload, type YieldEventInfo, type ZoneEventInfo, agentPlayDebug, boundsContain, clampWorldPosition, configureAgentPlayDebug, isAgentPlayDebugEnabled, langchainRegistration, resetAgentPlayDebug };
package/dist/index.js ADDED
@@ -0,0 +1,485 @@
1
+ // src/world-events.ts
2
+ var PLAYER_ADDED_EVENT = "world:player_added";
3
+ var WORLD_STRUCTURES_EVENT = "world:structures";
4
+ var WORLD_INTERACTION_EVENT = "world:interaction";
5
+ var WORLD_AGENT_SIGNAL_EVENT = "world:agent_signal";
6
+ var WORLD_JOURNEY_EVENT = "world:journey";
7
+
8
+ // src/lib/world-bounds.ts
9
+ function clampWorldPosition(p, bounds) {
10
+ return {
11
+ x: Math.min(Math.max(p.x, bounds.minX), bounds.maxX),
12
+ y: Math.min(Math.max(p.y, bounds.minY), bounds.maxY)
13
+ };
14
+ }
15
+ function boundsContain(bounds, p) {
16
+ return p.x >= bounds.minX && p.x <= bounds.maxX && p.y >= bounds.minY && p.y <= bounds.maxY;
17
+ }
18
+
19
+ // src/lib/agent-play-debug.ts
20
+ var configuredDebug;
21
+ function configureAgentPlayDebug(opts) {
22
+ configuredDebug = opts.debug ?? void 0;
23
+ }
24
+ function resetAgentPlayDebug() {
25
+ configuredDebug = void 0;
26
+ }
27
+ function isAgentPlayDebugEnabled() {
28
+ if (configuredDebug === false) return false;
29
+ if (configuredDebug === true) return true;
30
+ return process.env.AGENT_PLAY_DEBUG === "1";
31
+ }
32
+ var MAX_JSON_LENGTH = 2e3;
33
+ function safeSerialize(detail) {
34
+ if (detail === void 0) return "";
35
+ try {
36
+ const seen = /* @__PURE__ */ new WeakSet();
37
+ const json = JSON.stringify(detail, (_k, v) => {
38
+ if (typeof v === "object" && v !== null) {
39
+ if (seen.has(v)) return "[Circular]";
40
+ seen.add(v);
41
+ }
42
+ if (typeof v === "bigint") return String(v);
43
+ return v;
44
+ });
45
+ if (typeof json !== "string") return String(detail);
46
+ return json.length > MAX_JSON_LENGTH ? `${json.slice(0, MAX_JSON_LENGTH)}\u2026` : json;
47
+ } catch {
48
+ return String(detail);
49
+ }
50
+ }
51
+ function agentPlayDebug(scope, message, detail) {
52
+ if (!isAgentPlayDebugEnabled()) return;
53
+ const tail = detail === void 0 ? "" : ` ${safeSerialize(detail)}`;
54
+ console.debug(`[agent-play:${scope}] ${message}${tail}`);
55
+ }
56
+
57
+ // src/platforms/langchain.ts
58
+ var CHAT_TOOL = "chat_tool";
59
+ function formatMissingAgentToolsError() {
60
+ return [
61
+ "langchainRegistration: expected a LangChain agent with a tools array.",
62
+ "",
63
+ " Pass the object returned from createAgent({ tools: [...] }) (or equivalent) so tool names are available for the play world.",
64
+ ' The tools array must include named tools; see the separate message if "chat_tool" or assist_* tools are missing.'
65
+ ].join("\n");
66
+ }
67
+ function formatMissingChatToolError() {
68
+ return [
69
+ 'langchainRegistration: missing required tool "chat_tool".',
70
+ "",
71
+ ' Add a tool named "chat_tool" to your LangChain agent so the play world can show chat and proximity interactions.',
72
+ ' Example: tool(() => "\u2026", { name: "chat_tool", description: "\u2026", schema: z.object({ \u2026 }) })',
73
+ "",
74
+ ' Tools whose names start with "assist_" are listed as assist actions on the watch UI; give each a Zod object schema so parameters can be shown in the UI.'
75
+ ].join("\n");
76
+ }
77
+ function parametersFromSchema(schema) {
78
+ if (schema === null || typeof schema !== "object") {
79
+ return {};
80
+ }
81
+ const z = schema;
82
+ if (typeof z._def?.shape !== "function") {
83
+ return { _note: "Pass a Zod object schema on each tool for parameter hints in the watch UI." };
84
+ }
85
+ const shape = z._def.shape();
86
+ const out = {};
87
+ for (const key of Object.keys(shape)) {
88
+ out[key] = { field: key };
89
+ }
90
+ return out;
91
+ }
92
+ function describeTool(t) {
93
+ return {
94
+ name: t.name,
95
+ description: typeof t.description === "string" && t.description.length > 0 ? t.description : t.name,
96
+ parameters: parametersFromSchema(t.schema)
97
+ };
98
+ }
99
+ function extractToolsArray(agent) {
100
+ if (typeof agent !== "object" || agent === null) {
101
+ return null;
102
+ }
103
+ const a = agent;
104
+ if (Array.isArray(a.tools)) {
105
+ return a.tools;
106
+ }
107
+ if (a.options !== void 0 && typeof a.options === "object" && a.options !== null && "tools" in a.options && Array.isArray(a.options.tools)) {
108
+ return a.options.tools;
109
+ }
110
+ return null;
111
+ }
112
+ function langchainRegistration(agent) {
113
+ const rawTools = extractToolsArray(agent);
114
+ if (rawTools === null) {
115
+ throw new Error(formatMissingAgentToolsError());
116
+ }
117
+ const tools = rawTools;
118
+ const names = tools.map((x) => x.name);
119
+ if (!names.includes(CHAT_TOOL)) {
120
+ throw new Error(formatMissingChatToolError());
121
+ }
122
+ const assistTools = tools.filter((t) => t.name.startsWith("assist_")).map((t) => describeTool(t));
123
+ agentPlayDebug("langchain", "langchainRegistration", {
124
+ toolCount: names.length,
125
+ assistCount: assistTools.length
126
+ });
127
+ return {
128
+ type: "langchain",
129
+ toolNames: names,
130
+ assistTools
131
+ };
132
+ }
133
+
134
+ // src/lib/remote-play-world.ts
135
+ function formatMissingApiKeyError() {
136
+ return [
137
+ "RemotePlayWorld: options.apiKey is required.",
138
+ "",
139
+ " Register an agent with `agent-play create` (after `agent-play login`) and use the printed API key.",
140
+ " Pass it here so addPlayer can authenticate against the server repository when Redis is enabled.",
141
+ " If the server has no agent repository (local dev), still pass a non-empty placeholder string."
142
+ ].join("\n");
143
+ }
144
+ function normalizeBaseUrl(url) {
145
+ return url.replace(/\/$/, "");
146
+ }
147
+ function isRecord(v) {
148
+ return typeof v === "object" && v !== null;
149
+ }
150
+ var STRUCTURE_KINDS = [
151
+ "home",
152
+ "tool",
153
+ "api",
154
+ "database",
155
+ "model"
156
+ ];
157
+ function isWorldStructureKind(s) {
158
+ return STRUCTURE_KINDS.includes(s);
159
+ }
160
+ function parseWorldStructure(x) {
161
+ if (!isRecord(x)) return null;
162
+ if (typeof x.id !== "string" || typeof x.kind !== "string") return null;
163
+ if (!isWorldStructureKind(x.kind)) return null;
164
+ if (typeof x.x !== "number" || typeof x.y !== "number") return null;
165
+ const out = {
166
+ id: x.id,
167
+ kind: x.kind,
168
+ x: x.x,
169
+ y: x.y
170
+ };
171
+ if (typeof x.toolName === "string") out.toolName = x.toolName;
172
+ if (typeof x.label === "string") out.label = x.label;
173
+ return out;
174
+ }
175
+ function parseStructures(v) {
176
+ if (!Array.isArray(v)) return [];
177
+ const out = [];
178
+ for (const x of v) {
179
+ const row = parseWorldStructure(x);
180
+ if (row !== null) out.push(row);
181
+ }
182
+ return out;
183
+ }
184
+ function formatInvalidHoldSecondsError() {
185
+ return [
186
+ "RemotePlayWorld.hold().for(seconds): seconds must be a finite number.",
187
+ "",
188
+ " Example: await world.hold().for(3600)"
189
+ ].join("\n");
190
+ }
191
+ var RemotePlayWorld = class {
192
+ /** Normalized {@link RemotePlayWorldOptions.baseUrl} (no trailing slash). Used for `fetch` base. */
193
+ apiBase;
194
+ /** Trimmed account API key; sent on `addPlayer` and implied for RPC auth expectations. */
195
+ apiKey;
196
+ /** Optional bearer token merged into request headers when set. */
197
+ authToken;
198
+ /** Session id from `GET /api/agent-play/session`; `null` until {@link RemotePlayWorld.start} succeeds. */
199
+ sid = null;
200
+ /** When true, {@link RemotePlayWorld.close} is a no-op and listeners already ran. */
201
+ closed = false;
202
+ /** Unsubscribe callbacks registered via {@link RemotePlayWorld.onClose}. */
203
+ closeListeners = /* @__PURE__ */ new Set();
204
+ /**
205
+ * @param options - Base URL, API key, and optional auth token.
206
+ * @throws Error if `apiKey` is missing or whitespace-only (see {@link formatMissingApiKeyError}).
207
+ *
208
+ * @remarks **Callees:** {@link formatMissingApiKeyError}, {@link normalizeBaseUrl}.
209
+ */
210
+ constructor(options) {
211
+ if (typeof options.apiKey !== "string" || options.apiKey.trim().length === 0) {
212
+ throw new Error(formatMissingApiKeyError());
213
+ }
214
+ this.apiBase = normalizeBaseUrl(options.baseUrl);
215
+ this.apiKey = options.apiKey.trim();
216
+ this.authToken = options.authToken;
217
+ }
218
+ /**
219
+ * Registers a one-shot listener invoked when {@link RemotePlayWorld.close} runs (e.g. process shutdown).
220
+ *
221
+ * @param handler - Synchronous callback; errors are swallowed.
222
+ * @returns Unsubscribe function that removes this `handler` from the set.
223
+ *
224
+ * @remarks **Callers:** user code. **Callees:** `Set.prototype.add` / `delete`.
225
+ */
226
+ onClose(handler) {
227
+ this.closeListeners.add(handler);
228
+ return () => {
229
+ this.closeListeners.delete(handler);
230
+ };
231
+ }
232
+ /**
233
+ * Returns a helper that sleeps for wall-clock seconds (useful for `await world.hold().for(3600)`).
234
+ *
235
+ * @remarks **Callers:** user code. **Callees:** {@link formatInvalidHoldSecondsError}.
236
+ */
237
+ hold() {
238
+ return {
239
+ for: async (seconds) => {
240
+ if (typeof seconds !== "number" || !Number.isFinite(seconds)) {
241
+ throw new Error(formatInvalidHoldSecondsError());
242
+ }
243
+ const ms = Math.max(0, seconds) * 1e3;
244
+ await new Promise((resolve) => {
245
+ setTimeout(resolve, ms);
246
+ });
247
+ }
248
+ };
249
+ }
250
+ /**
251
+ * Authorization header map for requests that only need session cookie or bearer auth.
252
+ *
253
+ * @internal
254
+ * @remarks **Callers:** {@link RemotePlayWorld.start}, {@link RemotePlayWorld.addPlayer} (via headers),
255
+ * {@link RemotePlayWorld.registerMcp}, and any future GET-only calls.
256
+ */
257
+ authHeaders() {
258
+ if (this.authToken === void 0) return {};
259
+ return { Authorization: `Bearer ${this.authToken}` };
260
+ }
261
+ /**
262
+ * Headers for JSON `POST` bodies (RPC, players, MCP).
263
+ *
264
+ * @internal
265
+ * @remarks **Callers:** {@link RemotePlayWorld.addPlayer}, {@link RemotePlayWorld.rpc}, {@link RemotePlayWorld.registerMcp}.
266
+ * **Callees:** {@link authHeaders}.
267
+ */
268
+ jsonHeaders() {
269
+ return {
270
+ "content-type": "application/json",
271
+ ...this.authHeaders()
272
+ };
273
+ }
274
+ /**
275
+ * Creates a session and stores `sid` from `GET /api/agent-play/session`.
276
+ *
277
+ * @throws Error if the response is not OK or JSON lacks a non-empty `sid` string.
278
+ *
279
+ * @remarks **Callers:** user code. **Callees:** `fetch`, {@link isRecord}.
280
+ */
281
+ async start() {
282
+ const res = await fetch(`${this.apiBase}/api/agent-play/session`, {
283
+ headers: this.authHeaders()
284
+ });
285
+ if (!res.ok) {
286
+ throw new Error(`session failed: ${res.status}`);
287
+ }
288
+ const json = await res.json();
289
+ if (!isRecord(json) || typeof json.sid !== "string" || json.sid.length === 0) {
290
+ throw new Error("session: invalid response");
291
+ }
292
+ this.sid = json.sid;
293
+ }
294
+ /**
295
+ * Marks the client closed and invokes all {@link onClose} listeners once.
296
+ *
297
+ * @remarks **Callers:** user code. **Callees:** `Array.from` over {@link closeListeners}.
298
+ */
299
+ async close() {
300
+ if (this.closed) {
301
+ return;
302
+ }
303
+ this.closed = true;
304
+ for (const handler of Array.from(this.closeListeners)) {
305
+ try {
306
+ handler();
307
+ } catch {
308
+ }
309
+ }
310
+ }
311
+ /**
312
+ * @returns Current session id.
313
+ * @throws Error if {@link RemotePlayWorld.start} has not been called successfully.
314
+ *
315
+ * @remarks **Callers:** user code. **Callees:** none.
316
+ */
317
+ getSessionId() {
318
+ if (this.sid === null) {
319
+ throw new Error("RemotePlayWorld.start() must be called first");
320
+ }
321
+ return this.sid;
322
+ }
323
+ /**
324
+ * @returns Absolute watch URL for the session (`/agent-play/watch` on `apiBase`). Query `sid` is not appended;
325
+ * consumers append `?sid=` from {@link getSessionId} when needed.
326
+ *
327
+ * @remarks **Callers:** user code. **Callees:** `URL` constructor.
328
+ */
329
+ getPreviewUrl() {
330
+ const u = new URL("/agent-play/watch", this.apiBase);
331
+ u.search = "";
332
+ return u.toString();
333
+ }
334
+ /**
335
+ * Registers a player agent with the server for the current session.
336
+ *
337
+ * @param input - Name, type, `agent` registration from {@link langchainRegistration}, optional `agentId`.
338
+ * @returns Resolved player row with `previewUrl` and `structures`.
339
+ * @throws Error on HTTP errors or malformed JSON.
340
+ *
341
+ * @remarks **Callers:** user code. **Callees:** {@link getSessionId}, `fetch`, `JSON.parse`, {@link parseStructures}.
342
+ */
343
+ async addPlayer(input) {
344
+ const sid = this.getSessionId();
345
+ const url = `${this.apiBase}/api/agent-play/players?sid=${encodeURIComponent(sid)}`;
346
+ const res = await fetch(url, {
347
+ method: "POST",
348
+ headers: this.jsonHeaders(),
349
+ body: JSON.stringify({
350
+ name: input.name,
351
+ type: input.type,
352
+ agent: input.agent,
353
+ apiKey: this.apiKey,
354
+ agentId: input.agentId
355
+ })
356
+ });
357
+ const bodyText = await res.text();
358
+ if (!res.ok) {
359
+ throw new Error(`addPlayer: ${res.status} ${bodyText}`);
360
+ }
361
+ let json;
362
+ try {
363
+ json = JSON.parse(bodyText);
364
+ } catch {
365
+ throw new Error("addPlayer: invalid JSON");
366
+ }
367
+ if (!isRecord(json)) {
368
+ throw new Error("addPlayer: invalid response shape");
369
+ }
370
+ const playerId = json.playerId;
371
+ const previewUrl = json.previewUrl;
372
+ if (typeof playerId !== "string" || typeof previewUrl !== "string") {
373
+ throw new Error("addPlayer: missing playerId or previewUrl");
374
+ }
375
+ const structures = parseStructures(json.structures);
376
+ const now = /* @__PURE__ */ new Date();
377
+ return {
378
+ id: playerId,
379
+ name: input.name,
380
+ sid,
381
+ createdAt: now,
382
+ updatedAt: now,
383
+ previewUrl,
384
+ structures
385
+ };
386
+ }
387
+ /**
388
+ * Appends a chat-style interaction line for a player (RPC `recordInteraction`).
389
+ *
390
+ * @remarks **Callers:** user code. **Callees:** {@link rpc}.
391
+ */
392
+ async recordInteraction(input) {
393
+ await this.rpc("recordInteraction", {
394
+ playerId: input.playerId,
395
+ role: input.role,
396
+ text: input.text
397
+ });
398
+ }
399
+ /**
400
+ * Records a full journey for a player (RPC `recordJourney`).
401
+ *
402
+ * @remarks **Callers:** user code. **Callees:** {@link rpc}.
403
+ */
404
+ async recordJourney(playerId, journey) {
405
+ await this.rpc("recordJourney", { playerId, journey });
406
+ }
407
+ /**
408
+ * Re-syncs layout structures from an ordered tool name list (RPC `syncPlayerStructuresFromTools`).
409
+ *
410
+ * @remarks **Callers:** user code. **Callees:** {@link rpc}.
411
+ */
412
+ async syncPlayerStructuresFromTools(playerId, toolNames) {
413
+ await this.rpc("syncPlayerStructuresFromTools", { playerId, toolNames });
414
+ }
415
+ /**
416
+ * Registers an MCP server metadata row for the session (HTTP POST, not the same as RPC `op`).
417
+ *
418
+ * @returns New registration id string from JSON `{ id }`.
419
+ *
420
+ * @remarks **Callers:** user code. **Callees:** `fetch`, {@link getSessionId}, {@link jsonHeaders}.
421
+ */
422
+ async registerMcp(options) {
423
+ const sid = this.getSessionId();
424
+ const url = `${this.apiBase}/api/agent-play/mcp/register?sid=${encodeURIComponent(sid)}`;
425
+ const body = { name: options.name };
426
+ if (options.url !== void 0) {
427
+ body.url = options.url;
428
+ }
429
+ const res = await fetch(url, {
430
+ method: "POST",
431
+ headers: this.jsonHeaders(),
432
+ body: JSON.stringify(body)
433
+ });
434
+ const text = await res.text();
435
+ if (!res.ok) {
436
+ throw new Error(`registerMcp: ${res.status} ${text}`);
437
+ }
438
+ let json;
439
+ try {
440
+ json = JSON.parse(text);
441
+ } catch {
442
+ throw new Error("registerMcp: invalid JSON");
443
+ }
444
+ if (!isRecord(json) || typeof json.id !== "string") {
445
+ throw new Error("registerMcp: invalid response");
446
+ }
447
+ return json.id;
448
+ }
449
+ /**
450
+ * Posts `{ op, payload }` to `/api/agent-play/sdk/rpc?sid=...`.
451
+ *
452
+ * @internal
453
+ * @remarks **Callers:** {@link recordInteraction}, {@link recordJourney}, {@link syncPlayerStructuresFromTools}.
454
+ * **Callees:** {@link getSessionId}, `fetch`, {@link jsonHeaders}.
455
+ */
456
+ async rpc(op, payload) {
457
+ const sid = this.getSessionId();
458
+ const url = `${this.apiBase}/api/agent-play/sdk/rpc?sid=${encodeURIComponent(sid)}`;
459
+ const res = await fetch(url, {
460
+ method: "POST",
461
+ headers: this.jsonHeaders(),
462
+ body: JSON.stringify({ op, payload })
463
+ });
464
+ if (!res.ok) {
465
+ const t = await res.text();
466
+ throw new Error(`rpc ${op}: ${res.status} ${t}`);
467
+ }
468
+ }
469
+ };
470
+ export {
471
+ PLAYER_ADDED_EVENT,
472
+ RemotePlayWorld,
473
+ WORLD_AGENT_SIGNAL_EVENT,
474
+ WORLD_INTERACTION_EVENT,
475
+ WORLD_JOURNEY_EVENT,
476
+ WORLD_STRUCTURES_EVENT,
477
+ agentPlayDebug,
478
+ boundsContain,
479
+ clampWorldPosition,
480
+ configureAgentPlayDebug,
481
+ isAgentPlayDebugEnabled,
482
+ langchainRegistration,
483
+ resetAgentPlayDebug
484
+ };
485
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/world-events.ts","../src/lib/world-bounds.ts","../src/lib/agent-play-debug.ts","../src/platforms/langchain.ts","../src/lib/remote-play-world.ts"],"sourcesContent":["/**\n * String constants and payload shapes for SSE and in-process world events.\n *\n * @remarks **Emitters:** server `PlayWorld` and Redis fanout. **Consumers:** watch UI `EventSource`,\n * integration tests, and any host that forwards `POST` events.\n */\nimport type { WorldInteractionRole, WorldStructure } from \"./public-types.js\";\n\n/** Fired when `addPlayer` completes; payload includes snapshot row for the new player. */\nexport const PLAYER_ADDED_EVENT = \"world:player_added\";\n\n/** Fired when structures change (sync tools, layout refresh). */\nexport const WORLD_STRUCTURES_EVENT = \"world:structures\";\n\n/** Fired for each new chat/interaction line. */\nexport const WORLD_INTERACTION_EVENT = \"world:interaction\";\n\n/** Lightweight signals (zone, yield, assist, journey metadata, etc.). */\nexport const WORLD_AGENT_SIGNAL_EVENT = \"world:agent_signal\";\n\n/** Full journey + path update for a player. */\nexport const WORLD_JOURNEY_EVENT = \"world:journey\";\n\n/**\n * Payload for {@link WORLD_AGENT_SIGNAL_EVENT}.\n *\n * @property playerId - Target player.\n * @property kind - Signal category; `journey` often carries `{ stepCount }` in `data`.\n * @property data - Optional free-form metadata.\n */\nexport type WorldAgentSignalPayload = {\n playerId: string;\n kind: \"zone\" | \"yield\" | \"assist\" | \"chat\" | \"metadata\" | \"journey\";\n data?: Record<string, unknown>;\n};\n\n/**\n * Payload for {@link WORLD_INTERACTION_EVENT}.\n *\n * @property seq - Monotonic sequence for ordering in the UI.\n */\nexport type WorldInteractionPayload = {\n playerId: string;\n role: WorldInteractionRole;\n text: string;\n at: string;\n seq: number;\n};\n\n/**\n * Payload for {@link WORLD_STRUCTURES_EVENT}.\n *\n * @property type - Optional agent platform type string.\n */\nexport type WorldStructuresPayload = {\n playerId: string;\n name: string;\n structures: WorldStructure[];\n type?: string;\n};\n","/**\n * Axis-aligned rectangle in world coordinates (grid units). Used by the server to clamp paths\n * and by the watch UI to clamp joystick-driven movement.\n *\n * @remarks **Consumers:** {@link clampWorldPosition}, {@link boundsContain}; server `PlayWorld` and\n * play-ui canvas both import these helpers from `@agent-play/sdk`.\n */\nexport type WorldBounds = {\n /** Inclusive minimum X. */\n minX: number;\n /** Inclusive minimum Y. */\n minY: number;\n /** Inclusive maximum X. */\n maxX: number;\n /** Inclusive maximum Y. */\n maxY: number;\n};\n\n/**\n * Clamps a point to lie inside `bounds` along both axes.\n *\n * @param p - Position with `x` and `y` in world units.\n * @param bounds - Valid rectangle (`min` ≤ `max` per axis).\n * @returns Same point if inside, otherwise clamped to the nearest edge.\n *\n * @remarks **Callers:** server `PlayWorld` path enrichment; play-ui joystick and preview. **Callees:** `Math.min/Math.max`.\n */\nexport function clampWorldPosition(\n p: { x: number; y: number },\n bounds: WorldBounds\n): { x: number; y: number } {\n return {\n x: Math.min(Math.max(p.x, bounds.minX), bounds.maxX),\n y: Math.min(Math.max(p.y, bounds.minY), bounds.maxY),\n };\n}\n\n/**\n * @returns Whether `p` lies inside or on the border of `bounds`.\n *\n * @remarks **Callers:** optional UI checks. **Callees:** none.\n */\nexport function boundsContain(\n bounds: WorldBounds,\n p: { x: number; y: number }\n): boolean {\n return (\n p.x >= bounds.minX &&\n p.x <= bounds.maxX &&\n p.y >= bounds.minY &&\n p.y <= bounds.maxY\n );\n}\n","/**\n * Optional structured `console.debug` for SDK internals; gated by {@link configureAgentPlayDebug} or `AGENT_PLAY_DEBUG=1`.\n */\ntype DebugConfigure = {\n /** When set, overrides environment: `true` forces debug on, `false` forces off. */\n debug?: boolean;\n};\n\n/**\n * In-memory override for debug enablement (undefined = follow env only).\n *\n * @remarks **Writers:** {@link configureAgentPlayDebug}, {@link resetAgentPlayDebug}.\n * **Readers:** {@link isAgentPlayDebugEnabled}.\n */\nlet configuredDebug: boolean | undefined;\n\n/**\n * Sets whether SDK debug logging is enabled regardless of `AGENT_PLAY_DEBUG`.\n *\n * @param opts.debug - `true` / `false` to force; omit to clear override.\n *\n * @remarks **Callers:** tests and user code. **Callees:** none.\n */\nexport function configureAgentPlayDebug(opts: DebugConfigure): void {\n configuredDebug = opts.debug ?? undefined;\n}\n\n/**\n * Clears the in-memory override so only `AGENT_PLAY_DEBUG` applies.\n *\n * @remarks **Callers:** tests. **Callees:** none.\n */\nexport function resetAgentPlayDebug(): void {\n configuredDebug = undefined;\n}\n\n/**\n * @returns Whether debug logging should run: override wins, else `AGENT_PLAY_DEBUG === \"1\"`.\n *\n * @remarks **Callers:** {@link agentPlayDebug}. **Callees:** `process.env` read.\n */\nexport function isAgentPlayDebugEnabled(): boolean {\n if (configuredDebug === false) return false;\n if (configuredDebug === true) return true;\n return process.env.AGENT_PLAY_DEBUG === \"1\";\n}\n\n/** Max length of JSON detail string before truncation in {@link safeSerialize}. */\nconst MAX_JSON_LENGTH = 2000;\n\n/**\n * Serializes `detail` for log lines, truncating long JSON and handling circular refs.\n *\n * @internal\n * @remarks **Callers:** {@link agentPlayDebug} only. **Callees:** `JSON.stringify` with replacer.\n */\nfunction safeSerialize(detail: unknown): string {\n if (detail === undefined) return \"\";\n try {\n const seen = new WeakSet<object>();\n const json = JSON.stringify(detail, (_k, v: unknown) => {\n if (typeof v === \"object\" && v !== null) {\n if (seen.has(v)) return \"[Circular]\";\n seen.add(v);\n }\n if (typeof v === \"bigint\") return String(v);\n return v;\n });\n if (typeof json !== \"string\") return String(detail);\n return json.length > MAX_JSON_LENGTH\n ? `${json.slice(0, MAX_JSON_LENGTH)}…`\n : json;\n } catch {\n return String(detail);\n }\n}\n\n/**\n * Emits `console.debug` when {@link isAgentPlayDebugEnabled} is true.\n *\n * @param scope - Short label (e.g. `\"langchain\"`).\n * @param message - Human-readable message.\n * @param detail - Optional object serialized by {@link safeSerialize}.\n *\n * @remarks **Callers:** {@link langchainRegistration} and other SDK modules. **Callees:** {@link isAgentPlayDebugEnabled}, {@link safeSerialize}.\n */\nexport function agentPlayDebug(\n scope: string,\n message: string,\n detail?: unknown\n): void {\n if (!isAgentPlayDebugEnabled()) return;\n const tail =\n detail === undefined ? \"\" : ` ${safeSerialize(detail)}`;\n console.debug(`[agent-play:${scope}] ${message}${tail}`);\n}\n","/**\n * LangChain adapter: derives tool names and assist metadata from a LangChain agent for\n * {@link import(\"../public-types.js\").LangChainAgentRegistration}.\n *\n * @remarks **Primary export:** {@link langchainRegistration}. Private helpers build error strings and\n * `AssistToolSpec` rows from Zod schemas when available.\n */\nimport { agentPlayDebug } from \"../lib/agent-play-debug.js\";\nimport type { AssistToolSpec, LangChainAgentRegistration } from \"../public-types.js\";\n\n/** Required tool name enforced by the watch UI contract. */\nconst CHAT_TOOL = \"chat_tool\";\n\n/**\n * Error text when the agent has no `tools` array.\n *\n * @remarks **Callers:** {@link langchainRegistration}. **Callees:** none.\n */\nfunction formatMissingAgentToolsError(): string {\n return [\n \"langchainRegistration: expected a LangChain agent with a tools array.\",\n \"\",\n \" Pass the object returned from createAgent({ tools: [...] }) (or equivalent) so tool names are available for the play world.\",\n \" The tools array must include named tools; see the separate message if \\\"chat_tool\\\" or assist_* tools are missing.\",\n ].join(\"\\n\");\n}\n\n/**\n * Error text when `chat_tool` is missing from tool names.\n *\n * @remarks **Callers:** {@link langchainRegistration}. **Callees:** none.\n */\nfunction formatMissingChatToolError(): string {\n return [\n \"langchainRegistration: missing required tool \\\"chat_tool\\\".\",\n \"\",\n \" Add a tool named \\\"chat_tool\\\" to your LangChain agent so the play world can show chat and proximity interactions.\",\n \" Example: tool(() => \\\"…\\\", { name: \\\"chat_tool\\\", description: \\\"…\\\", schema: z.object({ … }) })\",\n \"\",\n \" Tools whose names start with \\\"assist_\\\" are listed as assist actions on the watch UI; give each a Zod object schema so parameters can be shown in the UI.\",\n ].join(\"\\n\");\n}\n\n/**\n * Best-effort parameter shape from a Zod object schema’s `shape()` for UI hints.\n *\n * @remarks **Callers:** {@link describeTool}. **Callees:** none.\n */\nfunction parametersFromSchema(schema: unknown): Record<string, unknown> {\n if (schema === null || typeof schema !== \"object\") {\n return {};\n }\n const z = schema as {\n _def?: { typeName?: string; shape?: () => Record<string, unknown> };\n };\n if (typeof z._def?.shape !== \"function\") {\n return { _note: \"Pass a Zod object schema on each tool for parameter hints in the watch UI.\" };\n }\n const shape = z._def.shape();\n const out: Record<string, unknown> = {};\n for (const key of Object.keys(shape)) {\n out[key] = { field: key };\n }\n return out;\n}\n\n/**\n * Builds an {@link AssistToolSpec} from a LangChain tool descriptor.\n *\n * @remarks **Callers:** {@link langchainRegistration} for `assist_*` tools only. **Callees:** {@link parametersFromSchema}.\n */\nfunction describeTool(t: {\n name: string;\n description?: string;\n schema?: unknown;\n}): AssistToolSpec {\n return {\n name: t.name,\n description:\n typeof t.description === \"string\" && t.description.length > 0\n ? t.description\n : t.name,\n parameters: parametersFromSchema(t.schema),\n };\n}\n\n/**\n * Reads `agent.tools` or `agent.options.tools` from common LangChain agent shapes.\n *\n * @returns The tools array, or `null` if not found.\n *\n * @remarks **Callers:** {@link langchainRegistration} only. **Callees:** none.\n */\nfunction extractToolsArray(agent: unknown): unknown[] | null {\n if (typeof agent !== \"object\" || agent === null) {\n return null;\n }\n const a = agent as {\n tools?: unknown;\n options?: { tools?: unknown };\n };\n if (Array.isArray(a.tools)) {\n return a.tools;\n }\n if (\n a.options !== undefined &&\n typeof a.options === \"object\" &&\n a.options !== null &&\n \"tools\" in a.options &&\n Array.isArray((a.options as { tools: unknown }).tools)\n ) {\n return (a.options as { tools: unknown[] }).tools;\n }\n return null;\n}\n\n/**\n * Validates a LangChain-style agent exposes tools (including required `chat_tool`) and returns\n * a {@link LangChainAgentRegistration} for `addPlayer`.\n *\n * @param agent - Return value from `createAgent` (or equivalent) with a `tools` array.\n * @throws Error if tools are missing or `chat_tool` is not present.\n *\n * @remarks **Callers:** user code before `RemotePlayWorld.addPlayer`. **Callees:** {@link extractToolsArray},\n * {@link formatMissingAgentToolsError}, {@link formatMissingChatToolError}, {@link describeTool}, {@link agentPlayDebug}.\n */\nexport function langchainRegistration(\n agent: unknown\n): LangChainAgentRegistration {\n const rawTools = extractToolsArray(agent);\n if (rawTools === null) {\n throw new Error(formatMissingAgentToolsError());\n }\n const tools =\n rawTools as readonly { name: string; description?: string; schema?: unknown }[];\n const names = tools.map((x) => x.name);\n if (!names.includes(CHAT_TOOL)) {\n throw new Error(formatMissingChatToolError());\n }\n const assistTools = tools\n .filter((t) => t.name.startsWith(\"assist_\"))\n .map((t) => describeTool(t));\n agentPlayDebug(\"langchain\", \"langchainRegistration\", {\n toolCount: names.length,\n assistCount: assistTools.length,\n });\n return {\n type: \"langchain\",\n toolNames: names,\n assistTools,\n };\n}\n","import type {\n AddPlayerInput,\n Journey,\n RecordInteractionInput,\n RegisteredPlayer,\n WorldStructure,\n WorldStructureKind,\n} from \"../public-types.js\";\n\n/** Options for {@link RemotePlayWorld}: API origin and credentials for RPC calls. */\nexport type RemotePlayWorldOptions = {\n /** Web UI base URL (no trailing slash), e.g. `https://host` or `http://127.0.0.1:3000`. */\n baseUrl: string;\n /** Account API key when the server uses `AgentRepository`; use a non-empty placeholder if none. */\n apiKey: string;\n /** Optional bearer token for authenticated routes. */\n authToken?: string;\n};\n\n/**\n * Builds the error string thrown when {@link RemotePlayWorld}'s constructor receives an empty `apiKey`.\n *\n * @remarks **Callers:** {@link RemotePlayWorld} constructor only.\n * **Callees:** none.\n */\nfunction formatMissingApiKeyError(): string {\n return [\n 'RemotePlayWorld: options.apiKey is required.',\n \"\",\n \" Register an agent with `agent-play create` (after `agent-play login`) and use the printed API key.\",\n \" Pass it here so addPlayer can authenticate against the server repository when Redis is enabled.\",\n \" If the server has no agent repository (local dev), still pass a non-empty placeholder string.\",\n ].join(\"\\n\");\n}\n\n/**\n * Strips a single trailing slash from a base URL for consistent `fetch` URL construction.\n *\n * @remarks **Callers:** {@link RemotePlayWorld} constructor.\n * **Callees:** none.\n */\nfunction normalizeBaseUrl(url: string): string {\n return url.replace(/\\/$/, \"\");\n}\n\n/**\n * Narrowing type guard for plain object records.\n *\n * @remarks **Callers:** JSON response parsing in {@link RemotePlayWorld.start}, {@link RemotePlayWorld.addPlayer},\n * {@link RemotePlayWorld.registerMcp}, and structure parsing helpers.\n */\nfunction isRecord(v: unknown): v is Record<string, unknown> {\n return typeof v === \"object\" && v !== null;\n}\n\nconst STRUCTURE_KINDS: readonly WorldStructureKind[] = [\n \"home\",\n \"tool\",\n \"api\",\n \"database\",\n \"model\",\n];\n\n/**\n * @remarks **Callers:** {@link parseWorldStructure}.\n */\nfunction isWorldStructureKind(s: string): s is WorldStructureKind {\n return (STRUCTURE_KINDS as readonly string[]).includes(s);\n}\n\n/**\n * Parses one server JSON structure into {@link WorldStructure} or `null` if invalid.\n *\n * @remarks **Callers:** {@link parseStructures}.\n * **Callees:** {@link isRecord}, {@link isWorldStructureKind}.\n */\nfunction parseWorldStructure(x: unknown): WorldStructure | null {\n if (!isRecord(x)) return null;\n if (typeof x.id !== \"string\" || typeof x.kind !== \"string\") return null;\n if (!isWorldStructureKind(x.kind)) return null;\n if (typeof x.x !== \"number\" || typeof x.y !== \"number\") return null;\n const out: WorldStructure = {\n id: x.id,\n kind: x.kind,\n x: x.x,\n y: x.y,\n };\n if (typeof x.toolName === \"string\") out.toolName = x.toolName;\n if (typeof x.label === \"string\") out.label = x.label;\n return out;\n}\n\n/**\n * Parses `structures` JSON array from `addPlayer` response.\n *\n * @remarks **Callers:** {@link RemotePlayWorld.addPlayer}.\n * **Callees:** {@link parseWorldStructure}.\n */\nfunction parseStructures(v: unknown): WorldStructure[] {\n if (!Array.isArray(v)) return [];\n const out: WorldStructure[] = [];\n for (const x of v) {\n const row = parseWorldStructure(x);\n if (row !== null) out.push(row);\n }\n return out;\n}\n\n/**\n * Error message for invalid `seconds` in {@link RemotePlayWorld.hold}.\n *\n * @remarks **Callers:** {@link RemotePlayWorld.hold `hold().for()`} only.\n */\nfunction formatInvalidHoldSecondsError(): string {\n return [\n \"RemotePlayWorld.hold().for(seconds): seconds must be a finite number.\",\n \"\",\n \" Example: await world.hold().for(3600)\",\n ].join(\"\\n\");\n}\n\n/**\n * Return value of {@link RemotePlayWorld.hold}: a delayed promise helper for long-running processes.\n */\nexport type RemotePlayWorldHold = {\n /**\n * Sleeps for the given number of seconds (non-negative).\n *\n * @remarks **Callers:** integration scripts that must keep the Node process alive after `start()`.\n * **Callees:** `setTimeout` via `Promise`.\n */\n for: (seconds: number) => Promise<void>;\n};\n\n/**\n * HTTP client for Agent Play: starts a session, registers players, records journeys and\n * interactions, and syncs structures. Designed for long-running Node processes with\n * {@link RemotePlayWorld.hold `hold().for()`} to keep the process alive.\n *\n * @remarks **Callers:** user code and SDK examples. All public methods except `constructor` require\n * {@link RemotePlayWorld.start} to have succeeded first (except `start` itself).\n *\n * **Protocol:** Uses `fetch` to `GET /api/agent-play/session`, `POST /api/agent-play/players`, and\n * `POST /api/agent-play/sdk/rpc` with JSON body `{ op, payload }`, plus MCP registration\n * `POST /api/agent-play/mcp/register`.\n */\nexport class RemotePlayWorld {\n /** Normalized {@link RemotePlayWorldOptions.baseUrl} (no trailing slash). Used for `fetch` base. */\n private readonly apiBase: string;\n /** Trimmed account API key; sent on `addPlayer` and implied for RPC auth expectations. */\n private readonly apiKey: string;\n /** Optional bearer token merged into request headers when set. */\n private readonly authToken: string | undefined;\n /** Session id from `GET /api/agent-play/session`; `null` until {@link RemotePlayWorld.start} succeeds. */\n private sid: string | null = null;\n /** When true, {@link RemotePlayWorld.close} is a no-op and listeners already ran. */\n private closed = false;\n /** Unsubscribe callbacks registered via {@link RemotePlayWorld.onClose}. */\n private readonly closeListeners = new Set<() => void>();\n\n /**\n * @param options - Base URL, API key, and optional auth token.\n * @throws Error if `apiKey` is missing or whitespace-only (see {@link formatMissingApiKeyError}).\n *\n * @remarks **Callees:** {@link formatMissingApiKeyError}, {@link normalizeBaseUrl}.\n */\n constructor(options: RemotePlayWorldOptions) {\n if (typeof options.apiKey !== \"string\" || options.apiKey.trim().length === 0) {\n throw new Error(formatMissingApiKeyError());\n }\n this.apiBase = normalizeBaseUrl(options.baseUrl);\n this.apiKey = options.apiKey.trim();\n this.authToken = options.authToken;\n }\n\n /**\n * Registers a one-shot listener invoked when {@link RemotePlayWorld.close} runs (e.g. process shutdown).\n *\n * @param handler - Synchronous callback; errors are swallowed.\n * @returns Unsubscribe function that removes this `handler` from the set.\n *\n * @remarks **Callers:** user code. **Callees:** `Set.prototype.add` / `delete`.\n */\n onClose(handler: () => void): () => void {\n this.closeListeners.add(handler);\n return () => {\n this.closeListeners.delete(handler);\n };\n }\n\n /**\n * Returns a helper that sleeps for wall-clock seconds (useful for `await world.hold().for(3600)`).\n *\n * @remarks **Callers:** user code. **Callees:** {@link formatInvalidHoldSecondsError}.\n */\n hold(): RemotePlayWorldHold {\n return {\n for: async (seconds: number) => {\n if (typeof seconds !== \"number\" || !Number.isFinite(seconds)) {\n throw new Error(formatInvalidHoldSecondsError());\n }\n const ms = Math.max(0, seconds) * 1000;\n await new Promise<void>((resolve) => {\n setTimeout(resolve, ms);\n });\n },\n };\n }\n\n /**\n * Authorization header map for requests that only need session cookie or bearer auth.\n *\n * @internal\n * @remarks **Callers:** {@link RemotePlayWorld.start}, {@link RemotePlayWorld.addPlayer} (via headers),\n * {@link RemotePlayWorld.registerMcp}, and any future GET-only calls.\n */\n private authHeaders(): Record<string, string> {\n if (this.authToken === undefined) return {};\n return { Authorization: `Bearer ${this.authToken}` };\n }\n\n /**\n * Headers for JSON `POST` bodies (RPC, players, MCP).\n *\n * @internal\n * @remarks **Callers:** {@link RemotePlayWorld.addPlayer}, {@link RemotePlayWorld.rpc}, {@link RemotePlayWorld.registerMcp}.\n * **Callees:** {@link authHeaders}.\n */\n private jsonHeaders(): Record<string, string> {\n return {\n \"content-type\": \"application/json\",\n ...this.authHeaders(),\n };\n }\n\n /**\n * Creates a session and stores `sid` from `GET /api/agent-play/session`.\n *\n * @throws Error if the response is not OK or JSON lacks a non-empty `sid` string.\n *\n * @remarks **Callers:** user code. **Callees:** `fetch`, {@link isRecord}.\n */\n async start(): Promise<void> {\n const res = await fetch(`${this.apiBase}/api/agent-play/session`, {\n headers: this.authHeaders(),\n });\n if (!res.ok) {\n throw new Error(`session failed: ${res.status}`);\n }\n const json: unknown = await res.json();\n if (!isRecord(json) || typeof json.sid !== \"string\" || json.sid.length === 0) {\n throw new Error(\"session: invalid response\");\n }\n this.sid = json.sid;\n }\n\n /**\n * Marks the client closed and invokes all {@link onClose} listeners once.\n *\n * @remarks **Callers:** user code. **Callees:** `Array.from` over {@link closeListeners}.\n */\n async close(): Promise<void> {\n if (this.closed) {\n return;\n }\n this.closed = true;\n for (const handler of Array.from(this.closeListeners)) {\n try {\n handler();\n } catch {\n // ignore listener errors\n }\n }\n }\n\n /**\n * @returns Current session id.\n * @throws Error if {@link RemotePlayWorld.start} has not been called successfully.\n *\n * @remarks **Callers:** user code. **Callees:** none.\n */\n getSessionId(): string {\n if (this.sid === null) {\n throw new Error(\"RemotePlayWorld.start() must be called first\");\n }\n return this.sid;\n }\n\n /**\n * @returns Absolute watch URL for the session (`/agent-play/watch` on `apiBase`). Query `sid` is not appended;\n * consumers append `?sid=` from {@link getSessionId} when needed.\n *\n * @remarks **Callers:** user code. **Callees:** `URL` constructor.\n */\n getPreviewUrl(): string {\n const u = new URL(\"/agent-play/watch\", this.apiBase);\n u.search = \"\";\n return u.toString();\n }\n\n /**\n * Registers a player agent with the server for the current session.\n *\n * @param input - Name, type, `agent` registration from {@link langchainRegistration}, optional `agentId`.\n * @returns Resolved player row with `previewUrl` and `structures`.\n * @throws Error on HTTP errors or malformed JSON.\n *\n * @remarks **Callers:** user code. **Callees:** {@link getSessionId}, `fetch`, `JSON.parse`, {@link parseStructures}.\n */\n async addPlayer(input: AddPlayerInput): Promise<RegisteredPlayer> {\n const sid = this.getSessionId();\n const url = `${this.apiBase}/api/agent-play/players?sid=${encodeURIComponent(sid)}`;\n const res = await fetch(url, {\n method: \"POST\",\n headers: this.jsonHeaders(),\n body: JSON.stringify({\n name: input.name,\n type: input.type,\n agent: input.agent,\n apiKey: this.apiKey,\n agentId: input.agentId,\n }),\n });\n const bodyText = await res.text();\n if (!res.ok) {\n throw new Error(`addPlayer: ${res.status} ${bodyText}`);\n }\n let json: unknown;\n try {\n json = JSON.parse(bodyText) as unknown;\n } catch {\n throw new Error(\"addPlayer: invalid JSON\");\n }\n if (!isRecord(json)) {\n throw new Error(\"addPlayer: invalid response shape\");\n }\n const playerId = json.playerId;\n const previewUrl = json.previewUrl;\n if (typeof playerId !== \"string\" || typeof previewUrl !== \"string\") {\n throw new Error(\"addPlayer: missing playerId or previewUrl\");\n }\n const structures = parseStructures(json.structures);\n const now = new Date();\n return {\n id: playerId,\n name: input.name,\n sid,\n createdAt: now,\n updatedAt: now,\n previewUrl,\n structures,\n };\n }\n\n /**\n * Appends a chat-style interaction line for a player (RPC `recordInteraction`).\n *\n * @remarks **Callers:** user code. **Callees:** {@link rpc}.\n */\n async recordInteraction(input: RecordInteractionInput): Promise<void> {\n await this.rpc(\"recordInteraction\", {\n playerId: input.playerId,\n role: input.role,\n text: input.text,\n });\n }\n\n /**\n * Records a full journey for a player (RPC `recordJourney`).\n *\n * @remarks **Callers:** user code. **Callees:** {@link rpc}.\n */\n async recordJourney(playerId: string, journey: Journey): Promise<void> {\n await this.rpc(\"recordJourney\", { playerId, journey });\n }\n\n /**\n * Re-syncs layout structures from an ordered tool name list (RPC `syncPlayerStructuresFromTools`).\n *\n * @remarks **Callers:** user code. **Callees:** {@link rpc}.\n */\n async syncPlayerStructuresFromTools(\n playerId: string,\n toolNames: string[]\n ): Promise<void> {\n await this.rpc(\"syncPlayerStructuresFromTools\", { playerId, toolNames });\n }\n\n /**\n * Registers an MCP server metadata row for the session (HTTP POST, not the same as RPC `op`).\n *\n * @returns New registration id string from JSON `{ id }`.\n *\n * @remarks **Callers:** user code. **Callees:** `fetch`, {@link getSessionId}, {@link jsonHeaders}.\n */\n async registerMcp(options: { name: string; url?: string }): Promise<string> {\n const sid = this.getSessionId();\n const url = `${this.apiBase}/api/agent-play/mcp/register?sid=${encodeURIComponent(sid)}`;\n const body: { name: string; url?: string } = { name: options.name };\n if (options.url !== undefined) {\n body.url = options.url;\n }\n const res = await fetch(url, {\n method: \"POST\",\n headers: this.jsonHeaders(),\n body: JSON.stringify(body),\n });\n const text = await res.text();\n if (!res.ok) {\n throw new Error(`registerMcp: ${res.status} ${text}`);\n }\n let json: unknown;\n try {\n json = JSON.parse(text) as unknown;\n } catch {\n throw new Error(\"registerMcp: invalid JSON\");\n }\n if (!isRecord(json) || typeof json.id !== \"string\") {\n throw new Error(\"registerMcp: invalid response\");\n }\n return json.id;\n }\n\n /**\n * Posts `{ op, payload }` to `/api/agent-play/sdk/rpc?sid=...`.\n *\n * @internal\n * @remarks **Callers:** {@link recordInteraction}, {@link recordJourney}, {@link syncPlayerStructuresFromTools}.\n * **Callees:** {@link getSessionId}, `fetch`, {@link jsonHeaders}.\n */\n private async rpc(op: string, payload: unknown): Promise<void> {\n const sid = this.getSessionId();\n const url = `${this.apiBase}/api/agent-play/sdk/rpc?sid=${encodeURIComponent(sid)}`;\n const res = await fetch(url, {\n method: \"POST\",\n headers: this.jsonHeaders(),\n body: JSON.stringify({ op, payload }),\n });\n if (!res.ok) {\n const t = await res.text();\n throw new Error(`rpc ${op}: ${res.status} ${t}`);\n }\n }\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Example 01 — LangChain agent against the hosted web UI (session + RPC)
3
+ *
4
+ * App shape today: **@agent-play/web-ui** serves `/api/agent-play/session`, player registration,
5
+ * SDK RPC, SSE events, and the watch canvas at `/agent-play/watch`. This script uses only the
6
+ * public SDK (`RemotePlayWorld` + `langchainRegistration`); it does not embed Express or `PlayWorld`.
7
+ *
8
+ * Prerequisites
9
+ * - Start the app: `npm run dev -w @agent-play/web-ui` (from repo root). Optional: Redis for
10
+ * durable sessions; see docs for `REDIS_URL`.
11
+ * - If the server uses registered agents (Redis + repository), run `agent-play login`,
12
+ * `agent-play create-key`, then `agent-play create`, then pass **`agentId`** and set
13
+ * **`AGENT_PLAY_API_KEY`** on **`RemotePlayWorld`** (see SDK `AddPlayerInput` and
14
+ * `RemotePlayWorldOptions` JSDoc).
15
+ * - `OPENAI_API_KEY` is only needed if you extend this script to call the model; registration-only
16
+ * runs use a placeholder below.
17
+ *
18
+ * Run (from `packages/sdk` or via `npm run example` at repo root):
19
+ * `tsx -r dotenv/config examples/01-remote-web-ui-langchain.ts`
20
+ *
21
+ * Env: `AGENT_PLAY_WEB_UI_URL`, `AGENT_PLAY_API_KEY`, `AGENT_PLAY_HOLD_SECONDS` (default 3600),
22
+ * `AGENT_PLAY_AGENT_ID` when using a registered agent repository, optional `OPENAI_API_KEY`.
23
+ */
24
+
25
+ import { RemotePlayWorld, langchainRegistration } from "../src/index.js";
26
+ import { createAgent, tool } from "langchain";
27
+ import { ChatOpenAI } from "@langchain/openai";
28
+ import { z } from "zod";
29
+
30
+ const model = new ChatOpenAI({
31
+ apiKey: process.env.OPENAI_API_KEY ?? "unused-registration-only",
32
+ model: "gpt-4.1",
33
+ });
34
+
35
+ const chatTool = tool(
36
+ ({ message }: { message: string }) => `echo:${message}`,
37
+ {
38
+ name: "chat_tool",
39
+ description: "Record chat for the play world",
40
+ schema: z.object({ message: z.string() }),
41
+ }
42
+ );
43
+
44
+ const increment = tool(
45
+ ({ n }: { n: number }) => String(n + 1),
46
+ {
47
+ name: "increment",
48
+ description: "Add one to a number",
49
+ schema: z.object({ n: z.number() }),
50
+ }
51
+ );
52
+
53
+ const agent = createAgent({
54
+ name: "remote-demo",
55
+ model,
56
+ tools: [chatTool, increment],
57
+ systemPrompt: "Use increment when the user asks to bump a number.",
58
+ });
59
+
60
+ async function main() {
61
+ const base = process.env.AGENT_PLAY_WEB_UI_URL ?? "http://127.0.0.1:3000";
62
+ const apiKey = process.env.AGENT_PLAY_API_KEY ?? "dev-placeholder";
63
+ const holdSeconds = Number(process.env.AGENT_PLAY_HOLD_SECONDS ?? 3600);
64
+
65
+ const world = new RemotePlayWorld({ baseUrl: base, apiKey });
66
+ world.onClose(() => {
67
+ console.log("RemotePlayWorld closed.");
68
+ });
69
+ await world.start();
70
+
71
+ const player = await world.addPlayer({
72
+ name: "remote-demo",
73
+ type: "langchain",
74
+ agent: langchainRegistration(agent),
75
+ ...(process.env.AGENT_PLAY_AGENT_ID !== undefined &&
76
+ process.env.AGENT_PLAY_AGENT_ID.length > 0
77
+ ? { agentId: process.env.AGENT_PLAY_AGENT_ID }
78
+ : {}),
79
+ });
80
+
81
+ console.log("Open the watch UI (session is server-side; UI resolves session via API):");
82
+ console.log(player.previewUrl);
83
+ console.log(`Holding the process for ${String(holdSeconds)}s (set AGENT_PLAY_HOLD_SECONDS to change).`);
84
+
85
+ await world.hold().for(holdSeconds);
86
+ await world.close();
87
+ }
88
+
89
+ await main();
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Example 02 — Two LangChain agents as two players on one remote session
3
+ *
4
+ * Same architecture as example 01: **RemotePlayWorld** talks to **@agent-play/web-ui** over HTTP
5
+ * (`/api/agent-play/session`, `/api/agent-play/players`, `/api/agent-play/sdk/rpc`). One session
6
+ * (`sid`) holds multiple players; each `addPlayer` gets a distinct `playerId` and tool-derived
7
+ * structures.
8
+ *
9
+ * Open the printed preview URL once: both avatars share the same world and session.
10
+ *
11
+ * Prerequisites: web-ui running (`npm run dev -w @agent-play/web-ui`). With Redis-backed agents,
12
+ * run `agent-play login`, `agent-play create-key`, then `agent-play create` twice (max 2 agents
13
+ * per account), pass each **`agentId`** on **`addPlayer`**, and set **`AGENT_PLAY_API_KEY`** on
14
+ * **`RemotePlayWorld`** (one account key for both).
15
+ *
16
+ * Run: `tsx -r dotenv/config examples/02-remote-two-players-langchain.ts`
17
+ * Env: `AGENT_PLAY_WEB_UI_URL`, `AGENT_PLAY_API_KEY`, `AGENT_PLAY_HOLD_SECONDS` (default 3600),
18
+ * `AGENT_PLAY_AGENT_ID_ALPHA` / `AGENT_PLAY_AGENT_ID_BETA` when using a registered repository.
19
+ */
20
+
21
+ import { RemotePlayWorld, langchainRegistration } from "../src/index.js";
22
+ import { createAgent, tool } from "langchain";
23
+ import { ChatOpenAI } from "@langchain/openai";
24
+ import { z } from "zod";
25
+
26
+ const model = new ChatOpenAI({
27
+ apiKey: process.env.OPENAI_API_KEY ?? "unused-registration-only",
28
+ model: "gpt-4.1",
29
+ });
30
+
31
+ const chatToolAlpha = tool(
32
+ ({ message }: { message: string }) => `alpha:${message}`,
33
+ {
34
+ name: "chat_tool",
35
+ description: "Chat for alpha",
36
+ schema: z.object({ message: z.string() }),
37
+ }
38
+ );
39
+
40
+ const chatToolBeta = tool(
41
+ ({ message }: { message: string }) => `beta:${message}`,
42
+ {
43
+ name: "chat_tool",
44
+ description: "Chat for beta",
45
+ schema: z.object({ message: z.string() }),
46
+ }
47
+ );
48
+
49
+ const alphaOp = tool(
50
+ () => "alpha-ok",
51
+ {
52
+ name: "alpha_op",
53
+ description: "Alpha operation",
54
+ schema: z.object({}),
55
+ }
56
+ );
57
+
58
+ const betaOp = tool(
59
+ () => "beta-ok",
60
+ {
61
+ name: "beta_op",
62
+ description: "Beta operation",
63
+ schema: z.object({}),
64
+ }
65
+ );
66
+
67
+ const agentAlpha = createAgent({
68
+ name: "agent-alpha",
69
+ model,
70
+ tools: [chatToolAlpha, alphaOp],
71
+ systemPrompt: "Use alpha_op when the user asks for the alpha operation.",
72
+ });
73
+
74
+ const agentBeta = createAgent({
75
+ name: "agent-beta",
76
+ model,
77
+ tools: [chatToolBeta, betaOp],
78
+ systemPrompt: "Use beta_op when the user asks for the beta operation.",
79
+ });
80
+
81
+ async function main() {
82
+ const base = process.env.AGENT_PLAY_WEB_UI_URL ?? "http://127.0.0.1:3000";
83
+ const apiKey = process.env.AGENT_PLAY_API_KEY ?? "dev-placeholder";
84
+ const holdSeconds = Number(process.env.AGENT_PLAY_HOLD_SECONDS ?? 3600);
85
+
86
+ const world = new RemotePlayWorld({ baseUrl: base, apiKey });
87
+ await world.start();
88
+
89
+ const playerA = await world.addPlayer({
90
+ name: "alpha",
91
+ type: "langchain",
92
+ agent: langchainRegistration(agentAlpha),
93
+ ...(process.env.AGENT_PLAY_AGENT_ID_ALPHA !== undefined &&
94
+ process.env.AGENT_PLAY_AGENT_ID_ALPHA.length > 0
95
+ ? { agentId: process.env.AGENT_PLAY_AGENT_ID_ALPHA }
96
+ : {}),
97
+ });
98
+ const playerB = await world.addPlayer({
99
+ name: "beta",
100
+ type: "langchain",
101
+ agent: langchainRegistration(agentBeta),
102
+ ...(process.env.AGENT_PLAY_AGENT_ID_BETA !== undefined &&
103
+ process.env.AGENT_PLAY_AGENT_ID_BETA.length > 0
104
+ ? { agentId: process.env.AGENT_PLAY_AGENT_ID_BETA }
105
+ : {}),
106
+ });
107
+
108
+ console.log("Session id:", world.getSessionId());
109
+ console.log("Watch (both players):", playerA.previewUrl);
110
+ console.log("Player B id:", playerB.id);
111
+ console.log(`Holding the process for ${String(holdSeconds)}s (set AGENT_PLAY_HOLD_SECONDS to change).`);
112
+
113
+ await world.hold().for(holdSeconds);
114
+ await world.close();
115
+ }
116
+
117
+ await main();
@@ -0,0 +1,51 @@
1
+ # @agent-play/sdk examples
2
+
3
+ These scripts use the **public SDK** only: `RemotePlayWorld` (HTTP session + RPC to the app), **`langchainRegistration`**, **`hold().for()`**, and optional **`onClose`**. They do **not** embed Express or import `PlayWorld` from the server.
4
+
5
+ Run the **web UI** first so APIs exist:
6
+
7
+ ```bash
8
+ # from repository root
9
+ npm run dev -w @agent-play/web-ui
10
+ ```
11
+
12
+ With a **registered-agent** repository (**`REDIS_URL`** on the server): run **`agent-play login`**, **`agent-play create-key`** (once per account), **`agent-play create`** for each agent (up to two per account). Set **`AGENT_PLAY_API_KEY`** to the account key and pass **`AGENT_PLAY_AGENT_ID`** (and **`AGENT_PLAY_AGENT_ID_ALPHA`** / **`AGENT_PLAY_AGENT_ID_BETA`** for example 02) when calling **`addPlayer`**. Without Redis, use the examples’ placeholder API key and omit agent ids.
13
+
14
+ | Order | File | Purpose |
15
+ |------:|------|---------|
16
+ | 1 | [01-remote-web-ui-langchain.ts](./01-remote-web-ui-langchain.ts) | One LangChain registration, one player; process stays up via **`hold().for()`**. |
17
+ | 2 | [02-remote-two-players-langchain.ts](./02-remote-two-players-langchain.ts) | Two registrations, two players, same session. |
18
+
19
+ ## Environment
20
+
21
+ - `AGENT_PLAY_WEB_UI_URL` — Base URL of the running app (default `http://127.0.0.1:3000`).
22
+ - `AGENT_PLAY_API_KEY` — Account API key for **`RemotePlayWorld`** (use a dev placeholder if the server has no repository).
23
+ - `AGENT_PLAY_HOLD_SECONDS` — How long **`hold().for()`** waits (default `3600`).
24
+ - `AGENT_PLAY_AGENT_ID` / `AGENT_PLAY_AGENT_ID_ALPHA` / `AGENT_PLAY_AGENT_ID_BETA` — Registered agent ids when using Redis.
25
+ - `OPENAI_API_KEY` — Only if you extend the scripts to call the model; registration-only runs use a placeholder.
26
+ - `AGENT_PLAY_DEBUG=1` — Verbose SDK logging (see `configureAgentPlayDebug` in package exports).
27
+
28
+ ## Commands
29
+
30
+ From repo root:
31
+
32
+ ```bash
33
+ npm run example # example 01
34
+ npm run example:02 # example 02
35
+ ```
36
+
37
+ From `packages/sdk`:
38
+
39
+ ```bash
40
+ npx tsx -r dotenv/config examples/01-remote-web-ui-langchain.ts
41
+ npx tsx -r dotenv/config examples/02-remote-two-players-langchain.ts
42
+ ```
43
+
44
+ ## What the app provides
45
+
46
+ - `GET /api/agent-play/session` — Creates or resumes a session (`sid`).
47
+ - `POST /api/agent-play/players` — Registers a player; response includes a **preview URL** for `/agent-play/watch`.
48
+ - `POST /api/agent-play/sdk/rpc` — Tool sync, interactions, invoke ingestion, and `op: getSnapshot` (used by `RemotePlayWorld` and the watch UI for JSON snapshot).
49
+ - Watch UI loads snapshot via RPC + SSE (`/api/agent-play/...`) for live world state across instances when Redis is enabled.
50
+
51
+ Assist actions on the watch UI call `POST /api/agent-play/assist-tool` when **`assist_*`** tools were registered via **`langchainRegistration`**.
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@agent-play/sdk",
3
+ "type": "module",
4
+ "version": "0.0.1",
5
+ "description": "Node.js SDK to register agents, stream world state, and connect to the Agent Play web UI over HTTP.",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "examples"
17
+ ],
18
+ "author": "Isaac Williams",
19
+ "license": "MIT",
20
+ "private": false,
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "https://github.com/wilforlan/agent-play.git",
24
+ "directory": "packages/sdk"
25
+ },
26
+ "publishConfig": {
27
+ "access": "public"
28
+ },
29
+ "scripts": {
30
+ "build": "tsup",
31
+ "example": "tsx -r dotenv/config examples/01-remote-web-ui-langchain.ts",
32
+ "example:02": "tsx -r dotenv/config examples/02-remote-two-players-langchain.ts",
33
+ "lint": "eslint src",
34
+ "lint:fix": "npm run lint --fix",
35
+ "test": "vitest run"
36
+ },
37
+ "dependencies": {
38
+ "@langchain/core": "^1.1.35",
39
+ "@langchain/openai": "^1.3.0",
40
+ "dotenv": "^17.3.1",
41
+ "eventsource-client": "^1.2.0",
42
+ "langchain": "^1.2.36",
43
+ "node-fetch": "^3.3.2",
44
+ "ws": "^8.18.0",
45
+ "uuidv4": "^6.2.13"
46
+ },
47
+ "devDependencies": {
48
+ "@types/node": "^25.5.0",
49
+ "@types/ws": "^8.5.13",
50
+ "@types/express": "^4.17.21",
51
+ "@types/supertest": "^6.0.3",
52
+ "express": "^4.21.2",
53
+ "supertest": "^7.1.4",
54
+ "tsup": "^8.5.1",
55
+ "tsx": "^4.21.0",
56
+ "typescript": "^5.9.3",
57
+ "vitest": "^3.0.9"
58
+ },
59
+ "peerDependencies": {
60
+ "express": "^4.21.0 || ^5.0.0"
61
+ }
62
+ }