soulbridge 0.0.103 → 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.
- package/dist/packages/cli/src/cli.js +57 -2
- package/dist/packages/core/src/context.js +19 -3
- package/dist/packages/core/src/index.d.ts +6 -1
- package/dist/packages/core/src/index.js +10 -0
- package/dist/packages/core/src/portable.d.ts +17 -0
- package/dist/packages/core/src/portable.js +93 -0
- package/dist/packages/core/src/sqlite.js +39 -0
- package/dist/packages/core/src/store.d.ts +5 -1
- package/dist/packages/core/src/types.d.ts +27 -0
- package/package.json +1 -1
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* soulbridge demo
|
|
15
15
|
*/
|
|
16
16
|
import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
17
|
-
import { dirname, resolve } from 'node:path';
|
|
17
|
+
import { dirname, join, resolve } from 'node:path';
|
|
18
18
|
// node:sqlite prints an ExperimentalWarning the moment it loads. Replace the default
|
|
19
19
|
// printer with a filter: the CLI's own output stays clean, other warnings still show.
|
|
20
20
|
// Must run before the dynamic import below, which is why that import is dynamic.
|
|
@@ -221,9 +221,27 @@ async function main() {
|
|
|
221
221
|
return;
|
|
222
222
|
}
|
|
223
223
|
case 'export': {
|
|
224
|
+
// Two shapes: a legacy JSONL of memories (--out), and the portable bundle (--dir)
|
|
225
|
+
// that carries the whole record, a readable summary and the current injection.
|
|
226
|
+
if (flags.dir) {
|
|
227
|
+
const { soul } = open(flags);
|
|
228
|
+
const dir = resolve(String(flags.dir));
|
|
229
|
+
mkdirSync(dir, { recursive: true });
|
|
230
|
+
const bundle = soul.portableBundle();
|
|
231
|
+
writeFileSync(join(dir, 'archive.jsonl'), `${bundle.archive.join('\n')}\n`, 'utf8');
|
|
232
|
+
writeFileSync(join(dir, 'summary.md'), bundle.summary, 'utf8');
|
|
233
|
+
writeFileSync(join(dir, 'injection.txt'), bundle.injection, 'utf8');
|
|
234
|
+
out({
|
|
235
|
+
ok: true,
|
|
236
|
+
dir,
|
|
237
|
+
records: bundle.archive.length,
|
|
238
|
+
files: ['archive.jsonl', 'summary.md', 'injection.txt'],
|
|
239
|
+
});
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
224
242
|
const file = flags.out;
|
|
225
243
|
if (!file)
|
|
226
|
-
throw new Error('usage: soulbridge export --out
|
|
244
|
+
throw new Error('usage: soulbridge export --out <file> | --dir <directory>');
|
|
227
245
|
const { soul, deviceId } = open(flags);
|
|
228
246
|
const body = exportJsonl(soul, deviceId);
|
|
229
247
|
const target = resolve(file);
|
|
@@ -409,6 +427,41 @@ async function main() {
|
|
|
409
427
|
out({ db: dbPath, ...soul.stats() });
|
|
410
428
|
return;
|
|
411
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
|
+
}
|
|
412
465
|
case 'demo':
|
|
413
466
|
await demo();
|
|
414
467
|
return;
|
|
@@ -425,6 +478,8 @@ async function main() {
|
|
|
425
478
|
' recall <q> [--limit N] 召回',
|
|
426
479
|
' reflect --text|--file 会话反思',
|
|
427
480
|
' export --out <file> 导出 JSONL',
|
|
481
|
+
' export --dir <directory> 导出可携带档案(archive/summary/injection)',
|
|
482
|
+
' why <factId> 这条事实的依据:来源轮次 / 证据',
|
|
428
483
|
' import --in <file> 导入 JSONL',
|
|
429
484
|
' import --from <steering.md> 导入 AGENTS.md / CLAUDE.md',
|
|
430
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);
|
|
@@ -30,7 +30,7 @@ export * from './storage/config.ts';
|
|
|
30
30
|
export * from './storage/merge.ts';
|
|
31
31
|
export * from './storage/steering.ts';
|
|
32
32
|
export * from './storage/transcript.ts';
|
|
33
|
-
import type { AffectSignal, ContextRequest, Correction, Memory, ContextResult, Emotion, LlmFn, MemoryType, ObserveResult, ReflectResult, ScoredMemory, Turn, CommitResult, Entity, ExtractDraft, ConclusionDraft, Fact, ThreadAttribution, TurnThread, FactDetail, FactSpec, Gap, GapSpec, SessionHandoff, TurnFindResult, SessionSummary, TurnPair, TurnStep, Visibility } from './types.ts';
|
|
33
|
+
import type { AffectSignal, ContextRequest, Correction, Memory, ContextResult, Emotion, LlmFn, MemoryType, ObserveResult, ReflectResult, ScoredMemory, Turn, CommitResult, Entity, ExtractDraft, ConclusionDraft, Fact, ThreadAttribution, TurnThread, FactDetail, FactSpec, Gap, GapSpec, SessionHandoff, TurnFindResult, SessionSummary, TurnPair, TurnStep, PortableBundle, Visibility } from './types.ts';
|
|
34
34
|
import type { SoulStore } from './store.ts';
|
|
35
35
|
import type { AffectAnalyzer } from './affect/port.ts';
|
|
36
36
|
import type { Rule } from './rules.ts';
|
|
@@ -80,6 +80,11 @@ export declare class Soul {
|
|
|
80
80
|
readonly dedupeWindowMs: number;
|
|
81
81
|
readonly onLlmRaw?: (raw: string) => void;
|
|
82
82
|
constructor(options: SoulOptions);
|
|
83
|
+
/**
|
|
84
|
+
* Everything a host needs to carry this soul to another machine or tool:
|
|
85
|
+
* the full archive, a readable summary, and the injection the model would see.
|
|
86
|
+
*/
|
|
87
|
+
portableBundle(now?: number): PortableBundle;
|
|
83
88
|
/** Verb 1: session start. */
|
|
84
89
|
context(request?: Partial<ContextRequest>): ContextResult;
|
|
85
90
|
/**
|
|
@@ -21,6 +21,7 @@ export * from './storage/config.js';
|
|
|
21
21
|
export * from './storage/merge.js';
|
|
22
22
|
export * from './storage/steering.js';
|
|
23
23
|
export * from './storage/transcript.js';
|
|
24
|
+
import { buildPortableBundle } from './portable.js';
|
|
24
25
|
import { FACT_STATUS, MEMORY_STATUS } from './config/status.js';
|
|
25
26
|
import { HOLDER_WORDS } from './config/domains.js';
|
|
26
27
|
import { isActive, isRetracted, roleOf } from './roles.js';
|
|
@@ -100,6 +101,15 @@ export class Soul {
|
|
|
100
101
|
});
|
|
101
102
|
}
|
|
102
103
|
}
|
|
104
|
+
/**
|
|
105
|
+
* Everything a host needs to carry this soul to another machine or tool:
|
|
106
|
+
* the full archive, a readable summary, and the injection the model would see.
|
|
107
|
+
*/
|
|
108
|
+
portableBundle(now = Date.now()) {
|
|
109
|
+
const dump = this.store.dump(this.soulId, this.userId);
|
|
110
|
+
const injection = this.context({}).text;
|
|
111
|
+
return buildPortableBundle(dump, this.soulId, this.userId, injection, now);
|
|
112
|
+
}
|
|
103
113
|
/** Verb 1: session start. */
|
|
104
114
|
context(request = {}) {
|
|
105
115
|
// Spread, not a field list: an explicit list silently dropped every option
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Portable bundle: the whole record, a human-readable summary, and the injection text.
|
|
3
|
+
*
|
|
4
|
+
* Three layers, because they answer different questions (spec/vocabulary.v1 §7):
|
|
5
|
+
* archive — everything, including raw exchanges; the recipient can rebuild the store.
|
|
6
|
+
* summary — what a person can read in a minute: rules, profile, conclusions, promises,
|
|
7
|
+
* entities, open gaps. No ids unless they help.
|
|
8
|
+
* injection — what the model would see right now; the smallest useful transfer.
|
|
9
|
+
*
|
|
10
|
+
* Nothing here filters by importance. An export that silently drops "unimportant" rows
|
|
11
|
+
* cannot be the source of truth, and importance is a semantic judgement we do not store.
|
|
12
|
+
*/
|
|
13
|
+
import type { PortableDump, PortableBundle } from './types.ts';
|
|
14
|
+
export declare function buildPortableSummary(dump: PortableDump, soulId: string, userId: string, now: number): string;
|
|
15
|
+
/** One JSON object per line, tagged so a reader can stream it without a schema. */
|
|
16
|
+
export declare function buildArchiveLines(dump: PortableDump): string[];
|
|
17
|
+
export declare function buildPortableBundle(dump: PortableDump, soulId: string, userId: string, injection: string, now: number): PortableBundle;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
export function buildPortableSummary(dump, soulId, userId, now) {
|
|
2
|
+
const lines = [];
|
|
3
|
+
lines.push(`# soulbridge 档案 · ${userId}`);
|
|
4
|
+
lines.push('');
|
|
5
|
+
lines.push(`- soul: ${soulId}`);
|
|
6
|
+
lines.push(`- 导出时间: ${new Date(now).toISOString()}`);
|
|
7
|
+
lines.push(`- 规模: 事实 ${dump.facts.length} · 实体 ${dump.entities.length} · 线索 ${dump.threads.length} · 轮次 ${dump.turns.length} · 缺口 ${dump.gaps.length}`);
|
|
8
|
+
lines.push('');
|
|
9
|
+
const byRole = (role) => dump.facts.filter((fact) => fact.role === role && fact.status === 'active');
|
|
10
|
+
const slotText = (fact, name) => {
|
|
11
|
+
const slot = fact.slots.find((item) => item.slot === name);
|
|
12
|
+
return slot?.value.kind === 'text' ? slot.value.value : undefined;
|
|
13
|
+
};
|
|
14
|
+
const rules = byRole('rule');
|
|
15
|
+
if (rules.length > 0) {
|
|
16
|
+
lines.push(`## 规则(${rules.length})`);
|
|
17
|
+
for (const rule of rules) {
|
|
18
|
+
const trigger = slotText(rule, '触发');
|
|
19
|
+
const action = slotText(rule, '动作') ?? rule.statement;
|
|
20
|
+
lines.push(trigger ? `- 当 ${trigger}:${action}` : `- ${action}`);
|
|
21
|
+
}
|
|
22
|
+
lines.push('');
|
|
23
|
+
}
|
|
24
|
+
const profile = byRole('profile');
|
|
25
|
+
if (profile.length > 0) {
|
|
26
|
+
lines.push(`## 画像(${profile.length})`);
|
|
27
|
+
for (const line of profile)
|
|
28
|
+
lines.push(`- ${line.statement}`);
|
|
29
|
+
lines.push('');
|
|
30
|
+
}
|
|
31
|
+
const judgments = byRole('judgment');
|
|
32
|
+
if (judgments.length > 0) {
|
|
33
|
+
lines.push(`## 结论(${judgments.length})`);
|
|
34
|
+
for (const line of judgments.slice(0, 30))
|
|
35
|
+
lines.push(`- ${line.statement}`);
|
|
36
|
+
lines.push('');
|
|
37
|
+
}
|
|
38
|
+
const promises = byRole('promise');
|
|
39
|
+
if (promises.length > 0) {
|
|
40
|
+
lines.push(`## 承诺(${promises.length})`);
|
|
41
|
+
for (const line of promises)
|
|
42
|
+
lines.push(`- ${line.statement}`);
|
|
43
|
+
lines.push('');
|
|
44
|
+
}
|
|
45
|
+
if (dump.entities.length > 0) {
|
|
46
|
+
lines.push(`## 实体(${dump.entities.length})`);
|
|
47
|
+
for (const entity of dump.entities.slice(0, 50)) {
|
|
48
|
+
const aliases = entity.aliases.length > 0 ? `(别名:${entity.aliases.join('、')})` : '';
|
|
49
|
+
lines.push(`- ${entity.name} · ${entity.type}${aliases}`);
|
|
50
|
+
}
|
|
51
|
+
lines.push('');
|
|
52
|
+
}
|
|
53
|
+
const openGaps = dump.gaps.filter((gap) => gap.status === 'open');
|
|
54
|
+
if (openGaps.length > 0) {
|
|
55
|
+
lines.push(`## 未闭合的缺口(${openGaps.length})`);
|
|
56
|
+
for (const gap of openGaps.slice(0, 20))
|
|
57
|
+
lines.push(`- ${gap.topic}`);
|
|
58
|
+
lines.push('');
|
|
59
|
+
}
|
|
60
|
+
return lines.join('\n');
|
|
61
|
+
}
|
|
62
|
+
/** One JSON object per line, tagged so a reader can stream it without a schema. */
|
|
63
|
+
export function buildArchiveLines(dump) {
|
|
64
|
+
const lines = [];
|
|
65
|
+
// `record` is the discriminator, not `type`: Entity has its own `type` field, and a
|
|
66
|
+
// spread after the tag would silently overwrite it.
|
|
67
|
+
for (const entity of dump.entities)
|
|
68
|
+
lines.push(JSON.stringify({ record: 'entity', ...entity }));
|
|
69
|
+
for (const fact of dump.facts)
|
|
70
|
+
lines.push(JSON.stringify({ record: 'fact', ...fact }));
|
|
71
|
+
for (const thread of dump.threads)
|
|
72
|
+
lines.push(JSON.stringify({ record: 'thread', ...thread }));
|
|
73
|
+
for (const gap of dump.gaps)
|
|
74
|
+
lines.push(JSON.stringify({ record: 'gap', ...gap }));
|
|
75
|
+
for (const turn of dump.turns)
|
|
76
|
+
lines.push(JSON.stringify({ record: 'turn', ...turn }));
|
|
77
|
+
for (const step of dump.steps)
|
|
78
|
+
lines.push(JSON.stringify({ record: 'step', ...step }));
|
|
79
|
+
for (const correction of dump.corrections)
|
|
80
|
+
lines.push(JSON.stringify({ record: 'correction', ...correction }));
|
|
81
|
+
return lines;
|
|
82
|
+
}
|
|
83
|
+
export function buildPortableBundle(dump, soulId, userId, injection, now) {
|
|
84
|
+
return {
|
|
85
|
+
schemaVersion: 1,
|
|
86
|
+
exportedAt: now,
|
|
87
|
+
soulId,
|
|
88
|
+
userId,
|
|
89
|
+
summary: buildPortableSummary(dump, soulId, userId, now),
|
|
90
|
+
injection,
|
|
91
|
+
archive: buildArchiveLines(dump),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
@@ -646,6 +646,45 @@ export function openSqlite(file) {
|
|
|
646
646
|
deviceId: row.device_id == null ? undefined : String(row.device_id),
|
|
647
647
|
}));
|
|
648
648
|
},
|
|
649
|
+
corrections(soulId, userId, limit = 100_000) {
|
|
650
|
+
return this.recentCorrections(soulId, userId, limit);
|
|
651
|
+
},
|
|
652
|
+
dump(soulId, userId) {
|
|
653
|
+
const facts = this.facts({ soulId, userId, limit: 100_000 });
|
|
654
|
+
const entities = this.findEntities({ soulId, userId, limit: 100_000 });
|
|
655
|
+
const threads = db
|
|
656
|
+
.prepare('SELECT * FROM threads WHERE soul_id = ? AND user_id = ? ORDER BY opened_at ASC')
|
|
657
|
+
.all(soulId, userId).map(rowToThread);
|
|
658
|
+
const gaps = db
|
|
659
|
+
.prepare('SELECT * FROM gaps WHERE soul_id = ? AND user_id = ? ORDER BY opened_at ASC')
|
|
660
|
+
.all(soulId, userId).map(rowToGap);
|
|
661
|
+
const turns = db
|
|
662
|
+
.prepare('SELECT * FROM turns WHERE soul_id = ? AND user_id = ? ORDER BY created_at ASC')
|
|
663
|
+
.all(soulId, userId).map(rowToTurnPair);
|
|
664
|
+
const steps = db
|
|
665
|
+
.prepare(`SELECT s.* FROM steps s JOIN turns t ON t.id = s.turn_id
|
|
666
|
+
WHERE t.soul_id = ? AND t.user_id = ? ORDER BY s.turn_id, s.idx`)
|
|
667
|
+
.all(soulId, userId);
|
|
668
|
+
return {
|
|
669
|
+
entities,
|
|
670
|
+
facts,
|
|
671
|
+
threads,
|
|
672
|
+
gaps,
|
|
673
|
+
turns,
|
|
674
|
+
steps: steps.map((row) => ({
|
|
675
|
+
turnId: String(row.turn_id),
|
|
676
|
+
idx: Number(row.idx),
|
|
677
|
+
kind: row.kind,
|
|
678
|
+
text: row.text == null ? undefined : String(row.text),
|
|
679
|
+
toolName: row.tool_name == null ? undefined : String(row.tool_name),
|
|
680
|
+
toolArgsChars: row.tool_args_chars == null ? undefined : Number(row.tool_args_chars),
|
|
681
|
+
toolResultChars: row.tool_result_chars == null ? undefined : Number(row.tool_result_chars),
|
|
682
|
+
ok: row.ok == null ? undefined : Boolean(row.ok),
|
|
683
|
+
at: Number(row.at),
|
|
684
|
+
})),
|
|
685
|
+
corrections: this.recentCorrections(soulId, userId, 100_000),
|
|
686
|
+
};
|
|
687
|
+
},
|
|
649
688
|
countCorrections(soulId, userId) {
|
|
650
689
|
const row = db
|
|
651
690
|
.prepare('SELECT COUNT(*) AS n FROM corrections WHERE soul_id = ? AND user_id = ?')
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* The only port. Every added method is a storage engine's opinion leaking into
|
|
3
3
|
* the domain, so the surface stays deliberately small.
|
|
4
4
|
*/
|
|
5
|
-
import type { AffectSignal, Correction, Emotion, Entity, EntityQuery, EntitySpec, Fact, FactQuery, FactRole, FactSpec, FactStatus, Gap, GapSpec, Identity, Memory, QuerySpec, SessionSummary, SlotValue, TurnPair, TurnPairSpec, TurnFindResult, TurnFindSpec, TurnSpec, TurnStep, TurnThread, ThreadSpec } from './types.ts';
|
|
5
|
+
import type { AffectSignal, Correction, Emotion, Entity, EntityQuery, EntitySpec, Fact, FactQuery, FactRole, FactSpec, FactStatus, Gap, GapSpec, Identity, Memory, PortableDump, QuerySpec, SessionSummary, SlotValue, TurnPair, TurnPairSpec, TurnFindResult, TurnFindSpec, TurnSpec, TurnStep, TurnThread, ThreadSpec } from './types.ts';
|
|
6
6
|
export interface SoulStore {
|
|
7
7
|
ensureSoul(identity: Identity): void;
|
|
8
8
|
identity(soulId: string): Identity | null;
|
|
@@ -103,6 +103,10 @@ export interface SoulStore {
|
|
|
103
103
|
putGap(spec: GapSpec): Gap;
|
|
104
104
|
openGaps(soulId: string, userId: string, limit?: number): Gap[];
|
|
105
105
|
closeGap(id: string, closedByFactId?: string, at?: number): void;
|
|
106
|
+
/** Corrections the user made, newest first — the objective signal for behaviour changes. */
|
|
107
|
+
corrections(soulId: string, userId: string, limit?: number): Correction[];
|
|
108
|
+
/** Everything a portable bundle needs, scoped to one soul/user. */
|
|
109
|
+
dump(soulId: string, userId: string): PortableDump;
|
|
106
110
|
/** Exchanges visible to `userId`; scored against `spec.query` when given. */
|
|
107
111
|
recallTurnPairs(spec: TurnPairSpec): TurnPair[];
|
|
108
112
|
countTurnPairs(soulId: string, userId: string, sessionId?: string): number;
|
|
@@ -61,6 +61,33 @@ export interface TurnPair {
|
|
|
61
61
|
metadata: Record<string, unknown>;
|
|
62
62
|
createdAt: number;
|
|
63
63
|
}
|
|
64
|
+
/** One step of a turn, flattened for the archive. */
|
|
65
|
+
export interface PortableStep extends TurnStep {
|
|
66
|
+
turnId: string;
|
|
67
|
+
}
|
|
68
|
+
/** Everything the portable bundle needs, scoped to one soul/user. */
|
|
69
|
+
export interface PortableDump {
|
|
70
|
+
entities: Entity[];
|
|
71
|
+
facts: Fact[];
|
|
72
|
+
threads: TurnThread[];
|
|
73
|
+
gaps: Gap[];
|
|
74
|
+
turns: TurnPair[];
|
|
75
|
+
steps: PortableStep[];
|
|
76
|
+
corrections: Correction[];
|
|
77
|
+
}
|
|
78
|
+
/** The three layers of the portability contract (spec/vocabulary.v1 §7). */
|
|
79
|
+
export interface PortableBundle {
|
|
80
|
+
schemaVersion: number;
|
|
81
|
+
exportedAt: number;
|
|
82
|
+
soulId: string;
|
|
83
|
+
userId: string;
|
|
84
|
+
/** Human-readable: rules, profile, conclusions, promises, entities, open gaps. */
|
|
85
|
+
summary: string;
|
|
86
|
+
/** What the model would see right now. */
|
|
87
|
+
injection: string;
|
|
88
|
+
/** One JSON object per line, including raw turns — the source of truth. */
|
|
89
|
+
archive: string[];
|
|
90
|
+
}
|
|
64
91
|
/** What a recall call may narrow by. Everything is optional. */
|
|
65
92
|
export interface TurnPairSpec {
|
|
66
93
|
soulId: string;
|