tinker-agent 2.0.0 → 2.2.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 +44 -1
- package/README.md +27 -2
- package/package.json +2 -1
- package/src/agent/context-meter.ts +2 -4
- package/src/agent/runtime-session.ts +9 -2
- package/src/agent/session-ledger.ts +12 -5
- package/src/agent/tool-result-content.ts +76 -0
- package/src/agent/types.ts +14 -2
- package/src/cli/config.ts +4 -0
- package/src/cli/model-profiles.ts +41 -2
- package/src/cli/public-config-contract.ts +30 -7
- package/src/cli/runner-dependencies.ts +5 -0
- package/src/cli/tui-memory.ts +1 -0
- package/src/cli/tui-runner.tsx +4 -0
- package/src/context/compiled-context-hash.ts +2 -1
- package/src/context/compiled-context-validator.ts +13 -4
- package/src/context/context-protocol-validator.ts +33 -2
- package/src/context/context-revision-compiler.ts +2 -1
- package/src/context/context-revision.ts +8 -2
- package/src/context/context-swap-renderer.ts +46 -12
- package/src/context/prefix-retirement-planner.ts +13 -9
- package/src/context/protocol-frame.ts +74 -7
- package/src/context/swap-planner.ts +19 -14
- package/src/events/observation-text-log.ts +1 -1
- package/src/events/stdout-event-printer.ts +6 -0
- package/src/image/image-asset-store.ts +32 -3
- package/src/memory/contracts.ts +63 -3
- package/src/memory/memory-coordinator.ts +319 -49
- package/src/memory/memory-extractor.ts +48 -48
- package/src/memory/memory-get-tool.ts +86 -0
- package/src/memory/memory-search-tool.ts +122 -33
- package/src/memory/memory-store.ts +227 -20
- package/src/model/fake-model-client.ts +129 -76
- package/src/model/model-client.ts +62 -11
- package/src/model/openai-chat-mapping.ts +2 -1
- package/src/model/openai-chat-model-client.ts +22 -10
- package/src/model/openai-model-utils.ts +61 -30
- package/src/model/openai-responses-mapping.ts +25 -1
- package/src/model/openai-responses-model-client.ts +27 -11
- package/src/model/token-estimator.ts +10 -0
- package/src/observation/observation-builder.ts +100 -25
- package/src/session/session-history-reader.ts +128 -5
- package/src/session/session-schema.ts +59 -9
- package/src/session/session-store.ts +343 -196
- package/src/tools/registry.ts +18 -0
- package/src/tools/types.ts +46 -0
- package/src/tools/view-image.ts +89 -0
- package/src/tools/wait.ts +85 -0
- package/src/tui/components/memory-browser.tsx +3 -0
- package/src/tui/components/prompt-input.tsx +48 -25
- package/src/tui/event-store.ts +61 -2
package/src/memory/contracts.ts
CHANGED
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
import type { SessionId, TurnId } from "../ids/runtime-id";
|
|
2
2
|
|
|
3
3
|
export const MEMORY_SEARCH_TOOL_NAME = "MemorySearch" as const;
|
|
4
|
-
export const
|
|
5
|
-
export const
|
|
4
|
+
export const MEMORY_GET_TOOL_NAME = "MemoryGet" as const;
|
|
5
|
+
export const MEMORY_SCHEMA_VERSION = 2 as const;
|
|
6
6
|
export const MAX_MEMORY_TEXT_BYTES = 512;
|
|
7
|
+
export const MAX_MEMORY_SUMMARY_BYTES = 4_096;
|
|
8
|
+
export const MAX_SEARCH_RESULT_SUMMARY_BYTES = 1_536;
|
|
7
9
|
export const MAX_MEMORY_QUERY_BYTES = 1_024;
|
|
10
|
+
export const MAX_MEMORY_ID_BYTES = 64;
|
|
8
11
|
export const MEMORY_SEARCH_LIMIT = 5;
|
|
12
|
+
export const MEMORY_RECALL_CANDIDATE_LIMIT = 20;
|
|
13
|
+
export const MAX_MEMORY_KEYWORDS = 8;
|
|
14
|
+
export const MAX_MEMORY_KEYWORD_BYTES = 128;
|
|
15
|
+
export const MEMORY_RRF_K = 60;
|
|
16
|
+
export const MEMORY_EXTRACTION_QUEUE_CAPACITY = 64;
|
|
17
|
+
export const MEMORY_SEARCH_DIAGNOSTIC_VECTOR_SCORES_LIMIT = 10;
|
|
9
18
|
|
|
10
19
|
export type MemoryEmbeddingKind = "openai-compatible";
|
|
11
20
|
|
|
@@ -32,6 +41,7 @@ export type MemoryPaths = {
|
|
|
32
41
|
|
|
33
42
|
export type MemoryWriteCandidate = {
|
|
34
43
|
readonly text: string;
|
|
44
|
+
readonly summary: string;
|
|
35
45
|
readonly embedding: Float32Array;
|
|
36
46
|
};
|
|
37
47
|
|
|
@@ -57,18 +67,51 @@ export type MemoryInsertedRecord = {
|
|
|
57
67
|
export type MemorySearchMatch = {
|
|
58
68
|
readonly memoryId: string;
|
|
59
69
|
readonly text: string;
|
|
70
|
+
readonly summary: string;
|
|
60
71
|
readonly score: number;
|
|
61
72
|
readonly sourceWorkspace: string;
|
|
73
|
+
readonly sourceSessionId: string;
|
|
62
74
|
readonly createdAt: string;
|
|
63
75
|
};
|
|
64
76
|
|
|
77
|
+
export type MemoryRecallPath = "vector" | "fts";
|
|
78
|
+
|
|
79
|
+
export type MemoryFtsMatch = {
|
|
80
|
+
readonly memoryId: string;
|
|
81
|
+
readonly text: string;
|
|
82
|
+
readonly summary: string;
|
|
83
|
+
readonly bm25: number;
|
|
84
|
+
readonly sourceWorkspace: string;
|
|
85
|
+
readonly sourceSessionId: string;
|
|
86
|
+
readonly createdAt: string;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export type MemoryHybridMatch = {
|
|
90
|
+
readonly memoryId: string;
|
|
91
|
+
readonly text: string;
|
|
92
|
+
readonly summary: string;
|
|
93
|
+
readonly score: number;
|
|
94
|
+
readonly via: readonly MemoryRecallPath[];
|
|
95
|
+
readonly sourceWorkspace: string;
|
|
96
|
+
readonly sourceSessionId: string;
|
|
97
|
+
readonly createdAt: string;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export type MemoryRecallDegraded = "vector" | "fts";
|
|
101
|
+
|
|
65
102
|
export type StoredMemorySummary = {
|
|
66
103
|
readonly memoryId: string;
|
|
67
104
|
readonly text: string;
|
|
105
|
+
readonly summary: string;
|
|
68
106
|
readonly sourceWorkspace: string;
|
|
107
|
+
readonly sourceSessionId: string;
|
|
69
108
|
readonly createdAt: string;
|
|
70
109
|
};
|
|
71
110
|
|
|
111
|
+
export type StoredMemoryRecord = StoredMemorySummary & {
|
|
112
|
+
readonly sourceTurnId: string;
|
|
113
|
+
};
|
|
114
|
+
|
|
72
115
|
export type MemoryExtractionRejectedCounts = {
|
|
73
116
|
readonly duplicate: number;
|
|
74
117
|
readonly secret: number;
|
|
@@ -98,8 +141,13 @@ export type MemorySearchDiagnostic = {
|
|
|
98
141
|
readonly workspace: string;
|
|
99
142
|
readonly sessionId: string;
|
|
100
143
|
readonly queryBytes: number;
|
|
144
|
+
readonly keywordCount: number;
|
|
101
145
|
readonly returned: number;
|
|
146
|
+
readonly vectorReturned: number;
|
|
147
|
+
readonly ftsReturned: number;
|
|
148
|
+
readonly degraded: MemoryRecallDegraded | null;
|
|
102
149
|
readonly scores: readonly number[];
|
|
150
|
+
readonly vectorScores: readonly number[];
|
|
103
151
|
readonly ms: number;
|
|
104
152
|
};
|
|
105
153
|
|
|
@@ -110,9 +158,21 @@ export type MemoryInitDiagnostic = {
|
|
|
110
158
|
readonly reason: string;
|
|
111
159
|
};
|
|
112
160
|
|
|
161
|
+
export type MemoryGetDiagnostic = {
|
|
162
|
+
readonly at: string;
|
|
163
|
+
readonly kind: "get";
|
|
164
|
+
readonly outcome: "ok" | "failed";
|
|
165
|
+
readonly reason: string | null;
|
|
166
|
+
readonly workspace: string;
|
|
167
|
+
readonly sessionId: string;
|
|
168
|
+
readonly found: boolean;
|
|
169
|
+
readonly ms: number;
|
|
170
|
+
};
|
|
171
|
+
|
|
113
172
|
export type MemoryDiagnostic =
|
|
114
173
|
| MemoryExtractionDiagnostic
|
|
115
174
|
| MemorySearchDiagnostic
|
|
175
|
+
| MemoryGetDiagnostic
|
|
116
176
|
| MemoryInitDiagnostic;
|
|
117
177
|
|
|
118
178
|
export class MemoryError extends Error {
|
|
@@ -136,7 +196,7 @@ export function boundedMemoryError(error: unknown): string {
|
|
|
136
196
|
return truncateUtf8(singleLine, 400);
|
|
137
197
|
}
|
|
138
198
|
|
|
139
|
-
function truncateUtf8(value: string, maxBytes: number): string {
|
|
199
|
+
export function truncateUtf8(value: string, maxBytes: number): string {
|
|
140
200
|
if (Buffer.byteLength(value, "utf8") <= maxBytes) {
|
|
141
201
|
return value;
|
|
142
202
|
}
|
|
@@ -8,17 +8,35 @@ import type { SessionId } from "../ids/runtime-id";
|
|
|
8
8
|
import type { ModelContextBudget } from "../model/model-context-profile";
|
|
9
9
|
import type { ModelClient } from "../model/model-client";
|
|
10
10
|
import type { CompletedTurnSnapshot } from "../session/session-store";
|
|
11
|
-
import type {
|
|
11
|
+
import type {
|
|
12
|
+
MemoryGetRawResult,
|
|
13
|
+
MemorySearchRawResult,
|
|
14
|
+
ToolExecutor,
|
|
15
|
+
} from "../tools/types";
|
|
12
16
|
import {
|
|
13
17
|
boundedMemoryError,
|
|
18
|
+
MAX_SEARCH_RESULT_SUMMARY_BYTES,
|
|
19
|
+
MEMORY_EXTRACTION_QUEUE_CAPACITY,
|
|
14
20
|
memoryErrorCode,
|
|
21
|
+
MEMORY_GET_TOOL_NAME,
|
|
22
|
+
MEMORY_RECALL_CANDIDATE_LIMIT,
|
|
23
|
+
MEMORY_RRF_K,
|
|
24
|
+
MEMORY_SEARCH_DIAGNOSTIC_VECTOR_SCORES_LIMIT,
|
|
25
|
+
MEMORY_SEARCH_LIMIT,
|
|
15
26
|
MEMORY_SEARCH_TOOL_NAME,
|
|
16
27
|
MemoryError,
|
|
28
|
+
truncateUtf8,
|
|
17
29
|
type MemoryEmbeddingConfig,
|
|
18
30
|
type MemoryExtractionDiagnostic,
|
|
19
31
|
type MemoryExtractionRejectedCounts,
|
|
32
|
+
type MemoryFtsMatch,
|
|
33
|
+
type MemoryGetDiagnostic,
|
|
34
|
+
type MemoryHybridMatch,
|
|
20
35
|
type MemoryPaths,
|
|
36
|
+
type MemoryRecallDegraded,
|
|
37
|
+
type MemoryRecallPath,
|
|
21
38
|
type MemorySearchDiagnostic,
|
|
39
|
+
type MemorySearchMatch,
|
|
22
40
|
type StoredMemorySummary,
|
|
23
41
|
} from "./contracts";
|
|
24
42
|
import {
|
|
@@ -33,6 +51,7 @@ import {
|
|
|
33
51
|
type MemoryExtractionResult,
|
|
34
52
|
} from "./memory-extractor";
|
|
35
53
|
import { ExtractedMemoryLog, MemoryLog } from "./memory-log";
|
|
54
|
+
import { createMemoryGetToolExecutor } from "./memory-get-tool";
|
|
36
55
|
import { createMemorySearchToolExecutor } from "./memory-search-tool";
|
|
37
56
|
import { MemoryStore, resolveMemoryPaths } from "./memory-store";
|
|
38
57
|
import { normalizeEmbedding } from "./vector";
|
|
@@ -62,7 +81,7 @@ export type CreateMemoryCoordinatorInput = {
|
|
|
62
81
|
export class MemoryCoordinator implements CompletedTurnHook {
|
|
63
82
|
private accepting = true;
|
|
64
83
|
private active?: ActiveMemoryTask;
|
|
65
|
-
private pending
|
|
84
|
+
private pending: MemoryWorkerTask[] = [];
|
|
66
85
|
|
|
67
86
|
private constructor(
|
|
68
87
|
private readonly store: MemoryStore,
|
|
@@ -124,7 +143,10 @@ export class MemoryCoordinator implements CompletedTurnHook {
|
|
|
124
143
|
this.start(task);
|
|
125
144
|
return;
|
|
126
145
|
}
|
|
127
|
-
this.pending
|
|
146
|
+
if (this.pending.length >= MEMORY_EXTRACTION_QUEUE_CAPACITY) {
|
|
147
|
+
this.pending.shift();
|
|
148
|
+
}
|
|
149
|
+
this.pending.push(task);
|
|
128
150
|
}
|
|
129
151
|
|
|
130
152
|
recordFailure(input: CompletedTurnHookFailure): void {
|
|
@@ -145,8 +167,18 @@ export class MemoryCoordinator implements CompletedTurnHook {
|
|
|
145
167
|
readonly sessionId: SessionId;
|
|
146
168
|
}): ToolExecutor {
|
|
147
169
|
return createMemorySearchToolExecutor({
|
|
148
|
-
search: (query, signal) => this.search(query, signal, input),
|
|
149
|
-
recordInvalidCall: (
|
|
170
|
+
search: (query, keywords, signal) => this.search(query, keywords, signal, input),
|
|
171
|
+
recordInvalidCall: (invalid) => this.recordInvalidSearch(invalid, input),
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
createGetToolExecutor(input: {
|
|
176
|
+
readonly workspaceRoot: string;
|
|
177
|
+
readonly sessionId: SessionId;
|
|
178
|
+
}): ToolExecutor {
|
|
179
|
+
return createMemoryGetToolExecutor({
|
|
180
|
+
get: (memoryId, signal) => this.get(memoryId, signal, input),
|
|
181
|
+
recordInvalidCall: () => this.recordInvalidGet(input),
|
|
150
182
|
});
|
|
151
183
|
}
|
|
152
184
|
|
|
@@ -159,7 +191,7 @@ export class MemoryCoordinator implements CompletedTurnHook {
|
|
|
159
191
|
return;
|
|
160
192
|
}
|
|
161
193
|
this.accepting = false;
|
|
162
|
-
this.pending =
|
|
194
|
+
this.pending = [];
|
|
163
195
|
this.active?.controller.abort(
|
|
164
196
|
new MemoryError(
|
|
165
197
|
"memory_coordinator_disposed",
|
|
@@ -183,8 +215,7 @@ export class MemoryCoordinator implements CompletedTurnHook {
|
|
|
183
215
|
return;
|
|
184
216
|
}
|
|
185
217
|
this.active = undefined;
|
|
186
|
-
const next = this.accepting ? this.pending : undefined;
|
|
187
|
-
this.pending = undefined;
|
|
218
|
+
const next = this.accepting ? this.pending.shift() : undefined;
|
|
188
219
|
if (next !== undefined) {
|
|
189
220
|
this.start(next);
|
|
190
221
|
}
|
|
@@ -204,7 +235,7 @@ export class MemoryCoordinator implements CompletedTurnHook {
|
|
|
204
235
|
try {
|
|
205
236
|
extraction = await this.extractor.extract(task.extractionEvidenceText, signal);
|
|
206
237
|
inputTokens = extraction.inputTokens;
|
|
207
|
-
returned = extraction.
|
|
238
|
+
returned = extraction.memory === null ? 0 : 1;
|
|
208
239
|
} catch (error) {
|
|
209
240
|
if (error instanceof MemoryExtractionSkippedError) {
|
|
210
241
|
inputTokens = error.inputTokens;
|
|
@@ -246,17 +277,28 @@ export class MemoryCoordinator implements CompletedTurnHook {
|
|
|
246
277
|
return;
|
|
247
278
|
}
|
|
248
279
|
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
280
|
+
const candidate = extraction.memory;
|
|
281
|
+
if (candidate === null) {
|
|
282
|
+
await this.log.append(
|
|
283
|
+
extractionDiagnostic({
|
|
284
|
+
clock: this.clock,
|
|
285
|
+
outcome: "ok",
|
|
286
|
+
reason: null,
|
|
287
|
+
workspace: task.workspaceRoot,
|
|
288
|
+
turnId: task.turnId,
|
|
289
|
+
inputTokens,
|
|
290
|
+
returned,
|
|
291
|
+
ms: elapsedMs(startedAt),
|
|
292
|
+
}),
|
|
293
|
+
);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (
|
|
298
|
+
containsSensitiveMemory(candidate.text) ||
|
|
299
|
+
containsSensitiveMemory(candidate.summary)
|
|
300
|
+
) {
|
|
301
|
+
rejected = Object.freeze({ ...rejected, secret: 1 });
|
|
260
302
|
await this.log.append(
|
|
261
303
|
extractionDiagnostic({
|
|
262
304
|
clock: this.clock,
|
|
@@ -273,24 +315,20 @@ export class MemoryCoordinator implements CompletedTurnHook {
|
|
|
273
315
|
return;
|
|
274
316
|
}
|
|
275
317
|
|
|
276
|
-
let
|
|
318
|
+
let embedding: Float32Array;
|
|
277
319
|
try {
|
|
278
|
-
const rawEmbeddings = await this.embeddingClient.embed(
|
|
279
|
-
if (rawEmbeddings.length !==
|
|
320
|
+
const rawEmbeddings = await this.embeddingClient.embed([candidate.text], signal);
|
|
321
|
+
if (rawEmbeddings.length !== 1 || rawEmbeddings[0] === undefined) {
|
|
280
322
|
throw new MemoryError(
|
|
281
323
|
"memory_embedding_response_invalid",
|
|
282
324
|
"Embedding response count does not match the memory candidate count.",
|
|
283
325
|
);
|
|
284
326
|
}
|
|
285
|
-
|
|
286
|
-
rawEmbeddings.map((vector) =>
|
|
287
|
-
normalizeEmbedding(vector, this.embeddingDimensions),
|
|
288
|
-
),
|
|
289
|
-
);
|
|
327
|
+
embedding = normalizeEmbedding(rawEmbeddings[0], this.embeddingDimensions);
|
|
290
328
|
} catch (error) {
|
|
291
329
|
rejected = Object.freeze({
|
|
292
330
|
...rejected,
|
|
293
|
-
embedding:
|
|
331
|
+
embedding: 1,
|
|
294
332
|
});
|
|
295
333
|
await this.log.append(
|
|
296
334
|
extractionDiagnostic({
|
|
@@ -315,10 +353,13 @@ export class MemoryCoordinator implements CompletedTurnHook {
|
|
|
315
353
|
workspaceRoot: task.workspaceRoot,
|
|
316
354
|
sessionId: task.sessionId,
|
|
317
355
|
turnId: task.turnId,
|
|
318
|
-
candidates:
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
356
|
+
candidates: [
|
|
357
|
+
{
|
|
358
|
+
text: candidate.text,
|
|
359
|
+
summary: candidate.summary,
|
|
360
|
+
embedding,
|
|
361
|
+
},
|
|
362
|
+
],
|
|
322
363
|
});
|
|
323
364
|
rejected = Object.freeze({
|
|
324
365
|
...rejected,
|
|
@@ -366,22 +407,56 @@ export class MemoryCoordinator implements CompletedTurnHook {
|
|
|
366
407
|
}
|
|
367
408
|
|
|
368
409
|
private async search(
|
|
369
|
-
query: string,
|
|
410
|
+
query: string | null,
|
|
411
|
+
keywords: readonly string[],
|
|
370
412
|
signal: AbortSignal,
|
|
371
413
|
source: { readonly workspaceRoot: string; readonly sessionId: SessionId },
|
|
372
414
|
): Promise<MemorySearchRawResult> {
|
|
373
415
|
const startedAt = performance.now();
|
|
374
|
-
const queryBytes = Buffer.byteLength(query, "utf8");
|
|
416
|
+
const queryBytes = query === null ? 0 : Buffer.byteLength(query, "utf8");
|
|
417
|
+
let degraded: MemoryRecallDegraded | null = null;
|
|
418
|
+
let vectorMatches: readonly MemorySearchMatch[] = [];
|
|
419
|
+
let ftsMatches: readonly MemoryFtsMatch[] = [];
|
|
375
420
|
try {
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
421
|
+
if (query !== null) {
|
|
422
|
+
try {
|
|
423
|
+
const raw = await this.embeddingClient.embed([query], signal);
|
|
424
|
+
if (raw.length !== 1 || raw[0] === undefined) {
|
|
425
|
+
throw new MemoryError(
|
|
426
|
+
"memory_embedding_response_invalid",
|
|
427
|
+
"Embedding response did not contain the query vector.",
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
const queryEmbedding = normalizeEmbedding(raw[0], this.embeddingDimensions);
|
|
431
|
+
vectorMatches = this.store.search(
|
|
432
|
+
queryEmbedding,
|
|
433
|
+
MEMORY_RECALL_CANDIDATE_LIMIT,
|
|
434
|
+
);
|
|
435
|
+
} catch (error) {
|
|
436
|
+
if (signal.aborted || keywords.length === 0) {
|
|
437
|
+
throw error;
|
|
438
|
+
}
|
|
439
|
+
degraded = "vector";
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
if (keywords.length > 0) {
|
|
443
|
+
try {
|
|
444
|
+
ftsMatches = this.store.searchFts(keywords, MEMORY_RECALL_CANDIDATE_LIMIT);
|
|
445
|
+
} catch (error) {
|
|
446
|
+
if (signal.aborted) {
|
|
447
|
+
throw error;
|
|
448
|
+
}
|
|
449
|
+
const vectorUsable = query !== null && degraded === null;
|
|
450
|
+
if (!vectorUsable) {
|
|
451
|
+
throw error;
|
|
452
|
+
}
|
|
453
|
+
degraded = "fts";
|
|
454
|
+
}
|
|
382
455
|
}
|
|
383
|
-
const
|
|
384
|
-
|
|
456
|
+
const fused = fuseMemoryRecall({ vector: vectorMatches, fts: ftsMatches }).slice(
|
|
457
|
+
0,
|
|
458
|
+
MEMORY_SEARCH_LIMIT,
|
|
459
|
+
);
|
|
385
460
|
await this.log.append(
|
|
386
461
|
searchDiagnostic({
|
|
387
462
|
clock: this.clock,
|
|
@@ -390,19 +465,31 @@ export class MemoryCoordinator implements CompletedTurnHook {
|
|
|
390
465
|
workspace: source.workspaceRoot,
|
|
391
466
|
sessionId: source.sessionId,
|
|
392
467
|
queryBytes,
|
|
393
|
-
|
|
394
|
-
|
|
468
|
+
keywordCount: keywords.length,
|
|
469
|
+
returned: fused.length,
|
|
470
|
+
vectorReturned: vectorMatches.length,
|
|
471
|
+
ftsReturned: ftsMatches.length,
|
|
472
|
+
degraded,
|
|
473
|
+
scores: fused.map((match) => roundScore(match.score)),
|
|
474
|
+
vectorScores: vectorMatches
|
|
475
|
+
.slice(0, MEMORY_SEARCH_DIAGNOSTIC_VECTOR_SCORES_LIMIT)
|
|
476
|
+
.map((match) => roundScore(match.score)),
|
|
395
477
|
ms: elapsedMs(startedAt),
|
|
396
478
|
}),
|
|
397
479
|
);
|
|
398
480
|
return {
|
|
399
481
|
ok: true,
|
|
482
|
+
degraded,
|
|
400
483
|
matches: Object.freeze(
|
|
401
|
-
|
|
484
|
+
fused.map((match) =>
|
|
402
485
|
Object.freeze({
|
|
486
|
+
memoryId: match.memoryId,
|
|
403
487
|
text: match.text,
|
|
488
|
+
summary: truncateUtf8(match.summary, MAX_SEARCH_RESULT_SUMMARY_BYTES),
|
|
404
489
|
score: match.score,
|
|
490
|
+
via: match.via,
|
|
405
491
|
sourceWorkspace: match.sourceWorkspace,
|
|
492
|
+
sourceSessionId: match.sourceSessionId,
|
|
406
493
|
createdAt: match.createdAt,
|
|
407
494
|
}),
|
|
408
495
|
),
|
|
@@ -420,6 +507,7 @@ export class MemoryCoordinator implements CompletedTurnHook {
|
|
|
420
507
|
workspace: source.workspaceRoot,
|
|
421
508
|
sessionId: source.sessionId,
|
|
422
509
|
queryBytes,
|
|
510
|
+
keywordCount: keywords.length,
|
|
423
511
|
ms: elapsedMs(startedAt),
|
|
424
512
|
}),
|
|
425
513
|
);
|
|
@@ -434,7 +522,7 @@ export class MemoryCoordinator implements CompletedTurnHook {
|
|
|
434
522
|
}
|
|
435
523
|
|
|
436
524
|
private recordInvalidSearch(
|
|
437
|
-
queryBytes: number,
|
|
525
|
+
invalid: { readonly queryBytes: number; readonly keywordCount: number },
|
|
438
526
|
source: { readonly workspaceRoot: string; readonly sessionId: SessionId },
|
|
439
527
|
): Promise<void> {
|
|
440
528
|
return this.log.append(
|
|
@@ -444,7 +532,82 @@ export class MemoryCoordinator implements CompletedTurnHook {
|
|
|
444
532
|
reason: "memory_search_args_invalid",
|
|
445
533
|
workspace: source.workspaceRoot,
|
|
446
534
|
sessionId: source.sessionId,
|
|
447
|
-
queryBytes,
|
|
535
|
+
queryBytes: invalid.queryBytes,
|
|
536
|
+
keywordCount: invalid.keywordCount,
|
|
537
|
+
ms: 0,
|
|
538
|
+
}),
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
private async get(
|
|
543
|
+
memoryId: string,
|
|
544
|
+
signal: AbortSignal,
|
|
545
|
+
source: { readonly workspaceRoot: string; readonly sessionId: SessionId },
|
|
546
|
+
): Promise<MemoryGetRawResult> {
|
|
547
|
+
const startedAt = performance.now();
|
|
548
|
+
try {
|
|
549
|
+
signal.throwIfAborted();
|
|
550
|
+
const record = this.store.getById(memoryId);
|
|
551
|
+
await this.log.append(
|
|
552
|
+
getDiagnostic({
|
|
553
|
+
clock: this.clock,
|
|
554
|
+
outcome: "ok",
|
|
555
|
+
reason: null,
|
|
556
|
+
workspace: source.workspaceRoot,
|
|
557
|
+
sessionId: source.sessionId,
|
|
558
|
+
found: record !== undefined,
|
|
559
|
+
ms: elapsedMs(startedAt),
|
|
560
|
+
}),
|
|
561
|
+
);
|
|
562
|
+
return {
|
|
563
|
+
ok: true,
|
|
564
|
+
memory:
|
|
565
|
+
record === undefined
|
|
566
|
+
? null
|
|
567
|
+
: Object.freeze({
|
|
568
|
+
memoryId: record.memoryId,
|
|
569
|
+
text: record.text,
|
|
570
|
+
summary: record.summary,
|
|
571
|
+
sourceWorkspace: record.sourceWorkspace,
|
|
572
|
+
sourceSessionId: record.sourceSessionId,
|
|
573
|
+
sourceTurnId: record.sourceTurnId,
|
|
574
|
+
createdAt: record.createdAt,
|
|
575
|
+
}),
|
|
576
|
+
};
|
|
577
|
+
} catch (error) {
|
|
578
|
+
if (signal.aborted) {
|
|
579
|
+
throw error;
|
|
580
|
+
}
|
|
581
|
+
await this.log.append(
|
|
582
|
+
getDiagnostic({
|
|
583
|
+
clock: this.clock,
|
|
584
|
+
outcome: "failed",
|
|
585
|
+
reason: memoryErrorCode(error, "memory_get_failed"),
|
|
586
|
+
workspace: source.workspaceRoot,
|
|
587
|
+
sessionId: source.sessionId,
|
|
588
|
+
found: false,
|
|
589
|
+
ms: elapsedMs(startedAt),
|
|
590
|
+
}),
|
|
591
|
+
);
|
|
592
|
+
return {
|
|
593
|
+
ok: false,
|
|
594
|
+
error: boundedMemoryError(error),
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
private recordInvalidGet(source: {
|
|
600
|
+
readonly workspaceRoot: string;
|
|
601
|
+
readonly sessionId: SessionId;
|
|
602
|
+
}): Promise<void> {
|
|
603
|
+
return this.log.append(
|
|
604
|
+
getDiagnostic({
|
|
605
|
+
clock: this.clock,
|
|
606
|
+
outcome: "failed",
|
|
607
|
+
reason: "memory_get_args_invalid",
|
|
608
|
+
workspace: source.workspaceRoot,
|
|
609
|
+
sessionId: source.sessionId,
|
|
610
|
+
found: false,
|
|
448
611
|
ms: 0,
|
|
449
612
|
}),
|
|
450
613
|
);
|
|
@@ -463,7 +626,10 @@ export function buildExtractionEvidenceText(
|
|
|
463
626
|
}
|
|
464
627
|
const messages = snapshot.messages
|
|
465
628
|
.filter(
|
|
466
|
-
(message) =>
|
|
629
|
+
(message) =>
|
|
630
|
+
message.role !== "tool" ||
|
|
631
|
+
(message.name !== MEMORY_SEARCH_TOOL_NAME &&
|
|
632
|
+
message.name !== MEMORY_GET_TOOL_NAME),
|
|
467
633
|
)
|
|
468
634
|
.map((message) => ({ ...message }));
|
|
469
635
|
return JSON.stringify(
|
|
@@ -520,8 +686,13 @@ function searchDiagnostic(input: {
|
|
|
520
686
|
readonly workspace: string;
|
|
521
687
|
readonly sessionId: string;
|
|
522
688
|
readonly queryBytes: number;
|
|
689
|
+
readonly keywordCount: number;
|
|
523
690
|
readonly returned?: number;
|
|
691
|
+
readonly vectorReturned?: number;
|
|
692
|
+
readonly ftsReturned?: number;
|
|
693
|
+
readonly degraded?: MemoryRecallDegraded | null;
|
|
524
694
|
readonly scores?: readonly number[];
|
|
695
|
+
readonly vectorScores?: readonly number[];
|
|
525
696
|
readonly ms: number;
|
|
526
697
|
}): MemorySearchDiagnostic {
|
|
527
698
|
return Object.freeze({
|
|
@@ -532,8 +703,34 @@ function searchDiagnostic(input: {
|
|
|
532
703
|
workspace: input.workspace,
|
|
533
704
|
sessionId: input.sessionId,
|
|
534
705
|
queryBytes: input.queryBytes,
|
|
706
|
+
keywordCount: input.keywordCount,
|
|
535
707
|
returned: input.returned ?? 0,
|
|
708
|
+
vectorReturned: input.vectorReturned ?? 0,
|
|
709
|
+
ftsReturned: input.ftsReturned ?? 0,
|
|
710
|
+
degraded: input.degraded ?? null,
|
|
536
711
|
scores: Object.freeze([...(input.scores ?? [])]),
|
|
712
|
+
vectorScores: Object.freeze([...(input.vectorScores ?? [])]),
|
|
713
|
+
ms: input.ms,
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function getDiagnostic(input: {
|
|
718
|
+
readonly clock: () => string;
|
|
719
|
+
readonly outcome: MemoryGetDiagnostic["outcome"];
|
|
720
|
+
readonly reason: string | null;
|
|
721
|
+
readonly workspace: string;
|
|
722
|
+
readonly sessionId: string;
|
|
723
|
+
readonly found: boolean;
|
|
724
|
+
readonly ms: number;
|
|
725
|
+
}): MemoryGetDiagnostic {
|
|
726
|
+
return Object.freeze({
|
|
727
|
+
at: input.clock(),
|
|
728
|
+
kind: "get",
|
|
729
|
+
outcome: input.outcome,
|
|
730
|
+
reason: input.reason,
|
|
731
|
+
workspace: input.workspace,
|
|
732
|
+
sessionId: input.sessionId,
|
|
733
|
+
found: input.found,
|
|
537
734
|
ms: input.ms,
|
|
538
735
|
});
|
|
539
736
|
}
|
|
@@ -554,3 +751,76 @@ function elapsedMs(startedAt: number): number {
|
|
|
554
751
|
function roundScore(score: number): number {
|
|
555
752
|
return Math.round(score * 1_000) / 1_000;
|
|
556
753
|
}
|
|
754
|
+
|
|
755
|
+
export function fuseMemoryRecall(input: {
|
|
756
|
+
readonly vector: readonly MemorySearchMatch[];
|
|
757
|
+
readonly fts: readonly MemoryFtsMatch[];
|
|
758
|
+
}): readonly MemoryHybridMatch[] {
|
|
759
|
+
type Entry = {
|
|
760
|
+
readonly memoryId: string;
|
|
761
|
+
readonly text: string;
|
|
762
|
+
readonly summary: string;
|
|
763
|
+
readonly sourceWorkspace: string;
|
|
764
|
+
readonly sourceSessionId: string;
|
|
765
|
+
readonly createdAt: string;
|
|
766
|
+
score: number;
|
|
767
|
+
readonly via: Set<MemoryRecallPath>;
|
|
768
|
+
};
|
|
769
|
+
const entries = new Map<string, Entry>();
|
|
770
|
+
const accumulate = (
|
|
771
|
+
match: {
|
|
772
|
+
readonly memoryId: string;
|
|
773
|
+
readonly text: string;
|
|
774
|
+
readonly summary: string;
|
|
775
|
+
readonly sourceWorkspace: string;
|
|
776
|
+
readonly sourceSessionId: string;
|
|
777
|
+
readonly createdAt: string;
|
|
778
|
+
},
|
|
779
|
+
rank: number,
|
|
780
|
+
path: MemoryRecallPath,
|
|
781
|
+
): void => {
|
|
782
|
+
const contribution = 1 / (MEMORY_RRF_K + rank);
|
|
783
|
+
const existing = entries.get(match.memoryId);
|
|
784
|
+
if (existing !== undefined) {
|
|
785
|
+
existing.score += contribution;
|
|
786
|
+
existing.via.add(path);
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
entries.set(match.memoryId, {
|
|
790
|
+
memoryId: match.memoryId,
|
|
791
|
+
text: match.text,
|
|
792
|
+
summary: match.summary,
|
|
793
|
+
sourceWorkspace: match.sourceWorkspace,
|
|
794
|
+
sourceSessionId: match.sourceSessionId,
|
|
795
|
+
createdAt: match.createdAt,
|
|
796
|
+
score: contribution,
|
|
797
|
+
via: new Set([path]),
|
|
798
|
+
});
|
|
799
|
+
};
|
|
800
|
+
input.vector.forEach((match, index) => accumulate(match, index + 1, "vector"));
|
|
801
|
+
input.fts.forEach((match, index) => accumulate(match, index + 1, "fts"));
|
|
802
|
+
|
|
803
|
+
return Object.freeze(
|
|
804
|
+
[...entries.values()]
|
|
805
|
+
.sort(
|
|
806
|
+
(left, right) =>
|
|
807
|
+
right.score - left.score ||
|
|
808
|
+
right.createdAt.localeCompare(left.createdAt) ||
|
|
809
|
+
left.memoryId.localeCompare(right.memoryId),
|
|
810
|
+
)
|
|
811
|
+
.map((entry) =>
|
|
812
|
+
Object.freeze({
|
|
813
|
+
memoryId: entry.memoryId,
|
|
814
|
+
text: entry.text,
|
|
815
|
+
summary: entry.summary,
|
|
816
|
+
score: entry.score,
|
|
817
|
+
via: Object.freeze(
|
|
818
|
+
(["vector", "fts"] as const).filter((path) => entry.via.has(path)),
|
|
819
|
+
),
|
|
820
|
+
sourceWorkspace: entry.sourceWorkspace,
|
|
821
|
+
sourceSessionId: entry.sourceSessionId,
|
|
822
|
+
createdAt: entry.createdAt,
|
|
823
|
+
}),
|
|
824
|
+
),
|
|
825
|
+
);
|
|
826
|
+
}
|