opencode-memory-pro 1.3.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 +24 -0
- package/README.md +409 -0
- package/dist/config.d.ts +3 -0
- package/dist/config.js +398 -0
- package/dist/embedder.d.ts +26 -0
- package/dist/embedder.js +260 -0
- package/dist/extract.d.ts +4 -0
- package/dist/extract.js +181 -0
- package/dist/graph.js +701 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +953 -0
- package/dist/llm.d.ts +14 -0
- package/dist/llm.js +212 -0
- package/dist/logger.d.ts +9 -0
- package/dist/logger.js +126 -0
- package/dist/ports.d.ts +34 -0
- package/dist/ports.js +129 -0
- package/dist/preference.d.ts +10 -0
- package/dist/preference.js +125 -0
- package/dist/scope.d.ts +2 -0
- package/dist/scope.js +48 -0
- package/dist/store.d.ts +194 -0
- package/dist/store.js +2738 -0
- package/dist/summarize.d.ts +52 -0
- package/dist/summarize.js +350 -0
- package/dist/tools/episodic.d.ts +68 -0
- package/dist/tools/episodic.js +145 -0
- package/dist/tools/feedback.d.ts +51 -0
- package/dist/tools/feedback.js +112 -0
- package/dist/tools/index.d.ts +3 -0
- package/dist/tools/index.js +3 -0
- package/dist/tools/memory.d.ts +293 -0
- package/dist/tools/memory.js +1487 -0
- package/dist/types.d.ts +489 -0
- package/dist/types.js +54 -0
- package/dist/utils.d.ts +18 -0
- package/dist/utils.js +214 -0
- package/package.json +49 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { ContentType, ContentDetection, SummarizedContent, SummarizationConfig, InjectionConfig, SearchResult } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Detects whether content contains code and its type
|
|
4
|
+
*/
|
|
5
|
+
export declare function detectContentType(text: string): ContentDetection;
|
|
6
|
+
/**
|
|
7
|
+
* Calculates bracket balance for code detection
|
|
8
|
+
*/
|
|
9
|
+
export declare function calculateBracketBalance(text: string): number;
|
|
10
|
+
/**
|
|
11
|
+
* Counts code-related keywords
|
|
12
|
+
*/
|
|
13
|
+
export declare function countCodeKeywords(text: string): number;
|
|
14
|
+
/**
|
|
15
|
+
* Calculates ratio of indented lines
|
|
16
|
+
*/
|
|
17
|
+
export declare function calculateIndentationRatio(text: string): number;
|
|
18
|
+
/**
|
|
19
|
+
* Estimates token count for content
|
|
20
|
+
*/
|
|
21
|
+
export declare function estimateTokens(text: string, contentType: ContentType): number;
|
|
22
|
+
/**
|
|
23
|
+
* Truncates text to max characters
|
|
24
|
+
*/
|
|
25
|
+
export declare function truncateText(text: string, maxChars: number): string;
|
|
26
|
+
/**
|
|
27
|
+
* Smart truncation for code - finds complete statement boundaries
|
|
28
|
+
*/
|
|
29
|
+
export declare function smartTruncateCode(code: string, maxLines: number, config?: {
|
|
30
|
+
preserveComments?: boolean;
|
|
31
|
+
preserveImports?: boolean;
|
|
32
|
+
}): string;
|
|
33
|
+
/**
|
|
34
|
+
* Extracts key sentences from text
|
|
35
|
+
*/
|
|
36
|
+
export declare function extractKeySentences(text: string, targetChars: number): string;
|
|
37
|
+
export declare function splitCodeAndText(text: string): Array<{
|
|
38
|
+
type: "code" | "text";
|
|
39
|
+
content: string;
|
|
40
|
+
}>;
|
|
41
|
+
/**
|
|
42
|
+
* Main summarization function
|
|
43
|
+
*/
|
|
44
|
+
export declare function summarizeContent(text: string, config: SummarizationConfig): SummarizedContent;
|
|
45
|
+
/**
|
|
46
|
+
* Calculates injection limit based on mode
|
|
47
|
+
*/
|
|
48
|
+
export declare function calculateInjectionLimit(results: SearchResult[], config: InjectionConfig): number;
|
|
49
|
+
/**
|
|
50
|
+
* Creates default summarization config from injection config
|
|
51
|
+
*/
|
|
52
|
+
export declare function createSummarizationConfig(injection: InjectionConfig): SummarizationConfig;
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
// Code keywords used for content detection
|
|
2
|
+
const CODE_KEYWORDS = [
|
|
3
|
+
"function", "async", "await", "const", "let", "var", "return", "class", "interface", "type",
|
|
4
|
+
"import", "export", "from", "default", "extends", "implements", "new", "this", "super",
|
|
5
|
+
"def ", "async def", "func ", "fn ", "pub fn", "impl ", "struct ", "enum ",
|
|
6
|
+
"=>", "->", "::", "if (", "for (", "while (", "try {", "catch (", "throw ",
|
|
7
|
+
];
|
|
8
|
+
// Keywords for key sentence extraction
|
|
9
|
+
const KEY_SENTENCE_PATTERNS = [
|
|
10
|
+
/(?:fixed|resolved|works?\s+now|successful|done|完成|已解決|修復|成功)/i,
|
|
11
|
+
/(?:probleme|issue|bug|error|fail|錯誤|問題|失敗)/i,
|
|
12
|
+
/(?:solution|fix|resolve|解決方案|修正)/i,
|
|
13
|
+
/(?:because|root\s+cause|原因|由於)/i,
|
|
14
|
+
/(?:decide|decision|tradeoff|architecture|決定|架構|採用)/i,
|
|
15
|
+
/(?:prefer|preference|偏好|習慣)/i,
|
|
16
|
+
];
|
|
17
|
+
/**
|
|
18
|
+
* Detects whether content contains code and its type
|
|
19
|
+
*/
|
|
20
|
+
export function detectContentType(text) {
|
|
21
|
+
const hasMarkdownCode = /```[\s\S]*?```/.test(text);
|
|
22
|
+
const bracketBalance = calculateBracketBalance(text);
|
|
23
|
+
const codeKeywords = countCodeKeywords(text);
|
|
24
|
+
const indentationRatio = calculateIndentationRatio(text);
|
|
25
|
+
const codeScore = (hasMarkdownCode ? 2 : 0) +
|
|
26
|
+
(bracketBalance > 3 ? 1 : 0) +
|
|
27
|
+
(codeKeywords > 5 ? 1 : 0) +
|
|
28
|
+
(indentationRatio > 0.3 ? 1 : 0);
|
|
29
|
+
if (codeScore >= 5) {
|
|
30
|
+
return { hasCode: true, isPureCode: true };
|
|
31
|
+
}
|
|
32
|
+
if (codeScore >= 3) {
|
|
33
|
+
return { hasCode: true, isPureCode: false };
|
|
34
|
+
}
|
|
35
|
+
if (hasMarkdownCode || codeKeywords > 10) {
|
|
36
|
+
return { hasCode: true, isPureCode: false };
|
|
37
|
+
}
|
|
38
|
+
return { hasCode: false, isPureCode: false };
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Calculates bracket balance for code detection
|
|
42
|
+
*/
|
|
43
|
+
export function calculateBracketBalance(text) {
|
|
44
|
+
const openBrackets = (text.match(/[{([]/g) || []).length;
|
|
45
|
+
const closeBrackets = (text.match(/[})\]]/g) || []).length;
|
|
46
|
+
return Math.abs(openBrackets - closeBrackets) + Math.min(openBrackets, closeBrackets);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Counts code-related keywords
|
|
50
|
+
*/
|
|
51
|
+
export function countCodeKeywords(text) {
|
|
52
|
+
const lower = text.toLowerCase();
|
|
53
|
+
let count = 0;
|
|
54
|
+
for (const keyword of CODE_KEYWORDS) {
|
|
55
|
+
if (lower.includes(keyword.toLowerCase())) {
|
|
56
|
+
count += 1;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return count;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Calculates ratio of indented lines
|
|
63
|
+
*/
|
|
64
|
+
export function calculateIndentationRatio(text) {
|
|
65
|
+
const lines = text.split("\n");
|
|
66
|
+
if (lines.length === 0)
|
|
67
|
+
return 0;
|
|
68
|
+
const indentedLines = lines.filter((line) => /^\s{2,}/.test(line));
|
|
69
|
+
return indentedLines.length / lines.length;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Estimates token count for content
|
|
73
|
+
*/
|
|
74
|
+
export function estimateTokens(text, contentType) {
|
|
75
|
+
// Count Chinese characters
|
|
76
|
+
const chineseChars = (text.match(/[\u4e00-\u9fff]/g) || []).length;
|
|
77
|
+
const nonChineseChars = text.length - chineseChars;
|
|
78
|
+
// Chinese ~2 chars/token, English/other ~4 chars/token
|
|
79
|
+
const baseTokens = Math.ceil(chineseChars / 2 + nonChineseChars / 4);
|
|
80
|
+
// Code has higher token density
|
|
81
|
+
if (contentType === "code") {
|
|
82
|
+
return Math.ceil(baseTokens * 1.2);
|
|
83
|
+
}
|
|
84
|
+
return baseTokens;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Truncates text to max characters
|
|
88
|
+
*/
|
|
89
|
+
export function truncateText(text, maxChars) {
|
|
90
|
+
if (text.length <= maxChars)
|
|
91
|
+
return text;
|
|
92
|
+
return `${text.slice(0, maxChars - 3)}...`;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Smart truncation for code - finds complete statement boundaries
|
|
96
|
+
*/
|
|
97
|
+
export function smartTruncateCode(code, maxLines, config) {
|
|
98
|
+
const lines = code.split("\n");
|
|
99
|
+
if (lines.length <= maxLines)
|
|
100
|
+
return code;
|
|
101
|
+
let braceBalance = 0;
|
|
102
|
+
let lastCompleteIndex = maxLines;
|
|
103
|
+
let foundComplete = false;
|
|
104
|
+
// Calculate brace balance and find last complete statement
|
|
105
|
+
for (let i = 0; i < Math.min(lines.length, maxLines + 10); i++) {
|
|
106
|
+
const line = lines[i];
|
|
107
|
+
braceBalance += (line.match(/{/g) || []).length;
|
|
108
|
+
braceBalance -= (line.match(/}/g) || []).length;
|
|
109
|
+
if (i >= maxLines - 5 && braceBalance === 0 && i < lines.length - 1) {
|
|
110
|
+
lastCompleteIndex = i + 1;
|
|
111
|
+
foundComplete = true;
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// If no complete boundary found, use maxLines
|
|
116
|
+
if (!foundComplete) {
|
|
117
|
+
lastCompleteIndex = maxLines;
|
|
118
|
+
}
|
|
119
|
+
// Build truncated code
|
|
120
|
+
let result = lines.slice(0, lastCompleteIndex).join("\n");
|
|
121
|
+
// Add truncation indicator
|
|
122
|
+
result += "\n// ... (truncated)";
|
|
123
|
+
return result;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Extracts key sentences from text
|
|
127
|
+
*/
|
|
128
|
+
export function extractKeySentences(text, targetChars) {
|
|
129
|
+
const sentences = text.split(/[。.!?\n]+/).filter((s) => s.trim().length > 0);
|
|
130
|
+
const keySentences = [];
|
|
131
|
+
let currentLength = 0;
|
|
132
|
+
// First pass: sentences matching key patterns
|
|
133
|
+
for (const sentence of sentences) {
|
|
134
|
+
const trimmed = sentence.trim();
|
|
135
|
+
if (KEY_SENTENCE_PATTERNS.some((pattern) => pattern.test(trimmed))) {
|
|
136
|
+
if (currentLength + trimmed.length > targetChars && keySentences.length > 0) {
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
keySentences.push(trimmed);
|
|
140
|
+
currentLength += trimmed.length + 1;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// Second pass: fill remaining with first sentences if needed
|
|
144
|
+
if (currentLength < targetChars * 0.5) {
|
|
145
|
+
for (const sentence of sentences) {
|
|
146
|
+
const trimmed = sentence.trim();
|
|
147
|
+
if (!keySentences.includes(trimmed)) {
|
|
148
|
+
if (currentLength + trimmed.length > targetChars) {
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
keySentences.push(trimmed);
|
|
152
|
+
currentLength += trimmed.length + 1;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return keySentences.join(" → ");
|
|
157
|
+
}
|
|
158
|
+
export function splitCodeAndText(text) {
|
|
159
|
+
const parts = [];
|
|
160
|
+
const codeBlockRegex = /```[\s\S]*?```/g;
|
|
161
|
+
let lastIndex = 0;
|
|
162
|
+
let match = codeBlockRegex.exec(text);
|
|
163
|
+
while (match !== null) {
|
|
164
|
+
if (match.index > lastIndex) {
|
|
165
|
+
const textPart = text.slice(lastIndex, match.index).trim();
|
|
166
|
+
if (textPart) {
|
|
167
|
+
parts.push({ type: "text", content: textPart });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
parts.push({ type: "code", content: match[0] });
|
|
171
|
+
lastIndex = match.index + match[0].length;
|
|
172
|
+
match = codeBlockRegex.exec(text);
|
|
173
|
+
}
|
|
174
|
+
if (lastIndex < text.length) {
|
|
175
|
+
const remaining = text.slice(lastIndex).trim();
|
|
176
|
+
if (remaining) {
|
|
177
|
+
parts.push({ type: "text", content: remaining });
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return parts;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Main summarization function
|
|
184
|
+
*/
|
|
185
|
+
export function summarizeContent(text, config) {
|
|
186
|
+
const detection = detectContentType(text);
|
|
187
|
+
const originalLength = text.length;
|
|
188
|
+
// Determine content type
|
|
189
|
+
const contentType = detection.isPureCode
|
|
190
|
+
? "code"
|
|
191
|
+
: detection.hasCode
|
|
192
|
+
? "mixed"
|
|
193
|
+
: "text";
|
|
194
|
+
// No summarization
|
|
195
|
+
if (config.mode === "none") {
|
|
196
|
+
return {
|
|
197
|
+
type: "kept",
|
|
198
|
+
content: truncateText(text, config.textThreshold * 4), // Max chars limit
|
|
199
|
+
originalLength,
|
|
200
|
+
estimatedTokens: estimateTokens(text, contentType),
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
// Pure text
|
|
204
|
+
if (contentType === "text") {
|
|
205
|
+
if (text.length <= config.textThreshold) {
|
|
206
|
+
return {
|
|
207
|
+
type: "kept",
|
|
208
|
+
content: text,
|
|
209
|
+
originalLength,
|
|
210
|
+
estimatedTokens: estimateTokens(text, contentType),
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
if (config.mode === "truncate") {
|
|
214
|
+
const truncated = truncateText(text, config.summaryTargetChars);
|
|
215
|
+
return {
|
|
216
|
+
type: "truncated",
|
|
217
|
+
content: truncated,
|
|
218
|
+
originalLength,
|
|
219
|
+
estimatedTokens: estimateTokens(truncated, contentType),
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
const extracted = extractKeySentences(text, config.summaryTargetChars);
|
|
223
|
+
return {
|
|
224
|
+
type: "summarized",
|
|
225
|
+
content: extracted,
|
|
226
|
+
originalLength,
|
|
227
|
+
estimatedTokens: estimateTokens(extracted, contentType),
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
// Pure code
|
|
231
|
+
if (contentType === "code") {
|
|
232
|
+
if (text.length <= config.codeThreshold) {
|
|
233
|
+
return {
|
|
234
|
+
type: "kept",
|
|
235
|
+
content: text,
|
|
236
|
+
originalLength,
|
|
237
|
+
estimatedTokens: estimateTokens(text, contentType),
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
const truncated = smartTruncateCode(text, config.maxCodeLines, {
|
|
241
|
+
preserveComments: config.preserveComments,
|
|
242
|
+
preserveImports: config.preserveImports,
|
|
243
|
+
});
|
|
244
|
+
return {
|
|
245
|
+
type: "truncated",
|
|
246
|
+
content: truncated,
|
|
247
|
+
originalLength,
|
|
248
|
+
estimatedTokens: estimateTokens(truncated, contentType),
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
// Mixed content
|
|
252
|
+
if (config.mode === "auto" || config.mode === "extract") {
|
|
253
|
+
const parts = splitCodeAndText(text);
|
|
254
|
+
const summarizedParts = [];
|
|
255
|
+
for (const part of parts) {
|
|
256
|
+
if (part.type === "text") {
|
|
257
|
+
if (part.content.length <= config.textThreshold) {
|
|
258
|
+
summarizedParts.push(part.content);
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
summarizedParts.push(extractKeySentences(part.content, config.summaryTargetChars / 2));
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
if (part.content.length <= config.codeThreshold) {
|
|
266
|
+
summarizedParts.push(part.content);
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
summarizedParts.push(smartTruncateCode(part.content, config.maxCodeLines));
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return {
|
|
274
|
+
type: "mixed",
|
|
275
|
+
content: summarizedParts.join("\n\n"),
|
|
276
|
+
originalLength,
|
|
277
|
+
estimatedTokens: estimateTokens(summarizedParts.join("\n\n"), contentType),
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
// Fallback: truncate
|
|
281
|
+
return {
|
|
282
|
+
type: "truncated",
|
|
283
|
+
content: truncateText(text, config.summaryTargetChars),
|
|
284
|
+
originalLength,
|
|
285
|
+
estimatedTokens: estimateTokens(truncateText(text, config.summaryTargetChars), contentType),
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Calculates injection limit based on mode
|
|
290
|
+
*/
|
|
291
|
+
export function calculateInjectionLimit(results, config) {
|
|
292
|
+
// Filter by injection floor
|
|
293
|
+
const filteredResults = results.filter((r) => r.score >= config.injectionFloor);
|
|
294
|
+
// Fixed mode: simple limit
|
|
295
|
+
if (config.mode === "fixed") {
|
|
296
|
+
return Math.min(config.maxMemories, filteredResults.length);
|
|
297
|
+
}
|
|
298
|
+
// Budget mode: accumulate until budget exhausted
|
|
299
|
+
if (config.mode === "budget") {
|
|
300
|
+
let accumulatedTokens = 0;
|
|
301
|
+
let count = 0;
|
|
302
|
+
for (const result of filteredResults) {
|
|
303
|
+
const tokens = estimateTokens(result.record.text, detectContentType(result.record.text).isPureCode ? "code" : "text");
|
|
304
|
+
if (accumulatedTokens + tokens > config.budgetTokens && count >= config.minMemories) {
|
|
305
|
+
break;
|
|
306
|
+
}
|
|
307
|
+
accumulatedTokens += tokens;
|
|
308
|
+
count += 1;
|
|
309
|
+
if (count >= config.maxMemories) {
|
|
310
|
+
break;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return Math.max(config.minMemories, Math.min(count, config.maxMemories));
|
|
314
|
+
}
|
|
315
|
+
// Adaptive mode: stop on score drop
|
|
316
|
+
if (config.mode === "adaptive") {
|
|
317
|
+
let count = 0;
|
|
318
|
+
let prevScore = filteredResults[0]?.score ?? 0;
|
|
319
|
+
for (const result of filteredResults) {
|
|
320
|
+
const scoreDrop = prevScore - result.score;
|
|
321
|
+
// Stop if score drops below tolerance (but respect minimum)
|
|
322
|
+
if (scoreDrop > config.scoreDropTolerance && count >= config.minMemories) {
|
|
323
|
+
break;
|
|
324
|
+
}
|
|
325
|
+
count += 1;
|
|
326
|
+
prevScore = result.score;
|
|
327
|
+
if (count >= config.maxMemories) {
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return Math.max(config.minMemories, Math.min(count, filteredResults.length));
|
|
332
|
+
}
|
|
333
|
+
// Fallback
|
|
334
|
+
return Math.min(config.maxMemories, filteredResults.length);
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Creates default summarization config from injection config
|
|
338
|
+
*/
|
|
339
|
+
export function createSummarizationConfig(injection) {
|
|
340
|
+
return {
|
|
341
|
+
mode: injection.summarization,
|
|
342
|
+
textThreshold: 300,
|
|
343
|
+
codeThreshold: injection.codeSummarization.pureCodeThreshold,
|
|
344
|
+
summaryTargetChars: injection.summaryTargetChars,
|
|
345
|
+
maxCodeLines: injection.codeSummarization.maxCodeLines,
|
|
346
|
+
codeTruncationMode: injection.codeSummarization.codeTruncationMode,
|
|
347
|
+
preserveComments: injection.codeSummarization.preserveComments,
|
|
348
|
+
preserveImports: injection.codeSummarization.preserveImports,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { ToolRuntimeState } from "./memory.js";
|
|
2
|
+
export declare function createEpisodicTools(state: ToolRuntimeState): {
|
|
3
|
+
task_episode_create: {
|
|
4
|
+
description: string;
|
|
5
|
+
args: {
|
|
6
|
+
taskId: import("zod").ZodString;
|
|
7
|
+
scope: import("zod").ZodOptional<import("zod").ZodString>;
|
|
8
|
+
description: import("zod").ZodOptional<import("zod").ZodString>;
|
|
9
|
+
};
|
|
10
|
+
execute(args: {
|
|
11
|
+
taskId: string;
|
|
12
|
+
scope?: string | undefined;
|
|
13
|
+
description?: string | undefined;
|
|
14
|
+
}, context: import("@opencode-ai/plugin").ToolContext): Promise<string>;
|
|
15
|
+
};
|
|
16
|
+
task_episode_query: {
|
|
17
|
+
description: string;
|
|
18
|
+
args: {
|
|
19
|
+
scope: import("zod").ZodOptional<import("zod").ZodString>;
|
|
20
|
+
state: import("zod").ZodOptional<import("zod").ZodString>;
|
|
21
|
+
limit: import("zod").ZodDefault<import("zod").ZodNumber>;
|
|
22
|
+
};
|
|
23
|
+
execute(args: {
|
|
24
|
+
limit: number;
|
|
25
|
+
scope?: string | undefined;
|
|
26
|
+
state?: string | undefined;
|
|
27
|
+
}, context: import("@opencode-ai/plugin").ToolContext): Promise<string>;
|
|
28
|
+
};
|
|
29
|
+
similar_task_recall: {
|
|
30
|
+
description: string;
|
|
31
|
+
args: {
|
|
32
|
+
query: import("zod").ZodString;
|
|
33
|
+
threshold: import("zod").ZodDefault<import("zod").ZodNumber>;
|
|
34
|
+
limit: import("zod").ZodDefault<import("zod").ZodNumber>;
|
|
35
|
+
scope: import("zod").ZodOptional<import("zod").ZodString>;
|
|
36
|
+
};
|
|
37
|
+
execute(args: {
|
|
38
|
+
query: string;
|
|
39
|
+
threshold: number;
|
|
40
|
+
limit: number;
|
|
41
|
+
scope?: string | undefined;
|
|
42
|
+
}, context: import("@opencode-ai/plugin").ToolContext): Promise<string>;
|
|
43
|
+
};
|
|
44
|
+
retry_budget_suggest: {
|
|
45
|
+
description: string;
|
|
46
|
+
args: {
|
|
47
|
+
errorType: import("zod").ZodString;
|
|
48
|
+
minSamples: import("zod").ZodDefault<import("zod").ZodNumber>;
|
|
49
|
+
scope: import("zod").ZodOptional<import("zod").ZodString>;
|
|
50
|
+
};
|
|
51
|
+
execute(args: {
|
|
52
|
+
errorType: string;
|
|
53
|
+
minSamples: number;
|
|
54
|
+
scope?: string | undefined;
|
|
55
|
+
}, context: import("@opencode-ai/plugin").ToolContext): Promise<string>;
|
|
56
|
+
};
|
|
57
|
+
recovery_strategy_suggest: {
|
|
58
|
+
description: string;
|
|
59
|
+
args: {
|
|
60
|
+
taskId: import("zod").ZodString;
|
|
61
|
+
scope: import("zod").ZodOptional<import("zod").ZodString>;
|
|
62
|
+
};
|
|
63
|
+
execute(args: {
|
|
64
|
+
taskId: string;
|
|
65
|
+
scope?: string | undefined;
|
|
66
|
+
}, context: import("@opencode-ai/plugin").ToolContext): Promise<string>;
|
|
67
|
+
};
|
|
68
|
+
};
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { tool } from "@opencode-ai/plugin";
|
|
2
|
+
import { deriveProjectScope } from "../scope.js";
|
|
3
|
+
import { generateId, parseJsonObject } from "../utils.js";
|
|
4
|
+
function unavailableMessage(provider) {
|
|
5
|
+
return `Memory store unavailable (${provider} embedding may be offline). Will retry automatically.`;
|
|
6
|
+
}
|
|
7
|
+
export function createEpisodicTools(state) {
|
|
8
|
+
return {
|
|
9
|
+
task_episode_create: tool({
|
|
10
|
+
description: "Create a new task episode record for tracking",
|
|
11
|
+
args: {
|
|
12
|
+
taskId: tool.schema.string().min(1),
|
|
13
|
+
scope: tool.schema.string().optional(),
|
|
14
|
+
description: tool.schema.string().optional(),
|
|
15
|
+
},
|
|
16
|
+
execute: async (args, context) => {
|
|
17
|
+
await state.ensureInitialized();
|
|
18
|
+
if (!state.initialized)
|
|
19
|
+
return unavailableMessage(state.config.embedding.provider);
|
|
20
|
+
const activeScope = args.scope ?? deriveProjectScope(context.directory || context.worktree);
|
|
21
|
+
const episode = {
|
|
22
|
+
id: generateId(),
|
|
23
|
+
sessionId: context.sessionID,
|
|
24
|
+
scope: activeScope,
|
|
25
|
+
taskId: args.taskId,
|
|
26
|
+
state: "pending",
|
|
27
|
+
startTime: Date.now(),
|
|
28
|
+
endTime: 0,
|
|
29
|
+
commandsJson: "[]",
|
|
30
|
+
validationOutcomesJson: "[]",
|
|
31
|
+
successPatternsJson: "[]",
|
|
32
|
+
retryAttemptsJson: "[]",
|
|
33
|
+
recoveryStrategiesJson: "[]",
|
|
34
|
+
metadataJson: JSON.stringify({ description: args.description }),
|
|
35
|
+
};
|
|
36
|
+
await state.store.createTaskEpisode(episode);
|
|
37
|
+
return `Created task episode ${episode.id} for task ${args.taskId} in scope ${activeScope}`;
|
|
38
|
+
},
|
|
39
|
+
}),
|
|
40
|
+
task_episode_query: tool({
|
|
41
|
+
description: "Query task episodes by scope and state",
|
|
42
|
+
args: {
|
|
43
|
+
scope: tool.schema.string().optional(),
|
|
44
|
+
state: tool.schema.string().optional(),
|
|
45
|
+
limit: tool.schema.number().int().min(1).max(100).default(10),
|
|
46
|
+
},
|
|
47
|
+
execute: async (args, context) => {
|
|
48
|
+
await state.ensureInitialized();
|
|
49
|
+
if (!state.initialized)
|
|
50
|
+
return unavailableMessage(state.config.embedding.provider);
|
|
51
|
+
const activeScope = args.scope ?? deriveProjectScope(context.directory || context.worktree);
|
|
52
|
+
const stateFilter = args.state;
|
|
53
|
+
const episodes = await state.store.queryTaskEpisodes(activeScope, stateFilter);
|
|
54
|
+
if (episodes.length === 0) {
|
|
55
|
+
return `No task episodes found in scope ${activeScope}`;
|
|
56
|
+
}
|
|
57
|
+
const limited = episodes.slice(0, args.limit ?? 10);
|
|
58
|
+
return limited.map((ep) => {
|
|
59
|
+
const meta = parseJsonObject(ep.metadataJson, {});
|
|
60
|
+
return `[${ep.id}] ${ep.taskId} - ${ep.state} (${new Date(ep.startTime).toISOString().split("T")[0]}) ${meta.description ? `- ${meta.description}` : ""}`;
|
|
61
|
+
}).join("\n");
|
|
62
|
+
},
|
|
63
|
+
}),
|
|
64
|
+
similar_task_recall: tool({
|
|
65
|
+
description: "Find similar past tasks using semantic search",
|
|
66
|
+
args: {
|
|
67
|
+
query: tool.schema.string().min(1),
|
|
68
|
+
threshold: tool.schema.number().min(0).max(1).default(0.85),
|
|
69
|
+
limit: tool.schema.number().int().min(1).max(10).default(3),
|
|
70
|
+
scope: tool.schema.string().optional(),
|
|
71
|
+
},
|
|
72
|
+
execute: async (args, context) => {
|
|
73
|
+
await state.ensureInitialized();
|
|
74
|
+
if (!state.initialized)
|
|
75
|
+
return unavailableMessage(state.config.embedding.provider);
|
|
76
|
+
const activeScope = args.scope ?? deriveProjectScope(context.directory || context.worktree);
|
|
77
|
+
let queryVector = [];
|
|
78
|
+
try {
|
|
79
|
+
queryVector = await state.embedder.embed(args.query);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
queryVector = [];
|
|
83
|
+
}
|
|
84
|
+
const similar = await state.store.findSimilarTasks(activeScope, args.query, args.threshold ?? 0.85, queryVector);
|
|
85
|
+
if (similar.length === 0) {
|
|
86
|
+
return `No similar tasks found for "${args.query}"`;
|
|
87
|
+
}
|
|
88
|
+
const limited = similar.slice(0, args.limit ?? 3);
|
|
89
|
+
return limited.map((ep) => {
|
|
90
|
+
const commands = parseJsonObject(ep.commandsJson, []);
|
|
91
|
+
const outcomes = parseJsonObject(ep.validationOutcomesJson, []);
|
|
92
|
+
return `Task: ${ep.taskId} (${ep.state})
|
|
93
|
+
Commands: ${commands.slice(0, 3).join(" → ")}
|
|
94
|
+
Validations: ${outcomes.map((o) => `${o.type}:${o.status}`).join(", ") || "none"}
|
|
95
|
+
`;
|
|
96
|
+
}).join("\n");
|
|
97
|
+
},
|
|
98
|
+
}),
|
|
99
|
+
retry_budget_suggest: tool({
|
|
100
|
+
description: "Get retry budget suggestion based on historical data",
|
|
101
|
+
args: {
|
|
102
|
+
errorType: tool.schema.string(),
|
|
103
|
+
minSamples: tool.schema.number().int().min(1).default(3),
|
|
104
|
+
scope: tool.schema.string().optional(),
|
|
105
|
+
},
|
|
106
|
+
execute: async (args, context) => {
|
|
107
|
+
await state.ensureInitialized();
|
|
108
|
+
if (!state.initialized)
|
|
109
|
+
return unavailableMessage(state.config.embedding.provider);
|
|
110
|
+
const activeScope = args.scope ?? deriveProjectScope(context.directory || context.worktree);
|
|
111
|
+
const result = await state.store.suggestRetryBudget(activeScope, args.minSamples ?? 3);
|
|
112
|
+
if (!result) {
|
|
113
|
+
return `Insufficient data for retry budget suggestion (need at least ${args.minSamples} failed tasks)`;
|
|
114
|
+
}
|
|
115
|
+
return JSON.stringify({
|
|
116
|
+
suggestedRetries: result.suggestedRetries,
|
|
117
|
+
confidence: result.confidence.toFixed(2),
|
|
118
|
+
basedOnCount: result.basedOnCount,
|
|
119
|
+
shouldStop: result.shouldStop,
|
|
120
|
+
stopReason: result.stopReason,
|
|
121
|
+
}, null, 2);
|
|
122
|
+
},
|
|
123
|
+
}),
|
|
124
|
+
recovery_strategy_suggest: tool({
|
|
125
|
+
description: "Get recovery strategy suggestions after failures",
|
|
126
|
+
args: {
|
|
127
|
+
taskId: tool.schema.string().min(1),
|
|
128
|
+
scope: tool.schema.string().optional(),
|
|
129
|
+
},
|
|
130
|
+
execute: async (args, context) => {
|
|
131
|
+
await state.ensureInitialized();
|
|
132
|
+
if (!state.initialized)
|
|
133
|
+
return unavailableMessage(state.config.embedding.provider);
|
|
134
|
+
const activeScope = args.scope ?? deriveProjectScope(context.directory || context.worktree);
|
|
135
|
+
const strategies = await state.store.suggestRecoveryStrategies(activeScope, args.taskId);
|
|
136
|
+
if (strategies.length === 0) {
|
|
137
|
+
return `No recovery strategies found for task ${args.taskId}`;
|
|
138
|
+
}
|
|
139
|
+
return strategies.map((s) => {
|
|
140
|
+
return `- ${s.strategy}: ${s.reason} (confidence: ${s.confidence.toFixed(2)}${s.basedOnTask ? `, based on: ${s.basedOnTask}` : ""})`;
|
|
141
|
+
}).join("\n");
|
|
142
|
+
},
|
|
143
|
+
}),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { ToolRuntimeState } from "./memory.js";
|
|
2
|
+
export declare function createFeedbackTools(state: ToolRuntimeState): {
|
|
3
|
+
memory_feedback_missing: {
|
|
4
|
+
description: string;
|
|
5
|
+
args: {
|
|
6
|
+
text: import("zod").ZodString;
|
|
7
|
+
labels: import("zod").ZodDefault<import("zod").ZodArray<import("zod").ZodString>>;
|
|
8
|
+
scope: import("zod").ZodOptional<import("zod").ZodString>;
|
|
9
|
+
};
|
|
10
|
+
execute(args: {
|
|
11
|
+
text: string;
|
|
12
|
+
labels: string[];
|
|
13
|
+
scope?: string | undefined;
|
|
14
|
+
}, context: import("@opencode-ai/plugin").ToolContext): Promise<string>;
|
|
15
|
+
};
|
|
16
|
+
memory_feedback_wrong: {
|
|
17
|
+
description: string;
|
|
18
|
+
args: {
|
|
19
|
+
id: import("zod").ZodString;
|
|
20
|
+
reason: import("zod").ZodOptional<import("zod").ZodString>;
|
|
21
|
+
scope: import("zod").ZodOptional<import("zod").ZodString>;
|
|
22
|
+
};
|
|
23
|
+
execute(args: {
|
|
24
|
+
id: string;
|
|
25
|
+
reason?: string | undefined;
|
|
26
|
+
scope?: string | undefined;
|
|
27
|
+
}, context: import("@opencode-ai/plugin").ToolContext): Promise<string>;
|
|
28
|
+
};
|
|
29
|
+
memory_feedback_useful: {
|
|
30
|
+
description: string;
|
|
31
|
+
args: {
|
|
32
|
+
id: import("zod").ZodString;
|
|
33
|
+
helpful: import("zod").ZodBoolean;
|
|
34
|
+
scope: import("zod").ZodOptional<import("zod").ZodString>;
|
|
35
|
+
};
|
|
36
|
+
execute(args: {
|
|
37
|
+
id: string;
|
|
38
|
+
helpful: boolean;
|
|
39
|
+
scope?: string | undefined;
|
|
40
|
+
}, context: import("@opencode-ai/plugin").ToolContext): Promise<string>;
|
|
41
|
+
};
|
|
42
|
+
memory_effectiveness: {
|
|
43
|
+
description: string;
|
|
44
|
+
args: {
|
|
45
|
+
scope: import("zod").ZodOptional<import("zod").ZodString>;
|
|
46
|
+
};
|
|
47
|
+
execute(args: {
|
|
48
|
+
scope?: string | undefined;
|
|
49
|
+
}, context: import("@opencode-ai/plugin").ToolContext): Promise<string>;
|
|
50
|
+
};
|
|
51
|
+
};
|