@equationalapplications/core-llm-wiki 4.4.0 → 4.5.1

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
@@ -9,7 +9,9 @@ Pure TypeScript business logic for LLM Wiki Memory.
9
9
  - **Platform-agnostic** — Zero runtime dependencies; works with any SQLite driver via the `SQLiteAdapter` interface
10
10
  - **Semantic search** — Vector embeddings via your LLM's `embed` function, ranked by cosine similarity
11
11
  - **Keyword fallback** — MiniSearch in-memory index for offline/degraded scenarios when embeddings unavailable
12
- - **Retrieval tuning** — Per-call overrides for `maxResults`, `preFilterLimit`, and `hybridWeight` blend
12
+ - **Retrieval tuning** — Per-call overrides for `maxResults`, `preFilterLimit`, `hybridWeight`, `tierWeights`, and `includeZeroWeightEntities`
13
+ - **Multi-entity reads** — Search across multiple `entity_id` namespaces in one pass with per-entity score multipliers (`tierWeights`); optional `factScores` and `metadata` for explainability
14
+ - **Immutable vs mutable facts** — Use `WikiFact.source_type` to distinguish document-sourced facts (`immutable_document`) from derived or user-provided facts (`librarian_inferred`, `user_stated`, `user_confirmed`). Immutable document facts are not rewritten by `runLibrarian()` or `runHeal()` and can only be removed by `forget()` or re-ingesting.
13
15
  - **Full-featured memory** — Facts, tasks, events, maintenance jobs (librarian, heal, reembed, prune)
14
16
  - **Type-safe** — Built with TypeScript, full type exports
15
17
 
@@ -121,6 +123,19 @@ const memory = await wikiMemory.read('user-123', 'my preferences', {
121
123
  preFilterLimit: 20,
122
124
  hybridWeight: 0.5,
123
125
  });
126
+
127
+ // Multi-entity with tier weights
128
+ const multiMemory = await wikiMemory.read(['tier_wisdom', 'tier_fact', 'tier_working'], 'my preferences', {
129
+ maxResults: 8,
130
+ tierWeights: {
131
+ tier_wisdom: 2, // high-confidence curated notes boosted 2×
132
+ tier_fact: 1, // neutral baseline
133
+ tier_working: 0.25, // recent but unvetted context downranked
134
+ },
135
+ // includeZeroWeightEntities: true — include 0-weight entities as bottom-ranked filler
136
+ });
137
+ // multiMemory.factScores — optional Record<factId, weightedScore> for returned facts; may be absent/undefined
138
+ // multiMemory.metadata — optional { query, entityIds, tierWeights }; may be absent/undefined
124
139
  ```
125
140
 
126
141
  **Hybrid scoring blends:**
@@ -130,6 +145,15 @@ const memory = await wikiMemory.read('user-123', 'my preferences', {
130
145
 
131
146
  True cosine-range pure semantic ranking (including negative cosine values) is used when `hybridWeight` is left `undefined`.
132
147
 
148
+ **Tier weights:**
149
+ - `tierWeights` applies a per-entity multiplier after semantic/keyword scoring: `finalScore = retrievalScore × weight`
150
+ - Missing weights default to `1.0`. Negative weights clamp to `0`. Non-finite weights default to `1.0`.
151
+ - `tierWeights[entity] = 0` skips that entity's scored retrieval branch (no compute cost).
152
+ - `includeZeroWeightEntities: true` includes zero-weight entities as bottom-ranked filler instead of skipping them.
153
+ - `factScores` is present for array-shaped `entityId` calls only when the query is non-empty and at least one fact is scored; empty-query ("recent facts") reads leave it absent even when `entityId` is an array. Plain string calls never expose it. `metadata` is present for all array-shaped calls regardless of query.
154
+ - `maxResults` applies globally across all requested entities.
155
+ - Tasks are capped at `min(20 × entityCount, 200)`; events at `min(10 × entityCount, 100)` for multi-entity reads.
156
+
133
157
  **Pre-filtering optimization:**
134
158
  When `preFilterLimit: 50` is set with 1000 facts, cosine similarity is computed only for the top 50 MiniSearch keyword matches, reducing O(N) scoring to O(50).
135
159
 
@@ -398,6 +422,71 @@ await wikiMemory.write('user-123', {
398
422
  const memory = await wikiMemory.read('user-123', 'coding style preferences');
399
423
  ```
400
424
 
425
+ ### Multi-entity weighted reads
426
+
427
+ `read()` accepts either one entity id or an array of entity ids. Facts are always merged globally before `maxResults` is applied. For single-entity reads, tasks are uncapped and events are capped at 10. For multi-entity reads, tasks are capped at `min(20 × entity count, 200)` and events at `min(10 × entity count, 100)` — per-entity representation in the returned bundle is not guaranteed.
428
+
429
+ ```ts
430
+ const memory = await wikiMemory.read(['tier_wisdom', 'tier_fact', 'tier_working'], 'Which source should I trust?', {
431
+ maxResults: 8,
432
+ tierWeights: {
433
+ tier_wisdom: 2,
434
+ tier_fact: 1,
435
+ tier_working: 0.25,
436
+ },
437
+ });
438
+
439
+ console.log(memory.metadata);
440
+ console.log(memory.factScores);
441
+ ```
442
+
443
+ ### Librarian prompt override contract
444
+
445
+ Core exports prompt utilities for weighted retrieval-based synthesis. Use `mapLibrarianOptionsToReadOptions()` to map `entityWeights` to `tierWeights`, then hydrate a prompt with `query`, `context`, and `tasks`.
446
+
447
+ ```ts
448
+ import {
449
+ DEFAULT_LIBRARIAN_SYNTHESIS_PROMPT,
450
+ formatContext,
451
+ hydrateLibrarianPrompt,
452
+ mapLibrarianOptionsToReadOptions,
453
+ validateLibrarianPromptTemplate,
454
+ } from '@equationalapplications/core-llm-wiki';
455
+
456
+ const options = {
457
+ entityWeights: { tier_wisdom: 2, tier_fact: 1, tier_working: 0.25 },
458
+ systemPrompt: `You are a strict fact checker.
459
+ Question:
460
+ {{query}}
461
+
462
+ Retrieved context:
463
+ {{context}}
464
+
465
+ {{tasks}}`,
466
+ };
467
+
468
+ const query = 'Which source should I trust for recent project decisions?';
469
+
470
+ const memory = await wikiMemory.read(['tier_wisdom', 'tier_fact', 'tier_working'], query, {
471
+ ...mapLibrarianOptionsToReadOptions(options),
472
+ maxResults: 8,
473
+ });
474
+
475
+ const template = options.systemPrompt ?? DEFAULT_LIBRARIAN_SYNTHESIS_PROMPT;
476
+ const warnings = validateLibrarianPromptTemplate(template, {
477
+ custom: options.systemPrompt != null,
478
+ taskCount: memory.tasks.length,
479
+ });
480
+
481
+ for (const warning of warnings) console.warn(warning);
482
+
483
+ const finalPrompt = hydrateLibrarianPrompt(template, {
484
+ query,
485
+ context: formatContext(memory, { includeEntityIds: true, includeFactScores: true }),
486
+ tasks: formatContext({ facts: [], tasks: memory.tasks, events: [] }, { format: 'plain' }),
487
+ });
488
+ ```
489
+
401
490
  ## Adapter Interface
402
491
 
403
492
  Implement `SQLiteAdapter` to use your platform's SQLite driver:
@@ -488,7 +577,7 @@ const adapter: SQLiteAdapter = {
488
577
 
489
578
  ```mermaid
490
579
  flowchart TD
491
- A["read(entityId, query)"] --> B{hybridWeight = 0?}
580
+ A["read(entityId | entityId[], query, options?)"] --> B{hybridWeight = 0?}
492
581
  B -->|Yes| C["MiniSearch only<br/>(skip embed)"]
493
582
  B -->|No| D{embed available?}
494
583
  D -->|No| C
package/dist/index.d.mts CHANGED
@@ -313,6 +313,8 @@ interface FormatContextOptions {
313
313
  maxEvents?: number;
314
314
  includeConfidence?: boolean;
315
315
  includeTags?: boolean;
316
+ includeEntityIds?: boolean;
317
+ includeFactScores?: boolean;
316
318
  factWeights?: {
317
319
  confidence?: number;
318
320
  accessCount?: number;
@@ -531,6 +533,28 @@ declare function formatMemoryDump(dump: MemoryDump): FormattedMemoryDump;
531
533
 
532
534
  declare function parseEmbedding(blob: Uint8Array | null | undefined, text: string | null | undefined): Float32Array | null;
533
535
 
536
+ interface LibrarianOptions {
537
+ /** If provided, replaces the default Librarian system instructions. */
538
+ systemPrompt?: string;
539
+ /** entity_id -> score multiplier, forwarded to WikiMemory.read() as tierWeights. */
540
+ entityWeights?: Record<string, number>;
541
+ /** Forwarded to WikiMemory.read() for zero-weight filler context. */
542
+ includeZeroWeightEntities?: boolean;
543
+ temperature?: number;
544
+ }
545
+ interface LibrarianPromptVariables {
546
+ context: string;
547
+ tasks: string;
548
+ query: string;
549
+ }
550
+ declare const DEFAULT_LIBRARIAN_SYNTHESIS_PROMPT = "You are a careful memory synthesis assistant.\nUse only the retrieved context when answering the request.\nPreserve source provenance when facts come from different entity namespaces.\n\nRequest:\n{{query}}\n\nRetrieved context:\n{{context}}\n\nOpen tasks:\n{{tasks}}";
551
+ declare function hydrateLibrarianPrompt(template: string, variables: LibrarianPromptVariables): string;
552
+ declare function validateLibrarianPromptTemplate(template: string, options: {
553
+ custom: boolean;
554
+ taskCount: number;
555
+ }): string[];
556
+ declare function mapLibrarianOptionsToReadOptions(options: LibrarianOptions): Pick<ReadOptions, 'tierWeights' | 'includeZeroWeightEntities'>;
557
+
534
558
  declare function createWiki(db: SQLiteAdapter, options: WikiOptions): WikiMemory;
535
559
 
536
- export { type EntityStatus, type ExtractedFact, type ExtractedTask, type FormatContextOptions, type FormattedMemoryDump, type LLMProvider, type MemoryBundle, type MemoryDump, PrunePartialFailureError, type ReadOptions, type SQLiteAdapter, type VectorRanker, type VectorRankerFallback, type VectorRankerRankArgs, type VectorRankerSemanticResult, WikiBusyError, type WikiBusyOperation, type WikiCheckpoint, type WikiConfig, type WikiEvent, type WikiFact, WikiMemory, type WikiOptions, type WikiTask, createWiki, formatContext, formatMemoryDump, parseEmbedding };
560
+ export { DEFAULT_LIBRARIAN_SYNTHESIS_PROMPT, type EntityStatus, type ExtractedFact, type ExtractedTask, type FormatContextOptions, type FormattedMemoryDump, type LLMProvider, type LibrarianOptions, type LibrarianPromptVariables, type MemoryBundle, type MemoryDump, PrunePartialFailureError, type ReadOptions, type SQLiteAdapter, type VectorRanker, type VectorRankerFallback, type VectorRankerRankArgs, type VectorRankerSemanticResult, WikiBusyError, type WikiBusyOperation, type WikiCheckpoint, type WikiConfig, type WikiEvent, type WikiFact, WikiMemory, type WikiOptions, type WikiTask, createWiki, formatContext, formatMemoryDump, hydrateLibrarianPrompt, mapLibrarianOptionsToReadOptions, parseEmbedding, validateLibrarianPromptTemplate };
package/dist/index.d.ts CHANGED
@@ -313,6 +313,8 @@ interface FormatContextOptions {
313
313
  maxEvents?: number;
314
314
  includeConfidence?: boolean;
315
315
  includeTags?: boolean;
316
+ includeEntityIds?: boolean;
317
+ includeFactScores?: boolean;
316
318
  factWeights?: {
317
319
  confidence?: number;
318
320
  accessCount?: number;
@@ -531,6 +533,28 @@ declare function formatMemoryDump(dump: MemoryDump): FormattedMemoryDump;
531
533
 
532
534
  declare function parseEmbedding(blob: Uint8Array | null | undefined, text: string | null | undefined): Float32Array | null;
533
535
 
536
+ interface LibrarianOptions {
537
+ /** If provided, replaces the default Librarian system instructions. */
538
+ systemPrompt?: string;
539
+ /** entity_id -> score multiplier, forwarded to WikiMemory.read() as tierWeights. */
540
+ entityWeights?: Record<string, number>;
541
+ /** Forwarded to WikiMemory.read() for zero-weight filler context. */
542
+ includeZeroWeightEntities?: boolean;
543
+ temperature?: number;
544
+ }
545
+ interface LibrarianPromptVariables {
546
+ context: string;
547
+ tasks: string;
548
+ query: string;
549
+ }
550
+ declare const DEFAULT_LIBRARIAN_SYNTHESIS_PROMPT = "You are a careful memory synthesis assistant.\nUse only the retrieved context when answering the request.\nPreserve source provenance when facts come from different entity namespaces.\n\nRequest:\n{{query}}\n\nRetrieved context:\n{{context}}\n\nOpen tasks:\n{{tasks}}";
551
+ declare function hydrateLibrarianPrompt(template: string, variables: LibrarianPromptVariables): string;
552
+ declare function validateLibrarianPromptTemplate(template: string, options: {
553
+ custom: boolean;
554
+ taskCount: number;
555
+ }): string[];
556
+ declare function mapLibrarianOptionsToReadOptions(options: LibrarianOptions): Pick<ReadOptions, 'tierWeights' | 'includeZeroWeightEntities'>;
557
+
534
558
  declare function createWiki(db: SQLiteAdapter, options: WikiOptions): WikiMemory;
535
559
 
536
- export { type EntityStatus, type ExtractedFact, type ExtractedTask, type FormatContextOptions, type FormattedMemoryDump, type LLMProvider, type MemoryBundle, type MemoryDump, PrunePartialFailureError, type ReadOptions, type SQLiteAdapter, type VectorRanker, type VectorRankerFallback, type VectorRankerRankArgs, type VectorRankerSemanticResult, WikiBusyError, type WikiBusyOperation, type WikiCheckpoint, type WikiConfig, type WikiEvent, type WikiFact, WikiMemory, type WikiOptions, type WikiTask, createWiki, formatContext, formatMemoryDump, parseEmbedding };
560
+ export { DEFAULT_LIBRARIAN_SYNTHESIS_PROMPT, type EntityStatus, type ExtractedFact, type ExtractedTask, type FormatContextOptions, type FormattedMemoryDump, type LLMProvider, type LibrarianOptions, type LibrarianPromptVariables, type MemoryBundle, type MemoryDump, PrunePartialFailureError, type ReadOptions, type SQLiteAdapter, type VectorRanker, type VectorRankerFallback, type VectorRankerRankArgs, type VectorRankerSemanticResult, WikiBusyError, type WikiBusyOperation, type WikiCheckpoint, type WikiConfig, type WikiEvent, type WikiFact, WikiMemory, type WikiOptions, type WikiTask, createWiki, formatContext, formatMemoryDump, hydrateLibrarianPrompt, mapLibrarianOptionsToReadOptions, parseEmbedding, validateLibrarianPromptTemplate };
package/dist/index.js CHANGED
@@ -2793,16 +2793,20 @@ function scoreFactFor(fact, weights, now) {
2793
2793
  const recencyDecay = Math.exp(-ageDays / 30);
2794
2794
  return confW * weights.confidence + Math.log(1 + fact.access_count) * weights.accessCount + recencyDecay * weights.recency;
2795
2795
  }
2796
- function renderFactMarkdown(fact, includeConfidence, includeTags) {
2796
+ function renderFactMarkdown(fact, includeConfidence, includeTags, includeEntityIds, score) {
2797
2797
  const confPart = includeConfidence ? ` (${fact.confidence})` : "";
2798
2798
  const tagPart = includeTags && fact.tags.length > 0 ? ` [${fact.tags.join(", ")}]` : "";
2799
- return `- **${fact.title}**${confPart}${tagPart}
2799
+ const sourcePart = includeEntityIds ? ` {entity_id=${fact.entity_id}}` : "";
2800
+ const scorePart = score !== void 0 ? ` {score=${score.toFixed(4)}}` : "";
2801
+ return `- **${fact.title}**${confPart}${tagPart}${sourcePart}${scorePart}
2800
2802
  ${fact.body.replace(/\n/g, "\n ")}`;
2801
2803
  }
2802
- function renderFactPlain(fact, includeConfidence, includeTags) {
2804
+ function renderFactPlain(fact, includeConfidence, includeTags, includeEntityIds, score) {
2803
2805
  const confPart = includeConfidence ? ` (${fact.confidence})` : "";
2804
2806
  const tagPart = includeTags && fact.tags.length > 0 ? ` [${fact.tags.join(", ")}]` : "";
2805
- return `${fact.title}${confPart}${tagPart}: ${fact.body}`;
2807
+ const sourcePart = includeEntityIds ? ` {entity_id=${fact.entity_id}}` : "";
2808
+ const scorePart = score !== void 0 ? ` {score=${score.toFixed(4)}}` : "";
2809
+ return `${fact.title}${confPart}${tagPart}${sourcePart}${scorePart}: ${fact.body}`;
2806
2810
  }
2807
2811
  function renderTaskMarkdown(task) {
2808
2812
  return `- [P${task.priority}] ${task.description.replace(/\n/g, "\n ")} (${task.status})`;
@@ -2826,6 +2830,8 @@ function formatContext(bundle, options) {
2826
2830
  maxEvents: options?.maxEvents ?? 10,
2827
2831
  includeConfidence: options?.includeConfidence ?? true,
2828
2832
  includeTags: options?.includeTags ?? true,
2833
+ includeEntityIds: options?.includeEntityIds ?? false,
2834
+ includeFactScores: options?.includeFactScores ?? false,
2829
2835
  factWeights: {
2830
2836
  confidence: options?.factWeights?.confidence ?? 1,
2831
2837
  accessCount: options?.factWeights?.accessCount ?? 0.3,
@@ -2837,7 +2843,7 @@ function formatContext(bundle, options) {
2837
2843
  validateMaxOption(opts.maxEvents, "maxEvents");
2838
2844
  const weights = opts.factWeights;
2839
2845
  const now = Date.now();
2840
- const sortedFacts = [...bundle.facts].sort((a, b) => scoreFactFor(b, weights, now) - scoreFactFor(a, weights, now)).slice(0, opts.maxFacts);
2846
+ const sortedFacts = bundle.factScores ? [...bundle.facts].slice(0, opts.maxFacts) : [...bundle.facts].sort((a, b) => scoreFactFor(b, weights, now) - scoreFactFor(a, weights, now)).slice(0, opts.maxFacts);
2841
2847
  const sortedTasks = [...bundle.tasks].sort((a, b) => b.priority - a.priority || a.created_at - b.created_at).slice(0, opts.maxTasks);
2842
2848
  const sortedEvents = [...bundle.events].sort((a, b) => b.created_at - a.created_at).slice(0, opts.maxEvents);
2843
2849
  if (sortedFacts.length === 0 && sortedTasks.length === 0 && sortedEvents.length === 0) {
@@ -2851,7 +2857,7 @@ function formatContext(bundle, options) {
2851
2857
  lines.push("");
2852
2858
  lines.push("### Known Facts");
2853
2859
  for (const fact of sortedFacts) {
2854
- lines.push(renderFactMarkdown(fact, opts.includeConfidence, opts.includeTags));
2860
+ lines.push(renderFactMarkdown(fact, opts.includeConfidence, opts.includeTags, opts.includeEntityIds, opts.includeFactScores ? bundle.factScores?.[fact.id] : void 0));
2855
2861
  }
2856
2862
  }
2857
2863
  if (sortedTasks.length > 0) {
@@ -2872,7 +2878,7 @@ function formatContext(bundle, options) {
2872
2878
  if (sortedFacts.length > 0) {
2873
2879
  lines.push("KNOWN FACTS:");
2874
2880
  for (const fact of sortedFacts) {
2875
- lines.push(renderFactPlain(fact, opts.includeConfidence, opts.includeTags));
2881
+ lines.push(renderFactPlain(fact, opts.includeConfidence, opts.includeTags, opts.includeEntityIds, opts.includeFactScores ? bundle.factScores?.[fact.id] : void 0));
2876
2882
  }
2877
2883
  }
2878
2884
  if (sortedTasks.length > 0) {
@@ -2991,17 +2997,60 @@ function formatMemoryDump(dump) {
2991
2997
  };
2992
2998
  }
2993
2999
 
3000
+ // src/librarianPrompt.ts
3001
+ var DEFAULT_LIBRARIAN_SYNTHESIS_PROMPT = `You are a careful memory synthesis assistant.
3002
+ Use only the retrieved context when answering the request.
3003
+ Preserve source provenance when facts come from different entity namespaces.
3004
+
3005
+ Request:
3006
+ {{query}}
3007
+
3008
+ Retrieved context:
3009
+ {{context}}
3010
+
3011
+ Open tasks:
3012
+ {{tasks}}`;
3013
+ function hydrateLibrarianPrompt(template, variables) {
3014
+ return template.replace(/\{\{(context|tasks|query)\}\}/g, (_, key) => variables[key]);
3015
+ }
3016
+ function validateLibrarianPromptTemplate(template, options) {
3017
+ if (!options.custom) return [];
3018
+ const warnings = [];
3019
+ if (!template.includes("{{context}}")) {
3020
+ warnings.push("Custom Librarian systemPrompt omits {{context}}; retrieved memory will not be injected.");
3021
+ }
3022
+ if (!template.includes("{{query}}")) {
3023
+ warnings.push("Custom Librarian systemPrompt omits {{query}}; the original request will not be injected.");
3024
+ }
3025
+ if (options.taskCount > 0 && !template.includes("{{tasks}}")) {
3026
+ warnings.push("Custom Librarian systemPrompt omits {{tasks}} while retrieved tasks are available.");
3027
+ }
3028
+ return warnings;
3029
+ }
3030
+ function mapLibrarianOptionsToReadOptions(options) {
3031
+ const readOptions = {};
3032
+ if (options.entityWeights !== void 0) readOptions.tierWeights = options.entityWeights;
3033
+ if (options.includeZeroWeightEntities !== void 0) {
3034
+ readOptions.includeZeroWeightEntities = options.includeZeroWeightEntities;
3035
+ }
3036
+ return readOptions;
3037
+ }
3038
+
2994
3039
  // src/index.ts
2995
3040
  function createWiki(db, options) {
2996
3041
  return new WikiMemory(db, options);
2997
3042
  }
2998
3043
 
3044
+ exports.DEFAULT_LIBRARIAN_SYNTHESIS_PROMPT = DEFAULT_LIBRARIAN_SYNTHESIS_PROMPT;
2999
3045
  exports.PrunePartialFailureError = PrunePartialFailureError;
3000
3046
  exports.WikiBusyError = WikiBusyError;
3001
3047
  exports.WikiMemory = WikiMemory;
3002
3048
  exports.createWiki = createWiki;
3003
3049
  exports.formatContext = formatContext;
3004
3050
  exports.formatMemoryDump = formatMemoryDump;
3051
+ exports.hydrateLibrarianPrompt = hydrateLibrarianPrompt;
3052
+ exports.mapLibrarianOptionsToReadOptions = mapLibrarianOptionsToReadOptions;
3005
3053
  exports.parseEmbedding = parseEmbedding;
3054
+ exports.validateLibrarianPromptTemplate = validateLibrarianPromptTemplate;
3006
3055
  //# sourceMappingURL=index.js.map
3007
3056
  //# sourceMappingURL=index.js.map