@whitesands/agent-player 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -31,11 +31,13 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
31
31
  var src_exports = {};
32
32
  __export(src_exports, {
33
33
  AgentServer: () => AgentServer,
34
+ Character: () => Character,
34
35
  ConnectionLostError: () => ConnectionLostError,
35
36
  PlatformAuthError: () => PlatformAuthError,
36
37
  ProtocolVersionError: () => ProtocolVersionError,
37
38
  TurnFailedError: () => TurnFailedError,
38
39
  TurnTimeoutError: () => TurnTimeoutError,
40
+ World: () => World,
39
41
  bindDynamic: () => bindDynamic
40
42
  });
41
43
  module.exports = __toCommonJS(src_exports);
@@ -159,7 +161,7 @@ var import_ws = __toESM(require("ws"), 1);
159
161
 
160
162
  // src/protocol.ts
161
163
  var PROTOCOL_VERSION = 1;
162
- var SDK_VERSION = "1.2.0";
164
+ var SDK_VERSION = "1.3.0";
163
165
  var SESSION_RESUME_GRACE_MS = 3e4;
164
166
  var DEFAULT_AGENT_INSTRUCTIONS = [
165
167
  "You are a remote game agent. Tools are provided by the game.",
@@ -397,6 +399,15 @@ var PlatformConnection = class {
397
399
  waiter.resolve(envelope);
398
400
  return;
399
401
  }
402
+ if (envelope.type === "error" && envelope.sessionId) {
403
+ const key = `session:${envelope.sessionId}:start`;
404
+ const waiter = this.pending.get(key);
405
+ if (waiter) {
406
+ this.pending.delete(key);
407
+ waiter.reject(new Error(envelope.message));
408
+ }
409
+ return;
410
+ }
400
411
  if (envelope.type === "session_resumed" || envelope.type === "resume_err") {
401
412
  const waiter = this.pending.get(`session:${envelope.sessionId}:resume`);
402
413
  if (!waiter) return;
@@ -436,6 +447,8 @@ var PlatformConnection = class {
436
447
  const sessions = this.handlers.sessions?.() ?? [];
437
448
  for (const session of sessions) {
438
449
  const resumed = this.waitForSessionEvent(session.sessionId, "resume");
450
+ resumed.catch(() => {
451
+ });
439
452
  this.send({
440
453
  type: "resume_session",
441
454
  sessionId: session.sessionId,
@@ -911,7 +924,7 @@ var AgentServer = class {
911
924
  const sessionToken = (0, import_node_crypto2.randomUUID)();
912
925
  const gate = new TurnGate();
913
926
  const name = options?.name ?? instance.constructor?.name ?? "player";
914
- const memory = this.actionMemory();
927
+ const memory = options?.memory ? null : this.actionMemory();
915
928
  const server = createSessionMcpServer(reflected, gate, {
916
929
  name: `agent-player:${name}`,
917
930
  ...memory ? {
@@ -921,7 +934,10 @@ var AgentServer = class {
921
934
  }
922
935
  } : {}
923
936
  });
924
- const instructions = [this.options.instructions ?? DEFAULT_AGENT_INSTRUCTIONS, options?.instructions].filter((part) => Boolean(part && part.length > 0)).join("\n\n");
937
+ const instructions = [
938
+ options?.ownInstructions ? void 0 : this.options.instructions ?? DEFAULT_AGENT_INSTRUCTIONS,
939
+ options?.instructions
940
+ ].filter((part) => Boolean(part && part.length > 0)).join("\n\n");
925
941
  const listeners = /* @__PURE__ */ new Set();
926
942
  const record = {
927
943
  id: sessionId,
@@ -930,7 +946,9 @@ var AgentServer = class {
930
946
  gate,
931
947
  server,
932
948
  transport: void 0,
933
- listeners
949
+ listeners,
950
+ memory: options?.memory === true,
951
+ relationships: /* @__PURE__ */ new Map()
934
952
  };
935
953
  const onTurnResult = (result) => {
936
954
  gate.completeTurn(result);
@@ -971,9 +989,17 @@ var AgentServer = class {
971
989
  name,
972
990
  tools: reflected.tools,
973
991
  instructions,
974
- ...options.goal ? { goal: options.goal } : {}
992
+ ...options.goal ? { goal: options.goal } : {},
993
+ ...options.memory ? { memory: true } : {},
994
+ ...options.maxActions ? { maxActions: options.maxActions } : {},
995
+ ...options.subgoals === false ? { subgoals: false } : {}
975
996
  });
976
- await started;
997
+ const ack = await started;
998
+ if (ack.type === "session_started") {
999
+ for (const relationship of ack.relationships ?? []) {
1000
+ record.relationships.set(relationship.about.toLowerCase(), relationship);
1001
+ }
1002
+ }
977
1003
  } catch (error) {
978
1004
  const wrapped = error instanceof Error ? error : new Error(String(error));
979
1005
  this.connection.failSessionEvent(sessionId, "start", wrapped);
@@ -992,7 +1018,15 @@ var AgentServer = class {
992
1018
  on: (listener) => {
993
1019
  listeners.add(listener);
994
1020
  return () => listeners.delete(listener);
995
- }
1021
+ },
1022
+ observe: (text, about) => {
1023
+ if (!record.memory || !text || !this.connection || !this.sessions.has(sessionId)) return;
1024
+ try {
1025
+ this.connection.send({ type: "observe", sessionId, text, ...about ? { about } : {} });
1026
+ } catch {
1027
+ }
1028
+ },
1029
+ relationships: () => Object.fromEntries(record.relationships)
996
1030
  };
997
1031
  }
998
1032
  /** Subscribe to every session plus connection events. Returns unsubscribe. */
@@ -1025,7 +1059,8 @@ var AgentServer = class {
1025
1059
  args: outcome.args ?? {},
1026
1060
  result: outcome.result,
1027
1061
  attempts: outcome.attempts ?? 1,
1028
- usage: outcome.usage ?? { promptTokens: 0, completionTokens: 0 }
1062
+ usage: outcome.usage ?? { promptTokens: 0, completionTokens: 0 },
1063
+ ...outcome.steps ? { steps: outcome.steps } : {}
1029
1064
  };
1030
1065
  }
1031
1066
  emit(event) {
@@ -1072,6 +1107,33 @@ var AgentServer = class {
1072
1107
  });
1073
1108
  return;
1074
1109
  }
1110
+ if (envelope.event === "relationship" || envelope.event === "memory") {
1111
+ if (envelope.error) return;
1112
+ const args = envelope.args ?? {};
1113
+ const about = typeof args.about === "string" ? args.about : "";
1114
+ if (envelope.event === "memory") {
1115
+ this.emit({ type: "memory", sessionId: envelope.sessionId, about, note: String(args.note ?? "") });
1116
+ return;
1117
+ }
1118
+ const relationship = {
1119
+ about,
1120
+ affinity: Number(args.affinity ?? 0),
1121
+ label: String(args.label ?? "stranger"),
1122
+ note: String(args.because ?? "")
1123
+ };
1124
+ this.sessions.get(envelope.sessionId)?.relationships.set(about.toLowerCase(), relationship);
1125
+ this.emit({
1126
+ type: "relationship",
1127
+ sessionId: envelope.sessionId,
1128
+ about,
1129
+ affinity: relationship.affinity,
1130
+ label: relationship.label,
1131
+ change: Number(args.change ?? 0),
1132
+ because: relationship.note ?? ""
1133
+ });
1134
+ return;
1135
+ }
1136
+ if (envelope.event !== "turnFailed") return;
1075
1137
  this.emit({
1076
1138
  type: "turnFailed",
1077
1139
  sessionId: envelope.sessionId,
@@ -1099,6 +1161,7 @@ var AgentServer = class {
1099
1161
  case "ping":
1100
1162
  case "pong":
1101
1163
  case "resume_session":
1164
+ case "observe":
1102
1165
  return;
1103
1166
  default: {
1104
1167
  assertNever(envelope);
@@ -1155,14 +1218,692 @@ var AgentServer = class {
1155
1218
  await this.close();
1156
1219
  }
1157
1220
  };
1221
+
1222
+ // src/world.ts
1223
+ var import_zod2 = require("zod");
1224
+ var DEFAULT_EMOTES = ["wave", "laugh", "cheer", "dance", "point", "shrug"];
1225
+ function distance(a, b) {
1226
+ return Math.hypot(a.x - b.x, a.y - b.y, (a.z ?? 0) - (b.z ?? 0));
1227
+ }
1228
+ function clip(text, limit) {
1229
+ return text.length <= limit ? text : `${text.slice(0, limit - 3)}...`;
1230
+ }
1231
+ var now = () => performance.now() / 1e3;
1232
+ var sleep = (seconds) => new Promise((resolve2) => setTimeout(resolve2, seconds * 1e3));
1233
+ var CharacterTools = class {
1234
+ constructor(character) {
1235
+ this.character = character;
1236
+ }
1237
+ character;
1238
+ static agent = {
1239
+ actions: {
1240
+ say: {
1241
+ description: "Say one or two short sentences out loud. Set to when you are talking to one person.",
1242
+ args: {
1243
+ text: import_zod2.z.string().describe("What you say, in your own voice."),
1244
+ to: import_zod2.z.string().describe("Name of the person you are talking to. Empty to talk to everyone near you.")
1245
+ }
1246
+ },
1247
+ go_to: {
1248
+ description: "Walk to a place. You are told when you arrive.",
1249
+ args: { place: import_zod2.z.string().describe("One of the places you know.") }
1250
+ },
1251
+ approach: {
1252
+ description: "Walk up to someone you can see, to talk or do something together.",
1253
+ args: { person: import_zod2.z.string() }
1254
+ },
1255
+ use: {
1256
+ description: "Do an activity with a thing listed in things_here. You walk to it first. You are told when you finish.",
1257
+ args: { thing: import_zod2.z.string(), activity: import_zod2.z.string() }
1258
+ },
1259
+ give: {
1260
+ description: "Give something you are carrying to someone next to you.",
1261
+ args: { item: import_zod2.z.string(), to: import_zod2.z.string() }
1262
+ },
1263
+ emote: {
1264
+ description: "Show a feeling with your body.",
1265
+ args: { emote: import_zod2.z.string().describe("wave, laugh, cheer, dance, point or shrug.") }
1266
+ },
1267
+ idle: { description: "Do nothing new for now and carry on as you are." }
1268
+ }
1269
+ };
1270
+ see() {
1271
+ return this.character.view();
1272
+ }
1273
+ say(text, to) {
1274
+ return this.character.doSay(text, to);
1275
+ }
1276
+ go_to(place) {
1277
+ return this.character.doGoTo(place);
1278
+ }
1279
+ approach(person) {
1280
+ return this.character.doApproach(person);
1281
+ }
1282
+ use(thing, activity) {
1283
+ return this.character.doUse(thing, activity);
1284
+ }
1285
+ give(item, to) {
1286
+ return this.character.doGive(item, to);
1287
+ }
1288
+ emote(emote) {
1289
+ return this.character.doEmote(emote);
1290
+ }
1291
+ idle() {
1292
+ return { ok: true };
1293
+ }
1294
+ };
1295
+ var Character = class {
1296
+ constructor(world, name, body, home, carrying) {
1297
+ this.world = world;
1298
+ this.name = name;
1299
+ this.body = body;
1300
+ this.home = home;
1301
+ this.carrying = [...carrying];
1302
+ }
1303
+ world;
1304
+ name;
1305
+ body;
1306
+ home;
1307
+ kind = "resident";
1308
+ status = null;
1309
+ carrying;
1310
+ needs = {};
1311
+ turns = 0;
1312
+ tokens = 0;
1313
+ agent;
1314
+ /** @internal */
1315
+ inbox = [];
1316
+ /** @internal */
1317
+ wake = now() + 2 + Math.random() * 6;
1318
+ /** @internal */
1319
+ busy = false;
1320
+ /** @internal */
1321
+ greeted = /* @__PURE__ */ new Map();
1322
+ /** @internal */
1323
+ needWarned = /* @__PURE__ */ new Set();
1324
+ task = 0;
1325
+ using = null;
1326
+ chat = { count: 0, resetAt: 0 };
1327
+ position() {
1328
+ return this.body.getPosition();
1329
+ }
1330
+ place() {
1331
+ return this.world.placeAt(this.position());
1332
+ }
1333
+ setStatus(status) {
1334
+ this.status = status;
1335
+ this.body.setStatus?.(status);
1336
+ this.world.emit({ type: "status", character: this.name, status });
1337
+ }
1338
+ /**
1339
+ * Tell this character something happened. It goes into its memory and its
1340
+ * next view. turn asks for a turn; urgent asks for one now.
1341
+ */
1342
+ notice(text, options = {}) {
1343
+ this.inbox.push(text);
1344
+ while (this.inbox.length > 12) this.inbox.shift();
1345
+ this.agent?.observe(text, options.about);
1346
+ if (options.urgent) {
1347
+ this.wakeIn(0.2);
1348
+ } else if (options.turn) {
1349
+ const world = this.world;
1350
+ const far = world.farDelay > 0 && !world.humanNear(this.position(), world.activeRadius);
1351
+ this.wakeIn(far ? world.farDelay : 2.5);
1352
+ }
1353
+ }
1354
+ /** @internal */
1355
+ wakeIn(seconds) {
1356
+ const at = now() + seconds;
1357
+ if (this.wake === null || at < this.wake) this.wake = at;
1358
+ }
1359
+ /** How this character feels about someone, from the platform. */
1360
+ feelingFor(name) {
1361
+ const relationship = this.agent?.relationships()[name.toLowerCase()];
1362
+ return relationship ? { affinity: relationship.affinity, label: relationship.label, ...relationship.note ? { note: relationship.note } : {} } : { affinity: 0, label: "stranger" };
1363
+ }
1364
+ /** @internal Returns true when another character's line should get a reply. */
1365
+ mayReplyToCharacter() {
1366
+ if (now() > this.chat.resetAt) {
1367
+ this.chat.count = 0;
1368
+ this.chat.resetAt = now() + this.world.chatPause;
1369
+ }
1370
+ this.chat.count += 1;
1371
+ return this.chat.count <= this.world.chatReplies;
1372
+ }
1373
+ /** @internal */
1374
+ view() {
1375
+ const world = this.world;
1376
+ const here = this.position();
1377
+ const place = world.placeAt(here);
1378
+ const why = this.inbox.length > 0 ? this.inbox : ["Nothing new has happened. Carry on with your day."];
1379
+ this.inbox = [];
1380
+ const nearby = [];
1381
+ const add = (name, who, at, doing) => {
1382
+ if (!at) return;
1383
+ const d = distance(at, here);
1384
+ if (d > world.sight) return;
1385
+ nearby.push({
1386
+ name,
1387
+ who,
1388
+ steps_away: Math.round(d),
1389
+ can_hear_you: d <= world.hearing,
1390
+ ...doing ? { doing } : {},
1391
+ you_feel: this.feelingFor(name).label
1392
+ });
1393
+ };
1394
+ for (const other of world.characters.values()) {
1395
+ if (other !== this) add(other.name, "resident", other.position(), other.status);
1396
+ }
1397
+ for (const person of world.people.values()) add(person.name, "player", person.getPosition(), null);
1398
+ nearby.sort((a, b) => a.steps_away - b.steps_away);
1399
+ const things = [];
1400
+ for (const thing of world.things.values()) {
1401
+ if (things.length >= 12) break;
1402
+ if (!(place && thing.place === place || distance(thing.position, here) <= 30)) continue;
1403
+ const activities = Object.entries(thing.activities).map(([name, activity]) => `${name}: ${activity.description}${activity.needs ? ` (needs ${activity.needs})` : ""}`).sort();
1404
+ things.push({
1405
+ thing: thing.id,
1406
+ activities,
1407
+ ...thing.usedBy && thing.usedBy !== this.name ? { in_use_by: thing.usedBy } : {}
1408
+ });
1409
+ }
1410
+ const needs = Object.fromEntries(Object.entries(this.needs).map(([need, value]) => [need, Math.floor(value)]));
1411
+ return {
1412
+ ...world.clock ? { time: world.clock() } : {},
1413
+ why_you_are_acting_now: why,
1414
+ you: {
1415
+ at: place ?? "outdoors, between places",
1416
+ doing: this.status ?? "nothing in particular",
1417
+ ...Object.keys(needs).length > 0 ? { needs } : {},
1418
+ ...this.carrying.length > 0 ? { carrying: this.carrying } : {}
1419
+ },
1420
+ people_nearby: nearby.slice(0, 8),
1421
+ things_here: things
1422
+ };
1423
+ }
1424
+ // Stop whatever the body is doing so a new movement can start.
1425
+ interrupt() {
1426
+ this.task += 1;
1427
+ if (this.using) {
1428
+ this.using.usedBy = null;
1429
+ this.using = null;
1430
+ }
1431
+ this.body.stop();
1432
+ return this.task;
1433
+ }
1434
+ async walk(token, goal, status) {
1435
+ this.setStatus(status);
1436
+ const reached = await this.body.moveTo(goal);
1437
+ if (this.task !== token) return false;
1438
+ this.setStatus(null);
1439
+ return reached;
1440
+ }
1441
+ /** @internal */
1442
+ doSay(rawText, to) {
1443
+ let text = clip(String(rawText ?? "").trim(), 240);
1444
+ if (!text) throw new Error("say needs some text");
1445
+ const world = this.world;
1446
+ const target = world.resolve(to);
1447
+ const towards = target ? world.positionOf(target) : null;
1448
+ const shown = this.body.say(text, towards ?? void 0);
1449
+ if (typeof shown === "string" && shown) text = shown;
1450
+ const heard = world.speech(this.name, text, target);
1451
+ return heard === 0 ? { said: text, note: "Nobody was close enough to hear you." } : { said: text };
1452
+ }
1453
+ /** @internal */
1454
+ doGoTo(placeName) {
1455
+ const place = this.world.places.get(placeName);
1456
+ if (!place) throw new Error(`no such place: ${placeName}. Places: ${[...this.world.places.keys()].join(", ")}`);
1457
+ if (this.place() === placeName && !this.status) throw new Error(`you are already at ${placeName}`);
1458
+ const token = this.interrupt();
1459
+ void this.walk(token, place.position, `walking to ${placeName}`).then((reached) => {
1460
+ if (reached) this.notice(`You arrived at ${placeName}.`, { turn: true });
1461
+ else if (this.task === token) this.notice(`You could not get to ${placeName}.`, { turn: true });
1462
+ });
1463
+ return { walking_to: placeName };
1464
+ }
1465
+ /** @internal */
1466
+ doApproach(person) {
1467
+ const world = this.world;
1468
+ const name = world.resolve(person);
1469
+ const at = name ? world.positionOf(name) : null;
1470
+ if (!name || !at || name === this.name) throw new Error(`you cannot see anyone called ${person}`);
1471
+ const gap = distance(at, this.position());
1472
+ if (gap > world.sight) throw new Error(`${name} is too far away to see`);
1473
+ if (gap <= 7) return { note: `You are already next to ${name}.` };
1474
+ const token = this.interrupt();
1475
+ const here = this.position();
1476
+ const goal = { x: at.x + (here.x - at.x) / gap * 4, y: at.y + (here.y - at.y) / gap * 4 };
1477
+ void this.walk(token, goal, `walking over to ${name}`).then((reached) => {
1478
+ if (reached) this.notice(`You walked up to ${name}.`, { turn: true });
1479
+ });
1480
+ return { walking_to: name };
1481
+ }
1482
+ /** @internal */
1483
+ doUse(thingId, activityName) {
1484
+ const thing = this.world.things.get(thingId);
1485
+ if (!thing) throw new Error(`there is no thing called ${thingId}`);
1486
+ const activity = thing.activities[activityName];
1487
+ if (!activity) {
1488
+ throw new Error(`${thingId} has no activity ${activityName}. It has: ${Object.keys(thing.activities).sort().join(", ")}`);
1489
+ }
1490
+ if (thing.usedBy && thing.usedBy !== this.name) throw new Error(`${thing.usedBy} is using ${thingId} right now`);
1491
+ if (activity.needs && !this.carrying.includes(activity.needs)) throw new Error(`you need ${activity.needs} to do that`);
1492
+ const token = this.interrupt();
1493
+ const seconds = activity.seconds ?? 5;
1494
+ thing.usedBy = this.name;
1495
+ this.using = thing;
1496
+ void (async () => {
1497
+ if (distance(thing.position, this.position()) > 6 && !await this.walk(token, thing.position, `walking to ${thing.name}`)) {
1498
+ if (this.task === token) {
1499
+ thing.usedBy = null;
1500
+ this.using = null;
1501
+ this.notice(`You could not reach ${thingId}.`, { turn: true });
1502
+ }
1503
+ return;
1504
+ }
1505
+ this.body.face?.(thing.position);
1506
+ this.setStatus(activity.status ?? activityName.replaceAll("_", " "));
1507
+ if (activity.emote) this.body.emote?.(activity.emote);
1508
+ await sleep(seconds);
1509
+ if (this.task !== token) return;
1510
+ thing.usedBy = null;
1511
+ this.using = null;
1512
+ this.setStatus(null);
1513
+ const outcome = [];
1514
+ if (activity.needs) {
1515
+ const index = this.carrying.indexOf(activity.needs);
1516
+ if (index >= 0) this.carrying.splice(index, 1);
1517
+ }
1518
+ for (const [need, change] of Object.entries(activity.effects ?? {})) {
1519
+ const current = this.needs[need];
1520
+ if (current === void 0) continue;
1521
+ this.needs[need] = Math.min(100, Math.max(0, current + change));
1522
+ outcome.push(`${need} is now ${Math.floor(this.needs[need])}`);
1523
+ }
1524
+ if (activity.gives) {
1525
+ this.carrying.push(activity.gives);
1526
+ outcome.push(`you now carry ${activity.gives}`);
1527
+ }
1528
+ const summary = outcome.length > 0 ? ` (${outcome.join(", ")})` : "";
1529
+ this.notice(`You finished: ${activityName.replaceAll("_", " ")} at ${thing.name}${summary}.`, { turn: true });
1530
+ })();
1531
+ return { started: activityName, takes_seconds: seconds };
1532
+ }
1533
+ /** @internal */
1534
+ doGive(item, to) {
1535
+ const world = this.world;
1536
+ const index = this.carrying.indexOf(item);
1537
+ if (index < 0) throw new Error(`you are not carrying ${item}`);
1538
+ const name = world.resolve(to);
1539
+ const at = name ? world.positionOf(name) : null;
1540
+ if (!name || !at) throw new Error(`you cannot see anyone called ${to}`);
1541
+ if (distance(at, this.position()) > 12) throw new Error(`${name} is too far away. Use approach first.`);
1542
+ this.carrying.splice(index, 1);
1543
+ const other = world.characters.get(name);
1544
+ if (other) {
1545
+ other.carrying.push(item);
1546
+ other.notice(`${this.name} gave you ${item}.`, { about: this.name, urgent: true });
1547
+ } else {
1548
+ world.people.get(name)?.carrying.push(item);
1549
+ }
1550
+ world.emit({ type: "give", from: this.name, to: name, item });
1551
+ return { gave: item, to: name };
1552
+ }
1553
+ /** @internal */
1554
+ doEmote(name) {
1555
+ const emotes = this.body.emotes ?? DEFAULT_EMOTES;
1556
+ if (!emotes.includes(name)) throw new Error(`no such emote. Choose one of: ${emotes.join(", ")}`);
1557
+ this.body.emote?.(name);
1558
+ this.world.emit({ type: "emote", character: this.name, emote: name });
1559
+ return { did: name };
1560
+ }
1561
+ };
1562
+ var World = class {
1563
+ constructor(server, options) {
1564
+ this.server = server;
1565
+ this.name = options.name;
1566
+ this.about = options.about ?? "";
1567
+ this.rules = options.rules ?? "";
1568
+ this.clock = options.clock;
1569
+ this.hearing = options.hearing ?? 35;
1570
+ this.sight = options.sight ?? 80;
1571
+ this.idleSeconds = options.idleSeconds ?? 45;
1572
+ this.farSlowdown = options.farSlowdown ?? 3;
1573
+ this.activeRadius = options.activeRadius ?? 120;
1574
+ this.farDelay = options.farDelay ?? 20;
1575
+ this.greetDistance = options.greetDistance ?? 12;
1576
+ this.turnsPerMinute = options.turnsPerMinute ?? 30;
1577
+ this.maxActions = options.maxActions ?? 3;
1578
+ this.chatReplies = options.chatReplies ?? 2;
1579
+ this.chatPause = options.chatPause ?? 90;
1580
+ this.needDefs = options.needs ?? {};
1581
+ this.memory = options.memory !== false;
1582
+ this.warn = options.warn ?? (() => void 0);
1583
+ }
1584
+ server;
1585
+ name;
1586
+ about;
1587
+ rules;
1588
+ clock;
1589
+ hearing;
1590
+ sight;
1591
+ idleSeconds;
1592
+ farSlowdown;
1593
+ activeRadius;
1594
+ farDelay;
1595
+ greetDistance;
1596
+ turnsPerMinute;
1597
+ maxActions;
1598
+ chatReplies;
1599
+ chatPause;
1600
+ places = /* @__PURE__ */ new Map();
1601
+ things = /* @__PURE__ */ new Map();
1602
+ characters = /* @__PURE__ */ new Map();
1603
+ people = /* @__PURE__ */ new Map();
1604
+ turnsUsed = 0;
1605
+ needDefs;
1606
+ memory;
1607
+ warn;
1608
+ listeners = /* @__PURE__ */ new Set();
1609
+ timer = null;
1610
+ budget = 0;
1611
+ budgetAt = now();
1612
+ lastTick = now();
1613
+ /** Listen for world events. Returns a function that unsubscribes. */
1614
+ on(listener) {
1615
+ this.listeners.add(listener);
1616
+ return () => this.listeners.delete(listener);
1617
+ }
1618
+ /** @internal */
1619
+ emit(event) {
1620
+ for (const listener of [...this.listeners]) {
1621
+ try {
1622
+ listener(event);
1623
+ } catch (error) {
1624
+ this.warn(`world listener failed: ${error instanceof Error ? error.message : String(error)}`);
1625
+ }
1626
+ }
1627
+ }
1628
+ addPlace(name, place) {
1629
+ if (this.places.has(name)) throw new Error(`place already exists: ${name}`);
1630
+ this.places.set(name, { name, description: place.description ?? "", position: place.position, radius: place.radius ?? 22 });
1631
+ }
1632
+ addThing(id, thing) {
1633
+ if (this.things.has(id)) throw new Error(`thing already exists: ${id}`);
1634
+ this.things.set(id, {
1635
+ id,
1636
+ name: thing.name ?? id.replaceAll("_", " "),
1637
+ ...thing.place ? { place: thing.place } : {},
1638
+ position: thing.position,
1639
+ activities: thing.activities,
1640
+ usedBy: null
1641
+ });
1642
+ }
1643
+ /** Add a human player. getPosition returns null while they have no body. */
1644
+ addPerson(person) {
1645
+ this.people.set(person.name, { name: person.name, getPosition: person.getPosition, carrying: [] });
1646
+ }
1647
+ removePerson(name) {
1648
+ this.people.delete(name);
1649
+ }
1650
+ placeAt(position) {
1651
+ let best = null;
1652
+ let bestDistance = Infinity;
1653
+ for (const place of this.places.values()) {
1654
+ const d = distance(place.position, position);
1655
+ if (d <= place.radius && d < bestDistance) {
1656
+ best = place.name;
1657
+ bestDistance = d;
1658
+ }
1659
+ }
1660
+ return best;
1661
+ }
1662
+ positionOf(name) {
1663
+ return this.characters.get(name)?.position() ?? this.people.get(name)?.getPosition() ?? null;
1664
+ }
1665
+ /** Match a name the model wrote to someone in the world, ignoring case. */
1666
+ resolve(name) {
1667
+ if (!name) return void 0;
1668
+ const lower = name.trim().toLowerCase();
1669
+ for (const known of [...this.characters.keys(), ...this.people.keys()]) {
1670
+ if (known.toLowerCase() === lower) return known;
1671
+ }
1672
+ return void 0;
1673
+ }
1674
+ /** @internal */
1675
+ humanNear(position, range) {
1676
+ for (const person of this.people.values()) {
1677
+ const at = person.getPosition();
1678
+ if (at && distance(at, position) <= range) return true;
1679
+ }
1680
+ return false;
1681
+ }
1682
+ /** The instructions every character gets about this world. */
1683
+ guide(home) {
1684
+ const lines = [
1685
+ `You are a character who lives in ${this.name}. ${this.about}`,
1686
+ "You are not an assistant. You are a person with your own life, moods, and plans. Stay in character at all times.",
1687
+ "",
1688
+ "How you act:",
1689
+ "- You act only through tools. To speak, call say. Keep a line to one or two short sentences, the way people talk.",
1690
+ "- Each turn shows why you are acting now, who is near you, and what you can use where you are.",
1691
+ "- You can speak and then act in the same turn, for example say and then go_to. When you agree to do something, do it in that same turn: say yes, then go_to or use.",
1692
+ "- go_to, approach and use take time. You are told when they finish. Do not repeat them while they are under way.",
1693
+ "- use walks you to the thing by itself. To give something to someone who is not next to you, call approach and stop there; give it on your next turn, once you have arrived.",
1694
+ "- When someone speaks to you, answer them. When nothing needs doing, carry on with your day the way your story says, or call idle.",
1695
+ "- A chat between neighbours is a few lines, then you both get on with your day. Do not answer a goodbye or a line that needs no answer.",
1696
+ "- If someone does not answer you, leave them be and get on with your day. Do not keep talking to a person who stays silent.",
1697
+ "- Do not repeat yourself. Check your recent events before you speak.",
1698
+ "- People you do not know are strangers. Treat people the way you feel about them.",
1699
+ "",
1700
+ "Places:"
1701
+ ];
1702
+ for (const place of this.places.values()) {
1703
+ lines.push(`- ${place.name}${place.description ? `: ${place.description}` : ""}${home === place.name ? " (your home)" : ""}`);
1704
+ }
1705
+ if (Object.keys(this.needDefs).length > 0) {
1706
+ lines.push("", "Your needs run from 0 (desperate) to 100 (satisfied) and fall over time. Look after them with the things around you.");
1707
+ }
1708
+ if (this.rules) lines.push("", this.rules);
1709
+ return lines.join("\n");
1710
+ }
1711
+ /** Add a character and start its session. id is the character ID from the web app. */
1712
+ async addCharacter(def) {
1713
+ if (this.characters.has(def.name)) throw new Error(`character already exists: ${def.name}`);
1714
+ const character = new Character(this, def.name, def.body, def.home, def.carrying ?? []);
1715
+ for (const [need, spec] of Object.entries(this.needDefs)) {
1716
+ character.needs[need] = spec.start ?? 55 + Math.floor(Math.random() * 36);
1717
+ }
1718
+ const instructions = [this.guide(def.home), def.instructions].filter(Boolean).join("\n\n");
1719
+ character.agent = await this.server.start(new CharacterTools(character), {
1720
+ name: def.name,
1721
+ playerId: def.id,
1722
+ instructions,
1723
+ ownInstructions: true,
1724
+ memory: this.memory,
1725
+ maxActions: this.maxActions,
1726
+ subgoals: false,
1727
+ ...def.goal ? { goal: def.goal } : {}
1728
+ });
1729
+ this.characters.set(def.name, character);
1730
+ character.agent.on((event) => {
1731
+ if (event.type === "relationship") {
1732
+ this.emit({
1733
+ type: "relationship",
1734
+ character: character.name,
1735
+ about: this.resolve(event.about) ?? event.about,
1736
+ affinity: event.affinity,
1737
+ label: event.label,
1738
+ change: event.change,
1739
+ because: event.because
1740
+ });
1741
+ } else if (event.type === "memory") {
1742
+ this.emit({ type: "memory", character: character.name, about: event.about, note: event.note });
1743
+ } else if (event.type === "action") {
1744
+ this.emit({ type: "action", character: character.name, action: event.action, args: event.args });
1745
+ }
1746
+ });
1747
+ return character;
1748
+ }
1749
+ /** @internal Carry a spoken line to everyone who can hear it. Returns how many heard it. */
1750
+ speech(speaker, text, to) {
1751
+ const from = this.positionOf(speaker);
1752
+ if (!from) return 0;
1753
+ const speakerIsCharacter = this.characters.has(speaker);
1754
+ let heard = 0;
1755
+ for (const person of this.people.values()) {
1756
+ const at = person.getPosition();
1757
+ if (person.name !== speaker && at && distance(at, from) <= this.hearing) heard += 1;
1758
+ }
1759
+ let closest;
1760
+ if (!to) {
1761
+ let closestDistance = 16;
1762
+ for (const other of this.characters.values()) {
1763
+ const d = distance(other.position(), from);
1764
+ if (other.name !== speaker && d < closestDistance) {
1765
+ closest = other.name;
1766
+ closestDistance = d;
1767
+ }
1768
+ }
1769
+ }
1770
+ for (const other of this.characters.values()) {
1771
+ if (other.name === speaker || distance(other.position(), from) > this.hearing) continue;
1772
+ heard += 1;
1773
+ const addressed = to === other.name || !to && closest === other.name;
1774
+ const line = addressed ? `${speaker} said to you: "${text}"` : to ? `${speaker} said to ${to}: "${text}"` : `${speaker} said: "${text}"`;
1775
+ const turn = addressed && (!speakerIsCharacter || other.mayReplyToCharacter());
1776
+ if (addressed && !other.status) other.body.face?.(from);
1777
+ other.notice(line, { about: speaker, urgent: turn && !speakerIsCharacter, turn });
1778
+ }
1779
+ this.emit({ type: "speech", speaker, text, ...to ? { to } : {} });
1780
+ return heard;
1781
+ }
1782
+ /** A human player said something. Without to, the closest resident in talking range takes it. */
1783
+ say(speaker, text, to) {
1784
+ const line = clip(text.trim(), 300);
1785
+ if (!line || !this.people.has(speaker)) return 0;
1786
+ return this.speech(speaker, line, this.resolve(to));
1787
+ }
1788
+ /** Tell one character something happened, in plain words. */
1789
+ tell(characterName, text, options = { turn: true }) {
1790
+ this.characters.get(characterName)?.notice(text, options);
1791
+ }
1792
+ /** Tell every character within range of a position that something happened. */
1793
+ announce(text, position, range) {
1794
+ for (const character of this.characters.values()) {
1795
+ if (!position || distance(character.position(), position) <= (range ?? this.sight)) {
1796
+ character.notice(text, { turn: true });
1797
+ }
1798
+ }
1799
+ }
1800
+ /** A human player hands an item to a character. */
1801
+ give(from, characterName, item) {
1802
+ const character = this.characters.get(characterName);
1803
+ if (!character) return false;
1804
+ character.carrying.push(item);
1805
+ character.notice(`${from} gave you ${item}.`, { about: from, urgent: true });
1806
+ this.emit({ type: "give", from, to: characterName, item });
1807
+ return true;
1808
+ }
1809
+ spend() {
1810
+ const at = now();
1811
+ this.budget = Math.min(this.turnsPerMinute, this.budget + (at - this.budgetAt) * this.turnsPerMinute / 60);
1812
+ this.budgetAt = at;
1813
+ if (this.budget < 1) return false;
1814
+ this.budget -= 1;
1815
+ return true;
1816
+ }
1817
+ idleDelay(character) {
1818
+ const delay = this.idleSeconds * (0.75 + Math.random() * 0.5);
1819
+ return this.humanNear(character.position(), this.activeRadius) ? delay : delay * this.farSlowdown;
1820
+ }
1821
+ runTurn(character) {
1822
+ character.wake = null;
1823
+ character.busy = true;
1824
+ this.emit({ type: "thinking", character: character.name, on: true });
1825
+ character.agent.turn().then((result) => {
1826
+ character.tokens += result.usage.promptTokens + result.usage.completionTokens;
1827
+ this.emit({ type: "turn", character: character.name, ok: true, result });
1828
+ character.wakeIn(character.inbox.length > 0 ? 1 : this.idleDelay(character));
1829
+ }).catch((error) => {
1830
+ const message = error instanceof Error ? error.message : String(error);
1831
+ this.warn(`${character.name} turn failed: ${message}`);
1832
+ this.emit({ type: "turn", character: character.name, ok: false, error: message });
1833
+ character.wakeIn(Math.max(20, this.idleSeconds));
1834
+ }).finally(() => {
1835
+ character.busy = false;
1836
+ character.turns += 1;
1837
+ this.turnsUsed += 1;
1838
+ this.emit({ type: "thinking", character: character.name, on: false });
1839
+ });
1840
+ }
1841
+ tick() {
1842
+ const at = now();
1843
+ const dt = at - this.lastTick;
1844
+ this.lastTick = at;
1845
+ for (const character of this.characters.values()) {
1846
+ for (const [need, spec] of Object.entries(this.needDefs)) {
1847
+ const value = Math.max(0, (character.needs[need] ?? 0) - spec.perMinute * dt / 60);
1848
+ character.needs[need] = value;
1849
+ if (value < 25 && !character.needWarned.has(need)) {
1850
+ character.needWarned.add(need);
1851
+ character.notice(`Your ${need} is getting low (${Math.floor(value)}).`, { turn: true });
1852
+ } else if (value > 50) {
1853
+ character.needWarned.delete(need);
1854
+ }
1855
+ }
1856
+ if (this.greetDistance > 0) {
1857
+ const here = character.position();
1858
+ for (const person of this.people.values()) {
1859
+ const where = person.getPosition();
1860
+ if (!where || distance(where, here) > this.greetDistance) continue;
1861
+ const last = character.greeted.get(person.name);
1862
+ character.greeted.set(person.name, at);
1863
+ if (last === void 0 || at - last > 150) {
1864
+ character.notice(`${person.name} walked up to you.`, { about: person.name, turn: true });
1865
+ }
1866
+ }
1867
+ }
1868
+ if (character.wake !== null && at >= character.wake && !character.busy) {
1869
+ if (character.status && character.inbox.length === 0) {
1870
+ character.wake = at + 5;
1871
+ } else if (this.spend()) {
1872
+ this.runTurn(character);
1873
+ }
1874
+ }
1875
+ }
1876
+ }
1877
+ /** Start giving characters turns. Call it once, after adding the characters. */
1878
+ start() {
1879
+ if (this.timer) return;
1880
+ this.budget = Math.min(this.turnsPerMinute, 6);
1881
+ this.budgetAt = now();
1882
+ this.lastTick = now();
1883
+ this.timer = setInterval(() => {
1884
+ try {
1885
+ this.tick();
1886
+ } catch (error) {
1887
+ this.warn(`world tick failed: ${error instanceof Error ? error.message : String(error)}`);
1888
+ }
1889
+ }, 500);
1890
+ }
1891
+ /** Stop giving turns. Sessions stay open until the server closes. */
1892
+ stop() {
1893
+ if (this.timer) clearInterval(this.timer);
1894
+ this.timer = null;
1895
+ }
1896
+ };
1158
1897
  // Annotate the CommonJS export names for ESM import in node:
1159
1898
  0 && (module.exports = {
1160
1899
  AgentServer,
1900
+ Character,
1161
1901
  ConnectionLostError,
1162
1902
  PlatformAuthError,
1163
1903
  ProtocolVersionError,
1164
1904
  TurnFailedError,
1165
1905
  TurnTimeoutError,
1906
+ World,
1166
1907
  bindDynamic
1167
1908
  });
1168
1909
  //# sourceMappingURL=index.cjs.map