soulbridge 0.0.104 → 0.0.106
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.js +36 -0
- package/dist/packages/core/src/config/labels.d.ts +14 -0
- package/dist/packages/core/src/config/labels.js +14 -0
- package/dist/packages/core/src/context.js +19 -4
- package/dist/packages/core/src/index.d.ts +1 -0
- package/dist/packages/core/src/index.js +1 -0
- package/package.json +1 -1
|
@@ -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> 会话转写 → 记忆与情绪采样',
|
|
@@ -20,3 +20,17 @@ export declare const CONCLUSION_TYPE = "\u7ED3\u8BBA";
|
|
|
20
20
|
* readers; retyping it in a second filter is how a layer silently drops out.
|
|
21
21
|
*/
|
|
22
22
|
export declare const ANCHOR_TYPES: readonly string[];
|
|
23
|
+
/**
|
|
24
|
+
* How a fact that came from my own reply is marked in injection (ADR 0035).
|
|
25
|
+
*
|
|
26
|
+
* Two renderers write fact lines — the core's `<memories>` block and the plugin's `<about>`
|
|
27
|
+
* block — and both must use the same marker, or the same fact reads differently depending on
|
|
28
|
+
* which path surfaced it. Declared here, imported by both.
|
|
29
|
+
*/
|
|
30
|
+
export declare const INFERRED_MARKER = "\u6211: ";
|
|
31
|
+
/**
|
|
32
|
+
* One explanation per block, not one per line.
|
|
33
|
+
*
|
|
34
|
+
* 341 of 646 facts are inferred, so a sentence on every line would double the injection.
|
|
35
|
+
*/
|
|
36
|
+
export declare const SOURCE_NOTES = "<source-notes>\u4EE5\u300C\u6211:\u300D\u5F00\u5934\u7684\u662F\u6211\u4E0A\u4E00\u8F6E\u8BF4\u7684\u8BDD\uFF0C\u672A\u7ECF\u7528\u6237\u786E\u8BA4\uFF1B\u6CA1\u6807\u7684\u90FD\u662F\u7528\u6237\u8BF4\u7684\u3002</source-notes>";
|
|
@@ -19,3 +19,17 @@ export const CONCLUSION_TYPE = '结论';
|
|
|
19
19
|
* readers; retyping it in a second filter is how a layer silently drops out.
|
|
20
20
|
*/
|
|
21
21
|
export const ANCHOR_TYPES = ['结论', '教训', '原则', '性格'];
|
|
22
|
+
/**
|
|
23
|
+
* How a fact that came from my own reply is marked in injection (ADR 0035).
|
|
24
|
+
*
|
|
25
|
+
* Two renderers write fact lines — the core's `<memories>` block and the plugin's `<about>`
|
|
26
|
+
* block — and both must use the same marker, or the same fact reads differently depending on
|
|
27
|
+
* which path surfaced it. Declared here, imported by both.
|
|
28
|
+
*/
|
|
29
|
+
export const INFERRED_MARKER = '我: ';
|
|
30
|
+
/**
|
|
31
|
+
* One explanation per block, not one per line.
|
|
32
|
+
*
|
|
33
|
+
* 341 of 646 facts are inferred, so a sentence on every line would double the injection.
|
|
34
|
+
*/
|
|
35
|
+
export const SOURCE_NOTES = '<source-notes>以「我:」开头的是我上一轮说的话,未经用户确认;没标的都是用户说的。</source-notes>';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { FACT_STATUS } from './config/status.js';
|
|
2
|
-
import { ANCHOR_TYPES } from './config/labels.js';
|
|
2
|
+
import { ANCHOR_TYPES, INFERRED_MARKER, SOURCE_NOTES } from './config/labels.js';
|
|
3
3
|
import { isActive, roleOf } from './roles.js';
|
|
4
4
|
import { escapeAttr, escapeBlockText } from './inject-text.js';
|
|
5
5
|
import { selectDirectives } from './affect/directives.js';
|
|
@@ -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 ? INFERRED_MARKER : '';
|
|
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,8 @@ 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);
|
|
114
129
|
const text = sections.join('\n\n');
|
|
115
130
|
if (request.track !== false && used.length > 0)
|
|
116
131
|
store.touch(used, now);
|
|
@@ -8,6 +8,7 @@ export * from './types.ts';
|
|
|
8
8
|
export type { SoulStore } from './store.ts';
|
|
9
9
|
export { openSqlite, SCHEMA_VERSION, AFFECT_RETENTION_MS, DbNewerThanBinaryError } from './sqlite.ts';
|
|
10
10
|
export { buildContext, styleDirectives, DEFAULT_MAX_CHARS, AFFECT_BUDGET, AFFECT_WINDOW_MS, } from './context.ts';
|
|
11
|
+
export { INFERRED_MARKER, SOURCE_NOTES } from './config/labels.ts';
|
|
11
12
|
export { reflect, ruleExtract, nextEmotion, isCorrection, correctionRule, detectCorrection, isQuestion, } from './reflect.ts';
|
|
12
13
|
export type { CorrectionLayer, CorrectionSignal, CorrectionVerdict } from './reflect.ts';
|
|
13
14
|
export { rank, scoreMemory, tokenize, relevance, bm25, buildCorpusStats } from './scoring.ts';
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
export * from './types.js';
|
|
8
8
|
export { openSqlite, SCHEMA_VERSION, AFFECT_RETENTION_MS, DbNewerThanBinaryError } from './sqlite.js';
|
|
9
9
|
export { buildContext, styleDirectives, DEFAULT_MAX_CHARS, AFFECT_BUDGET, AFFECT_WINDOW_MS, } from './context.js';
|
|
10
|
+
export { INFERRED_MARKER, SOURCE_NOTES } from './config/labels.js';
|
|
10
11
|
export { reflect, ruleExtract, nextEmotion, isCorrection, correctionRule, detectCorrection, isQuestion, } from './reflect.js';
|
|
11
12
|
export { rank, scoreMemory, tokenize, relevance, bm25, buildCorpusStats } from './scoring.js';
|
|
12
13
|
export { parseThreadLine } from './extract.js';
|