@animalabs/connectome-host 0.7.2 → 0.7.4
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/CHANGELOG.md +203 -10
- package/HEADLESS-FLEET-PLAN.md +22 -0
- package/README.md +22 -11
- package/docs/AGENT-ONBOARDING.md +20 -1
- package/docs/debug-context-api.md +2 -2
- package/docs/retrieval-traces.md +173 -0
- package/docs/webui-deployment.md +2 -1
- package/package.json +3 -3
- package/scripts/audit-module-optins.ts +288 -0
- package/scripts/warmup-session.ts +17 -3
- package/src/codex-subscription-adapter.ts +13 -1
- package/src/framework-agent-config.ts +59 -4
- package/src/framework-strategy.ts +33 -3
- package/src/headless.ts +14 -0
- package/src/index.ts +95 -35
- package/src/logging-adapter.ts +13 -2
- package/src/mcpl-config.ts +8 -0
- package/src/modules/fleet-module.ts +60 -1
- package/src/modules/fleet-types.ts +30 -1
- package/src/modules/identity-module.ts +274 -0
- package/src/modules/mcpl-admin-module.ts +78 -5
- package/src/modules/observers-module.ts +12 -0
- package/src/modules/retrieval-module.ts +254 -52
- package/src/modules/retrieval-trace-page.ts +254 -0
- package/src/modules/retrieval-trace.ts +904 -0
- package/src/modules/settings-module.ts +28 -2
- package/src/modules/subscription-gc-module.ts +54 -1
- package/src/modules/tts-relay-module.ts +33 -18
- package/src/modules/web-ui-module.ts +445 -894
- package/src/recipe.ts +137 -12
- package/src/retrieval-config.ts +39 -0
- package/src/strategies/frontdesk-strategy.ts +34 -125
- package/src/tui.ts +325 -54
- package/src/web/panel-data.ts +1187 -0
- package/src/web/protocol.ts +75 -10
- package/test/audit-module-optins.test.ts +167 -0
- package/test/bedrock-prompt-caching.test.ts +170 -0
- package/test/fleet-panel-request.test.ts +90 -0
- package/test/framework-strategy-defaults.test.ts +110 -0
- package/test/frontdesk-strategy.test.ts +25 -37
- package/test/headless-panel-request.test.ts +201 -0
- package/test/identity-and-surfaces.test.ts +157 -0
- package/test/mcpl-admin-module.test.ts +23 -0
- package/test/mock-headless-child.ts +14 -0
- package/test/retrieval-auth-loopback.test.ts +49 -0
- package/test/retrieval-config.test.ts +74 -0
- package/test/retrieval-module.test.ts +821 -0
- package/test/subscription-gc-module.test.ts +152 -0
- package/test/tui-format.test.ts +106 -0
- package/test/web-ui-context-coverage.test.ts +1 -1
- package/test/web-ui-module.test.ts +189 -3
- package/test/web-ui-observers.test.ts +8 -5
- package/test/web-ui-protocol.test.ts +0 -0
- package/web/bun.lock +345 -0
- package/web/src/App.tsx +159 -44
- package/web/src/Context.tsx +35 -8
- package/web/src/ContextDocument.tsx +20 -5
- package/web/src/Files.tsx +2 -8
- package/web/src/Lessons.tsx +2 -38
- package/web/src/Mcpl.tsx +80 -14
- package/web/src/Pins.tsx +5 -0
- package/web/src/Settings.tsx +5 -0
- package/web/vite.config.ts +8 -2
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
* RetrievalModule — LLM-as-retriever for semantic memory.
|
|
3
3
|
*
|
|
4
4
|
* Three-step retrieval pipeline running in gatherContext():
|
|
5
|
-
* 1. Flag concepts
|
|
6
|
-
*
|
|
5
|
+
* 1. Flag concepts: identify concepts being discussed that might benefit
|
|
6
|
+
* from background knowledge
|
|
7
7
|
* 2. Mechanical query: keyword-match against LessonsModule
|
|
8
|
-
* 3. Validate relevance
|
|
8
|
+
* 3. Validate relevance: filter to actually relevant lessons
|
|
9
9
|
*
|
|
10
|
-
* Steps 1 and 3 use
|
|
10
|
+
* Steps 1 and 3 use the configured retrieval model and optional reasoning.
|
|
11
11
|
* Results are cached to avoid redundant calls on unchanged context.
|
|
12
12
|
*/
|
|
13
13
|
|
|
@@ -24,16 +24,35 @@ import type {
|
|
|
24
24
|
import type { Membrane, NormalizedRequest } from '@animalabs/membrane';
|
|
25
25
|
import type { ContextInjection } from '@animalabs/context-manager';
|
|
26
26
|
import type { LessonsModule, Lesson } from './lessons-module.js';
|
|
27
|
+
import {
|
|
28
|
+
RetrievalTraceStore,
|
|
29
|
+
type RetrievalTraceListOptions,
|
|
30
|
+
type RetrievalTraceRun,
|
|
31
|
+
} from './retrieval-trace.js';
|
|
27
32
|
|
|
28
33
|
// ---------------------------------------------------------------------------
|
|
29
34
|
// Configuration
|
|
30
35
|
// ---------------------------------------------------------------------------
|
|
31
36
|
|
|
37
|
+
export type RetrievalReasoningEffort = 'none' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' | 'max';
|
|
38
|
+
|
|
39
|
+
export interface RetrievalReasoningConfig {
|
|
40
|
+
effort: RetrievalReasoningEffort;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface RecentRetrievalContext {
|
|
44
|
+
text: string;
|
|
45
|
+
messageCount: number;
|
|
46
|
+
messageIds: string[];
|
|
47
|
+
}
|
|
48
|
+
|
|
32
49
|
export interface RetrievalModuleConfig {
|
|
33
|
-
/** Membrane instance for
|
|
50
|
+
/** Membrane instance for retrieval calls */
|
|
34
51
|
membrane: Membrane;
|
|
35
52
|
/** Model to use for retrieval calls (default: claude-haiku-4-5-20251001) */
|
|
36
53
|
retrievalModel?: string;
|
|
54
|
+
/** Optional OpenAI reasoning effort, applied to both retrieval LLM stages. */
|
|
55
|
+
retrievalReasoning?: RetrievalReasoningConfig;
|
|
37
56
|
/** Max lessons to inject (default: 5) */
|
|
38
57
|
maxInjectedLessons?: number;
|
|
39
58
|
/** Minimum lesson confidence for injection (default: 0.3) */
|
|
@@ -67,6 +86,10 @@ export class RetrievalModule implements Module {
|
|
|
67
86
|
private config: RetrievalModuleConfig;
|
|
68
87
|
private lastContextHash = '';
|
|
69
88
|
private cachedInjections: ContextInjection[] = [];
|
|
89
|
+
private cachedLessonIds: string[] = [];
|
|
90
|
+
private cachedLessons: Lesson[] = [];
|
|
91
|
+
private cachedSourceTraceId: number | undefined;
|
|
92
|
+
private readonly traceStore = new RetrievalTraceStore();
|
|
70
93
|
|
|
71
94
|
constructor(config: RetrievalModuleConfig) {
|
|
72
95
|
this.config = config;
|
|
@@ -93,50 +116,115 @@ export class RetrievalModule implements Module {
|
|
|
93
116
|
return {};
|
|
94
117
|
}
|
|
95
118
|
|
|
119
|
+
/** Recent retrieval runs, newest first. Exact conversation inputs are opt-in. */
|
|
120
|
+
getRetrievalTraces(options?: RetrievalTraceListOptions) {
|
|
121
|
+
return this.traceStore.list(options);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private beginTrace(agentName: string): RetrievalTraceRun | undefined {
|
|
125
|
+
try {
|
|
126
|
+
const providerParams = this.retrievalProviderParams();
|
|
127
|
+
return this.traceStore.begin({
|
|
128
|
+
agentName,
|
|
129
|
+
model: this.config.retrievalModel ?? 'claude-haiku-4-5-20251001',
|
|
130
|
+
...(this.config.retrievalReasoning
|
|
131
|
+
? { requestedReasoning: this.config.retrievalReasoning }
|
|
132
|
+
: {}),
|
|
133
|
+
...(providerParams ? { providerParams } : {}),
|
|
134
|
+
minConfidence: this.config.minConfidence ?? 0.3,
|
|
135
|
+
maxCandidates: 20,
|
|
136
|
+
maxInjectedLessons: this.config.maxInjectedLessons ?? 5,
|
|
137
|
+
});
|
|
138
|
+
} catch {
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
96
143
|
/**
|
|
97
144
|
* Run the 3-step retrieval pipeline before each inference.
|
|
98
145
|
*/
|
|
99
|
-
async gatherContext(
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
146
|
+
async gatherContext(agentName: string): Promise<ContextInjection[]> {
|
|
147
|
+
const trace = this.beginTrace(agentName);
|
|
148
|
+
if (!this.ctx) {
|
|
149
|
+
trace?.finish('not-started');
|
|
150
|
+
return [];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
let lessons: Lesson[];
|
|
154
|
+
let recentMessages: string;
|
|
155
|
+
let contextHash: string;
|
|
156
|
+
try {
|
|
157
|
+
// These lookups, eligibility checks, context rendering, and hashing are
|
|
158
|
+
// pre-existing throwing paths. Record their failure, then preserve the
|
|
159
|
+
// upstream rejection rather than applying the provider-stage fail-open.
|
|
160
|
+
const lessonsModule = this.ctx.getModule<LessonsModule>('lessons');
|
|
161
|
+
if (!lessonsModule) {
|
|
162
|
+
trace?.finish('no-lessons-module');
|
|
163
|
+
return [];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
lessons = lessonsModule.getLessons().filter(
|
|
167
|
+
l => !l.deprecated && l.confidence >= (this.config.minConfidence ?? 0.3)
|
|
168
|
+
);
|
|
169
|
+
if (lessons.length === 0) {
|
|
170
|
+
trace?.finish('no-eligible-lessons');
|
|
171
|
+
return [];
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const recent = this.getRecentContext();
|
|
175
|
+
if (!recent) {
|
|
176
|
+
trace?.finish('no-recent-context');
|
|
177
|
+
return [];
|
|
178
|
+
}
|
|
179
|
+
recentMessages = recent.text;
|
|
180
|
+
|
|
181
|
+
// Check cache: if context hasn't changed, reuse cached results.
|
|
182
|
+
contextHash = this.hashContext(recentMessages);
|
|
183
|
+
trace?.setContext(contextHash, recentMessages, recent.messageCount, recent.messageIds);
|
|
184
|
+
if (contextHash === this.lastContextHash && this.cachedInjections.length > 0) {
|
|
185
|
+
trace?.recordCacheHit(
|
|
186
|
+
this.cachedSourceTraceId,
|
|
187
|
+
this.cachedLessonIds,
|
|
188
|
+
this.cachedLessons,
|
|
189
|
+
this.cachedInjections,
|
|
190
|
+
);
|
|
191
|
+
trace?.finish('cache-hit');
|
|
192
|
+
return this.cachedInjections;
|
|
193
|
+
}
|
|
194
|
+
} catch (error) {
|
|
195
|
+
trace?.finish('error', error);
|
|
196
|
+
throw error;
|
|
119
197
|
}
|
|
120
198
|
|
|
121
199
|
try {
|
|
122
|
-
// Step 1: Flag concepts
|
|
123
|
-
const concepts = await this.flagConcepts(recentMessages);
|
|
200
|
+
// Step 1: Flag concepts
|
|
201
|
+
const concepts = await this.flagConcepts(recentMessages, trace);
|
|
124
202
|
if (concepts.length === 0) {
|
|
125
203
|
this.lastContextHash = contextHash;
|
|
126
204
|
this.cachedInjections = [];
|
|
205
|
+
this.cachedLessonIds = [];
|
|
206
|
+
this.cachedLessons = [];
|
|
207
|
+
this.cachedSourceTraceId = undefined;
|
|
208
|
+
trace?.finish('no-concepts');
|
|
127
209
|
return [];
|
|
128
210
|
}
|
|
129
211
|
|
|
130
212
|
// Step 2: Mechanical query (keyword matching)
|
|
131
213
|
const candidates = this.queryCandidates(concepts, lessons);
|
|
214
|
+
trace?.recordCandidates(concepts, candidates);
|
|
132
215
|
if (candidates.length === 0) {
|
|
133
216
|
this.lastContextHash = contextHash;
|
|
134
217
|
this.cachedInjections = [];
|
|
218
|
+
this.cachedLessonIds = [];
|
|
219
|
+
this.cachedLessons = [];
|
|
220
|
+
this.cachedSourceTraceId = undefined;
|
|
221
|
+
trace?.finish('no-candidates');
|
|
135
222
|
return [];
|
|
136
223
|
}
|
|
137
224
|
|
|
138
|
-
// Step 3: Validate relevance
|
|
139
|
-
const relevant = await this.validateRelevance(recentMessages, candidates);
|
|
225
|
+
// Step 3: Validate relevance
|
|
226
|
+
const relevant = await this.validateRelevance(recentMessages, candidates, trace);
|
|
227
|
+
trace?.recordRelevant(relevant);
|
|
140
228
|
|
|
141
229
|
// Build injection
|
|
142
230
|
const maxLessons = this.config.maxInjectedLessons ?? 5;
|
|
@@ -145,6 +233,10 @@ export class RetrievalModule implements Module {
|
|
|
145
233
|
if (injected.length === 0) {
|
|
146
234
|
this.lastContextHash = contextHash;
|
|
147
235
|
this.cachedInjections = [];
|
|
236
|
+
this.cachedLessonIds = [];
|
|
237
|
+
this.cachedLessons = [];
|
|
238
|
+
this.cachedSourceTraceId = undefined;
|
|
239
|
+
trace?.finish('no-relevant-lessons');
|
|
148
240
|
return [];
|
|
149
241
|
}
|
|
150
242
|
|
|
@@ -154,15 +246,25 @@ export class RetrievalModule implements Module {
|
|
|
154
246
|
|
|
155
247
|
const injections: ContextInjection[] = [{
|
|
156
248
|
namespace: 'retrieval',
|
|
157
|
-
|
|
249
|
+
// 'afterUser', NOT 'system': retrieval content changes with recent
|
|
250
|
+
// context (contextHash above), so injecting it into the system prompt
|
|
251
|
+
// churns the very front of the KV cache and invalidates the entire
|
|
252
|
+
// prefix every turn. Tail injection keeps the stable prefix cached.
|
|
253
|
+
position: 'afterUser',
|
|
158
254
|
content: [{ type: 'text', text: `## Retrieved Knowledge\n${text}` }],
|
|
159
255
|
}];
|
|
160
256
|
|
|
161
257
|
this.lastContextHash = contextHash;
|
|
162
258
|
this.cachedInjections = injections;
|
|
259
|
+
this.cachedLessons = this.safeLessonSnapshots(injected);
|
|
260
|
+
this.cachedLessonIds = this.safeLessonIds(this.cachedLessons);
|
|
261
|
+
this.cachedSourceTraceId = trace?.id;
|
|
262
|
+
trace?.recordInjection(injected, injections);
|
|
263
|
+
trace?.finish('injected');
|
|
163
264
|
return injections;
|
|
164
265
|
} catch (err) {
|
|
165
266
|
// Fail open — don't block inference if retrieval fails
|
|
267
|
+
trace?.finish('error', err);
|
|
166
268
|
console.error('RetrievalModule: retrieval failed:', err);
|
|
167
269
|
return [];
|
|
168
270
|
}
|
|
@@ -172,24 +274,44 @@ export class RetrievalModule implements Module {
|
|
|
172
274
|
// Pipeline Steps
|
|
173
275
|
// =========================================================================
|
|
174
276
|
|
|
277
|
+
/** Build OpenAI request parameters for configured retrieval reasoning effort. */
|
|
278
|
+
private retrievalProviderParams(): Record<string, unknown> | undefined {
|
|
279
|
+
const reasoning = this.config.retrievalReasoning;
|
|
280
|
+
if (!reasoning) return undefined;
|
|
281
|
+
return { reasoning: { effort: reasoning.effort } };
|
|
282
|
+
}
|
|
283
|
+
|
|
175
284
|
/**
|
|
176
|
-
* Step 1: Use
|
|
285
|
+
* Step 1: Use the configured retrieval model to identify concepts that might benefit from background knowledge.
|
|
177
286
|
*/
|
|
178
|
-
private async flagConcepts(
|
|
287
|
+
private async flagConcepts(
|
|
288
|
+
recentContext: string,
|
|
289
|
+
trace?: RetrievalTraceRun,
|
|
290
|
+
): Promise<string[]> {
|
|
179
291
|
const model = this.config.retrievalModel ?? 'claude-haiku-4-5-20251001';
|
|
292
|
+
const providerParams = this.retrievalProviderParams();
|
|
293
|
+
const input = `Recent conversation:\n${recentContext}`;
|
|
180
294
|
|
|
181
295
|
const request: NormalizedRequest = {
|
|
182
296
|
messages: [
|
|
183
297
|
{
|
|
184
298
|
participant: 'user',
|
|
185
|
-
content: [{ type: 'text', text:
|
|
299
|
+
content: [{ type: 'text', text: input }],
|
|
186
300
|
},
|
|
187
301
|
],
|
|
188
302
|
system: CONCEPT_FLAG_PROMPT,
|
|
189
303
|
config: { model, maxTokens: 500, temperature: 0 },
|
|
304
|
+
...(providerParams ? { providerParams } : {}),
|
|
190
305
|
};
|
|
191
306
|
|
|
192
|
-
|
|
307
|
+
trace?.startConceptExtraction(CONCEPT_FLAG_PROMPT, input);
|
|
308
|
+
let response;
|
|
309
|
+
try {
|
|
310
|
+
response = await this.config.membrane.complete(request);
|
|
311
|
+
} catch (error) {
|
|
312
|
+
trace?.recordStageError('conceptExtraction', error);
|
|
313
|
+
throw error;
|
|
314
|
+
}
|
|
193
315
|
const text = response.content
|
|
194
316
|
.filter((b): b is { type: 'text'; text: string } => b.type === 'text')
|
|
195
317
|
.map(b => b.text)
|
|
@@ -198,16 +320,28 @@ export class RetrievalModule implements Module {
|
|
|
198
320
|
try {
|
|
199
321
|
const parsed = JSON.parse(text);
|
|
200
322
|
if (Array.isArray(parsed)) {
|
|
201
|
-
|
|
323
|
+
const concepts = parsed.filter((value): value is string => typeof value === 'string');
|
|
324
|
+
trace?.finishConceptExtraction(text, concepts, 'json', response.content);
|
|
325
|
+
return concepts;
|
|
202
326
|
}
|
|
327
|
+
trace?.finishConceptExtraction(text, [], 'invalid', response.content);
|
|
203
328
|
} catch {
|
|
204
|
-
// Try to extract from markdown
|
|
329
|
+
// Try to extract an array from a prose/markdown wrapper.
|
|
205
330
|
const match = text.match(/\[([^\]]*)\]/);
|
|
206
331
|
if (match) {
|
|
207
332
|
try {
|
|
208
|
-
|
|
333
|
+
const parsed = JSON.parse(`[${match[1]}]`);
|
|
334
|
+
if (Array.isArray(parsed)) {
|
|
335
|
+
const traceValues = parsed.filter((value): value is string => typeof value === 'string');
|
|
336
|
+
trace?.finishConceptExtraction(text, traceValues, 'array-extraction', response.content);
|
|
337
|
+
// Preserve the historical malformed-wrapper behavior exactly: mixed
|
|
338
|
+
// arrays proceed to queryCandidates(), which then fails open rather
|
|
339
|
+
// than silently becoming a different, partially valid concept set.
|
|
340
|
+
return parsed as string[];
|
|
341
|
+
}
|
|
209
342
|
} catch { /* fall through */ }
|
|
210
343
|
}
|
|
344
|
+
trace?.finishConceptExtraction(text, [], 'invalid', response.content);
|
|
211
345
|
}
|
|
212
346
|
return [];
|
|
213
347
|
}
|
|
@@ -246,32 +380,47 @@ export class RetrievalModule implements Module {
|
|
|
246
380
|
}
|
|
247
381
|
|
|
248
382
|
/**
|
|
249
|
-
* Step 3: Use
|
|
383
|
+
* Step 3: Use the configured retrieval model to validate which candidates are actually relevant.
|
|
250
384
|
*/
|
|
251
|
-
private async validateRelevance(
|
|
385
|
+
private async validateRelevance(
|
|
386
|
+
recentContext: string,
|
|
387
|
+
candidates: Lesson[],
|
|
388
|
+
trace?: RetrievalTraceRun,
|
|
389
|
+
): Promise<Lesson[]> {
|
|
252
390
|
if (candidates.length <= 3) {
|
|
253
|
-
// If only a few candidates, skip validation — they're probably all relevant
|
|
391
|
+
// If only a few candidates, skip validation — they're probably all relevant.
|
|
392
|
+
trace?.recordRelevanceSkipped('three-or-fewer-candidates');
|
|
254
393
|
return candidates;
|
|
255
394
|
}
|
|
256
395
|
|
|
257
396
|
const model = this.config.retrievalModel ?? 'claude-haiku-4-5-20251001';
|
|
397
|
+
const providerParams = this.retrievalProviderParams();
|
|
258
398
|
|
|
259
399
|
const candidateList = candidates.map(l =>
|
|
260
400
|
`[${l.id}] (${l.confidence.toFixed(2)}) ${l.content}`
|
|
261
401
|
).join('\n');
|
|
402
|
+
const input = `Current conversation:\n${recentContext}\n\nCandidate lessons:\n${candidateList}`;
|
|
262
403
|
|
|
263
404
|
const request: NormalizedRequest = {
|
|
264
405
|
messages: [
|
|
265
406
|
{
|
|
266
407
|
participant: 'user',
|
|
267
|
-
content: [{ type: 'text', text:
|
|
408
|
+
content: [{ type: 'text', text: input }],
|
|
268
409
|
},
|
|
269
410
|
],
|
|
270
411
|
system: RELEVANCE_VALIDATION_PROMPT,
|
|
271
412
|
config: { model, maxTokens: 500, temperature: 0 },
|
|
413
|
+
...(providerParams ? { providerParams } : {}),
|
|
272
414
|
};
|
|
273
415
|
|
|
274
|
-
|
|
416
|
+
trace?.startRelevance(RELEVANCE_VALIDATION_PROMPT, input);
|
|
417
|
+
let response;
|
|
418
|
+
try {
|
|
419
|
+
response = await this.config.membrane.complete(request);
|
|
420
|
+
} catch (error) {
|
|
421
|
+
trace?.recordStageError('relevance', error);
|
|
422
|
+
throw error;
|
|
423
|
+
}
|
|
275
424
|
const text = response.content
|
|
276
425
|
.filter((b): b is { type: 'text'; text: string } => b.type === 'text')
|
|
277
426
|
.map(b => b.text)
|
|
@@ -280,38 +429,91 @@ export class RetrievalModule implements Module {
|
|
|
280
429
|
try {
|
|
281
430
|
const parsed = JSON.parse(text);
|
|
282
431
|
if (Array.isArray(parsed)) {
|
|
283
|
-
const
|
|
432
|
+
const parsedIds = parsed.filter((value): value is string => typeof value === 'string');
|
|
433
|
+
trace?.finishRelevance(text, parsedIds, 'json', response.content);
|
|
434
|
+
const relevantIds = new Set(parsedIds);
|
|
284
435
|
return candidates.filter(l => relevantIds.has(l.id));
|
|
285
436
|
}
|
|
286
437
|
} catch {
|
|
287
|
-
//
|
|
288
|
-
return candidates.slice(0, 5);
|
|
438
|
+
// Fall through to confidence-ordered fallback.
|
|
289
439
|
}
|
|
290
|
-
|
|
440
|
+
|
|
441
|
+
const fallback = candidates.slice(0, 5);
|
|
442
|
+
trace?.finishRelevance(text, [], 'fallback', response.content);
|
|
443
|
+
return fallback;
|
|
291
444
|
}
|
|
292
445
|
|
|
293
446
|
// =========================================================================
|
|
294
447
|
// Helpers
|
|
295
448
|
// =========================================================================
|
|
296
449
|
|
|
297
|
-
private getRecentContext():
|
|
450
|
+
private getRecentContext(): RecentRetrievalContext | null {
|
|
298
451
|
if (!this.ctx) return null;
|
|
299
452
|
|
|
300
|
-
// Get the last few messages from the conversation
|
|
453
|
+
// Get the last few messages from the conversation.
|
|
301
454
|
const { messages } = this.ctx.queryMessages({});
|
|
302
455
|
if (messages.length === 0) return null;
|
|
303
456
|
|
|
304
|
-
// Take the last 10 messages for context
|
|
457
|
+
// Take the last 10 messages for context.
|
|
305
458
|
const recent = messages.slice(-10);
|
|
306
|
-
|
|
459
|
+
const text = recent
|
|
307
460
|
.map(m => {
|
|
308
|
-
const
|
|
461
|
+
const messageText = m.content
|
|
309
462
|
.filter((b): b is { type: 'text'; text: string } => b.type === 'text')
|
|
310
463
|
.map(b => b.text)
|
|
311
464
|
.join('\n');
|
|
312
|
-
return `${m.participant}: ${
|
|
465
|
+
return `${m.participant}: ${messageText}`;
|
|
313
466
|
})
|
|
314
467
|
.join('\n\n');
|
|
468
|
+
const messageIds = recent.flatMap(message => {
|
|
469
|
+
try {
|
|
470
|
+
const candidate = (message as unknown as { id?: unknown }).id;
|
|
471
|
+
return typeof candidate === 'string' || typeof candidate === 'number'
|
|
472
|
+
? [String(candidate)]
|
|
473
|
+
: [];
|
|
474
|
+
} catch {
|
|
475
|
+
// Trace-only metadata must never alter retrieval behavior.
|
|
476
|
+
return [];
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
return { text, messageCount: recent.length, messageIds };
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
private safeLessonIds(lessons: Lesson[]): string[] {
|
|
484
|
+
const ids: string[] = [];
|
|
485
|
+
for (const lesson of lessons) {
|
|
486
|
+
try {
|
|
487
|
+
if (typeof lesson.id === 'string') ids.push(lesson.id);
|
|
488
|
+
} catch {
|
|
489
|
+
// Cache provenance is observability metadata; omit unreadable IDs.
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
return ids;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
private safeLessonSnapshots(lessons: Lesson[]): Lesson[] {
|
|
496
|
+
const snapshots: Lesson[] = [];
|
|
497
|
+
for (const item of lessons) {
|
|
498
|
+
try {
|
|
499
|
+
snapshots.push({
|
|
500
|
+
id: item.id,
|
|
501
|
+
content: item.content,
|
|
502
|
+
confidence: item.confidence,
|
|
503
|
+
tags: [...item.tags],
|
|
504
|
+
evidence: [...item.evidence],
|
|
505
|
+
created: item.created,
|
|
506
|
+
updated: item.updated,
|
|
507
|
+
deprecated: item.deprecated,
|
|
508
|
+
...(item.deprecationReason !== undefined
|
|
509
|
+
? { deprecationReason: item.deprecationReason }
|
|
510
|
+
: {}),
|
|
511
|
+
});
|
|
512
|
+
} catch {
|
|
513
|
+
// Trace-only lesson snapshots must not alter retrieval behavior.
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
return snapshots;
|
|
315
517
|
}
|
|
316
518
|
|
|
317
519
|
private hashContext(text: string): string {
|