@sentry/junior-memory 0.76.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/LICENSE +201 -0
- package/dist/agent.d.ts +144 -0
- package/dist/cli/format.d.ts +5 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/search.d.ts +4 -0
- package/dist/cli/show.d.ts +4 -0
- package/dist/db/schema.d.ts +441 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +1773 -0
- package/dist/index.js.map +1 -0
- package/dist/plugin.d.ts +6 -0
- package/dist/process-session.d.ts +10 -0
- package/dist/recall.d.ts +12 -0
- package/dist/scope.d.ts +17 -0
- package/dist/store.d.ts +93 -0
- package/dist/tools.d.ts +103 -0
- package/dist/types.d.ts +89 -0
- package/migrations/0000_dizzy_millenium_guard.sql +37 -0
- package/migrations/0001_closed_madrox.sql +2 -0
- package/migrations/0002_light_silver_centurion.sql +17 -0
- package/migrations/meta/0000_snapshot.json +234 -0
- package/migrations/meta/0001_snapshot.json +234 -0
- package/migrations/meta/0002_snapshot.json +348 -0
- package/migrations/meta/_journal.json +27 -0
- package/package.json +48 -0
- package/src/agent.ts +437 -0
- package/src/cli/format.ts +30 -0
- package/src/cli/index.ts +15 -0
- package/src/cli/search.ts +119 -0
- package/src/cli/show.ts +44 -0
- package/src/db/schema.ts +130 -0
- package/src/index.ts +16 -0
- package/src/plugin.ts +103 -0
- package/src/process-session.ts +151 -0
- package/src/recall.ts +81 -0
- package/src/scope.ts +99 -0
- package/src/store.ts +761 -0
- package/src/tools.ts +487 -0
- package/src/types.ts +66 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1773 @@
|
|
|
1
|
+
// src/plugin.ts
|
|
2
|
+
import { defineJuniorPlugin } from "@sentry/junior-plugin-api";
|
|
3
|
+
|
|
4
|
+
// src/agent.ts
|
|
5
|
+
import { z as z2 } from "zod";
|
|
6
|
+
|
|
7
|
+
// src/types.ts
|
|
8
|
+
import {
|
|
9
|
+
localRequesterSchema,
|
|
10
|
+
localSourceSchema,
|
|
11
|
+
slackRequesterSchema,
|
|
12
|
+
slackSourceSchema
|
|
13
|
+
} from "@sentry/junior-plugin-api";
|
|
14
|
+
import { z } from "zod";
|
|
15
|
+
var MEMORY_TYPES = [
|
|
16
|
+
"preference",
|
|
17
|
+
"identity",
|
|
18
|
+
"relationship",
|
|
19
|
+
"knowledge",
|
|
20
|
+
"context",
|
|
21
|
+
"event",
|
|
22
|
+
"task",
|
|
23
|
+
"observation"
|
|
24
|
+
];
|
|
25
|
+
var MEMORY_SCOPES = ["personal", "conversation"];
|
|
26
|
+
var MEMORY_SUBJECT_TYPES = [
|
|
27
|
+
"user",
|
|
28
|
+
"conversation",
|
|
29
|
+
"general"
|
|
30
|
+
];
|
|
31
|
+
var MEMORY_SOURCE_PLATFORMS = [
|
|
32
|
+
"slack",
|
|
33
|
+
"local"
|
|
34
|
+
];
|
|
35
|
+
var MEMORY_EMBEDDING_METRICS = ["cosine"];
|
|
36
|
+
var MEMORY_EMBEDDING_DIMENSIONS = 1536;
|
|
37
|
+
var nonEmptyStringSchema = z.string().min(1);
|
|
38
|
+
var slackMemoryRuntimeContextSchema = z.object({
|
|
39
|
+
conversationId: nonEmptyStringSchema.optional(),
|
|
40
|
+
requester: slackRequesterSchema.optional(),
|
|
41
|
+
source: slackSourceSchema
|
|
42
|
+
}).strict();
|
|
43
|
+
var localMemoryRuntimeContextSchema = z.object({
|
|
44
|
+
conversationId: nonEmptyStringSchema.optional(),
|
|
45
|
+
requester: localRequesterSchema.optional(),
|
|
46
|
+
source: localSourceSchema
|
|
47
|
+
}).strict();
|
|
48
|
+
var memoryRuntimeContextSchema = z.union([
|
|
49
|
+
slackMemoryRuntimeContextSchema,
|
|
50
|
+
localMemoryRuntimeContextSchema
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
// src/agent.ts
|
|
54
|
+
var memoryTargetSchema = z2.enum(["requester", "conversation"]);
|
|
55
|
+
var memoryKindSchema = z2.enum(["preference", "procedure", "fact"]);
|
|
56
|
+
var memoryRejectReasonSchema = z2.enum([
|
|
57
|
+
"not_public_shareable",
|
|
58
|
+
"secret_or_credential",
|
|
59
|
+
"sensitive_personal",
|
|
60
|
+
"third_party_personal",
|
|
61
|
+
"vague_or_not_self_contained",
|
|
62
|
+
"not_durable",
|
|
63
|
+
"assistant_or_system_detail",
|
|
64
|
+
"unsupported_scope"
|
|
65
|
+
]);
|
|
66
|
+
var createMemoryRequestSchema = z2.object({
|
|
67
|
+
content: z2.string().min(1),
|
|
68
|
+
expiresAtMs: z2.number().finite().optional(),
|
|
69
|
+
runtimeContext: memoryRuntimeContextSchema,
|
|
70
|
+
sourceContext: z2.object({
|
|
71
|
+
currentUserText: z2.string().min(1).optional()
|
|
72
|
+
}).strict().optional()
|
|
73
|
+
}).strict();
|
|
74
|
+
var extractSessionRequestSchema = z2.object({
|
|
75
|
+
existingMemories: z2.array(
|
|
76
|
+
z2.object({
|
|
77
|
+
content: z2.string().min(1)
|
|
78
|
+
}).strict()
|
|
79
|
+
).max(10).default([]),
|
|
80
|
+
runtimeContext: memoryRuntimeContextSchema,
|
|
81
|
+
transcript: z2.array(
|
|
82
|
+
z2.discriminatedUnion("type", [
|
|
83
|
+
z2.object({
|
|
84
|
+
type: z2.literal("message"),
|
|
85
|
+
role: z2.enum(["user", "assistant"]),
|
|
86
|
+
text: z2.string().min(1)
|
|
87
|
+
}).strict(),
|
|
88
|
+
z2.object({
|
|
89
|
+
type: z2.literal("toolResult"),
|
|
90
|
+
toolName: z2.string().min(1),
|
|
91
|
+
isError: z2.boolean(),
|
|
92
|
+
text: z2.string().min(1)
|
|
93
|
+
}).strict()
|
|
94
|
+
])
|
|
95
|
+
).min(1)
|
|
96
|
+
}).strict();
|
|
97
|
+
var expiresAtMsSchema = z2.number().finite().nullable().describe(
|
|
98
|
+
"Expiration timestamp when the fact should expire, otherwise null."
|
|
99
|
+
);
|
|
100
|
+
var memoryReviewDecisionSchema = z2.discriminatedUnion("decision", [
|
|
101
|
+
z2.object({
|
|
102
|
+
decision: z2.literal("store"),
|
|
103
|
+
target: memoryTargetSchema,
|
|
104
|
+
content: z2.string().min(1),
|
|
105
|
+
expiresAtMs: z2.number().finite().optional()
|
|
106
|
+
}).strict(),
|
|
107
|
+
z2.object({
|
|
108
|
+
decision: z2.literal("reject"),
|
|
109
|
+
reason: memoryRejectReasonSchema
|
|
110
|
+
}).strict()
|
|
111
|
+
]);
|
|
112
|
+
var memoryReviewResponseSchema = z2.discriminatedUnion("decision", [
|
|
113
|
+
z2.object({
|
|
114
|
+
decision: z2.literal("store"),
|
|
115
|
+
kind: memoryKindSchema.describe(
|
|
116
|
+
"Use preference only for requester-owned personal preferences, opinions, habits, or workflows. Use procedure for reusable task or process instructions. Use fact for shared project, channel, operational, or runbook knowledge."
|
|
117
|
+
),
|
|
118
|
+
canonicalFact: z2.string().min(1).describe(
|
|
119
|
+
"Stored memory text. It must be self-contained and must not include requester names, requester/user labels, source labels, or first- or second-person wording."
|
|
120
|
+
),
|
|
121
|
+
expiresAtMs: expiresAtMsSchema
|
|
122
|
+
}).strict(),
|
|
123
|
+
z2.object({
|
|
124
|
+
decision: z2.literal("reject"),
|
|
125
|
+
reason: memoryRejectReasonSchema
|
|
126
|
+
}).strict()
|
|
127
|
+
]);
|
|
128
|
+
var extractedMemorySchema = z2.object({
|
|
129
|
+
kind: memoryKindSchema.describe(
|
|
130
|
+
"Use preference only for requester-owned personal preferences, opinions, habits, or workflows. Use procedure for reusable task or process instructions. Use fact for shared project, channel, operational, or runbook knowledge."
|
|
131
|
+
),
|
|
132
|
+
canonicalFact: z2.string().min(1).describe(
|
|
133
|
+
"Stored memory text as one self-contained fact. It must not include requester names, requester/user labels, source labels, or first- or second-person wording."
|
|
134
|
+
),
|
|
135
|
+
expiresAtMs: expiresAtMsSchema
|
|
136
|
+
}).strict();
|
|
137
|
+
var extractMemoriesResponseSchema = z2.object({
|
|
138
|
+
memories: z2.array(extractedMemorySchema).max(5).describe(
|
|
139
|
+
"Accepted public/shareable durable memories from the completed run. Return one object per distinct source assertion and classify it with kind."
|
|
140
|
+
)
|
|
141
|
+
}).strict();
|
|
142
|
+
var MEMORY_REVIEW_SYSTEM = [
|
|
143
|
+
"You are Junior's memory review agent.",
|
|
144
|
+
"Review one memory candidate and return one structured review decision.",
|
|
145
|
+
"Store only public/shareable, self-contained facts that are useful beyond this turn.",
|
|
146
|
+
"Reject secrets, credentials, private or sensitive personal details, gossip, speculative claims about other people, assistant/system implementation details, vague references, and low-durability chatter.",
|
|
147
|
+
"Use the runtime context only for authority and scope; do not accept model-provided actor ids, scope ids, aliases, or arbitrary subjects."
|
|
148
|
+
].join("\n");
|
|
149
|
+
var MEMORY_EXTRACTION_SYSTEM = [
|
|
150
|
+
"You are Junior's passive memory extraction agent. Return only structured memories worth storing.",
|
|
151
|
+
"Use the completed run transcript as source evidence, including user-authored messages and tool results.",
|
|
152
|
+
"Assistant text is context for interpreting the run, not independent evidence for new facts.",
|
|
153
|
+
"Reject secrets, credentials, private or sensitive personal details, gossip, speculative claims about other people, assistant/system implementation details, vague references, and low-durability chatter.",
|
|
154
|
+
"If no public, durable, self-contained memory remains after rewriting, return an empty memories array."
|
|
155
|
+
].join("\n");
|
|
156
|
+
var CANONICAL_CONTENT_RULES = [
|
|
157
|
+
"- Stored memory text must be a rewritten fact, not copied user wording or a sentence about who said it.",
|
|
158
|
+
"- Store the minimum useful assertion supported by source evidence; do not add adjacent steps, caveats, or generalized advice.",
|
|
159
|
+
"- Do not return both concise and expanded variants of the same source assertion; keep the shortest self-contained canonical memory.",
|
|
160
|
+
"- Put ownership in structured fields, not prose.",
|
|
161
|
+
"- For requester memories, omit the subject and write a stable fact such as 'Prefers X', 'Uses Y', or 'Thinks Z'.",
|
|
162
|
+
"- Drop perspective/provenance markers while preserving useful context.",
|
|
163
|
+
"- Remove requester names, display names, requester/user labels, first- or second-person wording, thread labels, channel labels, and source labels."
|
|
164
|
+
];
|
|
165
|
+
function targetForKind(kind) {
|
|
166
|
+
if (kind === "preference") {
|
|
167
|
+
return "requester";
|
|
168
|
+
}
|
|
169
|
+
return "conversation";
|
|
170
|
+
}
|
|
171
|
+
function escapeXml(value) {
|
|
172
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
173
|
+
}
|
|
174
|
+
function runtimeDescription(request) {
|
|
175
|
+
const runtime = request.runtimeContext;
|
|
176
|
+
const requester = runtime.requester?.platform === "slack" ? `slack:${runtime.requester.teamId}:${runtime.requester.userId}` : runtime.requester?.platform === "local" ? `local:${runtime.requester.userId}` : "none";
|
|
177
|
+
const source = runtime.source.platform === "slack" ? `slack:${runtime.source.teamId}:${runtime.source.channelId}` : `local:${runtime.source.conversationId}`;
|
|
178
|
+
const lines = [
|
|
179
|
+
`- requester: ${escapeXml(requester)}`,
|
|
180
|
+
`- source: ${escapeXml(source)}`,
|
|
181
|
+
`- has_conversation: ${runtime.conversationId ? "true" : "false"}`,
|
|
182
|
+
`- expires_at: ${request.expiresAtMs === void 0 ? "never" : escapeXml(new Date(request.expiresAtMs).toISOString())}`
|
|
183
|
+
];
|
|
184
|
+
return ["<runtime>", ...lines, "</runtime>"].join("\n");
|
|
185
|
+
}
|
|
186
|
+
function sourceContext(request) {
|
|
187
|
+
const currentUserText = request.sourceContext?.currentUserText?.trim();
|
|
188
|
+
if (!currentUserText) {
|
|
189
|
+
return void 0;
|
|
190
|
+
}
|
|
191
|
+
return [
|
|
192
|
+
"<source-context>",
|
|
193
|
+
"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.",
|
|
194
|
+
"<current-user-message>",
|
|
195
|
+
escapeXml(currentUserText),
|
|
196
|
+
"</current-user-message>",
|
|
197
|
+
"</source-context>"
|
|
198
|
+
].join("\n");
|
|
199
|
+
}
|
|
200
|
+
function existingMemoriesContext(request) {
|
|
201
|
+
if (request.existingMemories.length === 0) {
|
|
202
|
+
return "<existing-memories>[]</existing-memories>";
|
|
203
|
+
}
|
|
204
|
+
return [
|
|
205
|
+
"<existing-memories>",
|
|
206
|
+
"Use these only to skip memories that are already covered or semantically redundant. They are not source evidence for new memories.",
|
|
207
|
+
escapeXml(JSON.stringify(request.existingMemories)),
|
|
208
|
+
"</existing-memories>"
|
|
209
|
+
].join("\n");
|
|
210
|
+
}
|
|
211
|
+
function memoryKindsContext() {
|
|
212
|
+
return [
|
|
213
|
+
"<memory-kinds>",
|
|
214
|
+
"- preference: a durable first-person personal preference, opinion, habit, or workflow owned by the current requester. Stored as requester memory.",
|
|
215
|
+
"- 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.",
|
|
216
|
+
"- fact: stable shared project, channel, operational, or runbook knowledge that is not a personal requester preference. Direct answers to user inquiries qualify only when they are durable beyond this run. Stored as conversation memory.",
|
|
217
|
+
"</memory-kinds>"
|
|
218
|
+
].join("\n");
|
|
219
|
+
}
|
|
220
|
+
function reviewPrompt(request) {
|
|
221
|
+
const sections = [
|
|
222
|
+
"<memory-review-input>",
|
|
223
|
+
"Review the candidate memory using the runtime-owned context below.",
|
|
224
|
+
"",
|
|
225
|
+
runtimeDescription(request),
|
|
226
|
+
"",
|
|
227
|
+
sourceContext(request),
|
|
228
|
+
"",
|
|
229
|
+
"<candidate>",
|
|
230
|
+
escapeXml(request.content),
|
|
231
|
+
"</candidate>",
|
|
232
|
+
"",
|
|
233
|
+
"<rules>",
|
|
234
|
+
"- Return store only when the candidate is public/shareable, durable, and self-contained.",
|
|
235
|
+
"- First classify the memory kind: preference, procedure, or fact.",
|
|
236
|
+
"- Use kind=preference only for first-person facts authored by the current requester about their own preference, opinion, habit, identity, or workflow.",
|
|
237
|
+
"- 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 requester.",
|
|
238
|
+
"- Use kind=procedure for reusable task/process/runbook instructions.",
|
|
239
|
+
"- Use kind=fact for shared project, channel, operational, or runbook knowledge.",
|
|
240
|
+
"- 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.",
|
|
241
|
+
"- A candidate may be badly phrased by an outer assistant or extraction pass. When current-user-message contains the requester's own first-person memory fact, treat that as requester-authored source evidence and canonicalize the fact instead of rejecting for third-person wording.",
|
|
242
|
+
"- 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 fact.",
|
|
243
|
+
"- 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.",
|
|
244
|
+
"- Store content as person-less, source-less canonical knowledge. Ownership and source live in structured metadata, not prose.",
|
|
245
|
+
"- For requester memories, omit the subject and write the content as a stable fact such as 'Prefers X', 'Uses Y', or 'Thinks Z'.",
|
|
246
|
+
"- Remove requester names, display names, requester/user labels, first- or second-person wording, thread labels, channel labels, and source labels from stored content.",
|
|
247
|
+
"- Reject third-party personal profile facts, even if they mention a name.",
|
|
248
|
+
"- Reject vague content such as 'remember this' unless the candidate or current-user-message contains the concrete fact.",
|
|
249
|
+
"- Preserve the requested expiration when one exists; otherwise set expiresAtMs to null.",
|
|
250
|
+
"- If unsure, reject.",
|
|
251
|
+
"</rules>",
|
|
252
|
+
"</memory-review-input>"
|
|
253
|
+
].filter((section) => section !== void 0);
|
|
254
|
+
return sections.join("\n");
|
|
255
|
+
}
|
|
256
|
+
function runTranscriptContext(request) {
|
|
257
|
+
return [
|
|
258
|
+
"<run-transcript>",
|
|
259
|
+
...request.transcript.map((entry, index2) => {
|
|
260
|
+
if (entry.type === "toolResult") {
|
|
261
|
+
return [
|
|
262
|
+
`<tool-result index="${index2}" tool="${escapeXml(entry.toolName)}" is_error="${entry.isError ? "true" : "false"}">`,
|
|
263
|
+
escapeXml(entry.text),
|
|
264
|
+
"</tool-result>"
|
|
265
|
+
].join("\n");
|
|
266
|
+
}
|
|
267
|
+
return [
|
|
268
|
+
`<message index="${index2}" role="${entry.role}">`,
|
|
269
|
+
escapeXml(entry.text),
|
|
270
|
+
"</message>"
|
|
271
|
+
].join("\n");
|
|
272
|
+
}),
|
|
273
|
+
"</run-transcript>"
|
|
274
|
+
].join("\n");
|
|
275
|
+
}
|
|
276
|
+
function sessionExtractionPrompt(request) {
|
|
277
|
+
return [
|
|
278
|
+
"<memory-extraction-input>",
|
|
279
|
+
"Extract durable memories from this completed agent run using the runtime-owned context below.",
|
|
280
|
+
"",
|
|
281
|
+
runtimeDescription({
|
|
282
|
+
runtimeContext: request.runtimeContext
|
|
283
|
+
}),
|
|
284
|
+
"",
|
|
285
|
+
existingMemoriesContext(request),
|
|
286
|
+
"",
|
|
287
|
+
memoryKindsContext(),
|
|
288
|
+
"",
|
|
289
|
+
runTranscriptContext(request),
|
|
290
|
+
"",
|
|
291
|
+
"<rules>",
|
|
292
|
+
"- Return at most five memories.",
|
|
293
|
+
"- Use user messages and successful tool results as source evidence for storable facts.",
|
|
294
|
+
"- Use failed tool results only when the failure reveals durable process knowledge, not transient errors.",
|
|
295
|
+
"- Use assistant messages only as context; do not store the assistant's claims unless supported by user messages or tool results.",
|
|
296
|
+
"- Return one memory per distinct fact.",
|
|
297
|
+
"- 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.",
|
|
298
|
+
"- Store direct answers to user inquiries only when they are stable operational/project knowledge, not values that naturally change over time.",
|
|
299
|
+
"- Do not store point-in-time analytics, search, issue, metric, incident, availability, or status answers just because a tool produced them.",
|
|
300
|
+
"- 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.",
|
|
301
|
+
"- 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.",
|
|
302
|
+
"- Set kind=procedure for reusable task/process/runbook instructions.",
|
|
303
|
+
"- Set kind=fact for shared team, project, channel, runbook, or operational knowledge.",
|
|
304
|
+
"- Set kind=preference only for clear durable first-person facts authored by the current requester about their own preference, opinion, habit, identity, or workflow.",
|
|
305
|
+
"- 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 requester.",
|
|
306
|
+
"- User-authored task instructions are procedures, not preferences, unless they explicitly describe the requester's personal preference or habit.",
|
|
307
|
+
"- Procedural statements such as 'for X, do Y', 'when X, do Y', and 'to accomplish X, do Y' belong in procedures.",
|
|
308
|
+
...CANONICAL_CONTENT_RULES,
|
|
309
|
+
"- Skip a candidate when existing-memories already cover the same durable fact.",
|
|
310
|
+
"- Reject third-party personal profile facts, even if they mention a name.",
|
|
311
|
+
"- If unsure, return no memory for that candidate.",
|
|
312
|
+
"</rules>",
|
|
313
|
+
"</memory-extraction-input>"
|
|
314
|
+
].join("\n");
|
|
315
|
+
}
|
|
316
|
+
function createMemoryAgent(model) {
|
|
317
|
+
return {
|
|
318
|
+
async extractSessionMemories(rawRequest) {
|
|
319
|
+
const request = extractSessionRequestSchema.parse(rawRequest);
|
|
320
|
+
const result = await model.completeObject({
|
|
321
|
+
schema: extractMemoriesResponseSchema,
|
|
322
|
+
system: MEMORY_EXTRACTION_SYSTEM,
|
|
323
|
+
prompt: sessionExtractionPrompt(request),
|
|
324
|
+
maxTokens: 1e3
|
|
325
|
+
});
|
|
326
|
+
return extractedMemoriesFromResponse(
|
|
327
|
+
extractMemoriesResponseSchema.parse(result.object)
|
|
328
|
+
);
|
|
329
|
+
},
|
|
330
|
+
async reviewCreateRequest(rawRequest) {
|
|
331
|
+
const request = parseCreateMemoryRequest(rawRequest);
|
|
332
|
+
const result = await model.completeObject({
|
|
333
|
+
schema: memoryReviewResponseSchema,
|
|
334
|
+
system: MEMORY_REVIEW_SYSTEM,
|
|
335
|
+
prompt: reviewPrompt(request),
|
|
336
|
+
maxTokens: 700
|
|
337
|
+
});
|
|
338
|
+
const response = memoryReviewResponseSchema.parse(result.object);
|
|
339
|
+
return memoryReviewFromResponse(response);
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
function memoryReviewFromResponse(response) {
|
|
344
|
+
if (response.decision === "store") {
|
|
345
|
+
return parseMemoryReview({
|
|
346
|
+
decision: "store",
|
|
347
|
+
target: targetForKind(response.kind),
|
|
348
|
+
content: response.canonicalFact,
|
|
349
|
+
...response.expiresAtMs !== null ? { expiresAtMs: response.expiresAtMs } : {}
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
return parseMemoryReview({
|
|
353
|
+
decision: "reject",
|
|
354
|
+
reason: response.reason
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
function extractedMemoriesFromResponse(response) {
|
|
358
|
+
const toMemory = (memory) => ({
|
|
359
|
+
content: memory.canonicalFact,
|
|
360
|
+
expiresAtMs: memory.expiresAtMs,
|
|
361
|
+
target: targetForKind(memory.kind)
|
|
362
|
+
});
|
|
363
|
+
return response.memories.map(toMemory);
|
|
364
|
+
}
|
|
365
|
+
function parseMemoryReview(result) {
|
|
366
|
+
return memoryReviewDecisionSchema.parse(result);
|
|
367
|
+
}
|
|
368
|
+
function parseCreateMemoryRequest(request) {
|
|
369
|
+
return createMemoryRequestSchema.parse(request);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// src/cli/search.ts
|
|
373
|
+
import { InvalidArgumentError, Option } from "commander";
|
|
374
|
+
import { and, desc, eq, gt, ilike, isNull, or } from "drizzle-orm";
|
|
375
|
+
|
|
376
|
+
// src/db/schema.ts
|
|
377
|
+
import { sql } from "drizzle-orm";
|
|
378
|
+
import {
|
|
379
|
+
bigint,
|
|
380
|
+
check,
|
|
381
|
+
index,
|
|
382
|
+
integer,
|
|
383
|
+
pgTable,
|
|
384
|
+
text,
|
|
385
|
+
uniqueIndex,
|
|
386
|
+
vector
|
|
387
|
+
} from "drizzle-orm/pg-core";
|
|
388
|
+
var juniorMemoryMemories = pgTable(
|
|
389
|
+
"junior_memory_memories",
|
|
390
|
+
{
|
|
391
|
+
id: text("id").primaryKey(),
|
|
392
|
+
scope: text("scope", { enum: MEMORY_SCOPES }).notNull(),
|
|
393
|
+
scopeKey: text("scope_key").notNull(),
|
|
394
|
+
type: text("type", { enum: MEMORY_TYPES }).notNull(),
|
|
395
|
+
subjectType: text("subject_type", { enum: MEMORY_SUBJECT_TYPES }).notNull(),
|
|
396
|
+
subjectKey: text("subject_key"),
|
|
397
|
+
content: text("content").notNull(),
|
|
398
|
+
sourcePlatform: text("source_platform", {
|
|
399
|
+
enum: MEMORY_SOURCE_PLATFORMS
|
|
400
|
+
}).notNull(),
|
|
401
|
+
sourceKey: text("source_key").notNull(),
|
|
402
|
+
idempotencyKey: text("idempotency_key"),
|
|
403
|
+
observedAtMs: bigint("observed_at_ms", { mode: "number" }).notNull(),
|
|
404
|
+
createdAtMs: bigint("created_at_ms", { mode: "number" }).notNull(),
|
|
405
|
+
expiresAtMs: bigint("expires_at_ms", { mode: "number" }),
|
|
406
|
+
supersededAtMs: bigint("superseded_at_ms", { mode: "number" }),
|
|
407
|
+
supersededById: text("superseded_by_id"),
|
|
408
|
+
archivedAtMs: bigint("archived_at_ms", { mode: "number" }),
|
|
409
|
+
archiveReason: text("archive_reason")
|
|
410
|
+
},
|
|
411
|
+
(table) => [
|
|
412
|
+
index("junior_memory_memories_visible_idx").on(table.scope, table.scopeKey, table.createdAtMs.desc(), table.id).where(
|
|
413
|
+
sql`${table.archivedAtMs} IS NULL AND ${table.supersededAtMs} IS NULL AND ${table.supersededById} IS NULL`
|
|
414
|
+
),
|
|
415
|
+
index("junior_memory_memories_expiration_idx").on(table.expiresAtMs).where(
|
|
416
|
+
sql`${table.archivedAtMs} IS NULL AND ${table.expiresAtMs} IS NOT NULL`
|
|
417
|
+
),
|
|
418
|
+
uniqueIndex("junior_memory_memories_idempotency_idx").on(table.scope, table.scopeKey, table.idempotencyKey).where(
|
|
419
|
+
sql`${table.idempotencyKey} IS NOT NULL AND ${table.archivedAtMs} IS NULL AND ${table.supersededAtMs} IS NULL AND ${table.supersededById} IS NULL`
|
|
420
|
+
),
|
|
421
|
+
check(
|
|
422
|
+
"junior_memory_memories_scope_check",
|
|
423
|
+
sql`${table.scope} IN ('personal', 'conversation')`
|
|
424
|
+
),
|
|
425
|
+
check(
|
|
426
|
+
"junior_memory_memories_type_check",
|
|
427
|
+
sql`${table.type} IN (
|
|
428
|
+
'preference',
|
|
429
|
+
'identity',
|
|
430
|
+
'relationship',
|
|
431
|
+
'knowledge',
|
|
432
|
+
'context',
|
|
433
|
+
'event',
|
|
434
|
+
'task',
|
|
435
|
+
'observation'
|
|
436
|
+
)`
|
|
437
|
+
),
|
|
438
|
+
check(
|
|
439
|
+
"junior_memory_memories_subject_type_check",
|
|
440
|
+
sql`${table.subjectType} IN ('user', 'conversation', 'general')`
|
|
441
|
+
),
|
|
442
|
+
check(
|
|
443
|
+
"junior_memory_memories_subject_key_check",
|
|
444
|
+
sql`(${table.subjectType} = 'general' AND ${table.subjectKey} IS NULL) OR (${table.subjectType} IN ('user', 'conversation') AND ${table.subjectKey} IS NOT NULL AND length(${table.subjectKey}) > 0)`
|
|
445
|
+
),
|
|
446
|
+
check(
|
|
447
|
+
"junior_memory_memories_source_platform_check",
|
|
448
|
+
sql`${table.sourcePlatform} IN ('slack', 'local')`
|
|
449
|
+
)
|
|
450
|
+
]
|
|
451
|
+
);
|
|
452
|
+
var juniorMemoryEmbeddings = pgTable(
|
|
453
|
+
"junior_memory_embeddings",
|
|
454
|
+
{
|
|
455
|
+
memoryId: text("memory_id").primaryKey().references(() => juniorMemoryMemories.id, { onDelete: "cascade" }),
|
|
456
|
+
provider: text("provider").notNull(),
|
|
457
|
+
model: text("model").notNull(),
|
|
458
|
+
dimensions: integer("dimensions").notNull(),
|
|
459
|
+
metric: text("metric", { enum: MEMORY_EMBEDDING_METRICS }).notNull(),
|
|
460
|
+
contentHash: text("content_hash").notNull(),
|
|
461
|
+
embedding: vector("embedding", {
|
|
462
|
+
dimensions: MEMORY_EMBEDDING_DIMENSIONS
|
|
463
|
+
}).notNull(),
|
|
464
|
+
createdAtMs: bigint("created_at_ms", { mode: "number" }).notNull()
|
|
465
|
+
},
|
|
466
|
+
(table) => [
|
|
467
|
+
index("junior_memory_embeddings_model_idx").on(
|
|
468
|
+
table.provider,
|
|
469
|
+
table.model,
|
|
470
|
+
table.dimensions,
|
|
471
|
+
table.metric
|
|
472
|
+
),
|
|
473
|
+
check(
|
|
474
|
+
"junior_memory_embeddings_metric_check",
|
|
475
|
+
sql`${table.metric} IN ('cosine')`
|
|
476
|
+
),
|
|
477
|
+
check(
|
|
478
|
+
"junior_memory_embeddings_dimensions_check",
|
|
479
|
+
sql`${table.dimensions} = ${sql.raw(String(MEMORY_EMBEDDING_DIMENSIONS))}`
|
|
480
|
+
)
|
|
481
|
+
]
|
|
482
|
+
);
|
|
483
|
+
|
|
484
|
+
// src/cli/format.ts
|
|
485
|
+
function formatDate(ms) {
|
|
486
|
+
return ms === null ? "-" : new Date(ms).toISOString();
|
|
487
|
+
}
|
|
488
|
+
function formatMemory(row, args) {
|
|
489
|
+
const lines = [
|
|
490
|
+
`id=${row.id}`,
|
|
491
|
+
`scope=${row.scope}`,
|
|
492
|
+
`scope_key=${row.scopeKey}`,
|
|
493
|
+
`subject_type=${row.subjectType}`,
|
|
494
|
+
...row.subjectKey ? [`subject_key=${row.subjectKey}`] : [],
|
|
495
|
+
`type=${row.type}`,
|
|
496
|
+
`created_at=${formatDate(row.createdAtMs)}`,
|
|
497
|
+
`observed_at=${formatDate(row.observedAtMs)}`,
|
|
498
|
+
`expires_at=${formatDate(row.expiresAtMs)}`,
|
|
499
|
+
`archived_at=${formatDate(row.archivedAtMs)}`
|
|
500
|
+
];
|
|
501
|
+
if (args.showContent) {
|
|
502
|
+
lines.push(`content=${row.content}`);
|
|
503
|
+
}
|
|
504
|
+
return lines.join("\n");
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// src/cli/search.ts
|
|
508
|
+
function parseLimit(value) {
|
|
509
|
+
const parsed = Number(value);
|
|
510
|
+
if (!Number.isFinite(parsed)) {
|
|
511
|
+
throw new InvalidArgumentError("--limit must be a number");
|
|
512
|
+
}
|
|
513
|
+
return Math.min(100, Math.max(1, Math.floor(parsed)));
|
|
514
|
+
}
|
|
515
|
+
async function runSearch(ctx, queryParts, options) {
|
|
516
|
+
const query = (queryParts ?? []).join(" ").trim();
|
|
517
|
+
const nowMs = Date.now();
|
|
518
|
+
const terms = [
|
|
519
|
+
...new Set(
|
|
520
|
+
query.toLowerCase().split(/[^a-z0-9_'-]+/).map((term) => term.trim()).filter((term) => term.length >= 2)
|
|
521
|
+
)
|
|
522
|
+
];
|
|
523
|
+
const db = ctx.db;
|
|
524
|
+
const activeExpirationPredicate = or(
|
|
525
|
+
isNull(juniorMemoryMemories.expiresAtMs),
|
|
526
|
+
gt(juniorMemoryMemories.expiresAtMs, nowMs)
|
|
527
|
+
);
|
|
528
|
+
const predicates = [
|
|
529
|
+
eq(juniorMemoryMemories.scope, options.scope),
|
|
530
|
+
eq(juniorMemoryMemories.scopeKey, options.scopeKey),
|
|
531
|
+
isNull(juniorMemoryMemories.archivedAtMs),
|
|
532
|
+
isNull(juniorMemoryMemories.supersededAtMs),
|
|
533
|
+
isNull(juniorMemoryMemories.supersededById)
|
|
534
|
+
];
|
|
535
|
+
if (activeExpirationPredicate) {
|
|
536
|
+
predicates.push(activeExpirationPredicate);
|
|
537
|
+
}
|
|
538
|
+
if (terms.length > 0) {
|
|
539
|
+
const termPredicate = or(
|
|
540
|
+
...terms.map((term) => ilike(juniorMemoryMemories.content, `%${term}%`))
|
|
541
|
+
);
|
|
542
|
+
if (termPredicate) {
|
|
543
|
+
predicates.push(termPredicate);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
const rows = await db.select().from(juniorMemoryMemories).where(and(...predicates)).orderBy(desc(juniorMemoryMemories.createdAtMs)).limit(options.limit);
|
|
547
|
+
if (rows.length === 0) {
|
|
548
|
+
await ctx.io.writeOutput("No memories matched.\n");
|
|
549
|
+
return 0;
|
|
550
|
+
}
|
|
551
|
+
await ctx.io.writeOutput(
|
|
552
|
+
`${rows.map(
|
|
553
|
+
(row) => formatMemory(row, { showContent: Boolean(options.showContent) })
|
|
554
|
+
).join("\n\n")}
|
|
555
|
+
`
|
|
556
|
+
);
|
|
557
|
+
return 0;
|
|
558
|
+
}
|
|
559
|
+
function configureMemorySearchCommand(parent, junior) {
|
|
560
|
+
parent.command("search").description("Search visible memories").argument("[query...]", "Search query").addOption(
|
|
561
|
+
new Option("--scope <scope>", "Memory scope").choices([...MEMORY_SCOPES]).makeOptionMandatory()
|
|
562
|
+
).requiredOption("--scope-key <key>", "Scope key").addOption(
|
|
563
|
+
new Option("--limit <n>", "Maximum rows").argParser(parseLimit).default(20)
|
|
564
|
+
).option("--show-content", "Print raw memory content").action(
|
|
565
|
+
junior.action(async (ctx, queryParts, options) => {
|
|
566
|
+
return await runSearch(
|
|
567
|
+
ctx,
|
|
568
|
+
queryParts,
|
|
569
|
+
options
|
|
570
|
+
);
|
|
571
|
+
})
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// src/cli/show.ts
|
|
576
|
+
import { eq as eq2 } from "drizzle-orm";
|
|
577
|
+
async function runShow(ctx, id) {
|
|
578
|
+
const db = ctx.db;
|
|
579
|
+
const rows = await db.select().from(juniorMemoryMemories).where(eq2(juniorMemoryMemories.id, id)).limit(1);
|
|
580
|
+
if (!rows[0]) {
|
|
581
|
+
await ctx.io.writeError(`Memory not found: ${id}
|
|
582
|
+
`);
|
|
583
|
+
return 1;
|
|
584
|
+
}
|
|
585
|
+
await ctx.io.writeOutput(`${formatMemory(rows[0], { showContent: true })}
|
|
586
|
+
`);
|
|
587
|
+
return 0;
|
|
588
|
+
}
|
|
589
|
+
function configureMemoryShowCommand(parent, junior) {
|
|
590
|
+
parent.command("show").description("Show one memory").argument("<id>", "Memory id").action(
|
|
591
|
+
junior.action(async (ctx, id) => {
|
|
592
|
+
return await runShow(ctx, id);
|
|
593
|
+
})
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// src/cli/index.ts
|
|
598
|
+
function createMemoryCliCommand() {
|
|
599
|
+
return {
|
|
600
|
+
name: "memory",
|
|
601
|
+
summary: "Inspect Junior memory state",
|
|
602
|
+
configure(command, junior) {
|
|
603
|
+
configureMemorySearchCommand(command, junior);
|
|
604
|
+
configureMemoryShowCommand(command, junior);
|
|
605
|
+
}
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// src/tools.ts
|
|
610
|
+
import { Type } from "@sinclair/typebox";
|
|
611
|
+
import { Value } from "@sinclair/typebox/value";
|
|
612
|
+
import {
|
|
613
|
+
getSourceKey,
|
|
614
|
+
PluginToolInputError
|
|
615
|
+
} from "@sentry/junior-plugin-api";
|
|
616
|
+
|
|
617
|
+
// src/store.ts
|
|
618
|
+
import { createHash, randomUUID } from "crypto";
|
|
619
|
+
import {
|
|
620
|
+
and as and2,
|
|
621
|
+
asc,
|
|
622
|
+
desc as desc2,
|
|
623
|
+
eq as eq3,
|
|
624
|
+
gt as gt2,
|
|
625
|
+
ilike as ilike2,
|
|
626
|
+
isNull as isNull2,
|
|
627
|
+
like,
|
|
628
|
+
or as or2,
|
|
629
|
+
sql as sql2
|
|
630
|
+
} from "drizzle-orm";
|
|
631
|
+
import { cosineDistance } from "drizzle-orm/sql/functions";
|
|
632
|
+
import { z as z3 } from "zod";
|
|
633
|
+
|
|
634
|
+
// src/scope.ts
|
|
635
|
+
function sourceConversationKey(ctx) {
|
|
636
|
+
if (ctx.source.platform === "local") {
|
|
637
|
+
return ctx.source.conversationId;
|
|
638
|
+
}
|
|
639
|
+
const threadKey = ctx.source.threadTs ?? ctx.source.messageTs;
|
|
640
|
+
if (!threadKey) {
|
|
641
|
+
return void 0;
|
|
642
|
+
}
|
|
643
|
+
return `slack:${ctx.source.teamId}:${ctx.source.channelId}:${threadKey}`;
|
|
644
|
+
}
|
|
645
|
+
function requesterScopeKey(ctx) {
|
|
646
|
+
const requester = ctx.requester;
|
|
647
|
+
if (!requester?.userId) {
|
|
648
|
+
return void 0;
|
|
649
|
+
}
|
|
650
|
+
if (requester.platform === "slack") {
|
|
651
|
+
return `slack:${requester.teamId}:${requester.userId}`;
|
|
652
|
+
}
|
|
653
|
+
return `local:${requester.userId}`;
|
|
654
|
+
}
|
|
655
|
+
function deriveMemoryScope(ctx, scope) {
|
|
656
|
+
if (scope === "personal") {
|
|
657
|
+
const scopeKey2 = requesterScopeKey(ctx);
|
|
658
|
+
if (!scopeKey2) {
|
|
659
|
+
throw new Error("Personal memory requires requester context.");
|
|
660
|
+
}
|
|
661
|
+
return { scope, scopeKey: scopeKey2 };
|
|
662
|
+
}
|
|
663
|
+
const scopeKey = sourceConversationKey(ctx);
|
|
664
|
+
if (!scopeKey) {
|
|
665
|
+
throw new Error("Conversation memory requires conversation context.");
|
|
666
|
+
}
|
|
667
|
+
return { scope, scopeKey };
|
|
668
|
+
}
|
|
669
|
+
function deriveMemorySubject(ctx, scope) {
|
|
670
|
+
if (scope.scope === "personal") {
|
|
671
|
+
const subjectKey2 = requesterScopeKey(ctx);
|
|
672
|
+
if (!subjectKey2) {
|
|
673
|
+
throw new Error("User-subject memory requires requester context.");
|
|
674
|
+
}
|
|
675
|
+
return { subjectType: "user", subjectKey: subjectKey2 };
|
|
676
|
+
}
|
|
677
|
+
const subjectKey = sourceConversationKey(ctx);
|
|
678
|
+
if (!subjectKey) {
|
|
679
|
+
throw new Error(
|
|
680
|
+
"Conversation-subject memory requires conversation context."
|
|
681
|
+
);
|
|
682
|
+
}
|
|
683
|
+
return { subjectType: "conversation", subjectKey };
|
|
684
|
+
}
|
|
685
|
+
function deriveVisibleMemoryScopes(ctx) {
|
|
686
|
+
const scopes = [];
|
|
687
|
+
try {
|
|
688
|
+
scopes.push(deriveMemoryScope(ctx, "personal"));
|
|
689
|
+
} catch {
|
|
690
|
+
}
|
|
691
|
+
try {
|
|
692
|
+
scopes.push(deriveMemoryScope(ctx, "conversation"));
|
|
693
|
+
} catch {
|
|
694
|
+
}
|
|
695
|
+
return scopes;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// src/store.ts
|
|
699
|
+
var DEFAULT_LIST_LIMIT = 50;
|
|
700
|
+
var DEFAULT_SEARCH_LIMIT = 10;
|
|
701
|
+
var VECTOR_SEARCH_OVERFETCH = 4;
|
|
702
|
+
var MAX_MEMORY_CONTENT_CHARS = 4e3;
|
|
703
|
+
var EMBEDDING_METRIC = "cosine";
|
|
704
|
+
var nonEmptyStringSchema2 = z3.string().min(1);
|
|
705
|
+
var memoryContentSchema = z3.string().refine((content) => content.trim().length > 0, {
|
|
706
|
+
message: "Memory content is required."
|
|
707
|
+
});
|
|
708
|
+
var numberSchema = z3.number().finite();
|
|
709
|
+
var createMemoryInputSchema = z3.object({
|
|
710
|
+
content: memoryContentSchema,
|
|
711
|
+
expiresAtMs: numberSchema.optional(),
|
|
712
|
+
idempotencyKey: nonEmptyStringSchema2
|
|
713
|
+
}).strict();
|
|
714
|
+
var listMemoriesInputSchema = z3.object({
|
|
715
|
+
limit: numberSchema.optional()
|
|
716
|
+
}).strict();
|
|
717
|
+
var searchMemoriesInputSchema = z3.object({
|
|
718
|
+
limit: numberSchema.optional(),
|
|
719
|
+
query: nonEmptyStringSchema2
|
|
720
|
+
}).strict();
|
|
721
|
+
var archiveMemoryInputSchema = z3.object({
|
|
722
|
+
id: nonEmptyStringSchema2,
|
|
723
|
+
reason: nonEmptyStringSchema2.optional()
|
|
724
|
+
}).strict();
|
|
725
|
+
var clockSchema = z3.function({ input: [], output: numberSchema }).optional();
|
|
726
|
+
var memoryStoreOptionsSchema = z3.object({
|
|
727
|
+
now: clockSchema
|
|
728
|
+
}).strict();
|
|
729
|
+
var optionalNumberSchema = z3.preprocess(
|
|
730
|
+
(value) => value === null ? void 0 : value,
|
|
731
|
+
z3.coerce.number().optional()
|
|
732
|
+
);
|
|
733
|
+
var optionalStringSchema = z3.preprocess(
|
|
734
|
+
(value) => value === null ? void 0 : value,
|
|
735
|
+
z3.string().optional()
|
|
736
|
+
);
|
|
737
|
+
var optionalNonEmptyStringSchema = z3.preprocess(
|
|
738
|
+
(value) => value === null ? void 0 : value,
|
|
739
|
+
z3.string().min(1).optional()
|
|
740
|
+
);
|
|
741
|
+
var memoryRowSchema = z3.object({
|
|
742
|
+
archivedAtMs: optionalNumberSchema,
|
|
743
|
+
archiveReason: optionalStringSchema,
|
|
744
|
+
content: memoryContentSchema,
|
|
745
|
+
createdAtMs: z3.coerce.number(),
|
|
746
|
+
expiresAtMs: optionalNumberSchema,
|
|
747
|
+
id: z3.string().min(1),
|
|
748
|
+
idempotencyKey: optionalStringSchema,
|
|
749
|
+
observedAtMs: z3.coerce.number(),
|
|
750
|
+
scope: z3.enum(MEMORY_SCOPES),
|
|
751
|
+
scopeKey: z3.string().min(1),
|
|
752
|
+
sourceKey: z3.string().min(1),
|
|
753
|
+
sourcePlatform: z3.enum(MEMORY_SOURCE_PLATFORMS),
|
|
754
|
+
subjectKey: optionalNonEmptyStringSchema,
|
|
755
|
+
subjectType: z3.enum(MEMORY_SUBJECT_TYPES),
|
|
756
|
+
supersededAtMs: optionalNumberSchema,
|
|
757
|
+
supersededById: optionalStringSchema,
|
|
758
|
+
type: z3.enum(MEMORY_TYPES)
|
|
759
|
+
}).strict().superRefine((row, ctx) => {
|
|
760
|
+
if (row.subjectType === "general") {
|
|
761
|
+
if (row.subjectKey !== void 0) {
|
|
762
|
+
ctx.addIssue({
|
|
763
|
+
code: "custom",
|
|
764
|
+
message: "General-subject memory rows must not have a subject key.",
|
|
765
|
+
path: ["subjectKey"]
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
if (row.subjectKey === void 0) {
|
|
771
|
+
ctx.addIssue({
|
|
772
|
+
code: "custom",
|
|
773
|
+
message: "User and conversation memory rows require a subject key.",
|
|
774
|
+
path: ["subjectKey"]
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
});
|
|
778
|
+
var memoryRecordSchema = z3.object({
|
|
779
|
+
archivedAtMs: numberSchema.optional(),
|
|
780
|
+
archiveReason: nonEmptyStringSchema2.optional(),
|
|
781
|
+
content: memoryContentSchema,
|
|
782
|
+
createdAtMs: numberSchema,
|
|
783
|
+
expiresAtMs: numberSchema.optional(),
|
|
784
|
+
id: nonEmptyStringSchema2,
|
|
785
|
+
observedAtMs: numberSchema,
|
|
786
|
+
scope: z3.enum(MEMORY_SCOPES),
|
|
787
|
+
subjectType: z3.enum(MEMORY_SUBJECT_TYPES),
|
|
788
|
+
supersededAtMs: numberSchema.optional(),
|
|
789
|
+
supersededById: nonEmptyStringSchema2.optional(),
|
|
790
|
+
type: z3.enum(MEMORY_TYPES)
|
|
791
|
+
}).strict();
|
|
792
|
+
var embeddingVectorSchema = z3.array(numberSchema).length(MEMORY_EMBEDDING_DIMENSIONS);
|
|
793
|
+
var embeddingResultSchema = z3.object({
|
|
794
|
+
dimensions: z3.literal(MEMORY_EMBEDDING_DIMENSIONS),
|
|
795
|
+
model: nonEmptyStringSchema2,
|
|
796
|
+
provider: nonEmptyStringSchema2,
|
|
797
|
+
vectors: z3.array(embeddingVectorSchema)
|
|
798
|
+
}).strict();
|
|
799
|
+
function normalizeContent(content) {
|
|
800
|
+
return content.replace(/\s+/g, " ").trim();
|
|
801
|
+
}
|
|
802
|
+
function hashEmbeddedContent(content) {
|
|
803
|
+
return createHash("sha256").update(content, "utf8").digest("hex");
|
|
804
|
+
}
|
|
805
|
+
function boundedLimit(value, fallback) {
|
|
806
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
807
|
+
return fallback;
|
|
808
|
+
}
|
|
809
|
+
return Math.min(200, Math.max(1, Math.floor(value)));
|
|
810
|
+
}
|
|
811
|
+
function sourceKey(ctx) {
|
|
812
|
+
if (ctx.source.platform === "local") {
|
|
813
|
+
return ctx.source.conversationId;
|
|
814
|
+
}
|
|
815
|
+
const threadKey = ctx.source.threadTs ?? ctx.source.messageTs;
|
|
816
|
+
if (!threadKey) {
|
|
817
|
+
throw new Error(
|
|
818
|
+
"Memory source requires a Slack message or thread timestamp."
|
|
819
|
+
);
|
|
820
|
+
}
|
|
821
|
+
return `slack:${ctx.source.teamId}:${ctx.source.channelId}:${threadKey}`;
|
|
822
|
+
}
|
|
823
|
+
function parseMemoryRow(row) {
|
|
824
|
+
const parsed = memoryRowSchema.parse(row);
|
|
825
|
+
return memoryRecordSchema.parse({
|
|
826
|
+
id: parsed.id,
|
|
827
|
+
scope: parsed.scope,
|
|
828
|
+
type: parsed.type,
|
|
829
|
+
subjectType: parsed.subjectType,
|
|
830
|
+
content: parsed.content,
|
|
831
|
+
observedAtMs: parsed.observedAtMs,
|
|
832
|
+
createdAtMs: parsed.createdAtMs,
|
|
833
|
+
...parsed.expiresAtMs !== void 0 ? { expiresAtMs: parsed.expiresAtMs } : {},
|
|
834
|
+
...parsed.supersededAtMs !== void 0 ? { supersededAtMs: parsed.supersededAtMs } : {},
|
|
835
|
+
...parsed.supersededById ? { supersededById: parsed.supersededById } : {},
|
|
836
|
+
...parsed.archivedAtMs !== void 0 ? { archivedAtMs: parsed.archivedAtMs } : {},
|
|
837
|
+
...parsed.archiveReason ? { archiveReason: parsed.archiveReason } : {}
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
function visibleScopePredicate(scopes) {
|
|
841
|
+
if (scopes.length === 0) {
|
|
842
|
+
return void 0;
|
|
843
|
+
}
|
|
844
|
+
return or2(
|
|
845
|
+
...scopes.map(
|
|
846
|
+
(scope) => and2(
|
|
847
|
+
eq3(juniorMemoryMemories.scope, scope.scope),
|
|
848
|
+
eq3(juniorMemoryMemories.scopeKey, scope.scopeKey)
|
|
849
|
+
)
|
|
850
|
+
)
|
|
851
|
+
);
|
|
852
|
+
}
|
|
853
|
+
function activeVisiblePredicate(args) {
|
|
854
|
+
const scopePredicate = visibleScopePredicate(args.scopes);
|
|
855
|
+
if (!scopePredicate) {
|
|
856
|
+
return void 0;
|
|
857
|
+
}
|
|
858
|
+
return and2(
|
|
859
|
+
scopePredicate,
|
|
860
|
+
isNull2(juniorMemoryMemories.archivedAtMs),
|
|
861
|
+
isNull2(juniorMemoryMemories.supersededAtMs),
|
|
862
|
+
isNull2(juniorMemoryMemories.supersededById),
|
|
863
|
+
or2(
|
|
864
|
+
isNull2(juniorMemoryMemories.expiresAtMs),
|
|
865
|
+
gt2(juniorMemoryMemories.expiresAtMs, args.nowMs)
|
|
866
|
+
)
|
|
867
|
+
);
|
|
868
|
+
}
|
|
869
|
+
async function findByIdempotencyKey(args) {
|
|
870
|
+
const rows = await args.db.select().from(juniorMemoryMemories).where(
|
|
871
|
+
and2(
|
|
872
|
+
eq3(juniorMemoryMemories.scope, args.scope.scope),
|
|
873
|
+
eq3(juniorMemoryMemories.scopeKey, args.scope.scopeKey),
|
|
874
|
+
eq3(juniorMemoryMemories.idempotencyKey, args.idempotencyKey),
|
|
875
|
+
isNull2(juniorMemoryMemories.archivedAtMs),
|
|
876
|
+
isNull2(juniorMemoryMemories.supersededAtMs),
|
|
877
|
+
isNull2(juniorMemoryMemories.supersededById)
|
|
878
|
+
)
|
|
879
|
+
).limit(1);
|
|
880
|
+
return rows[0] ? parseMemoryRow(rows[0]) : void 0;
|
|
881
|
+
}
|
|
882
|
+
function searchScore(memory, terms) {
|
|
883
|
+
const haystack = memory.content.toLowerCase();
|
|
884
|
+
return terms.reduce(
|
|
885
|
+
(score, term) => score + (haystack.includes(term) ? 1 : 0),
|
|
886
|
+
0
|
|
887
|
+
);
|
|
888
|
+
}
|
|
889
|
+
function searchTerms(query) {
|
|
890
|
+
return [
|
|
891
|
+
...new Set(
|
|
892
|
+
query.toLowerCase().split(/[^a-z0-9_'-]+/).map((term) => term.trim()).filter((term) => term.length >= 2)
|
|
893
|
+
)
|
|
894
|
+
];
|
|
895
|
+
}
|
|
896
|
+
async function embedOne(embedder, text2) {
|
|
897
|
+
const normalized = normalizeContent(text2);
|
|
898
|
+
if (!normalized) {
|
|
899
|
+
throw new Error("Embedding text is required.");
|
|
900
|
+
}
|
|
901
|
+
const result = embeddingResultSchema.parse(
|
|
902
|
+
await embedder.embedTexts({ texts: [normalized] })
|
|
903
|
+
);
|
|
904
|
+
if (result.vectors.length !== 1) {
|
|
905
|
+
throw new Error("Embedding provider returned an unexpected vector count.");
|
|
906
|
+
}
|
|
907
|
+
return {
|
|
908
|
+
model: result.model,
|
|
909
|
+
provider: result.provider,
|
|
910
|
+
vector: result.vectors[0]
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
async function storeEmbedding(args) {
|
|
914
|
+
if (!args.embedder) {
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
try {
|
|
918
|
+
const existing = await args.db.select({ memoryId: juniorMemoryEmbeddings.memoryId }).from(juniorMemoryEmbeddings).where(eq3(juniorMemoryEmbeddings.memoryId, args.memoryId)).limit(1);
|
|
919
|
+
if (existing[0]) {
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
} catch {
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
let embedding;
|
|
926
|
+
try {
|
|
927
|
+
embedding = await embedOne(args.embedder, args.content);
|
|
928
|
+
} catch {
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
try {
|
|
932
|
+
await args.db.insert(juniorMemoryEmbeddings).values({
|
|
933
|
+
contentHash: hashEmbeddedContent(args.content),
|
|
934
|
+
createdAtMs: args.nowMs,
|
|
935
|
+
dimensions: MEMORY_EMBEDDING_DIMENSIONS,
|
|
936
|
+
embedding: embedding.vector,
|
|
937
|
+
memoryId: args.memoryId,
|
|
938
|
+
metric: EMBEDDING_METRIC,
|
|
939
|
+
model: embedding.model,
|
|
940
|
+
provider: embedding.provider
|
|
941
|
+
}).onConflictDoNothing();
|
|
942
|
+
} catch {
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
async function listVisibleMemories(args) {
|
|
947
|
+
const predicate = activeVisiblePredicate(args);
|
|
948
|
+
if (!predicate) {
|
|
949
|
+
return [];
|
|
950
|
+
}
|
|
951
|
+
const limit = boundedLimit(args.limit, DEFAULT_LIST_LIMIT);
|
|
952
|
+
const rows = await args.db.select().from(juniorMemoryMemories).where(predicate).orderBy(
|
|
953
|
+
desc2(juniorMemoryMemories.createdAtMs),
|
|
954
|
+
asc(juniorMemoryMemories.id)
|
|
955
|
+
).limit(limit);
|
|
956
|
+
return rows.map(parseMemoryRow);
|
|
957
|
+
}
|
|
958
|
+
async function searchVisibleMemories(args) {
|
|
959
|
+
const terms = searchTerms(args.query);
|
|
960
|
+
if (terms.length === 0) {
|
|
961
|
+
return [];
|
|
962
|
+
}
|
|
963
|
+
const predicate = activeVisiblePredicate(args);
|
|
964
|
+
if (!predicate) {
|
|
965
|
+
return [];
|
|
966
|
+
}
|
|
967
|
+
const rows = await args.db.select().from(juniorMemoryMemories).where(
|
|
968
|
+
and2(
|
|
969
|
+
predicate,
|
|
970
|
+
or2(
|
|
971
|
+
...terms.map(
|
|
972
|
+
(term) => ilike2(juniorMemoryMemories.content, `%${term}%`)
|
|
973
|
+
)
|
|
974
|
+
)
|
|
975
|
+
)
|
|
976
|
+
);
|
|
977
|
+
return rows.map(parseMemoryRow);
|
|
978
|
+
}
|
|
979
|
+
async function searchVisibleVectorMemories(args) {
|
|
980
|
+
if (!args.embedder) {
|
|
981
|
+
return [];
|
|
982
|
+
}
|
|
983
|
+
const predicate = activeVisiblePredicate(args);
|
|
984
|
+
if (!predicate) {
|
|
985
|
+
return [];
|
|
986
|
+
}
|
|
987
|
+
let embedding;
|
|
988
|
+
try {
|
|
989
|
+
embedding = await embedOne(args.embedder, args.query);
|
|
990
|
+
} catch {
|
|
991
|
+
return [];
|
|
992
|
+
}
|
|
993
|
+
const distance = cosineDistance(
|
|
994
|
+
juniorMemoryEmbeddings.embedding,
|
|
995
|
+
embedding.vector
|
|
996
|
+
);
|
|
997
|
+
const rows = await args.db.select({
|
|
998
|
+
contentHash: juniorMemoryEmbeddings.contentHash,
|
|
999
|
+
distance,
|
|
1000
|
+
memory: juniorMemoryMemories
|
|
1001
|
+
}).from(juniorMemoryMemories).innerJoin(
|
|
1002
|
+
juniorMemoryEmbeddings,
|
|
1003
|
+
eq3(juniorMemoryEmbeddings.memoryId, juniorMemoryMemories.id)
|
|
1004
|
+
).where(
|
|
1005
|
+
and2(
|
|
1006
|
+
predicate,
|
|
1007
|
+
eq3(juniorMemoryEmbeddings.provider, embedding.provider),
|
|
1008
|
+
eq3(juniorMemoryEmbeddings.model, embedding.model),
|
|
1009
|
+
eq3(juniorMemoryEmbeddings.dimensions, MEMORY_EMBEDDING_DIMENSIONS),
|
|
1010
|
+
eq3(juniorMemoryEmbeddings.metric, EMBEDDING_METRIC)
|
|
1011
|
+
)
|
|
1012
|
+
).orderBy(
|
|
1013
|
+
distance,
|
|
1014
|
+
desc2(juniorMemoryMemories.createdAtMs),
|
|
1015
|
+
asc(juniorMemoryMemories.id)
|
|
1016
|
+
).limit(args.limit);
|
|
1017
|
+
return rows.flatMap((row) => {
|
|
1018
|
+
const distanceValue = Number(row.distance);
|
|
1019
|
+
if (row.distance === null || !Number.isFinite(distanceValue) || hashEmbeddedContent(row.memory.content) !== row.contentHash) {
|
|
1020
|
+
return [];
|
|
1021
|
+
}
|
|
1022
|
+
return [
|
|
1023
|
+
{
|
|
1024
|
+
memory: parseMemoryRow(row.memory),
|
|
1025
|
+
score: 1 / (1 + Math.max(0, distanceValue))
|
|
1026
|
+
}
|
|
1027
|
+
];
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
function mergeSearchCandidates(candidates) {
|
|
1031
|
+
const byId = /* @__PURE__ */ new Map();
|
|
1032
|
+
for (const candidate of candidates) {
|
|
1033
|
+
const existing = byId.get(candidate.memory.id);
|
|
1034
|
+
if (existing) {
|
|
1035
|
+
existing.score += candidate.score;
|
|
1036
|
+
continue;
|
|
1037
|
+
}
|
|
1038
|
+
byId.set(candidate.memory.id, {
|
|
1039
|
+
memory: candidate.memory,
|
|
1040
|
+
score: candidate.score
|
|
1041
|
+
});
|
|
1042
|
+
}
|
|
1043
|
+
return [...byId.values()].sort(
|
|
1044
|
+
(left, right) => right.score - left.score || right.memory.createdAtMs - left.memory.createdAtMs || left.memory.id.localeCompare(right.memory.id)
|
|
1045
|
+
).map((candidate) => candidate.memory);
|
|
1046
|
+
}
|
|
1047
|
+
function createMemoryStore(db, context, options = {}) {
|
|
1048
|
+
const runtimeContext = memoryRuntimeContextSchema.parse(context);
|
|
1049
|
+
const parsedOptions = memoryStoreOptionsSchema.parse({ now: options.now });
|
|
1050
|
+
const embedder = options.embedder;
|
|
1051
|
+
const getNowMs = parsedOptions.now ?? Date.now;
|
|
1052
|
+
async function createScopedMemory(rawInput, scopeKind) {
|
|
1053
|
+
const input = createMemoryInputSchema.parse(rawInput);
|
|
1054
|
+
const nowMs = getNowMs();
|
|
1055
|
+
const content = normalizeContent(input.content);
|
|
1056
|
+
const scope = deriveMemoryScope(runtimeContext, scopeKind);
|
|
1057
|
+
const subject = deriveMemorySubject(runtimeContext, scope);
|
|
1058
|
+
if (content.length > MAX_MEMORY_CONTENT_CHARS) {
|
|
1059
|
+
throw new Error("Memory content exceeds the maximum length.");
|
|
1060
|
+
}
|
|
1061
|
+
const id = randomUUID();
|
|
1062
|
+
const rows = await db.insert(juniorMemoryMemories).values({
|
|
1063
|
+
content,
|
|
1064
|
+
createdAtMs: nowMs,
|
|
1065
|
+
expiresAtMs: input.expiresAtMs,
|
|
1066
|
+
id,
|
|
1067
|
+
idempotencyKey: input.idempotencyKey,
|
|
1068
|
+
observedAtMs: nowMs,
|
|
1069
|
+
scope: scope.scope,
|
|
1070
|
+
scopeKey: scope.scopeKey,
|
|
1071
|
+
sourceKey: sourceKey(runtimeContext),
|
|
1072
|
+
sourcePlatform: runtimeContext.source.platform,
|
|
1073
|
+
subjectKey: subject.subjectKey,
|
|
1074
|
+
subjectType: subject.subjectType,
|
|
1075
|
+
type: "knowledge"
|
|
1076
|
+
}).onConflictDoNothing({
|
|
1077
|
+
target: [
|
|
1078
|
+
juniorMemoryMemories.scope,
|
|
1079
|
+
juniorMemoryMemories.scopeKey,
|
|
1080
|
+
juniorMemoryMemories.idempotencyKey
|
|
1081
|
+
],
|
|
1082
|
+
where: sql2`${juniorMemoryMemories.idempotencyKey} IS NOT NULL AND ${juniorMemoryMemories.archivedAtMs} IS NULL AND ${juniorMemoryMemories.supersededAtMs} IS NULL AND ${juniorMemoryMemories.supersededById} IS NULL`
|
|
1083
|
+
}).returning();
|
|
1084
|
+
if (rows[0]) {
|
|
1085
|
+
const memory = parseMemoryRow(rows[0]);
|
|
1086
|
+
await storeEmbedding({
|
|
1087
|
+
content: memory.content,
|
|
1088
|
+
db,
|
|
1089
|
+
embedder,
|
|
1090
|
+
memoryId: memory.id,
|
|
1091
|
+
nowMs
|
|
1092
|
+
});
|
|
1093
|
+
return { created: true, memory };
|
|
1094
|
+
}
|
|
1095
|
+
const idempotent = await findByIdempotencyKey({
|
|
1096
|
+
db,
|
|
1097
|
+
idempotencyKey: input.idempotencyKey,
|
|
1098
|
+
scope
|
|
1099
|
+
});
|
|
1100
|
+
if (!idempotent) {
|
|
1101
|
+
throw new Error("Memory idempotency conflict did not resolve.");
|
|
1102
|
+
}
|
|
1103
|
+
await storeEmbedding({
|
|
1104
|
+
content: idempotent.content,
|
|
1105
|
+
db,
|
|
1106
|
+
embedder,
|
|
1107
|
+
memoryId: idempotent.id,
|
|
1108
|
+
nowMs
|
|
1109
|
+
});
|
|
1110
|
+
return { created: false, memory: idempotent };
|
|
1111
|
+
}
|
|
1112
|
+
return {
|
|
1113
|
+
async createMemory(input) {
|
|
1114
|
+
return await createScopedMemory(input, "personal");
|
|
1115
|
+
},
|
|
1116
|
+
async createConversationMemory(input) {
|
|
1117
|
+
return await createScopedMemory(input, "conversation");
|
|
1118
|
+
},
|
|
1119
|
+
async listMemories(input) {
|
|
1120
|
+
input = listMemoriesInputSchema.parse(input);
|
|
1121
|
+
const nowMs = getNowMs();
|
|
1122
|
+
const scopes = deriveVisibleMemoryScopes(runtimeContext);
|
|
1123
|
+
return await listVisibleMemories({
|
|
1124
|
+
db,
|
|
1125
|
+
limit: input.limit,
|
|
1126
|
+
nowMs,
|
|
1127
|
+
scopes
|
|
1128
|
+
});
|
|
1129
|
+
},
|
|
1130
|
+
async searchMemories(input) {
|
|
1131
|
+
input = searchMemoriesInputSchema.parse(input);
|
|
1132
|
+
const nowMs = getNowMs();
|
|
1133
|
+
const scopes = deriveVisibleMemoryScopes(runtimeContext);
|
|
1134
|
+
const limit = boundedLimit(input.limit, DEFAULT_SEARCH_LIMIT);
|
|
1135
|
+
const vectorCandidates = await searchVisibleVectorMemories({
|
|
1136
|
+
db,
|
|
1137
|
+
embedder,
|
|
1138
|
+
limit: limit * VECTOR_SEARCH_OVERFETCH,
|
|
1139
|
+
nowMs,
|
|
1140
|
+
query: input.query,
|
|
1141
|
+
scopes
|
|
1142
|
+
});
|
|
1143
|
+
const candidates = await searchVisibleMemories({
|
|
1144
|
+
db,
|
|
1145
|
+
nowMs,
|
|
1146
|
+
query: input.query,
|
|
1147
|
+
scopes
|
|
1148
|
+
});
|
|
1149
|
+
const terms = searchTerms(input.query);
|
|
1150
|
+
const lexicalCandidates = candidates.map((memory) => ({ memory, score: searchScore(memory, terms) })).filter((item) => item.score > 0);
|
|
1151
|
+
return mergeSearchCandidates([
|
|
1152
|
+
...vectorCandidates,
|
|
1153
|
+
...lexicalCandidates
|
|
1154
|
+
]).slice(0, limit);
|
|
1155
|
+
},
|
|
1156
|
+
async archiveMemory(input) {
|
|
1157
|
+
input = archiveMemoryInputSchema.parse(input);
|
|
1158
|
+
const nowMs = getNowMs();
|
|
1159
|
+
const scopes = deriveVisibleMemoryScopes(runtimeContext);
|
|
1160
|
+
const predicate = activeVisiblePredicate({ nowMs, scopes });
|
|
1161
|
+
const idPrefix = input.id.trim();
|
|
1162
|
+
if (!idPrefix) {
|
|
1163
|
+
throw new Error("Memory id is required.");
|
|
1164
|
+
}
|
|
1165
|
+
const rows = predicate ? await db.select().from(juniorMemoryMemories).where(
|
|
1166
|
+
and2(
|
|
1167
|
+
predicate,
|
|
1168
|
+
or2(
|
|
1169
|
+
eq3(juniorMemoryMemories.id, idPrefix),
|
|
1170
|
+
like(juniorMemoryMemories.id, `${idPrefix}%`)
|
|
1171
|
+
)
|
|
1172
|
+
)
|
|
1173
|
+
).orderBy(asc(juniorMemoryMemories.id)).limit(2) : [];
|
|
1174
|
+
if (rows.length === 0) {
|
|
1175
|
+
throw new Error("Memory was not found in the current context.");
|
|
1176
|
+
}
|
|
1177
|
+
if (rows.length > 1) {
|
|
1178
|
+
throw new Error("Memory id prefix is ambiguous.");
|
|
1179
|
+
}
|
|
1180
|
+
const memory = parseMemoryRow(rows[0]);
|
|
1181
|
+
const updated = await db.update(juniorMemoryMemories).set({
|
|
1182
|
+
archivedAtMs: nowMs,
|
|
1183
|
+
archiveReason: input.reason ?? "user_removed"
|
|
1184
|
+
}).where(eq3(juniorMemoryMemories.id, memory.id)).returning();
|
|
1185
|
+
await db.delete(juniorMemoryEmbeddings).where(eq3(juniorMemoryEmbeddings.memoryId, memory.id));
|
|
1186
|
+
return parseMemoryRow(updated[0]);
|
|
1187
|
+
}
|
|
1188
|
+
};
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
// src/tools.ts
|
|
1192
|
+
var MAX_TOOL_CONTENT_CHARS = 4e3;
|
|
1193
|
+
var DEFAULT_RESULT_LIMIT = 20;
|
|
1194
|
+
var DEFAULT_SEARCH_LIMIT2 = 10;
|
|
1195
|
+
var KNOWN_TOOL_INPUT_ERROR_MESSAGES = /* @__PURE__ */ new Set([
|
|
1196
|
+
"Conversation memory requires conversation context.",
|
|
1197
|
+
"Conversation-subject memory requires conversation context.",
|
|
1198
|
+
"Memory content is required.",
|
|
1199
|
+
"Memory content exceeds the maximum length.",
|
|
1200
|
+
"Memory id is required.",
|
|
1201
|
+
"Memory was not found in the current context.",
|
|
1202
|
+
"Memory id prefix is ambiguous.",
|
|
1203
|
+
"Personal memory requires requester context.",
|
|
1204
|
+
"User-subject memory requires requester context."
|
|
1205
|
+
]);
|
|
1206
|
+
function throwToolInputError(message) {
|
|
1207
|
+
throw new PluginToolInputError(message);
|
|
1208
|
+
}
|
|
1209
|
+
function asToolInputError(error) {
|
|
1210
|
+
if (error instanceof PluginToolInputError) {
|
|
1211
|
+
throw error;
|
|
1212
|
+
}
|
|
1213
|
+
if (error instanceof Error && KNOWN_TOOL_INPUT_ERROR_MESSAGES.has(error.message)) {
|
|
1214
|
+
throw new PluginToolInputError(error.message, { cause: error });
|
|
1215
|
+
}
|
|
1216
|
+
throw error;
|
|
1217
|
+
}
|
|
1218
|
+
function memoryRuntimeContext(context) {
|
|
1219
|
+
return memoryRuntimeContextSchema.parse({
|
|
1220
|
+
...context.conversationId ? { conversationId: context.conversationId } : {},
|
|
1221
|
+
...context.requester ? { requester: context.requester } : {},
|
|
1222
|
+
source: context.source
|
|
1223
|
+
});
|
|
1224
|
+
}
|
|
1225
|
+
function memoryStore(context) {
|
|
1226
|
+
return createMemoryStore(context.db, memoryRuntimeContext(context), {
|
|
1227
|
+
embedder: context.embedder
|
|
1228
|
+
});
|
|
1229
|
+
}
|
|
1230
|
+
function boundedLimit2(value, fallback) {
|
|
1231
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
1232
|
+
return fallback;
|
|
1233
|
+
}
|
|
1234
|
+
return Math.min(50, Math.max(1, Math.floor(value)));
|
|
1235
|
+
}
|
|
1236
|
+
function digitAt(value, index2) {
|
|
1237
|
+
const code = value.charCodeAt(index2);
|
|
1238
|
+
return code >= 48 && code <= 57;
|
|
1239
|
+
}
|
|
1240
|
+
function readDigits(value, start, length) {
|
|
1241
|
+
for (let index2 = start; index2 < start + length; index2++) {
|
|
1242
|
+
if (!digitAt(value, index2)) {
|
|
1243
|
+
return void 0;
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
return Number(value.slice(start, start + length));
|
|
1247
|
+
}
|
|
1248
|
+
function parseIsoTimestampParts(value) {
|
|
1249
|
+
if (value.length < 20 || value[4] !== "-" || value[7] !== "-" || value[10] !== "T" || value[13] !== ":" || value[16] !== ":") {
|
|
1250
|
+
return void 0;
|
|
1251
|
+
}
|
|
1252
|
+
const year = readDigits(value, 0, 4);
|
|
1253
|
+
const month = readDigits(value, 5, 2);
|
|
1254
|
+
const day = readDigits(value, 8, 2);
|
|
1255
|
+
const hour = readDigits(value, 11, 2);
|
|
1256
|
+
const minute = readDigits(value, 14, 2);
|
|
1257
|
+
const second = readDigits(value, 17, 2);
|
|
1258
|
+
if (year === void 0 || month === void 0 || day === void 0 || hour === void 0 || minute === void 0 || second === void 0) {
|
|
1259
|
+
return void 0;
|
|
1260
|
+
}
|
|
1261
|
+
let zoneStart = 19;
|
|
1262
|
+
if (value[zoneStart] === ".") {
|
|
1263
|
+
zoneStart += 1;
|
|
1264
|
+
const fractionStart = zoneStart;
|
|
1265
|
+
while (zoneStart < value.length && digitAt(value, zoneStart)) {
|
|
1266
|
+
zoneStart += 1;
|
|
1267
|
+
}
|
|
1268
|
+
if (zoneStart === fractionStart) {
|
|
1269
|
+
return void 0;
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
if (value[zoneStart] === "Z") {
|
|
1273
|
+
if (zoneStart !== value.length - 1) {
|
|
1274
|
+
return void 0;
|
|
1275
|
+
}
|
|
1276
|
+
} else if (value[zoneStart] === "+" || value[zoneStart] === "-") {
|
|
1277
|
+
if (zoneStart !== value.length - 6 || value[zoneStart + 3] !== ":" || readDigits(value, zoneStart + 1, 2) === void 0 || readDigits(value, zoneStart + 4, 2) === void 0) {
|
|
1278
|
+
return void 0;
|
|
1279
|
+
}
|
|
1280
|
+
} else {
|
|
1281
|
+
return void 0;
|
|
1282
|
+
}
|
|
1283
|
+
return { day, hour, minute, month, second, year };
|
|
1284
|
+
}
|
|
1285
|
+
function parseExpiresAt(value) {
|
|
1286
|
+
if (!value) {
|
|
1287
|
+
return void 0;
|
|
1288
|
+
}
|
|
1289
|
+
if (value === "never") {
|
|
1290
|
+
return void 0;
|
|
1291
|
+
}
|
|
1292
|
+
const parts = parseIsoTimestampParts(value);
|
|
1293
|
+
const expiresAtMs = Date.parse(value);
|
|
1294
|
+
if (!parts || !Number.isFinite(expiresAtMs)) {
|
|
1295
|
+
throwToolInputError('expires_at must be "never" or a valid ISO timestamp.');
|
|
1296
|
+
}
|
|
1297
|
+
const calendarDate = new Date(
|
|
1298
|
+
Date.UTC(parts.year, parts.month - 1, parts.day)
|
|
1299
|
+
);
|
|
1300
|
+
if (calendarDate.getUTCFullYear() !== parts.year || calendarDate.getUTCMonth() !== parts.month - 1 || calendarDate.getUTCDate() !== parts.day || parts.hour > 23 || parts.minute > 59 || parts.second > 59) {
|
|
1301
|
+
throwToolInputError('expires_at must be "never" or a valid ISO timestamp.');
|
|
1302
|
+
}
|
|
1303
|
+
return expiresAtMs;
|
|
1304
|
+
}
|
|
1305
|
+
function requireToolCallId(value) {
|
|
1306
|
+
if (!value) {
|
|
1307
|
+
throwToolInputError("Memory creation requires a tool call id.");
|
|
1308
|
+
}
|
|
1309
|
+
return value;
|
|
1310
|
+
}
|
|
1311
|
+
function requireMemoryContent(value) {
|
|
1312
|
+
if (value.trim().length === 0) {
|
|
1313
|
+
throwToolInputError("Memory content is required.");
|
|
1314
|
+
}
|
|
1315
|
+
return value;
|
|
1316
|
+
}
|
|
1317
|
+
var createMemoryInputSchema2 = Type.Object(
|
|
1318
|
+
{
|
|
1319
|
+
content: Type.String({
|
|
1320
|
+
minLength: 1,
|
|
1321
|
+
maxLength: MAX_TOOL_CONTENT_CHARS,
|
|
1322
|
+
description: "Self-contained public/shareable memory candidate. Include the subject in natural language when it matters; do not rely on surrounding chat context."
|
|
1323
|
+
}),
|
|
1324
|
+
expires_at: Type.Optional(
|
|
1325
|
+
Type.String({
|
|
1326
|
+
minLength: 1,
|
|
1327
|
+
description: 'Expiration selector. Omit or use "never" when the memory should not expire, or use an exact ISO timestamp such as "2027-06-21T00:00:00Z".'
|
|
1328
|
+
})
|
|
1329
|
+
)
|
|
1330
|
+
},
|
|
1331
|
+
{ additionalProperties: false }
|
|
1332
|
+
);
|
|
1333
|
+
var removeMemoryInputSchema = Type.Object(
|
|
1334
|
+
{
|
|
1335
|
+
id: Type.String({
|
|
1336
|
+
minLength: 1,
|
|
1337
|
+
description: "Memory id or unambiguous short id prefix to remove."
|
|
1338
|
+
})
|
|
1339
|
+
},
|
|
1340
|
+
{ additionalProperties: false }
|
|
1341
|
+
);
|
|
1342
|
+
var listMemoriesInputSchema2 = Type.Object(
|
|
1343
|
+
{
|
|
1344
|
+
limit: Type.Optional(
|
|
1345
|
+
Type.Number({
|
|
1346
|
+
minimum: 1,
|
|
1347
|
+
maximum: 50,
|
|
1348
|
+
description: "Maximum number of visible memories to return."
|
|
1349
|
+
})
|
|
1350
|
+
)
|
|
1351
|
+
},
|
|
1352
|
+
{ additionalProperties: false }
|
|
1353
|
+
);
|
|
1354
|
+
var searchMemoriesInputSchema2 = Type.Object(
|
|
1355
|
+
{
|
|
1356
|
+
query: Type.String({
|
|
1357
|
+
minLength: 1,
|
|
1358
|
+
description: "Search query for visible memory content."
|
|
1359
|
+
}),
|
|
1360
|
+
limit: Type.Optional(
|
|
1361
|
+
Type.Number({
|
|
1362
|
+
minimum: 1,
|
|
1363
|
+
maximum: 50,
|
|
1364
|
+
description: "Maximum number of matching memories to return."
|
|
1365
|
+
})
|
|
1366
|
+
)
|
|
1367
|
+
},
|
|
1368
|
+
{ additionalProperties: false }
|
|
1369
|
+
);
|
|
1370
|
+
function parseToolInput(schema, input) {
|
|
1371
|
+
try {
|
|
1372
|
+
if (!Value.Check(schema, input)) {
|
|
1373
|
+
throw new Error("Input does not match memory tool schema.");
|
|
1374
|
+
}
|
|
1375
|
+
return Value.Parse(schema, input);
|
|
1376
|
+
} catch (error) {
|
|
1377
|
+
throw new PluginToolInputError("Invalid memory tool input.", {
|
|
1378
|
+
cause: error
|
|
1379
|
+
});
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
function sourceIdempotencyKey(context) {
|
|
1383
|
+
const sourceKey2 = getSourceKey(context.source);
|
|
1384
|
+
if (!sourceKey2) {
|
|
1385
|
+
throwToolInputError("Memory creation requires source message context.");
|
|
1386
|
+
}
|
|
1387
|
+
return sourceKey2;
|
|
1388
|
+
}
|
|
1389
|
+
function createInput(context, input, toolCallId) {
|
|
1390
|
+
return {
|
|
1391
|
+
content: requireMemoryContent(input.content),
|
|
1392
|
+
idempotencyKey: `tool:${sourceIdempotencyKey(context)}:${toolCallId}`,
|
|
1393
|
+
...input.expiresAtMs !== void 0 ? { expiresAtMs: input.expiresAtMs } : {}
|
|
1394
|
+
};
|
|
1395
|
+
}
|
|
1396
|
+
function compactMemory(memory) {
|
|
1397
|
+
return {
|
|
1398
|
+
id: memory.id,
|
|
1399
|
+
content: memory.content,
|
|
1400
|
+
createdAtMs: memory.createdAtMs,
|
|
1401
|
+
...memory.expiresAtMs !== void 0 ? { expiresAtMs: memory.expiresAtMs } : {}
|
|
1402
|
+
};
|
|
1403
|
+
}
|
|
1404
|
+
function createMemoryCreateTool(context) {
|
|
1405
|
+
return {
|
|
1406
|
+
description: "Explicit memory-write tool. Use only when the latest user message directly asks Junior to remember, store, save, or forget-and-replace a public/shareable fact. Do not use for ordinary statements like 'I prefer X', 'I use Y', or 'X goes before Y' unless the user also asks you to remember/store/save it; passive memory learning handles those after the visible reply. Pass one self-contained natural-language candidate preserving the user's explicit memory intent. Do not ask the user to rephrase ordinary first-person facts, and do not rewrite them into display-name or third-person wording. Do not include secrets, private personal details, medical/legal/financial/sensitive facts, or another person's personal preference, opinion, habit, identity, relationship, workflow, or private life. Runtime context derives actor, scope, source, and subject ids; the memory agent decides the canonical stored content, subject, and target.",
|
|
1407
|
+
executionMode: "sequential",
|
|
1408
|
+
inputSchema: createMemoryInputSchema2,
|
|
1409
|
+
execute: async (input, options) => {
|
|
1410
|
+
const parsedInput = parseToolInput(
|
|
1411
|
+
createMemoryInputSchema2,
|
|
1412
|
+
input
|
|
1413
|
+
);
|
|
1414
|
+
const toolCallId = requireToolCallId(options.toolCallId);
|
|
1415
|
+
const requestedExpiresAtMs = parseExpiresAt(parsedInput.expires_at);
|
|
1416
|
+
const runtimeContext = memoryRuntimeContext(context);
|
|
1417
|
+
const store = memoryStore(context);
|
|
1418
|
+
const review = await (async () => {
|
|
1419
|
+
try {
|
|
1420
|
+
return parseMemoryReview(
|
|
1421
|
+
await context.agent.reviewCreateRequest(
|
|
1422
|
+
parseCreateMemoryRequest({
|
|
1423
|
+
content: requireMemoryContent(parsedInput.content),
|
|
1424
|
+
...requestedExpiresAtMs !== void 0 ? { expiresAtMs: requestedExpiresAtMs } : {},
|
|
1425
|
+
runtimeContext,
|
|
1426
|
+
...context.userText?.trim() ? {
|
|
1427
|
+
sourceContext: {
|
|
1428
|
+
currentUserText: context.userText.trim()
|
|
1429
|
+
}
|
|
1430
|
+
} : {}
|
|
1431
|
+
})
|
|
1432
|
+
)
|
|
1433
|
+
);
|
|
1434
|
+
} catch (error) {
|
|
1435
|
+
if (error instanceof PluginToolInputError) {
|
|
1436
|
+
throw error;
|
|
1437
|
+
}
|
|
1438
|
+
const detail = error instanceof Error && error.message.trim() ? `: ${error.message}` : "";
|
|
1439
|
+
throw new PluginToolInputError(
|
|
1440
|
+
`Memory agent review failed${detail}`,
|
|
1441
|
+
{ cause: error }
|
|
1442
|
+
);
|
|
1443
|
+
}
|
|
1444
|
+
})();
|
|
1445
|
+
if (review.decision === "reject") {
|
|
1446
|
+
throw new PluginToolInputError(
|
|
1447
|
+
`Memory was not stored: ${review.reason}`
|
|
1448
|
+
);
|
|
1449
|
+
}
|
|
1450
|
+
const memoryInput = createInput(
|
|
1451
|
+
context,
|
|
1452
|
+
{
|
|
1453
|
+
content: review.content,
|
|
1454
|
+
...review.expiresAtMs !== void 0 ? { expiresAtMs: review.expiresAtMs } : requestedExpiresAtMs !== void 0 ? { expiresAtMs: requestedExpiresAtMs } : {}
|
|
1455
|
+
},
|
|
1456
|
+
toolCallId
|
|
1457
|
+
);
|
|
1458
|
+
const result = await (async () => {
|
|
1459
|
+
try {
|
|
1460
|
+
if (review.target === "conversation") {
|
|
1461
|
+
return await store.createConversationMemory(memoryInput);
|
|
1462
|
+
}
|
|
1463
|
+
return await store.createMemory(memoryInput);
|
|
1464
|
+
} catch (error) {
|
|
1465
|
+
asToolInputError(error);
|
|
1466
|
+
}
|
|
1467
|
+
})();
|
|
1468
|
+
return {
|
|
1469
|
+
ok: true,
|
|
1470
|
+
created: result.created,
|
|
1471
|
+
memory: compactMemory(result.memory)
|
|
1472
|
+
};
|
|
1473
|
+
}
|
|
1474
|
+
};
|
|
1475
|
+
}
|
|
1476
|
+
function createMemoryRemoveTool(context) {
|
|
1477
|
+
return {
|
|
1478
|
+
description: "Forget one memory visible in the active context. Use only ids or short id prefixes returned by listMemories or searchMemories. Never remove memories by hidden actor, Slack, scope, or subject identifiers.",
|
|
1479
|
+
executionMode: "sequential",
|
|
1480
|
+
inputSchema: removeMemoryInputSchema,
|
|
1481
|
+
execute: async (input) => {
|
|
1482
|
+
const parsedInput = parseToolInput(
|
|
1483
|
+
removeMemoryInputSchema,
|
|
1484
|
+
input
|
|
1485
|
+
);
|
|
1486
|
+
const memory = await (async () => {
|
|
1487
|
+
try {
|
|
1488
|
+
return await memoryStore(context).archiveMemory({
|
|
1489
|
+
id: parsedInput.id,
|
|
1490
|
+
reason: "tool_removed"
|
|
1491
|
+
});
|
|
1492
|
+
} catch (error) {
|
|
1493
|
+
asToolInputError(error);
|
|
1494
|
+
}
|
|
1495
|
+
})();
|
|
1496
|
+
return {
|
|
1497
|
+
ok: true,
|
|
1498
|
+
memory: compactMemory(memory)
|
|
1499
|
+
};
|
|
1500
|
+
}
|
|
1501
|
+
};
|
|
1502
|
+
}
|
|
1503
|
+
function createMemoryListTool(context) {
|
|
1504
|
+
return {
|
|
1505
|
+
description: "List active memories visible in the current context. Use when the user asks what Junior remembers or when memory ids are needed before removing a memory.",
|
|
1506
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
1507
|
+
inputSchema: listMemoriesInputSchema2,
|
|
1508
|
+
execute: async (input) => {
|
|
1509
|
+
const parsedInput = parseToolInput(
|
|
1510
|
+
listMemoriesInputSchema2,
|
|
1511
|
+
input
|
|
1512
|
+
);
|
|
1513
|
+
const memories = await memoryStore(context).listMemories({
|
|
1514
|
+
limit: boundedLimit2(parsedInput.limit, DEFAULT_RESULT_LIMIT)
|
|
1515
|
+
});
|
|
1516
|
+
return {
|
|
1517
|
+
ok: true,
|
|
1518
|
+
memories: memories.map(compactMemory)
|
|
1519
|
+
};
|
|
1520
|
+
}
|
|
1521
|
+
};
|
|
1522
|
+
}
|
|
1523
|
+
function createMemorySearchTool(context) {
|
|
1524
|
+
return {
|
|
1525
|
+
description: "Search active memories visible in the current context. Use when the model needs targeted memory recall. The tool searches only the current requester and active conversation scopes.",
|
|
1526
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
1527
|
+
inputSchema: searchMemoriesInputSchema2,
|
|
1528
|
+
execute: async (input) => {
|
|
1529
|
+
const parsedInput = parseToolInput(
|
|
1530
|
+
searchMemoriesInputSchema2,
|
|
1531
|
+
input
|
|
1532
|
+
);
|
|
1533
|
+
const memories = await memoryStore(context).searchMemories({
|
|
1534
|
+
query: parsedInput.query,
|
|
1535
|
+
limit: boundedLimit2(parsedInput.limit, DEFAULT_SEARCH_LIMIT2)
|
|
1536
|
+
});
|
|
1537
|
+
return {
|
|
1538
|
+
ok: true,
|
|
1539
|
+
memories: memories.map(compactMemory)
|
|
1540
|
+
};
|
|
1541
|
+
}
|
|
1542
|
+
};
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
// src/process-session.ts
|
|
1546
|
+
import { createHash as createHash2 } from "crypto";
|
|
1547
|
+
import {
|
|
1548
|
+
getSourceKey as getSourceKey2,
|
|
1549
|
+
isPrivateSource
|
|
1550
|
+
} from "@sentry/junior-plugin-api";
|
|
1551
|
+
import { z as z4 } from "zod";
|
|
1552
|
+
var MEMORY_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
1553
|
+
"createMemory",
|
|
1554
|
+
"listMemories",
|
|
1555
|
+
"removeMemory",
|
|
1556
|
+
"searchMemories"
|
|
1557
|
+
]);
|
|
1558
|
+
var MEMORY_TASK_STATE_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
1559
|
+
var extractedMemoryCacheSchema = z4.array(
|
|
1560
|
+
z4.object({
|
|
1561
|
+
content: z4.string().min(1),
|
|
1562
|
+
expiresAtMs: z4.number().finite().nullable(),
|
|
1563
|
+
target: z4.enum(["requester", "conversation"])
|
|
1564
|
+
}).strict()
|
|
1565
|
+
);
|
|
1566
|
+
function memoryIdempotencySuffix(memory) {
|
|
1567
|
+
return createHash2("sha256").update(memory.target).update("\0").update(memory.content).update("\0").update(memory.expiresAtMs === null ? "never" : String(memory.expiresAtMs)).digest("hex").slice(0, 32);
|
|
1568
|
+
}
|
|
1569
|
+
function passiveInput(sessionId, memory, sourceKey2) {
|
|
1570
|
+
return {
|
|
1571
|
+
content: memory.content,
|
|
1572
|
+
idempotencyKey: `session:${sourceKey2}:${sessionId}:${memoryIdempotencySuffix(memory)}`,
|
|
1573
|
+
...memory.expiresAtMs !== null ? { expiresAtMs: memory.expiresAtMs } : {}
|
|
1574
|
+
};
|
|
1575
|
+
}
|
|
1576
|
+
async function getTaskMemories(context, extract) {
|
|
1577
|
+
const cacheKey = `memory-extraction:${context.id}`;
|
|
1578
|
+
const cached = await context.state.get(cacheKey);
|
|
1579
|
+
if (cached !== void 0) {
|
|
1580
|
+
return extractedMemoryCacheSchema.parse(cached);
|
|
1581
|
+
}
|
|
1582
|
+
const memories = await extract();
|
|
1583
|
+
if (memories.length > 0) {
|
|
1584
|
+
await context.state.set(cacheKey, memories, MEMORY_TASK_STATE_TTL_MS);
|
|
1585
|
+
}
|
|
1586
|
+
return memories;
|
|
1587
|
+
}
|
|
1588
|
+
async function processMemorySession(context) {
|
|
1589
|
+
const run = await context.run.load();
|
|
1590
|
+
if (run.transcript.some(
|
|
1591
|
+
(entry) => entry.type === "toolResult" && MEMORY_TOOL_NAMES.has(entry.toolName)
|
|
1592
|
+
)) {
|
|
1593
|
+
return;
|
|
1594
|
+
}
|
|
1595
|
+
if (run.source.platform !== "local" && isPrivateSource(run.source)) {
|
|
1596
|
+
return;
|
|
1597
|
+
}
|
|
1598
|
+
const sourceKey2 = getSourceKey2(run.source);
|
|
1599
|
+
if (!sourceKey2) {
|
|
1600
|
+
return;
|
|
1601
|
+
}
|
|
1602
|
+
const transcript = run.transcript.filter((entry) => entry.text?.trim()).map((entry) => ({ ...entry, text: entry.text.trim() }));
|
|
1603
|
+
const evidenceText = transcript.filter((entry) => entry.type === "toolResult" || entry.role === "user").map((entry) => entry.text).join("\n\n").trim();
|
|
1604
|
+
if (!evidenceText) {
|
|
1605
|
+
return;
|
|
1606
|
+
}
|
|
1607
|
+
const runtimeContext = memoryRuntimeContextSchema.parse({
|
|
1608
|
+
conversationId: run.conversationId,
|
|
1609
|
+
...run.requester ? { requester: run.requester } : {},
|
|
1610
|
+
source: run.source
|
|
1611
|
+
});
|
|
1612
|
+
const store = createMemoryStore(context.db, runtimeContext, {
|
|
1613
|
+
embedder: context.embedder
|
|
1614
|
+
});
|
|
1615
|
+
const memories = await getTaskMemories(context, async () => {
|
|
1616
|
+
const existingMemories = await store.searchMemories({
|
|
1617
|
+
limit: 10,
|
|
1618
|
+
query: evidenceText
|
|
1619
|
+
});
|
|
1620
|
+
const agent = createMemoryAgent(context.model);
|
|
1621
|
+
return await agent.extractSessionMemories({
|
|
1622
|
+
existingMemories: existingMemories.map((memory) => ({
|
|
1623
|
+
content: memory.content
|
|
1624
|
+
})),
|
|
1625
|
+
transcript,
|
|
1626
|
+
runtimeContext
|
|
1627
|
+
});
|
|
1628
|
+
});
|
|
1629
|
+
if (memories.length === 0) {
|
|
1630
|
+
return;
|
|
1631
|
+
}
|
|
1632
|
+
for (const memory of memories) {
|
|
1633
|
+
const input = passiveInput(run.runId, memory, sourceKey2);
|
|
1634
|
+
if (memory.target === "conversation") {
|
|
1635
|
+
await store.createConversationMemory(input);
|
|
1636
|
+
continue;
|
|
1637
|
+
}
|
|
1638
|
+
if (!run.requester) {
|
|
1639
|
+
continue;
|
|
1640
|
+
}
|
|
1641
|
+
await store.createMemory(input);
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
// src/recall.ts
|
|
1646
|
+
var DEFAULT_RECALL_LIMIT = 5;
|
|
1647
|
+
var MAX_PROMPT_CHARS = 1600;
|
|
1648
|
+
var MAX_MEMORY_LINE_CHARS = 320;
|
|
1649
|
+
function trimContent(content, maxLength) {
|
|
1650
|
+
const trimmed = content.trim();
|
|
1651
|
+
if (trimmed.length <= maxLength) {
|
|
1652
|
+
return trimmed;
|
|
1653
|
+
}
|
|
1654
|
+
return `${trimmed.slice(0, Math.max(0, maxLength - 3)).trimEnd()}...`;
|
|
1655
|
+
}
|
|
1656
|
+
function renderMemoryPrompt(memories) {
|
|
1657
|
+
const header = "Relevant memories for this request:";
|
|
1658
|
+
const footer = "Treat these as possibly stale context. Current user instructions and repository evidence take priority.";
|
|
1659
|
+
const lines = [];
|
|
1660
|
+
let totalChars = header.length + footer.length + 2;
|
|
1661
|
+
for (const memory of memories) {
|
|
1662
|
+
const line = `- ${trimContent(memory.content, MAX_MEMORY_LINE_CHARS)}`;
|
|
1663
|
+
if (totalChars + line.length + 1 > MAX_PROMPT_CHARS) {
|
|
1664
|
+
break;
|
|
1665
|
+
}
|
|
1666
|
+
lines.push(line);
|
|
1667
|
+
totalChars += line.length + 1;
|
|
1668
|
+
}
|
|
1669
|
+
if (lines.length === 0) {
|
|
1670
|
+
return void 0;
|
|
1671
|
+
}
|
|
1672
|
+
return `${header}
|
|
1673
|
+
${lines.join("\n")}
|
|
1674
|
+
|
|
1675
|
+
${footer}`;
|
|
1676
|
+
}
|
|
1677
|
+
async function createMemoryPromptMessages(context) {
|
|
1678
|
+
if (!context.text.trim()) {
|
|
1679
|
+
return void 0;
|
|
1680
|
+
}
|
|
1681
|
+
const runtimeContext = memoryRuntimeContextSchema.parse({
|
|
1682
|
+
...context.conversationId ? { conversationId: context.conversationId } : {},
|
|
1683
|
+
...context.requester ? { requester: context.requester } : {},
|
|
1684
|
+
source: context.source
|
|
1685
|
+
});
|
|
1686
|
+
const memories = await createMemoryStore(
|
|
1687
|
+
context.db,
|
|
1688
|
+
runtimeContext,
|
|
1689
|
+
context.embedder ? { embedder: context.embedder } : {}
|
|
1690
|
+
).searchMemories({
|
|
1691
|
+
query: context.text,
|
|
1692
|
+
limit: DEFAULT_RECALL_LIMIT
|
|
1693
|
+
});
|
|
1694
|
+
const text2 = renderMemoryPrompt(memories);
|
|
1695
|
+
return text2 ? [{ text: text2 }] : void 0;
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
// src/plugin.ts
|
|
1699
|
+
var MEMORY_MODEL_ENV = "AI_MEMORY_MODEL";
|
|
1700
|
+
function memoryModelId(options) {
|
|
1701
|
+
const explicitModelId = options.modelId?.trim();
|
|
1702
|
+
if (explicitModelId) {
|
|
1703
|
+
return explicitModelId;
|
|
1704
|
+
}
|
|
1705
|
+
const envModelId = process.env[MEMORY_MODEL_ENV]?.trim();
|
|
1706
|
+
return envModelId || void 0;
|
|
1707
|
+
}
|
|
1708
|
+
function memoryToolContext(ctx) {
|
|
1709
|
+
return {
|
|
1710
|
+
agent: ctx.agent,
|
|
1711
|
+
...ctx.conversationId ? { conversationId: ctx.conversationId } : {},
|
|
1712
|
+
...ctx.requester ? { requester: ctx.requester } : {},
|
|
1713
|
+
db: ctx.db,
|
|
1714
|
+
...ctx.embedder ? { embedder: ctx.embedder } : {},
|
|
1715
|
+
source: ctx.source,
|
|
1716
|
+
...ctx.userText ? { userText: ctx.userText } : {}
|
|
1717
|
+
};
|
|
1718
|
+
}
|
|
1719
|
+
function createMemoryPlugin(options = {}) {
|
|
1720
|
+
const modelId = memoryModelId(options);
|
|
1721
|
+
return defineJuniorPlugin({
|
|
1722
|
+
manifest: {
|
|
1723
|
+
name: "memory",
|
|
1724
|
+
displayName: "Memory",
|
|
1725
|
+
description: "Long-term Junior memory storage and recall"
|
|
1726
|
+
},
|
|
1727
|
+
model: modelId ? { structuredModelId: modelId } : { structuredModel: "default" },
|
|
1728
|
+
packageName: "@sentry/junior-memory",
|
|
1729
|
+
cli: {
|
|
1730
|
+
commands: [createMemoryCliCommand()]
|
|
1731
|
+
},
|
|
1732
|
+
tasks: {
|
|
1733
|
+
processSession: {
|
|
1734
|
+
async run(ctx) {
|
|
1735
|
+
await processMemorySession(ctx);
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
},
|
|
1739
|
+
hooks: {
|
|
1740
|
+
tools(ctx) {
|
|
1741
|
+
const context = memoryToolContext({
|
|
1742
|
+
...ctx,
|
|
1743
|
+
agent: createMemoryAgent(ctx.model),
|
|
1744
|
+
db: ctx.db,
|
|
1745
|
+
embedder: ctx.embedder
|
|
1746
|
+
});
|
|
1747
|
+
return {
|
|
1748
|
+
createMemory: createMemoryCreateTool(context),
|
|
1749
|
+
removeMemory: createMemoryRemoveTool(context),
|
|
1750
|
+
listMemories: createMemoryListTool(context),
|
|
1751
|
+
searchMemories: createMemorySearchTool(context)
|
|
1752
|
+
};
|
|
1753
|
+
},
|
|
1754
|
+
async userPrompt(ctx) {
|
|
1755
|
+
return await createMemoryPromptMessages({
|
|
1756
|
+
...ctx.conversationId ? { conversationId: ctx.conversationId } : {},
|
|
1757
|
+
...ctx.requester ? { requester: ctx.requester } : {},
|
|
1758
|
+
db: ctx.db,
|
|
1759
|
+
embedder: ctx.embedder,
|
|
1760
|
+
source: ctx.source,
|
|
1761
|
+
text: ctx.text
|
|
1762
|
+
});
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
});
|
|
1766
|
+
}
|
|
1767
|
+
var memoryPlugin = createMemoryPlugin();
|
|
1768
|
+
export {
|
|
1769
|
+
createMemoryPlugin,
|
|
1770
|
+
createMemoryStore,
|
|
1771
|
+
memoryPlugin
|
|
1772
|
+
};
|
|
1773
|
+
//# sourceMappingURL=index.js.map
|