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