@sentry/junior-memory 0.124.2 → 0.125.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 +11 -1
- package/dist/agent.d.ts +12 -2
- package/dist/api.d.ts +7 -1
- package/dist/events.d.ts +17 -1
- package/dist/index.js +168 -47
- package/dist/index.js.map +1 -1
- package/dist/operational-report.d.ts +2 -1
- package/dist/recall.d.ts +2 -1
- package/dist/store.d.ts +1 -0
- package/package.json +2 -2
- package/src/agent.ts +26 -8
- package/src/api.ts +20 -2
- package/src/events.ts +53 -16
- package/src/operational-report.ts +37 -1
- package/src/plugin.ts +17 -2
- package/src/process-session.ts +39 -28
- package/src/recall.ts +52 -5
- package/src/store.ts +2 -0
- package/src/tools.ts +1 -0
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import type { PluginOperationalReportContent } from "@sentry/junior-plugin-api";
|
|
1
|
+
import type { PluginConversationEventCostDay, PluginOperationalReportContent } from "@sentry/junior-plugin-api";
|
|
2
2
|
import type { MemoryDb } from "./store";
|
|
3
3
|
/** Build aggregate memory storage and indexing diagnostics for the System page. */
|
|
4
4
|
export declare function buildMemoryOperationalReport(args: {
|
|
5
5
|
db: MemoryDb;
|
|
6
|
+
extractionDays: PluginConversationEventCostDay[];
|
|
6
7
|
nowMs: number;
|
|
7
8
|
}): Promise<PluginOperationalReportContent>;
|
package/dist/recall.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type UserPromptContribution, type Actor, type PluginLogger, type Source } from "@sentry/junior-plugin-api";
|
|
1
|
+
import { type UserPromptContribution, type Actor, type PluginConversationEvents, type PluginLogger, type Source } from "@sentry/junior-plugin-api";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import type { MemoryAgent } from "./agent";
|
|
4
4
|
import { type MemoryDb, type MemoryEmbeddingProvider } from "./store";
|
|
@@ -7,6 +7,7 @@ export interface MemoryRecallContext {
|
|
|
7
7
|
conversationId?: string;
|
|
8
8
|
db: MemoryDb;
|
|
9
9
|
embedder?: MemoryEmbeddingProvider;
|
|
10
|
+
events?: PluginConversationEvents;
|
|
10
11
|
log: PluginLogger;
|
|
11
12
|
/** Maximum cosine distance for vector recall. Passed through to the memory store. */
|
|
12
13
|
maxVectorDistance?: number;
|
package/dist/store.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sentry/junior-memory",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.125.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"commander": "^14.0.3",
|
|
28
28
|
"drizzle-orm": "^0.45.2",
|
|
29
29
|
"zod": "^4.4.3",
|
|
30
|
-
"@sentry/junior-plugin-api": "0.
|
|
30
|
+
"@sentry/junior-plugin-api": "0.125.0"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"@types/node": "^25.9.1",
|
package/src/agent.ts
CHANGED
|
@@ -199,18 +199,30 @@ export type ExtractSessionRequest = z.output<
|
|
|
199
199
|
>;
|
|
200
200
|
export type ExtractedMemory = z.output<typeof extractedMemoryResultSchema>;
|
|
201
201
|
|
|
202
|
+
/** Memories proposed by passive extraction and the model cost of that pass. */
|
|
203
|
+
export type MemoryExtractionResult = {
|
|
204
|
+
costUsd?: number;
|
|
205
|
+
memories: ExtractedMemory[];
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
/** Memories admitted by automatic recall and the model cost of that decision. */
|
|
209
|
+
export type MemoryRecallResult = {
|
|
210
|
+
costUsd?: number;
|
|
211
|
+
relevantIds: string[];
|
|
212
|
+
};
|
|
213
|
+
|
|
202
214
|
export interface MemoryAgent {
|
|
203
215
|
/** Select candidate memories that directly help with the current request. */
|
|
204
216
|
selectRelevantMemories(
|
|
205
217
|
request: MemoryRecallInput,
|
|
206
|
-
): Promise<
|
|
218
|
+
): Promise<MemoryRecallResult> | MemoryRecallResult;
|
|
207
219
|
/** Classify a new preference against related active preferences. */
|
|
208
220
|
adjudicateSupersession(
|
|
209
221
|
request: MemorySupersessionInput,
|
|
210
222
|
): Promise<MemorySupersessionDecision> | MemorySupersessionDecision;
|
|
211
223
|
extractSessionMemories(
|
|
212
224
|
request: ExtractSessionRequest,
|
|
213
|
-
): Promise<
|
|
225
|
+
): Promise<MemoryExtractionResult> | MemoryExtractionResult;
|
|
214
226
|
reviewCreateRequest(
|
|
215
227
|
request: CreateMemoryRequest,
|
|
216
228
|
): Promise<MemoryReview> | MemoryReview;
|
|
@@ -540,9 +552,12 @@ export function createMemoryAgent(model: PluginModel): MemoryAgent {
|
|
|
540
552
|
});
|
|
541
553
|
const decision = memoryRecallDecisionSchema.parse(result.object);
|
|
542
554
|
const candidateIds = new Set(request.candidates.map(({ id }) => id));
|
|
543
|
-
return
|
|
544
|
-
|
|
545
|
-
|
|
555
|
+
return {
|
|
556
|
+
relevantIds: [...new Set(decision.relevantIds)].filter((id) =>
|
|
557
|
+
candidateIds.has(id),
|
|
558
|
+
),
|
|
559
|
+
...(result.costUsd !== undefined ? { costUsd: result.costUsd } : {}),
|
|
560
|
+
};
|
|
546
561
|
},
|
|
547
562
|
async adjudicateSupersession(rawRequest) {
|
|
548
563
|
const request = memorySupersessionInputSchema.parse(rawRequest);
|
|
@@ -562,9 +577,12 @@ export function createMemoryAgent(model: PluginModel): MemoryAgent {
|
|
|
562
577
|
prompt: sessionExtractionPrompt(request),
|
|
563
578
|
maxTokens: 1_000,
|
|
564
579
|
});
|
|
565
|
-
return
|
|
566
|
-
|
|
567
|
-
|
|
580
|
+
return {
|
|
581
|
+
memories: extractedMemoriesFromResponse(
|
|
582
|
+
extractMemoriesResponseSchema.parse(result.object),
|
|
583
|
+
),
|
|
584
|
+
...(result.costUsd !== undefined ? { costUsd: result.costUsd } : {}),
|
|
585
|
+
};
|
|
568
586
|
},
|
|
569
587
|
async reviewCreateRequest(rawRequest) {
|
|
570
588
|
const request = parseCreateMemoryRequest(rawRequest);
|
package/src/api.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { z } from "zod";
|
|
9
9
|
import {
|
|
10
10
|
pluginApiRouteRequestContextSchema,
|
|
11
|
+
type PluginConversationEventStats,
|
|
11
12
|
type PluginApiRouteRequestContext,
|
|
12
13
|
type PluginRouteApp,
|
|
13
14
|
type PluginUserPageActor,
|
|
@@ -47,9 +48,18 @@ const memoryDashboardDaySchema = z
|
|
|
47
48
|
})
|
|
48
49
|
.strict();
|
|
49
50
|
|
|
51
|
+
const memoryExtractionDaySchema = z
|
|
52
|
+
.object({
|
|
53
|
+
costUsd: z.number().finite().nonnegative(),
|
|
54
|
+
date: z.iso.date(),
|
|
55
|
+
events: z.number().int().min(0),
|
|
56
|
+
})
|
|
57
|
+
.strict();
|
|
58
|
+
|
|
50
59
|
export const memoryDashboardResponseSchema = z
|
|
51
60
|
.object({
|
|
52
61
|
days: z.array(memoryDashboardDaySchema).length(90),
|
|
62
|
+
extractionDays: z.array(memoryExtractionDaySchema).length(90),
|
|
53
63
|
generatedAt: z.iso.datetime(),
|
|
54
64
|
stats: z
|
|
55
65
|
.object({
|
|
@@ -85,6 +95,7 @@ const memoryListQuerySchema = z
|
|
|
85
95
|
interface MemoryApiOptions {
|
|
86
96
|
actors(email: string): Promise<PluginUserPageActor[]>;
|
|
87
97
|
db: MemoryDb;
|
|
98
|
+
eventStats: PluginConversationEventStats;
|
|
88
99
|
}
|
|
89
100
|
|
|
90
101
|
function json(body: unknown, status = 200): Response {
|
|
@@ -94,7 +105,9 @@ function json(body: unknown, status = 200): Response {
|
|
|
94
105
|
});
|
|
95
106
|
}
|
|
96
107
|
|
|
97
|
-
function apiMemory(
|
|
108
|
+
function apiMemory(
|
|
109
|
+
memory: PersonalMemoryRecord,
|
|
110
|
+
): z.output<typeof memoryApiSchema> {
|
|
98
111
|
return {
|
|
99
112
|
content: memory.content,
|
|
100
113
|
createdAt: new Date(memory.createdAtMs).toISOString(),
|
|
@@ -142,12 +155,17 @@ export function createMemoryApi(options: MemoryApiOptions): PluginRouteApp {
|
|
|
142
155
|
isDashboard &&
|
|
143
156
|
(request.method === "GET" || request.method === "HEAD")
|
|
144
157
|
) {
|
|
145
|
-
const [stats, days] = await Promise.all([
|
|
158
|
+
const [stats, days, extractionDays] = await Promise.all([
|
|
146
159
|
memories.stats(),
|
|
147
160
|
memories.timeline({ days: 90 }),
|
|
161
|
+
options.eventStats.costsByDay({
|
|
162
|
+
days: 90,
|
|
163
|
+
eventName: "memories_captured",
|
|
164
|
+
}),
|
|
148
165
|
]);
|
|
149
166
|
const body = memoryDashboardResponseSchema.parse({
|
|
150
167
|
days,
|
|
168
|
+
extractionDays,
|
|
151
169
|
generatedAt: new Date().toISOString(),
|
|
152
170
|
stats,
|
|
153
171
|
});
|
package/src/events.ts
CHANGED
|
@@ -21,8 +21,41 @@ function eventPreview(content: string): string {
|
|
|
21
21
|
return `${trimmed.slice(0, MAX_EVENT_PREVIEW_CHARS - 3).trimEnd()}...`;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
const capturedMemoriesSchema = z
|
|
25
|
+
.object({
|
|
26
|
+
memories: z.array(capturedMemorySchema).max(100),
|
|
27
|
+
costUsd: z.number().finite().nonnegative().optional(),
|
|
28
|
+
})
|
|
29
|
+
.strict();
|
|
30
|
+
|
|
31
|
+
const recalledMemoriesSchema = z
|
|
32
|
+
.object({
|
|
33
|
+
memories: z.array(z.string().min(1)).max(5),
|
|
34
|
+
costUsd: z.number().finite().nonnegative().optional(),
|
|
35
|
+
})
|
|
36
|
+
.strict();
|
|
37
|
+
|
|
38
|
+
function renderCapturedMemories(
|
|
39
|
+
event: z.output<typeof capturedMemoriesSchema>,
|
|
40
|
+
) {
|
|
41
|
+
const count = event.memories.length;
|
|
42
|
+
if (count === 0) return undefined;
|
|
43
|
+
return {
|
|
44
|
+
icon: "brain" as const,
|
|
45
|
+
title: count === 1 ? "Memory captured" : "Memories captured",
|
|
46
|
+
preview:
|
|
47
|
+
count === 1
|
|
48
|
+
? eventPreview(event.memories[0]!.content)
|
|
49
|
+
: `${count} memories`,
|
|
50
|
+
details: event.memories.map((memory) => ({
|
|
51
|
+
title: memory.content,
|
|
52
|
+
metadata: [memory.kind, memory.scope],
|
|
53
|
+
})),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Previous stored memory-capture event shape retained for transcript rendering. */
|
|
58
|
+
export const memoriesCapturedEventV1 = defineConversationEvent({
|
|
26
59
|
name: "memories_captured",
|
|
27
60
|
version: 1,
|
|
28
61
|
schema: z
|
|
@@ -30,20 +63,24 @@ export const memoriesCapturedEvent = defineConversationEvent({
|
|
|
30
63
|
memories: z.array(capturedMemorySchema).min(1).max(100),
|
|
31
64
|
})
|
|
32
65
|
.strict(),
|
|
33
|
-
renderEvent
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
66
|
+
renderEvent: renderCapturedMemories,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
/** Durable outcome emitted after every completed passive memory extraction. */
|
|
70
|
+
export const memoriesCapturedEvent = defineConversationEvent({
|
|
71
|
+
name: "memories_captured",
|
|
72
|
+
version: 2,
|
|
73
|
+
schema: capturedMemoriesSchema,
|
|
74
|
+
renderEvent: renderCapturedMemories,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
/** Durable outcome emitted after one completed automatic recall attempt. */
|
|
78
|
+
export const memoriesRecalledEvent = defineConversationEvent({
|
|
79
|
+
name: "memories_recalled",
|
|
80
|
+
version: 1,
|
|
81
|
+
schema: recalledMemoriesSchema,
|
|
82
|
+
renderEvent() {
|
|
83
|
+
return undefined;
|
|
47
84
|
},
|
|
48
85
|
});
|
|
49
86
|
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
PluginConversationEventCostDay,
|
|
3
|
+
PluginOperationalReportContent,
|
|
4
|
+
} from "@sentry/junior-plugin-api";
|
|
2
5
|
import { and, eq, gt, isNull, or, sql } from "drizzle-orm";
|
|
3
6
|
import { z } from "zod";
|
|
4
7
|
import { juniorMemoryEmbeddings, juniorMemoryMemories } from "./db/schema";
|
|
@@ -87,9 +90,20 @@ function formatPercent(value: number): string {
|
|
|
87
90
|
}).format(value);
|
|
88
91
|
}
|
|
89
92
|
|
|
93
|
+
function formatUsd(value: number): string {
|
|
94
|
+
const maximumFractionDigits = value > 0 && value < 0.01 ? 4 : 2;
|
|
95
|
+
return new Intl.NumberFormat("en-US", {
|
|
96
|
+
currency: "USD",
|
|
97
|
+
maximumFractionDigits,
|
|
98
|
+
minimumFractionDigits: 2,
|
|
99
|
+
style: "currency",
|
|
100
|
+
}).format(value);
|
|
101
|
+
}
|
|
102
|
+
|
|
90
103
|
/** Build aggregate memory storage and indexing diagnostics for the System page. */
|
|
91
104
|
export async function buildMemoryOperationalReport(args: {
|
|
92
105
|
db: MemoryDb;
|
|
106
|
+
extractionDays: PluginConversationEventCostDay[];
|
|
93
107
|
nowMs: number;
|
|
94
108
|
}): Promise<PluginOperationalReportContent> {
|
|
95
109
|
const active = and(
|
|
@@ -133,6 +147,11 @@ export async function buildMemoryOperationalReport(args: {
|
|
|
133
147
|
const activeCount = counts?.active ?? 0;
|
|
134
148
|
const embeddedCount = counts?.embedded ?? 0;
|
|
135
149
|
const embeddingCoverage = activeCount === 0 ? 0 : embeddedCount / activeCount;
|
|
150
|
+
const extractionThirtyDays = args.extractionDays.slice(-30);
|
|
151
|
+
const extractionCostThirtyDays = extractionThirtyDays.reduce(
|
|
152
|
+
(total, day) => total + day.costUsd,
|
|
153
|
+
0,
|
|
154
|
+
);
|
|
136
155
|
|
|
137
156
|
return {
|
|
138
157
|
generatedAt: new Date(args.nowMs).toISOString(),
|
|
@@ -143,6 +162,10 @@ export async function buildMemoryOperationalReport(args: {
|
|
|
143
162
|
tone: activeCount > 0 ? "good" : "neutral",
|
|
144
163
|
value: formatCount(activeCount),
|
|
145
164
|
},
|
|
165
|
+
{
|
|
166
|
+
label: "extraction cost · 30d",
|
|
167
|
+
value: formatUsd(extractionCostThirtyDays),
|
|
168
|
+
},
|
|
146
169
|
{
|
|
147
170
|
label: "created · 30d",
|
|
148
171
|
value: formatCount(counts?.createdThirtyDays ?? 0),
|
|
@@ -167,6 +190,19 @@ export async function buildMemoryOperationalReport(args: {
|
|
|
167
190
|
},
|
|
168
191
|
],
|
|
169
192
|
widgets: [
|
|
193
|
+
{
|
|
194
|
+
categories: args.extractionDays.map((day) => ({
|
|
195
|
+
id: day.date,
|
|
196
|
+
label: day.date,
|
|
197
|
+
values: { costUsd: day.costUsd },
|
|
198
|
+
})),
|
|
199
|
+
description: "Estimated model cost of passive memory extraction",
|
|
200
|
+
id: "extraction-cost",
|
|
201
|
+
series: [{ format: "usd", key: "costUsd", label: "Cost" }],
|
|
202
|
+
timeRangeDays: [...WINDOWS],
|
|
203
|
+
title: "Extraction cost",
|
|
204
|
+
type: "bar_chart",
|
|
205
|
+
},
|
|
170
206
|
{
|
|
171
207
|
categories: memoryDays.map((day) => ({
|
|
172
208
|
id: day.date,
|
package/src/plugin.ts
CHANGED
|
@@ -14,7 +14,11 @@ import {
|
|
|
14
14
|
import { processMemorySession } from "./process-session";
|
|
15
15
|
import { createMemoryPromptContributions } from "./recall";
|
|
16
16
|
import { buildMemoryOperationalReport } from "./operational-report";
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
memoriesCapturedEvent,
|
|
19
|
+
memoriesCapturedEventV1,
|
|
20
|
+
memoriesRecalledEvent,
|
|
21
|
+
} from "./events";
|
|
18
22
|
import type { MemoryDb } from "./store";
|
|
19
23
|
import { createMemoryUserPage } from "./user-pages";
|
|
20
24
|
|
|
@@ -101,7 +105,11 @@ export function memoryPlugin(options: MemoryPluginOptions = {}) {
|
|
|
101
105
|
? { structuredModelId: modelId }
|
|
102
106
|
: { structuredModel: "default" },
|
|
103
107
|
packageName: "@sentry/junior-memory",
|
|
104
|
-
conversationEvents: [
|
|
108
|
+
conversationEvents: [
|
|
109
|
+
memoriesCapturedEventV1,
|
|
110
|
+
memoriesCapturedEvent,
|
|
111
|
+
memoriesRecalledEvent,
|
|
112
|
+
],
|
|
105
113
|
cli: {
|
|
106
114
|
commands: [createMemoryCliCommand()],
|
|
107
115
|
},
|
|
@@ -115,8 +123,13 @@ export function memoryPlugin(options: MemoryPluginOptions = {}) {
|
|
|
115
123
|
userPages: [createMemoryUserPage()],
|
|
116
124
|
hooks: {
|
|
117
125
|
async operationalReport(ctx) {
|
|
126
|
+
const extractionDays = await ctx.eventStats.costsByDay({
|
|
127
|
+
days: 90,
|
|
128
|
+
eventName: "memories_captured",
|
|
129
|
+
});
|
|
118
130
|
return await buildMemoryOperationalReport({
|
|
119
131
|
db: ctx.db as MemoryDb,
|
|
132
|
+
extractionDays,
|
|
120
133
|
nowMs: ctx.nowMs,
|
|
121
134
|
});
|
|
122
135
|
},
|
|
@@ -124,6 +137,7 @@ export function memoryPlugin(options: MemoryPluginOptions = {}) {
|
|
|
124
137
|
return createMemoryApi({
|
|
125
138
|
actors: ctx.viewer.actors,
|
|
126
139
|
db: ctx.db as MemoryDb,
|
|
140
|
+
eventStats: ctx.eventStats,
|
|
127
141
|
});
|
|
128
142
|
},
|
|
129
143
|
tools(ctx) {
|
|
@@ -156,6 +170,7 @@ export function memoryPlugin(options: MemoryPluginOptions = {}) {
|
|
|
156
170
|
...(ctx.actor ? { actor: ctx.actor } : {}),
|
|
157
171
|
db: ctx.db as MemoryDb,
|
|
158
172
|
embedder: ctx.embedder,
|
|
173
|
+
events: ctx.events,
|
|
159
174
|
log: ctx.log,
|
|
160
175
|
maxVectorDistance: recallMaxVectorDistance(options),
|
|
161
176
|
source: ctx.source,
|
package/src/process-session.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
createMemoryAgent,
|
|
18
18
|
parseExtractedMemory,
|
|
19
19
|
type ExtractedMemory,
|
|
20
|
+
type MemoryExtractionResult,
|
|
20
21
|
} from "./agent";
|
|
21
22
|
import { MEMORY_KINDS, memoryRuntimeContextSchema } from "./types";
|
|
22
23
|
import { capturedMemory, memoriesCapturedEvent } from "./events";
|
|
@@ -28,20 +29,30 @@ const MEMORY_TOOL_NAMES = new Set([
|
|
|
28
29
|
"searchMemories",
|
|
29
30
|
]);
|
|
30
31
|
const MEMORY_TASK_STATE_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
31
|
-
const
|
|
32
|
+
const extractedMemorySchema = z
|
|
33
|
+
.object({
|
|
34
|
+
content: z.string().min(1),
|
|
35
|
+
expiresAtMs: z.number().finite().nullable(),
|
|
36
|
+
kind: z.enum(MEMORY_KINDS),
|
|
37
|
+
evidenceMessageIndices: z
|
|
38
|
+
.array(z.number().int().nonnegative())
|
|
39
|
+
.min(1)
|
|
40
|
+
.max(10),
|
|
41
|
+
})
|
|
42
|
+
.strict()
|
|
43
|
+
.transform(parseExtractedMemory);
|
|
44
|
+
const extractedMemoryCacheSchema = z.union([
|
|
32
45
|
z
|
|
33
46
|
.object({
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
kind: z.enum(MEMORY_KINDS),
|
|
37
|
-
evidenceMessageIndices: z
|
|
38
|
-
.array(z.number().int().nonnegative())
|
|
39
|
-
.min(1)
|
|
40
|
-
.max(10),
|
|
47
|
+
costUsd: z.number().finite().nonnegative().optional(),
|
|
48
|
+
memories: z.array(extractedMemorySchema).max(5),
|
|
41
49
|
})
|
|
42
|
-
.strict()
|
|
43
|
-
|
|
44
|
-
)
|
|
50
|
+
.strict(),
|
|
51
|
+
z
|
|
52
|
+
.array(extractedMemorySchema)
|
|
53
|
+
.max(5)
|
|
54
|
+
.transform((memories) => ({ memories })),
|
|
55
|
+
]);
|
|
45
56
|
|
|
46
57
|
/** Where a passively extracted memory may be stored, or dropped when unproven. */
|
|
47
58
|
type MemoryRouteTarget = "drop" | "personal" | "conversation";
|
|
@@ -178,10 +189,10 @@ function passiveInput(
|
|
|
178
189
|
};
|
|
179
190
|
}
|
|
180
191
|
|
|
181
|
-
async function
|
|
192
|
+
async function getTaskExtraction(
|
|
182
193
|
context: PluginTaskContext,
|
|
183
|
-
extract: () => Promise<
|
|
184
|
-
): Promise<
|
|
194
|
+
extract: () => Promise<MemoryExtractionResult>,
|
|
195
|
+
): Promise<MemoryExtractionResult> {
|
|
185
196
|
const cacheKey = `memory-extraction:${context.id}`;
|
|
186
197
|
const cached = await context.state.get(cacheKey);
|
|
187
198
|
if (cached !== undefined) {
|
|
@@ -191,11 +202,9 @@ async function getTaskMemories(
|
|
|
191
202
|
}
|
|
192
203
|
await context.state.delete(cacheKey);
|
|
193
204
|
}
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
}
|
|
198
|
-
return memories;
|
|
205
|
+
const extraction = await extract();
|
|
206
|
+
await context.state.set(cacheKey, extraction, MEMORY_TASK_STATE_TTL_MS);
|
|
207
|
+
return extraction;
|
|
199
208
|
}
|
|
200
209
|
|
|
201
210
|
/**
|
|
@@ -251,7 +260,7 @@ export async function processMemorySession(
|
|
|
251
260
|
supersessionDecider: agent,
|
|
252
261
|
});
|
|
253
262
|
await store.archiveExpiredMemories();
|
|
254
|
-
const
|
|
263
|
+
const extraction = await getTaskExtraction(context, async () => {
|
|
255
264
|
const existingMemories = await store.searchMemories({
|
|
256
265
|
limit: 10,
|
|
257
266
|
query: evidenceText,
|
|
@@ -265,14 +274,11 @@ export async function processMemorySession(
|
|
|
265
274
|
runtimeContext,
|
|
266
275
|
});
|
|
267
276
|
});
|
|
268
|
-
if (memories.length === 0) {
|
|
269
|
-
return;
|
|
270
|
-
}
|
|
271
277
|
|
|
272
278
|
const captured: ReturnType<typeof capturedMemory>[] = [];
|
|
273
|
-
for (const memory of memories) {
|
|
279
|
+
for (const memory of extraction.memories) {
|
|
274
280
|
// The routing gate stays even though extraction is also actor-gated:
|
|
275
|
-
//
|
|
281
|
+
// getTaskExtraction caches extraction output for 7 days, so a retry can replay
|
|
276
282
|
// preference proposals cached before this gate existed.
|
|
277
283
|
const target = routeExtractedMemory(memory, transcript, run);
|
|
278
284
|
if (target === "drop") {
|
|
@@ -287,7 +293,12 @@ export async function processMemorySession(
|
|
|
287
293
|
const result = await store.createMemory(input);
|
|
288
294
|
recordCapturedMemory(captured, result);
|
|
289
295
|
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
296
|
+
await context.events.emit(
|
|
297
|
+
memoriesCapturedEvent({
|
|
298
|
+
memories: captured,
|
|
299
|
+
...(extraction.costUsd !== undefined
|
|
300
|
+
? { costUsd: extraction.costUsd }
|
|
301
|
+
: {}),
|
|
302
|
+
}),
|
|
303
|
+
);
|
|
293
304
|
}
|
package/src/recall.ts
CHANGED
|
@@ -2,11 +2,13 @@ import {
|
|
|
2
2
|
definePromptContext,
|
|
3
3
|
type UserPromptContribution,
|
|
4
4
|
type Actor,
|
|
5
|
+
type PluginConversationEvents,
|
|
5
6
|
type PluginLogger,
|
|
6
7
|
type Source,
|
|
7
8
|
} from "@sentry/junior-plugin-api";
|
|
8
9
|
import { z } from "zod";
|
|
9
|
-
import type { MemoryAgent } from "./agent";
|
|
10
|
+
import type { MemoryAgent, MemoryRecallResult } from "./agent";
|
|
11
|
+
import { memoriesRecalledEvent } from "./events";
|
|
10
12
|
import {
|
|
11
13
|
createMemoryStore,
|
|
12
14
|
type MemoryDb,
|
|
@@ -25,6 +27,7 @@ export interface MemoryRecallContext {
|
|
|
25
27
|
conversationId?: string;
|
|
26
28
|
db: MemoryDb;
|
|
27
29
|
embedder?: MemoryEmbeddingProvider;
|
|
30
|
+
events?: PluginConversationEvents;
|
|
28
31
|
log: PluginLogger;
|
|
29
32
|
/** Maximum cosine distance for vector recall. Passed through to the memory store. */
|
|
30
33
|
maxVectorDistance?: number;
|
|
@@ -101,6 +104,28 @@ function renderMemoryPrompt(memories: RecalledMemory[]): string {
|
|
|
101
104
|
].join("\n");
|
|
102
105
|
}
|
|
103
106
|
|
|
107
|
+
function addUsd(
|
|
108
|
+
left: number | undefined,
|
|
109
|
+
right: number | undefined,
|
|
110
|
+
): number | undefined {
|
|
111
|
+
if (left === undefined) return right;
|
|
112
|
+
if (right === undefined) return left;
|
|
113
|
+
return Math.round((left + right) * 1e12) / 1e12;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function emitRecallOutcome(args: {
|
|
117
|
+
costUsd?: number;
|
|
118
|
+
events?: PluginConversationEvents;
|
|
119
|
+
memories: string[];
|
|
120
|
+
}): Promise<void> {
|
|
121
|
+
await args.events?.emit(
|
|
122
|
+
memoriesRecalledEvent({
|
|
123
|
+
memories: args.memories,
|
|
124
|
+
...(args.costUsd !== undefined ? { costUsd: args.costUsd } : {}),
|
|
125
|
+
}),
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
104
129
|
const memoryRecallContext = definePromptContext({
|
|
105
130
|
kind: "recall",
|
|
106
131
|
version: 1,
|
|
@@ -122,8 +147,19 @@ export async function createMemoryPromptContributions(
|
|
|
122
147
|
...(context.actor ? { actor: context.actor } : {}),
|
|
123
148
|
source: context.source,
|
|
124
149
|
});
|
|
150
|
+
let embeddingCostUsd: number | undefined;
|
|
151
|
+
const sourceEmbedder = context.embedder;
|
|
152
|
+
const embedder = sourceEmbedder
|
|
153
|
+
? {
|
|
154
|
+
async embedTexts(input: { texts: string[] }) {
|
|
155
|
+
const result = await sourceEmbedder.embedTexts(input);
|
|
156
|
+
embeddingCostUsd = addUsd(embeddingCostUsd, result.costUsd);
|
|
157
|
+
return result;
|
|
158
|
+
},
|
|
159
|
+
}
|
|
160
|
+
: undefined;
|
|
125
161
|
const candidates = await createMemoryStore(context.db, runtimeContext, {
|
|
126
|
-
...(
|
|
162
|
+
...(embedder ? { embedder } : {}),
|
|
127
163
|
...(context.maxVectorDistance !== undefined
|
|
128
164
|
? { maxVectorDistance: context.maxVectorDistance }
|
|
129
165
|
: {}),
|
|
@@ -132,11 +168,16 @@ export async function createMemoryPromptContributions(
|
|
|
132
168
|
limit: RECALL_CANDIDATE_LIMIT,
|
|
133
169
|
});
|
|
134
170
|
if (candidates.length === 0) {
|
|
171
|
+
await emitRecallOutcome({
|
|
172
|
+
...(embeddingCostUsd !== undefined ? { costUsd: embeddingCostUsd } : {}),
|
|
173
|
+
events: context.events,
|
|
174
|
+
memories: [],
|
|
175
|
+
});
|
|
135
176
|
return undefined;
|
|
136
177
|
}
|
|
137
|
-
let
|
|
178
|
+
let recall: MemoryRecallResult;
|
|
138
179
|
try {
|
|
139
|
-
|
|
180
|
+
recall = await context.agent.selectRelevantMemories({
|
|
140
181
|
candidates: candidates.map(({ content, id }) => ({ content, id })),
|
|
141
182
|
userRequest: context.text,
|
|
142
183
|
});
|
|
@@ -149,11 +190,17 @@ export async function createMemoryPromptContributions(
|
|
|
149
190
|
const candidatesById = new Map(
|
|
150
191
|
candidates.map((memory) => [memory.id, memory]),
|
|
151
192
|
);
|
|
152
|
-
const relevant = relevantIds
|
|
193
|
+
const relevant = recall.relevantIds
|
|
153
194
|
.map((id) => candidatesById.get(id))
|
|
154
195
|
.filter((memory): memory is MemoryRecord => memory !== undefined)
|
|
155
196
|
.slice(0, DEFAULT_RECALL_LIMIT);
|
|
156
197
|
const selected = selectPromptMemories(relevant);
|
|
198
|
+
const costUsd = addUsd(embeddingCostUsd, recall.costUsd);
|
|
199
|
+
await emitRecallOutcome({
|
|
200
|
+
...(costUsd !== undefined ? { costUsd } : {}),
|
|
201
|
+
events: context.events,
|
|
202
|
+
memories: selected.map(({ id }) => id),
|
|
203
|
+
});
|
|
157
204
|
if (selected.length === 0) {
|
|
158
205
|
return undefined;
|
|
159
206
|
}
|
package/src/store.ts
CHANGED
|
@@ -180,6 +180,7 @@ const embeddingVectorSchema = z
|
|
|
180
180
|
.length(MEMORY_EMBEDDING_DIMENSIONS);
|
|
181
181
|
const embeddingResultSchema = z
|
|
182
182
|
.object({
|
|
183
|
+
costUsd: z.number().finite().nonnegative().optional(),
|
|
183
184
|
dimensions: z.literal(MEMORY_EMBEDDING_DIMENSIONS),
|
|
184
185
|
model: nonEmptyStringSchema,
|
|
185
186
|
provider: nonEmptyStringSchema,
|
|
@@ -272,6 +273,7 @@ export interface ArchiveExpiredMemoriesResult {
|
|
|
272
273
|
export interface MemoryEmbeddingProvider {
|
|
273
274
|
/** Embed normalized memory text for derived vector retrieval. */
|
|
274
275
|
embedTexts(input: { texts: string[] }): Promise<{
|
|
276
|
+
costUsd?: number;
|
|
275
277
|
dimensions: number;
|
|
276
278
|
model: string;
|
|
277
279
|
provider: string;
|
package/src/tools.ts
CHANGED
|
@@ -420,6 +420,7 @@ function memoryToolResult<TData extends Record<string, unknown>>(
|
|
|
420
420
|
/** Create a tool that submits an explicit memory candidate for storage. */
|
|
421
421
|
export function createMemoryCreateTool(context: MemoryCreateToolContext) {
|
|
422
422
|
return definePluginTool({
|
|
423
|
+
approvalMode: "approve",
|
|
423
424
|
annotations: {
|
|
424
425
|
destructiveHint: false,
|
|
425
426
|
idempotentHint: true,
|