@sentry/junior-memory 0.90.0 → 0.92.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent.d.ts +47 -5
- package/dist/index.js +165 -74
- package/dist/index.js.map +1 -1
- package/dist/recall.d.ts +2 -2
- package/dist/store.d.ts +1 -1
- package/dist/tools.d.ts +2 -2
- package/dist/types.d.ts +4 -4
- package/package.json +2 -2
- package/src/agent.ts +106 -31
- package/src/plugin.ts +6 -5
- package/src/process-session.ts +116 -18
- package/src/recall.ts +9 -17
- package/src/scope.ts +11 -11
- package/src/store.ts +1 -1
- package/src/tools.ts +8 -8
- package/src/types.ts +4 -4
package/dist/recall.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { PromptMessage,
|
|
1
|
+
import type { PromptMessage, Actor, Source } from "@sentry/junior-plugin-api";
|
|
2
2
|
import { type MemoryDb, type MemoryEmbeddingProvider } from "./store";
|
|
3
3
|
export interface MemoryRecallContext {
|
|
4
4
|
conversationId?: string;
|
|
@@ -6,7 +6,7 @@ export interface MemoryRecallContext {
|
|
|
6
6
|
embedder?: MemoryEmbeddingProvider;
|
|
7
7
|
/** Maximum cosine distance for vector recall. Passed through to the memory store. */
|
|
8
8
|
maxVectorDistance?: number;
|
|
9
|
-
|
|
9
|
+
actor?: Actor;
|
|
10
10
|
source: Source;
|
|
11
11
|
text: string;
|
|
12
12
|
}
|
package/dist/store.d.ts
CHANGED
|
@@ -118,7 +118,7 @@ export interface MemoryStore {
|
|
|
118
118
|
archiveExpiredMemories(input?: ArchiveExpiredMemoriesInput): Promise<ArchiveExpiredMemoriesResult>;
|
|
119
119
|
/** Archive a visible memory in the current runtime context. */
|
|
120
120
|
archiveMemory(input: ArchiveMemoryInput): Promise<MemoryRecord>;
|
|
121
|
-
/** Store a personal memory for the current
|
|
121
|
+
/** Store a personal memory for the current actor. */
|
|
122
122
|
createMemory(input: CreateMemoryInput): Promise<CreateMemoryResult>;
|
|
123
123
|
/** Store a conversation memory for the current source conversation. */
|
|
124
124
|
createConversationMemory(input: CreateMemoryInput): Promise<CreateMemoryResult>;
|
package/dist/tools.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Source, type
|
|
1
|
+
import { type Source, type Actor } from "@sentry/junior-plugin-api";
|
|
2
2
|
import { type MemoryEmbeddingProvider, type MemoryDb, type MemorySupersessionDecider } from "./store";
|
|
3
3
|
import { type MemoryAgent } from "./agent";
|
|
4
4
|
export type MemoryReviewer = Pick<MemoryAgent, "reviewCreateRequest">;
|
|
@@ -8,7 +8,7 @@ export interface MemoryToolContext {
|
|
|
8
8
|
conversationId?: string;
|
|
9
9
|
db: MemoryDb;
|
|
10
10
|
embedder?: MemoryEmbeddingProvider;
|
|
11
|
-
|
|
11
|
+
actor?: Actor;
|
|
12
12
|
source: Source;
|
|
13
13
|
userText?: string;
|
|
14
14
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ export type MemoryEmbeddingMetric = (typeof MEMORY_EMBEDDING_METRICS)[number];
|
|
|
13
13
|
/** Runtime-owned memory invocation fields used for scope and source authority. */
|
|
14
14
|
export declare const slackMemoryRuntimeContextSchema: z.ZodObject<{
|
|
15
15
|
conversationId: z.ZodOptional<z.ZodString>;
|
|
16
|
-
|
|
16
|
+
actor: z.ZodOptional<z.ZodObject<{
|
|
17
17
|
platform: z.ZodLiteral<"slack">;
|
|
18
18
|
teamId: z.ZodString;
|
|
19
19
|
email: z.ZodOptional<z.ZodString>;
|
|
@@ -36,7 +36,7 @@ export declare const slackMemoryRuntimeContextSchema: z.ZodObject<{
|
|
|
36
36
|
/** Runtime-owned local memory invocation fields used for scope and source authority. */
|
|
37
37
|
export declare const localMemoryRuntimeContextSchema: z.ZodObject<{
|
|
38
38
|
conversationId: z.ZodOptional<z.ZodString>;
|
|
39
|
-
|
|
39
|
+
actor: z.ZodOptional<z.ZodObject<{
|
|
40
40
|
platform: z.ZodLiteral<"local">;
|
|
41
41
|
email: z.ZodOptional<z.ZodString>;
|
|
42
42
|
fullName: z.ZodOptional<z.ZodString>;
|
|
@@ -52,7 +52,7 @@ export declare const localMemoryRuntimeContextSchema: z.ZodObject<{
|
|
|
52
52
|
/** Runtime-owned memory invocation fields accepted by memory store operations. */
|
|
53
53
|
export declare const memoryRuntimeContextSchema: z.ZodUnion<readonly [z.ZodObject<{
|
|
54
54
|
conversationId: z.ZodOptional<z.ZodString>;
|
|
55
|
-
|
|
55
|
+
actor: z.ZodOptional<z.ZodObject<{
|
|
56
56
|
platform: z.ZodLiteral<"slack">;
|
|
57
57
|
teamId: z.ZodString;
|
|
58
58
|
email: z.ZodOptional<z.ZodString>;
|
|
@@ -73,7 +73,7 @@ export declare const memoryRuntimeContextSchema: z.ZodUnion<readonly [z.ZodObjec
|
|
|
73
73
|
}, z.core.$strict>;
|
|
74
74
|
}, z.core.$strict>, z.ZodObject<{
|
|
75
75
|
conversationId: z.ZodOptional<z.ZodString>;
|
|
76
|
-
|
|
76
|
+
actor: z.ZodOptional<z.ZodObject<{
|
|
77
77
|
platform: z.ZodLiteral<"local">;
|
|
78
78
|
email: z.ZodOptional<z.ZodString>;
|
|
79
79
|
fullName: z.ZodOptional<z.ZodString>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sentry/junior-memory",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.92.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.92.0"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"@types/node": "^25.9.1",
|
package/src/agent.ts
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { actorSchema, type PluginModel } from "@sentry/junior-plugin-api";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import type {
|
|
4
4
|
MemorySupersessionDecision,
|
|
5
5
|
MemorySupersessionInput,
|
|
6
6
|
} from "./store";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
MEMORY_KINDS,
|
|
9
|
+
memoryRuntimeContextSchema,
|
|
10
|
+
type MemoryKind,
|
|
11
|
+
} from "./types";
|
|
8
12
|
|
|
9
13
|
const memoryKindSchema = z.enum(MEMORY_KINDS);
|
|
10
14
|
const memoryRejectReasonSchema = z.enum([
|
|
@@ -30,6 +34,17 @@ const createMemoryRequestSchema = z
|
|
|
30
34
|
.optional(),
|
|
31
35
|
})
|
|
32
36
|
.strict();
|
|
37
|
+
const transcriptProvenanceSchema = z
|
|
38
|
+
.object({
|
|
39
|
+
authority: z.enum(["instruction", "context"]),
|
|
40
|
+
actor: actorSchema.optional(),
|
|
41
|
+
})
|
|
42
|
+
.strict();
|
|
43
|
+
const evidenceMessageIndicesSchema = z
|
|
44
|
+
.array(z.number().int().nonnegative())
|
|
45
|
+
.min(1)
|
|
46
|
+
.max(10)
|
|
47
|
+
.describe("Indices from <run-transcript> that directly support this memory.");
|
|
33
48
|
const extractSessionRequestSchema = z
|
|
34
49
|
.object({
|
|
35
50
|
existingMemories: z
|
|
@@ -42,6 +57,7 @@ const extractSessionRequestSchema = z
|
|
|
42
57
|
)
|
|
43
58
|
.max(10)
|
|
44
59
|
.default([]),
|
|
60
|
+
actors: z.array(actorSchema),
|
|
45
61
|
runtimeContext: memoryRuntimeContextSchema,
|
|
46
62
|
transcript: z
|
|
47
63
|
.array(
|
|
@@ -51,6 +67,8 @@ const extractSessionRequestSchema = z
|
|
|
51
67
|
type: z.literal("message"),
|
|
52
68
|
role: z.enum(["user", "assistant"]),
|
|
53
69
|
text: z.string().min(1),
|
|
70
|
+
provenance: transcriptProvenanceSchema.optional(),
|
|
71
|
+
isRunActor: z.boolean().optional(),
|
|
54
72
|
})
|
|
55
73
|
.strict(),
|
|
56
74
|
z
|
|
@@ -117,13 +135,13 @@ const memoryReviewResponseSchema = z.discriminatedUnion("decision", [
|
|
|
117
135
|
.object({
|
|
118
136
|
decision: z.literal("store"),
|
|
119
137
|
kind: memoryKindSchema.describe(
|
|
120
|
-
"Use preference only for
|
|
138
|
+
"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.",
|
|
121
139
|
),
|
|
122
140
|
canonicalFact: z
|
|
123
141
|
.string()
|
|
124
142
|
.min(1)
|
|
125
143
|
.describe(
|
|
126
|
-
"Stored memory text. It must be self-contained and must not include
|
|
144
|
+
"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.",
|
|
127
145
|
),
|
|
128
146
|
expiresAtMs: expiresAtMsSchema,
|
|
129
147
|
})
|
|
@@ -138,15 +156,16 @@ const memoryReviewResponseSchema = z.discriminatedUnion("decision", [
|
|
|
138
156
|
const extractedMemorySchema = z
|
|
139
157
|
.object({
|
|
140
158
|
kind: memoryKindSchema.describe(
|
|
141
|
-
"Use preference only for
|
|
159
|
+
"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.",
|
|
142
160
|
),
|
|
143
161
|
canonicalFact: z
|
|
144
162
|
.string()
|
|
145
163
|
.min(1)
|
|
146
164
|
.describe(
|
|
147
|
-
"Stored memory text as one self-contained fact. It must not include
|
|
165
|
+
"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.",
|
|
148
166
|
),
|
|
149
167
|
expiresAtMs: expiresAtMsSchema,
|
|
168
|
+
evidenceMessageIndices: evidenceMessageIndicesSchema,
|
|
150
169
|
})
|
|
151
170
|
.strict();
|
|
152
171
|
const extractedMemoryResultSchema = z
|
|
@@ -154,6 +173,7 @@ const extractedMemoryResultSchema = z
|
|
|
154
173
|
content: z.string().min(1),
|
|
155
174
|
expiresAtMs: expiresAtMsSchema,
|
|
156
175
|
kind: memoryKindSchema,
|
|
176
|
+
evidenceMessageIndices: evidenceMessageIndicesSchema,
|
|
157
177
|
})
|
|
158
178
|
.strict();
|
|
159
179
|
const extractMemoriesResponseSchema = z
|
|
@@ -220,7 +240,7 @@ const MEMORY_EXTRACTION_SYSTEM = [
|
|
|
220
240
|
].join("\n");
|
|
221
241
|
const MEMORY_SUPERSESSION_SYSTEM = [
|
|
222
242
|
"You are Junior's memory supersession agent.",
|
|
223
|
-
"Decide whether a new
|
|
243
|
+
"Decide whether a new actor preference clearly replaces existing active actor preferences.",
|
|
224
244
|
"Return supersedes_old only for obvious changed preferences about the same mutable slot.",
|
|
225
245
|
"If the facts are additive, different topics, duplicate, broader/narrower without direct replacement, or uncertain, do not supersede.",
|
|
226
246
|
].join("\n");
|
|
@@ -229,9 +249,9 @@ const CANONICAL_CONTENT_RULES = [
|
|
|
229
249
|
"- Store the minimum useful assertion supported by source evidence; do not add adjacent steps, caveats, or generalized advice.",
|
|
230
250
|
"- Do not return both concise and expanded variants of the same source assertion; keep the shortest self-contained canonical memory.",
|
|
231
251
|
"- Put ownership in structured fields, not prose.",
|
|
232
|
-
"- For
|
|
252
|
+
"- For actor memories, omit the subject and write a stable fact such as 'Prefers X', 'Uses Y', or 'Thinks Z'.",
|
|
233
253
|
"- Drop perspective/provenance markers while preserving useful context.",
|
|
234
|
-
"- Remove
|
|
254
|
+
"- Remove actor names, display names, actor/user labels, first- or second-person wording, thread labels, channel labels, and source labels.",
|
|
235
255
|
];
|
|
236
256
|
|
|
237
257
|
function escapeXml(value: string): string {
|
|
@@ -245,18 +265,18 @@ function runtimeDescription(
|
|
|
245
265
|
request: Pick<CreateMemoryRequest, "expiresAtMs" | "runtimeContext">,
|
|
246
266
|
): string {
|
|
247
267
|
const runtime = request.runtimeContext;
|
|
248
|
-
const
|
|
249
|
-
runtime.
|
|
250
|
-
? `slack:${runtime.
|
|
251
|
-
: runtime.
|
|
252
|
-
? `local:${runtime.
|
|
268
|
+
const actor =
|
|
269
|
+
runtime.actor?.platform === "slack"
|
|
270
|
+
? `slack:${runtime.actor.teamId}:${runtime.actor.userId}`
|
|
271
|
+
: runtime.actor?.platform === "local"
|
|
272
|
+
? `local:${runtime.actor.userId}`
|
|
253
273
|
: "none";
|
|
254
274
|
const source =
|
|
255
275
|
runtime.source.platform === "slack"
|
|
256
276
|
? `slack:${runtime.source.teamId}:${runtime.source.channelId}`
|
|
257
277
|
: `local:${runtime.source.conversationId}`;
|
|
258
278
|
const lines = [
|
|
259
|
-
`-
|
|
279
|
+
`- actor: ${escapeXml(actor)}`,
|
|
260
280
|
`- source: ${escapeXml(source)}`,
|
|
261
281
|
`- has_conversation: ${runtime.conversationId ? "true" : "false"}`,
|
|
262
282
|
`- expires_at: ${
|
|
@@ -295,14 +315,29 @@ function existingMemoriesContext(request: ExtractSessionRequest): string {
|
|
|
295
315
|
].join("\n");
|
|
296
316
|
}
|
|
297
317
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
318
|
+
/**
|
|
319
|
+
* Passive extraction offers personal preferences only on single-actor runs.
|
|
320
|
+
* Multi-actor runs restrict extraction to conversation-scoped kinds.
|
|
321
|
+
*/
|
|
322
|
+
function allowedExtractionKinds(actorCount: number): Set<MemoryKind> {
|
|
323
|
+
return actorCount === 1
|
|
324
|
+
? new Set<MemoryKind>(MEMORY_KINDS)
|
|
325
|
+
: new Set<MemoryKind>(["procedure", "knowledge"]);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function memoryKindsContext(allowedKinds: Set<MemoryKind>): string {
|
|
329
|
+
const lines = ["<memory-kinds>"];
|
|
330
|
+
if (allowedKinds.has("preference")) {
|
|
331
|
+
lines.push(
|
|
332
|
+
"- preference: a durable first-person personal preference, opinion, habit, or workflow owned by the current actor. Stored as actor memory.",
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
lines.push(
|
|
302
336
|
"- 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.",
|
|
303
|
-
"- knowledge: stable shared project, channel, operational, or runbook fact that is not a personal
|
|
337
|
+
"- 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.",
|
|
304
338
|
"</memory-kinds>",
|
|
305
|
-
|
|
339
|
+
);
|
|
340
|
+
return lines.join("\n");
|
|
306
341
|
}
|
|
307
342
|
|
|
308
343
|
function reviewPrompt(request: CreateMemoryRequest): string {
|
|
@@ -321,17 +356,17 @@ function reviewPrompt(request: CreateMemoryRequest): string {
|
|
|
321
356
|
"<rules>",
|
|
322
357
|
"- Return store only when the candidate is public/shareable, durable, and self-contained.",
|
|
323
358
|
"- First classify the memory kind: preference, procedure, or knowledge.",
|
|
324
|
-
"- Use kind=preference only for first-person facts authored by the current
|
|
325
|
-
"- 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
|
|
359
|
+
"- Use kind=preference only for first-person facts authored by the current actor about their own preference, opinion, habit, identity, or workflow.",
|
|
360
|
+
"- 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.",
|
|
326
361
|
"- Use kind=procedure for reusable task/process/runbook instructions.",
|
|
327
362
|
"- Use kind=knowledge for shared project, channel, operational, or runbook facts.",
|
|
328
363
|
"- 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.",
|
|
329
|
-
"- A candidate may be badly phrased by an outer assistant or extraction pass. When current-user-message contains the
|
|
364
|
+
"- 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.",
|
|
330
365
|
"- 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.",
|
|
331
366
|
"- 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.",
|
|
332
367
|
"- Store content as person-less, source-less canonical knowledge. Ownership and source live in structured metadata, not prose.",
|
|
333
|
-
"- For
|
|
334
|
-
"- Remove
|
|
368
|
+
"- For actor memories, omit the subject and write the content as a stable fact such as 'Prefers X', 'Uses Y', or 'Thinks Z'.",
|
|
369
|
+
"- Remove actor names, display names, actor/user labels, first- or second-person wording, thread labels, channel labels, and source labels from stored content.",
|
|
335
370
|
"- Reject third-party personal profile facts, even if they mention a name.",
|
|
336
371
|
"- Reject vague content such as 'remember this' unless the candidate or current-user-message contains the concrete fact.",
|
|
337
372
|
"- Preserve the requested expiration when one exists; otherwise set expiresAtMs to null.",
|
|
@@ -342,6 +377,20 @@ function reviewPrompt(request: CreateMemoryRequest): string {
|
|
|
342
377
|
return sections.join("\n");
|
|
343
378
|
}
|
|
344
379
|
|
|
380
|
+
function transcriptActorLabel(
|
|
381
|
+
actor: z.output<typeof actorSchema> | undefined,
|
|
382
|
+
): string {
|
|
383
|
+
if (!actor) {
|
|
384
|
+
return "none";
|
|
385
|
+
}
|
|
386
|
+
if (actor.platform === "system") {
|
|
387
|
+
return `system:${actor.name}`;
|
|
388
|
+
}
|
|
389
|
+
return actor.platform === "slack"
|
|
390
|
+
? `slack:${actor.teamId}:${actor.userId}`
|
|
391
|
+
: `local:${actor.userId}`;
|
|
392
|
+
}
|
|
393
|
+
|
|
345
394
|
function runTranscriptContext(request: ExtractSessionRequest): string {
|
|
346
395
|
return [
|
|
347
396
|
"<run-transcript>",
|
|
@@ -353,8 +402,11 @@ function runTranscriptContext(request: ExtractSessionRequest): string {
|
|
|
353
402
|
"</tool-result>",
|
|
354
403
|
].join("\n");
|
|
355
404
|
}
|
|
405
|
+
const authority = entry.provenance?.authority ?? "context";
|
|
406
|
+
const isRunActor = entry.isRunActor === true;
|
|
407
|
+
const actor = transcriptActorLabel(entry.provenance?.actor);
|
|
356
408
|
return [
|
|
357
|
-
`<message index="${index}" role="${entry.role}">`,
|
|
409
|
+
`<message index="${index}" role="${entry.role}" authority="${authority}" is_run_actor="${isRunActor ? "true" : "false"}" actor="${escapeXml(actor)}">`,
|
|
358
410
|
escapeXml(entry.text),
|
|
359
411
|
"</message>",
|
|
360
412
|
].join("\n");
|
|
@@ -364,6 +416,8 @@ function runTranscriptContext(request: ExtractSessionRequest): string {
|
|
|
364
416
|
}
|
|
365
417
|
|
|
366
418
|
function sessionExtractionPrompt(request: ExtractSessionRequest): string {
|
|
419
|
+
const allowedKinds = allowedExtractionKinds(request.actors.length);
|
|
420
|
+
const allowsPreference = allowedKinds.has("preference");
|
|
367
421
|
return [
|
|
368
422
|
"<memory-extraction-input>",
|
|
369
423
|
"Extract durable memories from this completed agent run using the runtime-owned context below.",
|
|
@@ -374,12 +428,21 @@ function sessionExtractionPrompt(request: ExtractSessionRequest): string {
|
|
|
374
428
|
"",
|
|
375
429
|
existingMemoriesContext(request),
|
|
376
430
|
"",
|
|
377
|
-
memoryKindsContext(),
|
|
431
|
+
memoryKindsContext(allowedKinds),
|
|
378
432
|
"",
|
|
379
433
|
runTranscriptContext(request),
|
|
380
434
|
"",
|
|
381
435
|
"<rules>",
|
|
382
436
|
"- Return at most five memories.",
|
|
437
|
+
"- Every returned memory must cite one or more evidenceMessageIndices from <run-transcript>.",
|
|
438
|
+
"- Cite only indices that directly support the stored fact; do not cite assistant messages as independent evidence.",
|
|
439
|
+
"- Each transcript message exposes authority (instruction or context), is_run_actor, and an actor id. Use these to classify evidence.",
|
|
440
|
+
...(allowsPreference
|
|
441
|
+
? [
|
|
442
|
+
"- 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.",
|
|
443
|
+
]
|
|
444
|
+
: []),
|
|
445
|
+
"- For a procedure or knowledge memory, cite run-actor instruction messages, public context messages, or successful tool results.",
|
|
383
446
|
"- Use user messages and successful tool results as source evidence for storable facts.",
|
|
384
447
|
"- Use failed tool results only when the failure reveals durable process knowledge, not transient errors.",
|
|
385
448
|
"- Use assistant messages only as context; do not store the assistant's claims unless supported by user messages or tool results.",
|
|
@@ -391,9 +454,20 @@ function sessionExtractionPrompt(request: ExtractSessionRequest): string {
|
|
|
391
454
|
"- 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.",
|
|
392
455
|
"- Set kind=procedure for reusable task/process/runbook instructions.",
|
|
393
456
|
"- Set kind=knowledge for shared team, project, channel, runbook, or operational facts.",
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
457
|
+
...(allowsPreference
|
|
458
|
+
? [
|
|
459
|
+
"- Set kind=preference only for clear durable first-person facts authored by the current actor about their own preference, opinion, habit, identity, or workflow.",
|
|
460
|
+
"- 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.",
|
|
461
|
+
"- 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.",
|
|
462
|
+
]
|
|
463
|
+
: [
|
|
464
|
+
"- This completed run has multiple run actors. Return only conversation-scoped procedure or knowledge memories.",
|
|
465
|
+
"- Do not return personal preferences, opinions, habits, identity facts, or workflow preferences from any actor in this run.",
|
|
466
|
+
"- 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.",
|
|
467
|
+
"- 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.",
|
|
468
|
+
]),
|
|
469
|
+
"- 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.",
|
|
470
|
+
"- User-authored task instructions are procedures, not preferences, unless they explicitly describe the actor's personal preference or habit.",
|
|
397
471
|
"- Procedural statements such as 'for X, do Y', 'when X, do Y', and 'to accomplish X, do Y' belong in procedures.",
|
|
398
472
|
...CANONICAL_CONTENT_RULES,
|
|
399
473
|
"- Skip a candidate when existing-memories already cover the same durable fact.",
|
|
@@ -505,6 +579,7 @@ function extractedMemoriesFromResponse(
|
|
|
505
579
|
content: memory.canonicalFact,
|
|
506
580
|
expiresAtMs: memory.expiresAtMs,
|
|
507
581
|
kind: memory.kind,
|
|
582
|
+
evidenceMessageIndices: memory.evidenceMessageIndices,
|
|
508
583
|
});
|
|
509
584
|
return response.memories.map(toMemory);
|
|
510
585
|
}
|
package/src/plugin.ts
CHANGED
|
@@ -15,7 +15,8 @@ import { createMemoryPromptMessages } from "./recall";
|
|
|
15
15
|
import type { MemoryDb } from "./store";
|
|
16
16
|
|
|
17
17
|
const MEMORY_MODEL_ENV = "AI_MEMORY_MODEL";
|
|
18
|
-
const MEMORY_RECALL_MAX_VECTOR_DISTANCE_ENV =
|
|
18
|
+
const MEMORY_RECALL_MAX_VECTOR_DISTANCE_ENV =
|
|
19
|
+
"MEMORY_RECALL_MAX_VECTOR_DISTANCE";
|
|
19
20
|
const DEFAULT_RECALL_MAX_VECTOR_DISTANCE = 0.45;
|
|
20
21
|
|
|
21
22
|
export interface MemoryPluginOptions {
|
|
@@ -52,14 +53,14 @@ function memoryToolContext(ctx: {
|
|
|
52
53
|
conversationId?: string;
|
|
53
54
|
db: MemoryToolContext["db"];
|
|
54
55
|
embedder?: MemoryToolContext["embedder"];
|
|
55
|
-
|
|
56
|
+
actor?: MemoryToolContext["actor"];
|
|
56
57
|
source: MemoryToolContext["source"];
|
|
57
58
|
userText?: string;
|
|
58
59
|
}): MemoryToolContext {
|
|
59
60
|
return {
|
|
60
61
|
agent: ctx.agent,
|
|
61
62
|
...(ctx.conversationId ? { conversationId: ctx.conversationId } : {}),
|
|
62
|
-
...(ctx.
|
|
63
|
+
...(ctx.actor ? { actor: ctx.actor } : {}),
|
|
63
64
|
db: ctx.db,
|
|
64
65
|
...(ctx.embedder ? { embedder: ctx.embedder } : {}),
|
|
65
66
|
source: ctx.source,
|
|
@@ -72,7 +73,7 @@ function memoryCreateToolContext(ctx: {
|
|
|
72
73
|
conversationId?: string;
|
|
73
74
|
db: MemoryCreateToolContext["db"];
|
|
74
75
|
embedder?: MemoryCreateToolContext["embedder"];
|
|
75
|
-
|
|
76
|
+
actor?: MemoryCreateToolContext["actor"];
|
|
76
77
|
source: MemoryCreateToolContext["source"];
|
|
77
78
|
supersessionDecider: MemoryCreateToolContext["supersessionDecider"];
|
|
78
79
|
userText?: string;
|
|
@@ -133,7 +134,7 @@ export function createMemoryPlugin(options: MemoryPluginOptions = {}) {
|
|
|
133
134
|
async userPrompt(ctx) {
|
|
134
135
|
return await createMemoryPromptMessages({
|
|
135
136
|
...(ctx.conversationId ? { conversationId: ctx.conversationId } : {}),
|
|
136
|
-
...(ctx.
|
|
137
|
+
...(ctx.actor ? { actor: ctx.actor } : {}),
|
|
137
138
|
db: ctx.db as MemoryDb,
|
|
138
139
|
embedder: ctx.embedder,
|
|
139
140
|
maxVectorDistance: recallMaxVectorDistance(options),
|
package/src/process-session.ts
CHANGED
|
@@ -2,6 +2,8 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import {
|
|
3
3
|
getSourceKey,
|
|
4
4
|
isPrivateSource,
|
|
5
|
+
type PluginRunContext,
|
|
6
|
+
type PluginRunTranscriptEntry,
|
|
5
7
|
type PluginTaskContext,
|
|
6
8
|
} from "@sentry/junior-plugin-api";
|
|
7
9
|
import { z } from "zod";
|
|
@@ -15,11 +17,7 @@ import {
|
|
|
15
17
|
parseExtractedMemory,
|
|
16
18
|
type ExtractedMemory,
|
|
17
19
|
} from "./agent";
|
|
18
|
-
import {
|
|
19
|
-
MEMORY_KINDS,
|
|
20
|
-
memoryRuntimeContextSchema,
|
|
21
|
-
type MemoryKind,
|
|
22
|
-
} from "./types";
|
|
20
|
+
import { MEMORY_KINDS, memoryRuntimeContextSchema } from "./types";
|
|
23
21
|
|
|
24
22
|
const MEMORY_TOOL_NAMES = new Set([
|
|
25
23
|
"createMemory",
|
|
@@ -34,21 +32,111 @@ const extractedMemoryCacheSchema = z.array(
|
|
|
34
32
|
content: z.string().min(1),
|
|
35
33
|
expiresAtMs: z.number().finite().nullable(),
|
|
36
34
|
kind: z.enum(MEMORY_KINDS),
|
|
35
|
+
evidenceMessageIndices: z
|
|
36
|
+
.array(z.number().int().nonnegative())
|
|
37
|
+
.min(1)
|
|
38
|
+
.max(10),
|
|
37
39
|
})
|
|
38
40
|
.strict()
|
|
39
41
|
.transform(parseExtractedMemory),
|
|
40
42
|
);
|
|
41
43
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
44
|
+
/** Where a passively extracted memory may be stored, or dropped when unproven. */
|
|
45
|
+
type MemoryRouteTarget = "drop" | "personal" | "conversation";
|
|
46
|
+
|
|
47
|
+
/** A cited entry is a run-actor durable instruction evidence entry. */
|
|
48
|
+
function isRunActorInstruction(entry: PluginRunTranscriptEntry): boolean {
|
|
49
|
+
return (
|
|
50
|
+
entry.type === "message" &&
|
|
51
|
+
entry.role === "user" &&
|
|
52
|
+
entry.provenance?.authority === "instruction" &&
|
|
53
|
+
entry.isRunActor === true
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** A cited entry is valid public conversation evidence for shared knowledge. */
|
|
58
|
+
function isConversationEvidence(entry: PluginRunTranscriptEntry): boolean {
|
|
59
|
+
if (entry.type === "toolResult") {
|
|
60
|
+
return entry.isError === false && Boolean(entry.text?.trim());
|
|
61
|
+
}
|
|
62
|
+
if (
|
|
63
|
+
entry.type === "message" &&
|
|
64
|
+
entry.role === "user" &&
|
|
65
|
+
entry.provenance?.authority === "instruction" &&
|
|
66
|
+
entry.isRunActor === false
|
|
67
|
+
) {
|
|
68
|
+
return Boolean(entry.provenance.actor);
|
|
69
|
+
}
|
|
70
|
+
return (
|
|
71
|
+
entry.type === "message" &&
|
|
72
|
+
entry.role === "user" &&
|
|
73
|
+
entry.provenance?.authority === "context"
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Resolve the deduplicated cited transcript entries, failing on bad indices. */
|
|
78
|
+
function citedEntries(
|
|
79
|
+
indices: number[],
|
|
80
|
+
transcript: PluginRunTranscriptEntry[],
|
|
81
|
+
): { valid: boolean; entries: PluginRunTranscriptEntry[] } {
|
|
82
|
+
const seen = new Set<number>();
|
|
83
|
+
const entries: PluginRunTranscriptEntry[] = [];
|
|
84
|
+
for (const index of indices) {
|
|
85
|
+
if (seen.has(index)) {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
seen.add(index);
|
|
89
|
+
const entry = transcript[index];
|
|
90
|
+
if (!entry) {
|
|
91
|
+
return { valid: false, entries: [] };
|
|
92
|
+
}
|
|
93
|
+
entries.push(entry);
|
|
45
94
|
}
|
|
46
|
-
return
|
|
95
|
+
return { valid: entries.length > 0, entries };
|
|
47
96
|
}
|
|
48
97
|
|
|
49
|
-
|
|
98
|
+
/**
|
|
99
|
+
* Verify an extracted memory against runtime-owned provenance on its cited
|
|
100
|
+
* evidence. This is a deterministic authority boundary, not a model decision:
|
|
101
|
+
* personal preferences require a single-actor run whose citations are all
|
|
102
|
+
* run-actor instructions, conversation knowledge requires run-actor instruction
|
|
103
|
+
* or valid public conversation evidence, and anything unproven (including
|
|
104
|
+
* missing provenance) is dropped. Multi-actor runs interleave first-person
|
|
105
|
+
* statements from different people, so they never store a preference regardless
|
|
106
|
+
* of citations; a personal preference can wait for a single-actor run.
|
|
107
|
+
*/
|
|
108
|
+
function routeExtractedMemory(
|
|
109
|
+
memory: ExtractedMemory,
|
|
110
|
+
transcript: PluginRunTranscriptEntry[],
|
|
111
|
+
run: Pick<PluginRunContext, "actor" | "actors">,
|
|
112
|
+
): MemoryRouteTarget {
|
|
113
|
+
const cited = citedEntries(memory.evidenceMessageIndices, transcript);
|
|
114
|
+
if (!cited.valid) {
|
|
115
|
+
return "drop";
|
|
116
|
+
}
|
|
117
|
+
if (memory.kind === "preference") {
|
|
118
|
+
// Only a run attributed to exactly one run actor may store a preference; an
|
|
119
|
+
// empty actor set (system runs) fails closed to "drop".
|
|
120
|
+
const exactlyOneRunActor = Boolean(run.actor) && run.actors.length === 1;
|
|
121
|
+
if (!exactlyOneRunActor) {
|
|
122
|
+
return "drop";
|
|
123
|
+
}
|
|
124
|
+
// Never downgrade an unproven first-person preference to conversation scope.
|
|
125
|
+
return cited.entries.every(isRunActorInstruction) ? "personal" : "drop";
|
|
126
|
+
}
|
|
127
|
+
return cited.entries.every(
|
|
128
|
+
(entry) => isRunActorInstruction(entry) || isConversationEvidence(entry),
|
|
129
|
+
)
|
|
130
|
+
? "conversation"
|
|
131
|
+
: "drop";
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function memoryIdempotencySuffix(
|
|
135
|
+
memory: ExtractedMemory,
|
|
136
|
+
target: MemoryRouteTarget,
|
|
137
|
+
): string {
|
|
50
138
|
return createHash("sha256")
|
|
51
|
-
.update(
|
|
139
|
+
.update(target)
|
|
52
140
|
.update("\0")
|
|
53
141
|
.update(memory.kind)
|
|
54
142
|
.update("\0")
|
|
@@ -63,10 +151,11 @@ function passiveInput(
|
|
|
63
151
|
sessionId: string,
|
|
64
152
|
memory: ExtractedMemory,
|
|
65
153
|
sourceKey: string,
|
|
154
|
+
target: MemoryRouteTarget,
|
|
66
155
|
): CreateMemoryInput {
|
|
67
156
|
return {
|
|
68
157
|
content: memory.content,
|
|
69
|
-
idempotencyKey: `session:${sourceKey}:${sessionId}:${memoryIdempotencySuffix(memory)}`,
|
|
158
|
+
idempotencyKey: `session:${sourceKey}:${sessionId}:${memoryIdempotencySuffix(memory, target)}`,
|
|
70
159
|
kind: memory.kind,
|
|
71
160
|
...(memory.expiresAtMs !== null ? { expiresAtMs: memory.expiresAtMs } : {}),
|
|
72
161
|
};
|
|
@@ -79,7 +168,11 @@ async function getTaskMemories(
|
|
|
79
168
|
const cacheKey = `memory-extraction:${context.id}`;
|
|
80
169
|
const cached = await context.state.get(cacheKey);
|
|
81
170
|
if (cached !== undefined) {
|
|
82
|
-
|
|
171
|
+
const parsed = extractedMemoryCacheSchema.safeParse(cached);
|
|
172
|
+
if (parsed.success) {
|
|
173
|
+
return parsed.data;
|
|
174
|
+
}
|
|
175
|
+
await context.state.delete(cacheKey);
|
|
83
176
|
}
|
|
84
177
|
const memories = await extract();
|
|
85
178
|
if (memories.length > 0) {
|
|
@@ -132,7 +225,7 @@ export async function processMemorySession(
|
|
|
132
225
|
|
|
133
226
|
const runtimeContext = memoryRuntimeContextSchema.parse({
|
|
134
227
|
conversationId: run.conversationId,
|
|
135
|
-
...(run.
|
|
228
|
+
...(run.actor ? { actor: run.actor } : {}),
|
|
136
229
|
source: run.source,
|
|
137
230
|
});
|
|
138
231
|
const agent = createMemoryAgent(context.model);
|
|
@@ -150,6 +243,7 @@ export async function processMemorySession(
|
|
|
150
243
|
existingMemories: existingMemories.map((memory) => ({
|
|
151
244
|
content: memory.content,
|
|
152
245
|
})),
|
|
246
|
+
actors: run.actors,
|
|
153
247
|
transcript,
|
|
154
248
|
runtimeContext,
|
|
155
249
|
});
|
|
@@ -159,12 +253,16 @@ export async function processMemorySession(
|
|
|
159
253
|
}
|
|
160
254
|
|
|
161
255
|
for (const memory of memories) {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
256
|
+
// The routing gate stays even though extraction is also actor-gated:
|
|
257
|
+
// getTaskMemories caches extraction output for 7 days, so a retry can replay
|
|
258
|
+
// preference proposals cached before this gate existed.
|
|
259
|
+
const target = routeExtractedMemory(memory, transcript, run);
|
|
260
|
+
if (target === "drop") {
|
|
165
261
|
continue;
|
|
166
262
|
}
|
|
167
|
-
|
|
263
|
+
const input = passiveInput(run.runId, memory, sourceKey, target);
|
|
264
|
+
if (target === "conversation") {
|
|
265
|
+
await store.createConversationMemory(input);
|
|
168
266
|
continue;
|
|
169
267
|
}
|
|
170
268
|
await store.createMemory(input);
|