@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/src/agent.ts DELETED
@@ -1,663 +0,0 @@
1
- import {
2
- actorSchema,
3
- sourceSchema,
4
- type PluginModel,
5
- } from "@sentry/junior-plugin-api";
6
- import { z } from "zod";
7
- import {
8
- memorySupersessionDecisionSchema,
9
- memorySupersessionInputSchema,
10
- type MemorySupersessionDecision,
11
- type MemorySupersessionInput,
12
- } from "./store";
13
- import {
14
- MEMORY_KINDS,
15
- memoryRuntimeContextSchema,
16
- type MemoryKind,
17
- } from "./types";
18
-
19
- const memoryKindSchema = z.enum(MEMORY_KINDS);
20
- const memoryRejectReasonSchema = z.enum([
21
- "not_public_shareable",
22
- "secret_or_credential",
23
- "sensitive_personal",
24
- "third_party_personal",
25
- "vague_or_not_self_contained",
26
- "not_durable",
27
- "assistant_or_system_detail",
28
- "unsupported_scope",
29
- ]);
30
- const memoryRecallCandidateSchema = z
31
- .object({
32
- content: z.string().min(1),
33
- id: z.string().min(1),
34
- })
35
- .strict();
36
- const memoryRecallInputSchema = z
37
- .object({
38
- candidates: z.array(memoryRecallCandidateSchema).min(1).max(20),
39
- userRequest: z.string().min(1),
40
- })
41
- .strict();
42
- const memoryRecallDecisionSchema = z
43
- .object({
44
- relevantIds: z
45
- .array(z.string().min(1))
46
- .max(20)
47
- .describe(
48
- "Candidate ids whose memories directly help with the current request, ordered by relevance.",
49
- ),
50
- })
51
- .strict();
52
- const createMemoryRequestSchema = z
53
- .object({
54
- content: z.string().min(1),
55
- expiresAtMs: z.number().finite().optional(),
56
- runtimeContext: memoryRuntimeContextSchema,
57
- sourceContext: z
58
- .object({
59
- currentUserText: z.string().min(1).optional(),
60
- })
61
- .strict()
62
- .optional(),
63
- })
64
- .strict();
65
- const transcriptProvenanceSchema = z
66
- .object({
67
- authority: z.enum(["instruction", "context"]),
68
- actor: actorSchema.optional(),
69
- })
70
- .strict();
71
- const evidenceMessageIndicesSchema = z
72
- .array(z.number().int().nonnegative())
73
- .min(1)
74
- .max(10)
75
- .describe("Indices from <run-transcript> that directly support this memory.");
76
- const extractSessionRequestSchema = z
77
- .object({
78
- existingMemories: z
79
- .array(
80
- z
81
- .object({
82
- content: z.string().min(1),
83
- })
84
- .strict(),
85
- )
86
- .max(10)
87
- .default([]),
88
- actors: z.array(actorSchema),
89
- runtimeContext: memoryRuntimeContextSchema,
90
- transcript: z
91
- .array(
92
- z.discriminatedUnion("type", [
93
- z
94
- .object({
95
- type: z.literal("message"),
96
- role: z.enum(["user", "assistant"]),
97
- text: z.string().min(1),
98
- provenance: transcriptProvenanceSchema.optional(),
99
- isRunActor: z.boolean().optional(),
100
- })
101
- .strict(),
102
- z
103
- .object({
104
- type: z.literal("toolResult"),
105
- toolName: z.string().min(1),
106
- isError: z.boolean(),
107
- text: z.string().min(1),
108
- })
109
- .strict(),
110
- ]),
111
- )
112
- .min(1),
113
- })
114
- .strict();
115
- const expiresAtMsSchema = z
116
- .number()
117
- .finite()
118
- .nullable()
119
- .describe(
120
- "Expiration timestamp when the fact should expire, otherwise null.",
121
- );
122
- const memoryReviewDecisionSchema = z.discriminatedUnion("decision", [
123
- z
124
- .object({
125
- decision: z.literal("store"),
126
- kind: memoryKindSchema,
127
- content: z.string().min(1),
128
- expiresAtMs: z.number().finite().optional(),
129
- })
130
- .strict(),
131
- z
132
- .object({
133
- decision: z.literal("reject"),
134
- reason: memoryRejectReasonSchema,
135
- })
136
- .strict(),
137
- ]);
138
- const memoryReviewResponseSchema = z.discriminatedUnion("decision", [
139
- z
140
- .object({
141
- decision: z.literal("store"),
142
- kind: memoryKindSchema.describe(
143
- "Use preference only for actor-owned personal preferences, opinions, habits, or workflows. Use procedure for reusable task or process instructions. Use knowledge for shared project, channel, operational, or runbook facts.",
144
- ),
145
- canonicalFact: z
146
- .string()
147
- .min(1)
148
- .describe(
149
- "Stored memory text. It must be self-contained and must not include actor names, actor/user labels, source labels, or first- or second-person wording.",
150
- ),
151
- expiresAtMs: expiresAtMsSchema,
152
- })
153
- .strict(),
154
- z
155
- .object({
156
- decision: z.literal("reject"),
157
- reason: memoryRejectReasonSchema,
158
- })
159
- .strict(),
160
- ]);
161
- const extractedMemorySchema = z
162
- .object({
163
- kind: memoryKindSchema.describe(
164
- "Use preference only for actor-owned personal preferences, opinions, habits, or workflows. Use procedure for reusable task or process instructions. Use knowledge for shared project, channel, operational, or runbook facts.",
165
- ),
166
- canonicalFact: z
167
- .string()
168
- .min(1)
169
- .describe(
170
- "Stored memory text as one self-contained fact. It must not include actor names, actor/user labels, source labels, or first- or second-person wording.",
171
- ),
172
- expiresAtMs: expiresAtMsSchema,
173
- evidenceMessageIndices: evidenceMessageIndicesSchema,
174
- })
175
- .strict();
176
- const extractedMemoryResultSchema = z
177
- .object({
178
- content: z.string().min(1),
179
- expiresAtMs: expiresAtMsSchema,
180
- kind: memoryKindSchema,
181
- evidenceMessageIndices: evidenceMessageIndicesSchema,
182
- })
183
- .strict();
184
- const extractMemoriesResponseSchema = z
185
- .object({
186
- memories: z
187
- .array(extractedMemorySchema)
188
- .max(5)
189
- .describe(
190
- "Accepted durable memories from the completed run. Return one object per distinct source assertion and classify it with kind.",
191
- ),
192
- })
193
- .strict();
194
- type MemoryReviewResponse = z.output<typeof memoryReviewResponseSchema>;
195
- type ExtractMemoriesResponse = z.output<typeof extractMemoriesResponseSchema>;
196
-
197
- export type MemoryReview = z.output<typeof memoryReviewDecisionSchema>;
198
- export type MemoryRecallInput = z.output<typeof memoryRecallInputSchema>;
199
-
200
- export type CreateMemoryRequest = z.output<typeof createMemoryRequestSchema>;
201
- export type ExtractSessionRequest = z.output<
202
- typeof extractSessionRequestSchema
203
- >;
204
- export type ExtractedMemory = z.output<typeof extractedMemoryResultSchema>;
205
-
206
- /** Memories proposed by passive extraction and the model cost of that pass. */
207
- export type MemoryExtractionResult = {
208
- costUsd?: number;
209
- memories: ExtractedMemory[];
210
- };
211
-
212
- /** Memories admitted by automatic recall and the model cost of that decision. */
213
- export type MemoryRecallResult = {
214
- costUsd?: number;
215
- relevantIds: string[];
216
- };
217
-
218
- export interface MemoryAgent {
219
- /** Select candidate memories that directly help with the current request. */
220
- selectRelevantMemories(
221
- request: MemoryRecallInput,
222
- ): Promise<MemoryRecallResult> | MemoryRecallResult;
223
- /** Classify a new preference against related active preferences. */
224
- adjudicateSupersession(
225
- request: MemorySupersessionInput,
226
- ): Promise<MemorySupersessionDecision> | MemorySupersessionDecision;
227
- extractSessionMemories(
228
- request: ExtractSessionRequest,
229
- ): Promise<MemoryExtractionResult> | MemoryExtractionResult;
230
- reviewCreateRequest(
231
- request: CreateMemoryRequest,
232
- ): Promise<MemoryReview> | MemoryReview;
233
- }
234
-
235
- const MEMORY_REVIEW_SYSTEM = [
236
- "You are Junior's memory review agent.",
237
- "Review one memory candidate and return one structured review decision.",
238
- "Store only self-contained facts that are useful beyond this turn and safe for the current Source.",
239
- "Reject secrets, credentials, private or sensitive personal details, gossip, speculative claims about other people, assistant/system implementation details, vague references, and low-durability chatter.",
240
- "Use the runtime context only for authority and scope; do not accept model-provided actor ids, scope ids, aliases, or arbitrary subjects.",
241
- ].join("\n");
242
- const MEMORY_EXTRACTION_SYSTEM = [
243
- "You are Junior's passive memory extraction agent. Return only structured memories worth storing.",
244
- "Use the completed run transcript as source evidence, including user-authored messages and tool results.",
245
- "Assistant text is context for interpreting the run, not independent evidence for new facts.",
246
- "Reject secrets, credentials, private or sensitive personal details, gossip, speculative claims about other people, assistant/system implementation details, vague references, and low-durability chatter.",
247
- "If no durable, self-contained memory remains after rewriting, return an empty memories array.",
248
- ].join("\n");
249
- const MEMORY_RECALL_SYSTEM = [
250
- "You are Junior's memory recall relevance agent.",
251
- "Select only memories that would directly help answer the user's current request.",
252
- "Reject memories that merely share a company, product family, repository vocabulary, programming language, or general engineering context.",
253
- "Prefer specific matches on the exact repository, workflow, command, test, CI setup, project, or user preference being asked about.",
254
- "An empty relevantIds array is correct when no candidate is directly helpful.",
255
- ].join("\n");
256
- const MEMORY_PREFERENCE_ADJUDICATION_SYSTEM = [
257
- "You are Junior's memory preference adjudication agent.",
258
- "Classify how one new actor preference relates to existing active actor preferences.",
259
- "Return duplicate when the same durable preference is merely phrased differently.",
260
- "Return supersedes_old only for an obvious changed value in the same mutable preference slot.",
261
- "Return distinct for additive preferences or different topics, and uncertain when the relationship is unclear.",
262
- ].join("\n");
263
- const CANONICAL_CONTENT_RULES = [
264
- "- Stored memory text must be a rewritten fact, not copied user wording or a sentence about who said it.",
265
- "- Store the minimum useful assertion supported by source evidence; do not add adjacent steps, caveats, or generalized advice.",
266
- "- Do not return both concise and expanded variants of the same source assertion; keep the shortest self-contained canonical memory.",
267
- "- Put ownership in structured fields, not prose.",
268
- "- For actor memories, omit the subject and write a stable fact such as 'Prefers X', 'Uses Y', or 'Thinks Z'.",
269
- "- Drop perspective/provenance markers while preserving useful context.",
270
- "- Remove actor names, display names, actor/user labels, first- or second-person wording, thread labels, channel labels, and source labels.",
271
- ];
272
-
273
- function escapeXml(value: string): string {
274
- return value
275
- .replaceAll("&", "&amp;")
276
- .replaceAll("<", "&lt;")
277
- .replaceAll(">", "&gt;");
278
- }
279
-
280
- function actorLabel(actor: z.output<typeof actorSchema> | undefined): string {
281
- if (!actor) {
282
- return "none";
283
- }
284
- switch (actor.platform) {
285
- case "system":
286
- return `system:${actor.name}`;
287
- case "slack":
288
- return `slack:${actor.teamId}:${actor.userId}`;
289
- case "local":
290
- return `local:${actor.userId}`;
291
- case "web":
292
- return `web:${actor.userId}`;
293
- }
294
- }
295
-
296
- function sourceLabel(source: z.output<typeof sourceSchema>): string {
297
- switch (source.kind) {
298
- case "slack":
299
- return `slack:${source.teamId}:${source.channelId}`;
300
- case "web":
301
- case "local":
302
- return `${source.kind}:${source.conversationId}`;
303
- case "resource_event":
304
- return `resource-event:${source.namespace}:${source.eventKey}`;
305
- case "scheduled_task":
306
- case "event_task":
307
- case "plugin_dispatch":
308
- case "agent_invocation":
309
- return source.kind;
310
- }
311
- }
312
-
313
- function runtimeDescription(
314
- request: Pick<CreateMemoryRequest, "expiresAtMs" | "runtimeContext">,
315
- ): string {
316
- const runtime = request.runtimeContext;
317
- const lines = [
318
- `- actor: ${escapeXml(actorLabel(runtime.actor))}`,
319
- `- source: ${escapeXml(sourceLabel(runtime.source))}`,
320
- `- has_conversation: ${runtime.conversationId ? "true" : "false"}`,
321
- `- expires_at: ${
322
- request.expiresAtMs === undefined
323
- ? "never"
324
- : escapeXml(new Date(request.expiresAtMs).toISOString())
325
- }`,
326
- ];
327
- return ["<runtime>", ...lines, "</runtime>"].join("\n");
328
- }
329
-
330
- function sourceContext(request: CreateMemoryRequest): string | undefined {
331
- const currentUserText = request.sourceContext?.currentUserText?.trim();
332
- if (!currentUserText) {
333
- return undefined;
334
- }
335
- return [
336
- "<source-context>",
337
- "The current user-authored text is source evidence for explicit memory requests. Use it to recover the concrete fact when the candidate is incomplete, vague, or over-personalized. Store only rewritten, self-contained memory content.",
338
- "<current-user-message>",
339
- escapeXml(currentUserText),
340
- "</current-user-message>",
341
- "</source-context>",
342
- ].join("\n");
343
- }
344
-
345
- function existingMemoriesContext(request: ExtractSessionRequest): string {
346
- if (request.existingMemories.length === 0) {
347
- return "<existing-memories>[]</existing-memories>";
348
- }
349
- return [
350
- "<existing-memories>",
351
- "Use these only to skip memories that are already covered or semantically redundant. They are not source evidence for new memories.",
352
- escapeXml(JSON.stringify(request.existingMemories)),
353
- "</existing-memories>",
354
- ].join("\n");
355
- }
356
-
357
- /**
358
- * Passive extraction offers personal preferences only on single-actor runs.
359
- * Multi-actor runs restrict extraction to conversation-subject kinds.
360
- */
361
- function allowedExtractionKinds(actorCount: number): Set<MemoryKind> {
362
- return actorCount === 1
363
- ? new Set<MemoryKind>(MEMORY_KINDS)
364
- : new Set<MemoryKind>(["procedure", "knowledge"]);
365
- }
366
-
367
- function memoryKindsContext(allowedKinds: Set<MemoryKind>): string {
368
- const lines = ["<memory-kinds>"];
369
- if (allowedKinds.has("preference")) {
370
- lines.push(
371
- "- preference: a durable first-person personal preference, opinion, habit, or workflow owned by the current actor. Stored as actor memory.",
372
- );
373
- }
374
- lines.push(
375
- "- procedure: reusable instructions for how a task, lookup, investigation, process, triage flow, or runbook should be done. Store the method, source-of-truth, prerequisite, or decision path when it took effort to discover. Stored as conversation memory.",
376
- "- knowledge: stable shared project, channel, operational, or runbook fact that is not a personal actor preference. Direct answers to user inquiries qualify only when they are durable beyond this run. Stored as conversation memory.",
377
- "</memory-kinds>",
378
- );
379
- return lines.join("\n");
380
- }
381
-
382
- function reviewPrompt(request: CreateMemoryRequest): string {
383
- const sections = [
384
- "<memory-review-input>",
385
- "Review the candidate memory using the runtime-owned context below.",
386
- "",
387
- runtimeDescription(request),
388
- "",
389
- sourceContext(request),
390
- "",
391
- "<candidate>",
392
- escapeXml(request.content),
393
- "</candidate>",
394
- "",
395
- "<rules>",
396
- "- Return store only when the candidate is durable, self-contained, and safe for the current Source.",
397
- "- First classify the memory kind: preference, procedure, or knowledge.",
398
- "- Use kind=preference only for first-person facts authored by the current actor about their own preference, opinion, habit, identity, or workflow.",
399
- "- Reject named third-person personal facts such as another person's preference, opinion, habit, identity, relationship, or workflow. Do not assume a named person is the current actor.",
400
- "- Use kind=procedure for reusable task/process/runbook instructions.",
401
- "- Use kind=knowledge for shared project, channel, operational, or runbook facts.",
402
- "- When current-user-message contains an explicit memory request with a concrete fact or procedure, extract from current-user-message even if the candidate is vague, incomplete, or phrased as an instruction.",
403
- "- A candidate may be badly phrased by an outer assistant or extraction pass. When current-user-message contains the actor's own first-person memory fact, treat that as actor-authored source evidence and canonicalize the fact instead of rejecting for third-person wording.",
404
- "- When candidate wording personalizes a shared task, process, runbook, project, channel, or operational fact, use current-user-message to recover the shared fact and classify it as procedure or knowledge.",
405
- "- Explicit procedure requests are valid when the source text contains both task context and action. Canonicalize them as shared procedure facts instead of rejecting them as vague.",
406
- "- Store content as person-less, source-less canonical knowledge. Ownership and source live in structured metadata, not prose.",
407
- "- For actor memories, omit the subject and write the content as a stable fact such as 'Prefers X', 'Uses Y', or 'Thinks Z'.",
408
- "- Remove actor names, display names, actor/user labels, first- or second-person wording, thread labels, channel labels, and source labels from stored content.",
409
- "- Reject third-party personal profile facts, even if they mention a name.",
410
- "- Reject vague content such as 'remember this' unless the candidate or current-user-message contains the concrete fact.",
411
- "- Preserve the requested expiration when one exists; otherwise set expiresAtMs to null.",
412
- "- If unsure, reject.",
413
- "</rules>",
414
- "</memory-review-input>",
415
- ].filter((section): section is string => section !== undefined);
416
- return sections.join("\n");
417
- }
418
-
419
- function runTranscriptContext(request: ExtractSessionRequest): string {
420
- return [
421
- "<run-transcript>",
422
- ...request.transcript.map((entry, index) => {
423
- if (entry.type === "toolResult") {
424
- return [
425
- `<tool-result index="${index}" tool="${escapeXml(entry.toolName)}" is_error="${entry.isError ? "true" : "false"}">`,
426
- escapeXml(entry.text),
427
- "</tool-result>",
428
- ].join("\n");
429
- }
430
- const authority = entry.provenance?.authority ?? "context";
431
- const isRunActor = entry.isRunActor === true;
432
- const actor = actorLabel(entry.provenance?.actor);
433
- return [
434
- `<message index="${index}" role="${entry.role}" authority="${authority}" is_run_actor="${isRunActor ? "true" : "false"}" actor="${escapeXml(actor)}">`,
435
- escapeXml(entry.text),
436
- "</message>",
437
- ].join("\n");
438
- }),
439
- "</run-transcript>",
440
- ].join("\n");
441
- }
442
-
443
- function sessionExtractionPrompt(request: ExtractSessionRequest): string {
444
- const allowedKinds = allowedExtractionKinds(request.actors.length);
445
- const allowsPreference = allowedKinds.has("preference");
446
- return [
447
- "<memory-extraction-input>",
448
- "Extract durable memories from this completed agent run using the runtime-owned context below.",
449
- "",
450
- runtimeDescription({
451
- runtimeContext: request.runtimeContext,
452
- }),
453
- "",
454
- existingMemoriesContext(request),
455
- "",
456
- memoryKindsContext(allowedKinds),
457
- "",
458
- runTranscriptContext(request),
459
- "",
460
- "<rules>",
461
- "- Return at most five memories.",
462
- "- Every returned memory must cite one or more evidenceMessageIndices from <run-transcript>.",
463
- "- Cite only indices that directly support the stored fact; do not cite assistant messages as independent evidence.",
464
- "- Each transcript message exposes authority (instruction or context), is_run_actor, and an actor id. Use these to classify evidence.",
465
- ...(allowsPreference
466
- ? [
467
- "- For a preference, cite only messages with authority=instruction and is_run_actor=true; a preference must be the run actor's own first-person fact.",
468
- ]
469
- : []),
470
- "- For a procedure or knowledge memory, cite run-actor instruction messages, public context messages, or successful tool results.",
471
- "- Use user messages and successful tool results as source evidence for storable facts.",
472
- "- Use failed tool results only when the failure reveals durable process knowledge, not transient errors.",
473
- "- Use assistant messages only as context; do not store the assistant's claims unless supported by user messages or tool results.",
474
- "- Return one memory per distinct fact.",
475
- "- Prefer storing how to achieve a result: stable source-of-truth, query location, workflow, prerequisite, caveat, or reusable decision path that took effort to discover.",
476
- "- Store direct answers to user inquiries only when they are stable operational/project knowledge, not values that naturally change over time.",
477
- "- Do not store point-in-time analytics, search, issue, metric, incident, availability, or status answers just because a tool produced them.",
478
- "- Do not store the fact that the user asked for advice, search, recall, planning, listing, inspection, or removal. Store only stable knowledge discovered in response, such as a reusable method or source-of-truth.",
479
- "- A user question asking how, what, where, or whether to do something is not source evidence for the answer. Store the answer only when supported by a user-authored factual statement or a tool result.",
480
- "- Set kind=procedure for reusable task/process/runbook instructions.",
481
- "- Set kind=knowledge for shared team, project, channel, runbook, or operational facts.",
482
- ...(allowsPreference
483
- ? [
484
- "- Set kind=preference only for clear durable first-person facts authored by the current actor about their own preference, opinion, habit, identity, or workflow.",
485
- "- A single task request or ask-for-help is never a durable preference, even when phrased as an ongoing action for this run (for example 'help me capture takeaways as we go'). Do not convert a one-off ask into a 'Prefers ...' memory.",
486
- "- A durable preference requires explicitly stated, generalizable first-person phrasing such as 'I prefer ...', 'I always ...', or 'I never ...' that describes how the actor wants things done in general, not just for the current task.",
487
- ]
488
- : [
489
- "- This completed run has multiple run actors. Return only conversation-scoped procedure or knowledge memories.",
490
- "- Do not return personal preferences, opinions, habits, identity facts, or workflow preferences from any actor in this run.",
491
- "- Do not convert a personal first-person statement into shared knowledge or procedure. Statements like 'I prefer ...', 'I use ...', 'I always ...', or 'I never ...' are not memory evidence in this run.",
492
- "- Shared team, channel, repository, or operational norms are eligible only when the source states them as collective practice or durable operational fact, not as one individual's preference.",
493
- ]),
494
- "- Reject named third-person personal facts such as another person's preference, opinion, habit, identity, relationship, or workflow. Do not assume a named person is the current actor.",
495
- "- User-authored task instructions are procedures, not preferences, unless they explicitly describe the actor's personal preference or habit.",
496
- "- Procedural statements such as 'for X, do Y', 'when X, do Y', and 'to accomplish X, do Y' belong in procedures.",
497
- ...CANONICAL_CONTENT_RULES,
498
- "- Skip a candidate when existing-memories already cover the same durable fact.",
499
- "- Reject third-party personal profile facts, even if they mention a name.",
500
- "- If unsure, return no memory for that candidate.",
501
- "</rules>",
502
- "</memory-extraction-input>",
503
- ].join("\n");
504
- }
505
-
506
- function recallRelevancePrompt(request: MemoryRecallInput): string {
507
- return [
508
- "<memory-recall-input>",
509
- "<user-request>",
510
- escapeXml(request.userRequest),
511
- "</user-request>",
512
- "",
513
- "<candidate-memories>",
514
- escapeXml(JSON.stringify(request.candidates)),
515
- "</candidate-memories>",
516
- "",
517
- "Return only ids from candidate-memories. Preserve the most relevant candidates first.",
518
- "</memory-recall-input>",
519
- ].join("\n");
520
- }
521
-
522
- function preferenceAdjudicationPrompt(
523
- request: MemorySupersessionInput,
524
- ): string {
525
- return [
526
- "<memory-preference-adjudication-input>",
527
- "Classify the candidate preference against the related active preferences.",
528
- "",
529
- runtimeDescription({
530
- runtimeContext: request.runtimeContext,
531
- }),
532
- "",
533
- "<candidate>",
534
- escapeXml(JSON.stringify(request.candidate)),
535
- "</candidate>",
536
- "",
537
- "<existing-memories>",
538
- escapeXml(JSON.stringify(request.existingMemories)),
539
- "</existing-memories>",
540
- "",
541
- "<rules>",
542
- "- Return duplicate when the candidate and one existing memory express the same durable preference or value with different wording.",
543
- "- Return supersedes_old only when the candidate and old memory describe the same mutable preference slot and the candidate is the newer value.",
544
- "- Examples of same mutable slot: preferred programming language, preferred review style, preferred notification cadence, preferred tool for a task.",
545
- "- Return distinct when the candidate is an additional preference or belongs to a different task or topic.",
546
- "- Return uncertain when broader or narrower wording makes equivalence or replacement unclear.",
547
- "- Do not supersede memories from different topics even if they are both preferences.",
548
- "- duplicateId and supersededIds may contain only ids from existing-memories.",
549
- "- If unsure, return uncertain.",
550
- "</rules>",
551
- "</memory-preference-adjudication-input>",
552
- ].join("\n");
553
- }
554
-
555
- /** Create the memory-owned agent that reviews, extracts, and recalls memories. */
556
- export function createMemoryAgent(model: PluginModel): MemoryAgent {
557
- return {
558
- async selectRelevantMemories(rawRequest) {
559
- const request = memoryRecallInputSchema.parse(rawRequest);
560
- const result = await model.completeObject({
561
- schema: memoryRecallDecisionSchema,
562
- system: MEMORY_RECALL_SYSTEM,
563
- prompt: recallRelevancePrompt(request),
564
- maxTokens: 400,
565
- });
566
- const decision = memoryRecallDecisionSchema.parse(result.object);
567
- const candidateIds = new Set(request.candidates.map(({ id }) => id));
568
- return {
569
- relevantIds: [...new Set(decision.relevantIds)].filter((id) =>
570
- candidateIds.has(id),
571
- ),
572
- ...(result.costUsd !== undefined ? { costUsd: result.costUsd } : undefined),
573
- };
574
- },
575
- async adjudicateSupersession(rawRequest) {
576
- const request = memorySupersessionInputSchema.parse(rawRequest);
577
- const result = await model.completeObject({
578
- schema: memorySupersessionDecisionSchema,
579
- system: MEMORY_PREFERENCE_ADJUDICATION_SYSTEM,
580
- prompt: preferenceAdjudicationPrompt(request),
581
- maxTokens: 400,
582
- });
583
- return memorySupersessionDecisionSchema.parse(result.object);
584
- },
585
- async extractSessionMemories(rawRequest) {
586
- const request = extractSessionRequestSchema.parse(rawRequest);
587
- const result = await model.completeObject({
588
- schema: extractMemoriesResponseSchema,
589
- system: MEMORY_EXTRACTION_SYSTEM,
590
- prompt: sessionExtractionPrompt(request),
591
- maxTokens: 1_000,
592
- });
593
- return {
594
- memories: extractedMemoriesFromResponse(
595
- extractMemoriesResponseSchema.parse(result.object),
596
- ),
597
- ...(result.costUsd !== undefined ? { costUsd: result.costUsd } : undefined),
598
- };
599
- },
600
- async reviewCreateRequest(rawRequest) {
601
- const request = parseCreateMemoryRequest(rawRequest);
602
- const result = await model.completeObject({
603
- schema: memoryReviewResponseSchema,
604
- system: MEMORY_REVIEW_SYSTEM,
605
- prompt: reviewPrompt(request),
606
- maxTokens: 700,
607
- });
608
- const response = memoryReviewResponseSchema.parse(result.object);
609
- return memoryReviewFromResponse(response);
610
- },
611
- };
612
- }
613
-
614
- function memoryReviewFromResponse(
615
- response: MemoryReviewResponse,
616
- ): MemoryReview {
617
- if (response.decision === "store") {
618
- return parseMemoryReview({
619
- decision: "store",
620
- kind: response.kind,
621
- content: response.canonicalFact,
622
- ...(response.expiresAtMs !== null
623
- ? { expiresAtMs: response.expiresAtMs }
624
- : undefined),
625
- });
626
- }
627
- return parseMemoryReview({
628
- decision: "reject",
629
- reason: response.reason,
630
- });
631
- }
632
-
633
- function extractedMemoriesFromResponse(
634
- response: ExtractMemoriesResponse,
635
- ): ExtractedMemory[] {
636
- const toMemory = (
637
- memory: z.output<typeof extractedMemorySchema>,
638
- ): ExtractedMemory =>
639
- parseExtractedMemory({
640
- content: memory.canonicalFact,
641
- expiresAtMs: memory.expiresAtMs,
642
- kind: memory.kind,
643
- evidenceMessageIndices: memory.evidenceMessageIndices,
644
- });
645
- return response.memories.map(toMemory);
646
- }
647
-
648
- /** Parse the canonical extracted-memory shape stored across task retries. */
649
- export function parseExtractedMemory(memory: unknown): ExtractedMemory {
650
- return extractedMemoryResultSchema.parse(memory);
651
- }
652
-
653
- /** Parse the structured decision returned by the memory agent. */
654
- export function parseMemoryReview(result: unknown): MemoryReview {
655
- return memoryReviewDecisionSchema.parse(result);
656
- }
657
-
658
- /** Parse the structured input sent to the memory agent. */
659
- export function parseCreateMemoryRequest(
660
- request: unknown,
661
- ): CreateMemoryRequest {
662
- return createMemoryRequestSchema.parse(request);
663
- }