@agent-play/sdk 3.0.2 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/.root +1 -0
- package/dist/browser-DzS5wcaZ.d.ts +344 -0
- package/dist/browser.d.ts +1 -0
- package/dist/browser.js +27 -0
- package/dist/browser.js.map +1 -0
- package/dist/chunk-VI2MTCE7.js +276 -0
- package/dist/chunk-VI2MTCE7.js.map +1 -0
- package/dist/index.d.ts +122 -307
- package/dist/index.js +733 -255
- package/dist/index.js.map +1 -1
- package/examples/01-remote-web-ui-langchain.ts +77 -16
- package/examples/02-remote-two-players-langchain.ts +57 -20
- package/examples/README.md +6 -4
- package/package.json +10 -3
package/dist/index.js
CHANGED
|
@@ -1,20 +1,31 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MINIMUM_PLAY_WORLD_BOUNDS,
|
|
3
|
+
PLAYER_CHAIN_GENESIS_STABLE_KEY,
|
|
4
|
+
PLAYER_CHAIN_HEADER_STABLE_KEY,
|
|
5
|
+
boundsContain,
|
|
6
|
+
clampWorldPosition,
|
|
7
|
+
expandBoundsToMinimumPlayArea,
|
|
8
|
+
mergeSnapshotWithPlayerChainNode,
|
|
9
|
+
parseAgentOccupantRow,
|
|
10
|
+
parseHumanOccupantRow,
|
|
11
|
+
parseMcpOccupantRow,
|
|
12
|
+
parsePlayerChainFanoutNotify,
|
|
13
|
+
parsePlayerChainFanoutNotifyFromSsePayload,
|
|
14
|
+
parsePlayerChainNodeRpcBody,
|
|
15
|
+
sortNodeRefsForSerializedFetch
|
|
16
|
+
} from "./chunk-VI2MTCE7.js";
|
|
17
|
+
|
|
1
18
|
// src/world-events.ts
|
|
19
|
+
var SESSION_CONNECTED_EVENT = "session:connected";
|
|
20
|
+
var SESSION_INVALID_EVENT = "session:invalid";
|
|
21
|
+
var SESSION_CLOSED_EVENT = "session:closed";
|
|
22
|
+
var SESSION_SSE_OPEN_EVENT = "session:sse_open";
|
|
23
|
+
var SESSION_SSE_ERROR_EVENT = "session:sse_error";
|
|
2
24
|
var PLAYER_ADDED_EVENT = "world:player_added";
|
|
3
25
|
var WORLD_INTERACTION_EVENT = "world:interaction";
|
|
4
26
|
var WORLD_AGENT_SIGNAL_EVENT = "world:agent_signal";
|
|
5
27
|
var WORLD_JOURNEY_EVENT = "world:journey";
|
|
6
28
|
|
|
7
|
-
// src/lib/world-bounds.ts
|
|
8
|
-
function clampWorldPosition(p, bounds) {
|
|
9
|
-
return {
|
|
10
|
-
x: Math.min(Math.max(p.x, bounds.minX), bounds.maxX),
|
|
11
|
-
y: Math.min(Math.max(p.y, bounds.minY), bounds.maxY)
|
|
12
|
-
};
|
|
13
|
-
}
|
|
14
|
-
function boundsContain(bounds, p) {
|
|
15
|
-
return p.x >= bounds.minX && p.x <= bounds.maxX && p.y >= bounds.minY && p.y <= bounds.maxY;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
29
|
// src/lib/agent-play-debug.ts
|
|
19
30
|
var configuredDebug;
|
|
20
31
|
function configureAgentPlayDebug(opts) {
|
|
@@ -73,6 +84,47 @@ function formatMissingChatToolError() {
|
|
|
73
84
|
' 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.'
|
|
74
85
|
].join("\n");
|
|
75
86
|
}
|
|
87
|
+
function unwrapZodCell(cell) {
|
|
88
|
+
let current = cell;
|
|
89
|
+
for (let depth = 0; depth < 32; depth++) {
|
|
90
|
+
if (current === null || typeof current !== "object") {
|
|
91
|
+
return current;
|
|
92
|
+
}
|
|
93
|
+
const def = current._def;
|
|
94
|
+
if (!def || typeof def.typeName !== "string") {
|
|
95
|
+
return current;
|
|
96
|
+
}
|
|
97
|
+
const { typeName } = def;
|
|
98
|
+
if (typeName === "ZodOptional" || typeName === "ZodNullable" || typeName === "ZodDefault") {
|
|
99
|
+
const inner = def.innerType;
|
|
100
|
+
current = inner !== null && typeof inner === "object" ? inner : void 0;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (typeName === "ZodEffects") {
|
|
104
|
+
current = def.schema;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
return current;
|
|
108
|
+
}
|
|
109
|
+
return current;
|
|
110
|
+
}
|
|
111
|
+
function fieldTypeFromZodCell(cell) {
|
|
112
|
+
const base = unwrapZodCell(cell);
|
|
113
|
+
if (base === null || typeof base !== "object") {
|
|
114
|
+
return "string";
|
|
115
|
+
}
|
|
116
|
+
const typeName = base._def?.typeName;
|
|
117
|
+
if (typeName === "ZodNumber") {
|
|
118
|
+
return "number";
|
|
119
|
+
}
|
|
120
|
+
if (typeName === "ZodBoolean") {
|
|
121
|
+
return "boolean";
|
|
122
|
+
}
|
|
123
|
+
if (typeName === "ZodString") {
|
|
124
|
+
return "string";
|
|
125
|
+
}
|
|
126
|
+
return "string";
|
|
127
|
+
}
|
|
76
128
|
function parametersFromSchema(schema) {
|
|
77
129
|
if (schema === null || typeof schema !== "object") {
|
|
78
130
|
return {};
|
|
@@ -84,7 +136,10 @@ function parametersFromSchema(schema) {
|
|
|
84
136
|
const shape = z._def.shape();
|
|
85
137
|
const out = {};
|
|
86
138
|
for (const key of Object.keys(shape)) {
|
|
87
|
-
out[key] = {
|
|
139
|
+
out[key] = {
|
|
140
|
+
field: key,
|
|
141
|
+
fieldType: fieldTypeFromZodCell(shape[key])
|
|
142
|
+
};
|
|
88
143
|
}
|
|
89
144
|
return out;
|
|
90
145
|
}
|
|
@@ -130,229 +185,93 @@ function langchainRegistration(agent) {
|
|
|
130
185
|
};
|
|
131
186
|
}
|
|
132
187
|
|
|
133
|
-
// src/lib/
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
let platform;
|
|
149
|
-
if (typeof raw.platform === "string") {
|
|
150
|
-
platform = raw.platform;
|
|
151
|
-
} else if (typeof raw.agentType === "string") {
|
|
152
|
-
platform = raw.agentType;
|
|
153
|
-
}
|
|
154
|
-
if (platform !== void 0) {
|
|
155
|
-
return { ...base, platform };
|
|
156
|
-
}
|
|
157
|
-
return base;
|
|
158
|
-
}
|
|
159
|
-
function parseMcpOccupantRow(raw) {
|
|
160
|
-
if (typeof raw.id !== "string" || typeof raw.name !== "string") {
|
|
161
|
-
throw new Error("occupant: mcp needs id and name");
|
|
162
|
-
}
|
|
163
|
-
if (typeof raw.x !== "number" || typeof raw.y !== "number") {
|
|
164
|
-
throw new Error("occupant: mcp needs numeric x and y");
|
|
165
|
-
}
|
|
166
|
-
const base = {
|
|
167
|
-
kind: "mcp",
|
|
168
|
-
id: raw.id,
|
|
169
|
-
name: raw.name,
|
|
170
|
-
x: raw.x,
|
|
171
|
-
y: raw.y
|
|
172
|
-
};
|
|
173
|
-
if (typeof raw.url === "string") {
|
|
174
|
-
return { ...base, url: raw.url };
|
|
175
|
-
}
|
|
176
|
-
return base;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// src/lib/world-chain-keys.ts
|
|
180
|
-
var PLAYER_CHAIN_GENESIS_STABLE_KEY = "__genesis__";
|
|
181
|
-
var PLAYER_CHAIN_HEADER_STABLE_KEY = "__header__";
|
|
188
|
+
// src/lib/remote-play-world.ts
|
|
189
|
+
import { randomUUID } from "crypto";
|
|
190
|
+
import {
|
|
191
|
+
deriveNodeIdFromPassword,
|
|
192
|
+
loadAgentPlayCredentialsFileFromPathSync,
|
|
193
|
+
loadRootKey,
|
|
194
|
+
nodeCredentialsMaterialFromHumanPassphrase,
|
|
195
|
+
resolveAgentPlayCredentialsPath
|
|
196
|
+
} from "@agent-play/node-tools";
|
|
197
|
+
import { HumanMessage } from "@langchain/core/messages";
|
|
198
|
+
import {
|
|
199
|
+
INTERCOM_RESPONSE_OP,
|
|
200
|
+
parseWorldIntercomEventPayload,
|
|
201
|
+
WORLD_INTERCOM_EVENT
|
|
202
|
+
} from "@agent-play/intercom";
|
|
182
203
|
|
|
183
|
-
// src/lib/
|
|
184
|
-
function
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
function stableOccupantSortKey(occ) {
|
|
188
|
-
if (occ.kind === "agent") {
|
|
189
|
-
return `agent:${occ.agentId}`;
|
|
204
|
+
// src/lib/intercom-langchain-chat-result.ts
|
|
205
|
+
function contentToText(content) {
|
|
206
|
+
if (typeof content === "string") {
|
|
207
|
+
return content;
|
|
190
208
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
return void 0;
|
|
203
|
-
}
|
|
204
|
-
if (typeof raw.updatedAt !== "string" || raw.updatedAt.length === 0) {
|
|
205
|
-
return void 0;
|
|
206
|
-
}
|
|
207
|
-
if (!Array.isArray(raw.nodes)) {
|
|
208
|
-
return void 0;
|
|
209
|
-
}
|
|
210
|
-
const nodes = [];
|
|
211
|
-
for (const row of raw.nodes) {
|
|
212
|
-
if (!isRecord(row)) {
|
|
213
|
-
return void 0;
|
|
214
|
-
}
|
|
215
|
-
if (typeof row.stableKey !== "string" || row.stableKey.length === 0) {
|
|
216
|
-
return void 0;
|
|
217
|
-
}
|
|
218
|
-
if (typeof row.leafIndex !== "number" || !Number.isFinite(row.leafIndex)) {
|
|
219
|
-
return void 0;
|
|
220
|
-
}
|
|
221
|
-
const ref = {
|
|
222
|
-
stableKey: row.stableKey,
|
|
223
|
-
leafIndex: row.leafIndex
|
|
224
|
-
};
|
|
225
|
-
if (row.removed === true) {
|
|
226
|
-
ref.removed = true;
|
|
227
|
-
}
|
|
228
|
-
if (typeof row.updatedAt === "string" && row.updatedAt.length > 0) {
|
|
229
|
-
ref.updatedAt = row.updatedAt;
|
|
230
|
-
}
|
|
231
|
-
nodes.push(ref);
|
|
209
|
+
if (Array.isArray(content)) {
|
|
210
|
+
return content.map((block) => {
|
|
211
|
+
if (typeof block === "string") {
|
|
212
|
+
return block;
|
|
213
|
+
}
|
|
214
|
+
if (typeof block === "object" && block !== null && "text" in block) {
|
|
215
|
+
const t = block.text;
|
|
216
|
+
return typeof t === "string" ? t : "";
|
|
217
|
+
}
|
|
218
|
+
return "";
|
|
219
|
+
}).join("");
|
|
232
220
|
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
if (!isRecord(sseData)) {
|
|
237
|
-
return void 0;
|
|
221
|
+
if (content !== null && typeof content === "object" && "text" in content) {
|
|
222
|
+
const t = content.text;
|
|
223
|
+
return typeof t === "string" ? t : JSON.stringify(content);
|
|
238
224
|
}
|
|
239
|
-
return
|
|
225
|
+
return JSON.stringify(content);
|
|
240
226
|
}
|
|
241
|
-
function
|
|
242
|
-
if (
|
|
243
|
-
|
|
244
|
-
}
|
|
245
|
-
const n = json.node;
|
|
246
|
-
if (n.kind === "genesis") {
|
|
247
|
-
if (n.stableKey !== PLAYER_CHAIN_GENESIS_STABLE_KEY || typeof n.text !== "string") {
|
|
248
|
-
throw new Error("getPlayerChainNode: invalid genesis node");
|
|
249
|
-
}
|
|
250
|
-
return {
|
|
251
|
-
kind: "genesis",
|
|
252
|
-
stableKey: PLAYER_CHAIN_GENESIS_STABLE_KEY,
|
|
253
|
-
text: n.text
|
|
254
|
-
};
|
|
227
|
+
function intercomResultRecordFromLangChainInvokeOutput(output) {
|
|
228
|
+
if (output === null || typeof output !== "object") {
|
|
229
|
+
return { mode: "chat", message: String(output) };
|
|
255
230
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
231
|
+
const o = output;
|
|
232
|
+
if ("structuredResponse" in o && o.structuredResponse !== void 0) {
|
|
233
|
+
const sr = o.structuredResponse;
|
|
234
|
+
if (sr !== null && typeof sr === "object" && !Array.isArray(sr)) {
|
|
235
|
+
return { ...sr };
|
|
259
236
|
}
|
|
260
|
-
|
|
261
|
-
if (!isRecord(b)) {
|
|
262
|
-
throw new Error("getPlayerChainNode: invalid header bounds");
|
|
263
|
-
}
|
|
264
|
-
const { minX, minY, maxX, maxY } = b;
|
|
265
|
-
if (typeof minX !== "number" || typeof minY !== "number" || typeof maxX !== "number" || typeof maxY !== "number") {
|
|
266
|
-
throw new Error("getPlayerChainNode: invalid header bounds");
|
|
267
|
-
}
|
|
268
|
-
return {
|
|
269
|
-
kind: "header",
|
|
270
|
-
stableKey: PLAYER_CHAIN_HEADER_STABLE_KEY,
|
|
271
|
-
sid: n.sid,
|
|
272
|
-
bounds: { minX, minY, maxX, maxY }
|
|
273
|
-
};
|
|
274
|
-
}
|
|
275
|
-
if (n.kind !== "occupant") {
|
|
276
|
-
throw new Error("getPlayerChainNode: unknown node kind");
|
|
277
|
-
}
|
|
278
|
-
if (typeof n.stableKey !== "string" || n.stableKey.length === 0) {
|
|
279
|
-
throw new Error("getPlayerChainNode: invalid occupant stableKey");
|
|
280
|
-
}
|
|
281
|
-
if (n.removed === true) {
|
|
282
|
-
return { kind: "occupant", stableKey: n.stableKey, removed: true };
|
|
283
|
-
}
|
|
284
|
-
const occ = n.occupant;
|
|
285
|
-
if (!isRecord(occ) || occ.kind !== "agent" && occ.kind !== "mcp") {
|
|
286
|
-
throw new Error("getPlayerChainNode: invalid occupant payload");
|
|
237
|
+
return { mode: "chat", structuredResponse: sr };
|
|
287
238
|
}
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
}
|
|
296
|
-
function mergeSnapshotWithPlayerChainNode(snapshot, node) {
|
|
297
|
-
if (node.kind === "genesis") {
|
|
298
|
-
return snapshot;
|
|
299
|
-
}
|
|
300
|
-
if (node.kind === "header") {
|
|
301
|
-
return {
|
|
302
|
-
...snapshot,
|
|
303
|
-
sid: node.sid,
|
|
304
|
-
worldMap: {
|
|
305
|
-
...snapshot.worldMap,
|
|
306
|
-
bounds: node.bounds
|
|
239
|
+
const messages = o.messages;
|
|
240
|
+
if (Array.isArray(messages) && messages.length > 0) {
|
|
241
|
+
const last = messages[messages.length - 1];
|
|
242
|
+
if (last !== null && typeof last === "object" && "content" in last) {
|
|
243
|
+
const text = contentToText(last.content);
|
|
244
|
+
if (text.trim().length > 0) {
|
|
245
|
+
return { mode: "chat", message: text };
|
|
307
246
|
}
|
|
308
|
-
};
|
|
309
|
-
}
|
|
310
|
-
if (node.removed === true) {
|
|
311
|
-
return {
|
|
312
|
-
...snapshot,
|
|
313
|
-
worldMap: {
|
|
314
|
-
...snapshot.worldMap,
|
|
315
|
-
occupants: snapshot.worldMap.occupants.filter(
|
|
316
|
-
(o) => stableOccupantSortKey(o) !== node.stableKey
|
|
317
|
-
)
|
|
318
|
-
}
|
|
319
|
-
};
|
|
320
|
-
}
|
|
321
|
-
if (node.removed !== false) {
|
|
322
|
-
throw new Error("mergeSnapshotWithPlayerChainNode: invalid occupant node");
|
|
323
|
-
}
|
|
324
|
-
const occ = node.occupant;
|
|
325
|
-
const key = stableOccupantSortKey(occ);
|
|
326
|
-
const occupants = snapshot.worldMap.occupants.filter(
|
|
327
|
-
(o) => stableOccupantSortKey(o) !== key
|
|
328
|
-
);
|
|
329
|
-
return {
|
|
330
|
-
...snapshot,
|
|
331
|
-
worldMap: {
|
|
332
|
-
...snapshot.worldMap,
|
|
333
|
-
occupants: [...occupants, occ]
|
|
334
247
|
}
|
|
335
|
-
}
|
|
248
|
+
}
|
|
249
|
+
return { mode: "chat", output: o };
|
|
336
250
|
}
|
|
337
251
|
|
|
338
252
|
// src/lib/remote-play-world.ts
|
|
339
|
-
|
|
253
|
+
var PLAYER_CONNECTION_HEARTBEAT_MAX_ATTEMPTS = 10;
|
|
254
|
+
var PLAYER_CONNECTION_HEARTBEAT_RETRY_DELAY_MS = 1e4;
|
|
255
|
+
function formatMissingCredentialsError() {
|
|
340
256
|
return [
|
|
341
|
-
"RemotePlayWorld:
|
|
342
|
-
""
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
257
|
+
"RemotePlayWorld: provide nodeCredentials: { rootKey, passw },",
|
|
258
|
+
"or run agent-play create-main-node so ~/.agent-play/credentials.json exists."
|
|
259
|
+
].join(" ");
|
|
260
|
+
}
|
|
261
|
+
function formatMissingBaseUrlError() {
|
|
262
|
+
return [
|
|
263
|
+
"RemotePlayWorld: baseUrl is missing.",
|
|
264
|
+
"Provide options.baseUrl, or ensure credentials.json contains serverUrl."
|
|
265
|
+
].join(" ");
|
|
347
266
|
}
|
|
348
267
|
function normalizeBaseUrl(url) {
|
|
349
268
|
return url.replace(/\/$/, "");
|
|
350
269
|
}
|
|
351
|
-
function
|
|
270
|
+
function isRecord(v) {
|
|
352
271
|
return typeof v === "object" && v !== null;
|
|
353
272
|
}
|
|
354
273
|
function parseBounds(raw) {
|
|
355
|
-
if (!
|
|
274
|
+
if (!isRecord(raw)) {
|
|
356
275
|
throw new Error("getWorldSnapshot: worldMap.bounds must be an object");
|
|
357
276
|
}
|
|
358
277
|
const { minX, minY, maxX, maxY } = raw;
|
|
@@ -364,7 +283,7 @@ function parseBounds(raw) {
|
|
|
364
283
|
return { minX, minY, maxX, maxY };
|
|
365
284
|
}
|
|
366
285
|
function parseWorldMap(raw) {
|
|
367
|
-
if (!
|
|
286
|
+
if (!isRecord(raw)) {
|
|
368
287
|
throw new Error("getWorldSnapshot: worldMap must be an object");
|
|
369
288
|
}
|
|
370
289
|
const bounds = parseBounds(raw.bounds);
|
|
@@ -375,8 +294,10 @@ function parseWorldMap(raw) {
|
|
|
375
294
|
const occupants = [];
|
|
376
295
|
const coordKeys = /* @__PURE__ */ new Set();
|
|
377
296
|
for (const row of occ) {
|
|
378
|
-
if (!
|
|
379
|
-
throw new Error(
|
|
297
|
+
if (!isRecord(row) || row.kind !== "human" && row.kind !== "agent" && row.kind !== "mcp") {
|
|
298
|
+
throw new Error(
|
|
299
|
+
"getWorldSnapshot: each occupant must have kind human, agent, or mcp"
|
|
300
|
+
);
|
|
380
301
|
}
|
|
381
302
|
const xy = typeof row.x === "number" && typeof row.y === "number" ? `${row.x},${row.y}` : "";
|
|
382
303
|
if (xy.length === 0) {
|
|
@@ -386,7 +307,9 @@ function parseWorldMap(raw) {
|
|
|
386
307
|
throw new Error("getWorldSnapshot: duplicate world map coordinate");
|
|
387
308
|
}
|
|
388
309
|
coordKeys.add(xy);
|
|
389
|
-
if (row.kind === "
|
|
310
|
+
if (row.kind === "human") {
|
|
311
|
+
occupants.push(parseHumanOccupantRow(row));
|
|
312
|
+
} else if (row.kind === "agent") {
|
|
390
313
|
occupants.push(parseAgentOccupantRow(row));
|
|
391
314
|
} else {
|
|
392
315
|
occupants.push(parseMcpOccupantRow(row));
|
|
@@ -395,7 +318,7 @@ function parseWorldMap(raw) {
|
|
|
395
318
|
return { bounds, occupants };
|
|
396
319
|
}
|
|
397
320
|
function parseAgentPlaySnapshot(snapshot) {
|
|
398
|
-
if (!
|
|
321
|
+
if (!isRecord(snapshot) || typeof snapshot.sid !== "string") {
|
|
399
322
|
throw new Error("getWorldSnapshot: invalid snapshot");
|
|
400
323
|
}
|
|
401
324
|
const worldMap = parseWorldMap(snapshot.worldMap);
|
|
@@ -403,7 +326,7 @@ function parseAgentPlaySnapshot(snapshot) {
|
|
|
403
326
|
if ("mcpServers" in snapshot && Array.isArray(snapshot.mcpServers)) {
|
|
404
327
|
const servers = [];
|
|
405
328
|
for (const m of snapshot.mcpServers) {
|
|
406
|
-
if (!
|
|
329
|
+
if (!isRecord(m) || typeof m.id !== "string" || typeof m.name !== "string") {
|
|
407
330
|
continue;
|
|
408
331
|
}
|
|
409
332
|
const row = {
|
|
@@ -418,24 +341,24 @@ function parseAgentPlaySnapshot(snapshot) {
|
|
|
418
341
|
return out;
|
|
419
342
|
}
|
|
420
343
|
function parseRegisteredAgentSummary(raw) {
|
|
421
|
-
if (!
|
|
422
|
-
throw new Error("
|
|
344
|
+
if (!isRecord(raw)) {
|
|
345
|
+
throw new Error("registerAgent: registeredAgent missing");
|
|
423
346
|
}
|
|
424
347
|
if (typeof raw.agentId !== "string" || typeof raw.name !== "string") {
|
|
425
|
-
throw new Error("
|
|
348
|
+
throw new Error("registerAgent: registeredAgent.agentId and name required");
|
|
426
349
|
}
|
|
427
350
|
if (!Array.isArray(raw.toolNames)) {
|
|
428
|
-
throw new Error("
|
|
351
|
+
throw new Error("registerAgent: registeredAgent.toolNames must be an array");
|
|
429
352
|
}
|
|
430
353
|
const toolNames = [];
|
|
431
354
|
for (const t of raw.toolNames) {
|
|
432
355
|
if (typeof t !== "string") {
|
|
433
|
-
throw new Error("
|
|
356
|
+
throw new Error("registerAgent: registeredAgent.toolNames must be strings");
|
|
434
357
|
}
|
|
435
358
|
toolNames.push(t);
|
|
436
359
|
}
|
|
437
360
|
if (typeof raw.zoneCount !== "number" || typeof raw.yieldCount !== "number" || typeof raw.flagged !== "boolean") {
|
|
438
|
-
throw new Error("
|
|
361
|
+
throw new Error("registerAgent: registeredAgent counters invalid");
|
|
439
362
|
}
|
|
440
363
|
return {
|
|
441
364
|
agentId: raw.agentId,
|
|
@@ -453,20 +376,89 @@ function formatInvalidHoldSecondsError() {
|
|
|
453
376
|
" Example: await world.hold().for(3600)"
|
|
454
377
|
].join("\n");
|
|
455
378
|
}
|
|
379
|
+
function getSseEventName(msg) {
|
|
380
|
+
if (typeof msg !== "object" || msg === null) {
|
|
381
|
+
return void 0;
|
|
382
|
+
}
|
|
383
|
+
const m = msg;
|
|
384
|
+
if (typeof m.event === "string" && m.event.length > 0) {
|
|
385
|
+
return m.event;
|
|
386
|
+
}
|
|
387
|
+
if (typeof m.type === "string" && m.type.length > 0) {
|
|
388
|
+
return m.type;
|
|
389
|
+
}
|
|
390
|
+
return void 0;
|
|
391
|
+
}
|
|
392
|
+
function invokeLangChainChatAgent(agent, input) {
|
|
393
|
+
if (typeof agent !== "object" || agent === null) {
|
|
394
|
+
throw new Error("intercom: chat agent must be a non-null object");
|
|
395
|
+
}
|
|
396
|
+
const inv = agent.invoke;
|
|
397
|
+
if (typeof inv !== "function") {
|
|
398
|
+
throw new Error("intercom: chat agent must have invoke()");
|
|
399
|
+
}
|
|
400
|
+
return Promise.resolve(
|
|
401
|
+
inv.call(agent, input)
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
function normalizeIntercomSubscribePlayerIds(options) {
|
|
405
|
+
const raw = "playerIds" in options ? [...options.playerIds] : [options.playerId];
|
|
406
|
+
return new Set(
|
|
407
|
+
raw.map((id) => id.trim()).filter((id) => id.length > 0)
|
|
408
|
+
);
|
|
409
|
+
}
|
|
456
410
|
var RemotePlayWorld = class {
|
|
457
411
|
apiBase;
|
|
458
|
-
|
|
459
|
-
|
|
412
|
+
rootKey;
|
|
413
|
+
/** Node id derived from hashed passphrase material + root (main or agent node id). */
|
|
414
|
+
derivedNodeId;
|
|
415
|
+
/** Hex password material (`hashNodePassword` on normalized human phrase); sent as `password` for repository addAgent. */
|
|
416
|
+
password;
|
|
417
|
+
onSessionEvent;
|
|
418
|
+
transportLog;
|
|
460
419
|
sid = null;
|
|
461
420
|
closed = false;
|
|
462
421
|
closeListeners = /* @__PURE__ */ new Set();
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
422
|
+
playerConnectionInfo = /* @__PURE__ */ new Map();
|
|
423
|
+
constructor(options = {}) {
|
|
424
|
+
const creds = loadAgentPlayCredentialsFileFromPathSync(
|
|
425
|
+
resolveAgentPlayCredentialsPath()
|
|
426
|
+
);
|
|
427
|
+
const resolvedBaseUrl = options.baseUrl ?? creds?.serverUrl;
|
|
428
|
+
if (resolvedBaseUrl === void 0 || resolvedBaseUrl.trim().length === 0) {
|
|
429
|
+
throw new Error(formatMissingBaseUrlError());
|
|
430
|
+
}
|
|
431
|
+
this.apiBase = normalizeBaseUrl(resolvedBaseUrl);
|
|
432
|
+
this.onSessionEvent = options.onSessionEvent;
|
|
433
|
+
this.transportLog = options.logging === "on";
|
|
434
|
+
const nc = options.nodeCredentials ?? (creds === null ? void 0 : { rootKey: loadRootKey(), passw: creds.passw });
|
|
435
|
+
if (nc !== void 0 && typeof nc.rootKey === "string" && nc.rootKey.trim().length > 0 && typeof nc.passw === "string" && nc.passw.length > 0) {
|
|
436
|
+
this.rootKey = nc.rootKey.trim().toLowerCase();
|
|
437
|
+
const material = nodeCredentialsMaterialFromHumanPassphrase(nc.passw);
|
|
438
|
+
this.password = material;
|
|
439
|
+
this.derivedNodeId = deriveNodeIdFromPassword({
|
|
440
|
+
password: material,
|
|
441
|
+
rootKey: this.rootKey
|
|
442
|
+
});
|
|
443
|
+
return;
|
|
466
444
|
}
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
445
|
+
throw new Error(formatMissingCredentialsError());
|
|
446
|
+
}
|
|
447
|
+
logTransport(event, detail) {
|
|
448
|
+
if (!this.transportLog) {
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
console.info(`[agent-play:RemotePlayWorld] ${event}`, detail);
|
|
452
|
+
}
|
|
453
|
+
truncateForLog(value, max = 1600) {
|
|
454
|
+
return value.length <= max ? value : `${value.slice(0, max)}\u2026`;
|
|
455
|
+
}
|
|
456
|
+
emitSessionEvent(event) {
|
|
457
|
+
this.logTransport("session:event", {
|
|
458
|
+
name: event.name,
|
|
459
|
+
detail: event.detail ?? {}
|
|
460
|
+
});
|
|
461
|
+
this.onSessionEvent?.(event);
|
|
470
462
|
}
|
|
471
463
|
onClose(handler) {
|
|
472
464
|
this.closeListeners.add(handler);
|
|
@@ -488,8 +480,10 @@ var RemotePlayWorld = class {
|
|
|
488
480
|
};
|
|
489
481
|
}
|
|
490
482
|
authHeaders() {
|
|
491
|
-
|
|
492
|
-
|
|
483
|
+
return {
|
|
484
|
+
"x-node-id": this.derivedNodeId,
|
|
485
|
+
"x-node-passw": this.password
|
|
486
|
+
};
|
|
493
487
|
}
|
|
494
488
|
jsonHeaders() {
|
|
495
489
|
return {
|
|
@@ -505,7 +499,51 @@ var RemotePlayWorld = class {
|
|
|
505
499
|
}
|
|
506
500
|
return fetch(input, { ...init, headers });
|
|
507
501
|
}
|
|
508
|
-
async
|
|
502
|
+
async validateNodeIdentity(options) {
|
|
503
|
+
const body = {
|
|
504
|
+
nodeId: options.nodeId,
|
|
505
|
+
rootKey: this.rootKey
|
|
506
|
+
};
|
|
507
|
+
if (options.mainNodeId !== void 0 && options.mainNodeId.trim().length > 0) {
|
|
508
|
+
body.mainNodeId = options.mainNodeId.trim();
|
|
509
|
+
}
|
|
510
|
+
const res = await fetch(`${this.apiBase}/api/nodes/validate`, {
|
|
511
|
+
method: "POST",
|
|
512
|
+
headers: this.jsonHeaders(),
|
|
513
|
+
body: JSON.stringify(body)
|
|
514
|
+
});
|
|
515
|
+
let json;
|
|
516
|
+
try {
|
|
517
|
+
json = await res.json();
|
|
518
|
+
} catch {
|
|
519
|
+
throw new Error(`node validation failed: ${String(res.status)} invalid JSON`);
|
|
520
|
+
}
|
|
521
|
+
if (!isRecord(json) || json.ok !== true) {
|
|
522
|
+
const reason = isRecord(json) && typeof json.reason === "string" ? json.reason : `HTTP ${String(res.status)}`;
|
|
523
|
+
throw new Error(`node validation failed: ${reason}`);
|
|
524
|
+
}
|
|
525
|
+
const nodeKind = isRecord(json) && typeof json.nodeKind === "string" ? json.nodeKind : void 0;
|
|
526
|
+
agentPlayDebug("remote-play-world", "node identity validated", {
|
|
527
|
+
nodeKind,
|
|
528
|
+
derivedNodeIdPrefix: `${this.derivedNodeId.slice(0, 8)}\u2026`
|
|
529
|
+
});
|
|
530
|
+
return nodeKind !== void 0 ? { nodeKind } : {};
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* Establishes the HTTP session via `GET /api/agent-play/session`. With {@link RemotePlayWorldConnectOptions.mainNodeId},
|
|
534
|
+
* validates node identity with `POST /api/nodes/validate` first.
|
|
535
|
+
*/
|
|
536
|
+
async connect(options) {
|
|
537
|
+
const mainNodeIdOpt = options?.mainNodeId?.trim();
|
|
538
|
+
if (mainNodeIdOpt !== void 0 && mainNodeIdOpt.length > 0) {
|
|
539
|
+
const validation = await this.validateNodeIdentity({
|
|
540
|
+
nodeId: this.derivedNodeId,
|
|
541
|
+
mainNodeId: mainNodeIdOpt
|
|
542
|
+
});
|
|
543
|
+
console.info(
|
|
544
|
+
`[agent-play] Node identity validated (${validation.nodeKind ?? "unknown"}).`
|
|
545
|
+
);
|
|
546
|
+
}
|
|
509
547
|
const res = await fetch(`${this.apiBase}/api/agent-play/session`, {
|
|
510
548
|
headers: this.authHeaders()
|
|
511
549
|
});
|
|
@@ -513,16 +551,38 @@ var RemotePlayWorld = class {
|
|
|
513
551
|
throw new Error(`session failed: ${res.status}`);
|
|
514
552
|
}
|
|
515
553
|
const json = await res.json();
|
|
516
|
-
if (!
|
|
554
|
+
if (!isRecord(json) || typeof json.sid !== "string" || json.sid.length === 0) {
|
|
517
555
|
throw new Error("session: invalid response");
|
|
518
556
|
}
|
|
519
557
|
this.sid = json.sid;
|
|
558
|
+
this.logTransport("connect:session", {
|
|
559
|
+
sid: this.sid,
|
|
560
|
+
apiBase: this.apiBase
|
|
561
|
+
});
|
|
562
|
+
this.emitSessionEvent({
|
|
563
|
+
name: SESSION_CONNECTED_EVENT,
|
|
564
|
+
detail: { sid: this.sid }
|
|
565
|
+
});
|
|
520
566
|
}
|
|
521
567
|
async close() {
|
|
522
568
|
if (this.closed) {
|
|
523
569
|
return;
|
|
524
570
|
}
|
|
571
|
+
for (const [playerId, connection] of Array.from(
|
|
572
|
+
this.playerConnectionInfo.entries()
|
|
573
|
+
)) {
|
|
574
|
+
clearInterval(connection.timer);
|
|
575
|
+
try {
|
|
576
|
+
await this.disconnectPlayerConnection({
|
|
577
|
+
playerId,
|
|
578
|
+
connectionId: connection.connectionId
|
|
579
|
+
});
|
|
580
|
+
} catch {
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
this.playerConnectionInfo.clear();
|
|
525
584
|
this.closed = true;
|
|
585
|
+
this.emitSessionEvent({ name: SESSION_CLOSED_EVENT });
|
|
526
586
|
for (const handler of Array.from(this.closeListeners)) {
|
|
527
587
|
try {
|
|
528
588
|
handler();
|
|
@@ -557,7 +617,7 @@ var RemotePlayWorld = class {
|
|
|
557
617
|
} catch {
|
|
558
618
|
throw new Error("getWorldSnapshot: invalid JSON");
|
|
559
619
|
}
|
|
560
|
-
if (!
|
|
620
|
+
if (!isRecord(json) || !("snapshot" in json)) {
|
|
561
621
|
throw new Error("getWorldSnapshot: invalid response shape");
|
|
562
622
|
}
|
|
563
623
|
return parseAgentPlaySnapshot(json.snapshot);
|
|
@@ -600,29 +660,65 @@ var RemotePlayWorld = class {
|
|
|
600
660
|
const { createEventSource } = await import("eventsource-client");
|
|
601
661
|
let snapshot = await this.getWorldSnapshot();
|
|
602
662
|
callbacks.onSnapshot(snapshot);
|
|
663
|
+
const sseUrl = `${this.apiBase}/api/agent-play/events?sid=${encodeURIComponent(
|
|
664
|
+
this.getSessionId()
|
|
665
|
+
)}`;
|
|
666
|
+
this.logTransport("subscribeWorldState:sse_open", { sseUrl });
|
|
603
667
|
const source = createEventSource({
|
|
604
|
-
url:
|
|
605
|
-
this.getSessionId()
|
|
606
|
-
)}`,
|
|
668
|
+
url: sseUrl,
|
|
607
669
|
fetch: (input, init) => this.mergeAuthFetch(input, init)
|
|
608
670
|
});
|
|
609
671
|
closeSource = () => {
|
|
610
672
|
source.close();
|
|
611
673
|
};
|
|
612
674
|
for await (const msg of source) {
|
|
675
|
+
const eventType = typeof msg === "object" && msg !== null && "type" in msg && typeof msg.type === "string" ? msg.type : "(no type)";
|
|
613
676
|
if (typeof msg.data !== "string") {
|
|
677
|
+
this.logTransport("sse:worldState:skip", {
|
|
678
|
+
reason: "data_not_string",
|
|
679
|
+
eventType
|
|
680
|
+
});
|
|
614
681
|
continue;
|
|
615
682
|
}
|
|
683
|
+
this.logTransport("sse:worldState:message", {
|
|
684
|
+
eventType,
|
|
685
|
+
dataLength: msg.data.length,
|
|
686
|
+
dataPreview: this.truncateForLog(msg.data)
|
|
687
|
+
});
|
|
616
688
|
let data;
|
|
617
689
|
try {
|
|
618
690
|
data = JSON.parse(msg.data);
|
|
619
691
|
} catch {
|
|
692
|
+
this.logTransport("sse:worldState:parseJson", {
|
|
693
|
+
reason: "invalid_json",
|
|
694
|
+
eventType
|
|
695
|
+
});
|
|
620
696
|
continue;
|
|
621
697
|
}
|
|
698
|
+
if (callbacks.onIntercomEvent) {
|
|
699
|
+
try {
|
|
700
|
+
const inter = parseWorldIntercomEventPayload(data);
|
|
701
|
+
this.logTransport("sse:worldState:intercom", {
|
|
702
|
+
status: inter.status,
|
|
703
|
+
requestId: inter.requestId,
|
|
704
|
+
kind: inter.kind,
|
|
705
|
+
channelKey: inter.channelKey
|
|
706
|
+
});
|
|
707
|
+
callbacks.onIntercomEvent(inter);
|
|
708
|
+
} catch {
|
|
709
|
+
this.logTransport("sse:worldState:intercom", {
|
|
710
|
+
reason: "not_world_intercom_payload"
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
}
|
|
622
714
|
const notify = parsePlayerChainFanoutNotifyFromSsePayload(data);
|
|
623
715
|
if (notify === void 0 || notify.nodes.length === 0) {
|
|
624
716
|
continue;
|
|
625
717
|
}
|
|
718
|
+
this.logTransport("sse:worldState:playerChainNotify", {
|
|
719
|
+
nodeCount: notify.nodes.length,
|
|
720
|
+
stableKeys: notify.nodes.map((n) => n.stableKey)
|
|
721
|
+
});
|
|
626
722
|
const ordered = sortNodeRefsForSerializedFetch(notify.nodes);
|
|
627
723
|
for (const ref of ordered) {
|
|
628
724
|
const node = await this.getPlayerChainNode(ref.stableKey);
|
|
@@ -631,21 +727,43 @@ var RemotePlayWorld = class {
|
|
|
631
727
|
callbacks.onSnapshot(snapshot);
|
|
632
728
|
}
|
|
633
729
|
} catch (e) {
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
730
|
+
const err = e instanceof Error ? e : new Error(String(e));
|
|
731
|
+
this.logTransport("sse:worldState:error", {
|
|
732
|
+
message: err.message
|
|
733
|
+
});
|
|
734
|
+
callbacks.onError?.(err);
|
|
637
735
|
}
|
|
638
736
|
})();
|
|
639
737
|
return {
|
|
640
738
|
close: () => {
|
|
739
|
+
this.logTransport("subscribeWorldState:close", {});
|
|
641
740
|
closeSource?.();
|
|
642
741
|
void task;
|
|
643
742
|
}
|
|
644
743
|
};
|
|
645
744
|
}
|
|
646
|
-
|
|
745
|
+
/**
|
|
746
|
+
* Registers an automation agent using **agent node id** (`nodeId`), sent to the server as `agentId`.
|
|
747
|
+
*/
|
|
748
|
+
async addAgent(input) {
|
|
647
749
|
const sid = this.getSessionId();
|
|
750
|
+
const effectiveMainNodeId = this.derivedNodeId;
|
|
751
|
+
const validation = await this.validateNodeIdentity({
|
|
752
|
+
nodeId: input.nodeId,
|
|
753
|
+
mainNodeId: effectiveMainNodeId
|
|
754
|
+
});
|
|
755
|
+
console.info(
|
|
756
|
+
[
|
|
757
|
+
"Agent Node Connection",
|
|
758
|
+
` status : validated`,
|
|
759
|
+
` nodeId : ${input.nodeId}`,
|
|
760
|
+
` nodeKind : ${validation.nodeKind ?? "unknown"}`,
|
|
761
|
+
` mainNode : ${effectiveMainNodeId}`
|
|
762
|
+
].join("\n")
|
|
763
|
+
);
|
|
648
764
|
const url = `${this.apiBase}/api/agent-play/players?sid=${encodeURIComponent(sid)}`;
|
|
765
|
+
const connectionId = randomUUID();
|
|
766
|
+
const leaseTtlSeconds = 45;
|
|
649
767
|
const res = await fetch(url, {
|
|
650
768
|
method: "POST",
|
|
651
769
|
headers: this.jsonHeaders(),
|
|
@@ -653,29 +771,60 @@ var RemotePlayWorld = class {
|
|
|
653
771
|
name: input.name,
|
|
654
772
|
type: input.type,
|
|
655
773
|
agent: input.agent,
|
|
656
|
-
|
|
657
|
-
|
|
774
|
+
mainNodeId: effectiveMainNodeId,
|
|
775
|
+
password: this.password,
|
|
776
|
+
agentId: input.nodeId,
|
|
777
|
+
connectionId,
|
|
778
|
+
leaseTtlSeconds
|
|
658
779
|
})
|
|
659
780
|
});
|
|
660
781
|
const bodyText = await res.text();
|
|
661
782
|
if (!res.ok) {
|
|
662
|
-
throw new Error(`
|
|
783
|
+
throw new Error(`addAgent: ${res.status} ${bodyText}`);
|
|
663
784
|
}
|
|
664
785
|
let body;
|
|
665
786
|
try {
|
|
666
787
|
body = JSON.parse(bodyText);
|
|
667
788
|
} catch {
|
|
668
|
-
throw new Error("
|
|
789
|
+
throw new Error("addAgent: invalid JSON");
|
|
669
790
|
}
|
|
670
|
-
if (!
|
|
671
|
-
throw new Error("
|
|
791
|
+
if (!isRecord(body)) {
|
|
792
|
+
throw new Error("addAgent: invalid response shape");
|
|
672
793
|
}
|
|
673
794
|
const playerId = body.playerId;
|
|
674
795
|
const previewUrl = body.previewUrl;
|
|
675
796
|
if (typeof playerId !== "string" || typeof previewUrl !== "string") {
|
|
676
|
-
throw new Error("
|
|
797
|
+
throw new Error("addAgent: missing playerId or previewUrl");
|
|
677
798
|
}
|
|
678
799
|
const registeredAgent = parseRegisteredAgentSummary(body.registeredAgent);
|
|
800
|
+
const bodyConnectionId = typeof body.connectionId === "string" && body.connectionId.length > 0 ? body.connectionId : connectionId;
|
|
801
|
+
const bodyLeaseTtlSeconds = typeof body.leaseTtlSeconds === "number" && Number.isFinite(body.leaseTtlSeconds) ? body.leaseTtlSeconds : leaseTtlSeconds;
|
|
802
|
+
const existingConnection = this.playerConnectionInfo.get(playerId);
|
|
803
|
+
if (existingConnection !== void 0) {
|
|
804
|
+
clearInterval(existingConnection.timer);
|
|
805
|
+
}
|
|
806
|
+
const timer = setInterval(() => {
|
|
807
|
+
void this.heartbeatPlayerConnection({
|
|
808
|
+
playerId,
|
|
809
|
+
connectionId: bodyConnectionId,
|
|
810
|
+
leaseTtlSeconds: bodyLeaseTtlSeconds
|
|
811
|
+
}).catch((err) => {
|
|
812
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
813
|
+
console.error("[agent-play:RemotePlayWorld] heartbeat:exhausted", {
|
|
814
|
+
playerId,
|
|
815
|
+
connectionId: bodyConnectionId,
|
|
816
|
+
leaseTtlSeconds: bodyLeaseTtlSeconds,
|
|
817
|
+
attempts: PLAYER_CONNECTION_HEARTBEAT_MAX_ATTEMPTS,
|
|
818
|
+
retryDelayMs: PLAYER_CONNECTION_HEARTBEAT_RETRY_DELAY_MS,
|
|
819
|
+
error: message
|
|
820
|
+
});
|
|
821
|
+
});
|
|
822
|
+
}, 12e3);
|
|
823
|
+
this.playerConnectionInfo.set(playerId, {
|
|
824
|
+
connectionId: bodyConnectionId,
|
|
825
|
+
leaseTtlSeconds: bodyLeaseTtlSeconds,
|
|
826
|
+
timer
|
|
827
|
+
});
|
|
679
828
|
const now = /* @__PURE__ */ new Date();
|
|
680
829
|
return {
|
|
681
830
|
id: playerId,
|
|
@@ -684,8 +833,34 @@ var RemotePlayWorld = class {
|
|
|
684
833
|
createdAt: now,
|
|
685
834
|
updatedAt: now,
|
|
686
835
|
previewUrl,
|
|
687
|
-
registeredAgent
|
|
836
|
+
registeredAgent,
|
|
837
|
+
connectionId: bodyConnectionId,
|
|
838
|
+
leaseTtlSeconds: bodyLeaseTtlSeconds
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
/**
|
|
842
|
+
* @deprecated Use {@link addAgent} with `nodeId` (agent node id) for integrations and automation.
|
|
843
|
+
*/
|
|
844
|
+
async addPlayer(input) {
|
|
845
|
+
const payload = {
|
|
846
|
+
name: input.name,
|
|
847
|
+
type: input.type,
|
|
848
|
+
agent: input.agent,
|
|
849
|
+
nodeId: input.agentId
|
|
688
850
|
};
|
|
851
|
+
if (input.version !== void 0) {
|
|
852
|
+
payload.version = input.version;
|
|
853
|
+
}
|
|
854
|
+
if (input.createdAt !== void 0) {
|
|
855
|
+
payload.createdAt = input.createdAt;
|
|
856
|
+
}
|
|
857
|
+
if (input.updatedAt !== void 0) {
|
|
858
|
+
payload.updatedAt = input.updatedAt;
|
|
859
|
+
}
|
|
860
|
+
if (input.mainNodeId !== void 0) {
|
|
861
|
+
payload.mainNodeId = input.mainNodeId;
|
|
862
|
+
}
|
|
863
|
+
return this.addAgent(payload);
|
|
689
864
|
}
|
|
690
865
|
async recordInteraction(input) {
|
|
691
866
|
await this.rpc("recordInteraction", {
|
|
@@ -697,6 +872,210 @@ var RemotePlayWorld = class {
|
|
|
697
872
|
async recordJourney(playerId, journey) {
|
|
698
873
|
await this.rpc("recordJourney", { playerId, journey });
|
|
699
874
|
}
|
|
875
|
+
async sendIntercomResponse(payload) {
|
|
876
|
+
const sid = this.getSessionId();
|
|
877
|
+
const url = `${this.apiBase}/api/agent-play/sdk/rpc?sid=${encodeURIComponent(sid)}`;
|
|
878
|
+
this.logTransport("intercom:sendResponse:request", {
|
|
879
|
+
url,
|
|
880
|
+
requestId: payload.requestId,
|
|
881
|
+
toPlayerId: payload.toPlayerId,
|
|
882
|
+
fromPlayerId: payload.fromPlayerId,
|
|
883
|
+
kind: payload.kind,
|
|
884
|
+
status: payload.status,
|
|
885
|
+
toolName: payload.toolName,
|
|
886
|
+
error: payload.error,
|
|
887
|
+
resultPreview: payload.result !== void 0 ? this.truncateForLog(JSON.stringify(payload.result)) : void 0
|
|
888
|
+
});
|
|
889
|
+
const res = await fetch(url, {
|
|
890
|
+
method: "POST",
|
|
891
|
+
headers: this.jsonHeaders(),
|
|
892
|
+
body: JSON.stringify({ op: INTERCOM_RESPONSE_OP, payload })
|
|
893
|
+
});
|
|
894
|
+
const okText = await res.text();
|
|
895
|
+
this.logTransport("intercom:sendResponse:http", {
|
|
896
|
+
requestId: payload.requestId,
|
|
897
|
+
httpStatus: res.status,
|
|
898
|
+
bodyPreview: this.truncateForLog(okText)
|
|
899
|
+
});
|
|
900
|
+
if (!res.ok) {
|
|
901
|
+
throw new Error(`intercomResponse: ${res.status} ${okText}`);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
/**
|
|
905
|
+
* Subscribes to the session SSE stream and handles **`forwarded`** intercom commands whose **`toPlayerId`** is in **`playerId`** or **`playerIds`**, invoking **`executeTool`** and posting **`intercomResponse`** (**`completed`** / **`failed`**).
|
|
906
|
+
* Uses a **single** SSE connection when **`playerIds`** lists multiple automation agents (recommended for several agents in one process).
|
|
907
|
+
* Not invoked automatically by {@link RemotePlayWorld.addAgent}.
|
|
908
|
+
*/
|
|
909
|
+
subscribeIntercomCommands(options) {
|
|
910
|
+
const subscribed = normalizeIntercomSubscribePlayerIds(options);
|
|
911
|
+
const playerIdsSorted = Array.from(subscribed).sort();
|
|
912
|
+
if (subscribed.size === 0) {
|
|
913
|
+
this.logTransport("subscribeIntercomCommands:skip", {
|
|
914
|
+
reason: "empty_player_ids"
|
|
915
|
+
});
|
|
916
|
+
return { close: () => {
|
|
917
|
+
} };
|
|
918
|
+
}
|
|
919
|
+
this.logTransport("subscribeIntercomCommands:start", {
|
|
920
|
+
playerIds: playerIdsSorted
|
|
921
|
+
});
|
|
922
|
+
const { executeTool, chatAgentsByPlayerId } = options;
|
|
923
|
+
let closeSource = null;
|
|
924
|
+
const task = (async () => {
|
|
925
|
+
try {
|
|
926
|
+
const { createEventSource } = await import("eventsource-client");
|
|
927
|
+
const sseUrl = `${this.apiBase}/api/agent-play/events?sid=${encodeURIComponent(
|
|
928
|
+
this.getSessionId()
|
|
929
|
+
)}`;
|
|
930
|
+
this.logTransport("subscribeIntercomCommands:sse_open", {
|
|
931
|
+
sseUrl,
|
|
932
|
+
subscribePlayerIds: playerIdsSorted
|
|
933
|
+
});
|
|
934
|
+
const source = createEventSource({
|
|
935
|
+
url: sseUrl,
|
|
936
|
+
fetch: (input, init) => this.mergeAuthFetch(input, init)
|
|
937
|
+
});
|
|
938
|
+
closeSource = () => {
|
|
939
|
+
source.close();
|
|
940
|
+
};
|
|
941
|
+
for await (const msg of source) {
|
|
942
|
+
const sseEvent = getSseEventName(msg);
|
|
943
|
+
if (sseEvent !== void 0 && sseEvent !== WORLD_INTERCOM_EVENT) {
|
|
944
|
+
continue;
|
|
945
|
+
}
|
|
946
|
+
if (typeof msg.data !== "string") {
|
|
947
|
+
const eventLabel = sseEvent ?? "(unset)";
|
|
948
|
+
this.logTransport("sse:intercomCommands:skip", {
|
|
949
|
+
reason: "data_not_string",
|
|
950
|
+
sseEvent: eventLabel
|
|
951
|
+
});
|
|
952
|
+
continue;
|
|
953
|
+
}
|
|
954
|
+
let data;
|
|
955
|
+
try {
|
|
956
|
+
data = JSON.parse(msg.data);
|
|
957
|
+
} catch {
|
|
958
|
+
this.logTransport("sse:intercomCommands:parseJson", {
|
|
959
|
+
reason: "invalid_json",
|
|
960
|
+
sseEvent: sseEvent ?? "(unset)"
|
|
961
|
+
});
|
|
962
|
+
continue;
|
|
963
|
+
}
|
|
964
|
+
let inter;
|
|
965
|
+
try {
|
|
966
|
+
inter = parseWorldIntercomEventPayload(data);
|
|
967
|
+
} catch {
|
|
968
|
+
continue;
|
|
969
|
+
}
|
|
970
|
+
if (inter.status !== "forwarded") {
|
|
971
|
+
continue;
|
|
972
|
+
}
|
|
973
|
+
const cmd = inter.command;
|
|
974
|
+
if (cmd === void 0) {
|
|
975
|
+
this.logTransport("sse:intercomCommands:skip", {
|
|
976
|
+
reason: "missing_command",
|
|
977
|
+
requestId: inter.requestId
|
|
978
|
+
});
|
|
979
|
+
continue;
|
|
980
|
+
}
|
|
981
|
+
if (!subscribed.has(cmd.toPlayerId)) {
|
|
982
|
+
continue;
|
|
983
|
+
}
|
|
984
|
+
this.logTransport("sse:intercomCommands:forwarded", {
|
|
985
|
+
requestId: cmd.requestId,
|
|
986
|
+
fromPlayerId: cmd.fromPlayerId,
|
|
987
|
+
toPlayerId: cmd.toPlayerId,
|
|
988
|
+
kind: cmd.kind,
|
|
989
|
+
toolName: cmd.toolName
|
|
990
|
+
});
|
|
991
|
+
const toolName = cmd.kind === "chat" ? "chat_tool" : cmd.toolName ?? "";
|
|
992
|
+
const args = cmd.kind === "chat" ? { text: cmd.text ?? "" } : cmd.args ?? {};
|
|
993
|
+
this.logTransport("intercom:executeTool", {
|
|
994
|
+
requestId: cmd.requestId,
|
|
995
|
+
toolName,
|
|
996
|
+
argsPreview: this.truncateForLog(JSON.stringify(args))
|
|
997
|
+
});
|
|
998
|
+
try {
|
|
999
|
+
this.logTransport("intercom:executeTool:started", {
|
|
1000
|
+
requestId: cmd.requestId,
|
|
1001
|
+
toolName,
|
|
1002
|
+
argsPreview: this.truncateForLog(JSON.stringify(args))
|
|
1003
|
+
});
|
|
1004
|
+
let result;
|
|
1005
|
+
if (cmd.kind === "chat" && chatAgentsByPlayerId !== void 0 && chatAgentsByPlayerId.has(cmd.toPlayerId)) {
|
|
1006
|
+
const lc = chatAgentsByPlayerId.get(cmd.toPlayerId);
|
|
1007
|
+
if (lc === void 0) {
|
|
1008
|
+
throw new Error("intercom: chatAgentsByPlayerId entry missing");
|
|
1009
|
+
}
|
|
1010
|
+
this.logTransport("intercom:langchain:invoke", {
|
|
1011
|
+
requestId: cmd.requestId,
|
|
1012
|
+
toPlayerId: cmd.toPlayerId,
|
|
1013
|
+
textPreview: this.truncateForLog(cmd.text ?? "")
|
|
1014
|
+
});
|
|
1015
|
+
const rawOut = await invokeLangChainChatAgent(lc, {
|
|
1016
|
+
messages: [new HumanMessage(cmd.text ?? "")]
|
|
1017
|
+
});
|
|
1018
|
+
result = intercomResultRecordFromLangChainInvokeOutput(rawOut);
|
|
1019
|
+
this.logTransport("intercom:langchain:invoke:completed", {
|
|
1020
|
+
requestId: cmd.requestId,
|
|
1021
|
+
resultPreview: this.truncateForLog(JSON.stringify(result))
|
|
1022
|
+
});
|
|
1023
|
+
} else {
|
|
1024
|
+
result = await Promise.resolve(
|
|
1025
|
+
executeTool({ toolName, args })
|
|
1026
|
+
);
|
|
1027
|
+
}
|
|
1028
|
+
this.logTransport("intercom:executeTool:completed", {
|
|
1029
|
+
requestId: cmd.requestId,
|
|
1030
|
+
toolName,
|
|
1031
|
+
argsPreview: this.truncateForLog(JSON.stringify(args)),
|
|
1032
|
+
resultPreview: this.truncateForLog(JSON.stringify(result))
|
|
1033
|
+
});
|
|
1034
|
+
await this.sendIntercomResponse({
|
|
1035
|
+
requestId: cmd.requestId,
|
|
1036
|
+
mainNodeId: cmd.mainNodeId,
|
|
1037
|
+
toPlayerId: cmd.fromPlayerId,
|
|
1038
|
+
fromPlayerId: cmd.toPlayerId,
|
|
1039
|
+
kind: cmd.kind,
|
|
1040
|
+
status: "completed",
|
|
1041
|
+
toolName: cmd.toolName,
|
|
1042
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1043
|
+
result
|
|
1044
|
+
});
|
|
1045
|
+
} catch (err) {
|
|
1046
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1047
|
+
this.logTransport("intercom:executeTool:error", {
|
|
1048
|
+
requestId: cmd.requestId,
|
|
1049
|
+
message
|
|
1050
|
+
});
|
|
1051
|
+
await this.sendIntercomResponse({
|
|
1052
|
+
requestId: cmd.requestId,
|
|
1053
|
+
mainNodeId: cmd.mainNodeId,
|
|
1054
|
+
toPlayerId: cmd.fromPlayerId,
|
|
1055
|
+
fromPlayerId: cmd.toPlayerId,
|
|
1056
|
+
kind: cmd.kind,
|
|
1057
|
+
status: "failed",
|
|
1058
|
+
toolName: cmd.toolName,
|
|
1059
|
+
error: message,
|
|
1060
|
+
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
1061
|
+
});
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
} catch (err) {
|
|
1065
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1066
|
+
this.logTransport("sse:intercomCommands:stream_error", { message });
|
|
1067
|
+
}
|
|
1068
|
+
})();
|
|
1069
|
+
return {
|
|
1070
|
+
close: () => {
|
|
1071
|
+
this.logTransport("subscribeIntercomCommands:close", {
|
|
1072
|
+
playerIds: playerIdsSorted
|
|
1073
|
+
});
|
|
1074
|
+
closeSource?.();
|
|
1075
|
+
void task;
|
|
1076
|
+
}
|
|
1077
|
+
};
|
|
1078
|
+
}
|
|
700
1079
|
async registerMcp(options) {
|
|
701
1080
|
const sid = this.getSessionId();
|
|
702
1081
|
const url = `${this.apiBase}/api/agent-play/mcp/register?sid=${encodeURIComponent(sid)}`;
|
|
@@ -719,7 +1098,7 @@ var RemotePlayWorld = class {
|
|
|
719
1098
|
} catch {
|
|
720
1099
|
throw new Error("registerMcp: invalid JSON");
|
|
721
1100
|
}
|
|
722
|
-
if (!
|
|
1101
|
+
if (!isRecord(json) || typeof json.id !== "string") {
|
|
723
1102
|
throw new Error("registerMcp: invalid response");
|
|
724
1103
|
}
|
|
725
1104
|
return json.id;
|
|
@@ -737,12 +1116,103 @@ var RemotePlayWorld = class {
|
|
|
737
1116
|
throw new Error(`rpc ${op}: ${res.status} ${t}`);
|
|
738
1117
|
}
|
|
739
1118
|
}
|
|
1119
|
+
async heartbeatPlayerConnection(input) {
|
|
1120
|
+
const sid = this.getSessionId();
|
|
1121
|
+
const url = `${this.apiBase}/api/agent-play/players/heartbeat?sid=${encodeURIComponent(sid)}`;
|
|
1122
|
+
const bodyJson = JSON.stringify({
|
|
1123
|
+
playerId: input.playerId,
|
|
1124
|
+
connectionId: input.connectionId,
|
|
1125
|
+
leaseTtlSeconds: input.leaseTtlSeconds
|
|
1126
|
+
});
|
|
1127
|
+
let lastError = new Error("heartbeat: no attempts completed");
|
|
1128
|
+
for (let attempt = 1; attempt <= PLAYER_CONNECTION_HEARTBEAT_MAX_ATTEMPTS; attempt += 1) {
|
|
1129
|
+
const attemptStartedAt = Date.now();
|
|
1130
|
+
try {
|
|
1131
|
+
const res = await fetch(url, {
|
|
1132
|
+
method: "POST",
|
|
1133
|
+
headers: this.jsonHeaders(),
|
|
1134
|
+
body: bodyJson
|
|
1135
|
+
});
|
|
1136
|
+
const text = await res.text();
|
|
1137
|
+
if (res.ok) {
|
|
1138
|
+
if (attempt > 1) {
|
|
1139
|
+
console.info("[agent-play:RemotePlayWorld] heartbeat:retry_recovered", {
|
|
1140
|
+
playerId: input.playerId,
|
|
1141
|
+
connectionId: input.connectionId,
|
|
1142
|
+
leaseTtlSeconds: input.leaseTtlSeconds,
|
|
1143
|
+
sid,
|
|
1144
|
+
url,
|
|
1145
|
+
successfulAttempt: attempt,
|
|
1146
|
+
attemptsUsed: attempt,
|
|
1147
|
+
maxAttempts: PLAYER_CONNECTION_HEARTBEAT_MAX_ATTEMPTS,
|
|
1148
|
+
durationMs: Date.now() - attemptStartedAt
|
|
1149
|
+
});
|
|
1150
|
+
}
|
|
1151
|
+
return;
|
|
1152
|
+
}
|
|
1153
|
+
lastError = new Error(`heartbeat: ${res.status} ${text}`);
|
|
1154
|
+
} catch (err) {
|
|
1155
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
1156
|
+
}
|
|
1157
|
+
const willRetry = attempt < PLAYER_CONNECTION_HEARTBEAT_MAX_ATTEMPTS;
|
|
1158
|
+
console.warn("[agent-play:RemotePlayWorld] heartbeat:attempt_failed", {
|
|
1159
|
+
phase: "player_connection_lease_refresh",
|
|
1160
|
+
playerId: input.playerId,
|
|
1161
|
+
connectionId: input.connectionId,
|
|
1162
|
+
leaseTtlSeconds: input.leaseTtlSeconds,
|
|
1163
|
+
sid,
|
|
1164
|
+
url,
|
|
1165
|
+
attempt,
|
|
1166
|
+
maxAttempts: PLAYER_CONNECTION_HEARTBEAT_MAX_ATTEMPTS,
|
|
1167
|
+
remainingAttemptsAfterThisFailure: PLAYER_CONNECTION_HEARTBEAT_MAX_ATTEMPTS - attempt,
|
|
1168
|
+
errorMessage: lastError.message,
|
|
1169
|
+
requestBodyBytes: bodyJson.length,
|
|
1170
|
+
attemptDurationMs: Date.now() - attemptStartedAt,
|
|
1171
|
+
nextAction: willRetry ? `sleep ${PLAYER_CONNECTION_HEARTBEAT_RETRY_DELAY_MS}ms then retry` : "no more retries; will throw",
|
|
1172
|
+
retryDelayMs: willRetry ? PLAYER_CONNECTION_HEARTBEAT_RETRY_DELAY_MS : 0
|
|
1173
|
+
});
|
|
1174
|
+
if (!willRetry) {
|
|
1175
|
+
throw lastError;
|
|
1176
|
+
}
|
|
1177
|
+
await new Promise((resolve) => {
|
|
1178
|
+
setTimeout(resolve, PLAYER_CONNECTION_HEARTBEAT_RETRY_DELAY_MS);
|
|
1179
|
+
});
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
async disconnectPlayerConnection(input) {
|
|
1183
|
+
const sid = this.getSessionId();
|
|
1184
|
+
const url = `${this.apiBase}/api/agent-play/players/disconnect?sid=${encodeURIComponent(sid)}`;
|
|
1185
|
+
await fetch(url, {
|
|
1186
|
+
method: "POST",
|
|
1187
|
+
headers: this.jsonHeaders(),
|
|
1188
|
+
body: JSON.stringify({
|
|
1189
|
+
playerId: input.playerId,
|
|
1190
|
+
connectionId: input.connectionId
|
|
1191
|
+
})
|
|
1192
|
+
});
|
|
1193
|
+
}
|
|
740
1194
|
};
|
|
1195
|
+
|
|
1196
|
+
// src/index.ts
|
|
1197
|
+
import {
|
|
1198
|
+
loadAgentPlayCredentialsFileFromPath,
|
|
1199
|
+
loadAgentPlayCredentialsFileFromPathSync as loadAgentPlayCredentialsFileFromPathSync2,
|
|
1200
|
+
loadRootKey as loadRootKey2,
|
|
1201
|
+
nodeCredentialsMaterialFromHumanPassphrase as nodeCredentialsMaterialFromHumanPassphrase2,
|
|
1202
|
+
parseAgentPlayCredentialsJson,
|
|
1203
|
+
resolveAgentPlayCredentialsPath as resolveAgentPlayCredentialsPath2
|
|
1204
|
+
} from "@agent-play/node-tools";
|
|
741
1205
|
export {
|
|
1206
|
+
MINIMUM_PLAY_WORLD_BOUNDS,
|
|
742
1207
|
PLAYER_ADDED_EVENT,
|
|
743
1208
|
PLAYER_CHAIN_GENESIS_STABLE_KEY,
|
|
744
1209
|
PLAYER_CHAIN_HEADER_STABLE_KEY,
|
|
745
1210
|
RemotePlayWorld,
|
|
1211
|
+
SESSION_CLOSED_EVENT,
|
|
1212
|
+
SESSION_CONNECTED_EVENT,
|
|
1213
|
+
SESSION_INVALID_EVENT,
|
|
1214
|
+
SESSION_SSE_ERROR_EVENT,
|
|
1215
|
+
SESSION_SSE_OPEN_EVENT,
|
|
746
1216
|
WORLD_AGENT_SIGNAL_EVENT,
|
|
747
1217
|
WORLD_INTERACTION_EVENT,
|
|
748
1218
|
WORLD_JOURNEY_EVENT,
|
|
@@ -750,13 +1220,21 @@ export {
|
|
|
750
1220
|
boundsContain,
|
|
751
1221
|
clampWorldPosition,
|
|
752
1222
|
configureAgentPlayDebug,
|
|
1223
|
+
expandBoundsToMinimumPlayArea,
|
|
1224
|
+
intercomResultRecordFromLangChainInvokeOutput,
|
|
753
1225
|
isAgentPlayDebugEnabled,
|
|
754
1226
|
langchainRegistration,
|
|
1227
|
+
loadAgentPlayCredentialsFileFromPath,
|
|
1228
|
+
loadAgentPlayCredentialsFileFromPathSync2 as loadAgentPlayCredentialsFileFromPathSync,
|
|
1229
|
+
loadRootKey2 as loadRootKey,
|
|
755
1230
|
mergeSnapshotWithPlayerChainNode,
|
|
1231
|
+
nodeCredentialsMaterialFromHumanPassphrase2 as nodeCredentialsMaterialFromHumanPassphrase,
|
|
1232
|
+
parseAgentPlayCredentialsJson,
|
|
756
1233
|
parsePlayerChainFanoutNotify,
|
|
757
1234
|
parsePlayerChainFanoutNotifyFromSsePayload,
|
|
758
1235
|
parsePlayerChainNodeRpcBody,
|
|
759
1236
|
resetAgentPlayDebug,
|
|
1237
|
+
resolveAgentPlayCredentialsPath2 as resolveAgentPlayCredentialsPath,
|
|
760
1238
|
sortNodeRefsForSerializedFetch
|
|
761
1239
|
};
|
|
762
1240
|
//# sourceMappingURL=index.js.map
|