opencode-fractal-memory 0.2.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 +493 -0
- package/agent/memory-hints.md +98 -0
- package/agent/memory-researcher.md +56 -0
- package/commands/memory-auto-test.md +10 -0
- package/commands/memory-cache-status.md +13 -0
- package/commands/memory-check-context.md +4 -0
- package/commands/memory-compress.md +13 -0
- package/commands/memory-dashboard.md +23 -0
- package/commands/memory-delete.md +24 -0
- package/commands/memory-detect-topics.md +28 -0
- package/commands/memory-distill.md +35 -0
- package/commands/memory-drilldown-query.md +28 -0
- package/commands/memory-drilldown.md +11 -0
- package/commands/memory-extract-patterns.md +4 -0
- package/commands/memory-generate-embeddings.md +26 -0
- package/commands/memory-get.md +26 -0
- package/commands/memory-help.md +55 -0
- package/commands/memory-injection-feedback.md +26 -0
- package/commands/memory-injection-stats.md +11 -0
- package/commands/memory-list.md +4 -0
- package/commands/memory-llm-compress.md +34 -0
- package/commands/memory-mcp.md +20 -0
- package/commands/memory-prune.md +4 -0
- package/commands/memory-rate.md +48 -0
- package/commands/memory-reflect.md +37 -0
- package/commands/memory-replace.md +26 -0
- package/commands/memory-retrieve.md +34 -0
- package/commands/memory-search.md +28 -0
- package/commands/memory-session-stats.md +4 -0
- package/commands/memory-set.md +31 -0
- package/commands/memory-stats.md +11 -0
- package/commands/memory-summarize.md +29 -0
- package/commands/memory-tool-stats.md +4 -0
- package/commands/memory-total-tokens.md +10 -0
- package/commands/memory-verify.md +4 -0
- package/commands/memory-version.md +9 -0
- package/dist/cache.js +39 -0
- package/dist/config.js +120 -0
- package/dist/embeddings.js +125 -0
- package/dist/ensure-models.js +70 -0
- package/dist/file-summary.js +143 -0
- package/dist/frontmatter.js +28 -0
- package/dist/hnsw-index.js +138 -0
- package/dist/hooks/auto-discover.js +4 -0
- package/dist/hooks/auto-distill.js +120 -0
- package/dist/hooks/auto-retrieve/content.js +47 -0
- package/dist/hooks/auto-retrieve/detection.js +50 -0
- package/dist/hooks/auto-retrieve/formatting.js +19 -0
- package/dist/hooks/auto-retrieve/index.js +163 -0
- package/dist/hooks/auto-retrieve/scoring.js +56 -0
- package/dist/hooks/auto-retrieve.js +1 -0
- package/dist/hooks/index.js +4 -0
- package/dist/hooks/predictive-rating.js +87 -0
- package/dist/journal.js +279 -0
- package/dist/logging.js +147 -0
- package/dist/management/helpers.js +227 -0
- package/dist/management/router.js +48 -0
- package/dist/management/routes.js +197 -0
- package/dist/management-server.js +4 -0
- package/dist/management-standalone.js +31 -0
- package/dist/mcp/logging.js +57 -0
- package/dist/mcp/server.js +251 -0
- package/dist/mcp/transform.js +48 -0
- package/dist/mcp-server.js +18 -0
- package/dist/memory.js +2 -0
- package/dist/ollama.js +74 -0
- package/dist/plugin/hooks.js +168 -0
- package/dist/plugin/index.js +28 -0
- package/dist/plugin/init.js +109 -0
- package/dist/plugin/state.js +75 -0
- package/dist/plugin/tools.js +45 -0
- package/dist/plugin.js +2 -0
- package/dist/procedural/store.js +1 -0
- package/dist/procedural/types.js +1 -0
- package/dist/seed-nodes.js +804 -0
- package/dist/storage/compress-ops.js +129 -0
- package/dist/storage/compression/formatters.js +243 -0
- package/dist/storage/compression/index.js +107 -0
- package/dist/storage/compression/patterns.js +138 -0
- package/dist/storage/expiration.js +66 -0
- package/dist/storage/index.js +1 -0
- package/dist/storage/injection-events.js +82 -0
- package/dist/storage/lifecycle.js +65 -0
- package/dist/storage/maintenance.js +60 -0
- package/dist/storage/migrations/definitions.js +374 -0
- package/dist/storage/migrations/index.js +21 -0
- package/dist/storage/navigation.js +98 -0
- package/dist/storage/queries/base.js +44 -0
- package/dist/storage/queries/links.js +32 -0
- package/dist/storage/queries/nodes.js +189 -0
- package/dist/storage/queries/search-helpers.js +239 -0
- package/dist/storage/scoring.js +36 -0
- package/dist/storage/search.js +233 -0
- package/dist/storage/session-tracking.js +180 -0
- package/dist/storage/sqlite.js +329 -0
- package/dist/storage/tool-usage.js +56 -0
- package/dist/storage/types.js +1 -0
- package/dist/storage/utils.js +94 -0
- package/dist/tools/auto-test.js +24 -0
- package/dist/tools/cache-status.js +36 -0
- package/dist/tools/compress.js +186 -0
- package/dist/tools/core.js +307 -0
- package/dist/tools/dashboard.js +97 -0
- package/dist/tools/help.js +59 -0
- package/dist/tools/index.js +12 -0
- package/dist/tools/inject.js +91 -0
- package/dist/tools/injection-debug.js +48 -0
- package/dist/tools/journal.js +105 -0
- package/dist/tools/llm-compress.js +41 -0
- package/dist/tools/middle-term.js +68 -0
- package/dist/tools/playbook.js +64 -0
- package/dist/tools/reflect.js +291 -0
- package/dist/tools/search.js +188 -0
- package/dist/tools/session.js +189 -0
- package/dist/tools/shared.js +74 -0
- package/dist/tools/skill.js +37 -0
- package/dist/tools/stats.js +256 -0
- package/dist/tools/version.js +13 -0
- package/dist/tools.js +18 -0
- package/dist/utils/hybridScore.js +67 -0
- package/management/public/app.js +1529 -0
- package/management/public/index.html +486 -0
- package/management/public/three.min.js +6 -0
- package/package.json +65 -0
- package/scripts/download-models.ts +16 -0
- package/scripts/postinstall.cjs +30 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { tool } from "@opencode-ai/plugin";
|
|
2
|
+
import { wrapWithTracking } from "./shared";
|
|
3
|
+
import { memLog } from "../logging";
|
|
4
|
+
export function MemoryReflect(store, client) {
|
|
5
|
+
const t = tool({
|
|
6
|
+
description: "Analyze a session to create lesson nodes from failures. Creates structured lessons with analysis, root cause, and actionable fix. Call this after session ends to learn from mistakes.",
|
|
7
|
+
args: {
|
|
8
|
+
session_id: tool.schema.string().optional().describe("Session ID to analyze. If not provided, prompts for it."),
|
|
9
|
+
dry_run: tool.schema.boolean().optional().default(false).describe("If true, outputs analysis without creating nodes for verification"),
|
|
10
|
+
use_llm: tool.schema.boolean().optional().default(false).describe("If true, uses LLM to generate more specific analysis and fixes"),
|
|
11
|
+
},
|
|
12
|
+
async execute(args, toolCtx) {
|
|
13
|
+
const sessionId = args.session_id ?? toolCtx.sessionID;
|
|
14
|
+
if (!sessionId) {
|
|
15
|
+
return "No session_id provided. Please provide a session ID to analyze.";
|
|
16
|
+
}
|
|
17
|
+
const stats = await store.getSessionStats(sessionId);
|
|
18
|
+
if (!stats) {
|
|
19
|
+
return `No data found for session: ${sessionId}`;
|
|
20
|
+
}
|
|
21
|
+
const failedCalls = stats.toolCalls.filter(tc => tc.success === false);
|
|
22
|
+
if (failedCalls.length === 0) {
|
|
23
|
+
return `## Session Reflection: ${sessionId}\n\nNo failed tools to analyze. Session completed successfully with ${stats.totalToolCalls} tool calls.`;
|
|
24
|
+
}
|
|
25
|
+
const dryRun = args.dry_run ?? false;
|
|
26
|
+
const analysisLines = [`## Session Reflection: ${sessionId}`, ""];
|
|
27
|
+
analysisLines.push(`**Status:** ${stats.status}`);
|
|
28
|
+
analysisLines.push(`**Total calls:** ${stats.totalToolCalls}`);
|
|
29
|
+
analysisLines.push(`**Failed:** ${failedCalls.length}`);
|
|
30
|
+
analysisLines.push("");
|
|
31
|
+
const toolFailureCounts = new Map();
|
|
32
|
+
const fileFailures = new Map();
|
|
33
|
+
for (const fc of failedCalls) {
|
|
34
|
+
const tool = fc.toolName;
|
|
35
|
+
toolFailureCounts.set(tool, (toolFailureCounts.get(tool) ?? 0) + 1);
|
|
36
|
+
if (fc.filePath) {
|
|
37
|
+
const existing = fileFailures.get(fc.filePath) ?? [];
|
|
38
|
+
existing.push(tool);
|
|
39
|
+
fileFailures.set(fc.filePath, existing);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
analysisLines.push("### Failed Tools");
|
|
43
|
+
for (const [tool, count] of toolFailureCounts) {
|
|
44
|
+
analysisLines.push(`- ${tool}: ${count} failure(s)`);
|
|
45
|
+
}
|
|
46
|
+
analysisLines.push("");
|
|
47
|
+
if (fileFailures.size > 0) {
|
|
48
|
+
analysisLines.push("### Files with Failures");
|
|
49
|
+
for (const [file, tools] of fileFailures) {
|
|
50
|
+
analysisLines.push(`- ${file}: ${tools.join(", ")}`);
|
|
51
|
+
}
|
|
52
|
+
analysisLines.push("");
|
|
53
|
+
}
|
|
54
|
+
analysisLines.push("### Analysis");
|
|
55
|
+
const lessons = [];
|
|
56
|
+
const patterns = {
|
|
57
|
+
memory_drilldown: (count) => `Avoid memory_drilldown with vague queries - use memory_search first (failed ${count}x)`,
|
|
58
|
+
memory_get: (count) => `Verify label exists before memory_get - search first if unsure (failed ${count}x)`,
|
|
59
|
+
memory_replace: (count) => `Re-read file before memory_replace to ensure content is current (failed ${count}x)`,
|
|
60
|
+
memory_delete: (count) => `Verify node exists before memory_delete (failed ${count}x)`,
|
|
61
|
+
edit: (count) => `Read file before edit, ensure oldText matches exactly (failed ${count}x)`,
|
|
62
|
+
write: (count) => `Check if file exists before write - prefer edit for existing files (failed ${count}x)`,
|
|
63
|
+
read: (count) => `Check file exists before read - use glob to find files (failed ${count}x)`,
|
|
64
|
+
glob: (count) => `Use more specific glob patterns (failed ${count}x)`,
|
|
65
|
+
grep: (count) => `Check file path exists before grep (failed ${count}x)`,
|
|
66
|
+
bash: (count) => `Verify command exists and paths are correct (failed ${count}x)`,
|
|
67
|
+
search: (count) => `Check search parameters (failed ${count}x)`,
|
|
68
|
+
};
|
|
69
|
+
const defaultPattern = (count, tool) => tool.startsWith("memory_")
|
|
70
|
+
? `Review ${tool} usage - verify inputs before calling (failed ${count}x)`
|
|
71
|
+
: `Check ${tool} arguments and prerequisites (failed ${count}x)`;
|
|
72
|
+
for (const [tool, count] of toolFailureCounts) {
|
|
73
|
+
const generator = patterns[tool] || defaultPattern;
|
|
74
|
+
lessons.push(`- ${generator(count, tool)}`);
|
|
75
|
+
}
|
|
76
|
+
if (lessons.length === 0) {
|
|
77
|
+
lessons.push("- Review tool arguments and ensure correct format");
|
|
78
|
+
}
|
|
79
|
+
for (const lesson of lessons) {
|
|
80
|
+
analysisLines.push(lesson);
|
|
81
|
+
}
|
|
82
|
+
const timestamp = Date.now();
|
|
83
|
+
const label = `lesson:${timestamp}`;
|
|
84
|
+
let content = analysisLines.join("\n");
|
|
85
|
+
if (args.use_llm && client && client?.session?.prompt) {
|
|
86
|
+
try {
|
|
87
|
+
const prompt = `Analyze these tool failures and provide specific, actionable fixes:
|
|
88
|
+
|
|
89
|
+
Session: ${sessionId}
|
|
90
|
+
Failed tools: ${[...toolFailureCounts].map(([t, c]) => `${t} (${c}x)`).join(", ")}
|
|
91
|
+
Files affected: ${[...fileFailures].map(([f, tools]) => `${f} (${tools.join(", ")})`).join(", ")}
|
|
92
|
+
|
|
93
|
+
For each failure type, provide a specific, concrete rule that would prevent the failure.
|
|
94
|
+
Format as a bullet list starting with "- ". Be specific about what to check/verify.
|
|
95
|
+
|
|
96
|
+
Example good rules:
|
|
97
|
+
- Before memory_drilldown, first run memory_search with keywords to verify the label exists
|
|
98
|
+
- Before edit, read the file and ensure oldText exactly matches what's in the file (no extra whitespace)
|
|
99
|
+
- Check file exists with glob before using read or edit on it
|
|
100
|
+
|
|
101
|
+
Your rules:`;
|
|
102
|
+
const result = await client
|
|
103
|
+
.session?.prompt({
|
|
104
|
+
path: { id: toolCtx.sessionID },
|
|
105
|
+
body: {
|
|
106
|
+
noReply: true,
|
|
107
|
+
parts: [{ type: 'text', text: prompt }]
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
if (result) {
|
|
111
|
+
const llmResponse = await (await result.text()).trim();
|
|
112
|
+
if (llmResponse) {
|
|
113
|
+
const llmRules = llmResponse.split("\n")
|
|
114
|
+
.filter((line) => line.trim().startsWith("-"))
|
|
115
|
+
.map((line) => line.trim());
|
|
116
|
+
if (llmRules.length > 0) {
|
|
117
|
+
lessons.push(...llmRules);
|
|
118
|
+
content += `\n\n### LLM-Generated Analysis\n${llmResponse}`;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
memLog("error", "reflect", "LLM analysis failed:", { error });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (dryRun) {
|
|
128
|
+
return `## Dry Run: Lesson Preview\n\n**Would create:** ${label}\n\n${content}\n\n---\n\n**Suggested fixes:**\n${lessons.join("\n")}\n\n---\n\nTo save these lessons, run without dry_run: true`;
|
|
129
|
+
}
|
|
130
|
+
await store.createNode({
|
|
131
|
+
scope: "global",
|
|
132
|
+
label,
|
|
133
|
+
content,
|
|
134
|
+
level: 0,
|
|
135
|
+
importance: 0.8,
|
|
136
|
+
type: "note",
|
|
137
|
+
parentIds: null,
|
|
138
|
+
});
|
|
139
|
+
const analysisNodeLabel = `${label}:analysis`;
|
|
140
|
+
await store.createNode({
|
|
141
|
+
scope: "global",
|
|
142
|
+
label: analysisNodeLabel,
|
|
143
|
+
content: analysisLines.slice(0, 15).join("\n"),
|
|
144
|
+
level: 0,
|
|
145
|
+
importance: 0.6,
|
|
146
|
+
type: "note",
|
|
147
|
+
parentIds: [label],
|
|
148
|
+
});
|
|
149
|
+
const fixNodeLabel = `${label}:fix`;
|
|
150
|
+
const fixContent = lessons.map(l => l.replace(/^- /, "- ")).join("\n");
|
|
151
|
+
await store.createNode({
|
|
152
|
+
scope: "global",
|
|
153
|
+
label: fixNodeLabel,
|
|
154
|
+
content: fixContent,
|
|
155
|
+
level: 0,
|
|
156
|
+
importance: 0.7,
|
|
157
|
+
type: "note",
|
|
158
|
+
parentIds: [label],
|
|
159
|
+
});
|
|
160
|
+
return `## Reflection Complete\n\nCreated lesson node: ${label}\n\n**Summary:** ${failedCalls.length} failures from ${stats.totalToolCalls} calls\n\n**Key lessons:**\n${lessons.join("\n")}\n\nReview and verify these lessons. Then use memory_distill to extract rules.`;
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
return wrapWithTracking(t, store, "memory_reflect");
|
|
164
|
+
}
|
|
165
|
+
export function MemoryDistill(store, client) {
|
|
166
|
+
const t = tool({
|
|
167
|
+
description: "Extract actionable rules from recent lesson nodes and update rule:mandatory nodes. Call this after reviewing lessons from memory_reflect.",
|
|
168
|
+
args: {
|
|
169
|
+
dry_run: tool.schema.boolean().optional().describe("Show what would change without applying (default: false)").default(false),
|
|
170
|
+
use_llm: tool.schema.boolean().optional().describe("Use LLM to generate more specific rules from lessons").default(false),
|
|
171
|
+
},
|
|
172
|
+
async execute(args, toolCtx) {
|
|
173
|
+
const lessons = await store.listNodes("global");
|
|
174
|
+
const lessonNodes = lessons.filter(n => n.label?.startsWith("lesson:") && !n.label.includes(":", 7));
|
|
175
|
+
if (lessonNodes.length === 0) {
|
|
176
|
+
return "No lesson nodes found. Run memory_reflect first to create lessons.";
|
|
177
|
+
}
|
|
178
|
+
const recentLessons = lessonNodes
|
|
179
|
+
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())
|
|
180
|
+
.slice(0, 10);
|
|
181
|
+
const fixes = [];
|
|
182
|
+
const allLessons = lessons.filter(n => n.label?.startsWith("lesson:"));
|
|
183
|
+
for (const lesson of recentLessons) {
|
|
184
|
+
const fixNode = allLessons.find(n => n.label === `${lesson.label}:fix`);
|
|
185
|
+
if (fixNode?.content) {
|
|
186
|
+
fixes.push(...fixNode.content.split("\n").filter(l => l.trim()));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
const uniqueFixes = [...new Set(fixes)];
|
|
190
|
+
const distilledRules = [];
|
|
191
|
+
for (const fix of uniqueFixes) {
|
|
192
|
+
const match = fix.match(/^- (.+)$/);
|
|
193
|
+
const action = match?.[1];
|
|
194
|
+
if (action) {
|
|
195
|
+
if (action.includes("memory_drilldown")) {
|
|
196
|
+
distilledRules.push("- Avoid memory_drilldown with vague queries - use memory_search first");
|
|
197
|
+
}
|
|
198
|
+
else if (action.includes("memory_get")) {
|
|
199
|
+
distilledRules.push("- Always verify label exists before memory_get");
|
|
200
|
+
}
|
|
201
|
+
else if (action.includes("memory_replace")) {
|
|
202
|
+
distilledRules.push("- Re-read file before replace to ensure content is current");
|
|
203
|
+
}
|
|
204
|
+
else if (action.includes("read") || action.includes("glob")) {
|
|
205
|
+
distilledRules.push("- Check if file exists before read/glob");
|
|
206
|
+
}
|
|
207
|
+
else if (action.includes("search before get")) {
|
|
208
|
+
// Already in rules, skip
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
distilledRules.push(`- ${action}`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (args.use_llm && client && client?.session?.prompt) {
|
|
216
|
+
try {
|
|
217
|
+
const lessonContent = recentLessons.map(l => l.content).join("\n---\n");
|
|
218
|
+
const prompt = `Based on these lesson summaries, generate specific, actionable rules for the agent.
|
|
219
|
+
|
|
220
|
+
Lessons:
|
|
221
|
+
${lessonContent}
|
|
222
|
+
|
|
223
|
+
Current rules:
|
|
224
|
+
${distilledRules.join("\n")}
|
|
225
|
+
|
|
226
|
+
Generate more specific rules based on the lessons. Focus on:
|
|
227
|
+
1. What specific checks to do before calling tools
|
|
228
|
+
2. What common mistakes to avoid
|
|
229
|
+
3. What order of operations works best
|
|
230
|
+
|
|
231
|
+
Format as bullet points starting with "- ".
|
|
232
|
+
|
|
233
|
+
Your rules:`;
|
|
234
|
+
const result = await client
|
|
235
|
+
.session?.prompt({
|
|
236
|
+
path: { id: toolCtx.sessionID },
|
|
237
|
+
body: {
|
|
238
|
+
noReply: true,
|
|
239
|
+
parts: [{ type: 'text', text: prompt }]
|
|
240
|
+
},
|
|
241
|
+
});
|
|
242
|
+
if (result) {
|
|
243
|
+
const llmResponse = await (await result.text()).trim();
|
|
244
|
+
if (llmResponse) {
|
|
245
|
+
const llmRules = llmResponse.split("\n")
|
|
246
|
+
.filter((line) => line.trim().startsWith("-"))
|
|
247
|
+
.map((line) => line.trim());
|
|
248
|
+
if (llmRules.length > 0) {
|
|
249
|
+
distilledRules.push(...llmRules);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
catch (error) {
|
|
255
|
+
memLog("error", "reflect", "LLM distillation failed:", { error });
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
const output = ["## Rule Distillation Results", ""];
|
|
259
|
+
output.push(`Analyzed ${recentLessons.length} recent lessons`);
|
|
260
|
+
output.push(`Extracted ${distilledRules.length} potential rules`);
|
|
261
|
+
output.push("");
|
|
262
|
+
if (args.dry_run) {
|
|
263
|
+
output.push("### Proposed Changes (Dry Run)");
|
|
264
|
+
output.push("");
|
|
265
|
+
output.push("To apply these rules, run: memory_distill (without dry_run)");
|
|
266
|
+
output.push("To discard, just ignore this output.");
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
output.push("### Updated Rules");
|
|
270
|
+
}
|
|
271
|
+
const mandatoryNode = await store.getNodeByLabel("global", "rule:mandatory:memory").catch(() => null);
|
|
272
|
+
const existingRules = mandatoryNode?.content ?? "";
|
|
273
|
+
for (const rule of distilledRules) {
|
|
274
|
+
const ruleText = rule.replace(/^- /, "");
|
|
275
|
+
if (!existingRules.toLowerCase().includes(ruleText.toLowerCase().slice(0, 30))) {
|
|
276
|
+
output.push(rule);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
if (!args.dry_run && output.length > 4) {
|
|
280
|
+
const currentContent = mandatoryNode?.content ?? "## Memory Tool Mandatory Rules\ntag: rule:mandatory\n\n";
|
|
281
|
+
const newContent = currentContent + "\n" + distilledRules.map(r => `### Auto-Learned\n${r}`).join("\n");
|
|
282
|
+
if (mandatoryNode) {
|
|
283
|
+
await store.updateNode(mandatoryNode.id, { content: newContent });
|
|
284
|
+
output.push("\n**Updated rule:mandatory:memory**");
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return output.join("\n");
|
|
288
|
+
},
|
|
289
|
+
});
|
|
290
|
+
return wrapWithTracking(t, store, "memory_distill");
|
|
291
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { tool } from "@opencode-ai/plugin";
|
|
2
|
+
import { estimateTokens, generateEmbedding } from "../embeddings";
|
|
3
|
+
import { resolveNode, wrapWithContextWarning, wrapWithTracking, lastSearchResults } from "./shared";
|
|
4
|
+
export function MemoryDrilldown(store) {
|
|
5
|
+
const t = tool({
|
|
6
|
+
description: "Retrieve a memory node with path to source nodes (fractal retrieval).",
|
|
7
|
+
args: {
|
|
8
|
+
id: tool.schema.string().optional(),
|
|
9
|
+
label: tool.schema.string().optional(),
|
|
10
|
+
scope: tool.schema.enum(["global", "project"]).optional(),
|
|
11
|
+
max_depth: tool.schema.number().int().nonnegative().optional(),
|
|
12
|
+
},
|
|
13
|
+
async execute(args) {
|
|
14
|
+
const node = await resolveNode(store, args);
|
|
15
|
+
const nodeId = node.id;
|
|
16
|
+
const result = await store.retrieveFractal(nodeId, args.max_depth ?? 10);
|
|
17
|
+
const lines = [
|
|
18
|
+
`## Fractal Retrieval: ${result.node.label ?? result.node.id.slice(0, 8)}`,
|
|
19
|
+
`Level: ${result.node.level} | Depth: ${result.depth} | Relevance: ${result.relevanceScore.toFixed(2)}`,
|
|
20
|
+
"",
|
|
21
|
+
"### Path (current → sources)",
|
|
22
|
+
...result.path.map((n, i) => {
|
|
23
|
+
const indent = " ".repeat(i);
|
|
24
|
+
const marker = i === 0 ? "→" : "↳";
|
|
25
|
+
return `${indent}${marker} [L${n.level}] ${n.label ?? n.id.slice(0, 8)}${n.content.length > 50 ? "..." : ""}`;
|
|
26
|
+
}),
|
|
27
|
+
"",
|
|
28
|
+
"### Full Content",
|
|
29
|
+
result.node.content,
|
|
30
|
+
];
|
|
31
|
+
const scope = args.scope ?? result.node.scope;
|
|
32
|
+
const linkedNodes = await store.getLinkedNodes(scope, nodeId);
|
|
33
|
+
if (linkedNodes.length > 0) {
|
|
34
|
+
lines.push("", "### Linked Nodes");
|
|
35
|
+
for (const linked of linkedNodes) {
|
|
36
|
+
lines.push(`- [[${linked.label ?? linked.id.slice(0, 8)}]]: ${linked.content.slice(0, 100)}${linked.content.length > 100 ? "..." : ""}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const resultStr = lines.join("\n");
|
|
40
|
+
const pathTokens = result.path.reduce((sum, n) => sum + estimateTokens(n.content), 0);
|
|
41
|
+
return wrapWithContextWarning(resultStr, pathTokens);
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
return wrapWithTracking(t, store, "memory_drilldown");
|
|
45
|
+
}
|
|
46
|
+
export function MemorySearch(store) {
|
|
47
|
+
const t = tool({
|
|
48
|
+
description: "Search memory for relevant context.",
|
|
49
|
+
args: {
|
|
50
|
+
query: tool.schema.string(),
|
|
51
|
+
limit: tool.schema.number().int().positive().optional(),
|
|
52
|
+
min_level: tool.schema.number().int().nonnegative().optional(),
|
|
53
|
+
max_level: tool.schema.number().int().nonnegative().optional(),
|
|
54
|
+
min_usefulness: tool.schema.number().min(0).max(5).optional(),
|
|
55
|
+
bm25_weight: tool.schema.number().min(0).max(1).optional(),
|
|
56
|
+
rerank: tool.schema.boolean().optional(),
|
|
57
|
+
expand_links: tool.schema.boolean().optional(),
|
|
58
|
+
},
|
|
59
|
+
async execute(args) {
|
|
60
|
+
const queryEmbedding = await generateEmbedding(args.query);
|
|
61
|
+
const options = {
|
|
62
|
+
bm25Weight: args.bm25_weight ?? 0.4,
|
|
63
|
+
queryText: args.query,
|
|
64
|
+
rerank: args.rerank ?? true,
|
|
65
|
+
};
|
|
66
|
+
if (args.min_level !== undefined)
|
|
67
|
+
options.minLevel = args.min_level;
|
|
68
|
+
if (args.max_level !== undefined)
|
|
69
|
+
options.maxLevel = args.max_level;
|
|
70
|
+
if (args.min_usefulness !== undefined)
|
|
71
|
+
options.minUsefulness = args.min_usefulness;
|
|
72
|
+
let nodes = await store.searchByEmbedding(queryEmbedding, args.limit ?? 10, options);
|
|
73
|
+
lastSearchResults.length = 0;
|
|
74
|
+
lastSearchResults.push(...nodes.map(n => ({ id: n.id, label: n.label, scope: n.scope })));
|
|
75
|
+
if (args.expand_links !== false && nodes.length > 0) {
|
|
76
|
+
const expandLimit = (args.limit ?? 10) + 5;
|
|
77
|
+
const seenIds = new Set(nodes.map(n => n.id));
|
|
78
|
+
const linkedNodes = [];
|
|
79
|
+
for (const node of nodes) {
|
|
80
|
+
const links = await store.getLinkedNodes(node.scope, node.id);
|
|
81
|
+
for (const linked of links) {
|
|
82
|
+
if (!seenIds.has(linked.id)) {
|
|
83
|
+
seenIds.add(linked.id);
|
|
84
|
+
linkedNodes.push({ node: linked, linkedFrom: node.label ?? node.id.slice(0, 8) });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
for (const { node, linkedFrom } of linkedNodes) {
|
|
89
|
+
const boostedNode = {
|
|
90
|
+
...node,
|
|
91
|
+
importance: (node.importance ?? 0.5) * 0.8,
|
|
92
|
+
linkedFrom,
|
|
93
|
+
};
|
|
94
|
+
nodes.push(boostedNode);
|
|
95
|
+
}
|
|
96
|
+
nodes.sort((a, b) => (b.importance ?? 0) - (a.importance ?? 0));
|
|
97
|
+
nodes = nodes.slice(0, expandLimit);
|
|
98
|
+
}
|
|
99
|
+
if (nodes.length === 0) {
|
|
100
|
+
return "No matching memory found. Try different keywords.";
|
|
101
|
+
}
|
|
102
|
+
const lines = [
|
|
103
|
+
`## Memory Search Results (${nodes.length} matches)`,
|
|
104
|
+
"",
|
|
105
|
+
...nodes.map(n => {
|
|
106
|
+
const parentInfo = n.parentIds && n.parentIds.length > 0
|
|
107
|
+
? ` (← ${n.parentIds.length} sources)`
|
|
108
|
+
: "";
|
|
109
|
+
const linkInfo = n.linkedFrom
|
|
110
|
+
? ` [linked from ${n.linkedFrom}]`
|
|
111
|
+
: "";
|
|
112
|
+
const matchPct = (n.importance * 100).toFixed(0);
|
|
113
|
+
const label = n.label ?? n.id.slice(0, 8);
|
|
114
|
+
const content = n.summary || n.content.slice(0, 300);
|
|
115
|
+
return `### [L${n.level}] ${label} - ${matchPct}% match${parentInfo}${linkInfo}\n${content}${content.length >= 300 ? "..." : ""}`;
|
|
116
|
+
}),
|
|
117
|
+
"",
|
|
118
|
+
"Use memory_drilldown(label) to see full content of any node.",
|
|
119
|
+
"",
|
|
120
|
+
"**Self-Reflection**: After using these memories, rate their usefulness (0-5):",
|
|
121
|
+
" `memory_rate { label: \"<node-label>\", helpful: true, usefulness_score: <rating> }`",
|
|
122
|
+
];
|
|
123
|
+
const result = lines.join("\n");
|
|
124
|
+
const contentTokens = nodes.reduce((sum, n) => sum + estimateTokens(n.content), 0);
|
|
125
|
+
return wrapWithContextWarning(result, contentTokens);
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
return wrapWithTracking(t, store, "memory_search");
|
|
129
|
+
}
|
|
130
|
+
export function MemoryDrilldownQuery(store) {
|
|
131
|
+
const t = tool({
|
|
132
|
+
description: "Top-down drilldown query.",
|
|
133
|
+
args: {
|
|
134
|
+
query: tool.schema.string(),
|
|
135
|
+
max_results: tool.schema.number().int().positive().optional(),
|
|
136
|
+
},
|
|
137
|
+
async execute(args) {
|
|
138
|
+
const results = await store.drilldownQuery(args.query, args.max_results ?? 20);
|
|
139
|
+
if (results.length === 0) {
|
|
140
|
+
return `No memory found matching your query "${args.query}".
|
|
141
|
+
|
|
142
|
+
**Tip**: Use \`memory_search\` first with broader keywords.`;
|
|
143
|
+
}
|
|
144
|
+
const summaryResults = results.filter(r => r.level === "summary");
|
|
145
|
+
const intermediateResults = results.filter(r => r.level === "intermediate");
|
|
146
|
+
const detailResults = results.filter(r => r.level === "detail");
|
|
147
|
+
const lines = [
|
|
148
|
+
`## Top-Down Drilldown: "${args.query}"`,
|
|
149
|
+
"",
|
|
150
|
+
`Found ${results.length} relevant memory nodes (${summaryResults.length} summaries, ${intermediateResults.length} intermediate, ${detailResults.length} details).`,
|
|
151
|
+
"",
|
|
152
|
+
];
|
|
153
|
+
if (summaryResults.length > 0) {
|
|
154
|
+
lines.push("### High-Level Summaries");
|
|
155
|
+
for (const result of summaryResults) {
|
|
156
|
+
lines.push(`**[L${result.node.level}] ${result.node.label ?? result.node.id.slice(0, 8)}** (${(result.relevance * 100).toFixed(0)}% relevant)`);
|
|
157
|
+
lines.push(`> ${result.node.content.slice(0, 200)}${result.node.content.length > 200 ? "..." : ""}`);
|
|
158
|
+
if (result.path.length > 1) {
|
|
159
|
+
lines.push(`_Source path: ${result.path.map(n => n.label ?? n.id.slice(0, 8)).join(" → ")}_`);
|
|
160
|
+
}
|
|
161
|
+
lines.push("");
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (intermediateResults.length > 0) {
|
|
165
|
+
lines.push("### Weekly Summaries");
|
|
166
|
+
for (const result of intermediateResults) {
|
|
167
|
+
lines.push(`**[L${result.node.level}] ${result.node.label ?? result.node.id.slice(0, 8)}** (${(result.relevance * 100).toFixed(0)}% relevant)`);
|
|
168
|
+
lines.push(`> ${result.node.content.slice(0, 150)}${result.node.content.length > 150 ? "..." : ""}`);
|
|
169
|
+
lines.push("");
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (detailResults.length > 0) {
|
|
173
|
+
lines.push("### Specific Details");
|
|
174
|
+
for (const result of detailResults) {
|
|
175
|
+
lines.push(`**[L${result.node.level}] ${result.node.label ?? result.node.id.slice(0, 8)}** (${(result.relevance * 100).toFixed(0)}% relevant)`);
|
|
176
|
+
lines.push(`> ${result.node.content.slice(0, 150)}${result.node.content.length > 150 ? "..." : ""}`);
|
|
177
|
+
lines.push("");
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
lines.push("---");
|
|
181
|
+
lines.push("_Use `memory_drilldown(id=\"...\")` for full path to any specific node._");
|
|
182
|
+
const result = lines.join("\n");
|
|
183
|
+
const contentTokens = results.reduce((sum, r) => sum + estimateTokens(r.node.content), 0);
|
|
184
|
+
return wrapWithContextWarning(result, contentTokens);
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
return wrapWithTracking(t, store, "memory_drilldown_query");
|
|
188
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { tool } from "@opencode-ai/plugin";
|
|
2
|
+
import { estimateTokens } from "../embeddings";
|
|
3
|
+
import { CONTEXT_LIMIT, wrapWithTracking } from "./shared";
|
|
4
|
+
export function MemorySessionStats(store) {
|
|
5
|
+
const t = tool({
|
|
6
|
+
description: "Get statistics about the current session's tool calls. Use session_id to query a specific session.",
|
|
7
|
+
args: {
|
|
8
|
+
session_id: tool.schema.string().optional().describe("Session ID to query. If not provided, attempts to get current session."),
|
|
9
|
+
},
|
|
10
|
+
async execute(args) {
|
|
11
|
+
const sessionId = args.session_id;
|
|
12
|
+
if (!sessionId) {
|
|
13
|
+
return "No session_id provided. Pass a session ID to query specific session stats.";
|
|
14
|
+
}
|
|
15
|
+
const stats = await store.getSessionStats(sessionId);
|
|
16
|
+
if (!stats) {
|
|
17
|
+
return `No data found for session: ${sessionId}`;
|
|
18
|
+
}
|
|
19
|
+
const lines = [];
|
|
20
|
+
lines.push(`## Session: ${stats.sessionId}`);
|
|
21
|
+
lines.push(`**Status:** ${stats.status}`);
|
|
22
|
+
lines.push(`**Started:** ${new Date(stats.startedAt).toLocaleString()}`);
|
|
23
|
+
if (stats.endedAt) {
|
|
24
|
+
const duration = Math.round((stats.endedAt - stats.startedAt) / 1000 / 60);
|
|
25
|
+
lines.push(`**Duration:** ${duration} minutes`);
|
|
26
|
+
}
|
|
27
|
+
lines.push("");
|
|
28
|
+
lines.push("## Tool Usage Summary");
|
|
29
|
+
lines.push(`- Total calls: ${stats.totalToolCalls}`);
|
|
30
|
+
lines.push(`- File reads: ${stats.fileReads}`);
|
|
31
|
+
lines.push(`- File edits: ${stats.fileEdits}`);
|
|
32
|
+
lines.push(`- Bash commands: ${stats.bashCommands}`);
|
|
33
|
+
lines.push(`- Memory tools: ${stats.memoryTools}`);
|
|
34
|
+
lines.push(`- Failed tools: ${stats.failedTools}`);
|
|
35
|
+
lines.push("");
|
|
36
|
+
if (stats.uniqueFilesTouched.length > 0) {
|
|
37
|
+
lines.push("## Files Touched");
|
|
38
|
+
lines.push(stats.uniqueFilesTouched.slice(0, 20).map(f => `- ${f}`).join("\n"));
|
|
39
|
+
if (stats.uniqueFilesTouched.length > 20) {
|
|
40
|
+
lines.push(`_...and ${stats.uniqueFilesTouched.length - 20} more_`);
|
|
41
|
+
}
|
|
42
|
+
lines.push("");
|
|
43
|
+
}
|
|
44
|
+
if (stats.injectionCount > 0) {
|
|
45
|
+
lines.push("## Memory Correlation");
|
|
46
|
+
lines.push(`- Injections: ${stats.injectionCount}`);
|
|
47
|
+
lines.push(`- Tokens injected: ${stats.injectedTokens}`);
|
|
48
|
+
lines.push("");
|
|
49
|
+
}
|
|
50
|
+
if (stats.toolCalls.length > 0) {
|
|
51
|
+
lines.push("## Recent Tool Calls");
|
|
52
|
+
const recentCalls = stats.toolCalls.slice(-20);
|
|
53
|
+
for (const tc of recentCalls) {
|
|
54
|
+
const time = new Date(tc.timestamp).toLocaleTimeString();
|
|
55
|
+
const success = tc.success === false ? "❌" : tc.success === true ? "✓" : "?";
|
|
56
|
+
let entry = `${time} ${success} ${tc.toolName}`;
|
|
57
|
+
if (tc.filePath)
|
|
58
|
+
entry += ` (${tc.filePath})`;
|
|
59
|
+
if (tc.command)
|
|
60
|
+
entry += ` - ${tc.command.substring(0, 50)}`;
|
|
61
|
+
lines.push(entry);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return lines.join("\n");
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
return wrapWithTracking(t, store, "memory_session_stats");
|
|
68
|
+
}
|
|
69
|
+
export function MemoryTotalTokens(store, client) {
|
|
70
|
+
const t = tool({
|
|
71
|
+
description: "Get complete token analysis including both memory nodes and conversation history. Shows memory tokens, conversation tokens, and total usage against context limit.",
|
|
72
|
+
args: {
|
|
73
|
+
session_id: tool.schema.string().optional().describe("Session ID to analyze. If not provided, uses current session."),
|
|
74
|
+
include_messages: tool.schema.boolean().optional().describe("Include per-message token breakdown (default: false)").default(false),
|
|
75
|
+
},
|
|
76
|
+
async execute(args, toolCtx) {
|
|
77
|
+
const sessionId = args.session_id ?? toolCtx.sessionID;
|
|
78
|
+
if (!sessionId) {
|
|
79
|
+
return "No session ID provided. Please provide a session_id or run within a session context.";
|
|
80
|
+
}
|
|
81
|
+
const memoryNodes = await store.listNodes("all");
|
|
82
|
+
const memoryTokensByLevel = {};
|
|
83
|
+
let totalMemoryTokens = 0;
|
|
84
|
+
for (const node of memoryNodes) {
|
|
85
|
+
const tokens = estimateTokens(node.content);
|
|
86
|
+
memoryTokensByLevel[node.level] = (memoryTokensByLevel[node.level] || 0) + tokens;
|
|
87
|
+
totalMemoryTokens += tokens;
|
|
88
|
+
}
|
|
89
|
+
let conversationTokens = {
|
|
90
|
+
input: 0,
|
|
91
|
+
output: 0,
|
|
92
|
+
reasoning: 0,
|
|
93
|
+
cacheRead: 0,
|
|
94
|
+
cacheWrite: 0,
|
|
95
|
+
total: 0,
|
|
96
|
+
};
|
|
97
|
+
let messageCount = 0;
|
|
98
|
+
const messageBreakdown = [];
|
|
99
|
+
try {
|
|
100
|
+
const messagesResponse = await client?.session?.messages({
|
|
101
|
+
path: { id: sessionId },
|
|
102
|
+
});
|
|
103
|
+
if (messagesResponse && Array.isArray(messagesResponse)) {
|
|
104
|
+
for (const { info: message } of messagesResponse) {
|
|
105
|
+
if (message.role === "assistant") {
|
|
106
|
+
messageCount++;
|
|
107
|
+
const tokens = message.tokens || { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } };
|
|
108
|
+
conversationTokens.input += tokens.input || 0;
|
|
109
|
+
conversationTokens.output += tokens.output || 0;
|
|
110
|
+
conversationTokens.reasoning += tokens.reasoning || 0;
|
|
111
|
+
conversationTokens.cacheRead += tokens.cache?.read || 0;
|
|
112
|
+
conversationTokens.cacheWrite += tokens.cache?.write || 0;
|
|
113
|
+
if (args.include_messages) {
|
|
114
|
+
messageBreakdown.push({
|
|
115
|
+
id: message.id.slice(0, 8),
|
|
116
|
+
role: message.role,
|
|
117
|
+
tokens: {
|
|
118
|
+
input: tokens.input || 0,
|
|
119
|
+
output: tokens.output || 0,
|
|
120
|
+
reasoning: tokens.reasoning || 0,
|
|
121
|
+
},
|
|
122
|
+
cost: message.cost || 0,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
conversationTokens.total = conversationTokens.input + conversationTokens.output +
|
|
128
|
+
conversationTokens.reasoning + conversationTokens.cacheRead + conversationTokens.cacheWrite;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
// If we can't fetch messages, just show memory tokens
|
|
133
|
+
}
|
|
134
|
+
const totalTokens = totalMemoryTokens + conversationTokens.total;
|
|
135
|
+
const percentage = (totalTokens / CONTEXT_LIMIT * 100).toFixed(1);
|
|
136
|
+
const lines = [];
|
|
137
|
+
lines.push("## Complete Token Analysis");
|
|
138
|
+
lines.push(`**Session:** ${sessionId.slice(0, 8)}`);
|
|
139
|
+
lines.push("");
|
|
140
|
+
lines.push("### Memory System Tokens");
|
|
141
|
+
lines.push(`**Total Memory:** ${totalMemoryTokens.toLocaleString()} tokens`);
|
|
142
|
+
const levelNames = { 0: "L0 (raw)", 1: "L1", 2: "L2", 3: "L3", 4: "L4", 5: "L5" };
|
|
143
|
+
for (const [level, tokens] of Object.entries(memoryTokensByLevel)) {
|
|
144
|
+
const levelNum = parseInt(level);
|
|
145
|
+
const name = levelNames[levelNum] || `L${level}`;
|
|
146
|
+
const pct = ((tokens / totalMemoryTokens) * 100).toFixed(1);
|
|
147
|
+
lines.push(`- ${name}: ${tokens.toLocaleString()} tokens (${pct}%)`);
|
|
148
|
+
}
|
|
149
|
+
lines.push("");
|
|
150
|
+
lines.push("### Conversation Tokens");
|
|
151
|
+
lines.push(`**Total Conversation:** ${conversationTokens.total.toLocaleString()} tokens (${messageCount} messages)`);
|
|
152
|
+
lines.push(`- Input tokens: ${conversationTokens.input.toLocaleString()}`);
|
|
153
|
+
lines.push(`- Output tokens: ${conversationTokens.output.toLocaleString()}`);
|
|
154
|
+
lines.push(`- Reasoning tokens: ${conversationTokens.reasoning.toLocaleString()}`);
|
|
155
|
+
lines.push(`- Cache read tokens: ${conversationTokens.cacheRead.toLocaleString()}`);
|
|
156
|
+
lines.push(`- Cache write tokens: ${conversationTokens.cacheWrite.toLocaleString()}`);
|
|
157
|
+
lines.push("");
|
|
158
|
+
lines.push("### Summary");
|
|
159
|
+
lines.push(`**Total Tokens:** ${totalTokens.toLocaleString()} / ${CONTEXT_LIMIT.toLocaleString()} (${percentage}%)`);
|
|
160
|
+
lines.push(`- Memory: ${((totalMemoryTokens / totalTokens) * 100).toFixed(1)}%`);
|
|
161
|
+
lines.push(`- Conversation: ${((conversationTokens.total / totalTokens) * 100).toFixed(1)}%`);
|
|
162
|
+
lines.push("");
|
|
163
|
+
const contextRatio = totalTokens / CONTEXT_LIMIT;
|
|
164
|
+
if (contextRatio >= 0.8) {
|
|
165
|
+
lines.push("**⚠️ CRITICAL:** Context at " + (contextRatio * 100).toFixed(0) + "%. Run `memory_compress` immediately!");
|
|
166
|
+
}
|
|
167
|
+
else if (contextRatio >= 0.6) {
|
|
168
|
+
lines.push("**⚠️ WARNING:** Context at " + (contextRatio * 100).toFixed(0) + "%. Consider running `memory_compress`.");
|
|
169
|
+
}
|
|
170
|
+
else if (contextRatio >= 0.4) {
|
|
171
|
+
lines.push("**ℹ️ INFO:** Context at " + (contextRatio * 100).toFixed(0) + "%. Monitor and compress if needed.");
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
lines.push("**✅ OK:** Context at " + (contextRatio * 100).toFixed(0) + "%. Well within limits.");
|
|
175
|
+
}
|
|
176
|
+
if (args.include_messages && messageBreakdown.length > 0) {
|
|
177
|
+
lines.push("");
|
|
178
|
+
lines.push("### Message Breakdown (Top 10)");
|
|
179
|
+
const topMessages = messageBreakdown.slice(-10);
|
|
180
|
+
for (const msg of topMessages) {
|
|
181
|
+
const msgTotal = msg.tokens.input + msg.tokens.output + msg.tokens.reasoning;
|
|
182
|
+
lines.push(`- ${msg.id}: ${msgTotal.toLocaleString()} tokens (in:${msg.tokens.input.toLocaleString()} out:${msg.tokens.output.toLocaleString()} reason:${msg.tokens.reasoning.toLocaleString()})`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return lines.join("\n");
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
return wrapWithTracking(t, store, "memory_total_tokens");
|
|
189
|
+
}
|