tinker-agent 1.4.0 → 1.5.0
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 +23 -1
- package/README.md +54 -0
- package/package.json +5 -1
- package/src/agent/runtime-session.ts +79 -0
- package/src/cli/config.ts +29 -0
- package/src/cli/model-profiles.ts +84 -1
- package/src/cli/public-config-contract.ts +82 -0
- package/src/cli/runner-dependencies.ts +8 -0
- package/src/cli/tui-memory.ts +67 -0
- package/src/cli/tui-runner.tsx +27 -0
- package/src/context/context-policy.ts +2 -2
- package/src/events/stdout-event-printer.ts +1 -0
- package/src/memory/contracts.ts +148 -0
- package/src/memory/embedding-client.ts +105 -0
- package/src/memory/memory-coordinator.ts +556 -0
- package/src/memory/memory-extractor.ts +231 -0
- package/src/memory/memory-log.ts +88 -0
- package/src/memory/memory-search-tool.ts +100 -0
- package/src/memory/memory-store.ts +687 -0
- package/src/memory/vector.ts +153 -0
- package/src/model/fake-model-client.ts +971 -3
- package/src/observation/observation-builder.ts +20 -0
- package/src/session/session-store.ts +123 -0
- package/src/tools/registry.ts +4 -0
- package/src/tools/types.ts +16 -0
- package/src/tui/app.tsx +69 -2
- package/src/tui/clipboard.ts +22 -0
- package/src/tui/components/footer.tsx +9 -4
- package/src/tui/components/memory-browser.tsx +151 -0
- package/src/tui/event-store.ts +9 -2
- package/src/tui/slash-commands.ts +12 -0
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import type {
|
|
3
|
+
CompletedTurnHook,
|
|
4
|
+
CompletedTurnHookFailure,
|
|
5
|
+
CompletedTurnHookInput,
|
|
6
|
+
} from "../agent/runtime-session";
|
|
7
|
+
import type { SessionId } from "../ids/runtime-id";
|
|
8
|
+
import type { ModelContextBudget } from "../model/model-context-profile";
|
|
9
|
+
import type { ModelClient } from "../model/model-client";
|
|
10
|
+
import type { CompletedTurnSnapshot } from "../session/session-store";
|
|
11
|
+
import type { MemorySearchRawResult, ToolExecutor } from "../tools/types";
|
|
12
|
+
import {
|
|
13
|
+
boundedMemoryError,
|
|
14
|
+
memoryErrorCode,
|
|
15
|
+
MEMORY_SEARCH_TOOL_NAME,
|
|
16
|
+
MemoryError,
|
|
17
|
+
type MemoryEmbeddingConfig,
|
|
18
|
+
type MemoryExtractionDiagnostic,
|
|
19
|
+
type MemoryExtractionRejectedCounts,
|
|
20
|
+
type MemoryPaths,
|
|
21
|
+
type MemorySearchDiagnostic,
|
|
22
|
+
type StoredMemorySummary,
|
|
23
|
+
} from "./contracts";
|
|
24
|
+
import {
|
|
25
|
+
OpenAICompatibleEmbeddingClient,
|
|
26
|
+
type MemoryEmbeddingClient,
|
|
27
|
+
} from "./embedding-client";
|
|
28
|
+
import {
|
|
29
|
+
MemoryExtractionOutputError,
|
|
30
|
+
MemoryExtractionRequestError,
|
|
31
|
+
MemoryExtractionSkippedError,
|
|
32
|
+
MemoryExtractor,
|
|
33
|
+
type MemoryExtractionResult,
|
|
34
|
+
} from "./memory-extractor";
|
|
35
|
+
import { ExtractedMemoryLog, MemoryLog } from "./memory-log";
|
|
36
|
+
import { createMemorySearchToolExecutor } from "./memory-search-tool";
|
|
37
|
+
import { MemoryStore, resolveMemoryPaths } from "./memory-store";
|
|
38
|
+
import { normalizeEmbedding } from "./vector";
|
|
39
|
+
|
|
40
|
+
type MemoryWorkerTask = {
|
|
41
|
+
readonly workspaceRoot: string;
|
|
42
|
+
readonly sessionId: SessionId;
|
|
43
|
+
readonly turnId: CompletedTurnHookInput["turnId"];
|
|
44
|
+
readonly extractionEvidenceText: string;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
type ActiveMemoryTask = {
|
|
48
|
+
readonly task: MemoryWorkerTask;
|
|
49
|
+
readonly controller: AbortController;
|
|
50
|
+
completion?: Promise<void>;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export type CreateMemoryCoordinatorInput = {
|
|
54
|
+
readonly paths?: MemoryPaths;
|
|
55
|
+
readonly embedding: MemoryEmbeddingConfig;
|
|
56
|
+
readonly extractionContextBudget: ModelContextBudget;
|
|
57
|
+
readonly createExtractionClient: () => ModelClient;
|
|
58
|
+
readonly createEmbeddingClient?: () => MemoryEmbeddingClient;
|
|
59
|
+
readonly clock?: () => string;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export class MemoryCoordinator implements CompletedTurnHook {
|
|
63
|
+
private accepting = true;
|
|
64
|
+
private active?: ActiveMemoryTask;
|
|
65
|
+
private pending?: MemoryWorkerTask;
|
|
66
|
+
|
|
67
|
+
private constructor(
|
|
68
|
+
private readonly store: MemoryStore,
|
|
69
|
+
private readonly extractor: MemoryExtractor,
|
|
70
|
+
private readonly embeddingClient: MemoryEmbeddingClient,
|
|
71
|
+
private readonly log: MemoryLog,
|
|
72
|
+
private readonly extractedLog: ExtractedMemoryLog,
|
|
73
|
+
private readonly embeddingDimensions: number,
|
|
74
|
+
private readonly clock: () => string,
|
|
75
|
+
) {}
|
|
76
|
+
|
|
77
|
+
static async create(input: CreateMemoryCoordinatorInput): Promise<MemoryCoordinator> {
|
|
78
|
+
const paths = input.paths ?? resolveMemoryPaths();
|
|
79
|
+
const log = new MemoryLog(paths.log);
|
|
80
|
+
const extractedLog = new ExtractedMemoryLog(paths.extractedLog);
|
|
81
|
+
const store = await MemoryStore.open({
|
|
82
|
+
paths,
|
|
83
|
+
embedding: input.embedding,
|
|
84
|
+
clock: input.clock,
|
|
85
|
+
});
|
|
86
|
+
try {
|
|
87
|
+
const extractionClient = input.createExtractionClient();
|
|
88
|
+
const embeddingClient =
|
|
89
|
+
input.createEmbeddingClient?.() ??
|
|
90
|
+
new OpenAICompatibleEmbeddingClient(input.embedding);
|
|
91
|
+
return new MemoryCoordinator(
|
|
92
|
+
store,
|
|
93
|
+
new MemoryExtractor(extractionClient, input.extractionContextBudget),
|
|
94
|
+
embeddingClient,
|
|
95
|
+
log,
|
|
96
|
+
extractedLog,
|
|
97
|
+
input.embedding.dimensions,
|
|
98
|
+
input.clock ?? (() => new Date().toISOString()),
|
|
99
|
+
);
|
|
100
|
+
} catch (error) {
|
|
101
|
+
store.close();
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
enqueue(input: CompletedTurnHookInput): void {
|
|
107
|
+
if (!this.accepting) {
|
|
108
|
+
throw new MemoryError(
|
|
109
|
+
"completed_turn_enqueue_failed",
|
|
110
|
+
"Memory coordinator is shutting down.",
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
const extractionEvidenceText = buildExtractionEvidenceText(
|
|
114
|
+
input.workspaceRoot,
|
|
115
|
+
input.snapshot,
|
|
116
|
+
);
|
|
117
|
+
const task = Object.freeze({
|
|
118
|
+
workspaceRoot: input.workspaceRoot,
|
|
119
|
+
sessionId: input.sessionId,
|
|
120
|
+
turnId: input.turnId,
|
|
121
|
+
extractionEvidenceText,
|
|
122
|
+
});
|
|
123
|
+
if (this.active === undefined) {
|
|
124
|
+
this.start(task);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
this.pending = task;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
recordFailure(input: CompletedTurnHookFailure): void {
|
|
131
|
+
void this.log.append(
|
|
132
|
+
extractionDiagnostic({
|
|
133
|
+
clock: this.clock,
|
|
134
|
+
outcome: "failed",
|
|
135
|
+
reason: input.reason,
|
|
136
|
+
workspace: input.workspaceRoot,
|
|
137
|
+
turnId: input.turnId,
|
|
138
|
+
ms: 0,
|
|
139
|
+
}),
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
createSearchToolExecutor(input: {
|
|
144
|
+
readonly workspaceRoot: string;
|
|
145
|
+
readonly sessionId: SessionId;
|
|
146
|
+
}): ToolExecutor {
|
|
147
|
+
return createMemorySearchToolExecutor({
|
|
148
|
+
search: (query, signal) => this.search(query, signal, input),
|
|
149
|
+
recordInvalidCall: (queryBytes) => this.recordInvalidSearch(queryBytes, input),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
listStoredMemories(): readonly StoredMemorySummary[] {
|
|
154
|
+
return this.store.listStoredMemories();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
dispose(): void {
|
|
158
|
+
if (!this.accepting) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
this.accepting = false;
|
|
162
|
+
this.pending = undefined;
|
|
163
|
+
this.active?.controller.abort(
|
|
164
|
+
new MemoryError(
|
|
165
|
+
"memory_coordinator_disposed",
|
|
166
|
+
"Memory coordinator is shutting down.",
|
|
167
|
+
),
|
|
168
|
+
);
|
|
169
|
+
this.store.close();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private start(task: MemoryWorkerTask): void {
|
|
173
|
+
const active: ActiveMemoryTask = {
|
|
174
|
+
task,
|
|
175
|
+
controller: new AbortController(),
|
|
176
|
+
};
|
|
177
|
+
this.active = active;
|
|
178
|
+
active.completion = Promise.resolve()
|
|
179
|
+
.then(() => this.processTask(task, active.controller.signal))
|
|
180
|
+
.catch(() => undefined)
|
|
181
|
+
.finally(() => {
|
|
182
|
+
if (this.active !== active) {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
this.active = undefined;
|
|
186
|
+
const next = this.accepting ? this.pending : undefined;
|
|
187
|
+
this.pending = undefined;
|
|
188
|
+
if (next !== undefined) {
|
|
189
|
+
this.start(next);
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private async processTask(
|
|
195
|
+
task: MemoryWorkerTask,
|
|
196
|
+
signal: AbortSignal,
|
|
197
|
+
): Promise<void> {
|
|
198
|
+
const startedAt = performance.now();
|
|
199
|
+
let inputTokens = 0;
|
|
200
|
+
let returned = 0;
|
|
201
|
+
let rejected = emptyRejectedCounts();
|
|
202
|
+
|
|
203
|
+
let extraction: MemoryExtractionResult;
|
|
204
|
+
try {
|
|
205
|
+
extraction = await this.extractor.extract(task.extractionEvidenceText, signal);
|
|
206
|
+
inputTokens = extraction.inputTokens;
|
|
207
|
+
returned = extraction.memories.length;
|
|
208
|
+
} catch (error) {
|
|
209
|
+
if (error instanceof MemoryExtractionSkippedError) {
|
|
210
|
+
inputTokens = error.inputTokens;
|
|
211
|
+
await this.log.append(
|
|
212
|
+
extractionDiagnostic({
|
|
213
|
+
clock: this.clock,
|
|
214
|
+
outcome: "skipped",
|
|
215
|
+
reason: error.code,
|
|
216
|
+
workspace: task.workspaceRoot,
|
|
217
|
+
turnId: task.turnId,
|
|
218
|
+
inputTokens,
|
|
219
|
+
ms: elapsedMs(startedAt),
|
|
220
|
+
}),
|
|
221
|
+
);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
if (error instanceof MemoryExtractionOutputError) {
|
|
225
|
+
inputTokens = error.inputTokens;
|
|
226
|
+
returned = error.returned;
|
|
227
|
+
rejected = Object.freeze({ ...rejected, invalid: Math.max(1, returned) });
|
|
228
|
+
} else if (error instanceof MemoryExtractionRequestError) {
|
|
229
|
+
inputTokens = error.inputTokens;
|
|
230
|
+
}
|
|
231
|
+
await this.log.append(
|
|
232
|
+
extractionDiagnostic({
|
|
233
|
+
clock: this.clock,
|
|
234
|
+
outcome: signal.aborted ? "skipped" : "failed",
|
|
235
|
+
reason: signal.aborted
|
|
236
|
+
? "extraction_cancelled"
|
|
237
|
+
: memoryErrorCode(error, "extraction_model_failed"),
|
|
238
|
+
workspace: task.workspaceRoot,
|
|
239
|
+
turnId: task.turnId,
|
|
240
|
+
inputTokens,
|
|
241
|
+
returned,
|
|
242
|
+
rejected,
|
|
243
|
+
ms: elapsedMs(startedAt),
|
|
244
|
+
}),
|
|
245
|
+
);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const safeMemories = extraction.memories.filter((text) => {
|
|
250
|
+
if (!containsSensitiveMemory(text)) {
|
|
251
|
+
return true;
|
|
252
|
+
}
|
|
253
|
+
rejected = Object.freeze({
|
|
254
|
+
...rejected,
|
|
255
|
+
secret: rejected.secret + 1,
|
|
256
|
+
});
|
|
257
|
+
return false;
|
|
258
|
+
});
|
|
259
|
+
if (safeMemories.length === 0) {
|
|
260
|
+
await this.log.append(
|
|
261
|
+
extractionDiagnostic({
|
|
262
|
+
clock: this.clock,
|
|
263
|
+
outcome: "ok",
|
|
264
|
+
reason: null,
|
|
265
|
+
workspace: task.workspaceRoot,
|
|
266
|
+
turnId: task.turnId,
|
|
267
|
+
inputTokens,
|
|
268
|
+
returned,
|
|
269
|
+
rejected,
|
|
270
|
+
ms: elapsedMs(startedAt),
|
|
271
|
+
}),
|
|
272
|
+
);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
let embeddings: readonly Float32Array[];
|
|
277
|
+
try {
|
|
278
|
+
const rawEmbeddings = await this.embeddingClient.embed(safeMemories, signal);
|
|
279
|
+
if (rawEmbeddings.length !== safeMemories.length) {
|
|
280
|
+
throw new MemoryError(
|
|
281
|
+
"memory_embedding_response_invalid",
|
|
282
|
+
"Embedding response count does not match the memory candidate count.",
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
embeddings = Object.freeze(
|
|
286
|
+
rawEmbeddings.map((vector) =>
|
|
287
|
+
normalizeEmbedding(vector, this.embeddingDimensions),
|
|
288
|
+
),
|
|
289
|
+
);
|
|
290
|
+
} catch (error) {
|
|
291
|
+
rejected = Object.freeze({
|
|
292
|
+
...rejected,
|
|
293
|
+
embedding: safeMemories.length,
|
|
294
|
+
});
|
|
295
|
+
await this.log.append(
|
|
296
|
+
extractionDiagnostic({
|
|
297
|
+
clock: this.clock,
|
|
298
|
+
outcome: signal.aborted ? "skipped" : "failed",
|
|
299
|
+
reason: signal.aborted
|
|
300
|
+
? "extraction_cancelled"
|
|
301
|
+
: memoryErrorCode(error, "memory_embedding_failed"),
|
|
302
|
+
workspace: task.workspaceRoot,
|
|
303
|
+
turnId: task.turnId,
|
|
304
|
+
inputTokens,
|
|
305
|
+
returned,
|
|
306
|
+
rejected,
|
|
307
|
+
ms: elapsedMs(startedAt),
|
|
308
|
+
}),
|
|
309
|
+
);
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
try {
|
|
314
|
+
const result = this.store.insertBatch({
|
|
315
|
+
workspaceRoot: task.workspaceRoot,
|
|
316
|
+
sessionId: task.sessionId,
|
|
317
|
+
turnId: task.turnId,
|
|
318
|
+
candidates: safeMemories.map((text, index) => ({
|
|
319
|
+
text,
|
|
320
|
+
embedding: embeddings[index],
|
|
321
|
+
})),
|
|
322
|
+
});
|
|
323
|
+
rejected = Object.freeze({
|
|
324
|
+
...rejected,
|
|
325
|
+
duplicate: result.duplicate,
|
|
326
|
+
});
|
|
327
|
+
if (result.inserted.length > 0) {
|
|
328
|
+
await this.extractedLog.append({
|
|
329
|
+
at: result.inserted[0].createdAt,
|
|
330
|
+
workspace: task.workspaceRoot,
|
|
331
|
+
turnId: task.turnId,
|
|
332
|
+
memories: result.inserted,
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
await this.log.append(
|
|
336
|
+
extractionDiagnostic({
|
|
337
|
+
clock: this.clock,
|
|
338
|
+
outcome: "ok",
|
|
339
|
+
reason: null,
|
|
340
|
+
workspace: task.workspaceRoot,
|
|
341
|
+
turnId: task.turnId,
|
|
342
|
+
inputTokens,
|
|
343
|
+
returned,
|
|
344
|
+
written: result.written,
|
|
345
|
+
rejected,
|
|
346
|
+
ms: elapsedMs(startedAt),
|
|
347
|
+
}),
|
|
348
|
+
);
|
|
349
|
+
} catch (error) {
|
|
350
|
+
await this.log.append(
|
|
351
|
+
extractionDiagnostic({
|
|
352
|
+
clock: this.clock,
|
|
353
|
+
outcome: signal.aborted ? "skipped" : "failed",
|
|
354
|
+
reason: signal.aborted
|
|
355
|
+
? "extraction_cancelled"
|
|
356
|
+
: memoryErrorCode(error, "memory_write_failed"),
|
|
357
|
+
workspace: task.workspaceRoot,
|
|
358
|
+
turnId: task.turnId,
|
|
359
|
+
inputTokens,
|
|
360
|
+
returned,
|
|
361
|
+
rejected,
|
|
362
|
+
ms: elapsedMs(startedAt),
|
|
363
|
+
}),
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
private async search(
|
|
369
|
+
query: string,
|
|
370
|
+
signal: AbortSignal,
|
|
371
|
+
source: { readonly workspaceRoot: string; readonly sessionId: SessionId },
|
|
372
|
+
): Promise<MemorySearchRawResult> {
|
|
373
|
+
const startedAt = performance.now();
|
|
374
|
+
const queryBytes = Buffer.byteLength(query, "utf8");
|
|
375
|
+
try {
|
|
376
|
+
const raw = await this.embeddingClient.embed([query], signal);
|
|
377
|
+
if (raw.length !== 1 || raw[0] === undefined) {
|
|
378
|
+
throw new MemoryError(
|
|
379
|
+
"memory_embedding_response_invalid",
|
|
380
|
+
"Embedding response did not contain the query vector.",
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
const queryEmbedding = normalizeEmbedding(raw[0], this.embeddingDimensions);
|
|
384
|
+
const matches = this.store.search(queryEmbedding);
|
|
385
|
+
await this.log.append(
|
|
386
|
+
searchDiagnostic({
|
|
387
|
+
clock: this.clock,
|
|
388
|
+
outcome: "ok",
|
|
389
|
+
reason: null,
|
|
390
|
+
workspace: source.workspaceRoot,
|
|
391
|
+
sessionId: source.sessionId,
|
|
392
|
+
queryBytes,
|
|
393
|
+
returned: matches.length,
|
|
394
|
+
scores: matches.map((match) => roundScore(match.score)),
|
|
395
|
+
ms: elapsedMs(startedAt),
|
|
396
|
+
}),
|
|
397
|
+
);
|
|
398
|
+
return {
|
|
399
|
+
ok: true,
|
|
400
|
+
matches: Object.freeze(
|
|
401
|
+
matches.map((match) =>
|
|
402
|
+
Object.freeze({
|
|
403
|
+
text: match.text,
|
|
404
|
+
score: match.score,
|
|
405
|
+
sourceWorkspace: match.sourceWorkspace,
|
|
406
|
+
createdAt: match.createdAt,
|
|
407
|
+
}),
|
|
408
|
+
),
|
|
409
|
+
),
|
|
410
|
+
};
|
|
411
|
+
} catch (error) {
|
|
412
|
+
const reason = signal.aborted
|
|
413
|
+
? "memory_search_cancelled"
|
|
414
|
+
: memoryErrorCode(error, "memory_search_failed");
|
|
415
|
+
await this.log.append(
|
|
416
|
+
searchDiagnostic({
|
|
417
|
+
clock: this.clock,
|
|
418
|
+
outcome: signal.aborted ? "skipped" : "failed",
|
|
419
|
+
reason,
|
|
420
|
+
workspace: source.workspaceRoot,
|
|
421
|
+
sessionId: source.sessionId,
|
|
422
|
+
queryBytes,
|
|
423
|
+
ms: elapsedMs(startedAt),
|
|
424
|
+
}),
|
|
425
|
+
);
|
|
426
|
+
if (signal.aborted) {
|
|
427
|
+
throw error;
|
|
428
|
+
}
|
|
429
|
+
return {
|
|
430
|
+
ok: false,
|
|
431
|
+
error: boundedMemoryError(error),
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
private recordInvalidSearch(
|
|
437
|
+
queryBytes: number,
|
|
438
|
+
source: { readonly workspaceRoot: string; readonly sessionId: SessionId },
|
|
439
|
+
): Promise<void> {
|
|
440
|
+
return this.log.append(
|
|
441
|
+
searchDiagnostic({
|
|
442
|
+
clock: this.clock,
|
|
443
|
+
outcome: "failed",
|
|
444
|
+
reason: "memory_search_args_invalid",
|
|
445
|
+
workspace: source.workspaceRoot,
|
|
446
|
+
sessionId: source.sessionId,
|
|
447
|
+
queryBytes,
|
|
448
|
+
ms: 0,
|
|
449
|
+
}),
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
export function buildExtractionEvidenceText(
|
|
455
|
+
workspaceRoot: string,
|
|
456
|
+
snapshot: CompletedTurnSnapshot,
|
|
457
|
+
): string {
|
|
458
|
+
if (!path.isAbsolute(workspaceRoot)) {
|
|
459
|
+
throw new MemoryError(
|
|
460
|
+
"completed_turn_enqueue_failed",
|
|
461
|
+
"Completed turn workspace must be absolute.",
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
const messages = snapshot.messages
|
|
465
|
+
.filter(
|
|
466
|
+
(message) => message.role !== "tool" || message.name !== MEMORY_SEARCH_TOOL_NAME,
|
|
467
|
+
)
|
|
468
|
+
.map((message) => ({ ...message }));
|
|
469
|
+
return JSON.stringify(
|
|
470
|
+
{
|
|
471
|
+
workspaceRoot,
|
|
472
|
+
messages,
|
|
473
|
+
},
|
|
474
|
+
null,
|
|
475
|
+
2,
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
export function containsSensitiveMemory(text: string): boolean {
|
|
480
|
+
return [
|
|
481
|
+
/-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----/i,
|
|
482
|
+
/\b(?:authorization\s*:\s*)?bearer\s+[a-z0-9._~+/=-]{12,}\b/i,
|
|
483
|
+
/\b(?:sk-[a-z0-9_-]{12,}|gh[opusr]_[a-z0-9_]{20,}|github_pat_[a-z0-9_]{20,}|AKIA[0-9A-Z]{16}|AIza[0-9A-Za-z_-]{30,})\b/,
|
|
484
|
+
/\b(?:cookie|session(?:_?token|id)?)\s*[:=]\s*["']?[^\s"',;]{8,}/i,
|
|
485
|
+
/\b(?:password|passwd|pwd|secret|api[_-]?key|access[_-]?token|refresh[_-]?token)\s*[:=]\s*["']?[^\s"',;]{6,}/i,
|
|
486
|
+
].some((pattern) => pattern.test(text));
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function extractionDiagnostic(input: {
|
|
490
|
+
readonly clock: () => string;
|
|
491
|
+
readonly outcome: MemoryExtractionDiagnostic["outcome"];
|
|
492
|
+
readonly reason: string | null;
|
|
493
|
+
readonly workspace: string;
|
|
494
|
+
readonly turnId: string;
|
|
495
|
+
readonly ms: number;
|
|
496
|
+
readonly inputTokens?: number;
|
|
497
|
+
readonly returned?: number;
|
|
498
|
+
readonly written?: number;
|
|
499
|
+
readonly rejected?: MemoryExtractionRejectedCounts;
|
|
500
|
+
}): MemoryExtractionDiagnostic {
|
|
501
|
+
return Object.freeze({
|
|
502
|
+
at: input.clock(),
|
|
503
|
+
kind: "extraction",
|
|
504
|
+
outcome: input.outcome,
|
|
505
|
+
reason: input.reason,
|
|
506
|
+
workspace: input.workspace,
|
|
507
|
+
turnId: input.turnId,
|
|
508
|
+
inputTokens: input.inputTokens ?? 0,
|
|
509
|
+
returned: input.returned ?? 0,
|
|
510
|
+
written: input.written ?? 0,
|
|
511
|
+
rejected: input.rejected ?? emptyRejectedCounts(),
|
|
512
|
+
ms: input.ms,
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function searchDiagnostic(input: {
|
|
517
|
+
readonly clock: () => string;
|
|
518
|
+
readonly outcome: MemorySearchDiagnostic["outcome"];
|
|
519
|
+
readonly reason: string | null;
|
|
520
|
+
readonly workspace: string;
|
|
521
|
+
readonly sessionId: string;
|
|
522
|
+
readonly queryBytes: number;
|
|
523
|
+
readonly returned?: number;
|
|
524
|
+
readonly scores?: readonly number[];
|
|
525
|
+
readonly ms: number;
|
|
526
|
+
}): MemorySearchDiagnostic {
|
|
527
|
+
return Object.freeze({
|
|
528
|
+
at: input.clock(),
|
|
529
|
+
kind: "search",
|
|
530
|
+
outcome: input.outcome,
|
|
531
|
+
reason: input.reason,
|
|
532
|
+
workspace: input.workspace,
|
|
533
|
+
sessionId: input.sessionId,
|
|
534
|
+
queryBytes: input.queryBytes,
|
|
535
|
+
returned: input.returned ?? 0,
|
|
536
|
+
scores: Object.freeze([...(input.scores ?? [])]),
|
|
537
|
+
ms: input.ms,
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function emptyRejectedCounts(): MemoryExtractionRejectedCounts {
|
|
542
|
+
return Object.freeze({
|
|
543
|
+
duplicate: 0,
|
|
544
|
+
secret: 0,
|
|
545
|
+
invalid: 0,
|
|
546
|
+
embedding: 0,
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function elapsedMs(startedAt: number): number {
|
|
551
|
+
return Math.round((performance.now() - startedAt) * 100) / 100;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function roundScore(score: number): number {
|
|
555
|
+
return Math.round(score * 1_000) / 1_000;
|
|
556
|
+
}
|