soulbridge 0.0.6 → 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 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.4 · 已发布 npm · 接口仍在演进**
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
- ' stats 看状态',
374
+ ' status 一眼看它是否在工作',
375
+ ' stats 看原始状态(JSON)',
331
376
  ' demo 端到端演示(无需模型)',
332
377
  '',
333
378
  'env: SOULBRIDGE_DB, SOULBRIDGE_SOUL, SOULBRIDGE_USER, SOULBRIDGE_NAME,',
@@ -53,13 +53,21 @@ export function buildContext(store, request, now = Date.now()) {
53
53
  }
54
54
  }
55
55
  }
56
- const candidates = store.query({ soulId: request.soulId, userId: request.userId });
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)
@@ -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. */
@@ -5,6 +5,15 @@ interface Draft {
5
5
  content: string;
6
6
  importance: number;
7
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[];
8
17
  export declare function ruleExtract(turns: Turn[]): Draft[];
9
18
  /** Corrections raise tension and lower passion; an ordinary session lets tension decay. */
10
19
  export declare function nextEmotion(current: Emotion, turns: Turn[]): Emotion;
@@ -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
- { re: /(记住|别忘了|remember|don'?t forget)/i, type: 'fact', importance: 7 },
13
- { re: /(我喜欢|我习惯|我偏好|我倾向|i prefer|i like)/i, type: 'trait', importance: 6 },
14
- { re: /(以后|下次|每次|从现在起|from now on|always)/i, type: 'strategy', importance: 7 },
15
- { re: /(我决定|就这么定|原则是|i decided|my rule)/i, type: 'cognition', importance: 7 },
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.re.test(turn.content)) {
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
  }
@@ -57,6 +57,18 @@ export interface ContextRequest {
57
57
  userId: string;
58
58
  query?: string;
59
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;
60
72
  }
61
73
  export interface ContextResult {
62
74
  text: string;
package/package.json CHANGED
@@ -1,59 +1,59 @@
1
1
  {
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
- }
2
+ "name": "soulbridge",
3
+ "version": "0.0.7",
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": ">=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 && npm test"
54
+ },
55
+ "devDependencies": {
56
+ "@types/node": "^26.6.5",
57
+ "typescript": "^7.0.2"
58
+ }
59
59
  }