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) => ({
|
|
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 `<` is four characters), so cost is charged
|
|
101
111
|
// on the escaped text — the budget has to measure what actually gets injected.
|
|
102
|
-
const
|
|
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);
|