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/README.md
CHANGED
|
@@ -23,9 +23,12 @@ reminded of recent project context — regardless of what survived the summary.
|
|
|
23
23
|
This plugin connects to a Graphiti MCP server and:
|
|
24
24
|
|
|
25
25
|
- Injects relevant memories into the first user message of each session
|
|
26
|
-
-
|
|
26
|
+
- Periodically re-injects project memories as the conversation progresses
|
|
27
|
+
- Buffers user and assistant messages, flushing them to Graphiti on idle or
|
|
28
|
+
before compaction
|
|
27
29
|
- Preserves key facts during context compaction
|
|
28
|
-
-
|
|
30
|
+
- Saves compaction summaries as episodes so knowledge survives across boundaries
|
|
31
|
+
- Scopes memories per project (and per user) using directory-based group IDs
|
|
29
32
|
|
|
30
33
|
## Prerequisites
|
|
31
34
|
|
|
@@ -101,47 +104,61 @@ Create a config file at `~/.config/opencode/graphiti.jsonc`:
|
|
|
101
104
|
// Prefix for project group IDs (e.g. "opencode_my-project")
|
|
102
105
|
"groupIdPrefix": "opencode",
|
|
103
106
|
|
|
104
|
-
//
|
|
105
|
-
"
|
|
106
|
-
"maxNodes": 5,
|
|
107
|
-
"maxEpisodes": 5,
|
|
108
|
-
|
|
109
|
-
// Feature toggles
|
|
110
|
-
"injectOnFirstMessage": true,
|
|
111
|
-
"enableTriggerDetection": true,
|
|
112
|
-
"enableCompactionSave": true
|
|
107
|
+
// Number of user messages between memory re-injections (0 = disabled)
|
|
108
|
+
"injectionInterval": 10
|
|
113
109
|
}
|
|
114
110
|
```
|
|
115
111
|
|
|
116
|
-
All fields are optional — defaults are used for any missing
|
|
112
|
+
All fields are optional — defaults (shown above) are used for any missing
|
|
113
|
+
values.
|
|
117
114
|
|
|
118
115
|
## How It Works
|
|
119
116
|
|
|
120
117
|
### Memory Injection (`chat.message`)
|
|
121
118
|
|
|
122
119
|
On the first user message in a session, the plugin searches Graphiti for facts
|
|
123
|
-
and entities relevant to the message content.
|
|
124
|
-
|
|
120
|
+
and entities relevant to the message content. Results are split into project and
|
|
121
|
+
user scopes (70% / 30% budget split), formatted, and prepended to the
|
|
122
|
+
conversation as a synthetic context block.
|
|
123
|
+
|
|
124
|
+
The injection budget is calculated dynamically: 5% of the model's context limit
|
|
125
|
+
(resolved from the provider list) multiplied by 4 characters per token.
|
|
126
|
+
|
|
127
|
+
### Re-injection (`chat.message`)
|
|
128
|
+
|
|
129
|
+
When `injectionInterval` is greater than 0, the plugin periodically re-injects
|
|
130
|
+
project-scoped memories as the conversation progresses. After every N user
|
|
131
|
+
messages (where N is `injectionInterval`), stale synthetic memory parts are
|
|
132
|
+
replaced with fresh results. Re-injection uses the full character budget for
|
|
133
|
+
project memories only (no user scope).
|
|
125
134
|
|
|
126
|
-
###
|
|
135
|
+
### Message Buffering (`event`)
|
|
127
136
|
|
|
128
|
-
User messages are
|
|
137
|
+
User and assistant messages are buffered in memory as they arrive. The plugin
|
|
138
|
+
listens on `message.part.updated` to capture assistant text as it streams, and
|
|
139
|
+
on `message.updated` to finalize completed assistant replies. Buffered messages
|
|
140
|
+
are flushed to Graphiti as episodes:
|
|
129
141
|
|
|
130
|
-
-
|
|
131
|
-
|
|
132
|
-
-
|
|
133
|
-
|
|
142
|
+
- **On idle** (`session.idle`): when the session becomes idle with at least 50
|
|
143
|
+
bytes of buffered content.
|
|
144
|
+
- **Before compaction** (`session.compacted`): all buffered messages are flushed
|
|
145
|
+
immediately (no minimum size) so nothing is lost.
|
|
134
146
|
|
|
135
|
-
|
|
147
|
+
If the last buffered message is from the user (i.e. no assistant reply was
|
|
148
|
+
captured), the plugin fetches the latest assistant message from the session API
|
|
149
|
+
as a fallback before flushing.
|
|
136
150
|
|
|
137
|
-
### Compaction Preservation (`
|
|
151
|
+
### Compaction Preservation (`session.compacted` + `experimental.session.compacting`)
|
|
138
152
|
|
|
139
|
-
|
|
153
|
+
Compaction is handled entirely by OpenCode's native compaction mechanism. The
|
|
154
|
+
plugin participates in two ways:
|
|
140
155
|
|
|
141
|
-
1. **Before compaction
|
|
142
|
-
context
|
|
143
|
-
|
|
144
|
-
|
|
156
|
+
1. **Before compaction** (`experimental.session.compacting`): The plugin injects
|
|
157
|
+
known facts and entities into the compaction context using the same 70% / 30%
|
|
158
|
+
project/user budget split, so the summarizer preserves important knowledge.
|
|
159
|
+
2. **After compaction** (`session.compacted`): The compaction summary is saved
|
|
160
|
+
as an episode to Graphiti, ensuring knowledge survives across compaction
|
|
161
|
+
boundaries.
|
|
145
162
|
|
|
146
163
|
### Project Scoping
|
|
147
164
|
|
package/esm/src/config.d.ts
CHANGED
package/esm/src/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/src/config.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/src/config.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAcvD;;GAEG;AACH,wBAAgB,UAAU,IAAI,cAAc,CAc3C"}
|
package/esm/src/config.js
CHANGED
|
@@ -1,43 +1,29 @@
|
|
|
1
|
-
import * as dntShim from "../_dnt.shims.js";
|
|
2
1
|
import { cosmiconfigSync } from "cosmiconfig";
|
|
3
|
-
import
|
|
4
|
-
import { homedir } from "node:os";
|
|
5
|
-
import { join } from "node:path";
|
|
2
|
+
import * as z from "zod/mini";
|
|
6
3
|
const DEFAULT_CONFIG = {
|
|
7
4
|
endpoint: "http://localhost:8000/mcp",
|
|
8
5
|
groupIdPrefix: "opencode",
|
|
9
|
-
|
|
10
|
-
maxNodes: 5,
|
|
11
|
-
maxEpisodes: 5,
|
|
12
|
-
injectOnFirstMessage: true,
|
|
13
|
-
enableCompactionSave: true,
|
|
14
|
-
compactionThreshold: 0.8,
|
|
15
|
-
minTokensForCompaction: 50000,
|
|
16
|
-
compactionCooldownMs: 30000,
|
|
17
|
-
autoResumeAfterCompaction: true,
|
|
18
|
-
};
|
|
19
|
-
function parseJsonc(text) {
|
|
20
|
-
let stripped = text.replace(/(^|\s)\/\/.*$/gm, "$1");
|
|
21
|
-
stripped = stripped.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
22
|
-
return JSON.parse(stripped);
|
|
23
|
-
}
|
|
24
|
-
const createExplorer = (cwd) => {
|
|
25
|
-
return cosmiconfigSync("graphiti", { stopDir: cwd });
|
|
6
|
+
injectionInterval: 10,
|
|
26
7
|
};
|
|
8
|
+
const GraphitiConfigSchema = z.object({
|
|
9
|
+
endpoint: z.string(),
|
|
10
|
+
groupIdPrefix: z.string(),
|
|
11
|
+
injectionInterval: z.number(),
|
|
12
|
+
});
|
|
13
|
+
/**
|
|
14
|
+
* Load Graphiti configuration from JSONC files with defaults applied.
|
|
15
|
+
*/
|
|
27
16
|
export function loadConfig() {
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
catch {
|
|
39
|
-
config = null;
|
|
40
|
-
}
|
|
17
|
+
const explorer = cosmiconfigSync("graphiti", { searchStrategy: "global" });
|
|
18
|
+
const result = explorer.search();
|
|
19
|
+
const candidate = result?.config ?? {};
|
|
20
|
+
const merged = {
|
|
21
|
+
...DEFAULT_CONFIG,
|
|
22
|
+
...candidate,
|
|
23
|
+
};
|
|
24
|
+
const parsed = GraphitiConfigSchema.safeParse(merged);
|
|
25
|
+
if (parsed.success) {
|
|
26
|
+
return parsed.data;
|
|
41
27
|
}
|
|
42
|
-
return
|
|
28
|
+
return DEFAULT_CONFIG;
|
|
43
29
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Part } from "@opencode-ai/sdk";
|
|
2
|
+
import type { GraphitiClient } from "../services/client.js";
|
|
3
|
+
import type { SessionManager } from "../session.js";
|
|
4
|
+
/** Dependencies for the chat message handler. */
|
|
5
|
+
export interface ChatHandlerDeps {
|
|
6
|
+
sessionManager: SessionManager;
|
|
7
|
+
injectionInterval: number;
|
|
8
|
+
client: GraphitiClient;
|
|
9
|
+
}
|
|
10
|
+
/** Creates the `chat.message` hook handler. */
|
|
11
|
+
export declare function createChatHandler(deps: ChatHandlerDeps): ({ sessionID }: {
|
|
12
|
+
sessionID: string;
|
|
13
|
+
}, output: {
|
|
14
|
+
allow_buffering?: boolean;
|
|
15
|
+
parts: Part[];
|
|
16
|
+
message: {
|
|
17
|
+
sessionID: string;
|
|
18
|
+
id: string;
|
|
19
|
+
};
|
|
20
|
+
}) => Promise<void>;
|
|
21
|
+
//# sourceMappingURL=chat.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chat.d.ts","sourceRoot":"","sources":["../../../src/src/handlers/chat.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAC7C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAI5D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAGpD,iDAAiD;AACjD,MAAM,WAAW,eAAe;IAC9B,cAAc,EAAE,cAAc,CAAC;IAC/B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,MAAM,EAAE,cAAc,CAAC;CACxB;AAED,+CAA+C;AAC/C,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,eAAe,IA6FnD,eAAe;IAAE,SAAS,EAAE,MAAM,CAAA;CAAE,EACpC,QAAQ;IACN,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,KAAK,EAAE,IAAI,EAAE,CAAC;IACd,OAAO,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC;CAC5C,mBA4DJ"}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { formatMemoryContext } from "../services/context.js";
|
|
2
|
+
import { calculateInjectionBudget } from "../services/context-limit.js";
|
|
3
|
+
import { logger } from "../services/logger.js";
|
|
4
|
+
import { extractTextFromParts } from "../utils.js";
|
|
5
|
+
/** Creates the `chat.message` hook handler. */
|
|
6
|
+
export function createChatHandler(deps) {
|
|
7
|
+
const { sessionManager, injectionInterval, client } = deps;
|
|
8
|
+
const removeSyntheticMemoryParts = (parts) => parts.filter((part) => {
|
|
9
|
+
const synthetic = part.synthetic;
|
|
10
|
+
const id = typeof part.id === "string"
|
|
11
|
+
? String(part.id)
|
|
12
|
+
: "";
|
|
13
|
+
if (!synthetic)
|
|
14
|
+
return true;
|
|
15
|
+
if (id.startsWith("graphiti-memory-"))
|
|
16
|
+
return false;
|
|
17
|
+
if (id.startsWith("graphiti-refresh-"))
|
|
18
|
+
return false;
|
|
19
|
+
return true;
|
|
20
|
+
});
|
|
21
|
+
const injectMemoryContext = async (state, messageText, output, prefix, useUserScope, characterBudget) => {
|
|
22
|
+
const userGroupId = state.userGroupId;
|
|
23
|
+
const projectFactsPromise = client.searchFacts({
|
|
24
|
+
query: messageText,
|
|
25
|
+
groupIds: [state.groupId],
|
|
26
|
+
maxFacts: 50,
|
|
27
|
+
});
|
|
28
|
+
const projectNodesPromise = client.searchNodes({
|
|
29
|
+
query: messageText,
|
|
30
|
+
groupIds: [state.groupId],
|
|
31
|
+
maxNodes: 30,
|
|
32
|
+
});
|
|
33
|
+
const userFactsPromise = useUserScope && userGroupId
|
|
34
|
+
? client.searchFacts({
|
|
35
|
+
query: messageText,
|
|
36
|
+
groupIds: [userGroupId],
|
|
37
|
+
maxFacts: 20,
|
|
38
|
+
})
|
|
39
|
+
: Promise.resolve([]);
|
|
40
|
+
const userNodesPromise = useUserScope && userGroupId
|
|
41
|
+
? client.searchNodes({
|
|
42
|
+
query: messageText,
|
|
43
|
+
groupIds: [userGroupId],
|
|
44
|
+
maxNodes: 10,
|
|
45
|
+
})
|
|
46
|
+
: Promise.resolve([]);
|
|
47
|
+
const [projectFacts, projectNodes, userFacts, userNodes] = await Promise
|
|
48
|
+
.all([
|
|
49
|
+
projectFactsPromise,
|
|
50
|
+
projectNodesPromise,
|
|
51
|
+
userFactsPromise,
|
|
52
|
+
userNodesPromise,
|
|
53
|
+
]);
|
|
54
|
+
const projectContext = formatMemoryContext(projectFacts, projectNodes);
|
|
55
|
+
const userContext = formatMemoryContext(userFacts, userNodes);
|
|
56
|
+
if (!projectContext && !userContext)
|
|
57
|
+
return;
|
|
58
|
+
const projectBudget = useUserScope
|
|
59
|
+
? Math.floor(characterBudget * 0.7)
|
|
60
|
+
: characterBudget;
|
|
61
|
+
const userBudget = characterBudget - projectBudget;
|
|
62
|
+
const truncatedProject = projectContext.slice(0, projectBudget);
|
|
63
|
+
const truncatedUser = useUserScope ? userContext.slice(0, userBudget) : "";
|
|
64
|
+
const memoryContext = [truncatedProject, truncatedUser]
|
|
65
|
+
.filter((section) => section.trim().length > 0)
|
|
66
|
+
.join("\n\n")
|
|
67
|
+
.slice(0, characterBudget);
|
|
68
|
+
if (!memoryContext)
|
|
69
|
+
return;
|
|
70
|
+
output.parts.unshift({
|
|
71
|
+
type: "text",
|
|
72
|
+
text: memoryContext,
|
|
73
|
+
id: `${prefix}${Date.now()}`,
|
|
74
|
+
sessionID: output.message.sessionID,
|
|
75
|
+
messageID: output.message.id,
|
|
76
|
+
synthetic: true,
|
|
77
|
+
});
|
|
78
|
+
logger.info(`Injected ${projectFacts.length + userFacts.length} facts and ${projectNodes.length + userNodes.length} nodes`);
|
|
79
|
+
};
|
|
80
|
+
return async ({ sessionID }, output) => {
|
|
81
|
+
if (await sessionManager.isSubagentSession(sessionID)) {
|
|
82
|
+
logger.debug("Ignoring subagent chat message:", sessionID);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const { state, resolved } = await sessionManager.resolveSessionState(sessionID);
|
|
86
|
+
if (!resolved) {
|
|
87
|
+
output.allow_buffering = true;
|
|
88
|
+
logger.debug("Unable to resolve session for message:", { sessionID });
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (!state?.isMain) {
|
|
92
|
+
logger.debug("Ignoring subagent chat message:", sessionID);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
state.messageCount++;
|
|
96
|
+
const messageText = extractTextFromParts(output.parts);
|
|
97
|
+
if (!messageText)
|
|
98
|
+
return;
|
|
99
|
+
state.pendingMessages.push(`User: ${messageText}`);
|
|
100
|
+
logger.info("Buffered user message", {
|
|
101
|
+
hook: "chat.message",
|
|
102
|
+
sessionID,
|
|
103
|
+
messageLength: messageText.length,
|
|
104
|
+
});
|
|
105
|
+
const shouldInjectOnFirst = !state.injectedMemories;
|
|
106
|
+
const shouldReinject = !shouldInjectOnFirst &&
|
|
107
|
+
injectionInterval > 0 &&
|
|
108
|
+
(state.messageCount - state.lastInjectionMessageCount) >=
|
|
109
|
+
injectionInterval;
|
|
110
|
+
if (!shouldInjectOnFirst && !shouldReinject)
|
|
111
|
+
return;
|
|
112
|
+
if (shouldReinject) {
|
|
113
|
+
output.parts = removeSyntheticMemoryParts(output.parts);
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
const prefix = shouldReinject ? "graphiti-refresh-" : "graphiti-memory-";
|
|
117
|
+
const useUserScope = shouldInjectOnFirst;
|
|
118
|
+
const characterBudget = calculateInjectionBudget(state.contextLimit);
|
|
119
|
+
await injectMemoryContext(state, messageText, output, prefix, useUserScope, characterBudget);
|
|
120
|
+
state.injectedMemories = true;
|
|
121
|
+
state.lastInjectionMessageCount = state.messageCount;
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
logger.error("Failed to inject memories:", err);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { GraphitiClient } from "../services/client.js";
|
|
2
|
+
import type { SessionManager } from "../session.js";
|
|
3
|
+
/** Dependencies for the compacting handler. */
|
|
4
|
+
export interface CompactingHandlerDeps {
|
|
5
|
+
sessionManager: SessionManager;
|
|
6
|
+
client: GraphitiClient;
|
|
7
|
+
defaultGroupId: string;
|
|
8
|
+
}
|
|
9
|
+
/** Creates the `experimental.session.compacting` hook handler. */
|
|
10
|
+
export declare function createCompactingHandler(deps: CompactingHandlerDeps): ({ sessionID }: {
|
|
11
|
+
sessionID: string;
|
|
12
|
+
}, output: {
|
|
13
|
+
context: string[];
|
|
14
|
+
}) => Promise<void>;
|
|
15
|
+
//# sourceMappingURL=compacting.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compacting.d.ts","sourceRoot":"","sources":["../../../src/src/handlers/compacting.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAI5D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAEpD,+CAA+C;AAC/C,MAAM,WAAW,qBAAqB;IACpC,cAAc,EAAE,cAAc,CAAC;IAC/B,MAAM,EAAE,cAAc,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,kEAAkE;AAClE,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,qBAAqB,IAI/D,eAAe;IAAE,SAAS,EAAE,MAAM,CAAA;CAAE,EACpC,QAAQ;IAAE,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,mBAyBhC"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { getCompactionContext } from "../services/compaction.js";
|
|
2
|
+
import { calculateInjectionBudget } from "../services/context-limit.js";
|
|
3
|
+
import { logger } from "../services/logger.js";
|
|
4
|
+
/** Creates the `experimental.session.compacting` hook handler. */
|
|
5
|
+
export function createCompactingHandler(deps) {
|
|
6
|
+
const { sessionManager, client, defaultGroupId } = deps;
|
|
7
|
+
return async ({ sessionID }, output) => {
|
|
8
|
+
const state = sessionManager.getState(sessionID);
|
|
9
|
+
if (!state?.isMain) {
|
|
10
|
+
logger.debug("Ignoring non-main compaction context:", sessionID);
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
const groupId = state.groupId || defaultGroupId;
|
|
14
|
+
const characterBudget = calculateInjectionBudget(state.contextLimit);
|
|
15
|
+
const additionalContext = await getCompactionContext({
|
|
16
|
+
client,
|
|
17
|
+
characterBudget,
|
|
18
|
+
groupIds: {
|
|
19
|
+
project: groupId,
|
|
20
|
+
user: state.userGroupId,
|
|
21
|
+
},
|
|
22
|
+
contextStrings: output.context,
|
|
23
|
+
});
|
|
24
|
+
if (additionalContext.length > 0) {
|
|
25
|
+
output.context.push(...additionalContext);
|
|
26
|
+
logger.info("Injected persistent knowledge into compaction context");
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Event } from "@opencode-ai/sdk";
|
|
2
|
+
import type { GraphitiClient } from "../services/client.js";
|
|
3
|
+
import type { ProviderListClient } from "../services/context-limit.js";
|
|
4
|
+
import type { SessionManager } from "../session.js";
|
|
5
|
+
/** Dependencies for the event handler. */
|
|
6
|
+
export interface EventHandlerDeps {
|
|
7
|
+
sessionManager: SessionManager;
|
|
8
|
+
client: GraphitiClient;
|
|
9
|
+
defaultGroupId: string;
|
|
10
|
+
sdkClient: ProviderListClient;
|
|
11
|
+
directory: string;
|
|
12
|
+
groupIdPrefix: string;
|
|
13
|
+
}
|
|
14
|
+
/** Creates the `event` hook handler. */
|
|
15
|
+
export declare function createEventHandler(deps: EventHandlerDeps): ({ event }: {
|
|
16
|
+
event: Event;
|
|
17
|
+
}) => Promise<void>;
|
|
18
|
+
//# sourceMappingURL=event.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"event.d.ts","sourceRoot":"","sources":["../../../src/src/handlers/event.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAE5D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AAGvE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAGpD,0CAA0C;AAC1C,MAAM,WAAW,gBAAgB;IAC/B,cAAc,EAAE,cAAc,CAAC;IAC/B,MAAM,EAAE,cAAc,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,kBAAkB,CAAC;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,wCAAwC;AACxC,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,gBAAgB,IAWzC,WAAW;IAAE,KAAK,EAAE,KAAK,CAAA;CAAE,mBA+J1C"}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { handleCompaction } from "../services/compaction.js";
|
|
2
|
+
import { resolveContextLimit } from "../services/context-limit.js";
|
|
3
|
+
import { logger } from "../services/logger.js";
|
|
4
|
+
import { isTextPart, makeUserGroupId } from "../utils.js";
|
|
5
|
+
/** Creates the `event` hook handler. */
|
|
6
|
+
export function createEventHandler(deps) {
|
|
7
|
+
const { sessionManager, client, defaultGroupId, sdkClient, directory, groupIdPrefix, } = deps;
|
|
8
|
+
const defaultUserGroupId = makeUserGroupId(groupIdPrefix);
|
|
9
|
+
return async ({ event }) => {
|
|
10
|
+
try {
|
|
11
|
+
if (event.type === "session.created") {
|
|
12
|
+
const info = event.properties.info;
|
|
13
|
+
const sessionId = info.id;
|
|
14
|
+
const parentId = info.parentID ?? null;
|
|
15
|
+
const isMain = !parentId;
|
|
16
|
+
sessionManager.setParentId(sessionId, parentId);
|
|
17
|
+
logger.info("Session created:", {
|
|
18
|
+
sessionId,
|
|
19
|
+
isMain,
|
|
20
|
+
parentID: info.parentID,
|
|
21
|
+
});
|
|
22
|
+
if (isMain) {
|
|
23
|
+
sessionManager.setState(sessionId, {
|
|
24
|
+
groupId: defaultGroupId,
|
|
25
|
+
userGroupId: defaultUserGroupId,
|
|
26
|
+
injectedMemories: false,
|
|
27
|
+
lastInjectionMessageCount: 0,
|
|
28
|
+
messageCount: 0,
|
|
29
|
+
pendingMessages: [],
|
|
30
|
+
contextLimit: 200_000,
|
|
31
|
+
isMain,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
logger.debug("Ignoring subagent session:", sessionId);
|
|
36
|
+
}
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (event.type === "session.compacted") {
|
|
40
|
+
const sessionId = event.properties.sessionID;
|
|
41
|
+
const { state, resolved } = await sessionManager.resolveSessionState(sessionId);
|
|
42
|
+
if (!resolved) {
|
|
43
|
+
logger.debug("Unable to resolve session compaction:", sessionId);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (!state?.isMain) {
|
|
47
|
+
logger.debug("Ignoring non-main compaction:", sessionId);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const summary = event.properties.summary ||
|
|
51
|
+
"";
|
|
52
|
+
await sessionManager.flushPendingMessages(sessionId, "Buffered messages flushed before compaction", 0);
|
|
53
|
+
if (summary) {
|
|
54
|
+
await handleCompaction({
|
|
55
|
+
client,
|
|
56
|
+
groupId: state.groupId,
|
|
57
|
+
summary,
|
|
58
|
+
sessionId,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (event.type === "session.idle") {
|
|
64
|
+
const sessionId = event.properties.sessionID;
|
|
65
|
+
const { state, resolved } = await sessionManager.resolveSessionState(sessionId);
|
|
66
|
+
if (!resolved) {
|
|
67
|
+
logger.debug("Unable to resolve idle session:", sessionId);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (!state?.isMain) {
|
|
71
|
+
logger.debug("Ignoring non-main idle session:", sessionId);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
await sessionManager.flushPendingMessages(sessionId, "Buffered messages from OpenCode session", 50);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (event.type === "message.updated") {
|
|
78
|
+
const info = event.properties.info;
|
|
79
|
+
const sessionId = info.sessionID;
|
|
80
|
+
logger.info("Message event fired", {
|
|
81
|
+
hook: "message.updated",
|
|
82
|
+
eventType: "message.updated",
|
|
83
|
+
sessionId,
|
|
84
|
+
role: info.role,
|
|
85
|
+
messageID: info.id,
|
|
86
|
+
});
|
|
87
|
+
const { state, resolved } = await sessionManager.resolveSessionState(sessionId);
|
|
88
|
+
if (!resolved) {
|
|
89
|
+
logger.debug("Unable to resolve session for message update:", {
|
|
90
|
+
sessionId,
|
|
91
|
+
messageID: info.id,
|
|
92
|
+
role: info.role,
|
|
93
|
+
});
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (!state?.isMain) {
|
|
97
|
+
logger.debug("Ignoring non-main message update:", sessionId);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (info.role !== "assistant") {
|
|
101
|
+
sessionManager.deletePendingAssistant(sessionId, info.id);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const time = info.time;
|
|
105
|
+
if (!time?.completed)
|
|
106
|
+
return;
|
|
107
|
+
if (sessionManager.isAssistantBuffered(sessionId, info.id))
|
|
108
|
+
return;
|
|
109
|
+
sessionManager.finalizeAssistantMessage(state, sessionId, info.id, "message.updated");
|
|
110
|
+
if (info.tokens && info.providerID && info.modelID) {
|
|
111
|
+
resolveContextLimit(info.providerID, info.modelID, sdkClient, directory)
|
|
112
|
+
.then((limit) => {
|
|
113
|
+
state.contextLimit = limit;
|
|
114
|
+
})
|
|
115
|
+
.catch((err) => logger.debug("Failed to resolve context limit", err));
|
|
116
|
+
}
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (event.type === "message.part.updated") {
|
|
120
|
+
const part = event.properties.part;
|
|
121
|
+
if (!isTextPart(part))
|
|
122
|
+
return;
|
|
123
|
+
const sessionId = part.sessionID;
|
|
124
|
+
const messageId = part.messageID;
|
|
125
|
+
sessionManager.bufferAssistantPart(sessionId, messageId, part.text);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
logger.error("Event handler error", { type: event.type, err });
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
}
|
package/esm/src/index.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { Plugin } from "@opencode-ai/plugin";
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* OpenCode plugin entry point for Graphiti memory integration.
|
|
4
|
+
*/
|
|
3
5
|
export declare const graphiti: Plugin;
|
|
4
6
|
//# sourceMappingURL=index.d.ts.map
|
package/esm/src/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAe,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAe,MAAM,qBAAqB,CAAC;AAW/D;;GAEG;AACH,eAAO,MAAM,QAAQ,EAAE,MA4CtB,CAAC"}
|