@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,647 @@
1
+ import { behaviorPermissions, behaviorRevision, behaviorEvidenceVisible, effectiveBehavior, } from "./behavior.js";
2
+ import { resolveResourceLimits, BUFFERED_JSON_MAX_BYTES, } from "../../protocol/src/resource-policy.js";
3
+ import { SessionError } from "./participants.js";
4
+ export const DEFAULT_PLANNING_LIMITS = Object.freeze({
5
+ principalCalls: 64,
6
+ npcCalls: 32,
7
+ groupCalls: 32,
8
+ concurrentPlans: 1,
9
+ schedules: 32,
10
+ claims: 256,
11
+ reports: 1024,
12
+ runsPerSchedule: 32,
13
+ actorsPerPlan: 8,
14
+ assignmentsPerPlan: 16,
15
+ assignmentsPerActor: 4,
16
+ sharedEvents: 32,
17
+ contextBytes: 32768,
18
+ });
19
+ export function resolvePlanningLimits(requested = {}, ceilings = {}) {
20
+ return resolveResourceLimits({
21
+ layer: "group-scheduler",
22
+ defaults: { ...DEFAULT_PLANNING_LIMITS },
23
+ legacyCeilings: { ...DEFAULT_PLANNING_LIMITS },
24
+ requested,
25
+ ceilings,
26
+ implementationCeilings: {
27
+ actorsPerPlan: 64,
28
+ assignmentsPerPlan: 1024,
29
+ contextBytes: BUFFERED_JSON_MAX_BYTES,
30
+ },
31
+ });
32
+ }
33
+ const fail = (code) => {
34
+ throw new SessionError(code);
35
+ };
36
+ const id = (x) => typeof x === "string" && /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(x);
37
+ const ids = (x, maximum = 32) => Array.isArray(x) &&
38
+ x.length <= maximum &&
39
+ x.every(id) &&
40
+ new Set(x).size === x.length;
41
+ const integer = (x) => Number.isSafeInteger(x) && Number(x) >= 0;
42
+ const equal = (a, b) => JSON.stringify(a) === JSON.stringify(b);
43
+ const keys = (x, allowed) => Object.keys(x).every((k) => allowed.includes(k));
44
+ const initial = () => ({
45
+ format: "ifsoul-group-scheduler/0.4.0",
46
+ schedules: [],
47
+ claims: [],
48
+ reports: [],
49
+ cursors: [],
50
+ lastWallTime: 0,
51
+ calls: 0,
52
+ npcCalls: {},
53
+ groupCalls: {},
54
+ });
55
+ const count = (values, key) => Object.hasOwn(values, key) ? values[key] : 0;
56
+ const open = (c) => ["active", "proposed", "unknown"].includes(c.status);
57
+ function authorizedSession(s, access, sessionId, revision) {
58
+ if (!access || !id(access.principalId) || !id(access.hostId))
59
+ return fail("unauthorized");
60
+ const session = s.sessions.find((x) => x.sessionId === sessionId && x.hostId === access.hostId);
61
+ if (!session)
62
+ return fail("unauthorized");
63
+ if (revision !== undefined && session.revision !== revision)
64
+ return fail("stale");
65
+ return session;
66
+ }
67
+ function validateSpec(s, access, spec, limits, operator) {
68
+ if (!spec ||
69
+ !keys(spec, [
70
+ "sessionId",
71
+ "sessionRevision",
72
+ "groupId",
73
+ "actors",
74
+ "intents",
75
+ "objective",
76
+ "channelId",
77
+ "sharedEventIds",
78
+ "goalId",
79
+ ]) ||
80
+ !id(spec.sessionId) ||
81
+ !integer(spec.sessionRevision) ||
82
+ typeof spec.objective !== "string" ||
83
+ spec.objective.length < 1 ||
84
+ spec.objective.length > 512 ||
85
+ !id(spec.channelId) ||
86
+ (spec.goalId !== undefined && !id(spec.goalId)) ||
87
+ !ids(spec.sharedEventIds, limits.sharedEvents) ||
88
+ !Array.isArray(spec.actors) ||
89
+ spec.actors.length < 1 ||
90
+ spec.actors.length > limits.actorsPerPlan ||
91
+ !ids(spec.actors.map((x) => x?.participantId), limits.actorsPerPlan) ||
92
+ !Array.isArray(spec.intents) ||
93
+ spec.intents.length < 1 ||
94
+ spec.intents.length > 16 ||
95
+ !ids(spec.intents.map((x) => x?.intentId)))
96
+ return fail("invalid");
97
+ const session = authorizedSession(s, access, spec.sessionId, spec.sessionRevision);
98
+ const actorIds = spec.actors.map((x) => x?.participantId);
99
+ if (spec.goalId !== undefined &&
100
+ actorIds.some((npcId) => !effectiveBehavior(s, access, npcId, spec.sessionId, operator).goals.some((g) => g.goalId === spec.goalId &&
101
+ g.enabled &&
102
+ g.objective === spec.objective)))
103
+ return fail("unauthorized");
104
+ if (spec.actors.some((x) => !keys(x, ["participantId", "roleId", "capabilities"]) ||
105
+ !id(x.roleId) ||
106
+ !ids(x.capabilities) ||
107
+ !session.members.some((m) => m.participantId === x.participantId && m.role === "npc")))
108
+ return fail("unauthorized");
109
+ if (spec.intents.some((x) => !keys(x, ["intentId", "roleIds", "requiredCapabilities"]) ||
110
+ !ids(x.roleIds) ||
111
+ !x.roleIds.length ||
112
+ !ids(x.requiredCapabilities)))
113
+ return fail("invalid");
114
+ if (spec.groupId !== undefined) {
115
+ const group = session.groups.find((x) => x.groupId === spec.groupId);
116
+ if (!group || actorIds.some((x) => !group.participantIds.includes(x)))
117
+ return fail("unauthorized");
118
+ }
119
+ else if (actorIds.length !== 1)
120
+ return fail("invalid");
121
+ const channel = session.channels.find((x) => x.channelId === spec.channelId);
122
+ if (!channel || actorIds.some((x) => !channel.participantIds.includes(x)))
123
+ return fail("unauthorized");
124
+ const sharedEvents = spec.sharedEventIds.map((eventId) => {
125
+ const event = s.events.find((x) => x.eventId === eventId &&
126
+ x.sessionId === session.sessionId &&
127
+ x.channelId === spec.channelId);
128
+ if (!event ||
129
+ (s.behavior &&
130
+ actorIds.some((npcId) => !behaviorEvidenceVisible(s, access, npcId, spec.sessionId, eventId))) ||
131
+ actorIds.some((x) => ![
132
+ event.senderId,
133
+ ...event.recipientIds,
134
+ ...event.observerIds,
135
+ ].includes(x)))
136
+ return fail("unauthorized");
137
+ return {
138
+ eventId: event.eventId,
139
+ sessionId: event.sessionId,
140
+ revision: event.revision,
141
+ channelId: event.channelId,
142
+ senderId: event.senderId,
143
+ recipientIds: [...event.recipientIds],
144
+ observerIds: [...event.observerIds],
145
+ text: event.text,
146
+ };
147
+ });
148
+ if (new TextEncoder().encode(JSON.stringify({ spec, sharedEvents })).length >
149
+ limits.contextBytes)
150
+ return fail("limit");
151
+ return {
152
+ session,
153
+ sharedEvents,
154
+ actorIds,
155
+ groupKey: JSON.stringify([
156
+ session.sessionId,
157
+ spec.groupId === undefined
158
+ ? ["solo", actorIds[0]]
159
+ : ["group", spec.groupId],
160
+ ]),
161
+ };
162
+ }
163
+ /** Reject the whole proposal. One invalid assignment never grants another action. */
164
+ export function validGroupPlan(value, spec, limits = { ...DEFAULT_PLANNING_LIMITS }) {
165
+ if (!value || typeof value !== "object" || Array.isArray(value))
166
+ return false;
167
+ const plan = value;
168
+ if (!keys(plan, ["objective", "assignments"]) ||
169
+ plan.objective !== spec.objective ||
170
+ !Array.isArray(plan.assignments) ||
171
+ !plan.assignments.length ||
172
+ plan.assignments.length > limits.assignmentsPerPlan ||
173
+ !ids(plan.assignments.map((x) => x?.assignmentId), limits.assignmentsPerPlan))
174
+ return false;
175
+ if (spec.actors.some((actor) => !plan.assignments.some((a) => a.actorId === actor.participantId)))
176
+ return false;
177
+ for (const a of plan.assignments) {
178
+ if (!a ||
179
+ !keys(a, ["assignmentId", "actorId", "intentId", "dependsOn"]) ||
180
+ !ids(a.dependsOn, limits.assignmentsPerPlan) ||
181
+ a.dependsOn.includes(a.assignmentId) ||
182
+ a.dependsOn.some((d) => !plan.assignments.some((x) => x.assignmentId === d)))
183
+ return false;
184
+ const actor = spec.actors.find((x) => x.participantId === a.actorId);
185
+ const intent = spec.intents.find((x) => x.intentId === a.intentId);
186
+ if (!actor ||
187
+ !intent ||
188
+ !intent.roleIds.includes(actor.roleId) ||
189
+ intent.requiredCapabilities.some((c) => !actor.capabilities.includes(c)) ||
190
+ plan.assignments.filter((x) => x.actorId === a.actorId).length >
191
+ limits.assignmentsPerActor)
192
+ return false;
193
+ }
194
+ const visiting = new Set(), done = new Set();
195
+ const visit = (assignmentId) => {
196
+ if (visiting.has(assignmentId))
197
+ return false;
198
+ if (done.has(assignmentId))
199
+ return true;
200
+ visiting.add(assignmentId);
201
+ if (!plan.assignments
202
+ .find((x) => x.assignmentId === assignmentId)
203
+ .dependsOn.every(visit))
204
+ return false;
205
+ visiting.delete(assignmentId);
206
+ done.add(assignmentId);
207
+ return true;
208
+ };
209
+ return plan.assignments.every((x) => visit(x.assignmentId));
210
+ }
211
+ export function createGroupSchedulerRuntime(store, requestedBudgets = {}, ceilings = {}, behaviorOperator) {
212
+ behaviorOperator = behaviorOperator
213
+ ? structuredClone(behaviorOperator)
214
+ : undefined;
215
+ const report = resolvePlanningLimits(requestedBudgets, ceilings);
216
+ if (report.status !== "accepted")
217
+ throw new RangeError(`Unsupported planning budget: ${report.failure?.field}`);
218
+ const budgets = report.effective;
219
+ const run = (access, work) => {
220
+ if (!access || !id(access.principalId) || !id(access.hostId))
221
+ return fail("unauthorized");
222
+ return store.transact(access.principalId, (s) => {
223
+ if (!id(access.principalId) ||
224
+ !id(access.hostId) ||
225
+ s.format !== "ifsoul-participants/0.4.0")
226
+ return fail("unauthorized");
227
+ if (!s.planning || s.planning.format !== "ifsoul-group-scheduler/0.4.0")
228
+ return fail("invalid");
229
+ return structuredClone(work(s, s.planning));
230
+ });
231
+ };
232
+ const schedule = (s, p, access, scheduleId) => {
233
+ const config = p.schedules.find((x) => x.scheduleId === scheduleId);
234
+ if (!config)
235
+ return fail("unauthorized");
236
+ authorizedSession(s, access, config.spec.sessionId);
237
+ return config;
238
+ };
239
+ const release = (s, c) => {
240
+ for (const t of s.turns)
241
+ if (t.ticket.turnId === `${c.claimId}:${t.ticket.npcId}` &&
242
+ c.actorIds.includes(t.ticket.npcId) &&
243
+ t.ticket.eventId === c.claimId)
244
+ t.cancelled = true;
245
+ };
246
+ const supersede = (s, p, scheduleId) => {
247
+ for (const c of p.claims.filter((x) => x.scheduleId === scheduleId && open(x))) {
248
+ c.status =
249
+ c.status === "unknown" || c.adoptedIds?.length
250
+ ? "unknown"
251
+ : "superseded";
252
+ release(s, c);
253
+ }
254
+ };
255
+ const clockValue = (config, session, clock) => {
256
+ const value = config.clock === "wall"
257
+ ? clock.wallTimeMs
258
+ : clock.worldTicks[session.worldId];
259
+ if (!integer(clock.wallTimeMs) || !integer(value))
260
+ return fail("invalid");
261
+ return value;
262
+ };
263
+ return {
264
+ configure(access, expectedRevision, config) {
265
+ if (!access || !id(access.principalId) || !id(access.hostId))
266
+ return fail("unauthorized");
267
+ // New scheduling state is created only by this explicit trusted configuration operation.
268
+ return store.transact(access.principalId, (s) => {
269
+ if (!id(access.principalId) ||
270
+ !id(access.hostId) ||
271
+ s.format !== "ifsoul-participants/0.4.0")
272
+ return fail("unauthorized");
273
+ if (!config ||
274
+ !keys(config, [
275
+ "scheduleId",
276
+ "revision",
277
+ "state",
278
+ "spec",
279
+ "clock",
280
+ "interval",
281
+ "nextDue",
282
+ "maxRuns",
283
+ ]) ||
284
+ !id(config.scheduleId) ||
285
+ !integer(expectedRevision) ||
286
+ config.revision !== expectedRevision + 1 ||
287
+ !["running", "paused", "cancelled"].includes(config.state) ||
288
+ !["wall", "world"].includes(config.clock) ||
289
+ !integer(config.interval) ||
290
+ config.interval < 1 ||
291
+ config.interval > 86400000 ||
292
+ !integer(config.nextDue) ||
293
+ !integer(config.maxRuns) ||
294
+ config.maxRuns < 1 ||
295
+ config.maxRuns > budgets.runsPerSchedule)
296
+ return fail("invalid");
297
+ validateSpec(s, access, config.spec, budgets, behaviorOperator);
298
+ const p = s.planning ?? initial();
299
+ if (p.format !== "ifsoul-group-scheduler/0.4.0")
300
+ return fail("invalid");
301
+ const old = p.schedules.find((x) => x.scheduleId === config.scheduleId);
302
+ if (old) {
303
+ authorizedSession(s, access, old.spec.sessionId);
304
+ if (old.spec.sessionId !== config.spec.sessionId ||
305
+ old.clock !== config.clock ||
306
+ old.state === "cancelled")
307
+ return fail("conflict");
308
+ }
309
+ if ((old?.revision ?? 0) !== expectedRevision)
310
+ return fail("stale");
311
+ if (!old && p.schedules.length >= budgets.schedules)
312
+ return fail("limit");
313
+ supersede(s, p, config.scheduleId);
314
+ if (old)
315
+ p.schedules[p.schedules.indexOf(old)] = structuredClone(config);
316
+ else {
317
+ p.schedules.push(structuredClone(config));
318
+ p.cursors.push({
319
+ scheduleId: config.scheduleId,
320
+ runs: 0,
321
+ triggerKeys: [],
322
+ });
323
+ }
324
+ s.planning = p;
325
+ return structuredClone(config);
326
+ });
327
+ },
328
+ control(access, scheduleId, expectedRevision, action) {
329
+ return run(access, (s, p) => {
330
+ const config = schedule(s, p, access, scheduleId);
331
+ if (config.revision !== expectedRevision)
332
+ return fail("stale");
333
+ if (config.state === "cancelled" ||
334
+ !["pause", "resume", "cancel"].includes(action))
335
+ return fail("conflict");
336
+ supersede(s, p, scheduleId);
337
+ config.revision++;
338
+ config.state =
339
+ action === "pause"
340
+ ? "paused"
341
+ : action === "resume"
342
+ ? "running"
343
+ : "cancelled";
344
+ return config;
345
+ });
346
+ },
347
+ list(access) {
348
+ if (!access || !id(access.principalId) || !id(access.hostId))
349
+ return fail("unauthorized");
350
+ return store.transact(access.principalId, (s) => {
351
+ if (!id(access.principalId) || !id(access.hostId))
352
+ return fail("unauthorized");
353
+ if (s.format !== "ifsoul-participants/0.4.0")
354
+ return fail("invalid");
355
+ if (!s.planning)
356
+ return [];
357
+ if (s.planning.format !== "ifsoul-group-scheduler/0.4.0")
358
+ return fail("invalid");
359
+ return structuredClone(s.planning.schedules.filter((x) => s.sessions.some((session) => session.sessionId === x.spec.sessionId &&
360
+ session.hostId === access.hostId)));
361
+ });
362
+ },
363
+ claim(access, scheduleId, trigger, clock, workerId, leaseMs, hostAvailable) {
364
+ return run(access, (s, p) => {
365
+ const config = schedule(s, p, access, scheduleId);
366
+ const { session, sharedEvents, actorIds, groupKey } = validateSpec(s, access, config.spec, budgets, behaviorOperator);
367
+ if (!id(workerId) ||
368
+ !integer(leaseMs) ||
369
+ leaseMs < 1 ||
370
+ leaseMs > 60000 ||
371
+ !trigger ||
372
+ !["due", "manual", "event"].includes(trigger.kind))
373
+ return fail("invalid");
374
+ const now = clockValue(config, session, clock);
375
+ if (clock.wallTimeMs < p.lastWallTime)
376
+ return fail("stale");
377
+ p.lastWallTime = clock.wallTimeMs;
378
+ for (const expired of p.claims.filter((c) => c.status === "active" && c.leaseUntil <= clock.wallTimeMs)) {
379
+ expired.status = "unknown";
380
+ release(s, expired);
381
+ }
382
+ if (config.state !== "running")
383
+ return { status: "paused" };
384
+ if (!hostAvailable)
385
+ return { status: "host-unavailable" };
386
+ if (actorIds.some((npcId) => {
387
+ const p = behaviorPermissions(s, access, npcId, config.spec.sessionId, trigger.kind === "event" ? trigger.eventId : undefined, behaviorOperator);
388
+ return !p.observe || !p.requestAction;
389
+ }))
390
+ return { status: "policy-denied" };
391
+ const cursor = p.cursors.find((x) => x.scheduleId === scheduleId);
392
+ if (cursor.lastClock !== undefined && now < cursor.lastClock)
393
+ return fail("stale");
394
+ cursor.lastClock = now;
395
+ let triggerKey;
396
+ if (trigger.kind === "due") {
397
+ if (now < config.nextDue)
398
+ return { status: "not-due" };
399
+ triggerKey = `due:${config.nextDue}`;
400
+ }
401
+ else if (trigger.kind === "manual") {
402
+ if (!id(trigger.triggerId))
403
+ return fail("invalid");
404
+ triggerKey = `manual:${trigger.triggerId}`;
405
+ }
406
+ else {
407
+ if (!id(trigger.eventId) ||
408
+ !config.spec.sharedEventIds.includes(trigger.eventId))
409
+ return fail("unauthorized");
410
+ triggerKey = `event:${trigger.eventId}`;
411
+ }
412
+ if (cursor.triggerKeys.includes(triggerKey))
413
+ return { status: "replay" };
414
+ if (p.claims.some((c) => c.status === "unknown" &&
415
+ (c.groupKey === groupKey ||
416
+ c.actorIds.some((id) => actorIds.includes(id)))))
417
+ return { status: "unknown" };
418
+ // Trusted principal concurrency is bounded independently of per-group and per-NPC ownership.
419
+ if (p.claims.filter(open).length >= budgets.concurrentPlans ||
420
+ p.claims.some((c) => open(c) &&
421
+ (c.groupKey === groupKey ||
422
+ c.actorIds.some((id) => actorIds.includes(id)))) ||
423
+ s.turns.some((t) => actorIds.includes(t.ticket.npcId) &&
424
+ !t.output &&
425
+ !t.cancelled &&
426
+ s.sessions.some((x) => x.sessionId === t.ticket.sessionId &&
427
+ x.revision === t.ticket.revision)))
428
+ return { status: "busy" };
429
+ if (cursor.lastStarted !== undefined &&
430
+ now < cursor.lastStarted + config.interval)
431
+ return { status: "not-due" };
432
+ if (p.calls >= budgets.principalCalls ||
433
+ cursor.runs >= config.maxRuns ||
434
+ actorIds.some((x) => count(p.npcCalls, x) >= budgets.npcCalls) ||
435
+ count(p.groupCalls, groupKey) >= budgets.groupCalls ||
436
+ p.claims.length >= budgets.claims ||
437
+ s.turns.length + actorIds.length > 2048)
438
+ return { status: "budget" };
439
+ if (!integer(now + config.interval) ||
440
+ !integer(clock.wallTimeMs + leaseMs))
441
+ return fail("invalid");
442
+ const fence = p.claims.length + 1, claimId = `plan-${fence}`;
443
+ if (actorIds.some((x) => s.turns.some((t) => t.ticket.turnId === `${claimId}:${x}`)))
444
+ return fail("conflict");
445
+ const claim = {
446
+ claimId,
447
+ fence,
448
+ behaviorRevision: behaviorRevision(s),
449
+ workerId,
450
+ scheduleId,
451
+ scheduleRevision: config.revision,
452
+ sessionRevision: session.revision,
453
+ leaseUntil: clock.wallTimeMs + leaseMs,
454
+ triggerKey,
455
+ actorIds,
456
+ groupKey,
457
+ status: "active",
458
+ };
459
+ p.claims.push(claim);
460
+ p.calls++;
461
+ cursor.runs++;
462
+ cursor.lastStarted = now;
463
+ cursor.triggerKeys.push(triggerKey);
464
+ // Coalesce missed slots into one current planning request, never offline actions.
465
+ config.nextDue = now + config.interval;
466
+ p.groupCalls[groupKey] = count(p.groupCalls, groupKey) + 1;
467
+ for (const npcId of actorIds) {
468
+ p.npcCalls[npcId] = count(p.npcCalls, npcId) + 1;
469
+ s.turns.push({
470
+ ticket: {
471
+ turnId: `${claimId}:${npcId}`,
472
+ eventId: claimId,
473
+ sessionId: session.sessionId,
474
+ revision: session.revision,
475
+ npcId,
476
+ recipientIds: [],
477
+ observerIds: [],
478
+ },
479
+ });
480
+ }
481
+ const views = actorIds.map((npcId) => effectiveBehavior(s, access, npcId, config.spec.sessionId, behaviorOperator));
482
+ const policies = actorIds.map((npcId) => behaviorPermissions(s, access, npcId, config.spec.sessionId, trigger.kind === "event" ? trigger.eventId : undefined, behaviorOperator));
483
+ const sharedPolicy = Object.fromEntries(Object.keys(policies[0]).map((flag) => [
484
+ flag,
485
+ policies.every((p) => p[flag]),
486
+ ]));
487
+ const commonGoals = views[0].goals.filter((goal) => goal.enabled &&
488
+ views.every((view) => view.goals.some((other) => other.goalId === goal.goalId &&
489
+ other.objective === goal.objective &&
490
+ other.enabled)));
491
+ return {
492
+ claim,
493
+ spec: config.spec,
494
+ sharedEvents,
495
+ ...(s.behavior
496
+ ? {
497
+ behavior: {
498
+ configRevision: behaviorRevision(s),
499
+ policy: sharedPolicy,
500
+ goals: commonGoals,
501
+ },
502
+ }
503
+ : {}),
504
+ };
505
+ });
506
+ },
507
+ complete(access, ticket, output, clock) {
508
+ return run(access, (s, p) => {
509
+ const c = p.claims.find((x) => x.claimId === ticket.claimId);
510
+ if (!c ||
511
+ c.fence !== ticket.fence ||
512
+ c.workerId !== ticket.workerId ||
513
+ c.status !== "active")
514
+ return fail("stale");
515
+ const config = schedule(s, p, access, c.scheduleId);
516
+ if ((c.behaviorRevision ?? 0) !== behaviorRevision(s))
517
+ return fail("stale");
518
+ authorizedSession(s, access, config.spec.sessionId, c.sessionRevision);
519
+ if (config.revision !== c.scheduleRevision ||
520
+ config.state !== "running" ||
521
+ !integer(clock.wallTimeMs) ||
522
+ clock.wallTimeMs < p.lastWallTime ||
523
+ clock.wallTimeMs >= c.leaseUntil)
524
+ return fail("stale");
525
+ validateSpec(s, access, config.spec, budgets, behaviorOperator);
526
+ if (!validGroupPlan(output, config.spec, budgets))
527
+ return fail("invalid");
528
+ c.plan = structuredClone(output);
529
+ c.status = "proposed";
530
+ release(s, c);
531
+ return c;
532
+ });
533
+ },
534
+ fail(access, ticket, uncertain) {
535
+ return run(access, (s, p) => {
536
+ const c = p.claims.find((x) => x.claimId === ticket.claimId);
537
+ if (!c ||
538
+ c.fence !== ticket.fence ||
539
+ c.workerId !== ticket.workerId ||
540
+ c.status !== "active")
541
+ return fail("stale");
542
+ schedule(s, p, access, c.scheduleId);
543
+ c.status = uncertain ? "unknown" : "failed";
544
+ release(s, c);
545
+ return c;
546
+ });
547
+ },
548
+ report(access, report) {
549
+ return run(access, (s, p) => {
550
+ if (!report ||
551
+ !keys(report, [
552
+ "reportId",
553
+ "claimId",
554
+ "status",
555
+ "assignmentIds",
556
+ "summary",
557
+ ]) ||
558
+ !id(report.reportId) ||
559
+ !ids(report.assignmentIds, budgets.assignmentsPerPlan) ||
560
+ typeof report.summary !== "string" ||
561
+ report.summary.length < 1 ||
562
+ report.summary.length > 1024 ||
563
+ !["adopted", "rejected", "completed", "failed", "unknown"].includes(report.status))
564
+ return fail("invalid");
565
+ const c = p.claims.find((x) => x.claimId === report.claimId);
566
+ if (!c)
567
+ return fail("unauthorized");
568
+ const config = schedule(s, p, access, c.scheduleId);
569
+ const prior = p.reports.find((x) => x.reportId === report.reportId);
570
+ if (prior) {
571
+ if (!equal(prior, report))
572
+ return fail("conflict");
573
+ return c;
574
+ }
575
+ if (!c.plan ||
576
+ report.assignmentIds.some((x) => !c.plan.assignments.some((a) => a.assignmentId === x)) ||
577
+ p.reports.length >= budgets.reports)
578
+ return fail("invalid");
579
+ if (report.status === "adopted") {
580
+ if ((c.behaviorRevision ?? 0) !== behaviorRevision(s))
581
+ return fail("stale");
582
+ authorizedSession(s, access, config.spec.sessionId, c.sessionRevision);
583
+ if (config.revision !== c.scheduleRevision ||
584
+ config.state !== "running" ||
585
+ c.status !== "proposed" ||
586
+ c.adoptedIds ||
587
+ !report.assignmentIds.length)
588
+ return fail("stale");
589
+ if (report.assignmentIds.some((assignmentId) => c
590
+ .plan.assignments.find((a) => a.assignmentId === assignmentId)
591
+ .dependsOn.some((dependency) => !report.assignmentIds.includes(dependency))))
592
+ return fail("invalid");
593
+ c.adoptedIds = [...report.assignmentIds];
594
+ }
595
+ else if (report.status === "rejected") {
596
+ if (report.assignmentIds.length !== 0 ||
597
+ c.adoptedIds?.length ||
598
+ !["proposed", "unknown", "superseded"].includes(c.status))
599
+ return fail("conflict");
600
+ c.status = "resolved";
601
+ }
602
+ else if (report.status === "completed" ||
603
+ report.status === "failed") {
604
+ if (!c.adoptedIds?.length ||
605
+ !equal([...report.assignmentIds].sort(), [...c.adoptedIds].sort()) ||
606
+ c.status === "resolved")
607
+ return fail("conflict");
608
+ c.status = "resolved";
609
+ }
610
+ else {
611
+ if (!c.adoptedIds?.length || c.status === "resolved")
612
+ return fail("conflict");
613
+ c.status = "unknown";
614
+ }
615
+ p.reports.push(structuredClone(report));
616
+ return c;
617
+ });
618
+ },
619
+ reconcile(access, claimId) {
620
+ return run(access, (s, p) => {
621
+ const c = p.claims.find((x) => x.claimId === claimId);
622
+ if (!c)
623
+ return fail("unauthorized");
624
+ schedule(s, p, access, c.scheduleId);
625
+ // Only an uncertain model request with no host adoption may be acknowledged here.
626
+ // Adopted work requires correlated host outcome reports instead.
627
+ if (c.status !== "unknown" || c.adoptedIds?.length)
628
+ return fail("conflict");
629
+ c.status = "resolved";
630
+ release(s, c);
631
+ return c;
632
+ });
633
+ },
634
+ inspect(access, scheduleId) {
635
+ return run(access, (s, p) => {
636
+ const config = schedule(s, p, access, scheduleId);
637
+ return {
638
+ config,
639
+ cursor: p.cursors.find((x) => x.scheduleId === scheduleId),
640
+ claims: p.claims.filter((x) => x.scheduleId === scheduleId),
641
+ reports: p.reports.filter((r) => p.claims.some((c) => c.claimId === r.claimId && c.scheduleId === scheduleId)),
642
+ calls: p.calls,
643
+ };
644
+ });
645
+ },
646
+ };
647
+ }
@@ -7,3 +7,7 @@ export declare function resolveRuntimeLimits(requested?: Partial<RuntimeLimits>,
7
7
  export declare const exchangeKey: (characterId: string, category: string, id: string) => string;
8
8
  /** A bounded local orchestrator. No keys, provider construction, host execution or SQLite dependency. */
9
9
  export declare function createRuntime(options: RuntimeOptions): Runtime;
10
+ export * from "./participants.js";
11
+ export * from "./group-scheduler.js";
12
+ export type * from "./behavior-types.js";
13
+ export * from "./behavior.js";
@@ -73,7 +73,7 @@ export function createRuntime(options) {
73
73
  result,
74
74
  });
75
75
  const event = (s, payload, category, id, recordedAt, sourceType) => ({
76
- schemaVersion: "0.3.0",
76
+ schemaVersion: "0.4.0",
77
77
  kind: "memory-event",
78
78
  memoryId: `event-${createHash("sha256")
79
79
  .update(key(s, category, id))
@@ -505,3 +505,6 @@ export function createRuntime(options) {
505
505
  }),
506
506
  };
507
507
  }
508
+ export * from "./participants.js";
509
+ export * from "./group-scheduler.js";
510
+ export * from "./behavior.js";