soulbridge 0.0.5 → 0.0.7
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 +2 -1
- package/adapters/README.md +47 -0
- package/dist/packages/cli/src/cli.d.ts +2 -0
- package/dist/packages/cli/src/cli.js +47 -2
- 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/context.js +10 -2
- package/dist/packages/core/src/index.d.ts +73 -0
- package/dist/packages/core/src/index.js +4 -0
- package/dist/packages/core/src/reflect.d.ts +29 -0
- package/dist/packages/core/src/reflect.js +33 -6
- 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 +148 -0
- package/package.json +10 -1
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
> **给任何 agent 装上一个人格层。**
|
|
4
4
|
> A personality layer for any agent — continuity, forgetting, and growth across tools.
|
|
5
5
|
|
|
6
|
-
**状态:v0.0.
|
|
6
|
+
**状态:v0.0.6 · 已发布 npm · 接口仍在演进**
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
@@ -111,6 +111,7 @@ SQLite(默认) / Postgres(可选)
|
|
|
111
111
|
| [storage.v0](docs/spec/storage.v0.md) | 路径、身份、迁移、并发、导出格式 |
|
|
112
112
|
| [sync.v0](docs/spec/sync.v0.md) | 跨设备合并语义(v0.5 实现) |
|
|
113
113
|
| [transcript.v1](docs/spec/transcript.v1.md) | 可移植会话转写格式(ingest 的输入) |
|
|
114
|
+
| [适配器索引](adapters/README.md) | 各宿主适配器与接入契约 |
|
|
114
115
|
| [quality](docs/quality.md) | 质量模型、九条门禁、五条评审清单 |
|
|
115
116
|
| [references](docs/references.md) | 外部资料的去留与过期判断 |
|
|
116
117
|
| [runbook](docs/runbook.md) | 发版流程、维护操作、已知待办 |
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# 宿主适配器
|
|
2
|
+
|
|
3
|
+
soulbridge 的核心不知道任何 agent 宿主的存在([ADR 0024](../adr/0024-adapters.md))。
|
|
4
|
+
把四个动词接到具体宿主上的代码,都在这里列出的独立包里。
|
|
5
|
+
|
|
6
|
+
## 现有适配器
|
|
7
|
+
|
|
8
|
+
| 宿主 | 阶梯 | 形态 | 位置 |
|
|
9
|
+
|---|---|---|---|
|
|
10
|
+
| DSH | T0 | `SKILL.md`(模型自觉调用 CLI) | [dsh/SKILL.md](dsh/SKILL.md) |
|
|
11
|
+
| DSH | T2 | cordis 插件(系统提示注入 + 事件观察) | [dsh-plugin-soulbridge](https://github.com/Jason0826/dsh-plugin-soulbridge) |
|
|
12
|
+
|
|
13
|
+
> T1(会话转写摄取)与宿主无关:任何宿主导出 `transcript.v1.jsonl` 后,用
|
|
14
|
+
> `soulbridge ingest --file` 即可,见 [spec/transcript.v1.md](../spec/transcript.v1.md)。
|
|
15
|
+
|
|
16
|
+
## 适配器要做什么
|
|
17
|
+
|
|
18
|
+
| 时机 | 调用 | 失败时 |
|
|
19
|
+
|---|---|---|
|
|
20
|
+
| 提示组装 | `context({ track: false })` | 不注入 |
|
|
21
|
+
| 每条用户消息 | `observe(turn)` | 跳过 |
|
|
22
|
+
| 会话结束 | `reflect(turns)` | 丢弃 |
|
|
23
|
+
| 模型主动查询(可选) | `recall` / `remember` | 返回空 |
|
|
24
|
+
|
|
25
|
+
## 三条禁止
|
|
26
|
+
|
|
27
|
+
1. 不改变事实判断——affect 只影响措辞
|
|
28
|
+
2. 不编造记忆——没注入的就是没有
|
|
29
|
+
3. 不把个人记忆写进项目文件
|
|
30
|
+
|
|
31
|
+
## 自己写一个适配器
|
|
32
|
+
|
|
33
|
+
只需要 `soulbridge` 包暴露的:(TODO: 补 `soulbridge` 的 SDK 入口说明)
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
import { openSqlite, ensureConfig, resolveDbPath, Soul } from 'soulbridge';
|
|
37
|
+
|
|
38
|
+
const config = ensureConfig();
|
|
39
|
+
const soul = new Soul({
|
|
40
|
+
store: openSqlite(resolveDbPath()),
|
|
41
|
+
soulId: config.soulId,
|
|
42
|
+
userId: config.userId,
|
|
43
|
+
deviceId: config.deviceId,
|
|
44
|
+
});
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
然后按上表在宿主的对应时机调用四个方法即可。
|
|
@@ -24,7 +24,7 @@ process.on('warning', (warning) => {
|
|
|
24
24
|
return;
|
|
25
25
|
process.stderr.write(`${warning.name}: ${warning.message}\n`);
|
|
26
26
|
});
|
|
27
|
-
const { openSqlite, Soul, ensureConfig, resolveDbPath, analyzeAffect, validateLexicon, parseSteering, normalizeForDedupe, parseTranscriptJsonl, } = await import('../../core/src/index.js');
|
|
27
|
+
const { openSqlite, Soul, ensureConfig, resolveDbPath, analyzeAffect, validateLexicon, parseSteering, normalizeForDedupe, parseTranscriptJsonl, soulbridgeHome, } = await import('../../core/src/index.js');
|
|
28
28
|
const { llmAnalyzerFromEnv } = await import('./llm-analyzer.js');
|
|
29
29
|
function parse(argv) {
|
|
30
30
|
const [cmd = 'help', ...rest] = argv;
|
|
@@ -303,6 +303,50 @@ async function main() {
|
|
|
303
303
|
out({ ok: true, file: resolve(file), turns: turns.length, skipped, reflected, dryRun });
|
|
304
304
|
return;
|
|
305
305
|
}
|
|
306
|
+
case 'status': {
|
|
307
|
+
const config = ensureConfig();
|
|
308
|
+
const dbPath = resolveDbPath(flags.db);
|
|
309
|
+
const store = openSqlite(dbPath);
|
|
310
|
+
const memories = store.query({ soulId: config.soulId, userId: config.userId, limit: 100_000 });
|
|
311
|
+
const samples = store.recentAffect(config.soulId, config.userId, 0);
|
|
312
|
+
store.close();
|
|
313
|
+
const byType = new Map();
|
|
314
|
+
for (const memory of memories)
|
|
315
|
+
byType.set(memory.type, (byType.get(memory.type) ?? 0) + 1);
|
|
316
|
+
const typeText = [...byType].sort((a, b) => b[1] - a[1]).map(([t, n]) => `${t} ${n}`).join(' · ') || '—';
|
|
317
|
+
let plugin = null;
|
|
318
|
+
try {
|
|
319
|
+
plugin = JSON.parse(readFileSync(resolve(soulbridgeHome(), 'plugin-state.json'), 'utf8'));
|
|
320
|
+
}
|
|
321
|
+
catch {
|
|
322
|
+
plugin = null;
|
|
323
|
+
}
|
|
324
|
+
const ago = (value) => {
|
|
325
|
+
if (typeof value !== 'number' || value <= 0)
|
|
326
|
+
return '—';
|
|
327
|
+
const seconds = Math.max(0, Math.round((Date.now() - value) / 1000));
|
|
328
|
+
if (seconds < 60)
|
|
329
|
+
return `${seconds}s ago`;
|
|
330
|
+
if (seconds < 3600)
|
|
331
|
+
return `${Math.round(seconds / 60)}m ago`;
|
|
332
|
+
if (seconds < 86_400)
|
|
333
|
+
return `${Math.round(seconds / 3600)}h ago`;
|
|
334
|
+
return `${Math.round(seconds / 86_400)}d ago`;
|
|
335
|
+
};
|
|
336
|
+
const lines = [
|
|
337
|
+
`soul ${config.soulId} / ${config.userId}`,
|
|
338
|
+
`db ${dbPath}`,
|
|
339
|
+
`memories ${memories.length} (${typeText})`,
|
|
340
|
+
`affect ${samples.length} samples${samples.length > 0 ? ` · last ${ago(samples[0].at)}` : ''}`,
|
|
341
|
+
plugin
|
|
342
|
+
? `plugin v${plugin.version ?? '?'} · applied ${ago(plugin.appliedAt)} · inject ${plugin.injects ?? 0} · observe ${plugin.observes ?? 0} · fail ${plugin.observeFails ?? 0}`
|
|
343
|
+
: 'plugin no heartbeat (plugin never loaded, or runs on another machine)',
|
|
344
|
+
];
|
|
345
|
+
if (plugin?.lastError)
|
|
346
|
+
lines.push(`last error ${String(plugin.lastError)}`);
|
|
347
|
+
process.stdout.write(`${lines.join('\n')}\n`);
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
306
350
|
case 'stats': {
|
|
307
351
|
const { soul, dbPath } = open(flags);
|
|
308
352
|
out({ db: dbPath, ...soul.stats() });
|
|
@@ -327,7 +371,8 @@ async function main() {
|
|
|
327
371
|
' import --in <file> 导入 JSONL',
|
|
328
372
|
' import --from <steering.md> 导入 AGENTS.md / CLAUDE.md',
|
|
329
373
|
' ingest --file <transcript.jsonl> 会话转写 → 记忆与情绪采样',
|
|
330
|
-
'
|
|
374
|
+
' status 一眼看它是否在工作',
|
|
375
|
+
' stats 看原始状态(JSON)',
|
|
331
376
|
' demo 端到端演示(无需模型)',
|
|
332
377
|
'',
|
|
333
378
|
'env: SOULBRIDGE_DB, SOULBRIDGE_SOUL, SOULBRIDGE_USER, SOULBRIDGE_NAME,',
|
|
@@ -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;
|
|
@@ -53,13 +53,21 @@ export function buildContext(store, request, now = Date.now()) {
|
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
|
-
const candidates = store
|
|
56
|
+
const candidates = store
|
|
57
|
+
.query({ soulId: request.soulId, userId: request.userId })
|
|
58
|
+
.filter((memory) => (request.types === undefined || request.types.includes(memory.type)) &&
|
|
59
|
+
(request.minImportance === undefined || memory.importance >= request.minImportance));
|
|
57
60
|
const ranked = rank(candidates, request.query ?? '', now);
|
|
61
|
+
const maxItems = request.maxItems ?? Number.POSITIVE_INFINITY;
|
|
58
62
|
let chars = sections.join('\n\n').length;
|
|
59
63
|
let dropped = 0;
|
|
60
64
|
const lines = [];
|
|
61
65
|
const used = [];
|
|
62
66
|
for (const { memory } of ranked) {
|
|
67
|
+
if (lines.length >= maxItems) {
|
|
68
|
+
dropped += 1;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
63
71
|
const line = `- [${memory.type}] ${memory.content}`;
|
|
64
72
|
const cost = line.length + 1 + 10; // line + newline + section overhead share
|
|
65
73
|
if (chars + cost > maxChars) {
|
|
@@ -73,7 +81,7 @@ export function buildContext(store, request, now = Date.now()) {
|
|
|
73
81
|
if (lines.length > 0)
|
|
74
82
|
sections.push(`<memories>\n${lines.join('\n')}\n</memories>`);
|
|
75
83
|
const text = sections.join('\n\n');
|
|
76
|
-
if (used.length > 0)
|
|
84
|
+
if (request.track !== false && used.length > 0)
|
|
77
85
|
store.touch(used, now);
|
|
78
86
|
const result = { text, used, dropped, chars: text.length };
|
|
79
87
|
if (affectMeta)
|
|
@@ -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
|
+
}
|
|
@@ -48,6 +48,10 @@ export class Soul {
|
|
|
48
48
|
userId: this.userId,
|
|
49
49
|
query: request.query,
|
|
50
50
|
maxChars: request.maxChars,
|
|
51
|
+
track: request.track,
|
|
52
|
+
types: request.types,
|
|
53
|
+
minImportance: request.minImportance,
|
|
54
|
+
maxItems: request.maxItems,
|
|
51
55
|
});
|
|
52
56
|
}
|
|
53
57
|
/** Verb 3: on demand. */
|
|
@@ -0,0 +1,29 @@
|
|
|
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
|
+
interface Rule {
|
|
9
|
+
/** Stable id for tests and diagnostics. */
|
|
10
|
+
id: string;
|
|
11
|
+
type: MemoryType;
|
|
12
|
+
importance: number;
|
|
13
|
+
match: (text: string) => boolean;
|
|
14
|
+
}
|
|
15
|
+
/** Cheap, synchronous, no model. Deliberately conservative: false positives cost trust. */
|
|
16
|
+
export declare const RULES: Rule[];
|
|
17
|
+
export declare function ruleExtract(turns: Turn[]): Draft[];
|
|
18
|
+
/** Corrections raise tension and lower passion; an ordinary session lets tension decay. */
|
|
19
|
+
export declare function nextEmotion(current: Emotion, turns: Turn[]): Emotion;
|
|
20
|
+
export interface ReflectOptions {
|
|
21
|
+
soulId: string;
|
|
22
|
+
userId: string;
|
|
23
|
+
turns: Turn[];
|
|
24
|
+
now?: number;
|
|
25
|
+
/** Reserved for richer extraction; unused in v0. */
|
|
26
|
+
llm?: LlmFn;
|
|
27
|
+
}
|
|
28
|
+
export declare function reflect(store: SoulStore, options: ReflectOptions): Promise<ReflectResult>;
|
|
29
|
+
export {};
|
|
@@ -7,12 +7,39 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { randomUUID } from 'node:crypto';
|
|
9
9
|
import { tokenize } from './scoring.js';
|
|
10
|
+
/**
|
|
11
|
+
* A standing instruction needs both halves: when it applies, and what it asks
|
|
12
|
+
* for. A bare "每次" is usually a complaint ("每次都不能用"), so the trigger
|
|
13
|
+
* alone must not create a strategy.
|
|
14
|
+
*/
|
|
15
|
+
const TIME_TRIGGER = /(以后|下次|每次|从现在起|在.{0,12}之前|from now on|always)/i;
|
|
16
|
+
const DIRECTIVE = /(先|需要|要|必须|应该|请|别|不要|务必|记得|need to|should|must|please)/i;
|
|
10
17
|
/** Cheap, synchronous, no model. Deliberately conservative: false positives cost trust. */
|
|
11
|
-
const RULES = [
|
|
12
|
-
{
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
18
|
+
export const RULES = [
|
|
19
|
+
{
|
|
20
|
+
id: 'remember',
|
|
21
|
+
type: 'fact',
|
|
22
|
+
importance: 7,
|
|
23
|
+
match: (text) => /(记住|别忘了|remember|don'?t forget)/i.test(text),
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
id: 'preference',
|
|
27
|
+
type: 'trait',
|
|
28
|
+
importance: 6,
|
|
29
|
+
match: (text) => /(我喜欢|我习惯|我偏好|我倾向|i prefer|i like)/i.test(text),
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: 'standing-instruction',
|
|
33
|
+
type: 'strategy',
|
|
34
|
+
importance: 7,
|
|
35
|
+
match: (text) => TIME_TRIGGER.test(text) && DIRECTIVE.test(text),
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: 'decision',
|
|
39
|
+
type: 'cognition',
|
|
40
|
+
importance: 7,
|
|
41
|
+
match: (text) => /(我决定|就这么定|原则是|i decided|my rule)/i.test(text),
|
|
42
|
+
},
|
|
16
43
|
];
|
|
17
44
|
const CORRECTION = /(不对|不是这样|错了|你搞错|wrong|that'?s not right|incorrect)/i;
|
|
18
45
|
const clamp = (n, lo, hi) => Math.min(hi, Math.max(lo, n));
|
|
@@ -23,7 +50,7 @@ export function ruleExtract(turns) {
|
|
|
23
50
|
if (turn.role !== 'user')
|
|
24
51
|
continue;
|
|
25
52
|
for (const rule of RULES) {
|
|
26
|
-
if (rule.
|
|
53
|
+
if (rule.match(turn.content)) {
|
|
27
54
|
drafts.push({ type: rule.type, content: turn.content.trim(), importance: rule.importance });
|
|
28
55
|
break;
|
|
29
56
|
}
|
|
@@ -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,148 @@
|
|
|
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
|
+
* Whether this call counts as a recall (increments recallCount / lastRecalledAt).
|
|
62
|
+
* Host adapters assemble prompts repeatedly per turn; pass false to preview
|
|
63
|
+
* context without inflating recall statistics. Defaults to true.
|
|
64
|
+
*/
|
|
65
|
+
track?: boolean;
|
|
66
|
+
/** Only these memory types may be injected. Defaults to every type. */
|
|
67
|
+
types?: MemoryType[];
|
|
68
|
+
/** Skip memories below this importance (1..10). */
|
|
69
|
+
minImportance?: number;
|
|
70
|
+
/** Cap the number of injected memory lines. */
|
|
71
|
+
maxItems?: number;
|
|
72
|
+
}
|
|
73
|
+
export interface ContextResult {
|
|
74
|
+
text: string;
|
|
75
|
+
used: string[];
|
|
76
|
+
dropped: number;
|
|
77
|
+
chars: number;
|
|
78
|
+
/** Present only when the affect section was actually injected. */
|
|
79
|
+
affect?: {
|
|
80
|
+
confidence: number;
|
|
81
|
+
directives: string[];
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
export interface RecallRequest {
|
|
85
|
+
soulId: string;
|
|
86
|
+
userId: string;
|
|
87
|
+
q: string;
|
|
88
|
+
limit?: number;
|
|
89
|
+
}
|
|
90
|
+
export interface ScoredMemory {
|
|
91
|
+
memory: Memory;
|
|
92
|
+
score: number;
|
|
93
|
+
}
|
|
94
|
+
export interface QuerySpec {
|
|
95
|
+
soulId: string;
|
|
96
|
+
userId: string;
|
|
97
|
+
status?: MemoryStatus[];
|
|
98
|
+
types?: MemoryType[];
|
|
99
|
+
limit?: number;
|
|
100
|
+
}
|
|
101
|
+
export interface ReflectResult {
|
|
102
|
+
memories: number;
|
|
103
|
+
superseded: number;
|
|
104
|
+
emotion: Emotion;
|
|
105
|
+
}
|
|
106
|
+
export interface ObserveResult {
|
|
107
|
+
written: number;
|
|
108
|
+
emotion: Emotion;
|
|
109
|
+
/** Analysis of the user turn. Empty signal for assistant turns or on failure. */
|
|
110
|
+
affect: AffectSignal;
|
|
111
|
+
}
|
|
112
|
+
/** Injected by the caller. `core` never imports a model SDK. */
|
|
113
|
+
export type LlmFn = (prompt: string) => Promise<string> | string;
|
|
114
|
+
export type AffectCategory = 'joy' | 'anger' | 'sadness' | 'fear' | 'disgust' | 'surprise';
|
|
115
|
+
export type AffectPolarity = 'positive' | 'negative' | 'neutral';
|
|
116
|
+
export interface AffectHit {
|
|
117
|
+
term: string;
|
|
118
|
+
category: AffectCategory;
|
|
119
|
+
/** 1..9, already adjusted by degree modifiers. */
|
|
120
|
+
intensity: number;
|
|
121
|
+
polarity: AffectPolarity;
|
|
122
|
+
negated: boolean;
|
|
123
|
+
position: number;
|
|
124
|
+
}
|
|
125
|
+
export interface AffectFeatures {
|
|
126
|
+
fillers: number;
|
|
127
|
+
hedges: number;
|
|
128
|
+
vague: number;
|
|
129
|
+
tokens: number;
|
|
130
|
+
/** meaningful tokens / all tokens, 0..1 */
|
|
131
|
+
density: number;
|
|
132
|
+
}
|
|
133
|
+
export interface AffectSignal {
|
|
134
|
+
/** Which analyzer produced this: 'lexicon' | 'llm' | 'none'. */
|
|
135
|
+
analyzer: string;
|
|
136
|
+
hits: AffectHit[];
|
|
137
|
+
dominant: AffectCategory | null;
|
|
138
|
+
intensity: number;
|
|
139
|
+
valence: number;
|
|
140
|
+
arousal: number;
|
|
141
|
+
features: AffectFeatures;
|
|
142
|
+
confidence: number;
|
|
143
|
+
/** True when the text looks like withdrawal ("行吧", "随便") but no explicit emotion word. */
|
|
144
|
+
abstain: boolean;
|
|
145
|
+
}
|
|
146
|
+
export declare const EMPTY_AFFECT: AffectSignal;
|
|
147
|
+
/** Wire version of the four verbs. Additive changes bump MINOR (ADR 0005). */
|
|
148
|
+
export declare const PROTOCOL_VERSION = "0.2";
|
package/package.json
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "soulbridge",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.7",
|
|
4
4
|
"description": "A personality layer for any agent: continuity, forgetting, and growth across tools.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
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
|
+
},
|
|
7
16
|
"bin": {
|
|
8
17
|
"soulbridge": "dist/packages/cli/src/cli.js"
|
|
9
18
|
},
|