soulbridge 0.0.5 → 0.0.6
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/dist/packages/cli/src/cli.d.ts +2 -0
- package/dist/packages/cli/src/llm-analyzer.d.ts +21 -0
- package/dist/packages/core/src/affect/analyze.d.ts +35 -0
- package/dist/packages/core/src/affect/directives.d.ts +16 -0
- package/dist/packages/core/src/affect/index.d.ts +8 -0
- package/dist/packages/core/src/affect/lexicon.d.ts +42 -0
- package/dist/packages/core/src/affect/port.d.ts +12 -0
- package/dist/packages/core/src/affect/rules.d.ts +16 -0
- package/dist/packages/core/src/affect/seed-lexicon.d.ts +12 -0
- package/dist/packages/core/src/context.d.ts +19 -0
- package/dist/packages/core/src/index.d.ts +73 -0
- package/dist/packages/core/src/reflect.d.ts +20 -0
- package/dist/packages/core/src/scoring.d.ts +30 -0
- package/dist/packages/core/src/sqlite.d.ts +9 -0
- package/dist/packages/core/src/storage/config.d.ts +22 -0
- package/dist/packages/core/src/storage/merge.d.ts +22 -0
- package/dist/packages/core/src/storage/steering.d.ts +26 -0
- package/dist/packages/core/src/storage/transcript.d.ts +22 -0
- package/dist/packages/core/src/store.d.ts +24 -0
- package/dist/packages/core/src/style/measure.d.ts +12 -0
- package/dist/packages/core/src/types.d.ts +136 -0
- package/package.json +57 -48
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional LLM-backed affect analyzer (ADR 0015).
|
|
3
|
+
*
|
|
4
|
+
* Deliberately lives in the CLI package, not in core: core must stay pure and
|
|
5
|
+
* offline. Opt-in only; every failure is thrown so the caller can degrade to
|
|
6
|
+
* the lexicon analyzer. The raw text never leaves the machine unless the user
|
|
7
|
+
* configured an endpoint.
|
|
8
|
+
*/
|
|
9
|
+
import type { AffectAnalyzer } from '../../core/src/index.ts';
|
|
10
|
+
export interface LlmAnalyzerOptions {
|
|
11
|
+
/** Base URL, e.g. http://localhost:11434/v1 (Ollama) or any OpenAI-compatible endpoint. */
|
|
12
|
+
url: string;
|
|
13
|
+
model: string;
|
|
14
|
+
apiKey?: string;
|
|
15
|
+
timeoutMs?: number;
|
|
16
|
+
/** Confidence is capped so a self-assured model cannot bypass the injection gate. */
|
|
17
|
+
maxConfidence?: number;
|
|
18
|
+
}
|
|
19
|
+
export declare function createLlmAnalyzer(options: LlmAnalyzerOptions): AffectAnalyzer;
|
|
20
|
+
/** Build the analyzer from env vars, or null when not configured. */
|
|
21
|
+
export declare function llmAnalyzerFromEnv(): AffectAnalyzer | null;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule-based affect analyzer (ADR 0016 budget: 8 rules, no narrow phrases).
|
|
3
|
+
*
|
|
4
|
+
* Rules implemented here:
|
|
5
|
+
* R1 lexicon hit R2 negation window (3 tokens)
|
|
6
|
+
* R3 degree window (2) R4 punctuation emphasis (!! ?? ……)
|
|
7
|
+
* R5 repeated characters R6 withdrawal phrases (abstain, never injected)
|
|
8
|
+
* R7 code fences skipped R8 very short input halves confidence
|
|
9
|
+
*
|
|
10
|
+
* Pure function: no I/O, no model, no globals besides a per-lexicon cache.
|
|
11
|
+
*/
|
|
12
|
+
import type { AffectSignal } from '../types.ts';
|
|
13
|
+
import type { Lexicon } from './lexicon.ts';
|
|
14
|
+
export declare const MAX_INPUT_CHARS = 32000;
|
|
15
|
+
export declare const NEGATION_WINDOW = 3;
|
|
16
|
+
export declare const DEGREE_WINDOW = 2;
|
|
17
|
+
export declare class AffectBudgetError extends Error {
|
|
18
|
+
readonly code = "BUDGET_EXCEEDED";
|
|
19
|
+
constructor(length: number);
|
|
20
|
+
}
|
|
21
|
+
/** Full-width punctuation → ASCII, strip code fences and shell lines, drop emoji. */
|
|
22
|
+
export declare function normalizeForAffect(text: string): string;
|
|
23
|
+
/** Max forward match over the lexicon dictionary; punctuation and spaces are separators. */
|
|
24
|
+
export declare function tokenizeForAffect(text: string, dict: Set<string>, maxLen: number): string[];
|
|
25
|
+
export interface AnalyzeOptions {
|
|
26
|
+
lexicon?: Lexicon;
|
|
27
|
+
}
|
|
28
|
+
export declare function analyzeAffect(text: string, options?: AnalyzeOptions): AffectSignal;
|
|
29
|
+
/** Lexicon analyzer as a port implementation (ADR 0015). */
|
|
30
|
+
export declare const lexiconAnalyzer: {
|
|
31
|
+
id: string;
|
|
32
|
+
analyze(text: string): AffectSignal;
|
|
33
|
+
};
|
|
34
|
+
/** Empty signal helper so callers do not reach for the shared constant by accident. */
|
|
35
|
+
export declare function emptyAffect(analyzer?: string): AffectSignal;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whitelisted style directives (ADR 0014).
|
|
3
|
+
*
|
|
4
|
+
* The prompt may only ever contain text from this catalog. There is no code
|
|
5
|
+
* path that turns free-form analysis into prose, because a wrong "you seem
|
|
6
|
+
* angry" is worse than saying nothing.
|
|
7
|
+
*/
|
|
8
|
+
import type { AffectSignal } from '../types.ts';
|
|
9
|
+
export type DirectiveId = 'hedging' | 'fragmented' | 'negative_high' | 'positive_high' | 'vague_terms';
|
|
10
|
+
export interface Directive {
|
|
11
|
+
id: DirectiveId;
|
|
12
|
+
text: string;
|
|
13
|
+
}
|
|
14
|
+
export declare const DIRECTIVE_CATALOG: readonly Directive[];
|
|
15
|
+
/** Which catalog entries the signal justifies, in priority order, capped at `max`. */
|
|
16
|
+
export declare function selectDirectives(signal: AffectSignal, max?: number): Directive[];
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export type { AffectAnalyzer } from './port.ts';
|
|
2
|
+
export { analyzeAffect, lexiconAnalyzer, emptyAffect, AffectBudgetError, normalizeForAffect, tokenizeForAffect, MAX_INPUT_CHARS, } from './analyze.ts';
|
|
3
|
+
export type { AnalyzeOptions } from './analyze.ts';
|
|
4
|
+
export { SEED_LEXICON } from './seed-lexicon.ts';
|
|
5
|
+
export { validateLexicon, DEGREE_DELTA, NEGATORS, ABSTAIN_TERMS } from './lexicon.ts';
|
|
6
|
+
export type { Lexicon, LexiconEntry, DegreeWords } from './lexicon.ts';
|
|
7
|
+
export { DIRECTIVE_CATALOG, selectDirectives } from './directives.ts';
|
|
8
|
+
export type { Directive, DirectiveId } from './directives.ts';
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lexicon contract (ADR 0013).
|
|
3
|
+
*
|
|
4
|
+
* We adopt the *structure* of the DUTIR taxonomy (7 categories, intensity 1-9,
|
|
5
|
+
* polarity) because structures are not copyrightable. We do NOT ship any word
|
|
6
|
+
* list derived from an academic lexicon: the seed list in seed-lexicon.ts is
|
|
7
|
+
* written for this project. Users may import their own lexicon and carry the
|
|
8
|
+
* licensing consequences themselves.
|
|
9
|
+
*/
|
|
10
|
+
import type { AffectCategory, AffectPolarity } from '../types.ts';
|
|
11
|
+
export interface LexiconEntry {
|
|
12
|
+
category: AffectCategory;
|
|
13
|
+
/** 1..9 */
|
|
14
|
+
intensity: number;
|
|
15
|
+
polarity: AffectPolarity;
|
|
16
|
+
}
|
|
17
|
+
export interface DegreeWords {
|
|
18
|
+
weak: string[];
|
|
19
|
+
medium: string[];
|
|
20
|
+
strong: string[];
|
|
21
|
+
extreme: string[];
|
|
22
|
+
}
|
|
23
|
+
export interface Lexicon {
|
|
24
|
+
version: string;
|
|
25
|
+
lang: string;
|
|
26
|
+
entries: Record<string, LexiconEntry>;
|
|
27
|
+
fillers: string[];
|
|
28
|
+
hedges: string[];
|
|
29
|
+
vague: Record<string, string[]>;
|
|
30
|
+
degree: DegreeWords;
|
|
31
|
+
}
|
|
32
|
+
/** Multiplicative-ish adjustment applied to a hit's intensity. */
|
|
33
|
+
export declare const DEGREE_DELTA: Record<keyof DegreeWords, number>;
|
|
34
|
+
/** Words that flip polarity when they precede an emotion word. */
|
|
35
|
+
export declare const NEGATORS: string[];
|
|
36
|
+
/** Withdrawal phrases: no explicit emotion word, but the affect is negative and weak. */
|
|
37
|
+
export declare const ABSTAIN_TERMS: string[];
|
|
38
|
+
/**
|
|
39
|
+
* Validate an externally supplied lexicon. Returns human-readable problems;
|
|
40
|
+
* an empty array means the lexicon is usable.
|
|
41
|
+
*/
|
|
42
|
+
export declare function validateLexicon(value: unknown): string[];
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The analyzer port (ADR 0015).
|
|
3
|
+
*
|
|
4
|
+
* Rules are the default; a small model can be plugged in without touching
|
|
5
|
+
* core. Async analyzers MUST be cancellable by timeout at the call site and
|
|
6
|
+
* MUST be degradable to the lexicon analyzer.
|
|
7
|
+
*/
|
|
8
|
+
import type { AffectSignal } from '../types.ts';
|
|
9
|
+
export interface AffectAnalyzer {
|
|
10
|
+
readonly id: string;
|
|
11
|
+
analyze(text: string): AffectSignal | Promise<AffectSignal>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule registry (ADR 0016).
|
|
3
|
+
*
|
|
4
|
+
* The budget is part of the design: eight rules, no exceptions. New cases are
|
|
5
|
+
* not fixed by adding a ninth rule — that is the signal to move to the model
|
|
6
|
+
* adapter instead. Every rule listed here must have a test and an ablation
|
|
7
|
+
* number in spec/affect.v0.md.
|
|
8
|
+
*/
|
|
9
|
+
export interface RuleDescriptor {
|
|
10
|
+
readonly id: string;
|
|
11
|
+
readonly name: string;
|
|
12
|
+
readonly summary: string;
|
|
13
|
+
}
|
|
14
|
+
export declare const RULES: readonly RuleDescriptor[];
|
|
15
|
+
export declare const RULE_BUDGET = 8;
|
|
16
|
+
export declare const SEED_ENTRY_BUDGET = 120;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Seed lexicon, written for this project (MIT).
|
|
3
|
+
*
|
|
4
|
+
* Hard budget (ADR 0016): <= 120 emotion entries. Growing this list is not a
|
|
5
|
+
* fix for a missing case; if the budget is exhausted, the next step is the
|
|
6
|
+
* model adapter, not more words.
|
|
7
|
+
*
|
|
8
|
+
* Structure follows the DUTIR taxonomy (7 categories, intensity 1-9); no word
|
|
9
|
+
* list from that resource is copied here (ADR 0013).
|
|
10
|
+
*/
|
|
11
|
+
import type { Lexicon } from './lexicon.ts';
|
|
12
|
+
export declare const SEED_LEXICON: Lexicon;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context assembly: turn structured state into the text a host agent injects.
|
|
3
|
+
*
|
|
4
|
+
* Three rules:
|
|
5
|
+
* - never cut a sentence in half (drop whole memories instead)
|
|
6
|
+
* - every injected memory id is reported in `used`, so injection is auditable
|
|
7
|
+
* - the affect section is opt-in by confidence, capped in size, and only ever
|
|
8
|
+
* contains whitelisted sentences (ADR 0014)
|
|
9
|
+
*/
|
|
10
|
+
import type { ContextRequest, ContextResult, Emotion } from './types.ts';
|
|
11
|
+
import type { SoulStore } from './store.ts';
|
|
12
|
+
export declare const DEFAULT_MAX_CHARS = 1200;
|
|
13
|
+
/** Characters reserved for the affect section; if it does not fit, it is dropped whole. */
|
|
14
|
+
export declare const AFFECT_BUDGET = 160;
|
|
15
|
+
/** Only the most recent user turn counts; older samples are for evaluation, not injection. */
|
|
16
|
+
export declare const AFFECT_WINDOW_MS: number;
|
|
17
|
+
/** Emotion → concrete behavioural instruction. Values are -2..+2. */
|
|
18
|
+
export declare function styleDirectives(emotion: Emotion): string[];
|
|
19
|
+
export declare function buildContext(store: SoulStore, request: ContextRequest, now?: number): ContextResult;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* soulbridge core — the personality layer.
|
|
3
|
+
*
|
|
4
|
+
* Public surface is deliberately tiny: a facade (`Soul`) over four verbs.
|
|
5
|
+
* Anything a host needs beyond this belongs in an adapter.
|
|
6
|
+
*/
|
|
7
|
+
export * from './types.ts';
|
|
8
|
+
export type { SoulStore } from './store.ts';
|
|
9
|
+
export { openSqlite, SCHEMA_VERSION, AFFECT_RETENTION_MS, DbNewerThanBinaryError } from './sqlite.ts';
|
|
10
|
+
export { buildContext, styleDirectives, DEFAULT_MAX_CHARS, AFFECT_BUDGET, AFFECT_WINDOW_MS, } from './context.ts';
|
|
11
|
+
export { reflect, ruleExtract, nextEmotion } from './reflect.ts';
|
|
12
|
+
export { rank, scoreMemory, tokenize, relevance } from './scoring.ts';
|
|
13
|
+
export * from './affect/index.ts';
|
|
14
|
+
export { measureStyle } from './style/measure.ts';
|
|
15
|
+
export type { StyleMetrics } from './style/measure.ts';
|
|
16
|
+
export * from './storage/config.ts';
|
|
17
|
+
export * from './storage/merge.ts';
|
|
18
|
+
export * from './storage/steering.ts';
|
|
19
|
+
export * from './storage/transcript.ts';
|
|
20
|
+
import type { AffectSignal, ContextRequest, ContextResult, Emotion, LlmFn, MemoryType, ObserveResult, ReflectResult, ScoredMemory, Turn, Visibility } from './types.ts';
|
|
21
|
+
import type { SoulStore } from './store.ts';
|
|
22
|
+
import type { AffectAnalyzer } from './affect/port.ts';
|
|
23
|
+
export interface SoulOptions {
|
|
24
|
+
store: SoulStore;
|
|
25
|
+
soulId: string;
|
|
26
|
+
userId: string;
|
|
27
|
+
name?: string;
|
|
28
|
+
/** When the soul was born. Defaults to now; backdate it to demo long relationships. */
|
|
29
|
+
createdAt?: number;
|
|
30
|
+
/** Device identity for sync bookkeeping (ADR 0021). Optional in local use. */
|
|
31
|
+
deviceId?: string;
|
|
32
|
+
/** Defaults to the lexicon analyzer; pass an LLM-backed one to opt in (ADR 0015). */
|
|
33
|
+
analyzer?: AffectAnalyzer;
|
|
34
|
+
}
|
|
35
|
+
export declare class Soul {
|
|
36
|
+
readonly store: SoulStore;
|
|
37
|
+
readonly soulId: string;
|
|
38
|
+
readonly userId: string;
|
|
39
|
+
readonly name: string;
|
|
40
|
+
readonly deviceId: string;
|
|
41
|
+
readonly analyzer: AffectAnalyzer;
|
|
42
|
+
constructor(options: SoulOptions);
|
|
43
|
+
/** Verb 1: session start. */
|
|
44
|
+
context(request?: Partial<ContextRequest>): ContextResult;
|
|
45
|
+
/** Verb 3: on demand. */
|
|
46
|
+
recall(query: string, limit?: number): ScoredMemory[];
|
|
47
|
+
/** Pure analysis, no storage. Async because the analyzer may call a model. */
|
|
48
|
+
analyze(text: string): Promise<AffectSignal>;
|
|
49
|
+
/**
|
|
50
|
+
* Verb 2: after each turn.
|
|
51
|
+
*
|
|
52
|
+
* The analyzer may be async (a model); any failure degrades to the lexicon
|
|
53
|
+
* analyzer. Emotion is computed from the turn only — never from affect.
|
|
54
|
+
*/
|
|
55
|
+
observe(turn: Turn): Promise<ObserveResult>;
|
|
56
|
+
/**
|
|
57
|
+
* Explicit form of `observe`: the host decided this is worth keeping.
|
|
58
|
+
* Same write path, no extraction heuristics.
|
|
59
|
+
*/
|
|
60
|
+
remember(content: string, options?: {
|
|
61
|
+
type?: MemoryType;
|
|
62
|
+
importance?: number;
|
|
63
|
+
visibility?: Visibility;
|
|
64
|
+
}): string;
|
|
65
|
+
/** Verb 4: session end. */
|
|
66
|
+
reflect(turns: Turn[], llm?: LlmFn): Promise<ReflectResult>;
|
|
67
|
+
stats(): {
|
|
68
|
+
memories: number;
|
|
69
|
+
emotion: Emotion;
|
|
70
|
+
name: string;
|
|
71
|
+
};
|
|
72
|
+
private analyzeTurn;
|
|
73
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Emotion, LlmFn, MemoryType, ReflectResult, Turn } from './types.ts';
|
|
2
|
+
import type { SoulStore } from './store.ts';
|
|
3
|
+
interface Draft {
|
|
4
|
+
type: MemoryType;
|
|
5
|
+
content: string;
|
|
6
|
+
importance: number;
|
|
7
|
+
}
|
|
8
|
+
export declare function ruleExtract(turns: Turn[]): Draft[];
|
|
9
|
+
/** Corrections raise tension and lower passion; an ordinary session lets tension decay. */
|
|
10
|
+
export declare function nextEmotion(current: Emotion, turns: Turn[]): Emotion;
|
|
11
|
+
export interface ReflectOptions {
|
|
12
|
+
soulId: string;
|
|
13
|
+
userId: string;
|
|
14
|
+
turns: Turn[];
|
|
15
|
+
now?: number;
|
|
16
|
+
/** Reserved for richer extraction; unused in v0. */
|
|
17
|
+
llm?: LlmFn;
|
|
18
|
+
}
|
|
19
|
+
export declare function reflect(store: SoulStore, options: ReflectOptions): Promise<ReflectResult>;
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retrieval scoring. Pure functions, no I/O.
|
|
3
|
+
*
|
|
4
|
+
* score = 0.35*relevance + 0.25*importance + 0.20*recency + 0.15*vividness + 0.05*recall
|
|
5
|
+
*
|
|
6
|
+
* The weights are a claim, not a truth: changing them changes behaviour, so they
|
|
7
|
+
* live here (one place) and are covered by tests.
|
|
8
|
+
*/
|
|
9
|
+
import type { Memory } from './types.ts';
|
|
10
|
+
/** A memory's relevance halves every 30 days without being recalled. */
|
|
11
|
+
export declare const RECENCY_HALF_LIFE_DAYS = 30;
|
|
12
|
+
export declare const WEIGHTS: {
|
|
13
|
+
readonly relevance: 0.35;
|
|
14
|
+
readonly importance: 0.25;
|
|
15
|
+
readonly recency: 0.2;
|
|
16
|
+
readonly vividness: 0.15;
|
|
17
|
+
readonly recall: 0.05;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* CJK text has no spaces, so single characters are too coarse and the whole
|
|
21
|
+
* sentence is too fine: use character bigrams. Latin runs become words.
|
|
22
|
+
*/
|
|
23
|
+
export declare function tokenize(input: string): string[];
|
|
24
|
+
export declare function relevance(queryTokens: string[], memory: Memory): number;
|
|
25
|
+
export declare function recency(reference: number, now: number): number;
|
|
26
|
+
export declare function scoreMemory(memory: Memory, queryTokens: string[], now: number): number;
|
|
27
|
+
export declare function rank(memories: Memory[], query: string, now: number): Array<{
|
|
28
|
+
memory: Memory;
|
|
29
|
+
score: number;
|
|
30
|
+
}>;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { SoulStore } from './store.ts';
|
|
2
|
+
export declare const SCHEMA_VERSION = 2;
|
|
3
|
+
export declare const AFFECT_RETENTION_MS: number;
|
|
4
|
+
export declare class DbNewerThanBinaryError extends Error {
|
|
5
|
+
readonly code = "DB_NEWER_THAN_BINARY";
|
|
6
|
+
readonly dbVersion: number;
|
|
7
|
+
constructor(dbVersion: number);
|
|
8
|
+
}
|
|
9
|
+
export declare function openSqlite(file: string): SoulStore;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface SoulConfig {
|
|
2
|
+
soulId: string;
|
|
3
|
+
userId: string;
|
|
4
|
+
name: string;
|
|
5
|
+
deviceId: string;
|
|
6
|
+
}
|
|
7
|
+
export declare const DEFAULT_SOUL_ID = "xiaoling";
|
|
8
|
+
export declare const DEFAULT_USER_ID = "local";
|
|
9
|
+
export declare const DEFAULT_NAME = "\u5C0F\u7075";
|
|
10
|
+
export declare function soulbridgeHome(): string;
|
|
11
|
+
export declare function configPath(): string;
|
|
12
|
+
export declare function defaultDbPath(): string;
|
|
13
|
+
export declare function newDeviceId(): string;
|
|
14
|
+
/** DB location: explicit flag > env > home. Never the cwd. */
|
|
15
|
+
export declare function resolveDbPath(explicit?: string): string;
|
|
16
|
+
export declare function loadConfig(): SoulConfig | null;
|
|
17
|
+
export declare function saveConfig(config: SoulConfig): void;
|
|
18
|
+
/**
|
|
19
|
+
* Read the config, creating or completing it when needed.
|
|
20
|
+
* Env overrides win, but are not persisted (an override is per-process).
|
|
21
|
+
*/
|
|
22
|
+
export declare function ensureConfig(overrides?: Partial<SoulConfig>): SoulConfig;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Merge semantics for cross-device sync (ADR 0021).
|
|
3
|
+
*
|
|
4
|
+
* Pure functions, no I/O: the transport (folder sync, git, manual export) is
|
|
5
|
+
* someone else's problem; this file only decides who wins.
|
|
6
|
+
*
|
|
7
|
+
* Rule: higher `updatedAt` wins; on a tie the lexicographically larger
|
|
8
|
+
* `device` wins, so merging is deterministic regardless of arrival order.
|
|
9
|
+
*/
|
|
10
|
+
export interface SyncRecord {
|
|
11
|
+
id: string;
|
|
12
|
+
device: string;
|
|
13
|
+
at: number;
|
|
14
|
+
updatedAt: number;
|
|
15
|
+
deleted?: boolean;
|
|
16
|
+
}
|
|
17
|
+
export declare function pickLatest<T extends SyncRecord>(a: T, b: T): T;
|
|
18
|
+
/** Union by id, keeping tombstones (deletion must survive a merge). */
|
|
19
|
+
export declare function mergeRecords<T extends SyncRecord>(records: Iterable<T>): T[];
|
|
20
|
+
/** A record is visible only if the winning version is not a tombstone. */
|
|
21
|
+
export declare function isVisible(record: SyncRecord): boolean;
|
|
22
|
+
export declare function liveRecords<T extends SyncRecord>(records: Iterable<T>): T[];
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Steering-file import (ADR 0022).
|
|
3
|
+
*
|
|
4
|
+
* AGENTS.md / CLAUDE.md are good at *shared, reviewed* conventions. We do not
|
|
5
|
+
* replace them; we read them once and turn the stable statements into
|
|
6
|
+
* memories that can travel across tools.
|
|
7
|
+
*
|
|
8
|
+
* Parsing is deliberately conservative: bullets and short standalone lines
|
|
9
|
+
* only. Code fences, tables, badges and links are skipped — importing a
|
|
10
|
+
* hundred lines of YAML is worse than importing nothing.
|
|
11
|
+
*/
|
|
12
|
+
import type { MemoryType } from '../types.ts';
|
|
13
|
+
export interface SteeringDraft {
|
|
14
|
+
text: string;
|
|
15
|
+
type: MemoryType;
|
|
16
|
+
importance: number;
|
|
17
|
+
line: number;
|
|
18
|
+
}
|
|
19
|
+
export declare const STEERING_MAX_DRAFTS = 200;
|
|
20
|
+
/** Lowercase, strip punctuation and whitespace — for duplicate detection only. */
|
|
21
|
+
export declare function normalizeForDedupe(text: string): string;
|
|
22
|
+
/**
|
|
23
|
+
* Extract memory candidates from a steering file.
|
|
24
|
+
* Headings are dropped on purpose: they carry structure, not statements.
|
|
25
|
+
*/
|
|
26
|
+
export declare function parseSteering(text: string): SteeringDraft[];
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Portable transcript ingestion (ADR 0023).
|
|
3
|
+
*
|
|
4
|
+
* Host session stores are private and change shape without notice (the DSH
|
|
5
|
+
* store is a multi-frame zstd container whose later frames are not decodable
|
|
6
|
+
* with a standard decompressor). Rather than reverse-engineering it, the
|
|
7
|
+
* adapter contract is a plain JSONL file that any host can export.
|
|
8
|
+
*
|
|
9
|
+
* Accepted line shapes (first match wins):
|
|
10
|
+
* {"role":"user","content":"…"}
|
|
11
|
+
* {"type":"user","text":"…"}
|
|
12
|
+
* {"message":{"role":"user","content":"…"}}
|
|
13
|
+
* {"role":"user","content":[{"type":"text","text":"…"}]}
|
|
14
|
+
*
|
|
15
|
+
* Everything else (headers, tool calls, tool results, reasoning) is skipped.
|
|
16
|
+
*/
|
|
17
|
+
import type { Turn } from '../types.ts';
|
|
18
|
+
export interface TranscriptParseResult {
|
|
19
|
+
turns: Turn[];
|
|
20
|
+
skipped: number;
|
|
21
|
+
}
|
|
22
|
+
export declare function parseTranscriptJsonl(text: string): TranscriptParseResult;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The only port. Every added method is a storage engine's opinion leaking into
|
|
3
|
+
* the domain, so the surface stays deliberately small.
|
|
4
|
+
*/
|
|
5
|
+
import type { AffectSignal, Emotion, Identity, Memory, QuerySpec } from './types.ts';
|
|
6
|
+
export interface SoulStore {
|
|
7
|
+
ensureSoul(identity: Identity): void;
|
|
8
|
+
identity(soulId: string): Identity | null;
|
|
9
|
+
emotion(soulId: string, userId: string): Emotion;
|
|
10
|
+
saveEmotion(soulId: string, userId: string, emotion: Emotion, now?: number): void;
|
|
11
|
+
put(memory: Memory): void;
|
|
12
|
+
/** Active, non-deleted memories visible to `userId` (own private + shared). */
|
|
13
|
+
query(spec: QuerySpec): Memory[];
|
|
14
|
+
/** Recall bookkeeping: recallCount++, lastRecalledAt = now. */
|
|
15
|
+
touch(ids: string[], now?: number): void;
|
|
16
|
+
/** Persist one affect sample (payload only; no raw text). */
|
|
17
|
+
saveAffect(soulId: string, userId: string, signal: AffectSignal, at: number, deviceId?: string): void;
|
|
18
|
+
/** Samples newer than `sinceMs`, newest first. */
|
|
19
|
+
recentAffect(soulId: string, userId: string, sinceMs: number): AffectSignal[];
|
|
20
|
+
/** Drop samples older than `beforeMs`; returns rows removed. */
|
|
21
|
+
purgeAffect(beforeMs: number): number;
|
|
22
|
+
count(soulId: string, userId: string): number;
|
|
23
|
+
close(): void;
|
|
24
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Lexicon } from '../affect/lexicon.ts';
|
|
2
|
+
export interface StyleMetrics {
|
|
3
|
+
/** Characters in the raw reply. */
|
|
4
|
+
length: number;
|
|
5
|
+
/** Hedge words per 100 characters. */
|
|
6
|
+
hedgeRate: number;
|
|
7
|
+
/** Question marks (full-width normalised). */
|
|
8
|
+
questions: number;
|
|
9
|
+
/** Whether the reply opens by restating or confirming the request. */
|
|
10
|
+
confirmFirst: boolean;
|
|
11
|
+
}
|
|
12
|
+
export declare function measureStyle(text: string, lexicon?: Lexicon): StyleMetrics;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Domain model. Pure types + constants, no logic, no dependencies.
|
|
3
|
+
*
|
|
4
|
+
* Rule: if a field only makes sense for one storage engine, it does not belong here.
|
|
5
|
+
* (No `vector`, no `embeddingId`, no `rowid`.)
|
|
6
|
+
*/
|
|
7
|
+
export type MemoryType = 'semantic' | 'episodic' | 'strategy' | 'cognition' | 'relationship' | 'trait' | 'fact';
|
|
8
|
+
export type Visibility = 'private' | 'shared';
|
|
9
|
+
export type MemoryStatus = 'active' | 'superseded' | 'archived';
|
|
10
|
+
export interface Memory {
|
|
11
|
+
id: string;
|
|
12
|
+
soulId: string;
|
|
13
|
+
userId: string;
|
|
14
|
+
type: MemoryType;
|
|
15
|
+
content: string;
|
|
16
|
+
/** 1..10. How much this is worth remembering at all. */
|
|
17
|
+
importance: number;
|
|
18
|
+
/** 0..1. Decays over time; a vivid memory surfaces more easily. */
|
|
19
|
+
vividness: number;
|
|
20
|
+
recallCount: number;
|
|
21
|
+
status: MemoryStatus;
|
|
22
|
+
visibility: Visibility;
|
|
23
|
+
keywords: string[];
|
|
24
|
+
createdAt: number;
|
|
25
|
+
lastRecalledAt?: number;
|
|
26
|
+
validFrom?: number;
|
|
27
|
+
validUntil?: number;
|
|
28
|
+
supersededBy?: string;
|
|
29
|
+
/** Last-writer-wins clock for cross-device merge (ADR 0021). */
|
|
30
|
+
updatedAt?: number;
|
|
31
|
+
/** Tombstone; deletion is never physical on the first pass. */
|
|
32
|
+
deletedAt?: number;
|
|
33
|
+
}
|
|
34
|
+
export interface Identity {
|
|
35
|
+
soulId: string;
|
|
36
|
+
name: string;
|
|
37
|
+
creator?: string;
|
|
38
|
+
createdAt: number;
|
|
39
|
+
}
|
|
40
|
+
/** Six dimensions, each -2..+2. Baseline is slightly positive. */
|
|
41
|
+
export interface Emotion {
|
|
42
|
+
passion: number;
|
|
43
|
+
bond: number;
|
|
44
|
+
tension: number;
|
|
45
|
+
selfRegard: number;
|
|
46
|
+
curiosity: number;
|
|
47
|
+
contentment: number;
|
|
48
|
+
}
|
|
49
|
+
export declare const DEFAULT_EMOTION: Emotion;
|
|
50
|
+
export interface Turn {
|
|
51
|
+
role: 'user' | 'assistant';
|
|
52
|
+
content: string;
|
|
53
|
+
at?: number;
|
|
54
|
+
}
|
|
55
|
+
export interface ContextRequest {
|
|
56
|
+
soulId: string;
|
|
57
|
+
userId: string;
|
|
58
|
+
query?: string;
|
|
59
|
+
maxChars?: number;
|
|
60
|
+
}
|
|
61
|
+
export interface ContextResult {
|
|
62
|
+
text: string;
|
|
63
|
+
used: string[];
|
|
64
|
+
dropped: number;
|
|
65
|
+
chars: number;
|
|
66
|
+
/** Present only when the affect section was actually injected. */
|
|
67
|
+
affect?: {
|
|
68
|
+
confidence: number;
|
|
69
|
+
directives: string[];
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
export interface RecallRequest {
|
|
73
|
+
soulId: string;
|
|
74
|
+
userId: string;
|
|
75
|
+
q: string;
|
|
76
|
+
limit?: number;
|
|
77
|
+
}
|
|
78
|
+
export interface ScoredMemory {
|
|
79
|
+
memory: Memory;
|
|
80
|
+
score: number;
|
|
81
|
+
}
|
|
82
|
+
export interface QuerySpec {
|
|
83
|
+
soulId: string;
|
|
84
|
+
userId: string;
|
|
85
|
+
status?: MemoryStatus[];
|
|
86
|
+
types?: MemoryType[];
|
|
87
|
+
limit?: number;
|
|
88
|
+
}
|
|
89
|
+
export interface ReflectResult {
|
|
90
|
+
memories: number;
|
|
91
|
+
superseded: number;
|
|
92
|
+
emotion: Emotion;
|
|
93
|
+
}
|
|
94
|
+
export interface ObserveResult {
|
|
95
|
+
written: number;
|
|
96
|
+
emotion: Emotion;
|
|
97
|
+
/** Analysis of the user turn. Empty signal for assistant turns or on failure. */
|
|
98
|
+
affect: AffectSignal;
|
|
99
|
+
}
|
|
100
|
+
/** Injected by the caller. `core` never imports a model SDK. */
|
|
101
|
+
export type LlmFn = (prompt: string) => Promise<string> | string;
|
|
102
|
+
export type AffectCategory = 'joy' | 'anger' | 'sadness' | 'fear' | 'disgust' | 'surprise';
|
|
103
|
+
export type AffectPolarity = 'positive' | 'negative' | 'neutral';
|
|
104
|
+
export interface AffectHit {
|
|
105
|
+
term: string;
|
|
106
|
+
category: AffectCategory;
|
|
107
|
+
/** 1..9, already adjusted by degree modifiers. */
|
|
108
|
+
intensity: number;
|
|
109
|
+
polarity: AffectPolarity;
|
|
110
|
+
negated: boolean;
|
|
111
|
+
position: number;
|
|
112
|
+
}
|
|
113
|
+
export interface AffectFeatures {
|
|
114
|
+
fillers: number;
|
|
115
|
+
hedges: number;
|
|
116
|
+
vague: number;
|
|
117
|
+
tokens: number;
|
|
118
|
+
/** meaningful tokens / all tokens, 0..1 */
|
|
119
|
+
density: number;
|
|
120
|
+
}
|
|
121
|
+
export interface AffectSignal {
|
|
122
|
+
/** Which analyzer produced this: 'lexicon' | 'llm' | 'none'. */
|
|
123
|
+
analyzer: string;
|
|
124
|
+
hits: AffectHit[];
|
|
125
|
+
dominant: AffectCategory | null;
|
|
126
|
+
intensity: number;
|
|
127
|
+
valence: number;
|
|
128
|
+
arousal: number;
|
|
129
|
+
features: AffectFeatures;
|
|
130
|
+
confidence: number;
|
|
131
|
+
/** True when the text looks like withdrawal ("行吧", "随便") but no explicit emotion word. */
|
|
132
|
+
abstain: boolean;
|
|
133
|
+
}
|
|
134
|
+
export declare const EMPTY_AFFECT: AffectSignal;
|
|
135
|
+
/** Wire version of the four verbs. Additive changes bump MINOR (ADR 0005). */
|
|
136
|
+
export declare const PROTOCOL_VERSION = "0.2";
|
package/package.json
CHANGED
|
@@ -1,50 +1,59 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
"
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
"
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
"
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
"
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
"
|
|
49
|
-
|
|
2
|
+
"name": "soulbridge",
|
|
3
|
+
"version": "0.0.6",
|
|
4
|
+
"description": "A personality layer for any agent: continuity, forgetting, and growth across tools.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/packages/core/src/index.js",
|
|
8
|
+
"types": "./dist/packages/core/src/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/packages/core/src/index.d.ts",
|
|
12
|
+
"import": "./dist/packages/core/src/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./package.json": "./package.json"
|
|
15
|
+
},
|
|
16
|
+
"bin": {
|
|
17
|
+
"soulbridge": "dist/packages/cli/src/cli.js"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"adapters",
|
|
22
|
+
"README.md",
|
|
23
|
+
"LICENSE",
|
|
24
|
+
"NOTICE"
|
|
25
|
+
],
|
|
26
|
+
"workspaces": [
|
|
27
|
+
"packages/*"
|
|
28
|
+
],
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": "\u003e=22.18"
|
|
31
|
+
},
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/Jason0826/soulbridge.git"
|
|
35
|
+
},
|
|
36
|
+
"homepage": "https://github.com/Jason0826/soulbridge#readme",
|
|
37
|
+
"bugs": {
|
|
38
|
+
"url": "https://github.com/Jason0826/soulbridge/issues"
|
|
39
|
+
},
|
|
40
|
+
"keywords": [
|
|
41
|
+
"ai-agent",
|
|
42
|
+
"memory",
|
|
43
|
+
"personality",
|
|
44
|
+
"continuity",
|
|
45
|
+
"mcp",
|
|
46
|
+
"claude-code"
|
|
47
|
+
],
|
|
48
|
+
"scripts": {
|
|
49
|
+
"build": "tsc -p tsconfig.build.json",
|
|
50
|
+
"test": "node --test \"packages/core/test/*.test.ts\"",
|
|
51
|
+
"demo": "node packages/cli/src/cli.ts demo",
|
|
52
|
+
"typecheck": "tsc --noEmit",
|
|
53
|
+
"prepublishOnly": "npm run build \u0026\u0026 npm test"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@types/node": "^26.6.5",
|
|
57
|
+
"typescript": "^7.0.2"
|
|
58
|
+
}
|
|
50
59
|
}
|