@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.
@@ -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
@@ -141,6 +141,7 @@ export interface MemoryEmbeddingProvider {
141
141
  embedTexts(input: {
142
142
  texts: string[];
143
143
  }): Promise<{
144
+ costUsd?: number;
144
145
  dimensions: number;
145
146
  model: string;
146
147
  provider: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentry/junior-memory",
3
- "version": "0.124.2",
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.124.2"
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<string[]> | string[];
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<ExtractedMemory[]> | ExtractedMemory[];
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 [...new Set(decision.relevantIds)].filter((id) =>
544
- candidateIds.has(id),
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 extractedMemoriesFromResponse(
566
- extractMemoriesResponseSchema.parse(result.object),
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(memory: PersonalMemoryRecord): z.output<typeof memoryApiSchema> {
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
- /** Durable transcript event emitted after background memory capture. */
25
- export const memoriesCapturedEvent = defineConversationEvent({
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(event) {
34
- const count = event.memories.length;
35
- return {
36
- icon: "brain",
37
- title: count === 1 ? "Memory captured" : "Memories captured",
38
- preview:
39
- count === 1
40
- ? eventPreview(event.memories[0]!.content)
41
- : `${count} memories`,
42
- details: event.memories.map((memory) => ({
43
- title: memory.content,
44
- metadata: [memory.kind, memory.scope],
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 { PluginOperationalReportContent } from "@sentry/junior-plugin-api";
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 { memoriesCapturedEvent } from "./events";
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: [memoriesCapturedEvent],
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,
@@ -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 extractedMemoryCacheSchema = z.array(
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
- 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),
47
+ costUsd: z.number().finite().nonnegative().optional(),
48
+ memories: z.array(extractedMemorySchema).max(5),
41
49
  })
42
- .strict()
43
- .transform(parseExtractedMemory),
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 getTaskMemories(
192
+ async function getTaskExtraction(
182
193
  context: PluginTaskContext,
183
- extract: () => Promise<ExtractedMemory[]>,
184
- ): Promise<ExtractedMemory[]> {
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 memories = await extract();
195
- if (memories.length > 0) {
196
- await context.state.set(cacheKey, memories, MEMORY_TASK_STATE_TTL_MS);
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 memories = await getTaskMemories(context, async () => {
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
- // getTaskMemories caches extraction output for 7 days, so a retry can replay
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
- if (captured.length > 0) {
291
- await context.events.emit(memoriesCapturedEvent({ memories: captured }));
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
- ...(context.embedder ? { embedder: context.embedder } : {}),
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 relevantIds: string[];
178
+ let recall: MemoryRecallResult;
138
179
  try {
139
- relevantIds = await context.agent.selectRelevantMemories({
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,