soulbridge 0.0.13 → 0.0.14
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/README.md
CHANGED
|
@@ -114,6 +114,7 @@ SQLite(默认) / Postgres(可选)
|
|
|
114
114
|
| [适配器索引](adapters/README.md) | 各宿主适配器与接入契约 |
|
|
115
115
|
| [quality](docs/quality.md) | 质量模型、九条门禁、五条评审清单 |
|
|
116
116
|
| [references](docs/references.md) | 外部资料的去留与过期判断 |
|
|
117
|
+
| [roadmap](docs/roadmap.md) | 下一步做什么、借鉴了什么、明确不做什么 |
|
|
117
118
|
| [runbook](docs/runbook.md) | 发版流程、维护操作、已知待办 |
|
|
118
119
|
|
|
119
120
|
**规则**:架构级改动必须先有 RFC 或 ADR;聊天里达成的不算决定。
|
|
@@ -19,7 +19,7 @@ import { buildContext } from './context.js';
|
|
|
19
19
|
import { reflect, ruleExtract, nextEmotion } from './reflect.js';
|
|
20
20
|
import { rank, tokenize } from './scoring.js';
|
|
21
21
|
import { analyzeAffect, emptyAffect, lexiconAnalyzer } from './affect/analyze.js';
|
|
22
|
-
import { llmExtractTurn } from './reflect.js';
|
|
22
|
+
import { gateDrafts, llmExtractTurn } from './reflect.js';
|
|
23
23
|
export class Soul {
|
|
24
24
|
store;
|
|
25
25
|
soulId;
|
|
@@ -129,10 +129,13 @@ export class Soul {
|
|
|
129
129
|
}
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
|
+
// Deterministic write gates: shape -> semantics -> per-turn budget. Whatever
|
|
133
|
+
// they refuse is reported, so written: 0 always has a reason.
|
|
134
|
+
const gated = gateDrafts(drafts);
|
|
132
135
|
// observe runs every turn; repeating a sentence must not create a second row.
|
|
133
136
|
const seen = new Set(this.store.query({ soulId: this.soulId, userId: this.userId, limit: 10_000 }).map((m) => m.content.trim()));
|
|
134
137
|
let written = 0;
|
|
135
|
-
for (const draft of
|
|
138
|
+
for (const draft of gated.accepted) {
|
|
136
139
|
if (seen.has(draft.content.trim()))
|
|
137
140
|
continue;
|
|
138
141
|
seen.add(draft.content.trim());
|
|
@@ -164,7 +167,14 @@ export class Soul {
|
|
|
164
167
|
}
|
|
165
168
|
const emotion = nextEmotion(this.store.emotion(this.soulId, this.userId), [turn]);
|
|
166
169
|
this.store.saveEmotion(this.soulId, this.userId, emotion, now);
|
|
167
|
-
return {
|
|
170
|
+
return {
|
|
171
|
+
written,
|
|
172
|
+
committed: written > 0,
|
|
173
|
+
rejections: gated.rejected,
|
|
174
|
+
emotion,
|
|
175
|
+
affect,
|
|
176
|
+
extractedBy: written > 0 ? extractedBy : 'none',
|
|
177
|
+
};
|
|
168
178
|
}
|
|
169
179
|
/**
|
|
170
180
|
* Explicit form of `observe`: the host decided this is worth keeping.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Emotion, LlmFn, MemoryType, ReflectResult, Turn } from './types.ts';
|
|
1
|
+
import type { DraftRejection, Emotion, LlmFn, MemoryType, ReflectResult, Turn } from './types.ts';
|
|
2
2
|
import type { SoulStore } from './store.ts';
|
|
3
3
|
interface Draft {
|
|
4
4
|
type: MemoryType;
|
|
@@ -30,6 +30,20 @@ export declare const RULES: Rule[];
|
|
|
30
30
|
export declare function isCorrection(text: string): boolean;
|
|
31
31
|
/** Which marker matched, for statistics; null when the turn is not a correction. */
|
|
32
32
|
export declare function correctionRule(text: string): string | null;
|
|
33
|
+
/** Types an extractor may produce; episodic/relationship are host-local. */
|
|
34
|
+
export declare const EXTRACTABLE_TYPES: MemoryType[];
|
|
35
|
+
export declare const MAX_CONTENT_CHARS = 500;
|
|
36
|
+
/** A single turn should not flood the store. */
|
|
37
|
+
export declare const MAX_DRAFTS_PER_TURN = 3;
|
|
38
|
+
/**
|
|
39
|
+
* Deterministic write gates. A draft must pass shape, then semantics, then the
|
|
40
|
+
* per-turn budget; anything refused is reported with the gate that refused it,
|
|
41
|
+
* so a "written: 0" is explainable rather than mysterious.
|
|
42
|
+
*/
|
|
43
|
+
export declare function gateDrafts(drafts: Draft[]): {
|
|
44
|
+
accepted: Draft[];
|
|
45
|
+
rejected: DraftRejection[];
|
|
46
|
+
};
|
|
33
47
|
export declare function ruleExtract(turns: Turn[]): Draft[];
|
|
34
48
|
/** Corrections raise tension and lower passion; an ordinary session lets tension decay. */
|
|
35
49
|
export declare function nextEmotion(current: Emotion, turns: Turn[]): Emotion;
|
|
@@ -86,6 +86,41 @@ export function correctionRule(text) {
|
|
|
86
86
|
}
|
|
87
87
|
const clamp = (n, lo, hi) => Math.min(hi, Math.max(lo, n));
|
|
88
88
|
const round2 = (n) => Math.round(n * 100) / 100;
|
|
89
|
+
/** Types an extractor may produce; episodic/relationship are host-local. */
|
|
90
|
+
export const EXTRACTABLE_TYPES = ['strategy', 'trait', 'fact', 'cognition'];
|
|
91
|
+
export const MAX_CONTENT_CHARS = 500;
|
|
92
|
+
/** A single turn should not flood the store. */
|
|
93
|
+
export const MAX_DRAFTS_PER_TURN = 3;
|
|
94
|
+
/**
|
|
95
|
+
* Deterministic write gates. A draft must pass shape, then semantics, then the
|
|
96
|
+
* per-turn budget; anything refused is reported with the gate that refused it,
|
|
97
|
+
* so a "written: 0" is explainable rather than mysterious.
|
|
98
|
+
*/
|
|
99
|
+
export function gateDrafts(drafts) {
|
|
100
|
+
const accepted = [];
|
|
101
|
+
const rejected = [];
|
|
102
|
+
for (const draft of drafts) {
|
|
103
|
+
const content = draft.content.trim();
|
|
104
|
+
if (!EXTRACTABLE_TYPES.includes(draft.type)) {
|
|
105
|
+
rejected.push({ gate: 'shape', reason: `type ${draft.type} is not extractable`, content });
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (content.length < 2 || content.length > MAX_CONTENT_CHARS) {
|
|
109
|
+
rejected.push({ gate: 'shape', reason: `length ${content.length} (2..${MAX_CONTENT_CHARS})`, content });
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (content.replace(/[\s\p{P}\p{S}]/gu, '').length < 2) {
|
|
113
|
+
rejected.push({ gate: 'semantic', reason: 'no word characters', content });
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (accepted.length >= MAX_DRAFTS_PER_TURN) {
|
|
117
|
+
rejected.push({ gate: 'budget', reason: `more than ${MAX_DRAFTS_PER_TURN} drafts in one turn`, content });
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
accepted.push({ ...draft, content });
|
|
121
|
+
}
|
|
122
|
+
return { accepted, rejected };
|
|
123
|
+
}
|
|
89
124
|
export function ruleExtract(turns) {
|
|
90
125
|
const drafts = [];
|
|
91
126
|
for (const turn of turns) {
|
|
@@ -149,8 +184,9 @@ export async function reflect(store, options) {
|
|
|
149
184
|
}
|
|
150
185
|
const existing = store.query({ soulId: options.soulId, userId: options.userId });
|
|
151
186
|
const seen = new Set(existing.map((m) => m.content));
|
|
187
|
+
const { accepted, rejected } = gateDrafts(drafts);
|
|
152
188
|
let written = 0;
|
|
153
|
-
for (const draft of
|
|
189
|
+
for (const draft of accepted) {
|
|
154
190
|
if (seen.has(draft.content))
|
|
155
191
|
continue;
|
|
156
192
|
seen.add(draft.content);
|
|
@@ -177,7 +213,7 @@ export async function reflect(store, options) {
|
|
|
177
213
|
emotion = nextEmotion(emotion, options.turns);
|
|
178
214
|
store.saveEmotion(options.soulId, options.userId, emotion);
|
|
179
215
|
}
|
|
180
|
-
return { memories: written, superseded:
|
|
216
|
+
return { memories: written, superseded: null, emotion, committed: written > 0, rejections: rejected };
|
|
181
217
|
}
|
|
182
218
|
/* ------------------------------------------------------------------ *
|
|
183
219
|
* LLM distillation port (optional)
|
|
@@ -100,11 +100,30 @@ export interface QuerySpec {
|
|
|
100
100
|
}
|
|
101
101
|
export interface ReflectResult {
|
|
102
102
|
memories: number;
|
|
103
|
-
|
|
103
|
+
/**
|
|
104
|
+
* How many older memories were replaced by a newer conflicting one.
|
|
105
|
+
* `null` means this build does not track supersession yet — it used to report
|
|
106
|
+
* a hardcoded 0, which claimed a capability that did not exist.
|
|
107
|
+
*/
|
|
108
|
+
superseded: number | null;
|
|
104
109
|
emotion: Emotion;
|
|
110
|
+
/** True when at least one memory was durably written by this call. */
|
|
111
|
+
committed: boolean;
|
|
112
|
+
/** Drafts refused before writing, with the gate that refused them. */
|
|
113
|
+
rejections: DraftRejection[];
|
|
114
|
+
}
|
|
115
|
+
export type GateName = 'shape' | 'semantic' | 'budget';
|
|
116
|
+
export interface DraftRejection {
|
|
117
|
+
gate: GateName;
|
|
118
|
+
reason: string;
|
|
119
|
+
content: string;
|
|
105
120
|
}
|
|
106
121
|
export interface ObserveResult {
|
|
107
122
|
written: number;
|
|
123
|
+
/** True when at least one memory was durably written by this call. */
|
|
124
|
+
committed: boolean;
|
|
125
|
+
/** Drafts refused before writing, with the gate that refused them. */
|
|
126
|
+
rejections: DraftRejection[];
|
|
108
127
|
emotion: Emotion;
|
|
109
128
|
/** Analysis of the user turn. Empty signal for assistant turns or on failure. */
|
|
110
129
|
affect: AffectSignal;
|