opencode-graphiti 0.1.0 → 0.1.2
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/README.md +44 -27
- package/esm/src/config.d.ts +3 -0
- package/esm/src/config.d.ts.map +1 -1
- package/esm/src/config.js +21 -35
- package/esm/src/handlers/chat.d.ts +21 -0
- package/esm/src/handlers/chat.d.ts.map +1 -0
- package/esm/src/handlers/chat.js +127 -0
- package/esm/src/handlers/compacting.d.ts +15 -0
- package/esm/src/handlers/compacting.d.ts.map +1 -0
- package/esm/src/handlers/compacting.js +29 -0
- package/esm/src/handlers/event.d.ts +18 -0
- package/esm/src/handlers/event.d.ts.map +1 -0
- package/esm/src/handlers/event.js +132 -0
- package/esm/src/index.d.ts +3 -1
- package/esm/src/index.d.ts.map +1 -1
- package/esm/src/index.js +28 -419
- package/esm/src/services/client.d.ts +33 -5
- package/esm/src/services/client.d.ts.map +1 -1
- package/esm/src/services/client.js +42 -20
- package/esm/src/services/compaction.d.ts +18 -69
- package/esm/src/services/compaction.d.ts.map +1 -1
- package/esm/src/services/compaction.js +86 -112
- package/esm/src/services/context-limit.d.ts +14 -0
- package/esm/src/services/context-limit.d.ts.map +1 -0
- package/esm/src/services/context-limit.js +41 -0
- package/esm/src/services/context.d.ts +5 -0
- package/esm/src/services/context.d.ts.map +1 -1
- package/esm/src/services/context.js +19 -16
- package/esm/src/session.d.ts +90 -0
- package/esm/src/session.d.ts.map +1 -0
- package/esm/src/session.js +304 -0
- package/esm/src/types/index.d.ts +28 -9
- package/esm/src/types/index.d.ts.map +1 -1
- package/esm/src/utils.d.ts +21 -0
- package/esm/src/utils.d.ts.map +1 -0
- package/esm/src/utils.js +34 -0
- package/package.json +3 -2
- package/script/src/config.d.ts +3 -0
- package/script/src/config.d.ts.map +1 -1
- package/script/src/config.js +21 -35
- package/script/src/handlers/chat.d.ts +21 -0
- package/script/src/handlers/chat.d.ts.map +1 -0
- package/script/src/handlers/chat.js +130 -0
- package/script/src/handlers/compacting.d.ts +15 -0
- package/script/src/handlers/compacting.d.ts.map +1 -0
- package/script/src/handlers/compacting.js +32 -0
- package/script/src/handlers/event.d.ts +18 -0
- package/script/src/handlers/event.d.ts.map +1 -0
- package/script/src/handlers/event.js +135 -0
- package/script/src/index.d.ts +3 -1
- package/script/src/index.d.ts.map +1 -1
- package/script/src/index.js +30 -422
- package/script/src/services/client.d.ts +33 -5
- package/script/src/services/client.d.ts.map +1 -1
- package/script/src/services/client.js +42 -53
- package/script/src/services/compaction.d.ts +18 -69
- package/script/src/services/compaction.d.ts.map +1 -1
- package/script/src/services/compaction.js +86 -113
- package/script/src/services/context-limit.d.ts +14 -0
- package/script/src/services/context-limit.d.ts.map +1 -0
- package/script/src/services/context-limit.js +45 -0
- package/script/src/services/context.d.ts +5 -0
- package/script/src/services/context.d.ts.map +1 -1
- package/script/src/services/context.js +22 -16
- package/script/src/session.d.ts +90 -0
- package/script/src/session.d.ts.map +1 -0
- package/script/src/session.js +308 -0
- package/script/src/types/index.d.ts +28 -9
- package/script/src/types/index.d.ts.map +1 -1
- package/script/src/utils.d.ts +21 -0
- package/script/src/utils.d.ts.map +1 -0
- package/script/src/utils.js +44 -0
package/esm/src/index.js
CHANGED
|
@@ -1,22 +1,14 @@
|
|
|
1
1
|
import { loadConfig } from "./config.js";
|
|
2
|
+
import { createChatHandler } from "./handlers/chat.js";
|
|
3
|
+
import { createCompactingHandler } from "./handlers/compacting.js";
|
|
4
|
+
import { createEventHandler } from "./handlers/event.js";
|
|
2
5
|
import { GraphitiClient } from "./services/client.js";
|
|
3
|
-
import { createPreemptiveCompactionHandler, getCompactionContext, handleCompaction, } from "./services/compaction.js";
|
|
4
|
-
import { formatMemoryContext } from "./services/context.js";
|
|
5
6
|
import { logger } from "./services/logger.js";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
};
|
|
12
|
-
const isTextPart = (value) => {
|
|
13
|
-
if (!value || typeof value !== "object")
|
|
14
|
-
return false;
|
|
15
|
-
const part = value;
|
|
16
|
-
return part.type === "text" && typeof part.text === "string" &&
|
|
17
|
-
!part.synthetic;
|
|
18
|
-
};
|
|
19
|
-
const extractTextFromParts = (parts) => parts.filter(isTextPart).map((part) => part.text).join(" ").trim();
|
|
7
|
+
import { SessionManager } from "./session.js";
|
|
8
|
+
import { makeGroupId, makeUserGroupId } from "./utils.js";
|
|
9
|
+
/**
|
|
10
|
+
* OpenCode plugin entry point for Graphiti memory integration.
|
|
11
|
+
*/
|
|
20
12
|
export const graphiti = async (input) => {
|
|
21
13
|
const config = loadConfig();
|
|
22
14
|
const client = new GraphitiClient(config.endpoint);
|
|
@@ -27,410 +19,27 @@ export const graphiti = async (input) => {
|
|
|
27
19
|
logger.warn("Memory features will be unavailable until connection is established");
|
|
28
20
|
}
|
|
29
21
|
const defaultGroupId = makeGroupId(config.groupIdPrefix, input.directory);
|
|
22
|
+
const defaultUserGroupId = makeUserGroupId(config.groupIdPrefix);
|
|
30
23
|
logger.info("Plugin initialized. Group ID:", defaultGroupId);
|
|
31
|
-
const
|
|
32
|
-
const parentIdCache = new Map();
|
|
33
|
-
const pendingAssistantMessages = new Map();
|
|
34
|
-
const bufferedAssistantMessageIds = new Set();
|
|
35
|
-
const resolveParentId = async (sessionId) => {
|
|
36
|
-
if (parentIdCache.has(sessionId)) {
|
|
37
|
-
return parentIdCache.get(sessionId) ?? null;
|
|
38
|
-
}
|
|
39
|
-
try {
|
|
40
|
-
const response = await sdkClient.session.get({
|
|
41
|
-
path: { id: sessionId },
|
|
42
|
-
});
|
|
43
|
-
const sessionInfo = typeof response === "object" && response !== null &&
|
|
44
|
-
"data" in response
|
|
45
|
-
? response.data
|
|
46
|
-
: response;
|
|
47
|
-
if (!sessionInfo)
|
|
48
|
-
return undefined;
|
|
49
|
-
const parentId = sessionInfo.parentID ?? null;
|
|
50
|
-
parentIdCache.set(sessionId, parentId);
|
|
51
|
-
return parentId;
|
|
52
|
-
}
|
|
53
|
-
catch (err) {
|
|
54
|
-
logger.debug("Failed to resolve session parentID", { sessionId, err });
|
|
55
|
-
return undefined;
|
|
56
|
-
}
|
|
57
|
-
};
|
|
58
|
-
const resolveSessionState = async (sessionId) => {
|
|
59
|
-
const parentId = await resolveParentId(sessionId);
|
|
60
|
-
if (parentId === undefined)
|
|
61
|
-
return { state: null, resolved: false };
|
|
62
|
-
if (parentId) {
|
|
63
|
-
sessions.delete(sessionId);
|
|
64
|
-
return { state: null, resolved: true };
|
|
65
|
-
}
|
|
66
|
-
let state = sessions.get(sessionId);
|
|
67
|
-
if (!state) {
|
|
68
|
-
state = {
|
|
69
|
-
groupId: defaultGroupId,
|
|
70
|
-
injectedMemories: false,
|
|
71
|
-
messageCount: 0,
|
|
72
|
-
pendingMessages: [],
|
|
73
|
-
isMain: true,
|
|
74
|
-
};
|
|
75
|
-
sessions.set(sessionId, state);
|
|
76
|
-
}
|
|
77
|
-
return { state, resolved: true };
|
|
78
|
-
};
|
|
79
|
-
const isSubagentSession = async (sessionId) => {
|
|
80
|
-
const parentId = await resolveParentId(sessionId);
|
|
81
|
-
return !!parentId;
|
|
82
|
-
};
|
|
83
|
-
const fetchLatestAssistantMessage = async (sessionId) => {
|
|
84
|
-
try {
|
|
85
|
-
const response = await sdkClient.session.messages({
|
|
86
|
-
sessionID: sessionId,
|
|
87
|
-
limit: 20,
|
|
88
|
-
});
|
|
89
|
-
const payload = response && typeof response === "object" &&
|
|
90
|
-
"data" in response
|
|
91
|
-
? response.data
|
|
92
|
-
: response;
|
|
93
|
-
const messages = Array.isArray(payload)
|
|
94
|
-
? payload
|
|
95
|
-
: [];
|
|
96
|
-
if (messages.length === 0)
|
|
97
|
-
return null;
|
|
98
|
-
const lastAssistant = [...messages]
|
|
99
|
-
.reverse()
|
|
100
|
-
.find((message) => message.info?.role === "assistant");
|
|
101
|
-
if (!lastAssistant)
|
|
102
|
-
return null;
|
|
103
|
-
const text = extractTextFromParts(lastAssistant.parts);
|
|
104
|
-
if (!text)
|
|
105
|
-
return null;
|
|
106
|
-
return { id: lastAssistant.info?.id, text };
|
|
107
|
-
}
|
|
108
|
-
catch (err) {
|
|
109
|
-
logger.debug("Failed to list session messages for fallback", {
|
|
110
|
-
sessionId,
|
|
111
|
-
err,
|
|
112
|
-
});
|
|
113
|
-
return null;
|
|
114
|
-
}
|
|
115
|
-
};
|
|
116
|
-
const finalizeAssistantMessage = (state, sessionId, messageId, source) => {
|
|
117
|
-
const key = `${sessionId}:${messageId}`;
|
|
118
|
-
if (bufferedAssistantMessageIds.has(key))
|
|
119
|
-
return;
|
|
120
|
-
const buffered = pendingAssistantMessages.get(key);
|
|
121
|
-
pendingAssistantMessages.delete(key);
|
|
122
|
-
bufferedAssistantMessageIds.add(key);
|
|
123
|
-
const messageText = buffered?.text?.trim() ?? "";
|
|
124
|
-
const messagePreview = messageText.slice(0, 120);
|
|
125
|
-
logger.info("Assistant message completed", {
|
|
126
|
-
hook: source,
|
|
127
|
-
sessionId,
|
|
128
|
-
messageID: messageId,
|
|
129
|
-
source,
|
|
130
|
-
messageLength: messageText.length,
|
|
131
|
-
preview: messagePreview,
|
|
132
|
-
});
|
|
133
|
-
if (!messageText) {
|
|
134
|
-
logger.debug("Assistant message completed without buffered text", {
|
|
135
|
-
hook: source,
|
|
136
|
-
sessionId,
|
|
137
|
-
messageID: messageId,
|
|
138
|
-
source,
|
|
139
|
-
});
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
state.pendingMessages.push(`Assistant: ${messageText}`);
|
|
143
|
-
logger.info("Buffered assistant reply", {
|
|
144
|
-
hook: source,
|
|
145
|
-
sessionId,
|
|
146
|
-
messageID: messageId,
|
|
147
|
-
source,
|
|
148
|
-
messageLength: messageText.length,
|
|
149
|
-
preview: messagePreview,
|
|
150
|
-
});
|
|
151
|
-
};
|
|
152
|
-
const flushPendingMessages = async (sessionId, sourceDescription, minBytes) => {
|
|
153
|
-
const state = sessions.get(sessionId);
|
|
154
|
-
if (!state || state.pendingMessages.length === 0)
|
|
155
|
-
return;
|
|
156
|
-
const lastMessage = state.pendingMessages.at(-1);
|
|
157
|
-
if (lastMessage) {
|
|
158
|
-
const separatorIndex = lastMessage.indexOf(":");
|
|
159
|
-
const role = separatorIndex === -1
|
|
160
|
-
? lastMessage.trim().toLowerCase()
|
|
161
|
-
: lastMessage.slice(0, separatorIndex).trim().toLowerCase();
|
|
162
|
-
if (role === "user") {
|
|
163
|
-
const fallback = await fetchLatestAssistantMessage(sessionId);
|
|
164
|
-
if (fallback?.text) {
|
|
165
|
-
const fallbackKey = fallback.id
|
|
166
|
-
? `${sessionId}:${fallback.id}`
|
|
167
|
-
: undefined;
|
|
168
|
-
const alreadyBuffered = fallbackKey
|
|
169
|
-
? bufferedAssistantMessageIds.has(fallbackKey)
|
|
170
|
-
: state.pendingMessages.some((message) => message.startsWith("Assistant:") &&
|
|
171
|
-
message.includes(fallback.text));
|
|
172
|
-
if (!alreadyBuffered) {
|
|
173
|
-
state.pendingMessages.push(`Assistant: ${fallback.text}`);
|
|
174
|
-
if (fallbackKey) {
|
|
175
|
-
bufferedAssistantMessageIds.add(fallbackKey);
|
|
176
|
-
}
|
|
177
|
-
logger.info("Fallback assistant fetch used", {
|
|
178
|
-
sessionId,
|
|
179
|
-
messageID: fallback.id,
|
|
180
|
-
messageLength: fallback.text.length,
|
|
181
|
-
});
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
const combined = state.pendingMessages.join("\n\n");
|
|
187
|
-
if (combined.length < minBytes)
|
|
188
|
-
return;
|
|
189
|
-
const messagesToFlush = [...state.pendingMessages];
|
|
190
|
-
state.pendingMessages = [];
|
|
191
|
-
const messageLines = messagesToFlush.map((message) => {
|
|
192
|
-
const separatorIndex = message.indexOf(":");
|
|
193
|
-
const role = separatorIndex === -1
|
|
194
|
-
? "Unknown"
|
|
195
|
-
: message.slice(0, separatorIndex).trim();
|
|
196
|
-
const text = separatorIndex === -1
|
|
197
|
-
? message
|
|
198
|
-
: message.slice(separatorIndex + 1).trim();
|
|
199
|
-
return `${role}: ${text}`;
|
|
200
|
-
});
|
|
201
|
-
try {
|
|
202
|
-
const name = combined.slice(0, 80).replace(/\n/g, " ");
|
|
203
|
-
logger.info(`Flushing ${messagesToFlush.length} buffered message(s).`);
|
|
204
|
-
logger.info(`Buffered message contents:\n${messageLines.join("\n")}`, { sessionId });
|
|
205
|
-
await client.addEpisode({
|
|
206
|
-
name: `Buffered messages: ${name}`,
|
|
207
|
-
episodeBody: combined,
|
|
208
|
-
groupId: state.groupId,
|
|
209
|
-
source: "text",
|
|
210
|
-
sourceDescription,
|
|
211
|
-
});
|
|
212
|
-
logger.info("Flushed buffered messages to Graphiti");
|
|
213
|
-
}
|
|
214
|
-
catch (err) {
|
|
215
|
-
logger.error(`Failed to flush messages for ${sessionId}:`, err);
|
|
216
|
-
const currentState = sessions.get(sessionId);
|
|
217
|
-
if (currentState) {
|
|
218
|
-
currentState.pendingMessages = [
|
|
219
|
-
...messagesToFlush,
|
|
220
|
-
...currentState.pendingMessages,
|
|
221
|
-
];
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
};
|
|
225
|
-
const preemptiveCompaction = createPreemptiveCompactionHandler({
|
|
226
|
-
compactionThreshold: config.compactionThreshold,
|
|
227
|
-
minTokensForCompaction: config.minTokensForCompaction,
|
|
228
|
-
compactionCooldownMs: config.compactionCooldownMs,
|
|
229
|
-
autoResumeAfterCompaction: config.autoResumeAfterCompaction,
|
|
230
|
-
}, {
|
|
231
|
-
sdkClient: sdkClient,
|
|
232
|
-
directory: input.directory,
|
|
233
|
-
});
|
|
24
|
+
const sessionManager = new SessionManager(defaultGroupId, defaultUserGroupId, sdkClient, client);
|
|
234
25
|
return {
|
|
235
|
-
event:
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
pendingMessages: [],
|
|
254
|
-
isMain,
|
|
255
|
-
});
|
|
256
|
-
}
|
|
257
|
-
else {
|
|
258
|
-
logger.debug("Ignoring subagent session:", sessionId);
|
|
259
|
-
}
|
|
260
|
-
return;
|
|
261
|
-
}
|
|
262
|
-
if (event.type === "session.compacted") {
|
|
263
|
-
const sessionId = event.properties.sessionID;
|
|
264
|
-
const { state, resolved } = await resolveSessionState(sessionId);
|
|
265
|
-
if (!resolved) {
|
|
266
|
-
logger.debug("Unable to resolve session compaction:", sessionId);
|
|
267
|
-
return;
|
|
268
|
-
}
|
|
269
|
-
if (!state?.isMain) {
|
|
270
|
-
logger.debug("Ignoring non-main compaction:", sessionId);
|
|
271
|
-
return;
|
|
272
|
-
}
|
|
273
|
-
const summary = event.properties.summary ||
|
|
274
|
-
"";
|
|
275
|
-
await flushPendingMessages(sessionId, "Buffered messages flushed before compaction", 0);
|
|
276
|
-
if (summary) {
|
|
277
|
-
await handleCompaction({
|
|
278
|
-
client,
|
|
279
|
-
config,
|
|
280
|
-
groupId: state.groupId,
|
|
281
|
-
summary,
|
|
282
|
-
sessionId,
|
|
283
|
-
});
|
|
284
|
-
}
|
|
285
|
-
return;
|
|
286
|
-
}
|
|
287
|
-
if (event.type === "session.idle") {
|
|
288
|
-
const sessionId = event.properties.sessionID;
|
|
289
|
-
const { state, resolved } = await resolveSessionState(sessionId);
|
|
290
|
-
if (!resolved) {
|
|
291
|
-
logger.debug("Unable to resolve idle session:", sessionId);
|
|
292
|
-
return;
|
|
293
|
-
}
|
|
294
|
-
if (!state?.isMain) {
|
|
295
|
-
logger.debug("Ignoring non-main idle session:", sessionId);
|
|
296
|
-
return;
|
|
297
|
-
}
|
|
298
|
-
await flushPendingMessages(sessionId, "Buffered messages from OpenCode session", 50);
|
|
299
|
-
return;
|
|
300
|
-
}
|
|
301
|
-
if (event.type === "message.updated") {
|
|
302
|
-
const info = event.properties.info;
|
|
303
|
-
const sessionId = info.sessionID;
|
|
304
|
-
logger.info("Message event fired", {
|
|
305
|
-
hook: "message.updated",
|
|
306
|
-
eventType: "message.updated",
|
|
307
|
-
sessionId,
|
|
308
|
-
role: info.role,
|
|
309
|
-
messageID: info.id,
|
|
310
|
-
});
|
|
311
|
-
const { state, resolved } = await resolveSessionState(sessionId);
|
|
312
|
-
if (!resolved) {
|
|
313
|
-
logger.debug("Unable to resolve session for message update:", {
|
|
314
|
-
sessionId,
|
|
315
|
-
messageID: info.id,
|
|
316
|
-
role: info.role,
|
|
317
|
-
});
|
|
318
|
-
return;
|
|
319
|
-
}
|
|
320
|
-
if (!state?.isMain) {
|
|
321
|
-
logger.debug("Ignoring non-main message update:", sessionId);
|
|
322
|
-
return;
|
|
323
|
-
}
|
|
324
|
-
if (info.role !== "assistant") {
|
|
325
|
-
pendingAssistantMessages.delete(`${sessionId}:${info.id}`);
|
|
326
|
-
return;
|
|
327
|
-
}
|
|
328
|
-
const key = `${sessionId}:${info.id}`;
|
|
329
|
-
const time = info.time;
|
|
330
|
-
if (!time?.completed)
|
|
331
|
-
return;
|
|
332
|
-
if (bufferedAssistantMessageIds.has(key))
|
|
333
|
-
return;
|
|
334
|
-
finalizeAssistantMessage(state, sessionId, info.id, "message.updated");
|
|
335
|
-
if (info.tokens && info.providerID && info.modelID) {
|
|
336
|
-
preemptiveCompaction
|
|
337
|
-
.checkAndTriggerCompaction(sessionId, info.tokens, info.providerID, info.modelID)
|
|
338
|
-
.catch((err) => logger.error("Preemptive compaction check failed", err));
|
|
339
|
-
}
|
|
340
|
-
return;
|
|
341
|
-
}
|
|
342
|
-
if (event.type === "message.part.updated") {
|
|
343
|
-
const part = event.properties.part;
|
|
344
|
-
if (part.type !== "text" || part.synthetic)
|
|
345
|
-
return;
|
|
346
|
-
const sessionId = part.sessionID;
|
|
347
|
-
const messageId = part.messageID;
|
|
348
|
-
const key = `${sessionId}:${messageId}`;
|
|
349
|
-
pendingAssistantMessages.set(key, {
|
|
350
|
-
sessionId,
|
|
351
|
-
text: part.text,
|
|
352
|
-
});
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
catch (err) {
|
|
356
|
-
logger.error("Event handler error", { type: event.type, err });
|
|
357
|
-
}
|
|
358
|
-
},
|
|
359
|
-
"chat.message": async ({ sessionID }, output) => {
|
|
360
|
-
if (await isSubagentSession(sessionID)) {
|
|
361
|
-
logger.debug("Ignoring subagent chat message:", sessionID);
|
|
362
|
-
return;
|
|
363
|
-
}
|
|
364
|
-
const { state, resolved } = await resolveSessionState(sessionID);
|
|
365
|
-
if (!resolved) {
|
|
366
|
-
output.allow_buffering = true;
|
|
367
|
-
logger.debug("Unable to resolve session for message:", { sessionID });
|
|
368
|
-
return;
|
|
369
|
-
}
|
|
370
|
-
if (!state?.isMain) {
|
|
371
|
-
logger.debug("Ignoring subagent chat message:", sessionID);
|
|
372
|
-
return;
|
|
373
|
-
}
|
|
374
|
-
state.messageCount++;
|
|
375
|
-
const messageText = extractTextFromParts(output.parts);
|
|
376
|
-
if (!messageText)
|
|
377
|
-
return;
|
|
378
|
-
state.pendingMessages.push(`User: ${messageText}`);
|
|
379
|
-
logger.info("Buffered user message", {
|
|
380
|
-
hook: "chat.message",
|
|
381
|
-
sessionID,
|
|
382
|
-
messageLength: messageText.length,
|
|
383
|
-
});
|
|
384
|
-
if (!state.injectedMemories && config.injectOnFirstMessage) {
|
|
385
|
-
state.injectedMemories = true;
|
|
386
|
-
try {
|
|
387
|
-
const [facts, nodes] = await Promise.all([
|
|
388
|
-
client.searchFacts({
|
|
389
|
-
query: messageText,
|
|
390
|
-
groupIds: [state.groupId],
|
|
391
|
-
maxFacts: config.maxFacts,
|
|
392
|
-
}),
|
|
393
|
-
client.searchNodes({
|
|
394
|
-
query: messageText,
|
|
395
|
-
groupIds: [state.groupId],
|
|
396
|
-
maxNodes: config.maxNodes,
|
|
397
|
-
}),
|
|
398
|
-
]);
|
|
399
|
-
const memoryContext = formatMemoryContext(facts, nodes);
|
|
400
|
-
if (memoryContext) {
|
|
401
|
-
output.parts.unshift({
|
|
402
|
-
type: "text",
|
|
403
|
-
text: memoryContext,
|
|
404
|
-
id: `graphiti-memory-${Date.now()}`,
|
|
405
|
-
sessionID: output.message.sessionID,
|
|
406
|
-
messageID: output.message.id,
|
|
407
|
-
synthetic: true,
|
|
408
|
-
});
|
|
409
|
-
logger.info(`Injected ${facts.length} facts and ${nodes.length} nodes`);
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
catch (err) {
|
|
413
|
-
logger.error("Failed to inject memories:", err);
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
},
|
|
417
|
-
"experimental.session.compacting": async ({ sessionID }, output) => {
|
|
418
|
-
const state = sessions.get(sessionID);
|
|
419
|
-
if (!state?.isMain) {
|
|
420
|
-
logger.debug("Ignoring non-main compaction context:", sessionID);
|
|
421
|
-
return;
|
|
422
|
-
}
|
|
423
|
-
const groupId = state.groupId || defaultGroupId;
|
|
424
|
-
const additionalContext = await getCompactionContext({
|
|
425
|
-
client,
|
|
426
|
-
config,
|
|
427
|
-
groupId,
|
|
428
|
-
contextStrings: output.context,
|
|
429
|
-
});
|
|
430
|
-
if (additionalContext.length > 0) {
|
|
431
|
-
output.context.push(...additionalContext);
|
|
432
|
-
logger.info("Injected persistent knowledge into compaction context");
|
|
433
|
-
}
|
|
434
|
-
},
|
|
26
|
+
event: createEventHandler({
|
|
27
|
+
sessionManager,
|
|
28
|
+
client,
|
|
29
|
+
defaultGroupId,
|
|
30
|
+
sdkClient: sdkClient,
|
|
31
|
+
directory: input.directory,
|
|
32
|
+
groupIdPrefix: config.groupIdPrefix,
|
|
33
|
+
}),
|
|
34
|
+
"chat.message": createChatHandler({
|
|
35
|
+
sessionManager,
|
|
36
|
+
injectionInterval: config.injectionInterval,
|
|
37
|
+
client,
|
|
38
|
+
}),
|
|
39
|
+
"experimental.session.compacting": createCompactingHandler({
|
|
40
|
+
sessionManager,
|
|
41
|
+
client,
|
|
42
|
+
defaultGroupId,
|
|
43
|
+
}),
|
|
435
44
|
};
|
|
436
45
|
};
|
|
@@ -1,16 +1,39 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { GraphitiFact, GraphitiNode } from "../types/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Graphiti MCP client wrapper for connecting, querying,
|
|
4
|
+
* and persisting episodes with basic reconnection handling.
|
|
5
|
+
*/
|
|
2
6
|
export declare class GraphitiClient {
|
|
3
7
|
private client;
|
|
4
8
|
private transport;
|
|
5
9
|
private connected;
|
|
6
10
|
private endpoint;
|
|
11
|
+
/**
|
|
12
|
+
* Create a Graphiti client bound to the given MCP endpoint URL.
|
|
13
|
+
*/
|
|
7
14
|
constructor(endpoint: string);
|
|
15
|
+
/** Create a fresh MCP Client and Transport pair. */
|
|
16
|
+
private createClientAndTransport;
|
|
17
|
+
/**
|
|
18
|
+
* Establish a connection to the Graphiti MCP server.
|
|
19
|
+
* Creates a fresh Client/Transport if a previous attempt failed.
|
|
20
|
+
*/
|
|
8
21
|
connect(): Promise<boolean>;
|
|
22
|
+
/**
|
|
23
|
+
* Close the underlying MCP client connection.
|
|
24
|
+
*/
|
|
9
25
|
disconnect(): Promise<void>;
|
|
10
26
|
private callTool;
|
|
11
27
|
private isSessionExpired;
|
|
12
28
|
private reconnect;
|
|
29
|
+
/**
|
|
30
|
+
* Parse MCP tool results into JSON when possible.
|
|
31
|
+
* Public for testing.
|
|
32
|
+
*/
|
|
13
33
|
parseToolResult(result: unknown): unknown;
|
|
34
|
+
/**
|
|
35
|
+
* Add an episode to Graphiti memory.
|
|
36
|
+
*/
|
|
14
37
|
addEpisode(params: {
|
|
15
38
|
name: string;
|
|
16
39
|
episodeBody: string;
|
|
@@ -18,20 +41,25 @@ export declare class GraphitiClient {
|
|
|
18
41
|
source?: "text" | "json" | "message";
|
|
19
42
|
sourceDescription?: string;
|
|
20
43
|
}): Promise<void>;
|
|
44
|
+
/**
|
|
45
|
+
* Search Graphiti facts matching the provided query.
|
|
46
|
+
*/
|
|
21
47
|
searchFacts(params: {
|
|
22
48
|
query: string;
|
|
23
49
|
groupIds?: string[];
|
|
24
50
|
maxFacts?: number;
|
|
25
51
|
}): Promise<GraphitiFact[]>;
|
|
52
|
+
/**
|
|
53
|
+
* Search Graphiti nodes matching the provided query.
|
|
54
|
+
*/
|
|
26
55
|
searchNodes(params: {
|
|
27
56
|
query: string;
|
|
28
57
|
groupIds?: string[];
|
|
29
58
|
maxNodes?: number;
|
|
30
59
|
}): Promise<GraphitiNode[]>;
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}): Promise<GraphitiEpisode[]>;
|
|
60
|
+
/**
|
|
61
|
+
* Check whether the Graphiti MCP server is reachable.
|
|
62
|
+
*/
|
|
35
63
|
getStatus(): Promise<boolean>;
|
|
36
64
|
}
|
|
37
65
|
//# sourceMappingURL=client.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../src/src/services/client.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../src/src/services/client.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,YAAY,EAEZ,YAAY,EAEb,MAAM,mBAAmB,CAAC;AAG3B;;;GAGG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,SAAS,CAAgC;IACjD,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,QAAQ,CAAS;IAEzB;;OAEG;gBACS,QAAQ,EAAE,MAAM;IAM5B,oDAAoD;IACpD,OAAO,CAAC,wBAAwB;IAOhC;;;OAGG;IACG,OAAO,IAAI,OAAO,CAAC,OAAO,CAAC;IAgBjC;;OAEG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;YAOnB,QAAQ;IAkCtB,OAAO,CAAC,gBAAgB;YASV,SAAS;IAavB;;;OAGG;IACH,eAAe,CAAC,MAAM,EAAE,OAAO,GAAG,OAAO;IAyBzC;;OAEG;IACG,UAAU,CAAC,MAAM,EAAE;QACvB,IAAI,EAAE,MAAM,CAAC;QACb,WAAW,EAAE,MAAM,CAAC;QACpB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;QACrC,iBAAiB,CAAC,EAAE,MAAM,CAAC;KAC5B,GAAG,OAAO,CAAC,IAAI,CAAC;IAWjB;;OAEG;IACG,WAAW,CAAC,MAAM,EAAE;QACxB,KAAK,EAAE,MAAM,CAAC;QACd,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;QACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC;IAsB3B;;OAEG;IACG,WAAW,CAAC,MAAM,EAAE;QACxB,KAAK,EAAE,MAAM,CAAC;QACd,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;QACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC;IAsB3B;;OAEG;IACG,SAAS,IAAI,OAAO,CAAC,OAAO,CAAC;CAQpC"}
|
|
@@ -1,8 +1,14 @@
|
|
|
1
|
-
import * as dntShim from "../../_dnt.shims.js";
|
|
2
1
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
3
2
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
4
3
|
import { logger } from "./logger.js";
|
|
4
|
+
/**
|
|
5
|
+
* Graphiti MCP client wrapper for connecting, querying,
|
|
6
|
+
* and persisting episodes with basic reconnection handling.
|
|
7
|
+
*/
|
|
5
8
|
export class GraphitiClient {
|
|
9
|
+
/**
|
|
10
|
+
* Create a Graphiti client bound to the given MCP endpoint URL.
|
|
11
|
+
*/
|
|
6
12
|
constructor(endpoint) {
|
|
7
13
|
Object.defineProperty(this, "client", {
|
|
8
14
|
enumerable: true,
|
|
@@ -30,12 +36,23 @@ export class GraphitiClient {
|
|
|
30
36
|
});
|
|
31
37
|
this.endpoint = endpoint;
|
|
32
38
|
this.client = new Client({ name: "opencode-graphiti", version: "0.1.0" });
|
|
33
|
-
|
|
34
|
-
this.transport = new StreamableHTTPClientTransport(url);
|
|
39
|
+
this.transport = new StreamableHTTPClientTransport(new URL(endpoint));
|
|
35
40
|
}
|
|
41
|
+
/** Create a fresh MCP Client and Transport pair. */
|
|
42
|
+
createClientAndTransport() {
|
|
43
|
+
this.client = new Client({ name: "opencode-graphiti", version: "0.1.0" });
|
|
44
|
+
this.transport = new StreamableHTTPClientTransport(new URL(this.endpoint));
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Establish a connection to the Graphiti MCP server.
|
|
48
|
+
* Creates a fresh Client/Transport if a previous attempt failed.
|
|
49
|
+
*/
|
|
36
50
|
async connect() {
|
|
37
51
|
if (this.connected)
|
|
38
52
|
return true;
|
|
53
|
+
// If a previous connect() tainted the Client's internal state,
|
|
54
|
+
// create fresh instances so the retry starts cleanly.
|
|
55
|
+
this.createClientAndTransport();
|
|
39
56
|
try {
|
|
40
57
|
await this.client.connect(this.transport);
|
|
41
58
|
this.connected = true;
|
|
@@ -47,6 +64,9 @@ export class GraphitiClient {
|
|
|
47
64
|
return false;
|
|
48
65
|
}
|
|
49
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* Close the underlying MCP client connection.
|
|
69
|
+
*/
|
|
50
70
|
async disconnect() {
|
|
51
71
|
if (this.connected) {
|
|
52
72
|
await this.client.close();
|
|
@@ -90,17 +110,20 @@ export class GraphitiClient {
|
|
|
90
110
|
async reconnect() {
|
|
91
111
|
this.connected = false;
|
|
92
112
|
try {
|
|
93
|
-
await this.
|
|
113
|
+
await this.client.close();
|
|
94
114
|
}
|
|
95
115
|
catch {
|
|
96
|
-
// ignore
|
|
116
|
+
// ignore close errors on stale client
|
|
97
117
|
}
|
|
98
|
-
this.
|
|
118
|
+
this.createClientAndTransport();
|
|
99
119
|
await this.client.connect(this.transport);
|
|
100
120
|
this.connected = true;
|
|
101
121
|
logger.info("Reconnected to Graphiti MCP server");
|
|
102
122
|
}
|
|
103
|
-
|
|
123
|
+
/**
|
|
124
|
+
* Parse MCP tool results into JSON when possible.
|
|
125
|
+
* Public for testing.
|
|
126
|
+
*/
|
|
104
127
|
parseToolResult(result) {
|
|
105
128
|
const typedResult = result;
|
|
106
129
|
const content = typedResult.content;
|
|
@@ -124,6 +147,9 @@ export class GraphitiClient {
|
|
|
124
147
|
return text;
|
|
125
148
|
}
|
|
126
149
|
}
|
|
150
|
+
/**
|
|
151
|
+
* Add an episode to Graphiti memory.
|
|
152
|
+
*/
|
|
127
153
|
async addEpisode(params) {
|
|
128
154
|
await this.callTool("add_memory", {
|
|
129
155
|
name: params.name,
|
|
@@ -134,6 +160,9 @@ export class GraphitiClient {
|
|
|
134
160
|
});
|
|
135
161
|
logger.debug("Added episode:", params.name);
|
|
136
162
|
}
|
|
163
|
+
/**
|
|
164
|
+
* Search Graphiti facts matching the provided query.
|
|
165
|
+
*/
|
|
137
166
|
async searchFacts(params) {
|
|
138
167
|
try {
|
|
139
168
|
const result = await this.callTool("search_memory_facts", {
|
|
@@ -155,6 +184,9 @@ export class GraphitiClient {
|
|
|
155
184
|
return [];
|
|
156
185
|
}
|
|
157
186
|
}
|
|
187
|
+
/**
|
|
188
|
+
* Search Graphiti nodes matching the provided query.
|
|
189
|
+
*/
|
|
158
190
|
async searchNodes(params) {
|
|
159
191
|
try {
|
|
160
192
|
const result = await this.callTool("search_nodes", {
|
|
@@ -176,19 +208,9 @@ export class GraphitiClient {
|
|
|
176
208
|
return [];
|
|
177
209
|
}
|
|
178
210
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
group_ids: params.groupIds,
|
|
183
|
-
max_episodes: params.maxEpisodes || 10,
|
|
184
|
-
});
|
|
185
|
-
return result || [];
|
|
186
|
-
}
|
|
187
|
-
catch (err) {
|
|
188
|
-
logger.error("getEpisodes error:", err);
|
|
189
|
-
return [];
|
|
190
|
-
}
|
|
191
|
-
}
|
|
211
|
+
/**
|
|
212
|
+
* Check whether the Graphiti MCP server is reachable.
|
|
213
|
+
*/
|
|
192
214
|
async getStatus() {
|
|
193
215
|
try {
|
|
194
216
|
await this.callTool("get_status", {});
|