@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
|
@@ -0,0 +1,904 @@
|
|
|
1
|
+
import type { ContextInjection } from '@animalabs/context-manager';
|
|
2
|
+
import type { Lesson } from './lessons-module.js';
|
|
3
|
+
import type { RetrievalReasoningConfig } from './retrieval-module.js';
|
|
4
|
+
|
|
5
|
+
export const RETRIEVAL_TRACE_SCHEMA_VERSION = 1;
|
|
6
|
+
export const DEFAULT_RETRIEVAL_TRACE_CAPACITY = 100;
|
|
7
|
+
export const DEFAULT_RETRIEVAL_TRACE_BYTE_BUDGET = 8 * 1024 * 1024;
|
|
8
|
+
|
|
9
|
+
const MIN_RETRIEVAL_TRACE_BYTE_BUDGET = 1024;
|
|
10
|
+
const PROVIDER_SERIALIZATION_LIMITS = {
|
|
11
|
+
maxDepth: 8,
|
|
12
|
+
maxNodes: 512,
|
|
13
|
+
maxArrayItems: 64,
|
|
14
|
+
maxObjectKeys: 64,
|
|
15
|
+
maxStringBytes: 16 * 1024,
|
|
16
|
+
maxTotalStringBytes: 128 * 1024,
|
|
17
|
+
} as const;
|
|
18
|
+
|
|
19
|
+
export type RetrievalTraceOutcome =
|
|
20
|
+
| 'not-started'
|
|
21
|
+
| 'no-lessons-module'
|
|
22
|
+
| 'no-eligible-lessons'
|
|
23
|
+
| 'no-recent-context'
|
|
24
|
+
| 'cache-hit'
|
|
25
|
+
| 'no-concepts'
|
|
26
|
+
| 'no-candidates'
|
|
27
|
+
| 'no-relevant-lessons'
|
|
28
|
+
| 'injected'
|
|
29
|
+
| 'error';
|
|
30
|
+
|
|
31
|
+
export interface RetrievalLessonTrace {
|
|
32
|
+
id: string;
|
|
33
|
+
content: string;
|
|
34
|
+
confidence: number;
|
|
35
|
+
tags: string[];
|
|
36
|
+
evidence: string[];
|
|
37
|
+
created: number;
|
|
38
|
+
updated: number;
|
|
39
|
+
deprecated: boolean;
|
|
40
|
+
deprecationReason?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface RetrievalCandidateMatch {
|
|
44
|
+
concept: string;
|
|
45
|
+
keyword: string;
|
|
46
|
+
field: 'content' | 'tag';
|
|
47
|
+
tag?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface RetrievalCandidateTrace extends RetrievalLessonTrace {
|
|
51
|
+
matches: RetrievalCandidateMatch[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface RetrievalStageTrace {
|
|
55
|
+
systemPrompt: string;
|
|
56
|
+
/** Exact user input. Omitted from default HTTP views. */
|
|
57
|
+
input?: string;
|
|
58
|
+
rawOutput?: string;
|
|
59
|
+
/** Provider-returned content blocks, including any opaque/redacted reasoning blocks. */
|
|
60
|
+
responseContent?: unknown[];
|
|
61
|
+
responseContentTruncation?: RetrievalProviderTruncation;
|
|
62
|
+
parsedValues?: string[];
|
|
63
|
+
parseMode?: 'json' | 'array-extraction' | 'fallback' | 'invalid';
|
|
64
|
+
error?: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export type RetrievalProviderTruncationReason =
|
|
68
|
+
| 'max-depth'
|
|
69
|
+
| 'max-nodes'
|
|
70
|
+
| 'max-array-items'
|
|
71
|
+
| 'max-object-keys'
|
|
72
|
+
| 'max-string-bytes'
|
|
73
|
+
| 'max-total-string-bytes'
|
|
74
|
+
| 'non-json-value'
|
|
75
|
+
| 'serialization-error';
|
|
76
|
+
|
|
77
|
+
export interface RetrievalProviderTruncation {
|
|
78
|
+
truncated: true;
|
|
79
|
+
reasons: RetrievalProviderTruncationReason[];
|
|
80
|
+
retainedNodes: number;
|
|
81
|
+
retainedStringBytes: number;
|
|
82
|
+
limits: typeof PROVIDER_SERIALIZATION_LIMITS;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface RetrievalTrace {
|
|
86
|
+
schemaVersion: 1;
|
|
87
|
+
id: number;
|
|
88
|
+
startedAt: string;
|
|
89
|
+
completedAt?: string;
|
|
90
|
+
durationMs?: number;
|
|
91
|
+
agentName: string;
|
|
92
|
+
config: {
|
|
93
|
+
model: string;
|
|
94
|
+
requestedReasoning?: RetrievalReasoningConfig;
|
|
95
|
+
providerParams?: Record<string, unknown>;
|
|
96
|
+
providerParamsTruncation?: RetrievalProviderTruncation;
|
|
97
|
+
minConfidence: number;
|
|
98
|
+
maxCandidates: number;
|
|
99
|
+
maxInjectedLessons: number;
|
|
100
|
+
};
|
|
101
|
+
context?: {
|
|
102
|
+
hash: string;
|
|
103
|
+
messageCount: number;
|
|
104
|
+
messageIds: string[];
|
|
105
|
+
/** Rendered recent conversation. Omitted from default HTTP views. */
|
|
106
|
+
input?: string;
|
|
107
|
+
};
|
|
108
|
+
cache: {
|
|
109
|
+
hit: boolean;
|
|
110
|
+
sourceTraceId?: number;
|
|
111
|
+
sourceTraceEvicted?: boolean;
|
|
112
|
+
sourceTraceTruncated?: boolean;
|
|
113
|
+
};
|
|
114
|
+
conceptExtraction?: RetrievalStageTrace;
|
|
115
|
+
candidates: RetrievalCandidateTrace[];
|
|
116
|
+
relevance?: RetrievalStageTrace & {
|
|
117
|
+
ran: boolean;
|
|
118
|
+
skippedReason?: string;
|
|
119
|
+
};
|
|
120
|
+
relevantLessonIds: string[];
|
|
121
|
+
injected: {
|
|
122
|
+
lessonIds: string[];
|
|
123
|
+
lessons: RetrievalLessonTrace[];
|
|
124
|
+
namespace?: string;
|
|
125
|
+
position?: ContextInjection['position'];
|
|
126
|
+
block?: string;
|
|
127
|
+
};
|
|
128
|
+
outcome?: RetrievalTraceOutcome;
|
|
129
|
+
error?: string;
|
|
130
|
+
truncation?: {
|
|
131
|
+
truncated: true;
|
|
132
|
+
kind: 'tombstone';
|
|
133
|
+
reason: 'trace-exceeded-byte-budget';
|
|
134
|
+
originalBytes: number;
|
|
135
|
+
byteBudget: number;
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export interface RetrievalTraceListOptions {
|
|
140
|
+
limit?: number;
|
|
141
|
+
includeInputs?: boolean;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Structural interface used by WebUiModule to avoid importing RetrievalModule. */
|
|
145
|
+
export interface RetrievalTraceSource {
|
|
146
|
+
getRetrievalTraces(options?: RetrievalTraceListOptions): RetrievalTrace[];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export interface RetrievalTraceBeginOptions {
|
|
150
|
+
agentName: string;
|
|
151
|
+
model: string;
|
|
152
|
+
requestedReasoning?: RetrievalReasoningConfig;
|
|
153
|
+
providerParams?: Record<string, unknown>;
|
|
154
|
+
minConfidence: number;
|
|
155
|
+
maxCandidates: number;
|
|
156
|
+
maxInjectedLessons: number;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export interface RetrievalTraceStoreOptions {
|
|
160
|
+
capacity?: number;
|
|
161
|
+
byteBudget?: number;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function errorMessage(error: unknown): string {
|
|
165
|
+
try {
|
|
166
|
+
const value = error instanceof Error ? error.message : error;
|
|
167
|
+
return truncateUtf8(
|
|
168
|
+
typeof value === 'string' ? value : String(value),
|
|
169
|
+
PROVIDER_SERIALIZATION_LIMITS.maxStringBytes,
|
|
170
|
+
);
|
|
171
|
+
} catch {
|
|
172
|
+
return 'unavailable error';
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
interface ProviderSerializationState {
|
|
177
|
+
nodes: number;
|
|
178
|
+
stringBytes: number;
|
|
179
|
+
reasons: Set<RetrievalProviderTruncationReason>;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const textEncoder = new TextEncoder();
|
|
183
|
+
const arrayBufferByteLengthGetter = Object.getOwnPropertyDescriptor(
|
|
184
|
+
ArrayBuffer.prototype,
|
|
185
|
+
'byteLength',
|
|
186
|
+
)?.get;
|
|
187
|
+
const typedArrayByteLengthGetter = Object.getOwnPropertyDescriptor(
|
|
188
|
+
Object.getPrototypeOf(Uint8Array.prototype) as object,
|
|
189
|
+
'byteLength',
|
|
190
|
+
)?.get;
|
|
191
|
+
const dataViewByteLengthGetter = Object.getOwnPropertyDescriptor(
|
|
192
|
+
DataView.prototype,
|
|
193
|
+
'byteLength',
|
|
194
|
+
)?.get;
|
|
195
|
+
const mapSizeGetter = Object.getOwnPropertyDescriptor(Map.prototype, 'size')?.get;
|
|
196
|
+
const setSizeGetter = Object.getOwnPropertyDescriptor(Set.prototype, 'size')?.get;
|
|
197
|
+
const bigIntToString = BigInt.prototype.toString;
|
|
198
|
+
const dateToISOString = Date.prototype.toISOString;
|
|
199
|
+
|
|
200
|
+
function utf8Bytes(value: string): number {
|
|
201
|
+
return textEncoder.encode(value).byteLength;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function truncateUtf8(value: string, maxBytes: number): string {
|
|
205
|
+
if (maxBytes <= 0) return '';
|
|
206
|
+
if (utf8Bytes(value) <= maxBytes) return value;
|
|
207
|
+
const suffix = '...[truncated]';
|
|
208
|
+
const suffixBytes = utf8Bytes(suffix);
|
|
209
|
+
if (suffixBytes >= maxBytes) return suffix.slice(0, maxBytes);
|
|
210
|
+
|
|
211
|
+
let low = 0;
|
|
212
|
+
let high = value.length;
|
|
213
|
+
const contentBudget = maxBytes - suffixBytes;
|
|
214
|
+
while (low < high) {
|
|
215
|
+
const mid = Math.ceil((low + high) / 2);
|
|
216
|
+
if (utf8Bytes(value.slice(0, mid)) <= contentBudget) low = mid;
|
|
217
|
+
else high = mid - 1;
|
|
218
|
+
}
|
|
219
|
+
if (low > 0) {
|
|
220
|
+
const code = value.charCodeAt(low - 1);
|
|
221
|
+
if (code >= 0xd800 && code <= 0xdbff) low--;
|
|
222
|
+
}
|
|
223
|
+
return value.slice(0, low) + suffix;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function boundedProviderString(value: string, state: ProviderSerializationState): string {
|
|
227
|
+
const valueBytes = utf8Bytes(value);
|
|
228
|
+
const remainingTotal = Math.max(
|
|
229
|
+
0,
|
|
230
|
+
PROVIDER_SERIALIZATION_LIMITS.maxTotalStringBytes - state.stringBytes,
|
|
231
|
+
);
|
|
232
|
+
if (valueBytes > PROVIDER_SERIALIZATION_LIMITS.maxStringBytes) {
|
|
233
|
+
state.reasons.add('max-string-bytes');
|
|
234
|
+
}
|
|
235
|
+
if (valueBytes > remainingTotal) state.reasons.add('max-total-string-bytes');
|
|
236
|
+
const retained = truncateUtf8(
|
|
237
|
+
value,
|
|
238
|
+
Math.min(PROVIDER_SERIALIZATION_LIMITS.maxStringBytes, remainingTotal),
|
|
239
|
+
);
|
|
240
|
+
state.stringBytes += utf8Bytes(retained);
|
|
241
|
+
return retained;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function intrinsicNonnegativeInteger(
|
|
245
|
+
value: object,
|
|
246
|
+
getter: (() => unknown) | undefined,
|
|
247
|
+
): number | undefined {
|
|
248
|
+
if (!getter) return undefined;
|
|
249
|
+
try {
|
|
250
|
+
const metadata = Reflect.apply(getter, value, []);
|
|
251
|
+
return typeof metadata === 'number'
|
|
252
|
+
&& Number.isSafeInteger(metadata)
|
|
253
|
+
&& metadata >= 0
|
|
254
|
+
? metadata
|
|
255
|
+
: undefined;
|
|
256
|
+
} catch {
|
|
257
|
+
return undefined;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function jsonSafeValue(
|
|
262
|
+
value: unknown,
|
|
263
|
+
state: ProviderSerializationState,
|
|
264
|
+
ancestors = new WeakSet<object>(),
|
|
265
|
+
depth = 0,
|
|
266
|
+
): unknown {
|
|
267
|
+
if (state.nodes >= PROVIDER_SERIALIZATION_LIMITS.maxNodes) {
|
|
268
|
+
state.reasons.add('max-nodes');
|
|
269
|
+
return { type: 'truncated', reason: 'max-nodes', unavailable: true };
|
|
270
|
+
}
|
|
271
|
+
state.nodes++;
|
|
272
|
+
|
|
273
|
+
if (value === null || typeof value === 'boolean') return value;
|
|
274
|
+
if (typeof value === 'string') return boundedProviderString(value, state);
|
|
275
|
+
if (typeof value === 'number') {
|
|
276
|
+
return Number.isFinite(value)
|
|
277
|
+
? value
|
|
278
|
+
: { type: 'number', value: String(value), unavailable: true };
|
|
279
|
+
}
|
|
280
|
+
if (typeof value === 'bigint') {
|
|
281
|
+
return {
|
|
282
|
+
type: 'bigint',
|
|
283
|
+
value: boundedProviderString(Reflect.apply(bigIntToString, value, []), state),
|
|
284
|
+
unavailable: true,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
if (typeof value === 'undefined' || typeof value === 'symbol' || typeof value === 'function') {
|
|
288
|
+
return { type: typeof value, unavailable: true };
|
|
289
|
+
}
|
|
290
|
+
if (depth >= PROVIDER_SERIALIZATION_LIMITS.maxDepth) {
|
|
291
|
+
state.reasons.add('max-depth');
|
|
292
|
+
return { type: 'max-depth', unavailable: true };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const object = value as object;
|
|
296
|
+
if (value instanceof ArrayBuffer) {
|
|
297
|
+
state.reasons.add('non-json-value');
|
|
298
|
+
const byteLength = intrinsicNonnegativeInteger(value, arrayBufferByteLengthGetter);
|
|
299
|
+
return {
|
|
300
|
+
type: 'array-buffer',
|
|
301
|
+
...(byteLength !== undefined ? { byteLength } : {}),
|
|
302
|
+
unavailable: true,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
if (ArrayBuffer.isView(value)) {
|
|
306
|
+
state.reasons.add('non-json-value');
|
|
307
|
+
const getter = value instanceof DataView
|
|
308
|
+
? dataViewByteLengthGetter
|
|
309
|
+
: typedArrayByteLengthGetter;
|
|
310
|
+
const byteLength = intrinsicNonnegativeInteger(value, getter);
|
|
311
|
+
return {
|
|
312
|
+
type: 'array-buffer-view',
|
|
313
|
+
...(byteLength !== undefined ? { byteLength } : {}),
|
|
314
|
+
unavailable: true,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
if (value instanceof Map) {
|
|
318
|
+
state.reasons.add('non-json-value');
|
|
319
|
+
const size = intrinsicNonnegativeInteger(value, mapSizeGetter);
|
|
320
|
+
return { type: 'map', ...(size !== undefined ? { size } : {}), unavailable: true };
|
|
321
|
+
}
|
|
322
|
+
if (value instanceof Set) {
|
|
323
|
+
state.reasons.add('non-json-value');
|
|
324
|
+
const size = intrinsicNonnegativeInteger(value, setSizeGetter);
|
|
325
|
+
return { type: 'set', ...(size !== undefined ? { size } : {}), unavailable: true };
|
|
326
|
+
}
|
|
327
|
+
if (ancestors.has(object)) return { type: 'circular', unavailable: true };
|
|
328
|
+
ancestors.add(object);
|
|
329
|
+
try {
|
|
330
|
+
if (Array.isArray(value)) {
|
|
331
|
+
const result: unknown[] = [];
|
|
332
|
+
const retainedLength = Math.min(value.length, PROVIDER_SERIALIZATION_LIMITS.maxArrayItems);
|
|
333
|
+
if (value.length > retainedLength) state.reasons.add('max-array-items');
|
|
334
|
+
for (let i = 0; i < retainedLength; i++) {
|
|
335
|
+
if (state.nodes >= PROVIDER_SERIALIZATION_LIMITS.maxNodes) {
|
|
336
|
+
state.reasons.add('max-nodes');
|
|
337
|
+
result.push({ type: 'truncated', reason: 'max-nodes', unavailable: true });
|
|
338
|
+
break;
|
|
339
|
+
}
|
|
340
|
+
result.push(jsonSafeValue(value[i], state, ancestors, depth + 1));
|
|
341
|
+
}
|
|
342
|
+
if (value.length > retainedLength) {
|
|
343
|
+
result.push({
|
|
344
|
+
type: 'truncated',
|
|
345
|
+
reason: 'max-array-items',
|
|
346
|
+
omittedItems: value.length - retainedLength,
|
|
347
|
+
unavailable: true,
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
return result;
|
|
351
|
+
}
|
|
352
|
+
if (value instanceof Date) {
|
|
353
|
+
try {
|
|
354
|
+
return boundedProviderString(Reflect.apply(dateToISOString, value, []), state);
|
|
355
|
+
} catch {
|
|
356
|
+
return { type: 'date', unavailable: true };
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const result: Record<string, unknown> = Object.create(null) as Record<string, unknown>;
|
|
361
|
+
const retainedKeys: string[] = [];
|
|
362
|
+
let omittedKeys = false;
|
|
363
|
+
try {
|
|
364
|
+
for (const key in object as Record<string, unknown>) {
|
|
365
|
+
if (!Object.prototype.hasOwnProperty.call(object, key)) continue;
|
|
366
|
+
if (retainedKeys.length >= PROVIDER_SERIALIZATION_LIMITS.maxObjectKeys) {
|
|
367
|
+
omittedKeys = true;
|
|
368
|
+
break;
|
|
369
|
+
}
|
|
370
|
+
retainedKeys.push(key);
|
|
371
|
+
}
|
|
372
|
+
} catch {
|
|
373
|
+
return { type: 'unreadable', unavailable: true };
|
|
374
|
+
}
|
|
375
|
+
if (omittedKeys) state.reasons.add('max-object-keys');
|
|
376
|
+
for (const originalKey of retainedKeys) {
|
|
377
|
+
if (state.nodes >= PROVIDER_SERIALIZATION_LIMITS.maxNodes) {
|
|
378
|
+
state.reasons.add('max-nodes');
|
|
379
|
+
result.__truncated__ = { type: 'truncated', reason: 'max-nodes', unavailable: true };
|
|
380
|
+
break;
|
|
381
|
+
}
|
|
382
|
+
let key = boundedProviderString(originalKey, state);
|
|
383
|
+
for (let suffix = 2; Object.prototype.hasOwnProperty.call(result, key); suffix++) {
|
|
384
|
+
key = `${truncateUtf8(key, 256)}#${suffix}`;
|
|
385
|
+
}
|
|
386
|
+
try {
|
|
387
|
+
result[key] = jsonSafeValue(
|
|
388
|
+
(object as Record<string, unknown>)[originalKey],
|
|
389
|
+
state,
|
|
390
|
+
ancestors,
|
|
391
|
+
depth + 1,
|
|
392
|
+
);
|
|
393
|
+
} catch {
|
|
394
|
+
result[key] = { type: 'unreadable', unavailable: true };
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
if (omittedKeys) {
|
|
398
|
+
result.__truncated__ = {
|
|
399
|
+
type: 'truncated',
|
|
400
|
+
reason: 'max-object-keys',
|
|
401
|
+
omittedKeysAtLeast: 1,
|
|
402
|
+
unavailable: true,
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
return result;
|
|
406
|
+
} finally {
|
|
407
|
+
ancestors.delete(object);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function snapshotProviderValue(value: unknown): {
|
|
412
|
+
value: unknown;
|
|
413
|
+
truncation?: RetrievalProviderTruncation;
|
|
414
|
+
} {
|
|
415
|
+
const state: ProviderSerializationState = { nodes: 0, stringBytes: 0, reasons: new Set() };
|
|
416
|
+
try {
|
|
417
|
+
const snapshot = jsonSafeValue(value, state);
|
|
418
|
+
const reasons = [...state.reasons];
|
|
419
|
+
return {
|
|
420
|
+
value: snapshot,
|
|
421
|
+
...(reasons.length > 0 ? {
|
|
422
|
+
truncation: {
|
|
423
|
+
truncated: true,
|
|
424
|
+
reasons,
|
|
425
|
+
retainedNodes: state.nodes,
|
|
426
|
+
retainedStringBytes: state.stringBytes,
|
|
427
|
+
limits: PROVIDER_SERIALIZATION_LIMITS,
|
|
428
|
+
},
|
|
429
|
+
} : {}),
|
|
430
|
+
};
|
|
431
|
+
} catch {
|
|
432
|
+
return {
|
|
433
|
+
value: { type: 'unreadable', unavailable: true },
|
|
434
|
+
truncation: {
|
|
435
|
+
truncated: true,
|
|
436
|
+
reasons: ['serialization-error'],
|
|
437
|
+
retainedNodes: state.nodes,
|
|
438
|
+
retainedStringBytes: state.stringBytes,
|
|
439
|
+
limits: PROVIDER_SERIALIZATION_LIMITS,
|
|
440
|
+
},
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function snapshotResponseContent(content: readonly unknown[]): {
|
|
446
|
+
content: unknown[];
|
|
447
|
+
truncation?: RetrievalProviderTruncation;
|
|
448
|
+
} {
|
|
449
|
+
const snapshot = snapshotProviderValue(content);
|
|
450
|
+
return {
|
|
451
|
+
content: Array.isArray(snapshot.value) ? snapshot.value : [snapshot.value],
|
|
452
|
+
...(snapshot.truncation ? { truncation: snapshot.truncation } : {}),
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function lessonSnapshot(lesson: Lesson): RetrievalLessonTrace {
|
|
457
|
+
return {
|
|
458
|
+
id: lesson.id,
|
|
459
|
+
content: lesson.content,
|
|
460
|
+
confidence: lesson.confidence,
|
|
461
|
+
tags: [...lesson.tags],
|
|
462
|
+
evidence: [...lesson.evidence],
|
|
463
|
+
created: lesson.created,
|
|
464
|
+
updated: lesson.updated,
|
|
465
|
+
deprecated: lesson.deprecated,
|
|
466
|
+
...(lesson.deprecationReason !== undefined
|
|
467
|
+
? { deprecationReason: lesson.deprecationReason }
|
|
468
|
+
: {}),
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function candidateMatches(concepts: string[], lesson: Lesson): RetrievalCandidateMatch[] {
|
|
473
|
+
const matches: RetrievalCandidateMatch[] = [];
|
|
474
|
+
const content = lesson.content.toLowerCase();
|
|
475
|
+
const tags = lesson.tags.map(tag => ({ original: tag, lower: tag.toLowerCase() }));
|
|
476
|
+
|
|
477
|
+
for (const concept of concepts) {
|
|
478
|
+
for (const keyword of concept.toLowerCase().split(/\s+/)) {
|
|
479
|
+
if (content.includes(keyword)) {
|
|
480
|
+
matches.push({ concept, keyword, field: 'content' });
|
|
481
|
+
}
|
|
482
|
+
for (const tag of tags) {
|
|
483
|
+
if (tag.lower.includes(keyword)) {
|
|
484
|
+
matches.push({ concept, keyword, field: 'tag', tag: tag.original });
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
return matches;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Mutable handle for one retrieval run. Every mutation is guarded so tracing
|
|
495
|
+
* can never make retrieval fail; a malformed trace is less important than the
|
|
496
|
+
* inference it observes.
|
|
497
|
+
*/
|
|
498
|
+
export class RetrievalTraceRun {
|
|
499
|
+
private finished = false;
|
|
500
|
+
|
|
501
|
+
constructor(
|
|
502
|
+
private readonly store: RetrievalTraceStore,
|
|
503
|
+
private readonly trace: RetrievalTrace,
|
|
504
|
+
) {}
|
|
505
|
+
|
|
506
|
+
get id(): number {
|
|
507
|
+
return this.trace.id;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
private update(fn: (trace: RetrievalTrace) => void): void {
|
|
511
|
+
if (this.finished) return;
|
|
512
|
+
try {
|
|
513
|
+
this.store.update(this.trace, fn);
|
|
514
|
+
} catch {
|
|
515
|
+
// Observability is strictly fail-open.
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
setContext(hash: string, input: string, messageCount: number, messageIds: string[]): void {
|
|
520
|
+
this.update(trace => {
|
|
521
|
+
trace.context = { hash, input, messageCount, messageIds: [...messageIds] };
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
recordCacheHit(
|
|
526
|
+
sourceTraceId: number | undefined,
|
|
527
|
+
lessonIds: string[],
|
|
528
|
+
lessons: Lesson[],
|
|
529
|
+
injections: ContextInjection[],
|
|
530
|
+
): void {
|
|
531
|
+
this.update(trace => {
|
|
532
|
+
trace.cache = { hit: true };
|
|
533
|
+
if (sourceTraceId !== undefined) {
|
|
534
|
+
const sourceStatus = this.store.sourceStatus(sourceTraceId);
|
|
535
|
+
if (sourceStatus === 'exact') trace.cache.sourceTraceId = sourceTraceId;
|
|
536
|
+
else if (sourceStatus === 'truncated') {
|
|
537
|
+
trace.cache.sourceTraceId = sourceTraceId;
|
|
538
|
+
trace.cache.sourceTraceTruncated = true;
|
|
539
|
+
} else trace.cache.sourceTraceEvicted = true;
|
|
540
|
+
}
|
|
541
|
+
trace.injected.lessonIds = [...lessonIds];
|
|
542
|
+
trace.injected.lessons = lessons.map(lessonSnapshot);
|
|
543
|
+
recordInjectionShape(trace, injections);
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
startConceptExtraction(systemPrompt: string, input: string): void {
|
|
548
|
+
this.update(trace => {
|
|
549
|
+
trace.conceptExtraction = { systemPrompt, input };
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
finishConceptExtraction(
|
|
554
|
+
rawOutput: string,
|
|
555
|
+
parsedValues: string[],
|
|
556
|
+
parseMode: RetrievalStageTrace['parseMode'],
|
|
557
|
+
responseContent: readonly unknown[] = [],
|
|
558
|
+
): void {
|
|
559
|
+
this.update(trace => {
|
|
560
|
+
const snapshot = snapshotResponseContent(responseContent);
|
|
561
|
+
if (!trace.conceptExtraction) trace.conceptExtraction = { systemPrompt: '' };
|
|
562
|
+
trace.conceptExtraction.rawOutput = rawOutput;
|
|
563
|
+
trace.conceptExtraction.responseContent = snapshot.content;
|
|
564
|
+
if (snapshot.truncation) {
|
|
565
|
+
trace.conceptExtraction.responseContentTruncation = snapshot.truncation;
|
|
566
|
+
}
|
|
567
|
+
trace.conceptExtraction.parsedValues = [...parsedValues];
|
|
568
|
+
trace.conceptExtraction.parseMode = parseMode;
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
recordCandidates(concepts: string[], lessons: Lesson[]): void {
|
|
573
|
+
this.update(trace => {
|
|
574
|
+
trace.candidates = lessons.map(lesson => ({
|
|
575
|
+
...lessonSnapshot(lesson),
|
|
576
|
+
matches: candidateMatches(concepts, lesson),
|
|
577
|
+
}));
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
recordRelevanceSkipped(reason: string): void {
|
|
582
|
+
this.update(trace => {
|
|
583
|
+
trace.relevance = {
|
|
584
|
+
ran: false,
|
|
585
|
+
skippedReason: reason,
|
|
586
|
+
systemPrompt: '',
|
|
587
|
+
};
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
startRelevance(systemPrompt: string, input: string): void {
|
|
592
|
+
this.update(trace => {
|
|
593
|
+
trace.relevance = { ran: true, systemPrompt, input };
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
finishRelevance(
|
|
598
|
+
rawOutput: string,
|
|
599
|
+
parsedIds: string[],
|
|
600
|
+
parseMode: RetrievalStageTrace['parseMode'],
|
|
601
|
+
responseContent: readonly unknown[] = [],
|
|
602
|
+
): void {
|
|
603
|
+
this.update(trace => {
|
|
604
|
+
const snapshot = snapshotResponseContent(responseContent);
|
|
605
|
+
if (!trace.relevance) trace.relevance = { ran: true, systemPrompt: '' };
|
|
606
|
+
trace.relevance.rawOutput = rawOutput;
|
|
607
|
+
trace.relevance.responseContent = snapshot.content;
|
|
608
|
+
if (snapshot.truncation) trace.relevance.responseContentTruncation = snapshot.truncation;
|
|
609
|
+
trace.relevance.parsedValues = [...parsedIds];
|
|
610
|
+
trace.relevance.parseMode = parseMode;
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
recordRelevant(lessons: Lesson[]): void {
|
|
615
|
+
this.update(trace => {
|
|
616
|
+
trace.relevantLessonIds = lessons.map(lesson => lesson.id);
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
recordInjection(lessons: Lesson[], injections: ContextInjection[]): void {
|
|
621
|
+
this.update(trace => {
|
|
622
|
+
trace.injected = {
|
|
623
|
+
lessonIds: lessons.map(lesson => lesson.id),
|
|
624
|
+
lessons: lessons.map(lessonSnapshot),
|
|
625
|
+
};
|
|
626
|
+
recordInjectionShape(trace, injections);
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
recordStageError(stage: 'conceptExtraction' | 'relevance', error: unknown): void {
|
|
631
|
+
this.update(trace => {
|
|
632
|
+
const existing = trace[stage];
|
|
633
|
+
if (stage === 'relevance') {
|
|
634
|
+
trace.relevance = {
|
|
635
|
+
ran: true,
|
|
636
|
+
systemPrompt: existing?.systemPrompt ?? '',
|
|
637
|
+
...(existing?.input ? { input: existing.input } : {}),
|
|
638
|
+
error: errorMessage(error),
|
|
639
|
+
};
|
|
640
|
+
} else {
|
|
641
|
+
trace.conceptExtraction = {
|
|
642
|
+
systemPrompt: existing?.systemPrompt ?? '',
|
|
643
|
+
...(existing?.input ? { input: existing.input } : {}),
|
|
644
|
+
error: errorMessage(error),
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
finish(outcome: RetrievalTraceOutcome, error?: unknown): void {
|
|
651
|
+
if (this.finished) return;
|
|
652
|
+
this.finished = true;
|
|
653
|
+
try {
|
|
654
|
+
this.store.finish(this.trace, outcome, error);
|
|
655
|
+
} catch {
|
|
656
|
+
// Observability is strictly fail-open.
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
export class RetrievalTraceStore {
|
|
662
|
+
private readonly traces: RetrievalTrace[] = [];
|
|
663
|
+
private readonly sizes = new Map<RetrievalTrace, number>();
|
|
664
|
+
private readonly capacity: number;
|
|
665
|
+
private readonly byteBudget: number;
|
|
666
|
+
private payloadBytes = 0;
|
|
667
|
+
private nextId = 1;
|
|
668
|
+
|
|
669
|
+
constructor(options: RetrievalTraceStoreOptions | number = {}) {
|
|
670
|
+
const normalized = typeof options === 'number' ? { capacity: options } : options;
|
|
671
|
+
const requestedCapacity = normalized.capacity ?? DEFAULT_RETRIEVAL_TRACE_CAPACITY;
|
|
672
|
+
const requestedByteBudget = normalized.byteBudget ?? DEFAULT_RETRIEVAL_TRACE_BYTE_BUDGET;
|
|
673
|
+
this.capacity = Number.isFinite(requestedCapacity)
|
|
674
|
+
? Math.max(1, Math.min(DEFAULT_RETRIEVAL_TRACE_CAPACITY, Math.trunc(requestedCapacity)))
|
|
675
|
+
: DEFAULT_RETRIEVAL_TRACE_CAPACITY;
|
|
676
|
+
if (!Number.isFinite(requestedByteBudget)
|
|
677
|
+
|| requestedByteBudget < MIN_RETRIEVAL_TRACE_BYTE_BUDGET) {
|
|
678
|
+
throw new RangeError(
|
|
679
|
+
`Retrieval trace byteBudget must be at least ${MIN_RETRIEVAL_TRACE_BYTE_BUDGET}.`,
|
|
680
|
+
);
|
|
681
|
+
}
|
|
682
|
+
this.byteBudget = Math.trunc(requestedByteBudget);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
begin(options: RetrievalTraceBeginOptions): RetrievalTraceRun {
|
|
686
|
+
const providerParams = options.providerParams
|
|
687
|
+
? snapshotProviderValue(options.providerParams)
|
|
688
|
+
: undefined;
|
|
689
|
+
const trace: RetrievalTrace = {
|
|
690
|
+
schemaVersion: RETRIEVAL_TRACE_SCHEMA_VERSION,
|
|
691
|
+
id: this.nextId++,
|
|
692
|
+
startedAt: new Date().toISOString(),
|
|
693
|
+
agentName: options.agentName,
|
|
694
|
+
config: {
|
|
695
|
+
model: options.model,
|
|
696
|
+
...(options.requestedReasoning
|
|
697
|
+
? { requestedReasoning: { ...options.requestedReasoning } }
|
|
698
|
+
: {}),
|
|
699
|
+
...(providerParams ? {
|
|
700
|
+
providerParams: providerParams.value as Record<string, unknown>,
|
|
701
|
+
...(providerParams.truncation
|
|
702
|
+
? { providerParamsTruncation: providerParams.truncation }
|
|
703
|
+
: {}),
|
|
704
|
+
} : {}),
|
|
705
|
+
minConfidence: options.minConfidence,
|
|
706
|
+
maxCandidates: options.maxCandidates,
|
|
707
|
+
maxInjectedLessons: options.maxInjectedLessons,
|
|
708
|
+
},
|
|
709
|
+
cache: { hit: false },
|
|
710
|
+
candidates: [],
|
|
711
|
+
relevantLessonIds: [],
|
|
712
|
+
injected: { lessonIds: [], lessons: [] },
|
|
713
|
+
};
|
|
714
|
+
this.commit(trace);
|
|
715
|
+
return new RetrievalTraceRun(this, trace);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
has(id: number): boolean {
|
|
719
|
+
return this.traces.some(trace => trace.id === id);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
sourceStatus(id: number): 'exact' | 'truncated' | 'evicted' {
|
|
723
|
+
const source = this.traces.find(trace => trace.id === id);
|
|
724
|
+
if (!source) return 'evicted';
|
|
725
|
+
return source.truncation ? 'truncated' : 'exact';
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
get retainedBytes(): number {
|
|
729
|
+
return this.totalEncodedBytes();
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
update(trace: RetrievalTrace, fn: (trace: RetrievalTrace) => void): void {
|
|
733
|
+
if (!this.sizes.has(trace) || trace.truncation) return;
|
|
734
|
+
try {
|
|
735
|
+
fn(trace);
|
|
736
|
+
} finally {
|
|
737
|
+
this.enforceBounds(trace);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
finish(trace: RetrievalTrace, outcome: RetrievalTraceOutcome, error?: unknown): void {
|
|
742
|
+
if (!this.sizes.has(trace)) return;
|
|
743
|
+
try {
|
|
744
|
+
trace.outcome = outcome;
|
|
745
|
+
if (error !== undefined && !trace.truncation) trace.error = errorMessage(error);
|
|
746
|
+
trace.completedAt = new Date().toISOString();
|
|
747
|
+
trace.durationMs = Math.max(0, Date.now() - Date.parse(trace.startedAt));
|
|
748
|
+
} finally {
|
|
749
|
+
this.enforceBounds(trace);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
private commit(trace: RetrievalTrace): void {
|
|
754
|
+
this.traces.push(trace);
|
|
755
|
+
const size = encodedTraceBytes(trace);
|
|
756
|
+
this.sizes.set(trace, size);
|
|
757
|
+
this.payloadBytes += size;
|
|
758
|
+
this.enforceBounds(trace);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
private enforceBounds(mutatedTrace?: RetrievalTrace): void {
|
|
762
|
+
if (mutatedTrace && this.sizes.has(mutatedTrace)) {
|
|
763
|
+
this.refreshSize(mutatedTrace);
|
|
764
|
+
}
|
|
765
|
+
for (const retained of [...this.traces]) {
|
|
766
|
+
const size = this.sizes.get(retained) ?? Number.POSITIVE_INFINITY;
|
|
767
|
+
if (size > this.byteBudget && !retained.truncation) {
|
|
768
|
+
this.replaceWithTombstone(retained, size);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
while (this.traces.length > this.capacity || this.totalEncodedBytes() > this.byteBudget) {
|
|
773
|
+
const evicted = new Set<number>();
|
|
774
|
+
do {
|
|
775
|
+
const oldest = this.traces.shift();
|
|
776
|
+
if (!oldest) break;
|
|
777
|
+
const evictedId = oldest.id;
|
|
778
|
+
evicted.add(evictedId);
|
|
779
|
+
const size = this.sizes.get(oldest) ?? 0;
|
|
780
|
+
this.sizes.delete(oldest);
|
|
781
|
+
if (Number.isFinite(size)) this.payloadBytes -= size;
|
|
782
|
+
else this.recalculatePayloadBytes();
|
|
783
|
+
// A still-running RetrievalTraceRun may retain this object. Strip its
|
|
784
|
+
// payload as part of eviction so concurrent active runs cannot bypass
|
|
785
|
+
// the store's memory bound; the ID remains available for provenance.
|
|
786
|
+
for (const key of Object.keys(oldest)) Reflect.deleteProperty(oldest, key);
|
|
787
|
+
Object.assign(oldest, { id: evictedId });
|
|
788
|
+
} while (this.traces.length > this.capacity || this.totalEncodedBytes() > this.byteBudget);
|
|
789
|
+
if (evicted.size === 0) break;
|
|
790
|
+
this.rewriteEvictedProvenance(evicted);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
private replaceWithTombstone(trace: RetrievalTrace, originalBytes: number): void {
|
|
795
|
+
const sourceId = trace.id;
|
|
796
|
+
const tombstone: RetrievalTrace = {
|
|
797
|
+
schemaVersion: RETRIEVAL_TRACE_SCHEMA_VERSION,
|
|
798
|
+
id: trace.id,
|
|
799
|
+
startedAt: trace.startedAt,
|
|
800
|
+
...(trace.completedAt ? { completedAt: trace.completedAt } : {}),
|
|
801
|
+
...(trace.durationMs !== undefined ? { durationMs: trace.durationMs } : {}),
|
|
802
|
+
agentName: truncateUtf8(trace.agentName, 128),
|
|
803
|
+
config: {
|
|
804
|
+
model: truncateUtf8(trace.config.model, 128),
|
|
805
|
+
minConfidence: trace.config.minConfidence,
|
|
806
|
+
maxCandidates: trace.config.maxCandidates,
|
|
807
|
+
maxInjectedLessons: trace.config.maxInjectedLessons,
|
|
808
|
+
},
|
|
809
|
+
cache: { hit: trace.cache.hit },
|
|
810
|
+
candidates: [],
|
|
811
|
+
relevantLessonIds: [],
|
|
812
|
+
injected: { lessonIds: [], lessons: [] },
|
|
813
|
+
...(trace.outcome ? { outcome: trace.outcome } : {}),
|
|
814
|
+
truncation: {
|
|
815
|
+
truncated: true,
|
|
816
|
+
kind: 'tombstone',
|
|
817
|
+
reason: 'trace-exceeded-byte-budget',
|
|
818
|
+
originalBytes,
|
|
819
|
+
byteBudget: this.byteBudget,
|
|
820
|
+
},
|
|
821
|
+
};
|
|
822
|
+
for (const key of Object.keys(trace)) Reflect.deleteProperty(trace, key);
|
|
823
|
+
Object.assign(trace, tombstone);
|
|
824
|
+
this.refreshSize(trace);
|
|
825
|
+
|
|
826
|
+
for (const retained of this.traces) {
|
|
827
|
+
if (retained === trace || retained.cache.sourceTraceId !== sourceId) continue;
|
|
828
|
+
retained.cache.sourceTraceTruncated = true;
|
|
829
|
+
delete retained.cache.sourceTraceEvicted;
|
|
830
|
+
this.refreshSize(retained);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
private rewriteEvictedProvenance(evicted: Set<number>): void {
|
|
835
|
+
for (const retained of this.traces) {
|
|
836
|
+
if (retained.cache.sourceTraceId === undefined
|
|
837
|
+
|| !evicted.has(retained.cache.sourceTraceId)) continue;
|
|
838
|
+
delete retained.cache.sourceTraceId;
|
|
839
|
+
delete retained.cache.sourceTraceTruncated;
|
|
840
|
+
retained.cache.sourceTraceEvicted = true;
|
|
841
|
+
this.refreshSize(retained);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
private refreshSize(trace: RetrievalTrace): void {
|
|
846
|
+
const previous = this.sizes.get(trace) ?? 0;
|
|
847
|
+
const next = encodedTraceBytes(trace);
|
|
848
|
+
this.sizes.set(trace, next);
|
|
849
|
+
if (Number.isFinite(previous) && Number.isFinite(next)) {
|
|
850
|
+
this.payloadBytes += next - previous;
|
|
851
|
+
} else {
|
|
852
|
+
this.recalculatePayloadBytes();
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
private recalculatePayloadBytes(): void {
|
|
857
|
+
this.payloadBytes = 0;
|
|
858
|
+
for (const size of this.sizes.values()) this.payloadBytes += size;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
private totalEncodedBytes(): number {
|
|
862
|
+
return 2 + this.payloadBytes + Math.max(0, this.traces.length - 1);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
list(options: RetrievalTraceListOptions = {}): RetrievalTrace[] {
|
|
866
|
+
const requestedLimit = options.limit ?? 20;
|
|
867
|
+
const finiteLimit = Number.isFinite(requestedLimit) ? Math.trunc(requestedLimit) : 20;
|
|
868
|
+
const limit = Math.max(1, Math.min(this.capacity, finiteLimit));
|
|
869
|
+
const selected = this.traces.slice(-limit).reverse().map(trace => structuredClone(trace));
|
|
870
|
+
if (options.includeInputs) return selected;
|
|
871
|
+
|
|
872
|
+
for (const trace of selected) {
|
|
873
|
+
if (trace.context) delete trace.context.input;
|
|
874
|
+
if (trace.conceptExtraction) delete trace.conceptExtraction.input;
|
|
875
|
+
if (trace.relevance) delete trace.relevance.input;
|
|
876
|
+
}
|
|
877
|
+
return selected;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
function encodedTraceBytes(trace: RetrievalTrace): number {
|
|
882
|
+
try {
|
|
883
|
+
return utf8Bytes(JSON.stringify(trace));
|
|
884
|
+
} catch {
|
|
885
|
+
return Number.POSITIVE_INFINITY;
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
function recordInjectionShape(trace: RetrievalTrace, injections: ContextInjection[]): void {
|
|
890
|
+
const injection = injections[0];
|
|
891
|
+
if (!injection) return;
|
|
892
|
+
trace.injected.namespace = injection.namespace;
|
|
893
|
+
trace.injected.position = injection.position;
|
|
894
|
+
trace.injected.block = injectionText(injections);
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
function injectionText(injections: ContextInjection[]): string | undefined {
|
|
898
|
+
for (const injection of injections) {
|
|
899
|
+
for (const block of injection.content) {
|
|
900
|
+
if (block.type === 'text') return block.text;
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
return undefined;
|
|
904
|
+
}
|