@sentry/junior-memory 0.119.0 → 0.121.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/README.md +22 -6
- package/dist/agent.d.ts +11 -1
- package/dist/api.d.ts +45 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +794 -270
- package/dist/index.js.map +1 -1
- package/dist/operational-report.d.ts +7 -0
- package/dist/personal-store.d.ts +39 -0
- package/dist/personal.d.ts +21 -0
- package/dist/ranking.d.ts +20 -0
- package/dist/recall.d.ts +5 -7
- package/dist/store.d.ts +25 -0
- package/dist/user-pages.d.ts +2 -2
- package/migrations/0005_short_jamie_braddock.sql +1 -0
- package/migrations/meta/0005_snapshot.json +364 -0
- package/migrations/meta/_journal.json +7 -0
- package/package.json +2 -2
- package/src/agent.ts +65 -1
- package/src/api.ts +168 -0
- package/src/db/schema.ts +5 -0
- package/src/index.ts +6 -0
- package/src/operational-report.ts +191 -0
- package/src/personal-store.ts +195 -0
- package/src/personal.ts +120 -0
- package/src/plugin.ts +16 -0
- package/src/ranking.ts +98 -0
- package/src/recall.ts +32 -13
- package/src/store.ts +151 -236
- package/src/user-pages.ts +27 -62
package/src/plugin.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { defineJuniorPlugin } from "@sentry/junior-plugin-api";
|
|
2
2
|
import { createMemoryAgent } from "./agent";
|
|
3
|
+
import { createMemoryApi } from "./api";
|
|
3
4
|
import { createMemoryCliCommand } from "./cli";
|
|
4
5
|
import {
|
|
5
6
|
createMemoryCreateTool,
|
|
@@ -12,6 +13,7 @@ import {
|
|
|
12
13
|
} from "./tools";
|
|
13
14
|
import { processMemorySession } from "./process-session";
|
|
14
15
|
import { createMemoryPromptContributions } from "./recall";
|
|
16
|
+
import { buildMemoryOperationalReport } from "./operational-report";
|
|
15
17
|
import type { MemoryDb } from "./store";
|
|
16
18
|
import { createMemoryUserPage } from "./user-pages";
|
|
17
19
|
|
|
@@ -110,6 +112,18 @@ export function memoryPlugin(options: MemoryPluginOptions = {}) {
|
|
|
110
112
|
},
|
|
111
113
|
userPages: [createMemoryUserPage()],
|
|
112
114
|
hooks: {
|
|
115
|
+
async operationalReport(ctx) {
|
|
116
|
+
return await buildMemoryOperationalReport({
|
|
117
|
+
db: ctx.db as MemoryDb,
|
|
118
|
+
nowMs: ctx.nowMs,
|
|
119
|
+
});
|
|
120
|
+
},
|
|
121
|
+
apiRoutes(ctx) {
|
|
122
|
+
return createMemoryApi({
|
|
123
|
+
actors: ctx.viewer.actors,
|
|
124
|
+
db: ctx.db as MemoryDb,
|
|
125
|
+
});
|
|
126
|
+
},
|
|
113
127
|
tools(ctx) {
|
|
114
128
|
const agent = createMemoryAgent(ctx.model);
|
|
115
129
|
const context = memoryToolContext({
|
|
@@ -135,10 +149,12 @@ export function memoryPlugin(options: MemoryPluginOptions = {}) {
|
|
|
135
149
|
},
|
|
136
150
|
async userPrompt(ctx) {
|
|
137
151
|
return await createMemoryPromptContributions({
|
|
152
|
+
agent: createMemoryAgent(ctx.model),
|
|
138
153
|
...(ctx.conversationId ? { conversationId: ctx.conversationId } : {}),
|
|
139
154
|
...(ctx.actor ? { actor: ctx.actor } : {}),
|
|
140
155
|
db: ctx.db as MemoryDb,
|
|
141
156
|
embedder: ctx.embedder,
|
|
157
|
+
log: ctx.log,
|
|
142
158
|
maxVectorDistance: recallMaxVectorDistance(options),
|
|
143
159
|
source: ctx.source,
|
|
144
160
|
text: ctx.text,
|
package/src/ranking.ts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { MemoryRecord } from "./store";
|
|
2
|
+
|
|
3
|
+
const RECIPROCAL_RANK_FUSION_K = 60;
|
|
4
|
+
const ONE_DAY_MS = 24 * 60 * 60 * 1000;
|
|
5
|
+
|
|
6
|
+
export interface MemoryMatch {
|
|
7
|
+
exactIdentifier: boolean;
|
|
8
|
+
lexical?: {
|
|
9
|
+
rank: number;
|
|
10
|
+
};
|
|
11
|
+
memory: MemoryRecord;
|
|
12
|
+
sourceKey: string;
|
|
13
|
+
vector?: {
|
|
14
|
+
rank: number;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function reciprocalRank(rank: number): number {
|
|
19
|
+
return 1 / (RECIPROCAL_RANK_FUSION_K + rank);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function matchScore(match: MemoryMatch): number {
|
|
23
|
+
return (
|
|
24
|
+
(match.vector ? reciprocalRank(match.vector.rank) : 0) +
|
|
25
|
+
(match.lexical ? reciprocalRank(match.lexical.rank) : 0)
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function currentChannel(
|
|
30
|
+
match: Pick<MemoryMatch, "sourceKey">,
|
|
31
|
+
channelPrefix: string | undefined,
|
|
32
|
+
): boolean {
|
|
33
|
+
return channelPrefix ? match.sourceKey.startsWith(channelPrefix) : false;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function observedAgeRank(memory: MemoryRecord, nowMs: number): number {
|
|
37
|
+
const ageMs = Math.max(0, nowMs - memory.observedAtMs);
|
|
38
|
+
if (ageMs <= 7 * ONE_DAY_MS) {
|
|
39
|
+
return 3;
|
|
40
|
+
}
|
|
41
|
+
if (ageMs <= 30 * ONE_DAY_MS) {
|
|
42
|
+
return 2;
|
|
43
|
+
}
|
|
44
|
+
if (ageMs <= 90 * ONE_DAY_MS) {
|
|
45
|
+
return 1;
|
|
46
|
+
}
|
|
47
|
+
return 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Keep exact structured-identifier matches in the strongest relevance tier,
|
|
52
|
+
* then fuse lexical and vector ranks without comparing provider raw scores.
|
|
53
|
+
*/
|
|
54
|
+
export function rankMemoryMatches(
|
|
55
|
+
matches: MemoryMatch[],
|
|
56
|
+
options: {
|
|
57
|
+
channelPrefix?: string;
|
|
58
|
+
nowMs: number;
|
|
59
|
+
},
|
|
60
|
+
): MemoryMatch[] {
|
|
61
|
+
const byId = new Map<string, MemoryMatch>();
|
|
62
|
+
for (const match of matches) {
|
|
63
|
+
const existing = byId.get(match.memory.id);
|
|
64
|
+
if (!existing) {
|
|
65
|
+
byId.set(match.memory.id, match);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
byId.set(match.memory.id, {
|
|
69
|
+
...existing,
|
|
70
|
+
exactIdentifier: existing.exactIdentifier || match.exactIdentifier,
|
|
71
|
+
...(match.lexical ? { lexical: match.lexical } : {}),
|
|
72
|
+
...(match.vector ? { vector: match.vector } : {}),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
return [...byId.values()].sort((left, right) => {
|
|
76
|
+
const identifierDelta =
|
|
77
|
+
Number(right.exactIdentifier) - Number(left.exactIdentifier);
|
|
78
|
+
if (identifierDelta !== 0) {
|
|
79
|
+
return identifierDelta;
|
|
80
|
+
}
|
|
81
|
+
const scoreDelta = matchScore(right) - matchScore(left);
|
|
82
|
+
if (scoreDelta !== 0) {
|
|
83
|
+
return scoreDelta;
|
|
84
|
+
}
|
|
85
|
+
const channelDelta =
|
|
86
|
+
Number(currentChannel(right, options.channelPrefix)) -
|
|
87
|
+
Number(currentChannel(left, options.channelPrefix));
|
|
88
|
+
if (channelDelta !== 0) {
|
|
89
|
+
return channelDelta;
|
|
90
|
+
}
|
|
91
|
+
return (
|
|
92
|
+
observedAgeRank(right.memory, options.nowMs) -
|
|
93
|
+
observedAgeRank(left.memory, options.nowMs) ||
|
|
94
|
+
right.memory.observedAtMs - left.memory.observedAtMs ||
|
|
95
|
+
left.memory.id.localeCompare(right.memory.id)
|
|
96
|
+
);
|
|
97
|
+
});
|
|
98
|
+
}
|
package/src/recall.ts
CHANGED
|
@@ -2,9 +2,11 @@ import {
|
|
|
2
2
|
definePromptContext,
|
|
3
3
|
type UserPromptContribution,
|
|
4
4
|
type Actor,
|
|
5
|
+
type PluginLogger,
|
|
5
6
|
type Source,
|
|
6
7
|
} from "@sentry/junior-plugin-api";
|
|
7
8
|
import { z } from "zod";
|
|
9
|
+
import type { MemoryAgent } from "./agent";
|
|
8
10
|
import {
|
|
9
11
|
createMemoryStore,
|
|
10
12
|
type MemoryDb,
|
|
@@ -14,13 +16,16 @@ import {
|
|
|
14
16
|
import { memoryRuntimeContextSchema } from "./types";
|
|
15
17
|
|
|
16
18
|
const DEFAULT_RECALL_LIMIT = 5;
|
|
19
|
+
const RECALL_CANDIDATE_LIMIT = 20;
|
|
17
20
|
const MAX_PROMPT_CHARS = 4_000;
|
|
18
21
|
const MAX_MEMORY_LINE_CHARS = 600;
|
|
19
22
|
|
|
20
23
|
export interface MemoryRecallContext {
|
|
24
|
+
agent: Pick<MemoryAgent, "selectRelevantMemories">;
|
|
21
25
|
conversationId?: string;
|
|
22
26
|
db: MemoryDb;
|
|
23
27
|
embedder?: MemoryEmbeddingProvider;
|
|
28
|
+
log: PluginLogger;
|
|
24
29
|
/** Maximum cosine distance for vector recall. Passed through to the memory store. */
|
|
25
30
|
maxVectorDistance?: number;
|
|
26
31
|
actor?: Actor;
|
|
@@ -103,12 +108,7 @@ const memoryRecallContext = definePromptContext({
|
|
|
103
108
|
renderPrompt: (content) => renderMemoryPrompt(content.memories),
|
|
104
109
|
});
|
|
105
110
|
|
|
106
|
-
/**
|
|
107
|
-
* Build active memory recall contributions.
|
|
108
|
-
*
|
|
109
|
-
* Personal recall remains prompt-only so conversation reporting cannot widen
|
|
110
|
-
* actor-scoped memory visibility.
|
|
111
|
-
*/
|
|
111
|
+
/** Build active memory recall contributions. */
|
|
112
112
|
export async function createMemoryPromptContributions(
|
|
113
113
|
context: MemoryRecallContext,
|
|
114
114
|
): Promise<UserPromptContribution[] | undefined> {
|
|
@@ -122,21 +122,40 @@ export async function createMemoryPromptContributions(
|
|
|
122
122
|
...(context.actor ? { actor: context.actor } : {}),
|
|
123
123
|
source: context.source,
|
|
124
124
|
});
|
|
125
|
-
const
|
|
125
|
+
const candidates = await createMemoryStore(context.db, runtimeContext, {
|
|
126
126
|
...(context.embedder ? { embedder: context.embedder } : {}),
|
|
127
127
|
...(context.maxVectorDistance !== undefined
|
|
128
128
|
? { maxVectorDistance: context.maxVectorDistance }
|
|
129
129
|
: {}),
|
|
130
|
-
}).
|
|
130
|
+
}).recallMemories({
|
|
131
131
|
query: context.text,
|
|
132
|
-
limit:
|
|
132
|
+
limit: RECALL_CANDIDATE_LIMIT,
|
|
133
133
|
});
|
|
134
|
-
|
|
135
|
-
|
|
134
|
+
if (candidates.length === 0) {
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
let relevantIds: string[];
|
|
138
|
+
try {
|
|
139
|
+
relevantIds = await context.agent.selectRelevantMemories({
|
|
140
|
+
candidates: candidates.map(({ content, id }) => ({ content, id })),
|
|
141
|
+
userRequest: context.text,
|
|
142
|
+
});
|
|
143
|
+
} catch {
|
|
144
|
+
// Automatic recall is optional context; a relevance-model failure must not
|
|
145
|
+
// prevent the user's turn from continuing without recalled memory.
|
|
146
|
+
context.log.warn("memory_recall_selection_failed");
|
|
136
147
|
return undefined;
|
|
137
148
|
}
|
|
138
|
-
|
|
139
|
-
|
|
149
|
+
const candidatesById = new Map(
|
|
150
|
+
candidates.map((memory) => [memory.id, memory]),
|
|
151
|
+
);
|
|
152
|
+
const relevant = relevantIds
|
|
153
|
+
.map((id) => candidatesById.get(id))
|
|
154
|
+
.filter((memory): memory is MemoryRecord => memory !== undefined)
|
|
155
|
+
.slice(0, DEFAULT_RECALL_LIMIT);
|
|
156
|
+
const selected = selectPromptMemories(relevant);
|
|
157
|
+
if (selected.length === 0) {
|
|
158
|
+
return undefined;
|
|
140
159
|
}
|
|
141
160
|
return [memoryRecallContext({ memories: selected })];
|
|
142
161
|
}
|