@sentry/junior-memory 0.197.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.
@@ -1,311 +0,0 @@
1
- import { createHash } from "node:crypto";
2
- import {
3
- getSourceKey,
4
- type PluginRunContext,
5
- type PluginRunTranscriptEntry,
6
- type PluginTaskContext,
7
- } from "@sentry/junior-plugin-api";
8
- import { z } from "zod";
9
- import {
10
- createMemoryStore,
11
- type CreateMemoryInput,
12
- type CreateMemoryResult,
13
- type MemoryDb,
14
- } from "./store";
15
- import {
16
- createMemoryAgent,
17
- parseExtractedMemory,
18
- type ExtractedMemory,
19
- type MemoryExtractionResult,
20
- } from "./agent";
21
- import { MEMORY_KINDS, memoryRuntimeContextSchema } from "./types";
22
- import { capturedMemory, memoriesCapturedEvent } from "./events";
23
-
24
- const MEMORY_TOOL_NAMES = new Set([
25
- "createMemory",
26
- "listMemories",
27
- "removeMemory",
28
- "searchMemories",
29
- ]);
30
- const MEMORY_TASK_STATE_TTL_MS = 7 * 24 * 60 * 60 * 1000;
31
- const extractedMemorySchema = z
32
- .object({
33
- content: z.string().min(1),
34
- expiresAtMs: z.number().finite().nullable(),
35
- kind: z.enum(MEMORY_KINDS),
36
- evidenceMessageIndices: z
37
- .array(z.number().int().nonnegative())
38
- .min(1)
39
- .max(10),
40
- })
41
- .strict()
42
- .transform(parseExtractedMemory);
43
- const extractedMemoryCacheSchema = z.union([
44
- z
45
- .object({
46
- costUsd: z.number().finite().nonnegative().optional(),
47
- memories: z.array(extractedMemorySchema).max(5),
48
- })
49
- .strict(),
50
- z
51
- .array(extractedMemorySchema)
52
- .max(5)
53
- .transform((memories) => ({ memories })),
54
- ]);
55
-
56
- /** Subject for a passively extracted memory, or drop when unproven. */
57
- type MemorySubjectTarget = "drop" | "user" | "conversation";
58
-
59
- function recordCapturedMemory(
60
- captured: ReturnType<typeof capturedMemory>[],
61
- result: CreateMemoryResult,
62
- ): void {
63
- const supersededIds = new Set(result.supersededIds ?? []);
64
- for (let index = captured.length - 1; index >= 0; index -= 1) {
65
- if (supersededIds.has(captured[index]!.id)) {
66
- captured.splice(index, 1);
67
- }
68
- }
69
- if (result.created || result.idempotent) {
70
- captured.push(capturedMemory(result.memory));
71
- }
72
- }
73
-
74
- /** A cited entry is a run-actor durable instruction evidence entry. */
75
- function isRunActorInstruction(entry: PluginRunTranscriptEntry): boolean {
76
- return (
77
- entry.type === "message" &&
78
- entry.role === "user" &&
79
- entry.provenance?.authority === "instruction" &&
80
- entry.isRunActor === true
81
- );
82
- }
83
-
84
- /** A cited entry is valid public conversation evidence for shared knowledge. */
85
- function isConversationEvidence(entry: PluginRunTranscriptEntry): boolean {
86
- if (entry.type === "toolResult") {
87
- return entry.isError === false && Boolean(entry.text?.trim());
88
- }
89
- if (
90
- entry.type === "message" &&
91
- entry.role === "user" &&
92
- entry.provenance?.authority === "instruction" &&
93
- entry.isRunActor === false
94
- ) {
95
- return Boolean(entry.provenance.actor);
96
- }
97
- return (
98
- entry.type === "message" &&
99
- entry.role === "user" &&
100
- entry.provenance?.authority === "context"
101
- );
102
- }
103
-
104
- /** Resolve the deduplicated cited transcript entries, failing on bad indices. */
105
- function citedEntries(
106
- indices: number[],
107
- transcript: PluginRunTranscriptEntry[],
108
- ): { valid: boolean; entries: PluginRunTranscriptEntry[] } {
109
- const seen = new Set<number>();
110
- const entries: PluginRunTranscriptEntry[] = [];
111
- for (const index of indices) {
112
- if (seen.has(index)) {
113
- continue;
114
- }
115
- seen.add(index);
116
- const entry = transcript[index];
117
- if (!entry) {
118
- return { valid: false, entries: [] };
119
- }
120
- entries.push(entry);
121
- }
122
- return { valid: entries.length > 0, entries };
123
- }
124
-
125
- /**
126
- * Verify an extracted memory against runtime-owned provenance on its cited
127
- * evidence. This is a deterministic authority boundary, not a model decision:
128
- * personal preferences require a single-actor run whose citations are all
129
- * run-actor instructions, conversation knowledge requires run-actor instruction
130
- * or valid public conversation evidence, and anything unproven (including
131
- * missing provenance) is dropped. Multi-actor runs interleave first-person
132
- * statements from different people, so they never store a preference regardless
133
- * of citations; a personal preference can wait for a single-actor run.
134
- */
135
- function routeExtractedMemory(
136
- memory: ExtractedMemory,
137
- transcript: PluginRunTranscriptEntry[],
138
- run: Pick<PluginRunContext, "actor" | "actors" | "actorUserId">,
139
- ): MemorySubjectTarget {
140
- const cited = citedEntries(memory.evidenceMessageIndices, transcript);
141
- if (!cited.valid) {
142
- return "drop";
143
- }
144
- if (memory.kind === "preference") {
145
- // Only a run attributed to exactly one human run actor may store a preference.
146
- const exactlyOneHumanRunActor =
147
- run.actorUserId !== undefined &&
148
- run.actor !== undefined &&
149
- run.actor.platform !== "system" &&
150
- run.actors.length === 1 &&
151
- run.actors[0]?.platform !== "system";
152
- if (!exactlyOneHumanRunActor) {
153
- return "drop";
154
- }
155
- // Never downgrade an unproven first-person preference to conversation subject.
156
- return cited.entries.every(isRunActorInstruction) ? "user" : "drop";
157
- }
158
- return cited.entries.every(
159
- (entry) => isRunActorInstruction(entry) || isConversationEvidence(entry),
160
- )
161
- ? "conversation"
162
- : "drop";
163
- }
164
-
165
- function memoryIdempotencySuffix(
166
- memory: ExtractedMemory,
167
- target: MemorySubjectTarget,
168
- ): string {
169
- return createHash("sha256")
170
- .update(target)
171
- .update("\0")
172
- .update(memory.kind)
173
- .update("\0")
174
- .update(memory.content)
175
- .update("\0")
176
- .update(memory.expiresAtMs === null ? "never" : String(memory.expiresAtMs))
177
- .digest("hex")
178
- .slice(0, 32);
179
- }
180
-
181
- function passiveInput(
182
- sessionId: string,
183
- memory: ExtractedMemory,
184
- sourceKey: string,
185
- target: MemorySubjectTarget,
186
- ): CreateMemoryInput {
187
- return {
188
- content: memory.content,
189
- idempotencyKey: `session:${sourceKey}:${sessionId}:${memoryIdempotencySuffix(memory, target)}`,
190
- kind: memory.kind,
191
- ...(memory.expiresAtMs !== null
192
- ? { expiresAtMs: memory.expiresAtMs }
193
- : undefined),
194
- };
195
- }
196
-
197
- async function getTaskExtraction(
198
- context: PluginTaskContext,
199
- extract: () => Promise<MemoryExtractionResult>,
200
- ): Promise<MemoryExtractionResult> {
201
- const cacheKey = `memory-extraction:${context.id}`;
202
- const cached = await context.state.get(cacheKey);
203
- if (cached !== undefined) {
204
- const parsed = extractedMemoryCacheSchema.safeParse(cached);
205
- if (parsed.success) {
206
- return parsed.data;
207
- }
208
- await context.state.delete(cacheKey);
209
- }
210
- const extraction = await extract();
211
- await context.state.set(cacheKey, extraction, MEMORY_TASK_STATE_TTL_MS);
212
- return extraction;
213
- }
214
-
215
- /**
216
- * Extract and store memories from a completed session plugin task.
217
- *
218
- * Memory owns learning after a run and reads only the plugin run data.
219
- * Explicit memory tools stay separate so retries do not reinterpret user
220
- * requests.
221
- */
222
- export async function processMemorySession(
223
- context: PluginTaskContext,
224
- ): Promise<void> {
225
- const run = await context.run.load();
226
- // TODO(dcramer): Replace this hardcoded Source check when a plugin Run can
227
- // state whether passive memory extraction should run.
228
- if (run.source.kind !== "slack" && run.source.kind !== "web") {
229
- return;
230
- }
231
- // Memory tool turns already own memory management or recall; do not reinterpret
232
- // recalled memory output as fresh passive-learning evidence.
233
- if (
234
- run.transcript.some(
235
- (entry) =>
236
- entry.type === "toolResult" && MEMORY_TOOL_NAMES.has(entry.toolName),
237
- )
238
- ) {
239
- return;
240
- }
241
- const sourceKey = getSourceKey(run.source);
242
- if (!sourceKey || (run.source.visibility === "private" && !run.actorUserId)) {
243
- return;
244
- }
245
- const transcript = run.transcript
246
- .filter((entry) => entry.text?.trim())
247
- .map((entry) => ({ ...entry, text: entry.text!.trim() }));
248
- const evidenceText = transcript
249
- .filter((entry) => entry.type === "toolResult" || entry.role === "user")
250
- .map((entry) => entry.text)
251
- .join("\n\n")
252
- .trim();
253
- if (!evidenceText) {
254
- return;
255
- }
256
-
257
- const runtimeContext = memoryRuntimeContextSchema.parse({
258
- conversationId: run.conversationId,
259
- ...(run.locationId ? { locationId: run.locationId } : undefined),
260
- ...(run.actor ? { actor: run.actor } : undefined),
261
- source: run.source,
262
- ...(run.actorUserId ? { userId: run.actorUserId } : undefined),
263
- });
264
- const agent = createMemoryAgent(context.model);
265
- const store = createMemoryStore(context.db as MemoryDb, runtimeContext, {
266
- embedder: context.embedder,
267
- supersessionDecider: agent,
268
- });
269
- await store.archiveExpiredMemories();
270
- const extraction = await getTaskExtraction(context, async () => {
271
- const existingMemories = await store.searchMemories({
272
- limit: 10,
273
- query: evidenceText,
274
- });
275
- return await agent.extractSessionMemories({
276
- existingMemories: existingMemories.map((memory) => ({
277
- content: memory.content,
278
- })),
279
- actors: run.actors,
280
- transcript,
281
- runtimeContext,
282
- });
283
- });
284
-
285
- const captured: ReturnType<typeof capturedMemory>[] = [];
286
- for (const memory of extraction.memories) {
287
- // The routing gate stays even though extraction is also actor-gated:
288
- // getTaskExtraction caches extraction output for 7 days, so a retry can replay
289
- // preference proposals cached before this gate existed.
290
- const target = routeExtractedMemory(memory, transcript, run);
291
- if (target === "drop") {
292
- continue;
293
- }
294
- const input = passiveInput(run.runId, memory, sourceKey, target);
295
- if (target === "conversation") {
296
- const result = await store.createConversationMemory(input);
297
- recordCapturedMemory(captured, result);
298
- continue;
299
- }
300
- const result = await store.createMemory(input);
301
- recordCapturedMemory(captured, result);
302
- }
303
- await context.events.emit(
304
- memoriesCapturedEvent({
305
- memories: captured,
306
- ...(extraction.costUsd !== undefined
307
- ? { costUsd: extraction.costUsd }
308
- : undefined),
309
- }),
310
- );
311
- }
package/src/ranking.ts DELETED
@@ -1,107 +0,0 @@
1
- import type { MemoryRecord } from "./store";
2
-
3
- const RECIPROCAL_RANK_FUSION_K = 60;
4
- const ONE_DAY_MS = 24 * 60 * 60 * 1000;
5
- const DEFAULT_RRF_WEIGHT = 1;
6
-
7
- export interface MemoryMatch {
8
- lexical?: {
9
- rank: number;
10
- };
11
- memory: MemoryRecord;
12
- vector?: {
13
- rank: number;
14
- };
15
- }
16
-
17
- function reciprocalRank(rank: number, weight: number): number {
18
- return weight / (RECIPROCAL_RANK_FUSION_K + rank);
19
- }
20
-
21
- function matchScore(
22
- match: MemoryMatch,
23
- weights: { lexicalWeight: number; vectorWeight: number },
24
- ): number {
25
- return (
26
- (match.vector
27
- ? reciprocalRank(match.vector.rank, weights.vectorWeight)
28
- : 0) +
29
- (match.lexical
30
- ? reciprocalRank(match.lexical.rank, weights.lexicalWeight)
31
- : 0)
32
- );
33
- }
34
-
35
- function observedAgeRank(memory: MemoryRecord, nowMs: number): number {
36
- const ageMs = Math.max(0, nowMs - memory.observedAtMs);
37
- if (ageMs <= 7 * ONE_DAY_MS) {
38
- return 3;
39
- }
40
- if (ageMs <= 30 * ONE_DAY_MS) {
41
- return 2;
42
- }
43
- if (ageMs <= 90 * ONE_DAY_MS) {
44
- return 1;
45
- }
46
- return 0;
47
- }
48
-
49
- function positiveWeight(value: number | undefined, fallback: number): number {
50
- return value !== undefined && Number.isFinite(value) && value > 0
51
- ? value
52
- : fallback;
53
- }
54
-
55
- /** Fuse lexical and vector ranks without comparing provider raw scores. */
56
- export function rankMemoryMatches(
57
- matches: MemoryMatch[],
58
- options: {
59
- /** Optional RRF weight for the lexical leg. Defaults to 1. */
60
- lexicalWeight?: number;
61
- nowMs: number;
62
- /** Optional RRF weight for the vector leg. Defaults to 1. */
63
- vectorWeight?: number;
64
- },
65
- ): MemoryMatch[] {
66
- const weights = {
67
- lexicalWeight: positiveWeight(options.lexicalWeight, DEFAULT_RRF_WEIGHT),
68
- vectorWeight: positiveWeight(options.vectorWeight, DEFAULT_RRF_WEIGHT),
69
- };
70
- const byId = new Map<string, MemoryMatch>();
71
- for (const match of matches) {
72
- const existing = byId.get(match.memory.id);
73
- if (!existing) {
74
- byId.set(match.memory.id, match);
75
- continue;
76
- }
77
- // Keep the first rank from each search. The shared searches run first, so
78
- // the smaller private searches cannot replace their ranks.
79
- byId.set(match.memory.id, {
80
- ...existing,
81
- ...(!existing.lexical && match.lexical
82
- ? { lexical: match.lexical }
83
- : undefined),
84
- ...(!existing.vector && match.vector ? { vector: match.vector } : undefined),
85
- });
86
- }
87
- return [...byId.values()].sort((left, right) => {
88
- const scoreDelta = matchScore(right, weights) - matchScore(left, weights);
89
- if (scoreDelta !== 0) {
90
- return scoreDelta;
91
- }
92
- // Prefer private memory when RRF ties. Public and private searches can give
93
- // the same rank to common words.
94
- const privateDelta =
95
- Number(right.memory.scope === "private") -
96
- Number(left.memory.scope === "private");
97
- if (privateDelta !== 0) {
98
- return privateDelta;
99
- }
100
- return (
101
- observedAgeRank(right.memory, options.nowMs) -
102
- observedAgeRank(left.memory, options.nowMs) ||
103
- right.memory.observedAtMs - left.memory.observedAtMs ||
104
- left.memory.id.localeCompare(right.memory.id)
105
- );
106
- });
107
- }
package/src/recall.ts DELETED
@@ -1,212 +0,0 @@
1
- import {
2
- definePromptContext,
3
- type UserPromptContribution,
4
- type Actor,
5
- type Identity,
6
- type PluginConversationEvents,
7
- type PluginLogger,
8
- type Source,
9
- type User,
10
- } from "@sentry/junior-plugin-api";
11
- import { z } from "zod";
12
- import type { MemoryAgent, MemoryRecallResult } from "./agent";
13
- import { memoriesRecalledEvent } from "./events";
14
- import {
15
- createMemoryStore,
16
- type MemoryDb,
17
- type MemoryEmbeddingProvider,
18
- type MemoryRecord,
19
- } from "./store";
20
- import { memoryRuntimeContextSchema } from "./types";
21
-
22
- const RECALL_CANDIDATE_LIMIT = 20;
23
- const MAX_PROMPT_CHARS = 4_000;
24
- const MAX_MEMORY_LINE_CHARS = 600;
25
-
26
- export interface MemoryRecallContext {
27
- agent: Pick<MemoryAgent, "selectRelevantMemories">;
28
- conversationId?: string;
29
- db: MemoryDb;
30
- embedder?: MemoryEmbeddingProvider;
31
- events?: PluginConversationEvents;
32
- log: PluginLogger;
33
- locationId?: string;
34
- actor?: Actor;
35
- source: Source;
36
- text: string;
37
- users: {
38
- resolveActor(): Promise<{ identity: Identity; user?: User } | undefined>;
39
- };
40
- }
41
-
42
- function trimContent(content: string, maxLength: number): string {
43
- const trimmed = content.trim();
44
- if (trimmed.length <= maxLength) {
45
- return trimmed;
46
- }
47
- return `${trimmed.slice(0, Math.max(0, maxLength - 3)).trimEnd()}...`;
48
- }
49
-
50
- function formatObservedDate(observedAtMs: number): string {
51
- return new Date(observedAtMs).toISOString().slice(0, 10);
52
- }
53
-
54
- const recalledMemorySchema = z
55
- .object({
56
- id: z.string().min(1),
57
- content: z.string().min(1).max(MAX_MEMORY_LINE_CHARS),
58
- observedAtMs: z.number().finite(),
59
- // Stored version 1 uses the old scope labels. Prompt rendering ignores them.
60
- scope: z.enum(["personal", "conversation"]),
61
- kind: z.enum(["preference", "procedure", "knowledge"]),
62
- })
63
- .strict();
64
-
65
- /** Structured snapshot retained for one automatic memory recall. */
66
- export const memoryRecallContextSchema = z
67
- .object({
68
- // Count is a safety rail only. Admission packs by MAX_PROMPT_CHARS.
69
- memories: z.array(recalledMemorySchema).min(1).max(RECALL_CANDIDATE_LIMIT),
70
- })
71
- .strict();
72
-
73
- type RecalledMemory = z.output<typeof recalledMemorySchema>;
74
-
75
- function selectPromptMemories(memories: MemoryRecord[]): RecalledMemory[] {
76
- const header = "Relevant memories for this request:";
77
- const footer =
78
- "Treat these as possibly stale context. Current user instructions and repository evidence take priority.";
79
- const selected: RecalledMemory[] = [];
80
- let totalChars = header.length + footer.length + 2;
81
-
82
- for (const memory of memories) {
83
- const content = trimContent(memory.content, MAX_MEMORY_LINE_CHARS);
84
- const line = `- Observed ${formatObservedDate(memory.observedAtMs)}: ${content}`;
85
- if (totalChars + line.length + 1 > MAX_PROMPT_CHARS) {
86
- break;
87
- }
88
- selected.push({
89
- id: memory.id,
90
- content,
91
- observedAtMs: memory.observedAtMs,
92
- scope: memory.scope === "private" ? "personal" : "conversation",
93
- kind: memory.kind,
94
- });
95
- totalChars += line.length + 1;
96
- }
97
- return selected;
98
- }
99
-
100
- function renderMemoryPrompt(memories: RecalledMemory[]): string {
101
- return [
102
- "Relevant memories for this request:",
103
- ...memories.map(
104
- (memory) =>
105
- `- Observed ${formatObservedDate(memory.observedAtMs)}: ${memory.content}`,
106
- ),
107
- "",
108
- "Treat these as possibly stale context. Current user instructions and repository evidence take priority.",
109
- ].join("\n");
110
- }
111
-
112
- function addUsd(
113
- left: number | undefined,
114
- right: number | undefined,
115
- ): number | undefined {
116
- if (left === undefined) return right;
117
- if (right === undefined) return left;
118
- return Math.round((left + right) * 1e12) / 1e12;
119
- }
120
-
121
- async function emitRecallOutcome(args: {
122
- costUsd?: number;
123
- events?: PluginConversationEvents;
124
- memories: string[];
125
- }): Promise<void> {
126
- await args.events?.emit(
127
- memoriesRecalledEvent({
128
- memories: args.memories,
129
- ...(args.costUsd !== undefined ? { costUsd: args.costUsd } : undefined),
130
- }),
131
- );
132
- }
133
-
134
- const memoryRecallContext = definePromptContext({
135
- kind: "recall",
136
- version: 1,
137
- schema: memoryRecallContextSchema,
138
- renderPrompt: (content) => renderMemoryPrompt(content.memories),
139
- });
140
-
141
- /** Build active memory recall contributions. */
142
- export async function createMemoryPromptContributions(
143
- context: MemoryRecallContext,
144
- ): Promise<UserPromptContribution[] | undefined> {
145
- if (!context.text.trim()) {
146
- return undefined;
147
- }
148
- const actorUser = (await context.users.resolveActor())?.user;
149
- const runtimeContext = memoryRuntimeContextSchema.parse({
150
- ...(context.conversationId
151
- ? { conversationId: context.conversationId }
152
- : undefined),
153
- ...(context.actor ? { actor: context.actor } : undefined),
154
- ...(context.locationId ? { locationId: context.locationId } : undefined),
155
- source: context.source,
156
- ...(actorUser ? { userId: actorUser.id } : undefined),
157
- });
158
- let embeddingCostUsd: number | undefined;
159
- const sourceEmbedder = context.embedder;
160
- const embedder = sourceEmbedder
161
- ? {
162
- async embedTexts(input: { texts: string[] }) {
163
- const result = await sourceEmbedder.embedTexts(input);
164
- embeddingCostUsd = addUsd(embeddingCostUsd, result.costUsd);
165
- return result;
166
- },
167
- }
168
- : undefined;
169
- const candidates = await createMemoryStore(context.db, runtimeContext, {
170
- embedder,
171
- }).recallMemories({
172
- query: context.text,
173
- limit: RECALL_CANDIDATE_LIMIT,
174
- });
175
- if (candidates.length === 0) {
176
- await emitRecallOutcome({
177
- ...(embeddingCostUsd !== undefined ? { costUsd: embeddingCostUsd } : undefined),
178
- events: context.events,
179
- memories: [],
180
- });
181
- return undefined;
182
- }
183
- let recall: MemoryRecallResult;
184
- try {
185
- recall = await context.agent.selectRelevantMemories({
186
- candidates: candidates.map(({ content, id }) => ({ content, id })),
187
- userRequest: context.text,
188
- });
189
- } catch {
190
- // Automatic recall is optional context; a relevance-model failure must not
191
- // prevent the user's turn from continuing without recalled memory.
192
- context.log.warn("memory_recall_selection_failed");
193
- return undefined;
194
- }
195
- const candidatesById = new Map(
196
- candidates.map((memory) => [memory.id, memory]),
197
- );
198
- const relevant = recall.relevantIds
199
- .map((id) => candidatesById.get(id))
200
- .filter((memory): memory is MemoryRecord => memory !== undefined);
201
- const selected = selectPromptMemories(relevant);
202
- const costUsd = addUsd(embeddingCostUsd, recall.costUsd);
203
- await emitRecallOutcome({
204
- ...(costUsd !== undefined ? { costUsd } : undefined),
205
- events: context.events,
206
- memories: selected.map(({ id }) => id),
207
- });
208
- if (selected.length === 0) {
209
- return undefined;
210
- }
211
- return [memoryRecallContext({ memories: selected })];
212
- }
package/src/scope.ts DELETED
@@ -1,75 +0,0 @@
1
- import type {
2
- MemoryRuntimeContext,
3
- MemoryScope,
4
- MemorySubjectType,
5
- } from "./types";
6
-
7
- const PUBLIC_SCOPE_KEY = "public";
8
-
9
- /** Stored memory access rule. */
10
- export interface ResolvedMemoryScope {
11
- scope: MemoryScope;
12
- scopeKey: string;
13
- }
14
-
15
- /** What a stored memory is about. */
16
- export interface ResolvedMemorySubject {
17
- subjectKey: string;
18
- subjectType: Extract<MemorySubjectType, "user" | "conversation">;
19
- }
20
-
21
- /** Public memories are visible everywhere. */
22
- export const publicMemoryScope: ResolvedMemoryScope = {
23
- scope: "public",
24
- scopeKey: PUBLIC_SCOPE_KEY,
25
- };
26
-
27
- function privateMemoryScope(userId: string): ResolvedMemoryScope {
28
- return { scope: "private", scopeKey: userId };
29
- }
30
-
31
- /** Set memory access from the Source. */
32
- export function deriveMemoryScope(
33
- ctx: MemoryRuntimeContext,
34
- ): ResolvedMemoryScope {
35
- if ("visibility" in ctx.source && ctx.source.visibility === "public") {
36
- return publicMemoryScope;
37
- }
38
- if (!ctx.userId) {
39
- throw new Error("Private memory requires a User.");
40
- }
41
- return privateMemoryScope(ctx.userId);
42
- }
43
-
44
- /** Set what a memory is about. Access is set separately. */
45
- export function deriveMemorySubject(
46
- ctx: MemoryRuntimeContext,
47
- subjectType: Extract<MemorySubjectType, "user" | "conversation">,
48
- ): ResolvedMemorySubject {
49
- if (subjectType === "user") {
50
- if (!ctx.userId) {
51
- throw new Error("User memory requires a User.");
52
- }
53
- return { subjectType, subjectKey: ctx.userId };
54
- }
55
- const subjectKey = ctx.conversationId;
56
- if (!subjectKey) {
57
- throw new Error(
58
- "Conversation-subject memory requires conversation context.",
59
- );
60
- }
61
- return {
62
- subjectType,
63
- subjectKey,
64
- };
65
- }
66
-
67
- /** Return the memory scopes that the current User can access. */
68
- export function deriveVisibleMemoryScopes(
69
- ctx: MemoryRuntimeContext,
70
- ): ResolvedMemoryScope[] {
71
- if (!ctx.userId) {
72
- return [publicMemoryScope];
73
- }
74
- return [publicMemoryScope, privateMemoryScope(ctx.userId)];
75
- }