soulbridge 0.0.26 → 0.0.98

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 (44) hide show
  1. package/LICENSE +202 -202
  2. package/dist/packages/cli/src/cli.js +0 -0
  3. package/dist/packages/core/src/config/dimensions.d.ts +18 -0
  4. package/dist/packages/core/src/config/dimensions.js +33 -0
  5. package/dist/packages/core/src/config/domains.d.ts +70 -0
  6. package/dist/packages/core/src/config/domains.js +70 -0
  7. package/dist/packages/core/src/config/labels.d.ts +22 -0
  8. package/dist/packages/core/src/config/labels.js +21 -0
  9. package/dist/packages/core/src/config/status.d.ts +51 -0
  10. package/dist/packages/core/src/config/status.js +47 -0
  11. package/dist/packages/core/src/consolidate.d.ts +170 -0
  12. package/dist/packages/core/src/consolidate.js +311 -0
  13. package/dist/packages/core/src/context.d.ts +2 -0
  14. package/dist/packages/core/src/context.js +48 -17
  15. package/dist/packages/core/src/dimension-text.d.ts +6 -0
  16. package/dist/packages/core/src/dimension-text.js +24 -0
  17. package/dist/packages/core/src/dimensions.d.ts +28 -0
  18. package/dist/packages/core/src/dimensions.js +53 -0
  19. package/dist/packages/core/src/extract.d.ts +140 -0
  20. package/dist/packages/core/src/extract.js +611 -0
  21. package/dist/packages/core/src/index.d.ts +362 -10
  22. package/dist/packages/core/src/index.js +983 -127
  23. package/dist/packages/core/src/inject-text.d.ts +16 -0
  24. package/dist/packages/core/src/inject-text.js +24 -0
  25. package/dist/packages/core/src/profile-prompt.d.ts +18 -0
  26. package/dist/packages/core/src/profile-prompt.js +103 -0
  27. package/dist/packages/core/src/prompts.d.ts +122 -0
  28. package/dist/packages/core/src/prompts.generated.d.ts +9 -0
  29. package/dist/packages/core/src/prompts.generated.js +9 -0
  30. package/dist/packages/core/src/prompts.js +293 -0
  31. package/dist/packages/core/src/reflect.d.ts +7 -0
  32. package/dist/packages/core/src/reflect.js +25 -27
  33. package/dist/packages/core/src/retrieval.d.ts +39 -0
  34. package/dist/packages/core/src/retrieval.js +105 -0
  35. package/dist/packages/core/src/roles.d.ts +18 -0
  36. package/dist/packages/core/src/roles.js +17 -0
  37. package/dist/packages/core/src/rules.d.ts +69 -0
  38. package/dist/packages/core/src/rules.js +152 -0
  39. package/dist/packages/core/src/sqlite.d.ts +26 -5
  40. package/dist/packages/core/src/sqlite.js +944 -33
  41. package/dist/packages/core/src/store.d.ts +92 -3
  42. package/dist/packages/core/src/types.d.ts +442 -1
  43. package/dist/packages/core/src/types.js +1 -0
  44. package/package.json +5 -3
@@ -11,16 +11,31 @@ export { buildContext, styleDirectives, DEFAULT_MAX_CHARS, AFFECT_BUDGET, AFFECT
11
11
  export { reflect, ruleExtract, nextEmotion, isCorrection, correctionRule, detectCorrection, isQuestion, } from './reflect.ts';
12
12
  export type { CorrectionLayer, CorrectionSignal, CorrectionVerdict } from './reflect.ts';
13
13
  export { rank, scoreMemory, tokenize, relevance, bm25, buildCorpusStats } from './scoring.ts';
14
+ export { parseThreadLine } from './extract.ts';
15
+ export { renderConsolidationPrompt, renderVerificationPrompt, renderRelationPrompt, renderRuleClassifyPrompt, renderProfileMergePrompt, renderExtractionPrompt, } from './prompts.ts';
16
+ export type { ConsolidationPromptInput, VerificationPromptInput, RelationPromptInput, RuleClassifyPromptInput, ProfileMergePromptInput, ExtractionPromptInput, } from './prompts.ts';
17
+ export { gateConclusion, conclusionSlots, normalizeStatement, parseConclusionDraft, parseVerificationDraft, parseRelationDraft, readVerification, parseProfileMerge, groupProfileCandidates, isDerivedProfileLine, PROFILE_DIMENSIONS, } from './consolidate.ts';
18
+ export type { VerificationDraft, ConclusionRelation } from './consolidate.ts';
19
+ export type { ProfileLine, ProfileCandidate, ProfileGroup } from './consolidate.ts';
20
+ import type { ProfileLine } from './consolidate.ts';
21
+ export { fires, parseRuleClassifications, parseTrigger, selectRules } from './rules.ts';
22
+ export type { Rule, RuleTrigger, RuleClassification } from './rules.ts';
23
+ export { planRetrieval } from './retrieval.ts';
24
+ export type { RetrievalPlan, RetrievalPrimitive } from './retrieval.ts';
14
25
  export * from './affect/index.ts';
26
+ export { escapeBlockText, escapeAttr } from './inject-text.ts';
15
27
  export { measureStyle } from './style/measure.ts';
16
28
  export type { StyleMetrics } from './style/measure.ts';
17
29
  export * from './storage/config.ts';
18
30
  export * from './storage/merge.ts';
19
31
  export * from './storage/steering.ts';
20
32
  export * from './storage/transcript.ts';
21
- import type { AffectSignal, ContextRequest, Correction, Memory, ContextResult, Emotion, LlmFn, MemoryType, ObserveResult, ReflectResult, ScoredMemory, Turn, TurnPair, Visibility } from './types.ts';
33
+ import type { AffectSignal, ContextRequest, Correction, Memory, ContextResult, Emotion, LlmFn, MemoryType, ObserveResult, ReflectResult, ScoredMemory, Turn, CommitResult, Entity, ExtractDraft, ConclusionDraft, Fact, ThreadAttribution, TurnThread, FactDetail, FactSpec, Gap, GapSpec, SessionHandoff, TurnFindResult, SessionSummary, TurnPair, TurnStep, Visibility } from './types.ts';
22
34
  import type { SoulStore } from './store.ts';
23
35
  import type { AffectAnalyzer } from './affect/port.ts';
36
+ import type { Rule } from './rules.ts';
37
+ import type { RetrievalPlan } from './retrieval.ts';
38
+ import type { ExtractionContext } from './extract.ts';
24
39
  export { gateDrafts } from './reflect.ts';
25
40
  export interface SoulOptions {
26
41
  store: SoulStore;
@@ -71,6 +86,12 @@ export declare class Soul {
71
86
  * Audit view: every non-deleted row, including superseded and expired ones.
72
87
  * Prompt builders use `context`, which filters both out.
73
88
  */
89
+ /**
90
+ * The recent holdings, as the shape hosts already consume.
91
+ *
92
+ * Reads facts: the legacy table is inert after the migration, and a list that kept
93
+ * reading it would look empty while the system clearly remembers things.
94
+ */
74
95
  list(limit?: number): Memory[];
75
96
  /** Tombstone one memory so it never reaches a prompt again. */
76
97
  forget(id: string): boolean;
@@ -86,6 +107,23 @@ export declare class Soul {
86
107
  rule?: string;
87
108
  }): string;
88
109
  corrections(limit?: number): Correction[];
110
+ /**
111
+ * Was this turn one the user corrected?
112
+ *
113
+ * A correction is the highest-signal moment a lesson has: being told you were
114
+ * wrong is the consequence the experience comes with (brief 0023, T1). The link
115
+ * is the user's own words — corrections do not carry a turn id, and inventing a
116
+ * time window would misattribute a nearby turn.
117
+ */
118
+ wasCorrectedTurn(turnId: string): boolean;
119
+ /**
120
+ * The experiences of one turn that were flagged as lessons, and nothing else.
121
+ *
122
+ * Promotion reads the flag, never the statement: whether a sentence is "about how
123
+ * to work" is the semantic call the extractor already made, and re-deriving it in
124
+ * code would be the guess ADR 0027 forbids.
125
+ */
126
+ promotionCandidates(turnId: string): Fact[];
89
127
  /**
90
128
  * Store one exchange (RFC 0007). Idempotent on `(sessionId, turn)`: a repeat is
91
129
  * ignored by the store, so this returns nothing — there is no id worth handing
@@ -98,24 +136,322 @@ export declare class Soul {
98
136
  assistantText: string;
99
137
  at?: number;
100
138
  metadata?: Record<string, unknown>;
101
- }): void;
139
+ /**
140
+ * The real shape of the reply: reasoning / text / tool steps in order.
141
+ *
142
+ * Stored, never injected. Reasoning is a draft, so recall and prompts read
143
+ * `kind === 'text'` only; it is kept because "why did you say that" and
144
+ * consolidation both need it.
145
+ */
146
+ steps?: TurnStep[];
147
+ }): string;
148
+ /**
149
+ * One turn by id, for adapters that only kept the id (extraction queue,
150
+ * handoff). Returns null when the row is gone, so callers can drop the item
151
+ * instead of retrying forever.
152
+ */
153
+ turn(id: string): TurnPair | null;
102
154
  /** Recall exchanges, scored against `query` when it is non-empty. */
103
155
  recallTurnPairs(query: string, limit?: number): TurnPair[];
104
156
  countTurnPairs(): number;
105
- /** Verb 3: on demand. */
157
+ /**
158
+ * Every turn that matches, not the best few: for "how many times" and "what
159
+ * did we decide overall" the count matters more than the ranking, and the
160
+ * model does the counting (ADR 0027). `truncated` tells the caller the answer
161
+ * would be incomplete.
162
+ */
163
+ /**
164
+ * Turn text → drafts.
165
+ *
166
+ * Extraction is the model's job (ADR 0029: Generative Agents, Mem0 and
167
+ * Graphiti all extract with an LLM and keep code for validation and storage).
168
+ * The rules are a floor, not a ceiling: they matter when no model is
169
+ * configured and they cross-check the answer when one is. A model answer with
170
+ * no usable draft falls back to the rules instead of losing the turn.
171
+ */
172
+ extract(text: string, options?: {
173
+ at?: number;
174
+ llm?: (prompt: string) => Promise<string> | string;
175
+ /** 'auto' (default) uses the model then falls back; 'rules' forces rules. */
176
+ mode?: 'auto' | 'rules';
177
+ /** What the model may see; defaults to the recent store state. */
178
+ context?: ExtractionContext;
179
+ /**
180
+ * My reply in the same turn. The unit of memory is the exchange (ADR 0035):
181
+ * without this half, "my promises" is a rule the model can never use.
182
+ */
183
+ assistantText?: string;
184
+ }): Promise<ExtractDraft>;
185
+ /** The slice of the store the extractor is allowed to know about. */
186
+ extractionContext(limit?: number, options?: {
187
+ turnNo?: number;
188
+ sessionId?: string;
189
+ }): ExtractionContext;
190
+ /**
191
+ * Put a turn into a thread.
192
+ *
193
+ * Default is continuity (ADR 0034): a named thread joins when it exists and is open;
194
+ * an unreadable answer, a hallucinated id, or no answer at all joins the previous
195
+ * turn's thread instead of opening a new one — fragmentation is what starves
196
+ * consolidation, and threads can be split later while a lost topic cannot.
197
+ */
198
+ applyThread(attribution: ThreadAttribution | undefined, spec: {
199
+ sessionId: string;
200
+ turnNo: number;
201
+ turnId: string;
202
+ at: number;
203
+ fallbackTopic?: string;
204
+ /** The thread the previous turn belonged to; the default when unsure. */
205
+ previousThreadId?: string;
206
+ }): {
207
+ threadId: string;
208
+ created: boolean;
209
+ topic: string;
210
+ joinReason: string;
211
+ topicUpdated: boolean;
212
+ };
213
+ private openThreadFor;
214
+ /**
215
+ * Drafts → database.
216
+ *
217
+ * The gate is a labeller, not a filter. A sentence that parses is kept: an
218
+ * undeclared entity in a slot is kept as text (no entity is created), an unknown
219
+ * holder falls back to the world layer, and only an unreadable statement or an
220
+ * exact duplicate is refused. Everything degraded or refused is reported, so the
221
+ * loss is visible instead of silent.
222
+ */
223
+ commitDrafts(draft: ExtractDraft, options?: {
224
+ turnId?: string;
225
+ at?: number;
226
+ }): CommitResult;
227
+ /** Remember a person/thing/place; calling again just refreshes "last mentioned". */
228
+ rememberEntity(name: string, type: Entity['type'], options?: {
229
+ aliases?: string[];
230
+ attrs?: Record<string, unknown>;
231
+ seenAt?: number;
232
+ }): Entity;
233
+ findEntities(name: string, limit?: number): Entity[];
234
+ /** Write one connection between things. */
235
+ recordFact(spec: Omit<FactSpec, 'soulId' | 'userId'>): Fact;
236
+ /**
237
+ * Store one conclusion if the deterministic gate accepts it.
238
+ *
239
+ * A conclusion is not a new kind of record: it is a fact whose type is 结论,
240
+ * held by the soul itself, with one `fact` slot per citation. That way recall,
241
+ * export and supersede keep working, and the evidence can be followed back to
242
+ * the observation that produced it.
243
+ */
244
+ recordConclusion(draft: ConclusionDraft, options: {
245
+ allowed: string[];
246
+ mergeInto?: string;
247
+ }): {
248
+ ok: boolean;
249
+ reason?: string;
250
+ fact?: Fact;
251
+ superseded?: string;
252
+ merged?: string;
253
+ };
254
+ /**
255
+ * The rules we hold (ADR 0033): facts whose type is 规则 and whose 触发 slot parses.
256
+ *
257
+ * A rule without a usable trigger is skipped, not injected — an always-on rule is
258
+ * wallpaper, and wallpaper is how a lesson gets ignored.
259
+ */
260
+ rules(): Rule[];
261
+ /**
262
+ * Apply a rule classification (ADR 0033): retag as 规则 and store its trigger.
263
+ *
264
+ * The trigger is what makes the rule injectable later; a rule without one would
265
+ * have to be always-on, which is exactly what this split exists to avoid.
266
+ */
267
+ markRule(factId: string, classification: {
268
+ trigger: string;
269
+ action?: string;
270
+ }): void;
271
+ /** Apply a profile classification: it is about the person, not a triggered rule. */
272
+ markProfile(factId: string, options?: {
273
+ dimension?: string;
274
+ }): void;
275
+ /**
276
+ * Park a lesson that could not be classified.
277
+ *
278
+ * Neither bucket is safe for it: as a rule it would fire every turn, and as a
279
+ * profile line it would put an imperative into the description of a person. It
280
+ * goes to 待修, which nothing injects, and shows up in a review count.
281
+ */
282
+ markUnclassified(factId: string): void;
283
+ /** The judgments about the person that are waiting to be merged. */
284
+ profileItems(limit?: number): Fact[];
285
+ /**
286
+ * Replace a pile of judgments with the merged ones.
287
+ *
288
+ * Each merged line becomes a 画像 fact citing its sources, and every source it
289
+ * covers is superseded — not deleted, so the audit trail survives the compression.
290
+ */
291
+ applyProfileMerge(lines: ProfileLine[]): Fact[];
292
+ /** The merged profile, strongest first — what is injected every turn. */
293
+ profile(limit?: number): Fact[];
294
+ /** Which primitive this question needs (0020).
295
+ *
296
+ * The subject is resolved against the entities we hold, so "上次和 a 吃饭" can
297
+ * never be answered with the last closed topic.
298
+ */
299
+ planRetrieval(text: string): RetrievalPlan;
300
+ /** The entity timeline: what is known about one person/thing, newest first. */
301
+ latestAbout(entityName: string, options?: {
302
+ predicate?: string;
303
+ query?: string;
304
+ limit?: number;
305
+ }): Fact[];
306
+ /** Mark facts as surfaced; retrieval practice slows their decay (0019). */
307
+ recordRecalls(factIds: string[], at?: number): void;
308
+ /**
309
+ * Close topics the conversation has moved past.
310
+ *
311
+ * Three signals, in order of strength: the host's archive list (the user said
312
+ * this session is done), turns elapsed inside the running session, and — only
313
+ * as a fallback — silence in another session.
314
+ */
315
+ closeIdleThreads(sessionId: string, options?: {
316
+ idleTurns?: number;
317
+ idleMs?: number;
318
+ archivedSessionIds?: string[];
319
+ at?: number;
320
+ }): Array<{
321
+ id: string;
322
+ reason: 'archived' | 'quiet-turns' | 'quiet-time';
323
+ }>;
324
+ /** The thread the previous turn belonged to, for the attribution candidate list. */
325
+ threadBefore(sessionId: string, turnNo: number): string | null;
326
+ /**
327
+ * Everything a closed thread needs for consolidation: the topic, the turns in
328
+ * order (as text), and the facts extracted from those turns.
329
+ *
330
+ * The turns are what makes this a chapter rather than a batch: A B A means the
331
+ * facts alone have lost the resume.
332
+ */
333
+ threadContext(threadId: string): {
334
+ thread: TurnThread;
335
+ turns: Array<{
336
+ turnNo: number;
337
+ userText: string;
338
+ assistantText: string;
339
+ }>;
340
+ facts: Fact[];
341
+ } | null;
342
+ /**
343
+ * What "where were we" means: the last topic that actually finished, its
344
+ * conclusion, and the last couple of turns.
345
+ *
346
+ * A session boundary is not a topic boundary, so this is keyed on the closed
347
+ * thread, not on the previous session.
348
+ */
349
+ lastClosedThread(): {
350
+ thread: TurnThread;
351
+ conclusion: Fact | null;
352
+ /** Both sides of the last turns (ADR 0035): "where were we" includes what I said. */
353
+ turns: Array<{
354
+ turnNo: number;
355
+ userText: string;
356
+ assistantText: string;
357
+ }>;
358
+ } | null;
359
+ /** The entities a set of facts mentions — the key a conclusion is recalled by. */
360
+ entitiesOfFacts(factIds: string[]): string[];
361
+ /**
362
+ * Conclusions worth comparing a new one against.
363
+ *
364
+ * Ordered by shared evidence first, then recency: two conclusions drawn from
365
+ * the same facts are the likeliest restatements, and the recency tail catches
366
+ * the ones whose evidence was reworded.
367
+ */
368
+ conclusionCandidates(evidence: string[], limit?: number): Array<{
369
+ id: string;
370
+ statement: string;
371
+ evidence: number;
372
+ }>;
373
+ /**
374
+ * Fold new evidence into a held conclusion.
375
+ *
376
+ * The statement is not rewritten: keeping it makes the merge auditable, and a
377
+ * richer statement is the model's job at the next natural revision, not this
378
+ * function's.
379
+ */
380
+ mergeEvidence(into: string, evidence: string[]): {
381
+ ok: boolean;
382
+ reason?: string;
383
+ added?: number;
384
+ };
385
+ /** Current conclusions, newest first. Superseded ones are history, not state. */
386
+ conclusions(limit?: number): Fact[];
387
+ /**
388
+ * The newest facts, newest first — the input batch for consolidation.
389
+ *
390
+ * Observations only: conclusions share the same table, and feeding a conclusion
391
+ * back as evidence would let the model cite its own guess.
392
+ */
393
+ recentFacts(limit?: number): Fact[];
394
+ /** How many observations exist; consolidation triggers on growth, not time. */
395
+ factTotal(): number;
396
+ /**
397
+ * Everything that happened to or around one entity — the "about her" path.
398
+ *
399
+ * `order: 'asc'` gives the earliest fact, which is how "when we first met"
400
+ * is answered without storing a special label: the model reads the timeline.
401
+ */
402
+ factsAbout(entityId: string, options?: {
403
+ since?: number;
404
+ until?: number;
405
+ limit?: number;
406
+ order?: 'asc' | 'desc';
407
+ }): Fact[];
408
+ /** A fact plus the conversation behind it: hook first, detail on demand. */
409
+ factDetail(factId: string): FactDetail | null;
410
+ /** Open questions, highest tension first. */
411
+ openGaps(limit?: number): Gap[];
412
+ recordGap(spec: Omit<GapSpec, 'soulId' | 'userId'>): Gap;
413
+ closeGap(id: string, closedByFactId?: string): void;
414
+ findTurns(text: string, options?: {
415
+ sessionId?: string;
416
+ since?: number;
417
+ until?: number;
418
+ limit?: number;
419
+ }): TurnFindResult;
420
+ /** Conversations, most recent first, derived from the recorded exchanges. */
421
+ sessions(limit?: number): SessionSummary[];
422
+ /**
423
+ * What a new session inherits from the previous conversation.
424
+ *
425
+ * Measured: naive first/last picked "ok" and "停" — boundaries are not the
426
+ * same as signals. So the topic is the longest user message of that session
427
+ * (shortest messages carry no topic), and the tail is the last few exchanges
428
+ * whose user message has substance.
429
+ */
430
+ handoff(currentSessionId: string, tail?: number): SessionHandoff | null;
431
+ /**
432
+ * Verb 3: on demand.
433
+ *
434
+ * Reads facts, not the legacy `memories` table: after the migration that table is
435
+ * inert, and a recall that still read it would stay empty forever.
436
+ */
106
437
  recall(query: string, limit?: number): ScoredMemory[];
107
438
  /** Pure analysis, no storage. Async because the analyzer may call a model. */
108
439
  analyze(text: string): Promise<AffectSignal>;
109
440
  /**
110
- * Verb 2: after each turn.
441
+ * Verb 2: after each turn. Affect and emotion only.
111
442
  *
112
- * The analyzer may be async (a model); any failure degrades to the lexicon
113
- * analyzer. Emotion is computed from the turn only — never from affect.
443
+ * Memory writing moved to `extract()` + `commitDrafts()` (facts), which is the one
444
+ * representation the rest of the system reads. This method used to run a second
445
+ * model pass (`llmExtractTurn`) behind a second gate (`gateDrafts`) into the legacy
446
+ * `memories` table — two prompts, two gates, two model calls for the same message.
114
447
  */
115
448
  observe(turn: Turn): Promise<ObserveResult>;
116
449
  /**
117
450
  * Explicit form of `observe`: the host decided this is worth keeping.
118
- * Same write path, no extraction heuristics.
451
+ *
452
+ * Writes a fact, like every other write path. This used to be a row in the legacy
453
+ * `memories` table, which meant "remember this" produced something the rest of the
454
+ * system (anchors, recall, conclusions) had to be told about separately.
119
455
  */
120
456
  remember(content: string, options?: {
121
457
  type?: MemoryType;
@@ -123,9 +459,12 @@ export declare class Soul {
123
459
  visibility?: Visibility;
124
460
  }): string;
125
461
  /**
126
- * Explicit write requested by a host or its model (RFC 0005). Unlike
127
- * `remember`, it passes the same gates as `observe` and dedupes by content,
128
- * so a tool cannot bypass shape/semantics or flood duplicates.
462
+ * Explicit write requested by a host or its model (RFC 0005).
463
+ *
464
+ * Writes a fact and keeps the old dedupe contract: exact duplicate returns the
465
+ * existing id, and a paraphrase inside the dedupe window is treated as the same
466
+ * statement. Shape checks are the statement checks (non-empty, short enough);
467
+ * the old `gateDrafts` layer existed only to protect the retired table.
129
468
  */
130
469
  rememberExplicit(input: {
131
470
  content: string;
@@ -137,6 +476,19 @@ export declare class Soul {
137
476
  duplicate?: boolean;
138
477
  reason?: string;
139
478
  };
479
+ /**
480
+ * One-off: move the legacy `memories` rows into facts (ADR 0032), then retire them.
481
+ *
482
+ * The old rows are the only description of the soul's own lessons that exists, so
483
+ * they are copied — never rewritten — and marked superseded pointing at the new
484
+ * fact. Re-running is safe: a statement that already has a fact is linked, not
485
+ * copied again.
486
+ */
487
+ migrateLegacyMemories(): {
488
+ migrated: number;
489
+ linked: number;
490
+ skipped: number;
491
+ };
140
492
  /** Verb 4: session end. */
141
493
  reflect(turns: Turn[], options?: {
142
494
  updateEmotion?: boolean;