@sentry/junior-memory 0.198.0 → 0.199.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/dist/index.d.ts +887 -7
- package/package.json +5 -6
- package/dist/agent.d.ts +0 -280
- package/dist/api.d.ts +0 -121
- package/dist/cli/format.d.ts +0 -5
- package/dist/cli/index.d.ts +0 -3
- package/dist/cli/search.d.ts +0 -4
- package/dist/cli/show.d.ts +0 -4
- package/dist/db/schema.d.ts +0 -479
- package/dist/events.d.ts +0 -35
- package/dist/operational-report.d.ts +0 -8
- package/dist/plugin.d.ts +0 -9
- package/dist/process-session.d.ts +0 -9
- package/dist/ranking.d.ts +0 -18
- package/dist/recall.d.ts +0 -41
- package/dist/scope.d.ts +0 -19
- package/dist/store.d.ts +0 -229
- package/dist/tools.d.ts +0 -164
- package/dist/types.d.ts +0 -78
- package/dist/user-pages.d.ts +0 -4
- package/dist/viewer.d.ts +0 -91
- package/src/agent.ts +0 -663
- package/src/api.ts +0 -288
- package/src/cli/format.ts +0 -30
- package/src/cli/index.ts +0 -15
- package/src/cli/search.ts +0 -119
- package/src/cli/show.ts +0 -44
- package/src/db/schema.ts +0 -147
- package/src/events.ts +0 -107
- package/src/index.ts +0 -25
- package/src/operational-report.ts +0 -228
- package/src/plugin.ts +0 -186
- package/src/process-session.ts +0 -311
- package/src/ranking.ts +0 -107
- package/src/recall.ts +0 -212
- package/src/scope.ts +0 -75
- package/src/store.ts +0 -1578
- package/src/tools.ts +0 -586
- package/src/types.ts +0 -37
- package/src/user-pages.ts +0 -99
- package/src/viewer.ts +0 -441
package/src/process-session.ts
DELETED
|
@@ -1,311 +0,0 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import {
|
|
3
|
-
getSourceKey,
|
|
4
|
-
type PluginRunContext,
|
|
5
|
-
type PluginRunTranscriptEntry,
|
|
6
|
-
type PluginTaskContext,
|
|
7
|
-
} from "@sentry/junior-plugin-api";
|
|
8
|
-
import { z } from "zod";
|
|
9
|
-
import {
|
|
10
|
-
createMemoryStore,
|
|
11
|
-
type CreateMemoryInput,
|
|
12
|
-
type CreateMemoryResult,
|
|
13
|
-
type MemoryDb,
|
|
14
|
-
} from "./store";
|
|
15
|
-
import {
|
|
16
|
-
createMemoryAgent,
|
|
17
|
-
parseExtractedMemory,
|
|
18
|
-
type ExtractedMemory,
|
|
19
|
-
type MemoryExtractionResult,
|
|
20
|
-
} from "./agent";
|
|
21
|
-
import { MEMORY_KINDS, memoryRuntimeContextSchema } from "./types";
|
|
22
|
-
import { capturedMemory, memoriesCapturedEvent } from "./events";
|
|
23
|
-
|
|
24
|
-
const MEMORY_TOOL_NAMES = new Set([
|
|
25
|
-
"createMemory",
|
|
26
|
-
"listMemories",
|
|
27
|
-
"removeMemory",
|
|
28
|
-
"searchMemories",
|
|
29
|
-
]);
|
|
30
|
-
const MEMORY_TASK_STATE_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
31
|
-
const extractedMemorySchema = z
|
|
32
|
-
.object({
|
|
33
|
-
content: z.string().min(1),
|
|
34
|
-
expiresAtMs: z.number().finite().nullable(),
|
|
35
|
-
kind: z.enum(MEMORY_KINDS),
|
|
36
|
-
evidenceMessageIndices: z
|
|
37
|
-
.array(z.number().int().nonnegative())
|
|
38
|
-
.min(1)
|
|
39
|
-
.max(10),
|
|
40
|
-
})
|
|
41
|
-
.strict()
|
|
42
|
-
.transform(parseExtractedMemory);
|
|
43
|
-
const extractedMemoryCacheSchema = z.union([
|
|
44
|
-
z
|
|
45
|
-
.object({
|
|
46
|
-
costUsd: z.number().finite().nonnegative().optional(),
|
|
47
|
-
memories: z.array(extractedMemorySchema).max(5),
|
|
48
|
-
})
|
|
49
|
-
.strict(),
|
|
50
|
-
z
|
|
51
|
-
.array(extractedMemorySchema)
|
|
52
|
-
.max(5)
|
|
53
|
-
.transform((memories) => ({ memories })),
|
|
54
|
-
]);
|
|
55
|
-
|
|
56
|
-
/** Subject for a passively extracted memory, or drop when unproven. */
|
|
57
|
-
type MemorySubjectTarget = "drop" | "user" | "conversation";
|
|
58
|
-
|
|
59
|
-
function recordCapturedMemory(
|
|
60
|
-
captured: ReturnType<typeof capturedMemory>[],
|
|
61
|
-
result: CreateMemoryResult,
|
|
62
|
-
): void {
|
|
63
|
-
const supersededIds = new Set(result.supersededIds ?? []);
|
|
64
|
-
for (let index = captured.length - 1; index >= 0; index -= 1) {
|
|
65
|
-
if (supersededIds.has(captured[index]!.id)) {
|
|
66
|
-
captured.splice(index, 1);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
if (result.created || result.idempotent) {
|
|
70
|
-
captured.push(capturedMemory(result.memory));
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/** A cited entry is a run-actor durable instruction evidence entry. */
|
|
75
|
-
function isRunActorInstruction(entry: PluginRunTranscriptEntry): boolean {
|
|
76
|
-
return (
|
|
77
|
-
entry.type === "message" &&
|
|
78
|
-
entry.role === "user" &&
|
|
79
|
-
entry.provenance?.authority === "instruction" &&
|
|
80
|
-
entry.isRunActor === true
|
|
81
|
-
);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/** A cited entry is valid public conversation evidence for shared knowledge. */
|
|
85
|
-
function isConversationEvidence(entry: PluginRunTranscriptEntry): boolean {
|
|
86
|
-
if (entry.type === "toolResult") {
|
|
87
|
-
return entry.isError === false && Boolean(entry.text?.trim());
|
|
88
|
-
}
|
|
89
|
-
if (
|
|
90
|
-
entry.type === "message" &&
|
|
91
|
-
entry.role === "user" &&
|
|
92
|
-
entry.provenance?.authority === "instruction" &&
|
|
93
|
-
entry.isRunActor === false
|
|
94
|
-
) {
|
|
95
|
-
return Boolean(entry.provenance.actor);
|
|
96
|
-
}
|
|
97
|
-
return (
|
|
98
|
-
entry.type === "message" &&
|
|
99
|
-
entry.role === "user" &&
|
|
100
|
-
entry.provenance?.authority === "context"
|
|
101
|
-
);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/** Resolve the deduplicated cited transcript entries, failing on bad indices. */
|
|
105
|
-
function citedEntries(
|
|
106
|
-
indices: number[],
|
|
107
|
-
transcript: PluginRunTranscriptEntry[],
|
|
108
|
-
): { valid: boolean; entries: PluginRunTranscriptEntry[] } {
|
|
109
|
-
const seen = new Set<number>();
|
|
110
|
-
const entries: PluginRunTranscriptEntry[] = [];
|
|
111
|
-
for (const index of indices) {
|
|
112
|
-
if (seen.has(index)) {
|
|
113
|
-
continue;
|
|
114
|
-
}
|
|
115
|
-
seen.add(index);
|
|
116
|
-
const entry = transcript[index];
|
|
117
|
-
if (!entry) {
|
|
118
|
-
return { valid: false, entries: [] };
|
|
119
|
-
}
|
|
120
|
-
entries.push(entry);
|
|
121
|
-
}
|
|
122
|
-
return { valid: entries.length > 0, entries };
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Verify an extracted memory against runtime-owned provenance on its cited
|
|
127
|
-
* evidence. This is a deterministic authority boundary, not a model decision:
|
|
128
|
-
* personal preferences require a single-actor run whose citations are all
|
|
129
|
-
* run-actor instructions, conversation knowledge requires run-actor instruction
|
|
130
|
-
* or valid public conversation evidence, and anything unproven (including
|
|
131
|
-
* missing provenance) is dropped. Multi-actor runs interleave first-person
|
|
132
|
-
* statements from different people, so they never store a preference regardless
|
|
133
|
-
* of citations; a personal preference can wait for a single-actor run.
|
|
134
|
-
*/
|
|
135
|
-
function routeExtractedMemory(
|
|
136
|
-
memory: ExtractedMemory,
|
|
137
|
-
transcript: PluginRunTranscriptEntry[],
|
|
138
|
-
run: Pick<PluginRunContext, "actor" | "actors" | "actorUserId">,
|
|
139
|
-
): MemorySubjectTarget {
|
|
140
|
-
const cited = citedEntries(memory.evidenceMessageIndices, transcript);
|
|
141
|
-
if (!cited.valid) {
|
|
142
|
-
return "drop";
|
|
143
|
-
}
|
|
144
|
-
if (memory.kind === "preference") {
|
|
145
|
-
// Only a run attributed to exactly one human run actor may store a preference.
|
|
146
|
-
const exactlyOneHumanRunActor =
|
|
147
|
-
run.actorUserId !== undefined &&
|
|
148
|
-
run.actor !== undefined &&
|
|
149
|
-
run.actor.platform !== "system" &&
|
|
150
|
-
run.actors.length === 1 &&
|
|
151
|
-
run.actors[0]?.platform !== "system";
|
|
152
|
-
if (!exactlyOneHumanRunActor) {
|
|
153
|
-
return "drop";
|
|
154
|
-
}
|
|
155
|
-
// Never downgrade an unproven first-person preference to conversation subject.
|
|
156
|
-
return cited.entries.every(isRunActorInstruction) ? "user" : "drop";
|
|
157
|
-
}
|
|
158
|
-
return cited.entries.every(
|
|
159
|
-
(entry) => isRunActorInstruction(entry) || isConversationEvidence(entry),
|
|
160
|
-
)
|
|
161
|
-
? "conversation"
|
|
162
|
-
: "drop";
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
function memoryIdempotencySuffix(
|
|
166
|
-
memory: ExtractedMemory,
|
|
167
|
-
target: MemorySubjectTarget,
|
|
168
|
-
): string {
|
|
169
|
-
return createHash("sha256")
|
|
170
|
-
.update(target)
|
|
171
|
-
.update("\0")
|
|
172
|
-
.update(memory.kind)
|
|
173
|
-
.update("\0")
|
|
174
|
-
.update(memory.content)
|
|
175
|
-
.update("\0")
|
|
176
|
-
.update(memory.expiresAtMs === null ? "never" : String(memory.expiresAtMs))
|
|
177
|
-
.digest("hex")
|
|
178
|
-
.slice(0, 32);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function passiveInput(
|
|
182
|
-
sessionId: string,
|
|
183
|
-
memory: ExtractedMemory,
|
|
184
|
-
sourceKey: string,
|
|
185
|
-
target: MemorySubjectTarget,
|
|
186
|
-
): CreateMemoryInput {
|
|
187
|
-
return {
|
|
188
|
-
content: memory.content,
|
|
189
|
-
idempotencyKey: `session:${sourceKey}:${sessionId}:${memoryIdempotencySuffix(memory, target)}`,
|
|
190
|
-
kind: memory.kind,
|
|
191
|
-
...(memory.expiresAtMs !== null
|
|
192
|
-
? { expiresAtMs: memory.expiresAtMs }
|
|
193
|
-
: undefined),
|
|
194
|
-
};
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
async function getTaskExtraction(
|
|
198
|
-
context: PluginTaskContext,
|
|
199
|
-
extract: () => Promise<MemoryExtractionResult>,
|
|
200
|
-
): Promise<MemoryExtractionResult> {
|
|
201
|
-
const cacheKey = `memory-extraction:${context.id}`;
|
|
202
|
-
const cached = await context.state.get(cacheKey);
|
|
203
|
-
if (cached !== undefined) {
|
|
204
|
-
const parsed = extractedMemoryCacheSchema.safeParse(cached);
|
|
205
|
-
if (parsed.success) {
|
|
206
|
-
return parsed.data;
|
|
207
|
-
}
|
|
208
|
-
await context.state.delete(cacheKey);
|
|
209
|
-
}
|
|
210
|
-
const extraction = await extract();
|
|
211
|
-
await context.state.set(cacheKey, extraction, MEMORY_TASK_STATE_TTL_MS);
|
|
212
|
-
return extraction;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
/**
|
|
216
|
-
* Extract and store memories from a completed session plugin task.
|
|
217
|
-
*
|
|
218
|
-
* Memory owns learning after a run and reads only the plugin run data.
|
|
219
|
-
* Explicit memory tools stay separate so retries do not reinterpret user
|
|
220
|
-
* requests.
|
|
221
|
-
*/
|
|
222
|
-
export async function processMemorySession(
|
|
223
|
-
context: PluginTaskContext,
|
|
224
|
-
): Promise<void> {
|
|
225
|
-
const run = await context.run.load();
|
|
226
|
-
// TODO(dcramer): Replace this hardcoded Source check when a plugin Run can
|
|
227
|
-
// state whether passive memory extraction should run.
|
|
228
|
-
if (run.source.kind !== "slack" && run.source.kind !== "web") {
|
|
229
|
-
return;
|
|
230
|
-
}
|
|
231
|
-
// Memory tool turns already own memory management or recall; do not reinterpret
|
|
232
|
-
// recalled memory output as fresh passive-learning evidence.
|
|
233
|
-
if (
|
|
234
|
-
run.transcript.some(
|
|
235
|
-
(entry) =>
|
|
236
|
-
entry.type === "toolResult" && MEMORY_TOOL_NAMES.has(entry.toolName),
|
|
237
|
-
)
|
|
238
|
-
) {
|
|
239
|
-
return;
|
|
240
|
-
}
|
|
241
|
-
const sourceKey = getSourceKey(run.source);
|
|
242
|
-
if (!sourceKey || (run.source.visibility === "private" && !run.actorUserId)) {
|
|
243
|
-
return;
|
|
244
|
-
}
|
|
245
|
-
const transcript = run.transcript
|
|
246
|
-
.filter((entry) => entry.text?.trim())
|
|
247
|
-
.map((entry) => ({ ...entry, text: entry.text!.trim() }));
|
|
248
|
-
const evidenceText = transcript
|
|
249
|
-
.filter((entry) => entry.type === "toolResult" || entry.role === "user")
|
|
250
|
-
.map((entry) => entry.text)
|
|
251
|
-
.join("\n\n")
|
|
252
|
-
.trim();
|
|
253
|
-
if (!evidenceText) {
|
|
254
|
-
return;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
const runtimeContext = memoryRuntimeContextSchema.parse({
|
|
258
|
-
conversationId: run.conversationId,
|
|
259
|
-
...(run.locationId ? { locationId: run.locationId } : undefined),
|
|
260
|
-
...(run.actor ? { actor: run.actor } : undefined),
|
|
261
|
-
source: run.source,
|
|
262
|
-
...(run.actorUserId ? { userId: run.actorUserId } : undefined),
|
|
263
|
-
});
|
|
264
|
-
const agent = createMemoryAgent(context.model);
|
|
265
|
-
const store = createMemoryStore(context.db as MemoryDb, runtimeContext, {
|
|
266
|
-
embedder: context.embedder,
|
|
267
|
-
supersessionDecider: agent,
|
|
268
|
-
});
|
|
269
|
-
await store.archiveExpiredMemories();
|
|
270
|
-
const extraction = await getTaskExtraction(context, async () => {
|
|
271
|
-
const existingMemories = await store.searchMemories({
|
|
272
|
-
limit: 10,
|
|
273
|
-
query: evidenceText,
|
|
274
|
-
});
|
|
275
|
-
return await agent.extractSessionMemories({
|
|
276
|
-
existingMemories: existingMemories.map((memory) => ({
|
|
277
|
-
content: memory.content,
|
|
278
|
-
})),
|
|
279
|
-
actors: run.actors,
|
|
280
|
-
transcript,
|
|
281
|
-
runtimeContext,
|
|
282
|
-
});
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
const captured: ReturnType<typeof capturedMemory>[] = [];
|
|
286
|
-
for (const memory of extraction.memories) {
|
|
287
|
-
// The routing gate stays even though extraction is also actor-gated:
|
|
288
|
-
// getTaskExtraction caches extraction output for 7 days, so a retry can replay
|
|
289
|
-
// preference proposals cached before this gate existed.
|
|
290
|
-
const target = routeExtractedMemory(memory, transcript, run);
|
|
291
|
-
if (target === "drop") {
|
|
292
|
-
continue;
|
|
293
|
-
}
|
|
294
|
-
const input = passiveInput(run.runId, memory, sourceKey, target);
|
|
295
|
-
if (target === "conversation") {
|
|
296
|
-
const result = await store.createConversationMemory(input);
|
|
297
|
-
recordCapturedMemory(captured, result);
|
|
298
|
-
continue;
|
|
299
|
-
}
|
|
300
|
-
const result = await store.createMemory(input);
|
|
301
|
-
recordCapturedMemory(captured, result);
|
|
302
|
-
}
|
|
303
|
-
await context.events.emit(
|
|
304
|
-
memoriesCapturedEvent({
|
|
305
|
-
memories: captured,
|
|
306
|
-
...(extraction.costUsd !== undefined
|
|
307
|
-
? { costUsd: extraction.costUsd }
|
|
308
|
-
: undefined),
|
|
309
|
-
}),
|
|
310
|
-
);
|
|
311
|
-
}
|
package/src/ranking.ts
DELETED
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
import type { MemoryRecord } from "./store";
|
|
2
|
-
|
|
3
|
-
const RECIPROCAL_RANK_FUSION_K = 60;
|
|
4
|
-
const ONE_DAY_MS = 24 * 60 * 60 * 1000;
|
|
5
|
-
const DEFAULT_RRF_WEIGHT = 1;
|
|
6
|
-
|
|
7
|
-
export interface MemoryMatch {
|
|
8
|
-
lexical?: {
|
|
9
|
-
rank: number;
|
|
10
|
-
};
|
|
11
|
-
memory: MemoryRecord;
|
|
12
|
-
vector?: {
|
|
13
|
-
rank: number;
|
|
14
|
-
};
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function reciprocalRank(rank: number, weight: number): number {
|
|
18
|
-
return weight / (RECIPROCAL_RANK_FUSION_K + rank);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function matchScore(
|
|
22
|
-
match: MemoryMatch,
|
|
23
|
-
weights: { lexicalWeight: number; vectorWeight: number },
|
|
24
|
-
): number {
|
|
25
|
-
return (
|
|
26
|
-
(match.vector
|
|
27
|
-
? reciprocalRank(match.vector.rank, weights.vectorWeight)
|
|
28
|
-
: 0) +
|
|
29
|
-
(match.lexical
|
|
30
|
-
? reciprocalRank(match.lexical.rank, weights.lexicalWeight)
|
|
31
|
-
: 0)
|
|
32
|
-
);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function observedAgeRank(memory: MemoryRecord, nowMs: number): number {
|
|
36
|
-
const ageMs = Math.max(0, nowMs - memory.observedAtMs);
|
|
37
|
-
if (ageMs <= 7 * ONE_DAY_MS) {
|
|
38
|
-
return 3;
|
|
39
|
-
}
|
|
40
|
-
if (ageMs <= 30 * ONE_DAY_MS) {
|
|
41
|
-
return 2;
|
|
42
|
-
}
|
|
43
|
-
if (ageMs <= 90 * ONE_DAY_MS) {
|
|
44
|
-
return 1;
|
|
45
|
-
}
|
|
46
|
-
return 0;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function positiveWeight(value: number | undefined, fallback: number): number {
|
|
50
|
-
return value !== undefined && Number.isFinite(value) && value > 0
|
|
51
|
-
? value
|
|
52
|
-
: fallback;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/** Fuse lexical and vector ranks without comparing provider raw scores. */
|
|
56
|
-
export function rankMemoryMatches(
|
|
57
|
-
matches: MemoryMatch[],
|
|
58
|
-
options: {
|
|
59
|
-
/** Optional RRF weight for the lexical leg. Defaults to 1. */
|
|
60
|
-
lexicalWeight?: number;
|
|
61
|
-
nowMs: number;
|
|
62
|
-
/** Optional RRF weight for the vector leg. Defaults to 1. */
|
|
63
|
-
vectorWeight?: number;
|
|
64
|
-
},
|
|
65
|
-
): MemoryMatch[] {
|
|
66
|
-
const weights = {
|
|
67
|
-
lexicalWeight: positiveWeight(options.lexicalWeight, DEFAULT_RRF_WEIGHT),
|
|
68
|
-
vectorWeight: positiveWeight(options.vectorWeight, DEFAULT_RRF_WEIGHT),
|
|
69
|
-
};
|
|
70
|
-
const byId = new Map<string, MemoryMatch>();
|
|
71
|
-
for (const match of matches) {
|
|
72
|
-
const existing = byId.get(match.memory.id);
|
|
73
|
-
if (!existing) {
|
|
74
|
-
byId.set(match.memory.id, match);
|
|
75
|
-
continue;
|
|
76
|
-
}
|
|
77
|
-
// Keep the first rank from each search. The shared searches run first, so
|
|
78
|
-
// the smaller private searches cannot replace their ranks.
|
|
79
|
-
byId.set(match.memory.id, {
|
|
80
|
-
...existing,
|
|
81
|
-
...(!existing.lexical && match.lexical
|
|
82
|
-
? { lexical: match.lexical }
|
|
83
|
-
: undefined),
|
|
84
|
-
...(!existing.vector && match.vector ? { vector: match.vector } : undefined),
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
return [...byId.values()].sort((left, right) => {
|
|
88
|
-
const scoreDelta = matchScore(right, weights) - matchScore(left, weights);
|
|
89
|
-
if (scoreDelta !== 0) {
|
|
90
|
-
return scoreDelta;
|
|
91
|
-
}
|
|
92
|
-
// Prefer private memory when RRF ties. Public and private searches can give
|
|
93
|
-
// the same rank to common words.
|
|
94
|
-
const privateDelta =
|
|
95
|
-
Number(right.memory.scope === "private") -
|
|
96
|
-
Number(left.memory.scope === "private");
|
|
97
|
-
if (privateDelta !== 0) {
|
|
98
|
-
return privateDelta;
|
|
99
|
-
}
|
|
100
|
-
return (
|
|
101
|
-
observedAgeRank(right.memory, options.nowMs) -
|
|
102
|
-
observedAgeRank(left.memory, options.nowMs) ||
|
|
103
|
-
right.memory.observedAtMs - left.memory.observedAtMs ||
|
|
104
|
-
left.memory.id.localeCompare(right.memory.id)
|
|
105
|
-
);
|
|
106
|
-
});
|
|
107
|
-
}
|
package/src/recall.ts
DELETED
|
@@ -1,212 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
definePromptContext,
|
|
3
|
-
type UserPromptContribution,
|
|
4
|
-
type Actor,
|
|
5
|
-
type Identity,
|
|
6
|
-
type PluginConversationEvents,
|
|
7
|
-
type PluginLogger,
|
|
8
|
-
type Source,
|
|
9
|
-
type User,
|
|
10
|
-
} from "@sentry/junior-plugin-api";
|
|
11
|
-
import { z } from "zod";
|
|
12
|
-
import type { MemoryAgent, MemoryRecallResult } from "./agent";
|
|
13
|
-
import { memoriesRecalledEvent } from "./events";
|
|
14
|
-
import {
|
|
15
|
-
createMemoryStore,
|
|
16
|
-
type MemoryDb,
|
|
17
|
-
type MemoryEmbeddingProvider,
|
|
18
|
-
type MemoryRecord,
|
|
19
|
-
} from "./store";
|
|
20
|
-
import { memoryRuntimeContextSchema } from "./types";
|
|
21
|
-
|
|
22
|
-
const RECALL_CANDIDATE_LIMIT = 20;
|
|
23
|
-
const MAX_PROMPT_CHARS = 4_000;
|
|
24
|
-
const MAX_MEMORY_LINE_CHARS = 600;
|
|
25
|
-
|
|
26
|
-
export interface MemoryRecallContext {
|
|
27
|
-
agent: Pick<MemoryAgent, "selectRelevantMemories">;
|
|
28
|
-
conversationId?: string;
|
|
29
|
-
db: MemoryDb;
|
|
30
|
-
embedder?: MemoryEmbeddingProvider;
|
|
31
|
-
events?: PluginConversationEvents;
|
|
32
|
-
log: PluginLogger;
|
|
33
|
-
locationId?: string;
|
|
34
|
-
actor?: Actor;
|
|
35
|
-
source: Source;
|
|
36
|
-
text: string;
|
|
37
|
-
users: {
|
|
38
|
-
resolveActor(): Promise<{ identity: Identity; user?: User } | undefined>;
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function trimContent(content: string, maxLength: number): string {
|
|
43
|
-
const trimmed = content.trim();
|
|
44
|
-
if (trimmed.length <= maxLength) {
|
|
45
|
-
return trimmed;
|
|
46
|
-
}
|
|
47
|
-
return `${trimmed.slice(0, Math.max(0, maxLength - 3)).trimEnd()}...`;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function formatObservedDate(observedAtMs: number): string {
|
|
51
|
-
return new Date(observedAtMs).toISOString().slice(0, 10);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const recalledMemorySchema = z
|
|
55
|
-
.object({
|
|
56
|
-
id: z.string().min(1),
|
|
57
|
-
content: z.string().min(1).max(MAX_MEMORY_LINE_CHARS),
|
|
58
|
-
observedAtMs: z.number().finite(),
|
|
59
|
-
// Stored version 1 uses the old scope labels. Prompt rendering ignores them.
|
|
60
|
-
scope: z.enum(["personal", "conversation"]),
|
|
61
|
-
kind: z.enum(["preference", "procedure", "knowledge"]),
|
|
62
|
-
})
|
|
63
|
-
.strict();
|
|
64
|
-
|
|
65
|
-
/** Structured snapshot retained for one automatic memory recall. */
|
|
66
|
-
export const memoryRecallContextSchema = z
|
|
67
|
-
.object({
|
|
68
|
-
// Count is a safety rail only. Admission packs by MAX_PROMPT_CHARS.
|
|
69
|
-
memories: z.array(recalledMemorySchema).min(1).max(RECALL_CANDIDATE_LIMIT),
|
|
70
|
-
})
|
|
71
|
-
.strict();
|
|
72
|
-
|
|
73
|
-
type RecalledMemory = z.output<typeof recalledMemorySchema>;
|
|
74
|
-
|
|
75
|
-
function selectPromptMemories(memories: MemoryRecord[]): RecalledMemory[] {
|
|
76
|
-
const header = "Relevant memories for this request:";
|
|
77
|
-
const footer =
|
|
78
|
-
"Treat these as possibly stale context. Current user instructions and repository evidence take priority.";
|
|
79
|
-
const selected: RecalledMemory[] = [];
|
|
80
|
-
let totalChars = header.length + footer.length + 2;
|
|
81
|
-
|
|
82
|
-
for (const memory of memories) {
|
|
83
|
-
const content = trimContent(memory.content, MAX_MEMORY_LINE_CHARS);
|
|
84
|
-
const line = `- Observed ${formatObservedDate(memory.observedAtMs)}: ${content}`;
|
|
85
|
-
if (totalChars + line.length + 1 > MAX_PROMPT_CHARS) {
|
|
86
|
-
break;
|
|
87
|
-
}
|
|
88
|
-
selected.push({
|
|
89
|
-
id: memory.id,
|
|
90
|
-
content,
|
|
91
|
-
observedAtMs: memory.observedAtMs,
|
|
92
|
-
scope: memory.scope === "private" ? "personal" : "conversation",
|
|
93
|
-
kind: memory.kind,
|
|
94
|
-
});
|
|
95
|
-
totalChars += line.length + 1;
|
|
96
|
-
}
|
|
97
|
-
return selected;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function renderMemoryPrompt(memories: RecalledMemory[]): string {
|
|
101
|
-
return [
|
|
102
|
-
"Relevant memories for this request:",
|
|
103
|
-
...memories.map(
|
|
104
|
-
(memory) =>
|
|
105
|
-
`- Observed ${formatObservedDate(memory.observedAtMs)}: ${memory.content}`,
|
|
106
|
-
),
|
|
107
|
-
"",
|
|
108
|
-
"Treat these as possibly stale context. Current user instructions and repository evidence take priority.",
|
|
109
|
-
].join("\n");
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function addUsd(
|
|
113
|
-
left: number | undefined,
|
|
114
|
-
right: number | undefined,
|
|
115
|
-
): number | undefined {
|
|
116
|
-
if (left === undefined) return right;
|
|
117
|
-
if (right === undefined) return left;
|
|
118
|
-
return Math.round((left + right) * 1e12) / 1e12;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
async function emitRecallOutcome(args: {
|
|
122
|
-
costUsd?: number;
|
|
123
|
-
events?: PluginConversationEvents;
|
|
124
|
-
memories: string[];
|
|
125
|
-
}): Promise<void> {
|
|
126
|
-
await args.events?.emit(
|
|
127
|
-
memoriesRecalledEvent({
|
|
128
|
-
memories: args.memories,
|
|
129
|
-
...(args.costUsd !== undefined ? { costUsd: args.costUsd } : undefined),
|
|
130
|
-
}),
|
|
131
|
-
);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const memoryRecallContext = definePromptContext({
|
|
135
|
-
kind: "recall",
|
|
136
|
-
version: 1,
|
|
137
|
-
schema: memoryRecallContextSchema,
|
|
138
|
-
renderPrompt: (content) => renderMemoryPrompt(content.memories),
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
/** Build active memory recall contributions. */
|
|
142
|
-
export async function createMemoryPromptContributions(
|
|
143
|
-
context: MemoryRecallContext,
|
|
144
|
-
): Promise<UserPromptContribution[] | undefined> {
|
|
145
|
-
if (!context.text.trim()) {
|
|
146
|
-
return undefined;
|
|
147
|
-
}
|
|
148
|
-
const actorUser = (await context.users.resolveActor())?.user;
|
|
149
|
-
const runtimeContext = memoryRuntimeContextSchema.parse({
|
|
150
|
-
...(context.conversationId
|
|
151
|
-
? { conversationId: context.conversationId }
|
|
152
|
-
: undefined),
|
|
153
|
-
...(context.actor ? { actor: context.actor } : undefined),
|
|
154
|
-
...(context.locationId ? { locationId: context.locationId } : undefined),
|
|
155
|
-
source: context.source,
|
|
156
|
-
...(actorUser ? { userId: actorUser.id } : undefined),
|
|
157
|
-
});
|
|
158
|
-
let embeddingCostUsd: number | undefined;
|
|
159
|
-
const sourceEmbedder = context.embedder;
|
|
160
|
-
const embedder = sourceEmbedder
|
|
161
|
-
? {
|
|
162
|
-
async embedTexts(input: { texts: string[] }) {
|
|
163
|
-
const result = await sourceEmbedder.embedTexts(input);
|
|
164
|
-
embeddingCostUsd = addUsd(embeddingCostUsd, result.costUsd);
|
|
165
|
-
return result;
|
|
166
|
-
},
|
|
167
|
-
}
|
|
168
|
-
: undefined;
|
|
169
|
-
const candidates = await createMemoryStore(context.db, runtimeContext, {
|
|
170
|
-
embedder,
|
|
171
|
-
}).recallMemories({
|
|
172
|
-
query: context.text,
|
|
173
|
-
limit: RECALL_CANDIDATE_LIMIT,
|
|
174
|
-
});
|
|
175
|
-
if (candidates.length === 0) {
|
|
176
|
-
await emitRecallOutcome({
|
|
177
|
-
...(embeddingCostUsd !== undefined ? { costUsd: embeddingCostUsd } : undefined),
|
|
178
|
-
events: context.events,
|
|
179
|
-
memories: [],
|
|
180
|
-
});
|
|
181
|
-
return undefined;
|
|
182
|
-
}
|
|
183
|
-
let recall: MemoryRecallResult;
|
|
184
|
-
try {
|
|
185
|
-
recall = await context.agent.selectRelevantMemories({
|
|
186
|
-
candidates: candidates.map(({ content, id }) => ({ content, id })),
|
|
187
|
-
userRequest: context.text,
|
|
188
|
-
});
|
|
189
|
-
} catch {
|
|
190
|
-
// Automatic recall is optional context; a relevance-model failure must not
|
|
191
|
-
// prevent the user's turn from continuing without recalled memory.
|
|
192
|
-
context.log.warn("memory_recall_selection_failed");
|
|
193
|
-
return undefined;
|
|
194
|
-
}
|
|
195
|
-
const candidatesById = new Map(
|
|
196
|
-
candidates.map((memory) => [memory.id, memory]),
|
|
197
|
-
);
|
|
198
|
-
const relevant = recall.relevantIds
|
|
199
|
-
.map((id) => candidatesById.get(id))
|
|
200
|
-
.filter((memory): memory is MemoryRecord => memory !== undefined);
|
|
201
|
-
const selected = selectPromptMemories(relevant);
|
|
202
|
-
const costUsd = addUsd(embeddingCostUsd, recall.costUsd);
|
|
203
|
-
await emitRecallOutcome({
|
|
204
|
-
...(costUsd !== undefined ? { costUsd } : undefined),
|
|
205
|
-
events: context.events,
|
|
206
|
-
memories: selected.map(({ id }) => id),
|
|
207
|
-
});
|
|
208
|
-
if (selected.length === 0) {
|
|
209
|
-
return undefined;
|
|
210
|
-
}
|
|
211
|
-
return [memoryRecallContext({ memories: selected })];
|
|
212
|
-
}
|
package/src/scope.ts
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
MemoryRuntimeContext,
|
|
3
|
-
MemoryScope,
|
|
4
|
-
MemorySubjectType,
|
|
5
|
-
} from "./types";
|
|
6
|
-
|
|
7
|
-
const PUBLIC_SCOPE_KEY = "public";
|
|
8
|
-
|
|
9
|
-
/** Stored memory access rule. */
|
|
10
|
-
export interface ResolvedMemoryScope {
|
|
11
|
-
scope: MemoryScope;
|
|
12
|
-
scopeKey: string;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/** What a stored memory is about. */
|
|
16
|
-
export interface ResolvedMemorySubject {
|
|
17
|
-
subjectKey: string;
|
|
18
|
-
subjectType: Extract<MemorySubjectType, "user" | "conversation">;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/** Public memories are visible everywhere. */
|
|
22
|
-
export const publicMemoryScope: ResolvedMemoryScope = {
|
|
23
|
-
scope: "public",
|
|
24
|
-
scopeKey: PUBLIC_SCOPE_KEY,
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
function privateMemoryScope(userId: string): ResolvedMemoryScope {
|
|
28
|
-
return { scope: "private", scopeKey: userId };
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/** Set memory access from the Source. */
|
|
32
|
-
export function deriveMemoryScope(
|
|
33
|
-
ctx: MemoryRuntimeContext,
|
|
34
|
-
): ResolvedMemoryScope {
|
|
35
|
-
if ("visibility" in ctx.source && ctx.source.visibility === "public") {
|
|
36
|
-
return publicMemoryScope;
|
|
37
|
-
}
|
|
38
|
-
if (!ctx.userId) {
|
|
39
|
-
throw new Error("Private memory requires a User.");
|
|
40
|
-
}
|
|
41
|
-
return privateMemoryScope(ctx.userId);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/** Set what a memory is about. Access is set separately. */
|
|
45
|
-
export function deriveMemorySubject(
|
|
46
|
-
ctx: MemoryRuntimeContext,
|
|
47
|
-
subjectType: Extract<MemorySubjectType, "user" | "conversation">,
|
|
48
|
-
): ResolvedMemorySubject {
|
|
49
|
-
if (subjectType === "user") {
|
|
50
|
-
if (!ctx.userId) {
|
|
51
|
-
throw new Error("User memory requires a User.");
|
|
52
|
-
}
|
|
53
|
-
return { subjectType, subjectKey: ctx.userId };
|
|
54
|
-
}
|
|
55
|
-
const subjectKey = ctx.conversationId;
|
|
56
|
-
if (!subjectKey) {
|
|
57
|
-
throw new Error(
|
|
58
|
-
"Conversation-subject memory requires conversation context.",
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
return {
|
|
62
|
-
subjectType,
|
|
63
|
-
subjectKey,
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/** Return the memory scopes that the current User can access. */
|
|
68
|
-
export function deriveVisibleMemoryScopes(
|
|
69
|
-
ctx: MemoryRuntimeContext,
|
|
70
|
-
): ResolvedMemoryScope[] {
|
|
71
|
-
if (!ctx.userId) {
|
|
72
|
-
return [publicMemoryScope];
|
|
73
|
-
}
|
|
74
|
-
return [publicMemoryScope, privateMemoryScope(ctx.userId)];
|
|
75
|
-
}
|