@ifsoul/core 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -15,7 +15,7 @@ compose these public artifacts. See [repository ownership](https://github.com/op
15
15
  Use Node.js 22.18–24 and an ESM project:
16
16
 
17
17
  ```sh
18
- npm install @ifsoul/core@0.7.0
18
+ npm install @ifsoul/core@0.8.0
19
19
  ```
20
20
 
21
21
  The package includes compiled JavaScript and TypeScript declarations. Import the
@@ -6,4 +6,6 @@ export declare function createOllamaProvider(options?: {
6
6
  fetch?: FetchLike;
7
7
  limits?: Partial<ProviderLimits>;
8
8
  budgetPolicy?: ProviderBudgetPolicy;
9
+ /** Optional local sampling override within the supported product range [0, 1]. */
10
+ temperature?: number;
9
11
  }): OllamaClient;
@@ -23,6 +23,10 @@ const localModel = (value) => typeof value === "string" &&
23
23
  !value.endsWith(":cloud") &&
24
24
  !/(?:^|[/:_-])(?:embed(?:ding)?s?|nomic|bge)(?:[/:_-]|$)/i.test(value);
25
25
  export function createOllamaProvider(options = {}) {
26
+ const { temperature } = options;
27
+ if (temperature !== undefined &&
28
+ (!Number.isFinite(temperature) || temperature < 0 || temperature > 1))
29
+ throw new ProviderFailure("configuration");
26
30
  const fetcher = options.fetch ?? fetch;
27
31
  const discover = async (signal, limits) => {
28
32
  const base = loopbackBase(options.baseUrl ?? DEFAULT_OLLAMA_URL);
@@ -64,6 +68,7 @@ export function createOllamaProvider(options = {}) {
64
68
  ...(contextBudget
65
69
  ? { num_ctx: contextBudget.windowTokens }
66
70
  : {}),
71
+ ...(temperature === undefined ? {} : { temperature }),
67
72
  },
68
73
  }, limits);
69
74
  const raw = object(await fetchJSON(fetcher, `${base}/api/chat`, {
@@ -5,6 +5,52 @@ export const OPENAI_MODELS = [
5
5
  "gpt-4.1-mini-2025-04-14",
6
6
  ];
7
7
  export const DEFAULT_OPENAI_MODEL = OPENAI_MODELS[0];
8
+ /** Project generation constraints into the documented Structured Outputs subset. */
9
+ function responsesSchema(schema) {
10
+ const copy = structuredClone(schema);
11
+ function visit(value) {
12
+ if (!value || typeof value !== "object" || Array.isArray(value))
13
+ return;
14
+ const node = value;
15
+ if (typeof node.const === "string" &&
16
+ (node.type === undefined || node.type === "string") &&
17
+ (node.enum === undefined ||
18
+ (Array.isArray(node.enum) && node.enum.includes(node.const)))) {
19
+ node.type = "string";
20
+ node.enum = [node.const];
21
+ delete node.const;
22
+ }
23
+ if (node.type === "array" || node.items !== undefined)
24
+ delete node.uniqueItems;
25
+ for (const map of [
26
+ node.properties,
27
+ node.patternProperties,
28
+ node.$defs,
29
+ node.definitions,
30
+ ])
31
+ if (map && typeof map === "object" && !Array.isArray(map))
32
+ Object.values(map).forEach(visit);
33
+ for (const keyword of ["items", "prefixItems", "anyOf", "allOf", "oneOf"]) {
34
+ const child = node[keyword];
35
+ if (Array.isArray(child))
36
+ child.forEach(visit);
37
+ else
38
+ visit(child);
39
+ }
40
+ for (const keyword of [
41
+ "additionalProperties",
42
+ "contains",
43
+ "if",
44
+ "then",
45
+ "else",
46
+ "not",
47
+ "propertyNames",
48
+ ])
49
+ visit(node[keyword]);
50
+ }
51
+ visit(copy);
52
+ return copy;
53
+ }
8
54
  /** Credential is confined to this client's closure and Authorization header. */
9
55
  export function createOpenAIProvider(options) {
10
56
  return {
@@ -31,7 +77,7 @@ export function createOpenAIProvider(options) {
31
77
  type: "json_schema",
32
78
  name: "ifsoul_content",
33
79
  strict: true,
34
- schema: input.schema,
80
+ schema: responsesSchema(input.schema),
35
81
  },
36
82
  },
37
83
  }, limits);
@@ -6,6 +6,6 @@ export declare function canonical(value: unknown): string;
6
6
  export { resolveResourceLimits, BUFFERED_JSON_MAX_BYTES, MAX_TIMER_MS, } from "./resource-policy.js";
7
7
  export type { BudgetReport } from "./resource-policy.js";
8
8
  /** Current train identifier; wire families require exact kind/version validation. */
9
- export declare const IFSOUL_VERSION: "0.7.0";
9
+ export declare const IFSOUL_VERSION: "0.8.0";
10
10
  /** Structural current ModelProvider spec validation; not a provider wire transport. */
11
11
  export declare function validateModelProviderDocument(value: unknown): boolean;
@@ -47,7 +47,7 @@ export function canonical(value) {
47
47
  }
48
48
  export { resolveResourceLimits, BUFFERED_JSON_MAX_BYTES, MAX_TIMER_MS, } from "./resource-policy.js";
49
49
  /** Current train identifier; wire families require exact kind/version validation. */
50
- export const IFSOUL_VERSION = "0.7.0";
50
+ export const IFSOUL_VERSION = "0.8.0";
51
51
  /** Structural current ModelProvider spec validation; not a provider wire transport. */
52
52
  export function validateModelProviderDocument(value) {
53
53
  return validators.current_provider(value) === true;
@@ -1,4 +1,4 @@
1
- import type { ParticipantState, ParticipantStore, SessionAccess } from "./participants.js";
1
+ import type { AuthorizeSessionEvidence, SessionEvidenceQuery, ParticipantState, ParticipantStore, SessionAccess } from "./participants.js";
2
2
  import type { BehaviorConfiguration, BehaviorOperatorPolicy, BehaviorPermissions, BehaviorSettings, BehaviorInspection, BehaviorProposal, BehaviorEventKind, BehaviorEditorSnapshot } from "./behavior-types.js";
3
3
  export type * from "./behavior-types.js";
4
4
  export declare const DEFAULT_BEHAVIOR_OPERATOR: Readonly<BehaviorOperatorPolicy>;
@@ -20,12 +20,19 @@ export declare function effectiveBehavior(s: ParticipantState, access: SessionAc
20
20
  };
21
21
  export declare function behaviorPermissions(s: ParticipantState, access: SessionAccess, npcId: string, sessionId: string, eventId?: string, operator?: BehaviorOperatorPolicy): BehaviorPermissions;
22
22
  export declare const behaviorRevision: (s: ParticipantState) => number;
23
+ interface EvidenceVisibilityOptions {
24
+ authorizeEvidence?: AuthorizeSessionEvidence;
25
+ purpose?: SessionEvidenceQuery["purpose"];
26
+ targetParticipantId?: string;
27
+ audienceParticipantIds?: string[];
28
+ }
23
29
  /** Previously unobserved/unremembered evidence never becomes implicit model context. */
24
- export declare function behaviorEvidenceVisible(s: ParticipantState, access: SessionAccess, npcId: string, sessionId: string, eventId: string, operator?: BehaviorOperatorPolicy): boolean;
30
+ export declare function behaviorEvidenceVisible(s: ParticipantState, access: SessionAccess, npcId: string, sessionId: string, eventId: string, operator?: BehaviorOperatorPolicy, options?: EvidenceVisibilityOptions): boolean;
25
31
  export declare function createBehaviorRuntime(store: ParticipantStore, options?: {
26
32
  operator?: BehaviorOperatorPolicy;
27
33
  limits?: Partial<BehaviorLimits>;
28
34
  ceilings?: Partial<BehaviorLimits>;
35
+ authorizeEvidence?: AuthorizeSessionEvidence;
29
36
  }): {
30
37
  read(a: SessionAccess): BehaviorEditorSnapshot;
31
38
  update(a: SessionAccess, expectedRevision: number, config: BehaviorConfiguration): BehaviorConfiguration;
@@ -138,21 +138,64 @@ export function behaviorPermissions(s, access, npcId, sessionId, eventId, operat
138
138
  }
139
139
  export const behaviorRevision = (s) => s.behavior?.config.revision ?? 0;
140
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))
141
+ export function behaviorEvidenceVisible(s, access, npcId, sessionId, eventId, operator = DEFAULT_BEHAVIOR_OPERATOR, options = {}) {
142
+ const session = sessionFor(s, access, sessionId, npcId);
143
+ const event = s.events.find((item) => item.eventId === eventId);
144
+ const sourceSession = event && s.sessions.find((item) => item.sessionId === event.sessionId);
145
+ if (!event || !sourceSession)
146
146
  return false;
147
+ const restricted = sourceSession.hostId !== access.hostId ||
148
+ options.authorizeEvidence !== undefined;
147
149
  if (!s.behavior)
148
- return true;
149
- const observation = s.behavior.observations.find((x) => x.npcId === npcId && x.eventId === eventId);
150
+ return !restricted;
151
+ const observation = s.behavior.observations.find((item) => item.npcId === npcId && item.eventId === eventId);
150
152
  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);
153
+ if (!observation?.permissions.observe ||
154
+ !observation.permissions.remember ||
155
+ !current.observe ||
156
+ !current.remember)
157
+ return false;
158
+ if (!restricted)
159
+ return true;
160
+ const audienceParticipantIds = [
161
+ ...new Set(options.audienceParticipantIds ?? []),
162
+ ];
163
+ const requiredAudience = [
164
+ ...new Set([
165
+ npcId,
166
+ ...audienceParticipantIds,
167
+ ...(options.targetParticipantId ? [options.targetParticipantId] : []),
168
+ ]),
169
+ ];
170
+ const recordedAudience = [
171
+ event.senderId,
172
+ ...event.recipientIds,
173
+ ...event.observerIds,
174
+ ];
175
+ if (!options.authorizeEvidence ||
176
+ !session.recallWorldIds.includes(sourceSession.worldId) ||
177
+ !sourceSession.members.some((member) => member.participantId === npcId && member.role === "npc") ||
178
+ !requiredAudience.every((id) => recordedAudience.includes(id) &&
179
+ sourceSession.members.some((member) => member.participantId === id)))
180
+ return false;
181
+ const sourcePermissions = behaviorPermissions(s, { ...access, hostId: sourceSession.hostId }, npcId, sourceSession.sessionId, eventId, operator);
182
+ if (!sourcePermissions.observe || !sourcePermissions.remember)
183
+ return false;
184
+ try {
185
+ return (options.authorizeEvidence(structuredClone({
186
+ purpose: options.purpose ?? "recall",
187
+ access,
188
+ session,
189
+ npcId,
190
+ targetParticipantId: options.targetParticipantId,
191
+ audienceParticipantIds,
192
+ sourceSession,
193
+ event,
194
+ })) === true);
195
+ }
196
+ catch {
197
+ return false;
198
+ }
156
199
  }
157
200
  function validGoals(value) {
158
201
  return (Array.isArray(value) &&
@@ -213,6 +256,7 @@ const valueKey = (value) => JSON.stringify([
213
256
  value.worldId ?? null,
214
257
  ]);
215
258
  export function createBehaviorRuntime(store, options = {}) {
259
+ const authorizeEvidence = options.authorizeEvidence;
216
260
  const operator = structuredClone(options.operator ?? DEFAULT_BEHAVIOR_OPERATOR);
217
261
  if (!keys(operator, [
218
262
  "canEditConfig",
@@ -250,6 +294,37 @@ export function createBehaviorRuntime(store, options = {}) {
250
294
  };
251
295
  const inspect = (s, a, sessionId, npcId) => {
252
296
  const session = sessionFor(s, a, sessionId, npcId), b = s.behavior, config = base(s);
297
+ const belongs = (item) => item.npcId === npcId &&
298
+ (!item.worldId || item.worldId === session.worldId);
299
+ const values = (b?.values ?? []).filter(belongs);
300
+ const history = (b?.history ?? []).filter(belongs);
301
+ const deniedKeys = new Set();
302
+ const acceptedKeys = new Set();
303
+ if (authorizeEvidence) {
304
+ for (const entry of history) {
305
+ const key = valueKey(entry);
306
+ if (deniedKeys.has(key) || entry.kind !== "evidence")
307
+ continue;
308
+ const target = entry.targetParticipantId;
309
+ if (!entry.evidenceEventIds.length ||
310
+ !entry.evidenceEventIds.every((eventId) => behaviorEvidenceVisible(s, a, npcId, sessionId, eventId, operator, {
311
+ authorizeEvidence,
312
+ purpose: "relationship-evidence",
313
+ targetParticipantId: target,
314
+ audienceParticipantIds: target ? [target] : [],
315
+ }))) {
316
+ deniedKeys.add(key);
317
+ continue;
318
+ }
319
+ acceptedKeys.add(key);
320
+ }
321
+ }
322
+ const visible = (item) => {
323
+ if (!authorizeEvidence)
324
+ return true;
325
+ const key = valueKey(item);
326
+ return acceptedKeys.has(key) && !deniedKeys.has(key);
327
+ };
253
328
  return {
254
329
  npcId,
255
330
  sessionId,
@@ -257,8 +332,8 @@ export function createBehaviorRuntime(store, options = {}) {
257
332
  stateRevision: b ? revision(b, npcId) : 0,
258
333
  effective: effectiveBehavior(s, a, npcId, sessionId, operator),
259
334
  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)),
335
+ values: values.filter(visible),
336
+ history: history.filter(visible),
262
337
  };
263
338
  };
264
339
  return {
@@ -501,13 +576,26 @@ export function createBehaviorRuntime(store, options = {}) {
501
576
  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
577
  if (!e ||
503
578
  !sourceSession ||
504
- sourceSession.hostId !== a.hostId ||
505
579
  !session.recallWorldIds.includes(sourceSession.worldId) ||
506
580
  (d.scope === "world" &&
507
581
  sourceSession.worldId !== session.worldId) ||
508
582
  !observed?.permissions.observe ||
509
583
  ![e.senderId, ...e.recipientIds, ...e.observerIds].includes(proposal.npcId))
510
584
  return failure("unauthorized");
585
+ const external = sourceSession.hostId !== a.hostId;
586
+ if (external || authorizeEvidence) {
587
+ const target = change.targetParticipantId;
588
+ if ((external && d.kind !== "relationship") ||
589
+ !behaviorEvidenceVisible(s, a, proposal.npcId, proposal.sessionId, eventId, operator, {
590
+ authorizeEvidence,
591
+ purpose: "relationship-evidence",
592
+ targetParticipantId: target,
593
+ audienceParticipantIds: target ? [target] : [],
594
+ }) ||
595
+ (d.kind === "relationship" &&
596
+ !behaviorPermissions(s, { ...a, hostId: sourceSession.hostId }, proposal.npcId, sourceSession.sessionId, eventId, operator).updateRelationship))
597
+ return failure("unauthorized");
598
+ }
511
599
  const current = behaviorPermissions(s, a, proposal.npcId, proposal.sessionId, eventId, operator);
512
600
  if (!current.observe ||
513
601
  (d.kind === "relationship" &&
@@ -528,9 +616,11 @@ export function createBehaviorRuntime(store, options = {}) {
528
616
  : {}),
529
617
  ...(d.scope === "world" ? { worldId: session.worldId } : {}),
530
618
  }, key = valueKey(keyData);
619
+ const priorEvidence = new Set(b.history
620
+ .filter((entry) => valueKey(entry) === key)
621
+ .flatMap((entry) => entry.evidenceEventIds));
531
622
  if (changedKeys.has(key) ||
532
- b.history.some((h) => valueKey(h) === key &&
533
- h.evidenceEventIds.some((e) => change.evidenceEventIds.includes(e))))
623
+ change.evidenceEventIds.every((eventId) => priorEvidence.has(eventId)))
534
624
  return failure("conflict");
535
625
  changedKeys.add(key);
536
626
  const initialValues = effectiveBehavior(s, a, proposal.npcId, proposal.sessionId, operator).initialValues;
@@ -43,6 +43,18 @@ export interface AddressedEvent {
43
43
  observerIds: string[];
44
44
  text: string;
45
45
  }
46
+ /** Server-supplied authority over one existing event from a different host. */
47
+ export interface SessionEvidenceQuery {
48
+ purpose: "recall" | "relationship-evidence";
49
+ access: SessionAccess;
50
+ session: ParticipantSession;
51
+ npcId: string;
52
+ targetParticipantId?: string;
53
+ audienceParticipantIds: string[];
54
+ sourceSession: ParticipantSession;
55
+ event: AddressedEvent;
56
+ }
57
+ export type AuthorizeSessionEvidence = (query: Readonly<SessionEvidenceQuery>) => boolean;
46
58
  export interface SessionTurn {
47
59
  turnId: string;
48
60
  sessionId: string;
@@ -82,6 +94,7 @@ export declare class SessionError extends Error {
82
94
  /** No account system, model, transport or host mechanics are constructed here. */
83
95
  export declare function createParticipantRuntime(store: ParticipantStore, options?: {
84
96
  behaviorOperator?: BehaviorOperatorPolicy;
97
+ authorizeEvidence?: AuthorizeSessionEvidence;
85
98
  }): {
86
99
  /** Only trusted verified host mappings may call this; imported IDs never register themselves. */
87
100
  register(access: SessionAccess, participant: SessionParticipant): SessionParticipant;
@@ -30,6 +30,7 @@ const audience = (e) => [
30
30
  ];
31
31
  /** No account system, model, transport or host mechanics are constructed here. */
32
32
  export function createParticipantRuntime(store, options = {}) {
33
+ const authorizeEvidence = options.authorizeEvidence;
33
34
  const behaviorOperator = options.behaviorOperator
34
35
  ? structuredClone(options.behaviorOperator)
35
36
  : undefined;
@@ -166,13 +167,33 @@ export function createParticipantRuntime(store, options = {}) {
166
167
  history(access, sessionId, participantId) {
167
168
  return run(access, (s) => {
168
169
  const current = session(s, access, sessionId);
169
- if (!current.members.some((m) => m.participantId === participantId))
170
- fail("unauthorized");
171
- return s.events.filter((e) => e.sessionId === sessionId &&
172
- audience(e).includes(participantId) &&
173
- (current.members.find((m) => m.participantId === participantId)
174
- ?.role !== "npc" ||
175
- behaviorEvidenceVisible(s, access, participantId, sessionId, e.eventId)));
170
+ const member = current.members.find((item) => item.participantId === participantId);
171
+ if (!member)
172
+ return fail("unauthorized");
173
+ return s.events.filter((event) => {
174
+ const recordedAudience = audience(event);
175
+ if (event.sessionId !== sessionId ||
176
+ !recordedAudience.includes(participantId))
177
+ return false;
178
+ if (!authorizeEvidence)
179
+ return (member.role !== "npc" ||
180
+ behaviorEvidenceVisible(s, access, participantId, sessionId, event.eventId));
181
+ const npcs = current.members.filter((item) => item.role === "npc" &&
182
+ recordedAudience.includes(item.participantId) &&
183
+ (member.role === "player" ||
184
+ item.participantId === participantId));
185
+ const players = current.members.filter((item) => item.role === "player" &&
186
+ recordedAudience.includes(item.participantId));
187
+ return npcs.some((npc) => behaviorEvidenceVisible(s, access, npc.participantId, sessionId, event.eventId, behaviorOperator, {
188
+ authorizeEvidence,
189
+ audienceParticipantIds: recordedAudience.filter((id) => id !== npc.participantId),
190
+ targetParticipantId: member.role === "player"
191
+ ? participantId
192
+ : players.length === 1
193
+ ? players[0]?.participantId
194
+ : undefined,
195
+ }));
196
+ });
176
197
  });
177
198
  },
178
199
  /** Source-grounded relationship evidence, keyed by verified persistent player ID. */
@@ -185,11 +206,20 @@ export function createParticipantRuntime(store, options = {}) {
185
206
  return {
186
207
  npcId,
187
208
  playerId,
188
- events: s.events.filter((e) => audience(e).includes(npcId) &&
189
- audience(e).includes(playerId) &&
190
- s.sessions.some((source) => source.sessionId === e.sessionId &&
191
- source.hostId === access.hostId &&
192
- current.recallWorldIds.includes(source.worldId))),
209
+ events: s.events.filter((event) => {
210
+ if (![npcId, playerId].every((id) => audience(event).includes(id)))
211
+ return false;
212
+ const source = s.sessions.find((item) => item.sessionId === event.sessionId);
213
+ return (!!source &&
214
+ current.recallWorldIds.includes(source.worldId) &&
215
+ ((source.hostId === access.hostId && !authorizeEvidence) ||
216
+ behaviorEvidenceVisible(s, access, npcId, sessionId, event.eventId, behaviorOperator, {
217
+ authorizeEvidence,
218
+ purpose: "relationship-evidence",
219
+ targetParticipantId: playerId,
220
+ audienceParticipantIds: [playerId],
221
+ })));
222
+ }),
193
223
  };
194
224
  });
195
225
  },
@@ -243,16 +273,20 @@ export function createParticipantRuntime(store, options = {}) {
243
273
  behaviorRevision: behaviorRevision(s),
244
274
  });
245
275
  // Recall is NPC-private, with stable player identity across worlds. Sharing is checked at completion.
276
+ const participants = [...ticket.recipientIds, ...ticket.observerIds];
277
+ const playerIds = participants.filter((id) => current.members.some((member) => member.role === "player" && member.participantId === id));
246
278
  const memories = s.events
247
- .filter((e) => s.sessions.some((source) => source.sessionId === e.sessionId &&
248
- source.hostId === access.hostId &&
249
- current.recallWorldIds.includes(source.worldId)) &&
250
- behaviorEvidenceVisible(s, access, ticket.npcId, ticket.sessionId, e.eventId) &&
251
- [
252
- ticket.npcId,
253
- ...ticket.recipientIds,
254
- ...ticket.observerIds,
255
- ].every((p) => audience(e).includes(p)))
279
+ .filter((event) => {
280
+ const source = s.sessions.find((item) => item.sessionId === event.sessionId);
281
+ return (!!source &&
282
+ current.recallWorldIds.includes(source.worldId) &&
283
+ [ticket.npcId, ...participants].every((id) => audience(event).includes(id)) &&
284
+ behaviorEvidenceVisible(s, access, ticket.npcId, ticket.sessionId, event.eventId, source.hostId === access.hostId ? undefined : behaviorOperator, {
285
+ authorizeEvidence,
286
+ targetParticipantId: playerIds.length === 1 ? playerIds[0] : undefined,
287
+ audienceParticipantIds: participants,
288
+ }));
289
+ })
256
290
  .slice(-64);
257
291
  if (new TextEncoder().encode(JSON.stringify({ ticket, trigger, memories })).length > 65536)
258
292
  return fail("limit");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ifsoul/core",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Experimental public IfSoul protocol, runtime, memory and provider primitives",
package/release.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.7.0",
2
+ "version": "0.8.0",
3
3
  "current": {
4
4
  "protocol": "0.4.0",
5
5
  "modelProvider": "0.4.0",