@whitesands/agent-player 1.1.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/CHANGELOG.md +17 -0
- package/README.md +60 -5
- package/dist/index.cjs +887 -50
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +321 -5
- package/dist/index.d.ts +321 -5
- package/dist/index.js +883 -49
- package/dist/index.js.map +1 -1
- package/dist/protocol.cjs +16 -3
- package/dist/protocol.cjs.map +1 -1
- package/dist/protocol.d.cts +31 -3
- package/dist/protocol.d.ts +31 -3
- package/dist/protocol.js +14 -2
- package/dist/protocol.js.map +1 -1
- package/dist/testing.cjs +1 -1
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.d.cts +2 -2
- package/dist/testing.d.ts +2 -2
- package/dist/testing.js +1 -1
- package/dist/testing.js.map +1 -1
- package/dist/{types-D0p6GrVs.d.cts → types-BcH7_ozx.d.ts} +43 -2
- package/dist/{types-DXM_HZHu.d.ts → types-gmAkYoQr.d.cts} +43 -2
- package/package.json +4 -3
package/dist/index.cjs
CHANGED
|
@@ -31,11 +31,14 @@ 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
|
-
TurnTimeoutError: () => TurnTimeoutError
|
|
39
|
+
TurnTimeoutError: () => TurnTimeoutError,
|
|
40
|
+
World: () => World,
|
|
41
|
+
bindDynamic: () => bindDynamic
|
|
39
42
|
});
|
|
40
43
|
module.exports = __toCommonJS(src_exports);
|
|
41
44
|
|
|
@@ -158,7 +161,7 @@ var import_ws = __toESM(require("ws"), 1);
|
|
|
158
161
|
|
|
159
162
|
// src/protocol.ts
|
|
160
163
|
var PROTOCOL_VERSION = 1;
|
|
161
|
-
var SDK_VERSION = "1.
|
|
164
|
+
var SDK_VERSION = "1.3.0";
|
|
162
165
|
var SESSION_RESUME_GRACE_MS = 3e4;
|
|
163
166
|
var DEFAULT_AGENT_INSTRUCTIONS = [
|
|
164
167
|
"You are a remote game agent. Tools are provided by the game.",
|
|
@@ -167,6 +170,17 @@ var DEFAULT_AGENT_INSTRUCTIONS = [
|
|
|
167
170
|
"The view's done list is what this character has already called in this session, oldest first. Use it when choosing the next action.",
|
|
168
171
|
"If an action tool returns an error, retry once with a corrected action. A second failure ends the turn as failed."
|
|
169
172
|
].join("\n");
|
|
173
|
+
var PLAYER_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
174
|
+
function requirePlayerId(value) {
|
|
175
|
+
const id = value?.trim() ?? "";
|
|
176
|
+
if (id.length === 0) {
|
|
177
|
+
throw new Error("playerId is required. Copy the character ID from the web app");
|
|
178
|
+
}
|
|
179
|
+
if (!PLAYER_ID.test(id)) {
|
|
180
|
+
throw new Error(`playerId "${id}" is not a character ID. Copy the ID from the character in the web app`);
|
|
181
|
+
}
|
|
182
|
+
return id;
|
|
183
|
+
}
|
|
170
184
|
function assertNever(value) {
|
|
171
185
|
throw new Error(`Unhandled protocol message: ${JSON.stringify(value)}`);
|
|
172
186
|
}
|
|
@@ -279,6 +293,14 @@ var PlatformConnection = class {
|
|
|
279
293
|
});
|
|
280
294
|
});
|
|
281
295
|
}
|
|
296
|
+
/** Reject a start, stop, or resume wait. No-op when nothing is waiting. */
|
|
297
|
+
failSessionEvent(sessionId, kind, error) {
|
|
298
|
+
const key = `session:${sessionId}:${kind}`;
|
|
299
|
+
const waiter = this.pending.get(key);
|
|
300
|
+
if (!waiter) return;
|
|
301
|
+
this.pending.delete(key);
|
|
302
|
+
waiter.reject(error);
|
|
303
|
+
}
|
|
282
304
|
createTransport(sessionId) {
|
|
283
305
|
return new SessionMuxTransport(sessionId, (envelope) => this.send(envelope));
|
|
284
306
|
}
|
|
@@ -377,6 +399,15 @@ var PlatformConnection = class {
|
|
|
377
399
|
waiter.resolve(envelope);
|
|
378
400
|
return;
|
|
379
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
|
+
}
|
|
380
411
|
if (envelope.type === "session_resumed" || envelope.type === "resume_err") {
|
|
381
412
|
const waiter = this.pending.get(`session:${envelope.sessionId}:resume`);
|
|
382
413
|
if (!waiter) return;
|
|
@@ -416,6 +447,8 @@ var PlatformConnection = class {
|
|
|
416
447
|
const sessions = this.handlers.sessions?.() ?? [];
|
|
417
448
|
for (const session of sessions) {
|
|
418
449
|
const resumed = this.waitForSessionEvent(session.sessionId, "resume");
|
|
450
|
+
resumed.catch(() => {
|
|
451
|
+
});
|
|
419
452
|
this.send({
|
|
420
453
|
type: "resume_session",
|
|
421
454
|
sessionId: session.sessionId,
|
|
@@ -482,6 +515,7 @@ var PlatformConnection = class {
|
|
|
482
515
|
var import_zod = require("zod");
|
|
483
516
|
var import_zod_to_json_schema = require("zod-to-json-schema");
|
|
484
517
|
var SKIP = /* @__PURE__ */ new Set(["constructor"]);
|
|
518
|
+
var dynamicTools = /* @__PURE__ */ new WeakMap();
|
|
485
519
|
var SEE_DESCRIPTION = "Read the current game view for this player. Prefer wait, which returns this after the game grants a turn.";
|
|
486
520
|
var WAIT_DESCRIPTION = "Block until the game calls turn() for this session, then return the current see() view.";
|
|
487
521
|
function getParamNames(fn) {
|
|
@@ -525,6 +559,56 @@ function formatZodError(error) {
|
|
|
525
559
|
return `${path}: ${issue.message}`;
|
|
526
560
|
}).join("; ");
|
|
527
561
|
}
|
|
562
|
+
function rejectDynamicName(name) {
|
|
563
|
+
if (name.length === 0 || name === "wait" || name.startsWith("_")) {
|
|
564
|
+
throw new Error(`cannot publish action "${name}"`);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
function dynamicBindings(player) {
|
|
568
|
+
return dynamicTools.get(player) ?? /* @__PURE__ */ new Map();
|
|
569
|
+
}
|
|
570
|
+
function bindDynamic(player, name, description, args, callback) {
|
|
571
|
+
rejectDynamicName(name);
|
|
572
|
+
if (typeof callback !== "function") {
|
|
573
|
+
throw new Error(`cannot publish action "${name}"`);
|
|
574
|
+
}
|
|
575
|
+
const shape = args && Object.keys(args).length > 0 ? { ...args } : void 0;
|
|
576
|
+
let tools = dynamicTools.get(player);
|
|
577
|
+
if (!tools) {
|
|
578
|
+
tools = /* @__PURE__ */ new Map();
|
|
579
|
+
dynamicTools.set(player, tools);
|
|
580
|
+
}
|
|
581
|
+
tools.set(name, {
|
|
582
|
+
name,
|
|
583
|
+
description: description.length > 0 ? description : `Action: ${name}`,
|
|
584
|
+
...shape ? { args: shape } : {},
|
|
585
|
+
callback
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
function buildDynamicTool(binding) {
|
|
589
|
+
const shape = binding.args;
|
|
590
|
+
const paramNames = shape ? Object.keys(shape) : [];
|
|
591
|
+
const argsSchema = shape ? import_zod.z.object(shape) : void 0;
|
|
592
|
+
const inputSchema = argsSchema ? toJsonSchema(argsSchema) : { type: "object", properties: {} };
|
|
593
|
+
return {
|
|
594
|
+
name: binding.name,
|
|
595
|
+
description: binding.description,
|
|
596
|
+
paramNames,
|
|
597
|
+
...argsSchema ? { argsSchema } : {},
|
|
598
|
+
descriptor: { name: binding.name, description: binding.description, inputSchema },
|
|
599
|
+
async invoke(args) {
|
|
600
|
+
let parsed = {};
|
|
601
|
+
if (argsSchema) {
|
|
602
|
+
const result = argsSchema.safeParse(args ?? {});
|
|
603
|
+
if (!result.success) {
|
|
604
|
+
throw new Error(formatZodError(result.error));
|
|
605
|
+
}
|
|
606
|
+
parsed = result.data;
|
|
607
|
+
}
|
|
608
|
+
return await Promise.resolve(binding.callback(parsed));
|
|
609
|
+
}
|
|
610
|
+
};
|
|
611
|
+
}
|
|
528
612
|
function buildTool(instance, name, description, spec) {
|
|
529
613
|
const fn = instance[name];
|
|
530
614
|
if (typeof fn !== "function") {
|
|
@@ -560,24 +644,31 @@ function buildTool(instance, name, description, spec) {
|
|
|
560
644
|
}
|
|
561
645
|
function reflectPlayer(instance) {
|
|
562
646
|
const names = collectMethodNames(instance);
|
|
563
|
-
|
|
647
|
+
const dynamic = dynamicBindings(instance);
|
|
648
|
+
if (!names.includes("see") && !dynamic.has("see")) {
|
|
564
649
|
throw new Error("Player must implement a public see() method");
|
|
565
650
|
}
|
|
566
651
|
const metadata = readMetadata(instance);
|
|
567
652
|
const declared = metadata?.actions ?? {};
|
|
568
653
|
const className = instance.constructor?.name || "Player";
|
|
569
654
|
for (const name of Object.keys(declared)) {
|
|
570
|
-
if (!names.includes(name)
|
|
655
|
+
if (name === "see" || !names.includes(name) && !dynamic.has(name)) {
|
|
571
656
|
throw new Error(`Declared action "${name}" is not a public method on ${className}`);
|
|
572
657
|
}
|
|
573
658
|
}
|
|
574
|
-
const
|
|
575
|
-
const
|
|
576
|
-
const actions =
|
|
659
|
+
const dynamicSee = dynamic.get("see");
|
|
660
|
+
const see = dynamicSee ? buildDynamicTool(dynamicSee) : buildTool(instance, "see", SEE_DESCRIPTION);
|
|
661
|
+
const actions = [];
|
|
662
|
+
for (const name of names) {
|
|
663
|
+
if (name === "see" || dynamic.has(name)) continue;
|
|
577
664
|
const spec = declared[name];
|
|
578
665
|
const description = spec?.description ?? `Action: ${name}`;
|
|
579
|
-
|
|
580
|
-
}
|
|
666
|
+
actions.push(buildTool(instance, name, description, spec));
|
|
667
|
+
}
|
|
668
|
+
for (const [name, binding] of dynamic) {
|
|
669
|
+
if (name === "see") continue;
|
|
670
|
+
actions.push(buildDynamicTool(binding));
|
|
671
|
+
}
|
|
581
672
|
const wait = {
|
|
582
673
|
name: "wait",
|
|
583
674
|
description: WAIT_DESCRIPTION,
|
|
@@ -826,13 +917,14 @@ var AgentServer = class {
|
|
|
826
917
|
* `turn()` on that handle lets the remote model act once.
|
|
827
918
|
*/
|
|
828
919
|
async start(instance, options) {
|
|
920
|
+
const playerId = requirePlayerId(options.playerId);
|
|
829
921
|
await this.connect();
|
|
830
922
|
const reflected = reflectPlayer(instance);
|
|
831
923
|
const sessionId = (0, import_node_crypto2.randomUUID)();
|
|
832
924
|
const sessionToken = (0, import_node_crypto2.randomUUID)();
|
|
833
925
|
const gate = new TurnGate();
|
|
834
926
|
const name = options?.name ?? instance.constructor?.name ?? "player";
|
|
835
|
-
const memory = this.actionMemory();
|
|
927
|
+
const memory = options?.memory ? null : this.actionMemory();
|
|
836
928
|
const server = createSessionMcpServer(reflected, gate, {
|
|
837
929
|
name: `agent-player:${name}`,
|
|
838
930
|
...memory ? {
|
|
@@ -842,7 +934,10 @@ var AgentServer = class {
|
|
|
842
934
|
}
|
|
843
935
|
} : {}
|
|
844
936
|
});
|
|
845
|
-
const instructions = [
|
|
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");
|
|
846
941
|
const listeners = /* @__PURE__ */ new Set();
|
|
847
942
|
const record = {
|
|
848
943
|
id: sessionId,
|
|
@@ -851,47 +946,70 @@ var AgentServer = class {
|
|
|
851
946
|
gate,
|
|
852
947
|
server,
|
|
853
948
|
transport: void 0,
|
|
854
|
-
listeners
|
|
949
|
+
listeners,
|
|
950
|
+
memory: options?.memory === true,
|
|
951
|
+
relationships: /* @__PURE__ */ new Map()
|
|
855
952
|
};
|
|
856
953
|
const onTurnResult = (result) => {
|
|
857
954
|
gate.completeTurn(result);
|
|
858
955
|
};
|
|
859
956
|
const onEvent = (event) => this.emit(event);
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
957
|
+
this.sessions.set(sessionId, record);
|
|
958
|
+
try {
|
|
959
|
+
if (this.inline) {
|
|
960
|
+
const [serverTransport, clientTransport] = import_inMemory.InMemoryTransport.createLinkedPair();
|
|
961
|
+
await server.connect(serverTransport);
|
|
962
|
+
record.transport = serverTransport;
|
|
963
|
+
await this.inline.startSession({
|
|
964
|
+
sessionId,
|
|
965
|
+
sessionToken,
|
|
966
|
+
playerId,
|
|
967
|
+
name,
|
|
968
|
+
tools: reflected.tools,
|
|
969
|
+
instructions,
|
|
970
|
+
...options.goal ? { goal: options.goal } : {},
|
|
971
|
+
...this.logger ? { logger: this.logger } : {},
|
|
972
|
+
transport: clientTransport,
|
|
973
|
+
onTurnResult,
|
|
974
|
+
onEvent
|
|
975
|
+
});
|
|
976
|
+
} else {
|
|
977
|
+
if (!this.connection) throw new Error("Not connected");
|
|
978
|
+
const mux = this.connection.createTransport(sessionId);
|
|
979
|
+
await server.connect(mux);
|
|
980
|
+
record.transport = mux;
|
|
981
|
+
record.mux = mux;
|
|
982
|
+
const started = this.connection.waitForSessionEvent(sessionId, "start");
|
|
983
|
+
try {
|
|
984
|
+
this.connection.send({
|
|
985
|
+
type: "start_session",
|
|
986
|
+
sessionId,
|
|
987
|
+
sessionToken,
|
|
988
|
+
playerId,
|
|
989
|
+
name,
|
|
990
|
+
tools: reflected.tools,
|
|
991
|
+
instructions,
|
|
992
|
+
...options.goal ? { goal: options.goal } : {},
|
|
993
|
+
...options.memory ? { memory: true } : {},
|
|
994
|
+
...options.maxActions ? { maxActions: options.maxActions } : {},
|
|
995
|
+
...options.subgoals === false ? { subgoals: false } : {}
|
|
996
|
+
});
|
|
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
|
+
}
|
|
1003
|
+
} catch (error) {
|
|
1004
|
+
const wrapped = error instanceof Error ? error : new Error(String(error));
|
|
1005
|
+
this.connection.failSessionEvent(sessionId, "start", wrapped);
|
|
1006
|
+
await started.catch(() => void 0);
|
|
1007
|
+
throw wrapped;
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
} catch (error) {
|
|
1011
|
+
await this.stopSession(sessionId);
|
|
1012
|
+
throw error;
|
|
895
1013
|
}
|
|
896
1014
|
return {
|
|
897
1015
|
sessionId,
|
|
@@ -900,7 +1018,15 @@ var AgentServer = class {
|
|
|
900
1018
|
on: (listener) => {
|
|
901
1019
|
listeners.add(listener);
|
|
902
1020
|
return () => listeners.delete(listener);
|
|
903
|
-
}
|
|
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)
|
|
904
1030
|
};
|
|
905
1031
|
}
|
|
906
1032
|
/** Subscribe to every session plus connection events. Returns unsubscribe. */
|
|
@@ -933,7 +1059,8 @@ var AgentServer = class {
|
|
|
933
1059
|
args: outcome.args ?? {},
|
|
934
1060
|
result: outcome.result,
|
|
935
1061
|
attempts: outcome.attempts ?? 1,
|
|
936
|
-
usage: outcome.usage ?? { promptTokens: 0, completionTokens: 0 }
|
|
1062
|
+
usage: outcome.usage ?? { promptTokens: 0, completionTokens: 0 },
|
|
1063
|
+
...outcome.steps ? { steps: outcome.steps } : {}
|
|
937
1064
|
};
|
|
938
1065
|
}
|
|
939
1066
|
emit(event) {
|
|
@@ -980,6 +1107,33 @@ var AgentServer = class {
|
|
|
980
1107
|
});
|
|
981
1108
|
return;
|
|
982
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;
|
|
983
1137
|
this.emit({
|
|
984
1138
|
type: "turnFailed",
|
|
985
1139
|
sessionId: envelope.sessionId,
|
|
@@ -991,6 +1145,9 @@ var AgentServer = class {
|
|
|
991
1145
|
}
|
|
992
1146
|
case "error":
|
|
993
1147
|
log(this.logger, "error", envelope.message);
|
|
1148
|
+
if (envelope.sessionId) {
|
|
1149
|
+
this.connection?.failSessionEvent(envelope.sessionId, "start", new Error(envelope.message));
|
|
1150
|
+
}
|
|
994
1151
|
return;
|
|
995
1152
|
case "session_started":
|
|
996
1153
|
case "session_stopped":
|
|
@@ -1004,6 +1161,7 @@ var AgentServer = class {
|
|
|
1004
1161
|
case "ping":
|
|
1005
1162
|
case "pong":
|
|
1006
1163
|
case "resume_session":
|
|
1164
|
+
case "observe":
|
|
1007
1165
|
return;
|
|
1008
1166
|
default: {
|
|
1009
1167
|
assertNever(envelope);
|
|
@@ -1060,13 +1218,692 @@ var AgentServer = class {
|
|
|
1060
1218
|
await this.close();
|
|
1061
1219
|
}
|
|
1062
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
|
+
};
|
|
1063
1897
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1064
1898
|
0 && (module.exports = {
|
|
1065
1899
|
AgentServer,
|
|
1900
|
+
Character,
|
|
1066
1901
|
ConnectionLostError,
|
|
1067
1902
|
PlatformAuthError,
|
|
1068
1903
|
ProtocolVersionError,
|
|
1069
1904
|
TurnFailedError,
|
|
1070
|
-
TurnTimeoutError
|
|
1905
|
+
TurnTimeoutError,
|
|
1906
|
+
World,
|
|
1907
|
+
bindDynamic
|
|
1071
1908
|
});
|
|
1072
1909
|
//# sourceMappingURL=index.cjs.map
|