@tarquinen/opencode-dcp 0.1.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 +21 -0
- package/README.md +90 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +245 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/config.d.ts +5 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/config.js +8 -0
- package/dist/lib/config.js.map +1 -0
- package/dist/lib/factory.d.ts +2 -0
- package/dist/lib/factory.d.ts.map +1 -0
- package/dist/lib/factory.js +12 -0
- package/dist/lib/factory.js.map +1 -0
- package/dist/lib/janitor.d.ts +10 -0
- package/dist/lib/janitor.d.ts.map +1 -0
- package/dist/lib/janitor.js +233 -0
- package/dist/lib/janitor.js.map +1 -0
- package/dist/lib/logger.d.ts +12 -0
- package/dist/lib/logger.d.ts.map +1 -0
- package/dist/lib/logger.js +46 -0
- package/dist/lib/logger.js.map +1 -0
- package/dist/lib/state.d.ts +13 -0
- package/dist/lib/state.d.ts.map +1 -0
- package/dist/lib/state.js +29 -0
- package/dist/lib/state.js.map +1 -0
- package/package.json +46 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 tarquinen
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# Dynamic Context Pruning Plugin
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@tarquinen/opencode-dcp)
|
|
4
|
+
|
|
5
|
+
OpenCode plugin that optimizes token usage by analyzing conversation history and pruning obsolete tool outputs from requests.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Zero Configuration**: Uses the free `opencode/big-pickle` model - no API keys required
|
|
10
|
+
- **Automatic Optimization**: Runs in the background when sessions become idle
|
|
11
|
+
- **Smart Analysis**: Uses AI to identify truly obsolete context
|
|
12
|
+
- **Debug Logging**: Optional file-based logging for troubleshooting
|
|
13
|
+
|
|
14
|
+
## How It Works
|
|
15
|
+
|
|
16
|
+
1. When a session becomes idle, the Janitor analyzes the conversation using shadow inference
|
|
17
|
+
2. It identifies tool call outputs that are no longer relevant to the conversation
|
|
18
|
+
3. These IDs are stored in memory for that session
|
|
19
|
+
4. On subsequent requests, a custom fetch function filters out the obsolete tool responses
|
|
20
|
+
5. Result: Fewer tokens sent to the LLM, lower costs, faster responses
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
### Via NPM (Recommended)
|
|
25
|
+
|
|
26
|
+
Add the plugin to your OpenCode configuration:
|
|
27
|
+
|
|
28
|
+
**Global:** `~/.config/opencode/opencode.json`
|
|
29
|
+
**Project:** `.opencode/opencode.json`
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"plugin": [
|
|
34
|
+
"@tarquinen/opencode-dcp"
|
|
35
|
+
]
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Then restart OpenCode. The plugin will automatically:
|
|
40
|
+
- Analyze conversations when sessions become idle
|
|
41
|
+
- Identify obsolete tool outputs
|
|
42
|
+
- Prune them from future requests
|
|
43
|
+
- Save you tokens and money!
|
|
44
|
+
|
|
45
|
+
### Local Development
|
|
46
|
+
|
|
47
|
+
If you want to modify the plugin, you can clone this repository and use a local path:
|
|
48
|
+
|
|
49
|
+
```json
|
|
50
|
+
{
|
|
51
|
+
"plugin": [
|
|
52
|
+
"./plugin/dynamic-context-pruning"
|
|
53
|
+
]
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Configuration
|
|
58
|
+
|
|
59
|
+
No configuration required! The plugin works out of the box with:
|
|
60
|
+
- Zero-config setup using `opencode/big-pickle` model
|
|
61
|
+
- Automatic background optimization
|
|
62
|
+
- Smart AI-powered analysis
|
|
63
|
+
|
|
64
|
+
Optional: Enable debug logging by creating `~/.config/opencode/dynamic-context-pruning-config.json`:
|
|
65
|
+
|
|
66
|
+
```json
|
|
67
|
+
{
|
|
68
|
+
"debug": true
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Debug logs will be written to the plugin's `logs/` directory.
|
|
73
|
+
|
|
74
|
+
## Architecture
|
|
75
|
+
|
|
76
|
+
- **Janitor**: Background process that analyzes sessions using opencode/big-pickle model
|
|
77
|
+
- **State Manager**: In-memory store for pruned tool call IDs per session
|
|
78
|
+
- **Fetch Injector**: Injects custom fetch via `chat.params` hook
|
|
79
|
+
- **Pruning Fetch Wrapper**: Filters request bodies based on session state
|
|
80
|
+
- **Logger**: File-based debug logging system
|
|
81
|
+
|
|
82
|
+
## Development Status
|
|
83
|
+
|
|
84
|
+
Current implementation status: **Step 1 - Project Scaffolding** ✅
|
|
85
|
+
|
|
86
|
+
See `@notes/dynamic-context-pruning/IMPLEMENTATION-PLAN.md` for full implementation details.
|
|
87
|
+
|
|
88
|
+
## License
|
|
89
|
+
|
|
90
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
declare const _default: (ctx: import("@opencode-ai/plugin").PluginInput) => Promise<{
|
|
2
|
+
/**
|
|
3
|
+
* Event Hook: Triggers janitor analysis when session becomes idle
|
|
4
|
+
*/
|
|
5
|
+
event: ({ event }: {
|
|
6
|
+
event: import("@opencode-ai/sdk").Event;
|
|
7
|
+
}) => Promise<void>;
|
|
8
|
+
/**
|
|
9
|
+
* Chat Params Hook: Wraps fetch function to filter pruned tool responses
|
|
10
|
+
*/
|
|
11
|
+
"chat.params": (input: {
|
|
12
|
+
sessionID: string;
|
|
13
|
+
agent: string;
|
|
14
|
+
model: import("@opencode-ai/sdk").Model;
|
|
15
|
+
provider: import("@opencode-ai/sdk").Provider;
|
|
16
|
+
message: import("@opencode-ai/sdk").UserMessage;
|
|
17
|
+
}, output: {
|
|
18
|
+
temperature: number;
|
|
19
|
+
topP: number;
|
|
20
|
+
options: Record<string, any>;
|
|
21
|
+
}) => Promise<void>;
|
|
22
|
+
}>;
|
|
23
|
+
export default _default;
|
|
24
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":";IAqHQ;;OAEG;;;;IA8BH;;OAEG;;;;;;;;;;;;;AA9IX,wBAgRmB"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { getConfig } from "./lib/config";
|
|
2
|
+
import { Logger } from "./lib/logger";
|
|
3
|
+
import { StateManager } from "./lib/state";
|
|
4
|
+
import { Janitor } from "./lib/janitor";
|
|
5
|
+
import { dirname } from "path";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
export default (async (ctx) => {
|
|
8
|
+
const config = getConfig();
|
|
9
|
+
// Suppress AI SDK warnings about responseFormat (harmless for our use case)
|
|
10
|
+
if (typeof globalThis !== 'undefined') {
|
|
11
|
+
globalThis.AI_SDK_LOG_WARNINGS = false;
|
|
12
|
+
}
|
|
13
|
+
// Get the plugin's own directory (not ctx.directory which is the main OpenCode config dir)
|
|
14
|
+
const pluginDir = dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
const logger = new Logger(config.debug, pluginDir);
|
|
16
|
+
const stateManager = new StateManager();
|
|
17
|
+
const janitor = new Janitor(ctx.client, stateManager, logger);
|
|
18
|
+
// Track pruned counts per session for this request
|
|
19
|
+
const requestPrunedCounts = new Map();
|
|
20
|
+
// Store the original global fetch
|
|
21
|
+
const originalGlobalFetch = globalThis.fetch;
|
|
22
|
+
// Wrap globalThis.fetch to intercept ALL fetch calls
|
|
23
|
+
// This works because even if auth providers set a custom fetch,
|
|
24
|
+
// they ultimately call fetch() which goes through globalThis.fetch
|
|
25
|
+
globalThis.fetch = async (input, init) => {
|
|
26
|
+
// Check if this looks like an AI API request by examining the body
|
|
27
|
+
if (init?.body && typeof init.body === 'string') {
|
|
28
|
+
try {
|
|
29
|
+
const body = JSON.parse(init.body);
|
|
30
|
+
// Only process requests that have a messages array (AI requests)
|
|
31
|
+
if (body.messages && Array.isArray(body.messages)) {
|
|
32
|
+
logger.info("global-fetch", "🔥 AI REQUEST INTERCEPTED via global fetch!", {
|
|
33
|
+
url: typeof input === 'string' ? input.substring(0, 80) : 'URL object',
|
|
34
|
+
messageCount: body.messages.length
|
|
35
|
+
});
|
|
36
|
+
// Try to extract session ID from the request (might be in headers or we track it)
|
|
37
|
+
// For now, we'll use a simpler approach: collect ALL pruned IDs from all sessions
|
|
38
|
+
// This is safe because tool_call_ids are globally unique
|
|
39
|
+
const toolMessages = body.messages.filter((m) => m.role === 'tool');
|
|
40
|
+
if (toolMessages.length > 0) {
|
|
41
|
+
logger.debug("global-fetch", "Found tool messages in request", {
|
|
42
|
+
toolMessageCount: toolMessages.length,
|
|
43
|
+
toolCallIds: toolMessages.map((m) => m.tool_call_id).slice(0, 5)
|
|
44
|
+
});
|
|
45
|
+
// Collect all pruned IDs across all sessions, excluding subagent sessions
|
|
46
|
+
const allSessions = await ctx.client.session.list();
|
|
47
|
+
const allPrunedIds = new Set();
|
|
48
|
+
if (allSessions.data) {
|
|
49
|
+
for (const session of allSessions.data) {
|
|
50
|
+
// Skip subagent sessions (don't log - it's normal and would spam logs)
|
|
51
|
+
if (session.parentID) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const prunedIds = await stateManager.get(session.id);
|
|
55
|
+
prunedIds.forEach(id => allPrunedIds.add(id));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (allPrunedIds.size > 0) {
|
|
59
|
+
let replacedCount = 0;
|
|
60
|
+
body.messages = body.messages.map((m) => {
|
|
61
|
+
if (m.role === 'tool' && allPrunedIds.has(m.tool_call_id)) {
|
|
62
|
+
replacedCount++;
|
|
63
|
+
return {
|
|
64
|
+
...m,
|
|
65
|
+
content: '[Output removed to save context - information superseded or no longer needed]'
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
return m;
|
|
69
|
+
});
|
|
70
|
+
if (replacedCount > 0) {
|
|
71
|
+
logger.info("global-fetch", "✂️ Replaced pruned tool messages", {
|
|
72
|
+
totalPrunedIds: allPrunedIds.size,
|
|
73
|
+
replacedCount: replacedCount,
|
|
74
|
+
totalMessages: body.messages.length
|
|
75
|
+
});
|
|
76
|
+
// Update the request body with modified messages
|
|
77
|
+
init.body = JSON.stringify(body);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch (e) {
|
|
84
|
+
// Not a JSON body or not an AI request, ignore
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// Call the original fetch
|
|
88
|
+
return originalGlobalFetch(input, init);
|
|
89
|
+
};
|
|
90
|
+
logger.info("plugin", "Dynamic Context Pruning plugin initialized", {
|
|
91
|
+
debug: config.debug,
|
|
92
|
+
pluginDirectory: pluginDir,
|
|
93
|
+
opencodeDirectory: ctx.directory,
|
|
94
|
+
globalFetchWrapped: true
|
|
95
|
+
});
|
|
96
|
+
return {
|
|
97
|
+
/**
|
|
98
|
+
* Event Hook: Triggers janitor analysis when session becomes idle
|
|
99
|
+
*/
|
|
100
|
+
event: async ({ event }) => {
|
|
101
|
+
if (event.type === "session.status" && event.properties.status.type === "idle") {
|
|
102
|
+
// Get session info to check if it's a subagent
|
|
103
|
+
const result = await ctx.client.session.get({ path: { id: event.properties.sessionID } });
|
|
104
|
+
// Skip pruning for subagent sessions
|
|
105
|
+
if (result.data?.parentID) {
|
|
106
|
+
logger.debug("event", "Skipping janitor for subagent session", {
|
|
107
|
+
sessionID: event.properties.sessionID,
|
|
108
|
+
parentID: result.data.parentID
|
|
109
|
+
});
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
logger.debug("event", "Session became idle, triggering janitor", {
|
|
113
|
+
sessionID: event.properties.sessionID
|
|
114
|
+
});
|
|
115
|
+
// Fire and forget the janitor - don't block the event handler
|
|
116
|
+
janitor.run(event.properties.sessionID).catch(err => {
|
|
117
|
+
logger.error("event", "Janitor failed", {
|
|
118
|
+
sessionID: event.properties.sessionID,
|
|
119
|
+
error: err.message,
|
|
120
|
+
stack: err.stack
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
/**
|
|
126
|
+
* Chat Params Hook: Wraps fetch function to filter pruned tool responses
|
|
127
|
+
*/
|
|
128
|
+
"chat.params": async (input, output) => {
|
|
129
|
+
const sessionId = input.sessionID;
|
|
130
|
+
// Get session info to check if it's a subagent
|
|
131
|
+
const result = await ctx.client.session.get({ path: { id: sessionId } });
|
|
132
|
+
// Skip pruning for subagent sessions
|
|
133
|
+
if (result.data?.parentID) {
|
|
134
|
+
logger.debug("chat.params", "Skipping context pruning for subagent session", {
|
|
135
|
+
sessionID: sessionId,
|
|
136
|
+
parentID: result.data.parentID
|
|
137
|
+
});
|
|
138
|
+
return; // Don't wrap fetch, let it pass through unchanged
|
|
139
|
+
}
|
|
140
|
+
logger.debug("chat.params", "Wrapping fetch for session", {
|
|
141
|
+
sessionID: sessionId,
|
|
142
|
+
hasFetch: !!output.options["fetch"],
|
|
143
|
+
fetchType: output.options["fetch"] ? typeof output.options["fetch"] : "none"
|
|
144
|
+
});
|
|
145
|
+
// Get the existing fetch - this might be from auth provider or globalThis
|
|
146
|
+
const existingFetch = output.options["fetch"] ?? globalThis.fetch;
|
|
147
|
+
logger.debug("chat.params", "Existing fetch captured", {
|
|
148
|
+
sessionID: sessionId,
|
|
149
|
+
isGlobalFetch: existingFetch === globalThis.fetch
|
|
150
|
+
});
|
|
151
|
+
// Wrap the existing fetch with our pruning logic
|
|
152
|
+
output.options["fetch"] = async (fetchInput, init) => {
|
|
153
|
+
logger.info("pruning-fetch", "🔥 FETCH WRAPPER CALLED!", {
|
|
154
|
+
sessionId,
|
|
155
|
+
url: typeof fetchInput === 'string' ? fetchInput.substring(0, 100) : 'URL object'
|
|
156
|
+
});
|
|
157
|
+
logger.debug("pruning-fetch", "Request intercepted", { sessionId });
|
|
158
|
+
// Retrieve the list of pruned tool call IDs from state
|
|
159
|
+
const prunedIds = await stateManager.get(sessionId);
|
|
160
|
+
logger.debug("pruning-fetch", "Retrieved pruned IDs", {
|
|
161
|
+
sessionId,
|
|
162
|
+
prunedCount: prunedIds.length,
|
|
163
|
+
prunedIds: prunedIds.length > 0 ? prunedIds : undefined
|
|
164
|
+
});
|
|
165
|
+
// Log request body details before filtering
|
|
166
|
+
if (init?.body) {
|
|
167
|
+
try {
|
|
168
|
+
const bodyPreview = JSON.parse(init.body);
|
|
169
|
+
const toolMessages = bodyPreview.messages?.filter((m) => m.role === 'tool') || [];
|
|
170
|
+
logger.debug("pruning-fetch", "Request body before filtering", {
|
|
171
|
+
sessionId,
|
|
172
|
+
totalMessages: bodyPreview.messages?.length || 0,
|
|
173
|
+
toolMessages: toolMessages.length,
|
|
174
|
+
toolCallIds: toolMessages.map((m) => m.tool_call_id)
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
catch (e) {
|
|
178
|
+
// Ignore parse errors here
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
// Reset the count for this request
|
|
182
|
+
let prunedThisRequest = 0;
|
|
183
|
+
// Only attempt filtering if there are pruned IDs and a request body exists
|
|
184
|
+
if (prunedIds.length > 0 && init?.body) {
|
|
185
|
+
try {
|
|
186
|
+
// Parse the request body (expected to be JSON)
|
|
187
|
+
const body = JSON.parse(init.body);
|
|
188
|
+
const originalMessageCount = body.messages?.length || 0;
|
|
189
|
+
if (body.messages && Array.isArray(body.messages)) {
|
|
190
|
+
// Replace tool response messages whose tool_call_id is in the pruned list
|
|
191
|
+
// with a short placeholder message instead of removing them entirely.
|
|
192
|
+
// This preserves the message structure and avoids API validation errors.
|
|
193
|
+
body.messages = body.messages.map((m) => {
|
|
194
|
+
if (m.role === 'tool' && prunedIds.includes(m.tool_call_id)) {
|
|
195
|
+
prunedThisRequest++;
|
|
196
|
+
return {
|
|
197
|
+
...m,
|
|
198
|
+
content: '[Output removed to save context - information superseded or no longer needed]'
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
return m;
|
|
202
|
+
});
|
|
203
|
+
if (prunedThisRequest > 0) {
|
|
204
|
+
logger.info("pruning-fetch", "Replaced pruned tool messages", {
|
|
205
|
+
sessionId,
|
|
206
|
+
totalMessages: originalMessageCount,
|
|
207
|
+
replacedCount: prunedThisRequest,
|
|
208
|
+
prunedIds
|
|
209
|
+
});
|
|
210
|
+
// Log remaining tool messages
|
|
211
|
+
const remainingToolMessages = body.messages.filter((m) => m.role === 'tool');
|
|
212
|
+
logger.debug("pruning-fetch", "Tool messages after replacement", {
|
|
213
|
+
sessionId,
|
|
214
|
+
totalToolCount: remainingToolMessages.length,
|
|
215
|
+
toolCallIds: remainingToolMessages.map((m) => m.tool_call_id)
|
|
216
|
+
});
|
|
217
|
+
// Track how many were pruned for this request
|
|
218
|
+
requestPrunedCounts.set(sessionId, prunedThisRequest);
|
|
219
|
+
// Update the request body with modified messages
|
|
220
|
+
init.body = JSON.stringify(body);
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
logger.debug("pruning-fetch", "No messages replaced", {
|
|
224
|
+
sessionId,
|
|
225
|
+
messageCount: originalMessageCount
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
logger.error("pruning-fetch", "Failed to parse/filter request body", {
|
|
232
|
+
sessionId,
|
|
233
|
+
error: error.message,
|
|
234
|
+
stack: error.stack
|
|
235
|
+
});
|
|
236
|
+
// Continue with original request if parsing fails - don't break the request
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
// Call the EXISTING fetch (which might be from auth provider) with potentially modified body
|
|
240
|
+
return existingFetch(fetchInput, init);
|
|
241
|
+
};
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
});
|
|
245
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../lib/config.ts"],"names":[],"mappings":"AACA,MAAM,WAAW,YAAY;IACzB,KAAK,EAAE,OAAO,CAAA;CACjB;AAMD,wBAAgB,SAAS,IAAI,YAAY,CAGxC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../lib/config.ts"],"names":[],"mappings":"AAKA,MAAM,aAAa,GAAiB;IAChC,KAAK,EAAE,IAAI,CAAC,sCAAsC;CACrD,CAAA;AAED,MAAM,UAAU,SAAS;IACrB,8DAA8D;IAC9D,OAAO,aAAa,CAAA;AACxB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../../lib/factory.ts"],"names":[],"mappings":"AAGA,wBAAgB,iBAAiB,+CAShC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// lib/factory.ts
|
|
2
|
+
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
|
|
3
|
+
export function createShadowModel() {
|
|
4
|
+
// No credentials needed - big-pickle is public and free
|
|
5
|
+
const openai = createOpenAICompatible({
|
|
6
|
+
baseURL: "https://opencode.ai/zen/v1",
|
|
7
|
+
// No apiKey required - the endpoint is public
|
|
8
|
+
name: "opencode",
|
|
9
|
+
});
|
|
10
|
+
return openai("big-pickle");
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=factory.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"factory.js","sourceRoot":"","sources":["../../lib/factory.ts"],"names":[],"mappings":"AAAA,iBAAiB;AACjB,OAAO,EAAE,sBAAsB,EAAE,MAAM,2BAA2B,CAAA;AAElE,MAAM,UAAU,iBAAiB;IAC7B,wDAAwD;IACxD,MAAM,MAAM,GAAG,sBAAsB,CAAC;QAClC,OAAO,EAAE,4BAA4B;QACrC,8CAA8C;QAC9C,IAAI,EAAE,UAAU;KACnB,CAAC,CAAA;IAEF,OAAO,MAAM,CAAC,YAAY,CAAC,CAAA;AAC/B,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Logger } from "./logger";
|
|
2
|
+
import type { StateManager } from "./state";
|
|
3
|
+
export declare class Janitor {
|
|
4
|
+
private client;
|
|
5
|
+
private stateManager;
|
|
6
|
+
private logger;
|
|
7
|
+
constructor(client: any, stateManager: StateManager, logger: Logger);
|
|
8
|
+
run(sessionID: string): Promise<void>;
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=janitor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"janitor.d.ts","sourceRoot":"","sources":["../../lib/janitor.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,UAAU,CAAA;AACtC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAE3C,qBAAa,OAAO;IAEZ,OAAO,CAAC,MAAM;IACd,OAAO,CAAC,YAAY;IACpB,OAAO,CAAC,MAAM;gBAFN,MAAM,EAAE,GAAG,EACX,YAAY,EAAE,YAAY,EAC1B,MAAM,EAAE,MAAM;IAGpB,GAAG,CAAC,SAAS,EAAE,MAAM;CAkP9B"}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { generateObject } from "ai";
|
|
2
|
+
import { createShadowModel } from "./factory";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
export class Janitor {
|
|
5
|
+
client;
|
|
6
|
+
stateManager;
|
|
7
|
+
logger;
|
|
8
|
+
constructor(client, stateManager, logger) {
|
|
9
|
+
this.client = client;
|
|
10
|
+
this.stateManager = stateManager;
|
|
11
|
+
this.logger = logger;
|
|
12
|
+
}
|
|
13
|
+
async run(sessionID) {
|
|
14
|
+
this.logger.info("janitor", "Starting analysis", { sessionID });
|
|
15
|
+
try {
|
|
16
|
+
// Fetch session history from OpenCode API
|
|
17
|
+
this.logger.debug("janitor", "Fetching session messages", { sessionID });
|
|
18
|
+
const response = await this.client.session.messages({
|
|
19
|
+
path: { id: sessionID },
|
|
20
|
+
query: { limit: 100 }
|
|
21
|
+
});
|
|
22
|
+
// Handle the response format - it should be { data: Array<{info, parts}> } or just the array
|
|
23
|
+
const messages = response.data || response;
|
|
24
|
+
this.logger.debug("janitor", "Retrieved messages", {
|
|
25
|
+
sessionID,
|
|
26
|
+
messageCount: messages.length
|
|
27
|
+
});
|
|
28
|
+
// If there are no messages or very few, skip analysis
|
|
29
|
+
if (!messages || messages.length < 3) {
|
|
30
|
+
this.logger.debug("janitor", "Too few messages to analyze, skipping", {
|
|
31
|
+
sessionID,
|
|
32
|
+
messageCount: messages?.length || 0
|
|
33
|
+
});
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
// Extract tool call IDs from the session and track their output sizes
|
|
37
|
+
// Also track batch tool relationships
|
|
38
|
+
const toolCallIds = [];
|
|
39
|
+
const toolOutputs = new Map();
|
|
40
|
+
const batchToolChildren = new Map(); // batchID -> [childIDs]
|
|
41
|
+
let currentBatchId = null;
|
|
42
|
+
for (const msg of messages) {
|
|
43
|
+
if (msg.parts) {
|
|
44
|
+
for (const part of msg.parts) {
|
|
45
|
+
if (part.type === "tool" && part.callID) {
|
|
46
|
+
toolCallIds.push(part.callID);
|
|
47
|
+
// Track the output content for size calculation
|
|
48
|
+
if (part.state?.status === "completed" && part.state.output) {
|
|
49
|
+
toolOutputs.set(part.callID, part.state.output);
|
|
50
|
+
}
|
|
51
|
+
// Check if this is a batch tool by looking at the tool name
|
|
52
|
+
if (part.tool === "batch") {
|
|
53
|
+
const batchId = part.callID;
|
|
54
|
+
currentBatchId = batchId;
|
|
55
|
+
batchToolChildren.set(batchId, []);
|
|
56
|
+
this.logger.debug("janitor", "Found batch tool", {
|
|
57
|
+
sessionID,
|
|
58
|
+
batchID: currentBatchId
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
// If we're inside a batch and this is a prt_ (parallel) tool call, it's a child
|
|
62
|
+
else if (currentBatchId && part.callID.startsWith('prt_')) {
|
|
63
|
+
const children = batchToolChildren.get(currentBatchId);
|
|
64
|
+
children.push(part.callID);
|
|
65
|
+
this.logger.debug("janitor", "Added child to batch tool", {
|
|
66
|
+
sessionID,
|
|
67
|
+
batchID: currentBatchId,
|
|
68
|
+
childID: part.callID,
|
|
69
|
+
totalChildren: children.length
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
// If we hit a non-batch, non-prt_ tool, we're out of the batch
|
|
73
|
+
else if (currentBatchId && !part.callID.startsWith('prt_')) {
|
|
74
|
+
this.logger.debug("janitor", "Batch tool ended", {
|
|
75
|
+
sessionID,
|
|
76
|
+
batchID: currentBatchId,
|
|
77
|
+
totalChildren: batchToolChildren.get(currentBatchId).length
|
|
78
|
+
});
|
|
79
|
+
currentBatchId = null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// Log summary of batch tools found
|
|
86
|
+
if (batchToolChildren.size > 0) {
|
|
87
|
+
this.logger.debug("janitor", "Batch tool summary", {
|
|
88
|
+
sessionID,
|
|
89
|
+
batchCount: batchToolChildren.size,
|
|
90
|
+
batches: Array.from(batchToolChildren.entries()).map(([id, children]) => ({
|
|
91
|
+
batchID: id,
|
|
92
|
+
childCount: children.length,
|
|
93
|
+
childIDs: children
|
|
94
|
+
}))
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
// Get already pruned IDs to filter them out
|
|
98
|
+
const alreadyPrunedIds = await this.stateManager.get(sessionID);
|
|
99
|
+
const unprunedToolCallIds = toolCallIds.filter(id => !alreadyPrunedIds.includes(id));
|
|
100
|
+
this.logger.debug("janitor", "Found tool calls in session", {
|
|
101
|
+
sessionID,
|
|
102
|
+
toolCallCount: toolCallIds.length,
|
|
103
|
+
toolCallIds,
|
|
104
|
+
alreadyPrunedCount: alreadyPrunedIds.length,
|
|
105
|
+
unprunedCount: unprunedToolCallIds.length
|
|
106
|
+
});
|
|
107
|
+
// If there are no unpruned tool calls, skip analysis
|
|
108
|
+
if (unprunedToolCallIds.length === 0) {
|
|
109
|
+
this.logger.debug("janitor", "No unpruned tool calls found, skipping analysis", { sessionID });
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
// Use big-pickle model - no auth needed!
|
|
113
|
+
const model = createShadowModel();
|
|
114
|
+
this.logger.debug("janitor", "Starting shadow inference", { sessionID });
|
|
115
|
+
// Analyze which tool calls are obsolete
|
|
116
|
+
const result = await generateObject({
|
|
117
|
+
model,
|
|
118
|
+
mode: "json", // Use JSON mode instead of native structured outputs
|
|
119
|
+
schema: z.object({
|
|
120
|
+
pruned_tool_call_ids: z.array(z.string()),
|
|
121
|
+
reasoning: z.string(),
|
|
122
|
+
}),
|
|
123
|
+
prompt: `You are a conversation analyzer that identifies obsolete tool outputs in a coding session.
|
|
124
|
+
|
|
125
|
+
Your task: Analyze the session history and identify tool call IDs whose outputs are NO LONGER RELEVANT to the current conversation context.
|
|
126
|
+
|
|
127
|
+
Guidelines for identifying obsolete tool calls:
|
|
128
|
+
1. Tool outputs that were superseded by newer reads of the same file/resource
|
|
129
|
+
2. Exploratory reads that didn't lead to actual edits or meaningful discussion
|
|
130
|
+
3. Tool calls from >10 turns ago that are no longer referenced
|
|
131
|
+
4. Error outputs that were subsequently fixed
|
|
132
|
+
5. Tool calls whose information has been replaced by more recent operations
|
|
133
|
+
|
|
134
|
+
DO NOT prune:
|
|
135
|
+
- Recent tool calls (within last 5 turns)
|
|
136
|
+
- Tool calls that modified state (edits, writes, etc.)
|
|
137
|
+
- Tool calls whose outputs are actively being discussed
|
|
138
|
+
- Tool calls that produced errors still being debugged
|
|
139
|
+
|
|
140
|
+
Available tool call IDs in this session (not yet pruned): ${unprunedToolCallIds.join(", ")}
|
|
141
|
+
|
|
142
|
+
Session history:
|
|
143
|
+
${JSON.stringify(messages, null, 2)}
|
|
144
|
+
|
|
145
|
+
You MUST respond with valid JSON matching this exact schema:
|
|
146
|
+
{
|
|
147
|
+
"pruned_tool_call_ids": ["id1", "id2", ...],
|
|
148
|
+
"reasoning": "explanation of why these IDs were selected"
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
Return ONLY the tool call IDs that should be pruned (removed from future LLM requests).`
|
|
152
|
+
});
|
|
153
|
+
// Expand batch tool IDs to include their children
|
|
154
|
+
const expandedPrunedIds = new Set();
|
|
155
|
+
for (const prunedId of result.object.pruned_tool_call_ids) {
|
|
156
|
+
expandedPrunedIds.add(prunedId);
|
|
157
|
+
// If this is a batch tool, add all its children
|
|
158
|
+
const children = batchToolChildren.get(prunedId);
|
|
159
|
+
if (children) {
|
|
160
|
+
this.logger.debug("janitor", "Expanding batch tool to include children", {
|
|
161
|
+
sessionID,
|
|
162
|
+
batchID: prunedId,
|
|
163
|
+
childCount: children.length,
|
|
164
|
+
childIDs: children
|
|
165
|
+
});
|
|
166
|
+
children.forEach(childId => expandedPrunedIds.add(childId));
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const finalPrunedIds = Array.from(expandedPrunedIds);
|
|
170
|
+
this.logger.info("janitor", "Analysis complete", {
|
|
171
|
+
sessionID,
|
|
172
|
+
prunedCount: finalPrunedIds.length,
|
|
173
|
+
originalPrunedCount: result.object.pruned_tool_call_ids.length,
|
|
174
|
+
prunedIds: finalPrunedIds,
|
|
175
|
+
reasoning: result.object.reasoning
|
|
176
|
+
});
|
|
177
|
+
// Calculate approximate size saved from newly pruned tool outputs (using expanded IDs)
|
|
178
|
+
let totalCharsSaved = 0;
|
|
179
|
+
for (const prunedId of finalPrunedIds) {
|
|
180
|
+
const output = toolOutputs.get(prunedId);
|
|
181
|
+
if (output) {
|
|
182
|
+
totalCharsSaved += output.length;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
// Rough token estimate (1 token ≈ 4 characters for English text)
|
|
186
|
+
const estimatedTokensSaved = Math.round(totalCharsSaved / 4);
|
|
187
|
+
// Merge newly pruned IDs with existing ones (using expanded IDs)
|
|
188
|
+
const allPrunedIds = [...new Set([...alreadyPrunedIds, ...finalPrunedIds])];
|
|
189
|
+
await this.stateManager.set(sessionID, allPrunedIds);
|
|
190
|
+
this.logger.debug("janitor", "Updated state manager", {
|
|
191
|
+
sessionID,
|
|
192
|
+
totalPrunedCount: allPrunedIds.length,
|
|
193
|
+
newlyPrunedCount: finalPrunedIds.length
|
|
194
|
+
});
|
|
195
|
+
// Show toast notification if we pruned anything
|
|
196
|
+
if (finalPrunedIds.length > 0) {
|
|
197
|
+
try {
|
|
198
|
+
await this.client.tui.showToast({
|
|
199
|
+
body: {
|
|
200
|
+
title: "Context Pruned",
|
|
201
|
+
message: `Removed ${finalPrunedIds.length} tool output${finalPrunedIds.length > 1 ? 's' : ''} (~${estimatedTokensSaved.toLocaleString()} tokens saved)`,
|
|
202
|
+
variant: "success",
|
|
203
|
+
duration: 5000
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
this.logger.info("janitor", "Toast notification shown", {
|
|
207
|
+
sessionID,
|
|
208
|
+
prunedCount: finalPrunedIds.length,
|
|
209
|
+
estimatedTokensSaved,
|
|
210
|
+
totalCharsSaved
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
catch (toastError) {
|
|
214
|
+
this.logger.error("janitor", "Failed to show toast notification", {
|
|
215
|
+
sessionID,
|
|
216
|
+
error: toastError.message
|
|
217
|
+
});
|
|
218
|
+
// Don't fail the whole pruning operation if toast fails
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
this.logger.error("janitor", "Analysis failed", {
|
|
224
|
+
sessionID,
|
|
225
|
+
error: error.message,
|
|
226
|
+
stack: error.stack
|
|
227
|
+
});
|
|
228
|
+
// Don't throw - this is a fire-and-forget background process
|
|
229
|
+
// Silently fail and try again on next idle event
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
//# sourceMappingURL=janitor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"janitor.js","sourceRoot":"","sources":["../../lib/janitor.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare class Logger {
|
|
2
|
+
private logDir;
|
|
3
|
+
private enabled;
|
|
4
|
+
constructor(enabled: boolean, pluginDir: string);
|
|
5
|
+
private ensureLogDir;
|
|
6
|
+
private write;
|
|
7
|
+
info(component: string, message: string, data?: any): Promise<void>;
|
|
8
|
+
debug(component: string, message: string, data?: any): Promise<void>;
|
|
9
|
+
warn(component: string, message: string, data?: any): Promise<void>;
|
|
10
|
+
error(component: string, message: string, data?: any): Promise<void>;
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=logger.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../lib/logger.ts"],"names":[],"mappings":"AAKA,qBAAa,MAAM;IACf,OAAO,CAAC,MAAM,CAAQ;IACtB,OAAO,CAAC,OAAO,CAAS;gBAEZ,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM;YAKjC,YAAY;YAMZ,KAAK;IAoBnB,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,GAAG;IAInD,KAAK,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,GAAG;IAIpD,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,GAAG;IAInD,KAAK,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,GAAG;CAGvD"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// lib/logger.ts
|
|
2
|
+
import { writeFile, mkdir } from "fs/promises";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { existsSync } from "fs";
|
|
5
|
+
export class Logger {
|
|
6
|
+
logDir;
|
|
7
|
+
enabled;
|
|
8
|
+
constructor(enabled, pluginDir) {
|
|
9
|
+
this.enabled = enabled;
|
|
10
|
+
this.logDir = join(pluginDir, "logs");
|
|
11
|
+
}
|
|
12
|
+
async ensureLogDir() {
|
|
13
|
+
if (!existsSync(this.logDir)) {
|
|
14
|
+
await mkdir(this.logDir, { recursive: true });
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
async write(level, component, message, data) {
|
|
18
|
+
if (!this.enabled)
|
|
19
|
+
return;
|
|
20
|
+
await this.ensureLogDir();
|
|
21
|
+
const timestamp = new Date().toISOString();
|
|
22
|
+
const logEntry = {
|
|
23
|
+
timestamp,
|
|
24
|
+
level,
|
|
25
|
+
component,
|
|
26
|
+
message,
|
|
27
|
+
...(data && { data })
|
|
28
|
+
};
|
|
29
|
+
const logFile = join(this.logDir, `${new Date().toISOString().split('T')[0]}.log`);
|
|
30
|
+
const logLine = JSON.stringify(logEntry) + "\n";
|
|
31
|
+
await writeFile(logFile, logLine, { flag: "a" });
|
|
32
|
+
}
|
|
33
|
+
info(component, message, data) {
|
|
34
|
+
return this.write("INFO", component, message, data);
|
|
35
|
+
}
|
|
36
|
+
debug(component, message, data) {
|
|
37
|
+
return this.write("DEBUG", component, message, data);
|
|
38
|
+
}
|
|
39
|
+
warn(component, message, data) {
|
|
40
|
+
return this.write("WARN", component, message, data);
|
|
41
|
+
}
|
|
42
|
+
error(component, message, data) {
|
|
43
|
+
return this.write("ERROR", component, message, data);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=logger.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.js","sourceRoot":"","sources":["../../lib/logger.ts"],"names":[],"mappings":"AAAA,gBAAgB;AAChB,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,aAAa,CAAA;AAC9C,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAC3B,OAAO,EAAE,UAAU,EAAE,MAAM,IAAI,CAAA;AAE/B,MAAM,OAAO,MAAM;IACP,MAAM,CAAQ;IACd,OAAO,CAAS;IAExB,YAAY,OAAgB,EAAE,SAAiB;QAC3C,IAAI,CAAC,OAAO,GAAG,OAAO,CAAA;QACtB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,SAAS,EAAE,MAAM,CAAC,CAAA;IACzC,CAAC;IAEO,KAAK,CAAC,YAAY;QACtB,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;YAC3B,MAAM,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;QACjD,CAAC;IACL,CAAC;IAEO,KAAK,CAAC,KAAK,CAAC,KAAa,EAAE,SAAiB,EAAE,OAAe,EAAE,IAAU;QAC7E,IAAI,CAAC,IAAI,CAAC,OAAO;YAAE,OAAM;QAEzB,MAAM,IAAI,CAAC,YAAY,EAAE,CAAA;QAEzB,MAAM,SAAS,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAA;QAC1C,MAAM,QAAQ,GAAG;YACb,SAAS;YACT,KAAK;YACL,SAAS;YACT,OAAO;YACP,GAAG,CAAC,IAAI,IAAI,EAAE,IAAI,EAAE,CAAC;SACxB,CAAA;QAED,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAA;QAClF,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,GAAG,IAAI,CAAA;QAE/C,MAAM,SAAS,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,CAAA;IACpD,CAAC;IAED,IAAI,CAAC,SAAiB,EAAE,OAAe,EAAE,IAAU;QAC/C,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,CAAC,CAAA;IACvD,CAAC;IAED,KAAK,CAAC,SAAiB,EAAE,OAAe,EAAE,IAAU;QAChD,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,CAAC,CAAA;IACxD,CAAC;IAED,IAAI,CAAC,SAAiB,EAAE,OAAe,EAAE,IAAU;QAC/C,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,CAAC,CAAA;IACvD,CAAC;IAED,KAAK,CAAC,SAAiB,EAAE,OAAe,EAAE,IAAU;QAChD,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,CAAC,CAAA;IACxD,CAAC;CACJ"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface PruningMetadata {
|
|
2
|
+
prunedIds: string[];
|
|
3
|
+
lastPruneCount: number;
|
|
4
|
+
}
|
|
5
|
+
export declare class StateManager {
|
|
6
|
+
private state;
|
|
7
|
+
get(sessionID: string): Promise<string[]>;
|
|
8
|
+
set(sessionID: string, prunedIds: string[]): Promise<void>;
|
|
9
|
+
getLastPruneCount(sessionID: string): Promise<number>;
|
|
10
|
+
resetLastPruneCount(sessionID: string): Promise<void>;
|
|
11
|
+
clear(sessionID: string): Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=state.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../../lib/state.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,eAAe;IAC5B,SAAS,EAAE,MAAM,EAAE,CAAA;IACnB,cAAc,EAAE,MAAM,CAAA;CACzB;AAED,qBAAa,YAAY;IACrB,OAAO,CAAC,KAAK,CAA0C;IAEjD,GAAG,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAIzC,GAAG,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAW1D,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAIrD,mBAAmB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAOrD,KAAK,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAGhD"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// lib/state.ts
|
|
2
|
+
export class StateManager {
|
|
3
|
+
state = new Map();
|
|
4
|
+
async get(sessionID) {
|
|
5
|
+
return this.state.get(sessionID)?.prunedIds ?? [];
|
|
6
|
+
}
|
|
7
|
+
async set(sessionID, prunedIds) {
|
|
8
|
+
const existing = this.state.get(sessionID);
|
|
9
|
+
const previousCount = existing?.prunedIds.length ?? 0;
|
|
10
|
+
const newCount = prunedIds.length - previousCount;
|
|
11
|
+
this.state.set(sessionID, {
|
|
12
|
+
prunedIds,
|
|
13
|
+
lastPruneCount: newCount
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
async getLastPruneCount(sessionID) {
|
|
17
|
+
return this.state.get(sessionID)?.lastPruneCount ?? 0;
|
|
18
|
+
}
|
|
19
|
+
async resetLastPruneCount(sessionID) {
|
|
20
|
+
const existing = this.state.get(sessionID);
|
|
21
|
+
if (existing) {
|
|
22
|
+
existing.lastPruneCount = 0;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
async clear(sessionID) {
|
|
26
|
+
this.state.delete(sessionID);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=state.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"state.js","sourceRoot":"","sources":["../../lib/state.ts"],"names":[],"mappings":"AAAA,eAAe;AAOf,MAAM,OAAO,YAAY;IACb,KAAK,GAAiC,IAAI,GAAG,EAAE,CAAA;IAEvD,KAAK,CAAC,GAAG,CAAC,SAAiB;QACvB,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,SAAS,IAAI,EAAE,CAAA;IACrD,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,SAAiB,EAAE,SAAmB;QAC5C,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC,CAAA;QAC1C,MAAM,aAAa,GAAG,QAAQ,EAAE,SAAS,CAAC,MAAM,IAAI,CAAC,CAAA;QACrD,MAAM,QAAQ,GAAG,SAAS,CAAC,MAAM,GAAG,aAAa,CAAA;QAEjD,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,SAAS,EAAE;YACtB,SAAS;YACT,cAAc,EAAE,QAAQ;SAC3B,CAAC,CAAA;IACN,CAAC;IAED,KAAK,CAAC,iBAAiB,CAAC,SAAiB;QACrC,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,cAAc,IAAI,CAAC,CAAA;IACzD,CAAC;IAED,KAAK,CAAC,mBAAmB,CAAC,SAAiB;QACvC,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC,CAAA;QAC1C,IAAI,QAAQ,EAAE,CAAC;YACX,QAAQ,CAAC,cAAc,GAAG,CAAC,CAAA;QAC/B,CAAC;IACL,CAAC;IAED,KAAK,CAAC,KAAK,CAAC,SAAiB;QACzB,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,SAAS,CAAC,CAAA;IAChC,CAAC;CACJ"}
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json.schemastore.org/package.json",
|
|
3
|
+
"name": "@tarquinen/opencode-dcp",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "OpenCode plugin that optimizes token usage by pruning obsolete tool outputs from conversation context",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc",
|
|
11
|
+
"postbuild": "rm -rf dist/logs",
|
|
12
|
+
"prepublishOnly": "npm run build",
|
|
13
|
+
"dev": "opencode plugin dev",
|
|
14
|
+
"typecheck": "tsc --noEmit"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"opencode",
|
|
18
|
+
"opencode-plugin",
|
|
19
|
+
"plugin",
|
|
20
|
+
"context",
|
|
21
|
+
"pruning",
|
|
22
|
+
"optimization",
|
|
23
|
+
"tokens"
|
|
24
|
+
],
|
|
25
|
+
"author": "tarquinen",
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"@opencode-ai/plugin": "^0.13.7"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@ai-sdk/openai-compatible": "^1.0.0",
|
|
32
|
+
"@opencode-ai/sdk": "latest",
|
|
33
|
+
"ai": "^5.0.0",
|
|
34
|
+
"zod": "^3.23.0"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@opencode-ai/plugin": "latest",
|
|
38
|
+
"@types/node": "^22.0.0",
|
|
39
|
+
"typescript": "^5.7.0"
|
|
40
|
+
},
|
|
41
|
+
"files": [
|
|
42
|
+
"dist/",
|
|
43
|
+
"README.md",
|
|
44
|
+
"LICENSE"
|
|
45
|
+
]
|
|
46
|
+
}
|