@agentmemory/agentmemory 0.9.19 → 0.9.21
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/.env.example +2 -0
- package/README.md +16 -5
- package/dist/.env.example +2 -0
- package/dist/cli.mjs +24 -6
- package/dist/cli.mjs.map +1 -1
- package/dist/index.mjs +417 -24
- package/dist/index.mjs.map +1 -1
- package/dist/{src-2wwYDPGA.mjs → src-D5arboxc.mjs} +415 -25
- package/dist/src-D5arboxc.mjs.map +1 -0
- package/dist/{standalone-DMLk7YxP.mjs → standalone-C7BgzzIN.mjs} +32 -8
- package/dist/standalone-C7BgzzIN.mjs.map +1 -0
- package/dist/standalone.d.mts.map +1 -1
- package/dist/standalone.mjs +31 -7
- package/dist/standalone.mjs.map +1 -1
- package/dist/{tools-registry-Dz8ssuMf.mjs → tools-registry-CRTWUFw9.mjs} +5 -2
- package/dist/tools-registry-CRTWUFw9.mjs.map +1 -0
- package/dist/viewer/index.html +57 -12
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/hooks/hooks.codex.json +6 -10
- package/plugin/hooks/hooks.json +12 -12
- package/plugin/opencode/README.md +229 -0
- package/plugin/opencode/agentmemory-capture.ts +662 -0
- package/plugin/opencode/commands/recall.md +19 -0
- package/plugin/opencode/commands/remember.md +19 -0
- package/plugin/opencode/plugin.json +12 -0
- package/dist/src-2wwYDPGA.mjs.map +0 -1
- package/dist/standalone-DMLk7YxP.mjs.map +0 -1
- package/dist/tools-registry-Dz8ssuMf.mjs.map +0 -1
|
@@ -0,0 +1,662 @@
|
|
|
1
|
+
import type { Plugin } from "@opencode-ai/plugin";
|
|
2
|
+
|
|
3
|
+
const API = process.env.AGENTMEMORY_URL || "http://localhost:3111";
|
|
4
|
+
const FILE_TOOLS = new Set(["Read", "Write", "Edit", "Glob", "Grep"]);
|
|
5
|
+
const FILE_KEYS = ["filePath", "file_path", "path", "file", "pattern"];
|
|
6
|
+
const MAX_STASHED_FILES = 20;
|
|
7
|
+
|
|
8
|
+
const DEBUG = process.env.OPENCODE_AGENTMEMORY_DEBUG === "1";
|
|
9
|
+
const SECRET = process.env.AGENTMEMORY_SECRET || "";
|
|
10
|
+
|
|
11
|
+
function authHeaders(): Record<string, string> {
|
|
12
|
+
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
|
13
|
+
if (SECRET) headers["Authorization"] = `Bearer ${SECRET}`;
|
|
14
|
+
return headers;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function post(path: string, body: Record<string, unknown>, timeoutMs = 5000): Promise<void> {
|
|
18
|
+
try {
|
|
19
|
+
await fetch(`${API}/agentmemory${path}`, {
|
|
20
|
+
method: "POST",
|
|
21
|
+
headers: authHeaders(),
|
|
22
|
+
body: JSON.stringify(body),
|
|
23
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
24
|
+
});
|
|
25
|
+
} catch (e) {
|
|
26
|
+
if (DEBUG) console.error(`[agentmemory] POST ${path} failed:`, (e as Error).message);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function postJson(path: string, body: Record<string, unknown>): Promise<unknown | null> {
|
|
31
|
+
try {
|
|
32
|
+
const res = await fetch(`${API}/agentmemory${path}`, {
|
|
33
|
+
method: "POST",
|
|
34
|
+
headers: authHeaders(),
|
|
35
|
+
body: JSON.stringify(body),
|
|
36
|
+
signal: AbortSignal.timeout(5000),
|
|
37
|
+
});
|
|
38
|
+
return res.ok ? await res.json() : null;
|
|
39
|
+
} catch (e) {
|
|
40
|
+
if (DEBUG) console.error(`[agentmemory] POST ${path} failed:`, (e as Error).message);
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function observe(
|
|
46
|
+
sessionId: string,
|
|
47
|
+
hookType: string,
|
|
48
|
+
data: Record<string, unknown>,
|
|
49
|
+
): Promise<void> {
|
|
50
|
+
await post("/observe", {
|
|
51
|
+
hookType,
|
|
52
|
+
sessionId,
|
|
53
|
+
project: projectPath,
|
|
54
|
+
cwd: projectPath,
|
|
55
|
+
timestamp: new Date().toISOString(),
|
|
56
|
+
data,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let activeSessionId: string | null = null;
|
|
61
|
+
let pendingConfig: Record<string, unknown> | null = null;
|
|
62
|
+
let projectPath: string | null = null;
|
|
63
|
+
const stashedFiles = new Map<string, Set<string>>();
|
|
64
|
+
const seenSubtaskIds = new Map<string, Set<string>>();
|
|
65
|
+
const seenToolCallIds = new Map<string, Set<string>>();
|
|
66
|
+
const contextInjectedSessions = new Set<string>();
|
|
67
|
+
|
|
68
|
+
function stashFor(sid: string): Set<string> {
|
|
69
|
+
let s = stashedFiles.get(sid);
|
|
70
|
+
if (!s) { s = new Set<string>(); stashedFiles.set(sid, s); }
|
|
71
|
+
return s;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function subtaskSetFor(sid: string): Set<string> {
|
|
75
|
+
let s = seenSubtaskIds.get(sid);
|
|
76
|
+
if (!s) { s = new Set<string>(); seenSubtaskIds.set(sid, s); }
|
|
77
|
+
return s;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function toolCallSetFor(sid: string): Set<string> {
|
|
81
|
+
let s = seenToolCallIds.get(sid);
|
|
82
|
+
if (!s) { s = new Set<string>(); seenToolCallIds.set(sid, s); }
|
|
83
|
+
return s;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function pruneSessionMaps(sid: string): void {
|
|
87
|
+
stashedFiles.delete(sid);
|
|
88
|
+
seenSubtaskIds.delete(sid);
|
|
89
|
+
seenToolCallIds.delete(sid);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function safeSlice(v: unknown, max: number): string {
|
|
93
|
+
if (typeof v === "string") return v.slice(0, max);
|
|
94
|
+
if (v == null) return "";
|
|
95
|
+
try { return JSON.stringify(v).slice(0, max); } catch { return ""; }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const AGENTMEMORY_INSTRUCTIONS = `<agentmemory-instructions>
|
|
99
|
+
You have access to agentmemory for persistent cross-session memory. Use these tools proactively.
|
|
100
|
+
|
|
101
|
+
CORE TOOLS:
|
|
102
|
+
|
|
103
|
+
memory_save — Save an insight, decision, or fact to long-term memory.
|
|
104
|
+
Required: content (text), concepts (2-5 comma-separated keywords), type (pattern/preference/architecture/bug/workflow/fact)
|
|
105
|
+
Optional: files (comma-separated paths)
|
|
106
|
+
Use when: user says "remember this", after discovering a bug, after making an architectural decision, after learning a project convention.
|
|
107
|
+
|
|
108
|
+
memory_recall — Search past observations by keywords.
|
|
109
|
+
Use when: user says "recall", "what did we do", "do you remember", or needs context from past sessions.
|
|
110
|
+
|
|
111
|
+
memory_smart_search — Hybrid semantic+keyword search with progressive disclosure.
|
|
112
|
+
Use when: you need the most relevant past context, fuzzy/conceptual searches, or recall doesn't find what you need.
|
|
113
|
+
|
|
114
|
+
memory_sessions — List recent sessions with status and observation counts.
|
|
115
|
+
Use when: user asks about session/past history, "what did we work on".
|
|
116
|
+
|
|
117
|
+
memory_file_history — Get past observations about specific files (across all sessions).
|
|
118
|
+
Use when: you're about to edit a file and want to know its history, common pitfalls, or past edits.
|
|
119
|
+
|
|
120
|
+
memory_lesson_save — Save a lesson learned (what worked, what to avoid).
|
|
121
|
+
Use when: you discover a pattern that could help future sessions avoid mistakes.
|
|
122
|
+
|
|
123
|
+
memory_lesson_recall — Search lessons by query. Returns lessons sorted by confidence.
|
|
124
|
+
Use when: before making a decision, check if past lessons apply.
|
|
125
|
+
|
|
126
|
+
memory_governance_delete — Delete specific memories. Requires explicit user confirmation.
|
|
127
|
+
Use when: user says "forget this", "delete that memory".
|
|
128
|
+
|
|
129
|
+
memory_patterns — Detect recurring patterns across sessions.
|
|
130
|
+
Use when: you want to understand project-level trends over time.
|
|
131
|
+
|
|
132
|
+
memory_consolidate — Run the 4-tier memory consolidation pipeline.
|
|
133
|
+
Use when: you want to compress and organize accumulated session observations.
|
|
134
|
+
|
|
135
|
+
All memory tools start with \`agentmemory_memory_\`. Use the exact names as they appear in your tool list. Tool results are JSON. Always check what was returned before presenting to the user.
|
|
136
|
+
</agentmemory-instructions>`;
|
|
137
|
+
|
|
138
|
+
function extractFilePaths(args: Record<string, unknown>): string[] {
|
|
139
|
+
const files: string[] = [];
|
|
140
|
+
for (const key of FILE_KEYS) {
|
|
141
|
+
const val = args[key];
|
|
142
|
+
if (typeof val === "string" && val.length > 0) {
|
|
143
|
+
files.push(val);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return files;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function extractErrorMessage(err: unknown): string {
|
|
150
|
+
if (typeof err === "string") return err;
|
|
151
|
+
if (err && typeof err === "object") {
|
|
152
|
+
const e = err as Record<string, unknown>;
|
|
153
|
+
if (typeof e.message === "string") return e.message;
|
|
154
|
+
if (e.data && typeof e.data === "object") {
|
|
155
|
+
const d = e.data as Record<string, unknown>;
|
|
156
|
+
if (typeof d.message === "string") return d.message;
|
|
157
|
+
}
|
|
158
|
+
if (typeof e.name === "string") return e.name;
|
|
159
|
+
try { return JSON.stringify(err); } catch { return ""; }
|
|
160
|
+
}
|
|
161
|
+
return String(err ?? "");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export const AgentmemoryCapturePlugin: Plugin = async (ctx) => {
|
|
165
|
+
projectPath = ctx.worktree || ctx.project?.id || process.cwd();
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
event: async ({ event }) => {
|
|
169
|
+
const type = event.type;
|
|
170
|
+
const props = (event as any).properties || {};
|
|
171
|
+
|
|
172
|
+
// ── session.created ──
|
|
173
|
+
if (type === "session.created") {
|
|
174
|
+
const info = props.info as Record<string, unknown> | undefined;
|
|
175
|
+
activeSessionId = (info?.id as string) || props.sessionID || null;
|
|
176
|
+
if (!activeSessionId) return;
|
|
177
|
+
stashedFiles.set(activeSessionId, new Set());
|
|
178
|
+
seenSubtaskIds.delete(activeSessionId);
|
|
179
|
+
seenToolCallIds.delete(activeSessionId);
|
|
180
|
+
contextInjectedSessions.delete(activeSessionId);
|
|
181
|
+
await post("/session/start", {
|
|
182
|
+
sessionId: activeSessionId,
|
|
183
|
+
title: info?.title ?? null,
|
|
184
|
+
parentID: info?.parentID ?? null,
|
|
185
|
+
version: info?.version ?? null,
|
|
186
|
+
project: projectPath,
|
|
187
|
+
cwd: projectPath,
|
|
188
|
+
});
|
|
189
|
+
if (pendingConfig && activeSessionId) {
|
|
190
|
+
await observe(activeSessionId, "config_loaded", pendingConfig);
|
|
191
|
+
pendingConfig = null;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ── session.idle ── (summarize handled in session.status idle branch)
|
|
196
|
+
|
|
197
|
+
// ── session.status ──
|
|
198
|
+
if (type === "session.status") {
|
|
199
|
+
const status = props.status as Record<string, unknown> | undefined;
|
|
200
|
+
const sid = props.sessionID || activeSessionId;
|
|
201
|
+
if (!sid || !status) return;
|
|
202
|
+
if (status.type === "idle") {
|
|
203
|
+
await post("/summarize", { sessionId: sid });
|
|
204
|
+
}
|
|
205
|
+
await observe(sid, "session_status", {
|
|
206
|
+
status_type: status.type,
|
|
207
|
+
attempt: status.attempt ?? null,
|
|
208
|
+
message: safeSlice(status.message, 2000),
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ── session.compacted ──
|
|
213
|
+
if (type === "session.compacted") {
|
|
214
|
+
const sid = props.sessionID || activeSessionId;
|
|
215
|
+
if (sid) {
|
|
216
|
+
await post("/summarize", { sessionId: sid });
|
|
217
|
+
await observe(sid, "session_compacted", {});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ── session.updated ──
|
|
222
|
+
if (type === "session.updated") {
|
|
223
|
+
const info = props.info as Record<string, unknown> | undefined;
|
|
224
|
+
const sid = (info?.id as string) || props.sessionID || activeSessionId;
|
|
225
|
+
if (!sid) return;
|
|
226
|
+
await observe(sid, "session_updated", {
|
|
227
|
+
title: info?.title ?? null,
|
|
228
|
+
parentID: info?.parentID ?? null,
|
|
229
|
+
additions: (info?.summary as any)?.additions ?? null,
|
|
230
|
+
deletions: (info?.summary as any)?.deletions ?? null,
|
|
231
|
+
files: (info?.summary as any)?.files ?? null,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ── session.diff ──
|
|
236
|
+
if (type === "session.diff") {
|
|
237
|
+
const sid = props.sessionID || activeSessionId;
|
|
238
|
+
if (!sid || !Array.isArray(props.diff)) return;
|
|
239
|
+
const diffs = props.diff as Array<Record<string, unknown>>;
|
|
240
|
+
await observe(sid, "session_diff", {
|
|
241
|
+
files: diffs.map(d => d.file),
|
|
242
|
+
additions: diffs.reduce((s, d) => s + ((d.additions as number) || 0), 0),
|
|
243
|
+
deletions: diffs.reduce((s, d) => s + ((d.deletions as number) || 0), 0),
|
|
244
|
+
diffs: diffs.slice(0, 50),
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ── session.deleted ──
|
|
249
|
+
if (type === "session.deleted") {
|
|
250
|
+
const sid = props.info?.id || props.sessionID || activeSessionId;
|
|
251
|
+
if (!sid) {
|
|
252
|
+
if (DEBUG) console.error("[agentmemory] session.deleted with no session ID");
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
await post("/session/end", { sessionId: sid });
|
|
256
|
+
post("/crystals/auto", { olderThanDays: 7 }, 30000);
|
|
257
|
+
post("/consolidate-pipeline", { tier: "all", force: true }, 30000);
|
|
258
|
+
if (sid === activeSessionId) activeSessionId = null;
|
|
259
|
+
stashedFiles.delete(sid);
|
|
260
|
+
seenSubtaskIds.delete(sid);
|
|
261
|
+
seenToolCallIds.delete(sid);
|
|
262
|
+
contextInjectedSessions.delete(sid);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// ── session.error ──
|
|
266
|
+
if (type === "session.error") {
|
|
267
|
+
const sid = props.sessionID || activeSessionId;
|
|
268
|
+
if (sid) {
|
|
269
|
+
await observe(sid, "post_tool_failure", {
|
|
270
|
+
tool_name: "session.error",
|
|
271
|
+
tool_input: "",
|
|
272
|
+
tool_output: safeSlice(props.error, 8000),
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// ── message.updated ──
|
|
278
|
+
if (type === "message.updated") {
|
|
279
|
+
const info = props.info as Record<string, unknown> | undefined;
|
|
280
|
+
if (!info) return;
|
|
281
|
+
|
|
282
|
+
if (info.role === "assistant") {
|
|
283
|
+
const sid = props.sessionID || (info.sessionID as string) || activeSessionId;
|
|
284
|
+
if (!sid) return;
|
|
285
|
+
const tokens = info.tokens as Record<string, unknown> | undefined;
|
|
286
|
+
const error = info.error ? extractErrorMessage(info.error) : null;
|
|
287
|
+
await observe(sid, "assistant_message", {
|
|
288
|
+
messageID: info.id,
|
|
289
|
+
parentID: info.parentID,
|
|
290
|
+
modelID: info.modelID,
|
|
291
|
+
providerID: info.providerID,
|
|
292
|
+
mode: info.mode,
|
|
293
|
+
cost: info.cost ?? 0,
|
|
294
|
+
tokens: {
|
|
295
|
+
input: tokens?.input ?? 0,
|
|
296
|
+
output: tokens?.output ?? 0,
|
|
297
|
+
reasoning: tokens?.reasoning ?? 0,
|
|
298
|
+
cache_read: (tokens?.cache as any)?.read ?? 0,
|
|
299
|
+
cache_write: (tokens?.cache as any)?.write ?? 0,
|
|
300
|
+
},
|
|
301
|
+
finish: info.finish ?? null,
|
|
302
|
+
error,
|
|
303
|
+
duration_ms: (info.time && typeof (info.time as any).completed === "number")
|
|
304
|
+
? (info.time as any).completed - ((info.time as any).created || 0)
|
|
305
|
+
: null,
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// ── message.removed ──
|
|
311
|
+
if (type === "message.removed") {
|
|
312
|
+
const sid = props.sessionID || activeSessionId;
|
|
313
|
+
if (sid) {
|
|
314
|
+
await observe(sid, "message_removed", {
|
|
315
|
+
messageID: props.messageID,
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// ── message.part.updated ──
|
|
321
|
+
if (type === "message.part.updated") {
|
|
322
|
+
const part = props.part as Record<string, unknown> | undefined;
|
|
323
|
+
if (!part) return;
|
|
324
|
+
const sid = (part.sessionID as string) || props.sessionID || activeSessionId;
|
|
325
|
+
if (!sid) return;
|
|
326
|
+
|
|
327
|
+
if (part.type === "subtask") {
|
|
328
|
+
const subtaskId = part.id as string;
|
|
329
|
+
if (!subtaskId) return;
|
|
330
|
+
const subtaskSet = subtaskSetFor(sid);
|
|
331
|
+
if (subtaskSet.has(subtaskId)) return;
|
|
332
|
+
subtaskSet.add(subtaskId);
|
|
333
|
+
await observe(sid, "subagent_start", {
|
|
334
|
+
subtask_id: part.id,
|
|
335
|
+
agent: part.agent,
|
|
336
|
+
prompt: safeSlice(part.prompt, 4000),
|
|
337
|
+
description: safeSlice(part.description, 2000),
|
|
338
|
+
});
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (part.type === "tool") {
|
|
343
|
+
const state = part.state as Record<string, unknown> | undefined;
|
|
344
|
+
if (!state) return;
|
|
345
|
+
const callId = part.callID as string;
|
|
346
|
+
if (!callId) return;
|
|
347
|
+
const toolName = part.tool as string;
|
|
348
|
+
|
|
349
|
+
if (state.status === "completed") {
|
|
350
|
+
const callSet = toolCallSetFor(sid);
|
|
351
|
+
if (callSet.has(callId)) return;
|
|
352
|
+
callSet.add(callId);
|
|
353
|
+
const st = state as Record<string, unknown>;
|
|
354
|
+
const rawTime = (st.time as any) || {};
|
|
355
|
+
const startTime = typeof rawTime.start === "number" ? rawTime.start : null;
|
|
356
|
+
const endTime = typeof rawTime.end === "number" ? rawTime.end : null;
|
|
357
|
+
await observe(sid, "post_tool_use", {
|
|
358
|
+
tool_name: toolName,
|
|
359
|
+
call_id: callId,
|
|
360
|
+
tool_input: safeSlice(st.input, 4000),
|
|
361
|
+
tool_output: safeSlice(st.output, 8000),
|
|
362
|
+
title: st.title ?? null,
|
|
363
|
+
metadata: st.metadata || {},
|
|
364
|
+
duration_ms: (startTime != null && endTime != null) ? endTime - startTime : null,
|
|
365
|
+
attachments: Array.isArray(st.attachments)
|
|
366
|
+
? (st.attachments as Array<Record<string, unknown>>).map(a => a.filename || a.url)
|
|
367
|
+
: [],
|
|
368
|
+
});
|
|
369
|
+
} else if (state.status === "error") {
|
|
370
|
+
const callSet = toolCallSetFor(sid);
|
|
371
|
+
if (callSet.has(callId)) return;
|
|
372
|
+
callSet.add(callId);
|
|
373
|
+
const st = state as Record<string, unknown>;
|
|
374
|
+
const rawTime = (st.time as any) || {};
|
|
375
|
+
const startTime = typeof rawTime.start === "number" ? rawTime.start : null;
|
|
376
|
+
const endTime = typeof rawTime.end === "number" ? rawTime.end : null;
|
|
377
|
+
await observe(sid, "post_tool_failure", {
|
|
378
|
+
tool_name: toolName,
|
|
379
|
+
call_id: callId,
|
|
380
|
+
tool_input: safeSlice(st.input, 4000),
|
|
381
|
+
tool_output: safeSlice(st.error, 8000),
|
|
382
|
+
duration_ms: (startTime != null && endTime != null) ? endTime - startTime : null,
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (part.type === "step-finish") {
|
|
389
|
+
await observe(sid, "step_finish", {
|
|
390
|
+
messageID: part.messageID,
|
|
391
|
+
reason: part.reason ?? null,
|
|
392
|
+
cost: (part as any).cost ?? 0,
|
|
393
|
+
input_tokens: ((part as any).tokens?.input as number) ?? 0,
|
|
394
|
+
output_tokens: ((part as any).tokens?.output as number) ?? 0,
|
|
395
|
+
reasoning_tokens: ((part as any).tokens?.reasoning as number) ?? 0,
|
|
396
|
+
});
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (part.type === "reasoning") {
|
|
401
|
+
await observe(sid, "reasoning", {
|
|
402
|
+
messageID: part.messageID,
|
|
403
|
+
text: safeSlice((part as any).text, 4000),
|
|
404
|
+
});
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (part.type === "file") {
|
|
409
|
+
const filename = (part as any).filename || (part as any).url || null;
|
|
410
|
+
if (filename) stashFor(sid).add(filename);
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (part.type === "patch") {
|
|
415
|
+
await observe(sid, "patch_applied", {
|
|
416
|
+
messageID: part.messageID,
|
|
417
|
+
hash: (part as any).hash,
|
|
418
|
+
files: (part as any).files || [],
|
|
419
|
+
});
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (part.type === "compaction") {
|
|
424
|
+
await observe(sid, "compaction_event", {
|
|
425
|
+
messageID: part.messageID,
|
|
426
|
+
auto: (part as any).auto ?? false,
|
|
427
|
+
});
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (part.type === "agent") {
|
|
432
|
+
await observe(sid, "agent_selected", {
|
|
433
|
+
messageID: part.messageID,
|
|
434
|
+
name: (part as any).name,
|
|
435
|
+
});
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (part.type === "retry") {
|
|
440
|
+
await observe(sid, "retry_attempt", {
|
|
441
|
+
messageID: part.messageID,
|
|
442
|
+
attempt: (part as any).attempt,
|
|
443
|
+
error: safeSlice((part as any).error, 2000),
|
|
444
|
+
});
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// ── file.edited ──
|
|
450
|
+
if (type === "file.edited") {
|
|
451
|
+
const sid = props.sessionID || activeSessionId;
|
|
452
|
+
if (sid && typeof props.file === "string" && props.file.length > 0) {
|
|
453
|
+
const stash = stashFor(sid);
|
|
454
|
+
stash.add(props.file);
|
|
455
|
+
if (stash.size > MAX_STASHED_FILES) {
|
|
456
|
+
const keep = [...stash].slice(-MAX_STASHED_FILES);
|
|
457
|
+
stash.clear();
|
|
458
|
+
for (const f of keep) stash.add(f);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// ── permission.updated ──
|
|
464
|
+
if (type === "permission.updated") {
|
|
465
|
+
const sid = props.sessionID || activeSessionId;
|
|
466
|
+
if (!sid) return;
|
|
467
|
+
await observe(sid, "notification", {
|
|
468
|
+
notification_type: "permission_prompt",
|
|
469
|
+
permission: props.type || "unknown",
|
|
470
|
+
pattern: Array.isArray(props.pattern)
|
|
471
|
+
? props.pattern.join(", ")
|
|
472
|
+
: (props.pattern || ""),
|
|
473
|
+
tool_call_id: props.callID || null,
|
|
474
|
+
title: props.title || props.type || "",
|
|
475
|
+
metadata: props.metadata || {},
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// ── permission.replied ──
|
|
480
|
+
if (type === "permission.replied") {
|
|
481
|
+
const sid = props.sessionID || activeSessionId;
|
|
482
|
+
if (!sid) return;
|
|
483
|
+
await observe(sid, "permission_replied", {
|
|
484
|
+
permission_id: props.permissionID || props.requestID || "",
|
|
485
|
+
response: props.response || props.reply || "",
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// ── todo.updated ──
|
|
490
|
+
if (type === "todo.updated") {
|
|
491
|
+
const sid = props.sessionID || activeSessionId;
|
|
492
|
+
const todos = Array.isArray(props.todos) ? props.todos.slice(0, 100) : [];
|
|
493
|
+
if (!sid || todos.length === 0) return;
|
|
494
|
+
const completed = todos.filter((t: any) => t.status === "completed");
|
|
495
|
+
const active = todos.filter((t: any) => t.status !== "completed");
|
|
496
|
+
await observe(sid, "task_completed", {
|
|
497
|
+
completed: completed.map((t: any) => ({ content: t.content, priority: t.priority })),
|
|
498
|
+
in_progress: active.map((t: any) => ({ content: t.content, priority: t.priority })),
|
|
499
|
+
total: todos.length,
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// ── command.executed ──
|
|
504
|
+
if (type === "command.executed") {
|
|
505
|
+
const sid = props.sessionID || activeSessionId;
|
|
506
|
+
if (sid) {
|
|
507
|
+
await observe(sid, "command_executed", {
|
|
508
|
+
name: props.name,
|
|
509
|
+
arguments: props.arguments || "",
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
},
|
|
514
|
+
|
|
515
|
+
// ── chat.message ──
|
|
516
|
+
"chat.message": async (input, output) => {
|
|
517
|
+
const sid = input.sessionID || activeSessionId;
|
|
518
|
+
if (!sid) return;
|
|
519
|
+
const parts = output.parts || [];
|
|
520
|
+
const files = parts
|
|
521
|
+
.filter((p: any) => p.type === "file")
|
|
522
|
+
.map((p: any) => p.filename || p.url)
|
|
523
|
+
.filter(Boolean);
|
|
524
|
+
for (const f of files) {
|
|
525
|
+
const stash = stashFor(sid);
|
|
526
|
+
stash.add(f);
|
|
527
|
+
if (stash.size > MAX_STASHED_FILES) {
|
|
528
|
+
const keep = [...stash].slice(-MAX_STASHED_FILES);
|
|
529
|
+
stash.clear();
|
|
530
|
+
for (const k of keep) stash.add(k);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const textParts = parts.filter((p: any) => p.type === "text" && !p.synthetic && !p.ignored);
|
|
535
|
+
const userText = textParts.map((p: any) => p.text || "").join("\n");
|
|
536
|
+
|
|
537
|
+
await observe(sid, "prompt_submit", {
|
|
538
|
+
agent: input.agent ?? null,
|
|
539
|
+
model: input.model ?? null,
|
|
540
|
+
variant: input.variant ?? null,
|
|
541
|
+
prompt: userText.slice(0, 8000),
|
|
542
|
+
files: files.slice(0, 20),
|
|
543
|
+
parts_summary: parts.map((p: any) => p.type).filter(Boolean),
|
|
544
|
+
});
|
|
545
|
+
},
|
|
546
|
+
|
|
547
|
+
// ── chat.params ──
|
|
548
|
+
"chat.params": async (input, output) => {
|
|
549
|
+
if (!input.model || !output) return;
|
|
550
|
+
const sid = input.sessionID || activeSessionId;
|
|
551
|
+
if (!sid) return;
|
|
552
|
+
await observe(sid, "llm_params", {
|
|
553
|
+
agent: input.agent,
|
|
554
|
+
model: `${input.model.providerID}/${input.model.id}`,
|
|
555
|
+
provider_url: input.model.api?.url ?? null,
|
|
556
|
+
temperature: output.temperature,
|
|
557
|
+
topP: output.topP,
|
|
558
|
+
max_output_tokens: input.model.limit?.output ?? null,
|
|
559
|
+
context_limit: input.model.limit?.context ?? null,
|
|
560
|
+
cost_1k_input: input.model.cost?.input ?? 0,
|
|
561
|
+
cost_1k_output: input.model.cost?.output ?? 0,
|
|
562
|
+
});
|
|
563
|
+
},
|
|
564
|
+
|
|
565
|
+
// ── tool.execute.before ──
|
|
566
|
+
"tool.execute.before": async (input, output) => {
|
|
567
|
+
if (!FILE_TOOLS.has(input.tool)) return;
|
|
568
|
+
const sid = input.sessionID || activeSessionId;
|
|
569
|
+
if (!sid) return;
|
|
570
|
+
const args = output.args as Record<string, unknown> | undefined;
|
|
571
|
+
if (!args) return;
|
|
572
|
+
const stash = stashFor(sid);
|
|
573
|
+
for (const fp of extractFilePaths(args)) {
|
|
574
|
+
stash.add(fp);
|
|
575
|
+
}
|
|
576
|
+
if (stash.size > MAX_STASHED_FILES) {
|
|
577
|
+
const keep = [...stash].slice(-MAX_STASHED_FILES);
|
|
578
|
+
stash.clear();
|
|
579
|
+
for (const f of keep) stash.add(f);
|
|
580
|
+
}
|
|
581
|
+
},
|
|
582
|
+
|
|
583
|
+
// ── experimental.chat.system.transform ──
|
|
584
|
+
"experimental.chat.system.transform": async (input, output) => {
|
|
585
|
+
const sid = input.sessionID || activeSessionId;
|
|
586
|
+
if (!sid) return;
|
|
587
|
+
|
|
588
|
+
if (!contextInjectedSessions.has(sid)) {
|
|
589
|
+
if (!Array.isArray(output.system)) return;
|
|
590
|
+
output.system.push(AGENTMEMORY_INSTRUCTIONS);
|
|
591
|
+
const result = await postJson("/context", {
|
|
592
|
+
sessionId: sid,
|
|
593
|
+
project: projectPath,
|
|
594
|
+
});
|
|
595
|
+
const ctx = (result as any)?.context;
|
|
596
|
+
if (typeof ctx === "string" && ctx.length > 0) {
|
|
597
|
+
output.system.push(ctx);
|
|
598
|
+
}
|
|
599
|
+
contextInjectedSessions.add(sid);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const stash = stashFor(sid);
|
|
603
|
+
if (stash.size === 0) return;
|
|
604
|
+
const files = [...stash].slice(0, 10);
|
|
605
|
+
|
|
606
|
+
const enrichResult = await postJson("/enrich", {
|
|
607
|
+
sessionId: sid,
|
|
608
|
+
files,
|
|
609
|
+
toolName: "enrich_inject",
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
const enrichCtx = (enrichResult as any)?.context;
|
|
613
|
+
if (typeof enrichCtx === "string" && enrichCtx.length > 0) {
|
|
614
|
+
if (Array.isArray(output.system)) {
|
|
615
|
+
output.system.push(enrichCtx);
|
|
616
|
+
}
|
|
617
|
+
for (const f of files) stash.delete(f);
|
|
618
|
+
}
|
|
619
|
+
},
|
|
620
|
+
|
|
621
|
+
// ── experimental.session.compacting (WIP) ──
|
|
622
|
+
"experimental.session.compacting": async (input, output) => {
|
|
623
|
+
const sid = input.sessionID || activeSessionId;
|
|
624
|
+
if (!sid) return;
|
|
625
|
+
|
|
626
|
+
const result = await postJson("/context", {
|
|
627
|
+
sessionId: sid,
|
|
628
|
+
project: projectPath,
|
|
629
|
+
});
|
|
630
|
+
const ctx = (result as any)?.context;
|
|
631
|
+
if (typeof ctx === "string" && ctx.length > 0) {
|
|
632
|
+
if (Array.isArray(output.context)) {
|
|
633
|
+
output.context.push(ctx);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
},
|
|
637
|
+
|
|
638
|
+
// ── config ──
|
|
639
|
+
config: async (input) => {
|
|
640
|
+
const payload: Record<string, unknown> = {
|
|
641
|
+
theme: input.theme ?? null,
|
|
642
|
+
model: input.model ?? null,
|
|
643
|
+
autoupdate: input.autoupdate ?? null,
|
|
644
|
+
agents: typeof input.agent === "object" && input.agent !== null && !Array.isArray(input.agent)
|
|
645
|
+
? Object.keys(input.agent as Record<string, unknown>)
|
|
646
|
+
: Array.isArray(input.agent) ? input.agent : [],
|
|
647
|
+
mcp_servers: typeof input.mcp === "object" && input.mcp !== null && !Array.isArray(input.mcp)
|
|
648
|
+
? Object.keys(input.mcp as Record<string, unknown>)
|
|
649
|
+
: Array.isArray(input.mcp) ? input.mcp : [],
|
|
650
|
+
providers: typeof input.provider === "object" && input.provider !== null && !Array.isArray(input.provider)
|
|
651
|
+
? Object.keys(input.provider as Record<string, unknown>)
|
|
652
|
+
: Array.isArray(input.provider) ? input.provider : [],
|
|
653
|
+
permission: input.permission ?? null,
|
|
654
|
+
};
|
|
655
|
+
if (activeSessionId) {
|
|
656
|
+
await observe(activeSessionId, "config_loaded", payload);
|
|
657
|
+
} else {
|
|
658
|
+
pendingConfig = payload;
|
|
659
|
+
}
|
|
660
|
+
},
|
|
661
|
+
};
|
|
662
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Search past session observations and lessons for relevant context. Wrap the `memory_smart_search` and `memory_lesson_recall` MCP tools.
|
|
2
|
+
|
|
3
|
+
## Usage
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
/recall [query]
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Instructions
|
|
10
|
+
|
|
11
|
+
1. Call `memory_smart_search` with the query and `limit: 10` (hybrid BM25 + vector + graph search).
|
|
12
|
+
2. Call `memory_lesson_recall` with the same query and `limit: 5` (lesson search).
|
|
13
|
+
3. Combine results and present to the user:
|
|
14
|
+
- Group by session
|
|
15
|
+
- Show type, title, and narrative for each observation
|
|
16
|
+
- Highlight high-importance (>= 7) observations
|
|
17
|
+
- Show lessons separately with confidence scores
|
|
18
|
+
4. If no results, suggest 2-3 alternative search terms.
|
|
19
|
+
5. **Never hallucinate results.** Only present what the MCP tools actually return.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Explicitly save an insight, decision, or learning to agentmemory for future sessions. Wraps the `memory_save` MCP tool.
|
|
2
|
+
|
|
3
|
+
## Usage
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
/remember [what to remember]
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Instructions
|
|
10
|
+
|
|
11
|
+
1. Analyze what needs to be remembered — extract the core insight, decision, or fact.
|
|
12
|
+
2. Extract 2-5 searchable concepts (lowercased keyword phrases). Prefer specific terms ("jwt-refresh-rotation" over "auth").
|
|
13
|
+
3. Extract relevant file paths the memory references.
|
|
14
|
+
4. Call `memory_save` with:
|
|
15
|
+
- `content` — full text to remember (preserve user's phrasing)
|
|
16
|
+
- `concepts` — extracted concept list
|
|
17
|
+
- `files` — extracted file list (empty array if none)
|
|
18
|
+
- `type` — choose from: pattern, preference, architecture, bug, workflow, fact
|
|
19
|
+
5. Confirm the save and show the concepts tagged so the user knows retrieval terms.
|