soulbridge 0.0.104 → 0.0.105

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.
@@ -427,6 +427,41 @@ async function main() {
427
427
  out({ db: dbPath, ...soul.stats() });
428
428
  return;
429
429
  }
430
+ case 'why': {
431
+ // The reader for the 依据 slot: a fact I said must be traceable to the turn I said it
432
+ // in, and a conclusion must be traceable to the facts that support it.
433
+ const factId = positional[0];
434
+ if (!factId)
435
+ throw new Error('usage: soulbridge why <factId>');
436
+ const { soul } = open(flags);
437
+ const fact = soul.store.fact(factId);
438
+ if (!fact) {
439
+ out({ ok: false, error: `no fact ${factId}` });
440
+ return;
441
+ }
442
+ const basisSlot = fact.slots.find((slot) => slot.slot === '依据' && slot.value.kind === 'text');
443
+ const turn = basisSlot?.value.kind === 'text' ? soul.store.turn(basisSlot.value.value) : null;
444
+ out({
445
+ id: fact.id,
446
+ statement: fact.statement,
447
+ role: fact.role,
448
+ status: fact.status,
449
+ source: fact.source,
450
+ basis: turn
451
+ ? {
452
+ turnId: turn.id,
453
+ at: new Date(turn.at).toISOString(),
454
+ userText: turn.userText,
455
+ assistantText: turn.assistantText.slice(0, 200),
456
+ }
457
+ : null,
458
+ evidence: fact.slots
459
+ .filter((slot) => slot.slot === '证据' && slot.value.kind === 'fact')
460
+ .map((slot) => (slot.value.kind === 'fact' ? slot.value.factId : undefined))
461
+ .filter(Boolean),
462
+ });
463
+ return;
464
+ }
430
465
  case 'demo':
431
466
  await demo();
432
467
  return;
@@ -444,6 +479,7 @@ async function main() {
444
479
  ' reflect --text|--file 会话反思',
445
480
  ' export --out <file> 导出 JSONL',
446
481
  ' export --dir <directory> 导出可携带档案(archive/summary/injection)',
482
+ ' why <factId> 这条事实的依据:来源轮次 / 证据',
447
483
  ' import --in <file> 导入 JSONL',
448
484
  ' import --from <steering.md> 导入 AGENTS.md / CLAUDE.md',
449
485
  ' ingest --file <transcript.jsonl> 会话转写 → 记忆与情绪采样',
@@ -78,13 +78,22 @@ export function buildContext(store, request, now = Date.now()) {
78
78
  .facts({ soulId: request.soulId, userId: request.userId, limit: 300 })
79
79
  .filter((fact) => isActive(fact) &&
80
80
  (ANCHOR_ROLES.has(roleOf(fact)) || anchorTypes.has(fact.typeName ?? '') || explicitAnchor(fact)))
81
- .map((fact) => ({ id: fact.id, label: fact.typeName ?? '事实', text: fact.statement, score: fact.confidence }));
81
+ .map((fact) => ({
82
+ id: fact.id,
83
+ label: fact.typeName ?? '事实',
84
+ text: fact.statement,
85
+ score: fact.confidence,
86
+ // ADR 0035: my own side of the exchange is inferred and must not read like something
87
+ // the user said. The marker is two characters; a per-line sentence would double the
88
+ // injection, so the explanation is written once below the block.
89
+ inferred: fact.source === 'inferred',
90
+ }));
82
91
  for (const memory of store.query({ soulId: request.soulId, userId: request.userId })) {
83
92
  if (request.types !== undefined && !request.types.includes(memory.type))
84
93
  continue;
85
94
  if (request.minImportance !== undefined && memory.importance < request.minImportance)
86
95
  continue;
87
- anchors.push({ id: memory.id, label: memory.type, text: memory.content, score: memory.importance / 10 });
96
+ anchors.push({ id: memory.id, label: memory.type, text: memory.content, score: memory.importance / 10, inferred: false });
88
97
  }
89
98
  anchors.sort((a, b) => b.score - a.score);
90
99
  const maxItems = request.maxItems ?? Number.POSITIVE_INFINITY;
@@ -92,6 +101,7 @@ export function buildContext(store, request, now = Date.now()) {
92
101
  let dropped = 0;
93
102
  const lines = [];
94
103
  const used = [];
104
+ let sawInferred = false;
95
105
  for (const anchor of anchors) {
96
106
  if (lines.length >= maxItems) {
97
107
  dropped += 1;
@@ -99,7 +109,10 @@ export function buildContext(store, request, now = Date.now()) {
99
109
  }
100
110
  // Escaping can lengthen a line (one `&lt;` is four characters), so cost is charged
101
111
  // on the escaped text — the budget has to measure what actually gets injected.
102
- const line = `- [${escapeBlockText(anchor.label)}] ${escapeBlockText(anchor.text)}`;
112
+ const marker = anchor.inferred ? '我: ' : '';
113
+ const line = `- ${marker}[${escapeBlockText(anchor.label)}] ${escapeBlockText(anchor.text)}`;
114
+ if (anchor.inferred)
115
+ sawInferred = true;
103
116
  const cost = line.length + 1 + 10; // line + newline + section overhead share
104
117
  if (chars + cost > maxChars) {
105
118
  dropped += 1;
@@ -111,6 +124,9 @@ export function buildContext(store, request, now = Date.now()) {
111
124
  }
112
125
  if (request.includeAnchors !== false && lines.length > 0)
113
126
  sections.push(`<memories>\n${lines.join('\n')}\n</memories>`);
127
+ if (sawInferred) {
128
+ sections.push('<source-notes>以「我:」开头的是我上一轮说的话,未经用户确认;没标的都是用户说的。</source-notes>');
129
+ }
114
130
  const text = sections.join('\n\n');
115
131
  if (request.track !== false && used.length > 0)
116
132
  store.touch(used, now);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "soulbridge",
3
- "version": "0.0.104",
3
+ "version": "0.0.105",
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",