soulbridge 0.0.7 → 0.0.9
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 +23 -0
- package/dist/packages/core/src/index.d.ts +9 -2
- package/dist/packages/core/src/index.js +16 -2
- package/dist/packages/core/src/reflect.d.ts +18 -0
- package/dist/packages/core/src/reflect.js +45 -6
- package/dist/packages/core/src/sqlite.js +6 -0
- package/dist/packages/core/src/store.d.ts +5 -0
- package/package.json +1 -1
|
@@ -303,6 +303,27 @@ async function main() {
|
|
|
303
303
|
out({ ok: true, file: resolve(file), turns: turns.length, skipped, reflected, dryRun });
|
|
304
304
|
return;
|
|
305
305
|
}
|
|
306
|
+
case 'list': {
|
|
307
|
+
const { soul } = open(flags);
|
|
308
|
+
const memories = soul.list(flags.limit ? Number(flags.limit) : 20);
|
|
309
|
+
if (memories.length === 0) {
|
|
310
|
+
process.stdout.write('no memories yet\n');
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
for (const memory of memories) {
|
|
314
|
+
const preview = memory.content.replace(/\s+/g, ' ').slice(0, 72);
|
|
315
|
+
process.stdout.write(`${memory.id} ${memory.type.padEnd(9)} imp=${String(memory.importance).padEnd(2)} ${preview}\n`);
|
|
316
|
+
}
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
case 'forget': {
|
|
320
|
+
const id = positional[0];
|
|
321
|
+
if (!id)
|
|
322
|
+
throw new Error('usage: soulbridge forget <memory-id>');
|
|
323
|
+
const { soul } = open(flags);
|
|
324
|
+
out({ ok: soul.forget(id), id });
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
306
327
|
case 'status': {
|
|
307
328
|
const config = ensureConfig();
|
|
308
329
|
const dbPath = resolveDbPath(flags.db);
|
|
@@ -371,6 +392,8 @@ async function main() {
|
|
|
371
392
|
' import --in <file> 导入 JSONL',
|
|
372
393
|
' import --from <steering.md> 导入 AGENTS.md / CLAUDE.md',
|
|
373
394
|
' ingest --file <transcript.jsonl> 会话转写 → 记忆与情绪采样',
|
|
395
|
+
' list [--limit N] 看它记住了什么',
|
|
396
|
+
' forget <id> 忘掉一条记忆(墓碑)',
|
|
374
397
|
' status 一眼看它是否在工作',
|
|
375
398
|
' stats 看原始状态(JSON)',
|
|
376
399
|
' demo 端到端演示(无需模型)',
|
|
@@ -17,7 +17,7 @@ export * from './storage/config.ts';
|
|
|
17
17
|
export * from './storage/merge.ts';
|
|
18
18
|
export * from './storage/steering.ts';
|
|
19
19
|
export * from './storage/transcript.ts';
|
|
20
|
-
import type { AffectSignal, ContextRequest, ContextResult, Emotion, LlmFn, MemoryType, ObserveResult, ReflectResult, ScoredMemory, Turn, Visibility } from './types.ts';
|
|
20
|
+
import type { AffectSignal, ContextRequest, Memory, ContextResult, Emotion, LlmFn, MemoryType, ObserveResult, ReflectResult, ScoredMemory, Turn, Visibility } from './types.ts';
|
|
21
21
|
import type { SoulStore } from './store.ts';
|
|
22
22
|
import type { AffectAnalyzer } from './affect/port.ts';
|
|
23
23
|
export interface SoulOptions {
|
|
@@ -42,6 +42,10 @@ export declare class Soul {
|
|
|
42
42
|
constructor(options: SoulOptions);
|
|
43
43
|
/** Verb 1: session start. */
|
|
44
44
|
context(request?: Partial<ContextRequest>): ContextResult;
|
|
45
|
+
/** Every active memory, newest first. For `list` and for auditing injection. */
|
|
46
|
+
list(limit?: number): Memory[];
|
|
47
|
+
/** Tombstone one memory so it never reaches a prompt again. */
|
|
48
|
+
forget(id: string): boolean;
|
|
45
49
|
/** Verb 3: on demand. */
|
|
46
50
|
recall(query: string, limit?: number): ScoredMemory[];
|
|
47
51
|
/** Pure analysis, no storage. Async because the analyzer may call a model. */
|
|
@@ -63,7 +67,10 @@ export declare class Soul {
|
|
|
63
67
|
visibility?: Visibility;
|
|
64
68
|
}): string;
|
|
65
69
|
/** Verb 4: session end. */
|
|
66
|
-
reflect(turns: Turn[],
|
|
70
|
+
reflect(turns: Turn[], options?: {
|
|
71
|
+
llm?: LlmFn;
|
|
72
|
+
updateEmotion?: boolean;
|
|
73
|
+
}): Promise<ReflectResult>;
|
|
67
74
|
stats(): {
|
|
68
75
|
memories: number;
|
|
69
76
|
emotion: Emotion;
|
|
@@ -54,6 +54,14 @@ export class Soul {
|
|
|
54
54
|
maxItems: request.maxItems,
|
|
55
55
|
});
|
|
56
56
|
}
|
|
57
|
+
/** Every active memory, newest first. For `list` and for auditing injection. */
|
|
58
|
+
list(limit = 50) {
|
|
59
|
+
return this.store.query({ soulId: this.soulId, userId: this.userId, limit });
|
|
60
|
+
}
|
|
61
|
+
/** Tombstone one memory so it never reaches a prompt again. */
|
|
62
|
+
forget(id) {
|
|
63
|
+
return this.store.forget(id);
|
|
64
|
+
}
|
|
57
65
|
/** Verb 3: on demand. */
|
|
58
66
|
recall(query, limit = 5) {
|
|
59
67
|
const memories = this.store.query({ soulId: this.soulId, userId: this.userId });
|
|
@@ -131,8 +139,14 @@ export class Soul {
|
|
|
131
139
|
return id;
|
|
132
140
|
}
|
|
133
141
|
/** Verb 4: session end. */
|
|
134
|
-
async reflect(turns,
|
|
135
|
-
return reflect(this.store, {
|
|
142
|
+
async reflect(turns, options = {}) {
|
|
143
|
+
return reflect(this.store, {
|
|
144
|
+
soulId: this.soulId,
|
|
145
|
+
userId: this.userId,
|
|
146
|
+
turns,
|
|
147
|
+
llm: options.llm,
|
|
148
|
+
updateEmotion: options.updateEmotion,
|
|
149
|
+
});
|
|
136
150
|
}
|
|
137
151
|
stats() {
|
|
138
152
|
return {
|
|
@@ -11,7 +11,19 @@ interface Rule {
|
|
|
11
11
|
type: MemoryType;
|
|
12
12
|
importance: number;
|
|
13
13
|
match: (text: string) => boolean;
|
|
14
|
+
/** Optional rewrite: keep the instruction, drop the surrounding discussion. */
|
|
15
|
+
distill?: (text: string) => string | null;
|
|
14
16
|
}
|
|
17
|
+
/** Long strategies are noise in a prompt; keep the instruction, not the essay. */
|
|
18
|
+
export declare const MAX_STRATEGY_CHARS = 80;
|
|
19
|
+
/**
|
|
20
|
+
* Cut a standing instruction down to the clause that carries it.
|
|
21
|
+
*
|
|
22
|
+
* A user explains for two lines and states the rule once; storing the whole
|
|
23
|
+
* paragraph repeats the discussion into every future prompt. Find the trigger
|
|
24
|
+
* whose neighbourhood also carries a request, and keep from there.
|
|
25
|
+
*/
|
|
26
|
+
export declare function distillStrategy(text: string): string | null;
|
|
15
27
|
/** Cheap, synchronous, no model. Deliberately conservative: false positives cost trust. */
|
|
16
28
|
export declare const RULES: Rule[];
|
|
17
29
|
export declare function ruleExtract(turns: Turn[]): Draft[];
|
|
@@ -22,6 +34,12 @@ export interface ReflectOptions {
|
|
|
22
34
|
userId: string;
|
|
23
35
|
turns: Turn[];
|
|
24
36
|
now?: number;
|
|
37
|
+
/**
|
|
38
|
+
* Whether this call also advances emotion (default true). A caller that runs
|
|
39
|
+
* reflect per turn, alongside observe(), must pass false or the turn is
|
|
40
|
+
* counted twice.
|
|
41
|
+
*/
|
|
42
|
+
updateEmotion?: boolean;
|
|
25
43
|
/** Reserved for richer extraction; unused in v0. */
|
|
26
44
|
llm?: LlmFn;
|
|
27
45
|
}
|
|
@@ -14,6 +14,38 @@ import { tokenize } from './scoring.js';
|
|
|
14
14
|
*/
|
|
15
15
|
const TIME_TRIGGER = /(以后|下次|每次|从现在起|在.{0,12}之前|from now on|always)/i;
|
|
16
16
|
const DIRECTIVE = /(先|需要|要|必须|应该|请|别|不要|务必|记得|need to|should|must|please)/i;
|
|
17
|
+
/** Long strategies are noise in a prompt; keep the instruction, not the essay. */
|
|
18
|
+
export const MAX_STRATEGY_CHARS = 80;
|
|
19
|
+
/** How far after a trigger the request may sit and still belong to it. */
|
|
20
|
+
const DIRECTIVE_WINDOW = 40;
|
|
21
|
+
/**
|
|
22
|
+
* Cut a standing instruction down to the clause that carries it.
|
|
23
|
+
*
|
|
24
|
+
* A user explains for two lines and states the rule once; storing the whole
|
|
25
|
+
* paragraph repeats the discussion into every future prompt. Find the trigger
|
|
26
|
+
* whose neighbourhood also carries a request, and keep from there.
|
|
27
|
+
*/
|
|
28
|
+
export function distillStrategy(text) {
|
|
29
|
+
const trigger = new RegExp(TIME_TRIGGER.source, 'gi');
|
|
30
|
+
let match = trigger.exec(text);
|
|
31
|
+
let start = -1;
|
|
32
|
+
// The rule usually closes the paragraph ("...比如你每次…都需要…"), so the
|
|
33
|
+
// last qualifying trigger is the instruction, not the setup.
|
|
34
|
+
while (match !== null) {
|
|
35
|
+
const rest = text.slice(match.index);
|
|
36
|
+
if (DIRECTIVE.test(rest.slice(0, DIRECTIVE_WINDOW)))
|
|
37
|
+
start = match.index;
|
|
38
|
+
match = trigger.exec(text);
|
|
39
|
+
}
|
|
40
|
+
if (start < 0)
|
|
41
|
+
return null;
|
|
42
|
+
let clause = text.slice(start).replace(/^[\s,,、::]+/, '');
|
|
43
|
+
clause = clause.replace(/^(比如|例如|就像|也就是说|你|我)\s*/g, '');
|
|
44
|
+
if (clause.length > MAX_STRATEGY_CHARS)
|
|
45
|
+
clause = clause.slice(0, MAX_STRATEGY_CHARS);
|
|
46
|
+
clause = clause.trim();
|
|
47
|
+
return clause.length > 0 ? clause : null;
|
|
48
|
+
}
|
|
17
49
|
/** Cheap, synchronous, no model. Deliberately conservative: false positives cost trust. */
|
|
18
50
|
export const RULES = [
|
|
19
51
|
{
|
|
@@ -33,6 +65,7 @@ export const RULES = [
|
|
|
33
65
|
type: 'strategy',
|
|
34
66
|
importance: 7,
|
|
35
67
|
match: (text) => TIME_TRIGGER.test(text) && DIRECTIVE.test(text),
|
|
68
|
+
distill: distillStrategy,
|
|
36
69
|
},
|
|
37
70
|
{
|
|
38
71
|
id: 'decision',
|
|
@@ -50,10 +83,13 @@ export function ruleExtract(turns) {
|
|
|
50
83
|
if (turn.role !== 'user')
|
|
51
84
|
continue;
|
|
52
85
|
for (const rule of RULES) {
|
|
53
|
-
if (rule.match(turn.content))
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
86
|
+
if (!rule.match(turn.content))
|
|
87
|
+
continue;
|
|
88
|
+
// A rule may rewrite the turn; a strategy we cannot phrase is not stored.
|
|
89
|
+
const content = rule.distill ? rule.distill(turn.content) : turn.content.trim();
|
|
90
|
+
if (content)
|
|
91
|
+
drafts.push({ type: rule.type, content, importance: rule.importance });
|
|
92
|
+
break;
|
|
57
93
|
}
|
|
58
94
|
}
|
|
59
95
|
return drafts;
|
|
@@ -108,7 +144,10 @@ export async function reflect(store, options) {
|
|
|
108
144
|
store.put(memory);
|
|
109
145
|
written += 1;
|
|
110
146
|
}
|
|
111
|
-
|
|
112
|
-
|
|
147
|
+
let emotion = store.emotion(options.soulId, options.userId);
|
|
148
|
+
if (options.updateEmotion !== false) {
|
|
149
|
+
emotion = nextEmotion(emotion, options.turns);
|
|
150
|
+
store.saveEmotion(options.soulId, options.userId, emotion);
|
|
151
|
+
}
|
|
113
152
|
return { memories: written, superseded: 0, emotion };
|
|
114
153
|
}
|
|
@@ -225,6 +225,12 @@ export function openSqlite(file) {
|
|
|
225
225
|
stmt.run(now, id);
|
|
226
226
|
});
|
|
227
227
|
},
|
|
228
|
+
forget(id, now = Date.now()) {
|
|
229
|
+
const result = withRetry(() => db
|
|
230
|
+
.prepare('UPDATE memories SET deleted_at = ?, updated_at = ? WHERE id = ? AND deleted_at IS NULL')
|
|
231
|
+
.run(now, now, id));
|
|
232
|
+
return Number(result.changes ?? 0) > 0;
|
|
233
|
+
},
|
|
228
234
|
saveAffect(soulId, userId, signal, at, deviceId) {
|
|
229
235
|
withRetry(() => {
|
|
230
236
|
db.prepare(`INSERT OR IGNORE INTO affect_samples
|
|
@@ -13,6 +13,11 @@ export interface SoulStore {
|
|
|
13
13
|
query(spec: QuerySpec): Memory[];
|
|
14
14
|
/** Recall bookkeeping: recallCount++, lastRecalledAt = now. */
|
|
15
15
|
touch(ids: string[], now?: number): void;
|
|
16
|
+
/**
|
|
17
|
+
* Tombstone a memory so it never surfaces again (sync-safe: the row stays for
|
|
18
|
+
* merge, the deletion wins by updatedAt). Returns false when no such active row.
|
|
19
|
+
*/
|
|
20
|
+
forget(id: string, now?: number): boolean;
|
|
16
21
|
/** Persist one affect sample (payload only; no raw text). */
|
|
17
22
|
saveAffect(soulId: string, userId: string, signal: AffectSignal, at: number, deviceId?: string): void;
|
|
18
23
|
/** Samples newer than `sinceMs`, newest first. */
|