@ifsoul/core 0.3.0 → 0.5.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.
Files changed (147) hide show
  1. package/README.md +30 -33
  2. package/dist/packages/memory/src/sql.js +54 -5
  3. package/dist/packages/protocol/src/generated/validators.cjs +1 -1
  4. package/dist/packages/protocol/src/index.d.ts +1 -6
  5. package/dist/packages/protocol/src/index.js +3 -16
  6. package/dist/packages/protocol/src/types.d.ts +1 -1
  7. package/dist/packages/runtime/src/behavior-types.d.ts +150 -0
  8. package/dist/packages/runtime/src/behavior-types.js +1 -0
  9. package/dist/packages/runtime/src/behavior.d.ts +40 -0
  10. package/dist/packages/runtime/src/behavior.js +621 -0
  11. package/dist/packages/runtime/src/group-scheduler.d.ts +165 -0
  12. package/dist/packages/runtime/src/group-scheduler.js +647 -0
  13. package/dist/packages/runtime/src/index.d.ts +4 -0
  14. package/dist/packages/runtime/src/index.js +4 -1
  15. package/dist/packages/runtime/src/participants.d.ts +107 -0
  16. package/dist/packages/runtime/src/participants.js +309 -0
  17. package/dist/packages/runtime/src/types.d.ts +1 -0
  18. package/package.json +2 -3
  19. package/packages/protocol/schemas/current/archive.schema.json +16 -16
  20. package/packages/protocol/schemas/current/character.schema.json +21 -21
  21. package/packages/protocol/schemas/current/common.schema.json +15 -15
  22. package/packages/protocol/schemas/current/context.schema.json +24 -24
  23. package/packages/protocol/schemas/current/conversation.schema.json +39 -39
  24. package/packages/protocol/schemas/current/environment.schema.json +18 -18
  25. package/packages/protocol/schemas/current/memory.schema.json +66 -66
  26. package/packages/protocol/schemas/current/model-provider.schema.json +40 -40
  27. package/packages/protocol/schemas/current/planning.schema.json +61 -61
  28. package/packages/protocol/schemas/current/provider-common.schema.json +6 -6
  29. package/release.json +7 -12
  30. package/specs/README.md +3 -86
  31. package/specs/current/README.md +3 -5
  32. package/specs/current/examples/archive-fork.json +12 -12
  33. package/specs/current/examples/archive-resume.json +12 -12
  34. package/specs/current/examples/character-instance.json +1 -1
  35. package/specs/current/examples/character-template.json +1 -1
  36. package/specs/current/examples/context-after-inspection.json +1 -1
  37. package/specs/current/examples/context-chat.json +1 -1
  38. package/specs/current/examples/context-daily.json +1 -1
  39. package/specs/current/examples/context-no-speech.json +1 -1
  40. package/specs/current/examples/conversation-delivery.json +1 -1
  41. package/specs/current/examples/conversation-live-request.json +1 -1
  42. package/specs/current/examples/conversation-live-response.json +1 -1
  43. package/specs/current/examples/conversation-no-speech-request.json +1 -1
  44. package/specs/current/examples/conversation-no-speech-response.json +1 -1
  45. package/specs/current/examples/conversation-requested-request.json +1 -1
  46. package/specs/current/examples/conversation-requested-response.json +1 -1
  47. package/specs/current/examples/environment-chat.json +1 -1
  48. package/specs/current/examples/environment-game.json +1 -1
  49. package/specs/current/examples/environment-no-speech.json +1 -1
  50. package/specs/current/examples/intent-checkpoint.json +1 -1
  51. package/specs/current/examples/memory-episode.json +1 -1
  52. package/specs/current/examples/memory-event-conversation.json +1 -1
  53. package/specs/current/examples/memory-event-observed.json +2 -2
  54. package/specs/current/examples/memory-event-plan-proposal.json +2 -2
  55. package/specs/current/examples/memory-event-proposed.json +2 -2
  56. package/specs/current/examples/memory-fact.json +1 -1
  57. package/specs/current/examples/memory-relationship.json +1 -1
  58. package/specs/current/examples/memory-working-context.json +1 -1
  59. package/specs/current/examples/plan-cancel-request.json +1 -1
  60. package/specs/current/examples/plan-cancel-result.json +1 -1
  61. package/specs/current/examples/plan-daily.json +1 -1
  62. package/specs/current/examples/plan-partial-decision.json +1 -1
  63. package/specs/current/examples/portable-memory.jsonl +9 -9
  64. package/specs/current/examples/task-outcome.json +1 -1
  65. package/specs/current/examples/task-progress.json +1 -1
  66. package/specs/current/model-provider/examples/capabilities.json +1 -1
  67. package/specs/current/model-provider/examples/error.json +1 -1
  68. package/specs/current/model-provider/examples/request.json +1 -1
  69. package/specs/current/model-provider/examples/response.json +1 -1
  70. package/docs/npm-publishing.md +0 -44
  71. package/packages/protocol/schemas/action-result.schema.json +0 -187
  72. package/packages/protocol/schemas/action.schema.json +0 -68
  73. package/packages/protocol/schemas/archive-manifest.schema.json +0 -126
  74. package/packages/protocol/schemas/capability.schema.json +0 -131
  75. package/packages/protocol/schemas/character-instance.schema.json +0 -73
  76. package/packages/protocol/schemas/character-template.schema.json +0 -67
  77. package/packages/protocol/schemas/common.schema.json +0 -154
  78. package/packages/protocol/schemas/derived-memory.schema.json +0 -145
  79. package/packages/protocol/schemas/memory-event.schema.json +0 -123
  80. package/packages/protocol/schemas/model-provider.schema.json +0 -326
  81. package/packages/protocol/schemas/perception.schema.json +0 -89
  82. package/packages/protocol/schemas/permission-policy.schema.json +0 -153
  83. package/packages/protocol/schemas/v0.2/archive.schema.json +0 -205
  84. package/packages/protocol/schemas/v0.2/character.schema.json +0 -344
  85. package/packages/protocol/schemas/v0.2/common.schema.json +0 -239
  86. package/packages/protocol/schemas/v0.2/context.schema.json +0 -230
  87. package/packages/protocol/schemas/v0.2/conversation.schema.json +0 -340
  88. package/packages/protocol/schemas/v0.2/environment.schema.json +0 -244
  89. package/packages/protocol/schemas/v0.2/memory.schema.json +0 -495
  90. package/packages/protocol/schemas/v0.2/planning.schema.json +0 -443
  91. package/packages/protocol/schemas/world-event.schema.json +0 -74
  92. package/specs/character/README.md +0 -84
  93. package/specs/character/examples/archive-manifest.json +0 -32
  94. package/specs/character/examples/character-instance.json +0 -20
  95. package/specs/character/examples/character-template.json +0 -19
  96. package/specs/memory/README.md +0 -80
  97. package/specs/memory/examples/derived-memory.json +0 -21
  98. package/specs/memory/examples/memory-event.json +0 -35
  99. package/specs/model-provider/README.md +0 -85
  100. package/specs/model-provider/examples/capabilities.json +0 -10
  101. package/specs/model-provider/examples/error.json +0 -18
  102. package/specs/model-provider/examples/request.json +0 -17
  103. package/specs/model-provider/examples/response.json +0 -19
  104. package/specs/v0.2/README.md +0 -421
  105. package/specs/v0.2/examples/archive-fork.json +0 -64
  106. package/specs/v0.2/examples/archive-resume.json +0 -63
  107. package/specs/v0.2/examples/character-instance.json +0 -57
  108. package/specs/v0.2/examples/character-template.json +0 -45
  109. package/specs/v0.2/examples/context-after-inspection.json +0 -44
  110. package/specs/v0.2/examples/context-chat.json +0 -27
  111. package/specs/v0.2/examples/context-daily.json +0 -69
  112. package/specs/v0.2/examples/context-no-speech.json +0 -27
  113. package/specs/v0.2/examples/conversation-delivery.json +0 -17
  114. package/specs/v0.2/examples/conversation-live-request.json +0 -31
  115. package/specs/v0.2/examples/conversation-live-response.json +0 -23
  116. package/specs/v0.2/examples/conversation-no-speech-request.json +0 -23
  117. package/specs/v0.2/examples/conversation-no-speech-response.json +0 -22
  118. package/specs/v0.2/examples/conversation-requested-request.json +0 -31
  119. package/specs/v0.2/examples/conversation-requested-response.json +0 -23
  120. package/specs/v0.2/examples/environment-chat.json +0 -43
  121. package/specs/v0.2/examples/environment-game.json +0 -80
  122. package/specs/v0.2/examples/environment-no-speech.json +0 -47
  123. package/specs/v0.2/examples/intent-checkpoint.json +0 -29
  124. package/specs/v0.2/examples/memory-episode.json +0 -28
  125. package/specs/v0.2/examples/memory-event-conversation.json +0 -32
  126. package/specs/v0.2/examples/memory-event-observed.json +0 -50
  127. package/specs/v0.2/examples/memory-event-plan-proposal.json +0 -82
  128. package/specs/v0.2/examples/memory-event-proposed.json +0 -53
  129. package/specs/v0.2/examples/memory-fact.json +0 -23
  130. package/specs/v0.2/examples/memory-relationship.json +0 -25
  131. package/specs/v0.2/examples/memory-working-context.json +0 -24
  132. package/specs/v0.2/examples/plan-cancel-request.json +0 -16
  133. package/specs/v0.2/examples/plan-cancel-result.json +0 -21
  134. package/specs/v0.2/examples/plan-daily.json +0 -57
  135. package/specs/v0.2/examples/plan-partial-decision.json +0 -28
  136. package/specs/v0.2/examples/portable-memory.jsonl +0 -9
  137. package/specs/v0.2/examples/task-outcome.json +0 -21
  138. package/specs/v0.2/examples/task-progress.json +0 -19
  139. package/specs/world-adapter/README.md +0 -114
  140. package/specs/world-adapter/examples/action-result-unknown.json +0 -23
  141. package/specs/world-adapter/examples/action-result.json +0 -45
  142. package/specs/world-adapter/examples/action.json +0 -17
  143. package/specs/world-adapter/examples/capability.json +0 -44
  144. package/specs/world-adapter/examples/perception.json +0 -30
  145. package/specs/world-adapter/examples/permission-decision.json +0 -14
  146. package/specs/world-adapter/examples/permission-policy.json +0 -17
  147. package/specs/world-adapter/examples/world-event.json +0 -25
@@ -0,0 +1,621 @@
1
+ import { SessionError } from "./participants.js";
2
+ import { canonical } from "../../protocol/src/index.js";
3
+ import { resolveResourceLimits } from "../../protocol/src/resource-policy.js";
4
+ const permissions = [
5
+ "observe",
6
+ "remember",
7
+ "updateRelationship",
8
+ "speak",
9
+ "requestAction",
10
+ ];
11
+ export const DEFAULT_BEHAVIOR_OPERATOR = Object.freeze({
12
+ canEditConfig: false,
13
+ lockedParameterIds: [],
14
+ deniedPermissions: [],
15
+ lockedPolicy: false,
16
+ lockedGoals: false,
17
+ });
18
+ export const DEFAULT_BEHAVIOR_LIMITS = Object.freeze({
19
+ parameters: 32,
20
+ overrides: 128,
21
+ values: 4096,
22
+ history: 8192,
23
+ observations: 4096,
24
+ proposals: 2048,
25
+ changesPerProposal: 8,
26
+ evidencePerChange: 16,
27
+ });
28
+ const failure = (code) => {
29
+ throw new SessionError(code);
30
+ };
31
+ const id = (value) => typeof value === "string" &&
32
+ /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(value);
33
+ const finite = (value) => typeof value === "number" &&
34
+ Number.isFinite(value) &&
35
+ Math.abs(value) <= Number.MAX_SAFE_INTEGER;
36
+ const obj = (value) => !!value && typeof value === "object" && !Array.isArray(value);
37
+ const keys = (value, allowed) => obj(value) && Object.keys(value).every((k) => allowed.includes(k));
38
+ const same = (a, b) => canonical(a) === canonical(b);
39
+ const unique = (values, maximum) => Array.isArray(values) &&
40
+ values.length <= maximum &&
41
+ values.every(id) &&
42
+ new Set(values).size === values.length;
43
+ const revision = (state, npcId) => Object.hasOwn(state.stateRevisions, npcId) ? state.stateRevisions[npcId] : 0;
44
+ export function defaultBehaviorConfiguration() {
45
+ const general = {
46
+ observe: true,
47
+ remember: true,
48
+ updateRelationship: true,
49
+ speak: true,
50
+ requestAction: true,
51
+ };
52
+ return {
53
+ version: "ifsoul-behavior/0.4.0",
54
+ revision: 0,
55
+ parameters: ["trust", "cooperation", "caution"].map((parameterId) => ({
56
+ parameterId,
57
+ label: parameterId[0].toUpperCase() + parameterId.slice(1),
58
+ kind: "relationship",
59
+ scope: "portable",
60
+ initial: 50,
61
+ min: 0,
62
+ max: 100,
63
+ maxDelta: 5,
64
+ })),
65
+ defaults: {
66
+ policy: {
67
+ general,
68
+ provocation: { ...general, speak: false, requestAction: false },
69
+ },
70
+ goals: [],
71
+ },
72
+ overrides: [],
73
+ };
74
+ }
75
+ const base = (s) => {
76
+ if (s.behavior && s.behavior.format !== "ifsoul-behavior/0.4.0")
77
+ return failure("invalid");
78
+ return s.behavior?.config ?? defaultBehaviorConfiguration();
79
+ };
80
+ const requireState = (s, create = false) => {
81
+ if (s.behavior) {
82
+ if (s.behavior.format !== "ifsoul-behavior/0.4.0")
83
+ return failure("invalid");
84
+ return s.behavior;
85
+ }
86
+ if (!create)
87
+ return failure("invalid");
88
+ return (s.behavior = {
89
+ format: "ifsoul-behavior/0.4.0",
90
+ config: defaultBehaviorConfiguration(),
91
+ observations: [],
92
+ values: [],
93
+ history: [],
94
+ proposals: [],
95
+ stateRevisions: {},
96
+ });
97
+ };
98
+ function sessionFor(s, a, sessionId, npcId) {
99
+ const session = s.sessions.find((x) => x.sessionId === sessionId && x.hostId === a.hostId);
100
+ if (!session ||
101
+ !session.members.some((m) => m.participantId === npcId && m.role === "npc"))
102
+ return failure("unauthorized");
103
+ return session;
104
+ }
105
+ /** World -> matching groups in lexicographic ID order -> NPC -> immutable operator denies. */
106
+ export function effectiveBehavior(s, access, npcId, sessionId, operator = DEFAULT_BEHAVIOR_OPERATOR) {
107
+ const session = sessionFor(s, access, sessionId, npcId);
108
+ const config = base(s), effective = structuredClone({ ...config.defaults, initialValues: {} });
109
+ const groups = session.groups
110
+ .filter((g) => g.participantIds.includes(npcId))
111
+ .map((g) => g.groupId)
112
+ .sort();
113
+ const scopes = [
114
+ { kind: "world", id: session.worldId },
115
+ ...groups.map((id) => ({ kind: "group", id })),
116
+ { kind: "npc", id: npcId },
117
+ ];
118
+ for (const scope of scopes) {
119
+ const override = config.overrides.find((x) => x.scope.kind === scope.kind && x.scope.id === scope.id);
120
+ if (!override)
121
+ continue;
122
+ for (const category of ["general", "provocation"])
123
+ Object.assign(effective.policy[category], override.policy?.[category]);
124
+ if (override.goals)
125
+ effective.goals = structuredClone(override.goals);
126
+ if (override.initialValues)
127
+ Object.assign(effective.initialValues, override.initialValues);
128
+ }
129
+ for (const flag of operator.deniedPermissions)
130
+ for (const category of ["general", "provocation"])
131
+ effective.policy[category][flag] = false;
132
+ return effective;
133
+ }
134
+ export function behaviorPermissions(s, access, npcId, sessionId, eventId, operator = DEFAULT_BEHAVIOR_OPERATOR) {
135
+ const effective = effectiveBehavior(s, access, npcId, sessionId, operator);
136
+ const kind = s.behavior?.observations.find((x) => x.npcId === npcId && x.eventId === eventId)?.kind;
137
+ return effective.policy[kind === "provocation" ? "provocation" : "general"];
138
+ }
139
+ export const behaviorRevision = (s) => s.behavior?.config.revision ?? 0;
140
+ /** Previously unobserved/unremembered evidence never becomes implicit model context. */
141
+ export function behaviorEvidenceVisible(s, access, npcId, sessionId, eventId, operator = DEFAULT_BEHAVIOR_OPERATOR) {
142
+ sessionFor(s, access, sessionId, npcId);
143
+ const event = s.events.find((e) => e.eventId === eventId);
144
+ if (!event ||
145
+ !s.sessions.some((source) => source.sessionId === event.sessionId && source.hostId === access.hostId))
146
+ return false;
147
+ if (!s.behavior)
148
+ return true;
149
+ const observation = s.behavior.observations.find((x) => x.npcId === npcId && x.eventId === eventId);
150
+ const current = behaviorPermissions(s, access, npcId, sessionId, eventId, operator);
151
+ return (!!observation &&
152
+ observation.permissions.observe &&
153
+ observation.permissions.remember &&
154
+ current.observe &&
155
+ current.remember);
156
+ }
157
+ function validGoals(value) {
158
+ return (Array.isArray(value) &&
159
+ value.length <= 16 &&
160
+ unique(value.map((g) => g?.goalId), 16) &&
161
+ value.every((g) => keys(g, ["goalId", "objective", "priority", "enabled"]) &&
162
+ typeof g.objective === "string" &&
163
+ g.objective.length > 0 &&
164
+ g.objective.length <= 512 &&
165
+ Number.isInteger(g.priority) &&
166
+ g.priority >= 0 &&
167
+ g.priority <= 100 &&
168
+ typeof g.enabled === "boolean"));
169
+ }
170
+ const flags = (value, partial = false) => keys(value, permissions) &&
171
+ Object.values(value).every((x) => typeof x === "boolean") &&
172
+ (partial || Object.keys(value).length === permissions.length);
173
+ const policy = (value, partial = false) => keys(value, ["general", "provocation"]) &&
174
+ (partial || Object.keys(value).length === 2) &&
175
+ Object.values(value).every((x) => flags(x, partial));
176
+ function definition(value) {
177
+ return (keys(value, [
178
+ "parameterId",
179
+ "label",
180
+ "kind",
181
+ "scope",
182
+ "initial",
183
+ "min",
184
+ "max",
185
+ "maxDelta",
186
+ "decay",
187
+ ]) &&
188
+ id(value.parameterId) &&
189
+ typeof value.label === "string" &&
190
+ value.label.length > 0 &&
191
+ value.label.length <= 80 &&
192
+ ["trait", "state", "relationship"].includes(value.kind) &&
193
+ ["portable", "world"].includes(value.scope) &&
194
+ [value.initial, value.min, value.max, value.maxDelta].every(finite) &&
195
+ value.min < value.max &&
196
+ value.initial >= value.min &&
197
+ value.initial <= value.max &&
198
+ value.maxDelta > 0 &&
199
+ value.maxDelta <= value.max - value.min &&
200
+ !(value.kind === "trait" && (value.scope !== "portable" || value.decay)) &&
201
+ (value.decay === undefined ||
202
+ (keys(value.decay, ["perHour", "toward"]) &&
203
+ finite(value.decay.perHour) &&
204
+ value.decay.perHour > 0 &&
205
+ finite(value.decay.toward) &&
206
+ value.decay.toward >= value.min &&
207
+ value.decay.toward <= value.max)));
208
+ }
209
+ const valueKey = (value) => JSON.stringify([
210
+ value.npcId,
211
+ value.parameterId,
212
+ value.targetParticipantId ?? null,
213
+ value.worldId ?? null,
214
+ ]);
215
+ export function createBehaviorRuntime(store, options = {}) {
216
+ const operator = structuredClone(options.operator ?? DEFAULT_BEHAVIOR_OPERATOR);
217
+ if (!keys(operator, [
218
+ "canEditConfig",
219
+ "lockedParameterIds",
220
+ "deniedPermissions",
221
+ "lockedPolicy",
222
+ "lockedGoals",
223
+ ]) ||
224
+ typeof operator.canEditConfig !== "boolean" ||
225
+ !unique(operator.lockedParameterIds, 128) ||
226
+ !Array.isArray(operator.deniedPermissions) ||
227
+ operator.deniedPermissions.some((x) => !permissions.includes(x)) ||
228
+ typeof operator.lockedPolicy !== "boolean" ||
229
+ typeof operator.lockedGoals !== "boolean")
230
+ throw new RangeError("Invalid behavior operator policy");
231
+ const report = resolveResourceLimits({
232
+ layer: "behavior",
233
+ defaults: { ...DEFAULT_BEHAVIOR_LIMITS },
234
+ legacyCeilings: { ...DEFAULT_BEHAVIOR_LIMITS },
235
+ requested: options.limits,
236
+ ceilings: options.ceilings,
237
+ });
238
+ if (report.status !== "accepted")
239
+ throw new RangeError("Unsupported behavior limit");
240
+ const limits = report.effective;
241
+ const run = (a, work) => {
242
+ if (!a || !id(a.principalId) || !id(a.hostId))
243
+ return failure("unauthorized");
244
+ return store.transact(a.principalId, (s) => {
245
+ if (s.format !== "ifsoul-participants/0.4.0" ||
246
+ (s.behavior && s.behavior.format !== "ifsoul-behavior/0.4.0"))
247
+ return failure("invalid");
248
+ return structuredClone(work(s));
249
+ });
250
+ };
251
+ const inspect = (s, a, sessionId, npcId) => {
252
+ const session = sessionFor(s, a, sessionId, npcId), b = s.behavior, config = base(s);
253
+ return {
254
+ npcId,
255
+ sessionId,
256
+ configRevision: config.revision,
257
+ stateRevision: b ? revision(b, npcId) : 0,
258
+ effective: effectiveBehavior(s, a, npcId, sessionId, operator),
259
+ parameters: config.parameters,
260
+ values: (b?.values ?? []).filter((v) => v.npcId === npcId && (!v.worldId || v.worldId === session.worldId)),
261
+ history: (b?.history ?? []).filter((v) => v.npcId === npcId && (!v.worldId || v.worldId === session.worldId)),
262
+ };
263
+ };
264
+ return {
265
+ read(a) {
266
+ if (!operator.canEditConfig)
267
+ return failure("unauthorized");
268
+ return run(a, (s) => ({
269
+ config: base(s),
270
+ operator,
271
+ catalog: {
272
+ npcs: s.participants
273
+ .filter((p) => p.kind === "npc" &&
274
+ s.sessions.some((session) => session.members.some((m) => m.participantId === p.participantId && m.role === "npc")))
275
+ .map((p) => ({ npcId: p.participantId, label: p.participantId })),
276
+ sessions: s.sessions.map((x) => ({
277
+ sessionId: x.sessionId,
278
+ worldId: x.worldId,
279
+ npcIds: x.members
280
+ .filter((m) => m.role === "npc")
281
+ .map((m) => m.participantId),
282
+ })),
283
+ groups: s.sessions.flatMap((x) => x.groups.map((g) => ({
284
+ groupId: g.groupId,
285
+ npcIds: g.participantIds.filter((id) => s.participants.some((p) => p.participantId === id && p.kind === "npc")),
286
+ }))),
287
+ worlds: [...new Set(s.sessions.map((x) => x.worldId))],
288
+ },
289
+ }));
290
+ },
291
+ update(a, expectedRevision, config) {
292
+ return run(a, (s) => {
293
+ if (!operator.canEditConfig)
294
+ return failure("unauthorized");
295
+ const prior = base(s);
296
+ if (!keys(config, [
297
+ "version",
298
+ "revision",
299
+ "parameters",
300
+ "defaults",
301
+ "overrides",
302
+ ]) ||
303
+ config.version !== "ifsoul-behavior/0.4.0" ||
304
+ !Number.isSafeInteger(expectedRevision) ||
305
+ expectedRevision < 0 ||
306
+ config.revision !== expectedRevision + 1 ||
307
+ !Array.isArray(config.parameters) ||
308
+ !config.parameters.length ||
309
+ !unique(config.parameters.map((p) => p?.parameterId), limits.parameters) ||
310
+ !config.parameters.every(definition) ||
311
+ !keys(config.defaults, ["policy", "goals"]) ||
312
+ !policy(config.defaults.policy) ||
313
+ !validGoals(config.defaults.goals) ||
314
+ !Array.isArray(config.overrides) ||
315
+ config.overrides.length > limits.overrides)
316
+ return failure("invalid");
317
+ if (prior.revision !== expectedRevision)
318
+ return failure("conflict");
319
+ const scopeKeys = new Set();
320
+ for (const o of config.overrides) {
321
+ if (!keys(o, ["scope", "policy", "goals", "initialValues"]) ||
322
+ !keys(o.scope, ["kind", "id"]) ||
323
+ !["npc", "group", "world"].includes(o.scope.kind) ||
324
+ !id(o.scope.id) ||
325
+ (o.policy !== undefined && !policy(o.policy, true)) ||
326
+ (o.goals !== undefined && !validGoals(o.goals)) ||
327
+ (o.initialValues !== undefined &&
328
+ (!obj(o.initialValues) ||
329
+ Object.entries(o.initialValues).some(([id, value]) => {
330
+ const d = config.parameters.find((p) => p.parameterId === id);
331
+ return !d || !finite(value) || value < d.min || value > d.max;
332
+ }))))
333
+ return failure("invalid");
334
+ const key = JSON.stringify(o.scope);
335
+ if (scopeKeys.has(key))
336
+ return failure("conflict");
337
+ scopeKeys.add(key);
338
+ if ((o.scope.kind === "npc" &&
339
+ !s.participants.some((p) => p.participantId === o.scope.id && p.kind === "npc")) ||
340
+ (o.scope.kind === "world" &&
341
+ !s.sessions.some((x) => x.worldId === o.scope.id)) ||
342
+ (o.scope.kind === "group" &&
343
+ !s.sessions.some((x) => x.groups.some((g) => g.groupId === o.scope.id))))
344
+ return failure("unauthorized");
345
+ }
346
+ for (const locked of operator.lockedParameterIds) {
347
+ if (!same(prior.parameters.find((p) => p.parameterId === locked), config.parameters.find((p) => p.parameterId === locked)) ||
348
+ !same(prior.overrides
349
+ .filter((o) => o.initialValues && Object.hasOwn(o.initialValues, locked))
350
+ .map((o) => [o.scope, o.initialValues?.[locked]]), config.overrides
351
+ .filter((o) => o.initialValues && Object.hasOwn(o.initialValues, locked))
352
+ .map((o) => [o.scope, o.initialValues?.[locked]])))
353
+ return failure("unauthorized");
354
+ }
355
+ if (operator.lockedPolicy &&
356
+ (!same(prior.defaults.policy, config.defaults.policy) ||
357
+ !same(prior.overrides
358
+ .filter((o) => o.policy !== undefined)
359
+ .map((o) => [o.scope, o.policy]), config.overrides
360
+ .filter((o) => o.policy !== undefined)
361
+ .map((o) => [o.scope, o.policy]))))
362
+ return failure("unauthorized");
363
+ if (operator.lockedGoals &&
364
+ (!same(prior.defaults.goals, config.defaults.goals) ||
365
+ !same(prior.overrides
366
+ .filter((o) => o.goals !== undefined)
367
+ .map((o) => [o.scope, o.goals]), config.overrides
368
+ .filter((o) => o.goals !== undefined)
369
+ .map((o) => [o.scope, o.goals]))))
370
+ return failure("unauthorized");
371
+ for (const flag of operator.deniedPermissions) {
372
+ const was = (p) => obj(p) && Object.values(p).some((v) => obj(v) && v[flag] === true);
373
+ if ((was(config.defaults.policy) && !was(prior.defaults.policy)) ||
374
+ config.overrides.some((o) => was(o.policy) &&
375
+ !prior.overrides.some((p) => same(p.scope, o.scope) && was(p.policy))))
376
+ return failure("unauthorized");
377
+ }
378
+ for (const old of prior.parameters) {
379
+ if (s.behavior?.values.some((v) => v.parameterId === old.parameterId) ||
380
+ s.behavior?.history.some((v) => v.parameterId === old.parameterId)) {
381
+ const next = config.parameters.find((p) => p.parameterId === old.parameterId);
382
+ if (!next || next.kind !== old.kind || next.scope !== old.scope)
383
+ return failure("invalid");
384
+ }
385
+ }
386
+ for (const v of s.behavior?.values ?? []) {
387
+ const d = config.parameters.find((p) => p.parameterId === v.parameterId);
388
+ if (d && (v.value < d.min || v.value > d.max))
389
+ return failure("invalid");
390
+ }
391
+ const b = requireState(s, true);
392
+ b.config = structuredClone(config);
393
+ return config;
394
+ });
395
+ },
396
+ adminInspect(a, sessionId, npcId) {
397
+ if (!operator.canEditConfig)
398
+ return failure("unauthorized");
399
+ return run(a, (s) => {
400
+ const session = s.sessions.find((x) => x.sessionId === sessionId);
401
+ if (!session)
402
+ return failure("unauthorized");
403
+ return inspect(s, { ...a, hostId: session.hostId }, sessionId, npcId);
404
+ });
405
+ },
406
+ inspect(a, sessionId, npcId) {
407
+ return run(a, (s) => inspect(s, a, sessionId, npcId));
408
+ },
409
+ observe(a, sessionId, npcId, eventId, kind) {
410
+ return run(a, (s) => {
411
+ sessionFor(s, a, sessionId, npcId);
412
+ if (!["provocation", "helpful", "observation"].includes(kind))
413
+ return failure("invalid");
414
+ const e = s.events.find((e) => e.eventId === eventId && e.sessionId === sessionId);
415
+ if (!e ||
416
+ ![e.senderId, ...e.recipientIds, ...e.observerIds].includes(npcId))
417
+ return failure("unauthorized");
418
+ const b = requireState(s, true), prior = b.observations.find((o) => o.eventId === eventId && o.npcId === npcId);
419
+ if (prior) {
420
+ if (prior.kind !== kind)
421
+ return failure("conflict");
422
+ return { observation: prior, fresh: false };
423
+ }
424
+ if (b.observations.length >= limits.observations)
425
+ return failure("limit");
426
+ const effective = effectiveBehavior(s, a, npcId, sessionId, operator)
427
+ .policy[kind === "provocation" ? "provocation" : "general"];
428
+ const observation = {
429
+ eventId,
430
+ npcId,
431
+ kind,
432
+ configRevision: b.config.revision,
433
+ permissions: effective,
434
+ };
435
+ b.observations.push(observation);
436
+ return { observation, fresh: true };
437
+ });
438
+ },
439
+ apply(a, proposal, nowMs) {
440
+ return run(a, (s) => {
441
+ if (!keys(proposal, [
442
+ "proposalId",
443
+ "npcId",
444
+ "sessionId",
445
+ "configRevision",
446
+ "expectedStateRevision",
447
+ "changes",
448
+ ]) ||
449
+ !id(proposal.proposalId) ||
450
+ !finite(nowMs) ||
451
+ nowMs < 0 ||
452
+ !Array.isArray(proposal.changes) ||
453
+ !proposal.changes.length ||
454
+ proposal.changes.length > limits.changesPerProposal)
455
+ return failure("invalid");
456
+ const session = sessionFor(s, a, proposal.sessionId, proposal.npcId), b = requireState(s, true);
457
+ const prior = b.proposals.find((p) => p.proposal.proposalId === proposal.proposalId);
458
+ if (prior) {
459
+ if (!same(prior.proposal, proposal))
460
+ return failure("conflict");
461
+ return inspect(s, a, proposal.sessionId, proposal.npcId);
462
+ }
463
+ if (b.config.revision !== proposal.configRevision ||
464
+ revision(b, proposal.npcId) !== proposal.expectedStateRevision)
465
+ return failure("stale");
466
+ if (b.proposals.length >= limits.proposals ||
467
+ b.history.length + proposal.changes.length > limits.history)
468
+ return failure("limit");
469
+ const nextRevision = proposal.expectedStateRevision + 1, changedKeys = new Set();
470
+ for (const change of proposal.changes) {
471
+ if (!keys(change, [
472
+ "parameterId",
473
+ "targetParticipantId",
474
+ "delta",
475
+ "confidence",
476
+ "evidenceEventIds",
477
+ "reason",
478
+ ]) ||
479
+ !id(change.parameterId) ||
480
+ !finite(change.delta) ||
481
+ !finite(change.confidence) ||
482
+ change.confidence < 0 ||
483
+ change.confidence > 1 ||
484
+ !unique(change.evidenceEventIds, limits.evidencePerChange) ||
485
+ !change.evidenceEventIds.length ||
486
+ typeof change.reason !== "string" ||
487
+ !change.reason.length ||
488
+ change.reason.length > 512)
489
+ return failure("invalid");
490
+ const d = b.config.parameters.find((p) => p.parameterId === change.parameterId);
491
+ if (!d || d.kind === "trait" || Math.abs(change.delta) > d.maxDelta)
492
+ return failure("invalid");
493
+ if ((d.kind === "relationship" &&
494
+ (!id(change.targetParticipantId) ||
495
+ change.targetParticipantId === proposal.npcId ||
496
+ !s.participants.some((p) => p.participantId === change.targetParticipantId))) ||
497
+ (d.kind !== "relationship" &&
498
+ change.targetParticipantId !== undefined))
499
+ return failure("unauthorized");
500
+ for (const eventId of change.evidenceEventIds) {
501
+ const e = s.events.find((e) => e.eventId === eventId), sourceSession = e && s.sessions.find((x) => x.sessionId === e.sessionId), observed = b.observations.find((o) => o.eventId === eventId && o.npcId === proposal.npcId);
502
+ if (!e ||
503
+ !sourceSession ||
504
+ sourceSession.hostId !== a.hostId ||
505
+ !session.recallWorldIds.includes(sourceSession.worldId) ||
506
+ (d.scope === "world" &&
507
+ sourceSession.worldId !== session.worldId) ||
508
+ !observed?.permissions.observe ||
509
+ ![e.senderId, ...e.recipientIds, ...e.observerIds].includes(proposal.npcId))
510
+ return failure("unauthorized");
511
+ const current = behaviorPermissions(s, a, proposal.npcId, proposal.sessionId, eventId, operator);
512
+ if (!current.observe ||
513
+ (d.kind === "relationship" &&
514
+ (!current.updateRelationship ||
515
+ !observed.permissions.updateRelationship)))
516
+ return failure("unauthorized");
517
+ if (d.kind === "relationship" &&
518
+ !(e.senderId === change.targetParticipantId ||
519
+ (e.senderId === proposal.npcId &&
520
+ e.recipientIds.includes(change.targetParticipantId))))
521
+ return failure("unauthorized");
522
+ }
523
+ const keyData = {
524
+ npcId: proposal.npcId,
525
+ parameterId: d.parameterId,
526
+ ...(change.targetParticipantId
527
+ ? { targetParticipantId: change.targetParticipantId }
528
+ : {}),
529
+ ...(d.scope === "world" ? { worldId: session.worldId } : {}),
530
+ }, key = valueKey(keyData);
531
+ if (changedKeys.has(key) ||
532
+ b.history.some((h) => valueKey(h) === key &&
533
+ h.evidenceEventIds.some((e) => change.evidenceEventIds.includes(e))))
534
+ return failure("conflict");
535
+ changedKeys.add(key);
536
+ const initialValues = effectiveBehavior(s, a, proposal.npcId, proposal.sessionId, operator).initialValues;
537
+ const old = b.values.find((v) => valueKey(v) === key), initial = Object.hasOwn(initialValues, d.parameterId)
538
+ ? initialValues[d.parameterId]
539
+ : d.initial, before = old?.value ?? initial, after = Number((before + change.delta).toFixed(6));
540
+ if (after < d.min || after > d.max || (old && nowMs < old.updatedAt))
541
+ return failure("invalid");
542
+ const value = {
543
+ ...keyData,
544
+ value: after,
545
+ confidence: change.confidence,
546
+ revision: nextRevision,
547
+ updatedAt: nowMs,
548
+ };
549
+ if (old)
550
+ b.values[b.values.indexOf(old)] = value;
551
+ else {
552
+ if (b.values.length >= limits.values)
553
+ return failure("limit");
554
+ b.values.push(value);
555
+ }
556
+ b.history.push({
557
+ ...keyData,
558
+ changeId: `${proposal.proposalId}:${d.parameterId}`,
559
+ before,
560
+ after,
561
+ confidence: change.confidence,
562
+ revision: nextRevision,
563
+ configRevision: b.config.revision,
564
+ evidenceEventIds: [...change.evidenceEventIds],
565
+ reason: change.reason,
566
+ kind: "evidence",
567
+ recordedAt: nowMs,
568
+ });
569
+ }
570
+ b.stateRevisions[proposal.npcId] = nextRevision;
571
+ b.proposals.push({
572
+ proposal: structuredClone(proposal),
573
+ revision: nextRevision,
574
+ });
575
+ return inspect(s, a, proposal.sessionId, proposal.npcId);
576
+ });
577
+ },
578
+ decay(a, sessionId, npcId, nowMs) {
579
+ return run(a, (s) => {
580
+ const session = sessionFor(s, a, sessionId, npcId), b = requireState(s, true);
581
+ if (!finite(nowMs) || nowMs < 0)
582
+ return failure("invalid");
583
+ let changed = false;
584
+ const nextRevision = revision(b, npcId) + 1;
585
+ for (const v of b.values.filter((v) => v.npcId === npcId && (!v.worldId || v.worldId === session.worldId))) {
586
+ const d = b.config.parameters.find((p) => p.parameterId === v.parameterId);
587
+ if (!d?.decay || d.kind === "trait")
588
+ continue;
589
+ if (nowMs < v.updatedAt)
590
+ return failure("stale");
591
+ const step = Math.min(d.maxDelta, (d.decay.perHour * (nowMs - v.updatedAt)) / 3600000, Math.abs(d.decay.toward - v.value));
592
+ if (step <= 0)
593
+ continue;
594
+ if (b.history.length >= limits.history)
595
+ return failure("limit");
596
+ const before = v.value, after = Number((before + Math.sign(d.decay.toward - before) * step).toFixed(6));
597
+ if (after === before)
598
+ continue;
599
+ v.value = after;
600
+ v.revision = nextRevision;
601
+ v.updatedAt = nowMs;
602
+ b.history.push({
603
+ ...v,
604
+ changeId: `decay:${npcId}:${b.history.length + 1}`,
605
+ before,
606
+ after,
607
+ configRevision: b.config.revision,
608
+ evidenceEventIds: [],
609
+ reason: "Configured explicit-time decay",
610
+ kind: "decay",
611
+ recordedAt: nowMs,
612
+ });
613
+ changed = true;
614
+ }
615
+ if (changed)
616
+ b.stateRevisions[npcId] = nextRevision;
617
+ return inspect(s, a, sessionId, npcId);
618
+ });
619
+ },
620
+ };
621
+ }