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