soulbridge 0.0.17 → 0.0.19
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.
|
@@ -45,6 +45,12 @@ export interface SoulOptions {
|
|
|
45
45
|
* duplicate rather than a second row. Set 0 to disable. Defaults to 30s.
|
|
46
46
|
*/
|
|
47
47
|
dedupeWindowMs?: number;
|
|
48
|
+
/**
|
|
49
|
+
* Receives the model's raw reply, complete. Diagnostics belong to the adapter,
|
|
50
|
+
* not to core, which stays free of I/O; the DSH adapter appends it to a file
|
|
51
|
+
* so a parse failure is explainable instead of silent.
|
|
52
|
+
*/
|
|
53
|
+
onLlmRaw?: (raw: string) => void;
|
|
48
54
|
}
|
|
49
55
|
export declare class Soul {
|
|
50
56
|
readonly store: SoulStore;
|
|
@@ -56,6 +62,7 @@ export declare class Soul {
|
|
|
56
62
|
readonly llm?: LlmFn;
|
|
57
63
|
readonly llmMaxChars: number;
|
|
58
64
|
readonly dedupeWindowMs: number;
|
|
65
|
+
readonly onLlmRaw?: (raw: string) => void;
|
|
59
66
|
constructor(options: SoulOptions);
|
|
60
67
|
/** Verb 1: session start. */
|
|
61
68
|
context(request?: Partial<ContextRequest>): ContextResult;
|
|
@@ -31,6 +31,7 @@ export class Soul {
|
|
|
31
31
|
llm;
|
|
32
32
|
llmMaxChars;
|
|
33
33
|
dedupeWindowMs;
|
|
34
|
+
onLlmRaw;
|
|
34
35
|
constructor(options) {
|
|
35
36
|
this.store = options.store;
|
|
36
37
|
this.soulId = options.soulId;
|
|
@@ -41,6 +42,7 @@ export class Soul {
|
|
|
41
42
|
this.llm = options.llm;
|
|
42
43
|
this.llmMaxChars = options.llmMaxChars ?? 2000;
|
|
43
44
|
this.dedupeWindowMs = options.dedupeWindowMs ?? 30_000;
|
|
45
|
+
this.onLlmRaw = options.onLlmRaw;
|
|
44
46
|
if (!this.store.identity(this.soulId)) {
|
|
45
47
|
this.store.ensureSoul({
|
|
46
48
|
soulId: this.soulId,
|
|
@@ -130,20 +132,31 @@ export class Soul {
|
|
|
130
132
|
// a rule that already fired.
|
|
131
133
|
let drafts = ruleExtract([turn]);
|
|
132
134
|
let extractedBy = drafts.length > 0 ? 'rules' : 'none';
|
|
135
|
+
// A model that says "nothing to keep" and a model that answers garbage used
|
|
136
|
+
// to be indistinguishable; keep them apart for measurement.
|
|
137
|
+
let llmOutcome = 'off';
|
|
133
138
|
if (drafts.length === 0 && turn.role === 'user' && this.llm) {
|
|
134
139
|
if (turn.content.length <= this.llmMaxChars) {
|
|
135
140
|
try {
|
|
136
|
-
const draft = await llmExtractTurn(turn.content, this.llm);
|
|
141
|
+
const draft = await llmExtractTurn(turn.content, this.llm, this.onLlmRaw);
|
|
137
142
|
if (draft) {
|
|
138
143
|
drafts = [draft];
|
|
139
144
|
extractedBy = 'llm';
|
|
145
|
+
llmOutcome = 'stored';
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
llmOutcome = 'null';
|
|
140
149
|
}
|
|
141
150
|
}
|
|
142
151
|
catch {
|
|
143
152
|
// The model is an enhancement; a transport or parse failure leaves the
|
|
144
153
|
// rule result (nothing) in place.
|
|
154
|
+
llmOutcome = 'failed';
|
|
145
155
|
}
|
|
146
156
|
}
|
|
157
|
+
else {
|
|
158
|
+
llmOutcome = 'skipped';
|
|
159
|
+
}
|
|
147
160
|
}
|
|
148
161
|
// Deterministic write gates: shape -> semantics -> per-turn budget. Whatever
|
|
149
162
|
// they refuse is reported, so written: 0 always has a reason.
|
|
@@ -187,6 +200,7 @@ export class Soul {
|
|
|
187
200
|
written,
|
|
188
201
|
committed: written > 0,
|
|
189
202
|
rejections: gated.rejected,
|
|
203
|
+
llmOutcome,
|
|
190
204
|
emotion,
|
|
191
205
|
affect,
|
|
192
206
|
extractedBy: written > 0 ? extractedBy : 'none',
|
|
@@ -75,5 +75,5 @@ export interface LlmDraft {
|
|
|
75
75
|
* "nothing worth storing"; throws when the transport or the output is unusable,
|
|
76
76
|
* so the caller can fall back to rules.
|
|
77
77
|
*/
|
|
78
|
-
export declare function llmExtractTurn(text: string, llm: LlmFn): Promise<LlmDraft | null>;
|
|
78
|
+
export declare function llmExtractTurn(text: string, llm: LlmFn, onRaw?: (raw: string) => void): Promise<LlmDraft | null>;
|
|
79
79
|
export {};
|
|
@@ -252,8 +252,9 @@ function parseLlmJson(raw) {
|
|
|
252
252
|
* "nothing worth storing"; throws when the transport or the output is unusable,
|
|
253
253
|
* so the caller can fall back to rules.
|
|
254
254
|
*/
|
|
255
|
-
export async function llmExtractTurn(text, llm) {
|
|
255
|
+
export async function llmExtractTurn(text, llm, onRaw) {
|
|
256
256
|
const raw = await llm(`${LLM_PROMPT}\n\n用户:${text}`);
|
|
257
|
+
onRaw?.(String(raw));
|
|
257
258
|
const parsed = parseLlmJson(String(raw));
|
|
258
259
|
if (!parsed)
|
|
259
260
|
throw new Error('llm returned no JSON');
|
|
@@ -129,6 +129,11 @@ export interface ObserveResult {
|
|
|
129
129
|
committed: boolean;
|
|
130
130
|
/** Drafts refused before writing, with the gate that refused them. */
|
|
131
131
|
rejections: DraftRejection[];
|
|
132
|
+
/**
|
|
133
|
+
* What the optional model did, kept apart from `extractedBy` because a null
|
|
134
|
+
* answer and a broken answer used to look identical in the logs.
|
|
135
|
+
*/
|
|
136
|
+
llmOutcome: 'off' | 'skipped' | 'stored' | 'null' | 'failed';
|
|
132
137
|
emotion: Emotion;
|
|
133
138
|
/** Analysis of the user turn. Empty signal for assistant turns or on failure. */
|
|
134
139
|
affect: AffectSignal;
|