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,91 @@
|
|
|
1
|
+
import { tool } from "@opencode-ai/plugin";
|
|
2
|
+
import { generateEmbedding, estimateTokens } from "../embeddings";
|
|
3
|
+
import { computeHybridScore, tokenize } from "../utils/hybridScore";
|
|
4
|
+
export function MemoryInject(store) {
|
|
5
|
+
const t = tool({
|
|
6
|
+
description: "Inject relevant memories into the prompt using a token budget and cascading detail levels.",
|
|
7
|
+
args: {
|
|
8
|
+
query: tool.schema.string(),
|
|
9
|
+
maxTokens: tool.schema.number().int().positive().optional(),
|
|
10
|
+
includeConfidential: tool.schema.boolean().optional(),
|
|
11
|
+
costWeight: tool.schema.number().positive().optional(),
|
|
12
|
+
debug: tool.schema.boolean().optional(),
|
|
13
|
+
fallbackMessage: tool.schema.string().optional(),
|
|
14
|
+
maxNodes: tool.schema.number().int().positive().optional(),
|
|
15
|
+
maxLevel: tool.schema.number().int().optional(),
|
|
16
|
+
minConfidence: tool.schema.number().min(0).max(1).optional(),
|
|
17
|
+
budgetMode: tool.schema.enum(["dynamic", "strict"]).optional(),
|
|
18
|
+
},
|
|
19
|
+
async execute(args) {
|
|
20
|
+
const { query, maxTokens, includeConfidential, costWeight, debug, fallbackMessage, maxNodes, maxLevel, minConfidence, budgetMode } = args;
|
|
21
|
+
const embedding = await generateEmbedding(query);
|
|
22
|
+
const candidates = await store.searchByEmbedding(embedding, 100, {
|
|
23
|
+
minLevel: 0,
|
|
24
|
+
maxLevel: 4,
|
|
25
|
+
});
|
|
26
|
+
const filtered = candidates.filter((n) => {
|
|
27
|
+
const confidential = n.confidential;
|
|
28
|
+
if (!includeConfidential && confidential)
|
|
29
|
+
return false;
|
|
30
|
+
if (maxLevel !== undefined && (n.level ?? 0) > maxLevel)
|
|
31
|
+
return false;
|
|
32
|
+
if (minConfidence !== undefined && (n.confidence ?? 0) < minConfidence)
|
|
33
|
+
return false;
|
|
34
|
+
return true;
|
|
35
|
+
});
|
|
36
|
+
const nodeTokenCounts = new Map();
|
|
37
|
+
for (const n of filtered) {
|
|
38
|
+
const metaTokens = n.metadata?.tokenCount;
|
|
39
|
+
const count = metaTokens ?? estimateTokens(n.content);
|
|
40
|
+
nodeTokenCounts.set(n.id, count);
|
|
41
|
+
}
|
|
42
|
+
const queryTerms = query ? tokenize(query) : [];
|
|
43
|
+
const relevanceScores = filtered.map((node) => computeHybridScore(node, embedding, queryTerms));
|
|
44
|
+
const candidatesWithScore = filtered.map((node, i) => ({
|
|
45
|
+
node,
|
|
46
|
+
relevance: relevanceScores[i],
|
|
47
|
+
tokens: nodeTokenCounts.get(node.id) ?? 0,
|
|
48
|
+
score: (relevanceScores[i] ?? 0) - ((costWeight ?? 0) * (nodeTokenCounts.get(node.id) ?? 0)),
|
|
49
|
+
}));
|
|
50
|
+
candidatesWithScore.sort((a, b) => b.score - a.score);
|
|
51
|
+
let selectedNodes = [];
|
|
52
|
+
let usedTokens = 0;
|
|
53
|
+
const budget = maxTokens ?? 8000;
|
|
54
|
+
for (const cand of candidatesWithScore) {
|
|
55
|
+
if (usedTokens + cand.tokens > budget)
|
|
56
|
+
continue;
|
|
57
|
+
selectedNodes.push(cand.node);
|
|
58
|
+
usedTokens += cand.tokens;
|
|
59
|
+
}
|
|
60
|
+
if (maxNodes !== undefined && selectedNodes.length > maxNodes) {
|
|
61
|
+
selectedNodes = selectedNodes.slice(0, maxNodes);
|
|
62
|
+
usedTokens = selectedNodes.reduce((sum, n) => sum + (nodeTokenCounts.get(n.id) ?? 0), 0);
|
|
63
|
+
}
|
|
64
|
+
// Render selected nodes as XML-like blocks
|
|
65
|
+
const renderedNodes = selectedNodes.map(node => {
|
|
66
|
+
const content = node.content
|
|
67
|
+
.replaceAll("&", "&")
|
|
68
|
+
.replaceAll("<", "<")
|
|
69
|
+
.replaceAll(">", ">");
|
|
70
|
+
return `<node label="${node.label ?? node.id}">\n${content}\n</node>`;
|
|
71
|
+
}).join("\n\n");
|
|
72
|
+
const finalXml = selectedNodes.length > 0
|
|
73
|
+
? `<memory_nodes>\n${renderedNodes}\n</memory_nodes>`
|
|
74
|
+
: (fallbackMessage ?? "<fallback>No relevant memories fit the token budget.</fallback>");
|
|
75
|
+
const finalTokens = estimateTokens(finalXml);
|
|
76
|
+
if (debug) {
|
|
77
|
+
const debugInfo = {
|
|
78
|
+
budget,
|
|
79
|
+
usedTokens,
|
|
80
|
+
selectedCount: selectedNodes.length,
|
|
81
|
+
candidates: candidatesWithScore.map(c => ({ id: c.node.id, score: c.score, tokens: c.tokens })),
|
|
82
|
+
};
|
|
83
|
+
return `<debug>${JSON.stringify(debugInfo)}</debug>\n\n${finalXml}\n\n---\nToken count: ${finalTokens}`;
|
|
84
|
+
}
|
|
85
|
+
const pricePerThousand = 0.00015;
|
|
86
|
+
const costUsd = (finalTokens / 1000) * pricePerThousand;
|
|
87
|
+
return `${finalXml}\n\n---\nToken count: ${finalTokens}\nEstimated cost: $${costUsd.toFixed(6)}`;
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
return t;
|
|
91
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { tool } from "@opencode-ai/plugin";
|
|
2
|
+
import { estimateTokens } from "../embeddings";
|
|
3
|
+
import { wrapWithTracking } from "./shared";
|
|
4
|
+
const CONTEXT_LIMIT = 128000;
|
|
5
|
+
const MAX_INJECTION_TOKENS = 8000;
|
|
6
|
+
const HIGH_CONTEXT_THRESHOLD = 0.6;
|
|
7
|
+
const CRITICAL_CONTEXT_THRESHOLD = 0.8;
|
|
8
|
+
export function MemoryInjectionDebug(store) {
|
|
9
|
+
const t = tool({
|
|
10
|
+
description: "Show selected memory node IDs and token usage for the last injection (no side‑effects).",
|
|
11
|
+
args: {
|
|
12
|
+
session_id: tool.schema.string().optional().describe("Session ID – defaults to current session"),
|
|
13
|
+
},
|
|
14
|
+
async execute(args, toolCtx) {
|
|
15
|
+
const nodes = await store.listNodes("all");
|
|
16
|
+
const totalTokens = nodes.reduce((sum, n) => sum + estimateTokens(n.content), 0);
|
|
17
|
+
const contextPercent = totalTokens / CONTEXT_LIMIT;
|
|
18
|
+
const coreNodes = nodes.filter((n) => n.sticky || n.importance >= 0.9 || (n.scope === "global" && (n.type === "core" || n.label?.includes("persona") || n.label?.includes("preference"))));
|
|
19
|
+
const isCritical = contextPercent >= CRITICAL_CONTEXT_THRESHOLD;
|
|
20
|
+
const initialSelected = isCritical ? coreNodes.slice(0, 5) : [...coreNodes];
|
|
21
|
+
const budget = isCritical ? 2000 : MAX_INJECTION_TOKENS;
|
|
22
|
+
let injectionTokens = initialSelected.reduce((s, n) => s + estimateTokens(n.content), 0);
|
|
23
|
+
const selected = [...initialSelected];
|
|
24
|
+
if (!isCritical || injectionTokens < budget) {
|
|
25
|
+
for (const n of nodes) {
|
|
26
|
+
if (selected.includes(n))
|
|
27
|
+
continue;
|
|
28
|
+
const tkn = estimateTokens(n.content);
|
|
29
|
+
if (injectionTokens + tkn > budget)
|
|
30
|
+
break;
|
|
31
|
+
selected.push(n);
|
|
32
|
+
injectionTokens += tkn;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const mode = isCritical ? "critical" : contextPercent >= HIGH_CONTEXT_THRESHOLD ? "high" : "normal";
|
|
36
|
+
const result = {
|
|
37
|
+
mode,
|
|
38
|
+
contextPercent: contextPercent.toFixed(3),
|
|
39
|
+
selectedCount: selected.length,
|
|
40
|
+
injectionTokens,
|
|
41
|
+
budget: MAX_INJECTION_TOKENS,
|
|
42
|
+
nodeIds: selected.map((n) => n.id),
|
|
43
|
+
};
|
|
44
|
+
return JSON.stringify(result, null, 2);
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
return wrapWithTracking(t, store, "memory_injection_debug");
|
|
48
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { tool } from "@opencode-ai/plugin";
|
|
2
|
+
export function JournalWrite(store, ctx) {
|
|
3
|
+
const t = tool({
|
|
4
|
+
description: "Write a new journal entry. Use this to capture insights, technical discoveries, " +
|
|
5
|
+
"design decisions, observations, or reflections. Entries are append-only and cannot be edited. " +
|
|
6
|
+
"Tags are optional comma-separated names, e.g. \"perf, debugging\".",
|
|
7
|
+
args: {
|
|
8
|
+
title: tool.schema.string(),
|
|
9
|
+
body: tool.schema.string(),
|
|
10
|
+
tags: tool.schema.string().optional(),
|
|
11
|
+
},
|
|
12
|
+
async execute(args, toolCtx) {
|
|
13
|
+
const tags = args.tags
|
|
14
|
+
? args.tags
|
|
15
|
+
.split(",")
|
|
16
|
+
.map((t) => t.trim())
|
|
17
|
+
.filter(Boolean)
|
|
18
|
+
: undefined;
|
|
19
|
+
const entry = await store.write({
|
|
20
|
+
title: args.title,
|
|
21
|
+
body: args.body,
|
|
22
|
+
project: ctx.directory,
|
|
23
|
+
model: ctx.model,
|
|
24
|
+
provider: ctx.provider,
|
|
25
|
+
agent: toolCtx.agent,
|
|
26
|
+
sessionId: toolCtx.sessionID,
|
|
27
|
+
tags,
|
|
28
|
+
});
|
|
29
|
+
return `Journal entry created: ${entry.id}\n title: ${entry.title}\n created: ${entry.created.toISOString()}`;
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
return t;
|
|
33
|
+
}
|
|
34
|
+
export function JournalRead(store) {
|
|
35
|
+
const t = tool({
|
|
36
|
+
description: "Read a specific journal entry by its ID. Returns the full entry " +
|
|
37
|
+
"including metadata and body.",
|
|
38
|
+
args: {
|
|
39
|
+
id: tool.schema.string(),
|
|
40
|
+
},
|
|
41
|
+
async execute(args) {
|
|
42
|
+
const entry = await store.read(args.id);
|
|
43
|
+
const meta = [
|
|
44
|
+
`title: ${entry.title}`,
|
|
45
|
+
`created: ${entry.created.toISOString()}`,
|
|
46
|
+
entry.project ? `project: ${entry.project}` : null,
|
|
47
|
+
entry.model ? `model: ${entry.model}` : null,
|
|
48
|
+
entry.provider ? `provider: ${entry.provider}` : null,
|
|
49
|
+
entry.agent ? `agent: ${entry.agent}` : null,
|
|
50
|
+
entry.sessionId ? `session: ${entry.sessionId}` : null,
|
|
51
|
+
entry.tags.length > 0 ? `tags: ${entry.tags.join(", ")}` : null,
|
|
52
|
+
]
|
|
53
|
+
.filter(Boolean)
|
|
54
|
+
.join("\n");
|
|
55
|
+
return `${meta}\n\n${entry.body}`;
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
return t;
|
|
59
|
+
}
|
|
60
|
+
export function JournalSearch(store) {
|
|
61
|
+
const t = tool({
|
|
62
|
+
description: "Search journal entries using semantic similarity. Returns matching entries " +
|
|
63
|
+
"sorted by relevance. All filters are optional and combined with AND logic. " +
|
|
64
|
+
"Use with no arguments to list recent entries. Use offset to paginate.",
|
|
65
|
+
args: {
|
|
66
|
+
text: tool.schema.string().optional(),
|
|
67
|
+
project: tool.schema.string().optional(),
|
|
68
|
+
tags: tool.schema.string().optional(),
|
|
69
|
+
limit: tool.schema.number().int().positive().optional(),
|
|
70
|
+
offset: tool.schema.number().int().nonnegative().optional(),
|
|
71
|
+
},
|
|
72
|
+
async execute(args) {
|
|
73
|
+
const tags = args.tags
|
|
74
|
+
? args.tags
|
|
75
|
+
.split(",")
|
|
76
|
+
.map((t) => t.trim())
|
|
77
|
+
.filter(Boolean)
|
|
78
|
+
: undefined;
|
|
79
|
+
const result = await store.search({
|
|
80
|
+
text: args.text,
|
|
81
|
+
project: args.project,
|
|
82
|
+
tags,
|
|
83
|
+
limit: args.limit,
|
|
84
|
+
offset: args.offset,
|
|
85
|
+
});
|
|
86
|
+
if (result.entries.length === 0) {
|
|
87
|
+
const tagsLine = result.allTags.length > 0
|
|
88
|
+
? `\nTags in use: ${result.allTags.join(", ")}`
|
|
89
|
+
: "";
|
|
90
|
+
return `No journal entries found.${tagsLine}`;
|
|
91
|
+
}
|
|
92
|
+
const offset = args.offset ?? 0;
|
|
93
|
+
const header = `Found ${result.total} entries (showing ${offset + 1}–${offset + result.entries.length}):`;
|
|
94
|
+
const tagsLine = result.allTags.length > 0
|
|
95
|
+
? `\nTags in use: ${result.allTags.join(", ")}`
|
|
96
|
+
: "";
|
|
97
|
+
const lines = result.entries.map((e) => {
|
|
98
|
+
const tagStr = e.tags.length > 0 ? ` [${e.tags.join(", ")}]` : "";
|
|
99
|
+
return `${e.id}\n ${e.title}${tagStr}\n ${e.created.toISOString()}`;
|
|
100
|
+
});
|
|
101
|
+
return `${header}${tagsLine}\n\n${lines.join("\n\n")}`;
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
return t;
|
|
105
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { tool } from "@opencode-ai/plugin";
|
|
2
|
+
import { wrapWithTracking } from "./shared";
|
|
3
|
+
export function MemoryLlmCompress(store, client) {
|
|
4
|
+
const t = tool({
|
|
5
|
+
description: "LLM-powered compression of memory nodes. Uses the session LLM to generate richer, contextual summaries instead of regex-based extraction.",
|
|
6
|
+
args: {
|
|
7
|
+
scope: tool.schema.enum(["all", "global", "project"]).optional(),
|
|
8
|
+
level: tool.schema.number().int().nonnegative().optional(),
|
|
9
|
+
dry_run: tool.schema.boolean().optional().default(false),
|
|
10
|
+
},
|
|
11
|
+
async execute(args) {
|
|
12
|
+
const { scope, level, dry_run } = args;
|
|
13
|
+
if (!client) {
|
|
14
|
+
return "Error: No LLM client available. LLM compression requires an active session.";
|
|
15
|
+
}
|
|
16
|
+
if (dry_run) {
|
|
17
|
+
const candidates = await store.getCompressionCandidates(scope ?? "all", (level ?? 0));
|
|
18
|
+
if (candidates.length === 0) {
|
|
19
|
+
return `Dry run: no nodes to compress at level ${level ?? 0}.`;
|
|
20
|
+
}
|
|
21
|
+
return `Dry run: found ${candidates.length} candidates for LLM compression.\n` +
|
|
22
|
+
candidates.map(c => `- ${c.id.slice(0, 8)}: ${c.content.slice(0, 50)}...`).join("\n");
|
|
23
|
+
}
|
|
24
|
+
const result = await store.runCompression(scope ?? "all", false, client);
|
|
25
|
+
return `LLM compression completed: ${result.compressed} nodes compressed, ${result.created} summary nodes created.`;
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
return wrapWithTracking(t, store, "memory_llm_compress");
|
|
29
|
+
}
|
|
30
|
+
export function MemoryDetectTopics(store) {
|
|
31
|
+
const t = tool({
|
|
32
|
+
description: "Detect topic boundaries in memory (placeholder).",
|
|
33
|
+
args: {
|
|
34
|
+
scope: tool.schema.enum(["all", "global", "project"]).optional(),
|
|
35
|
+
},
|
|
36
|
+
async execute(args) {
|
|
37
|
+
return "Topic detection not implemented in this build.";
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
return wrapWithTracking(t, store, "memory_detect_topics");
|
|
41
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { tool } from "@opencode-ai/plugin";
|
|
2
|
+
/**
|
|
3
|
+
* Retrieve middle-term context snapshots (pre-compaction captures).
|
|
4
|
+
* These are created automatically before OpenCode's compaction process.
|
|
5
|
+
*/
|
|
6
|
+
export function MemoryMiddleTerm(store) {
|
|
7
|
+
return tool({
|
|
8
|
+
description: "Retrieve middle-term context snapshots (pre-compaction captures). These are created automatically before OpenCode's compaction process to preserve working state.",
|
|
9
|
+
args: {
|
|
10
|
+
sessionId: tool.schema.string().optional().describe("Filter by session ID"),
|
|
11
|
+
limit: tool.schema.number().optional().describe("Maximum number of results to return (default: 10)"),
|
|
12
|
+
},
|
|
13
|
+
async execute(args) {
|
|
14
|
+
// Get all nodes and filter by metadata.customType="middle-term" or type="note" with appropriate metadata
|
|
15
|
+
const allNodes = await store.listNodes("all");
|
|
16
|
+
let nodes = allNodes.filter(n => {
|
|
17
|
+
// Check if metadata contains customType: "middle-term" or if label starts with "middle-term:"
|
|
18
|
+
if (n.metadata && typeof n.metadata === 'object' && 'customType' in n.metadata) {
|
|
19
|
+
return n.metadata.customType === "middle-term";
|
|
20
|
+
}
|
|
21
|
+
return n.label?.startsWith("middle-term:") ?? false;
|
|
22
|
+
});
|
|
23
|
+
// Filter by sessionId if provided
|
|
24
|
+
if (args.sessionId) {
|
|
25
|
+
const sessionId = args.sessionId; // Create a const to help TypeScript
|
|
26
|
+
nodes = nodes.filter(n => {
|
|
27
|
+
try {
|
|
28
|
+
const data = JSON.parse(n.content);
|
|
29
|
+
return data.sessionId === sessionId;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return n.label?.includes(sessionId) ?? false;
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
// Limit results
|
|
37
|
+
nodes = nodes.slice(0, args.limit || 10);
|
|
38
|
+
if (nodes.length === 0) {
|
|
39
|
+
return "No middle-term context found.";
|
|
40
|
+
}
|
|
41
|
+
// Parse and return structured info
|
|
42
|
+
const results = nodes.map(n => {
|
|
43
|
+
try {
|
|
44
|
+
const data = JSON.parse(n.content);
|
|
45
|
+
return {
|
|
46
|
+
label: n.label,
|
|
47
|
+
timestamp: data.timestamp || "unknown",
|
|
48
|
+
sessionId: data.sessionId || "unknown",
|
|
49
|
+
workingCacheSize: data.workingCache?.length || 0,
|
|
50
|
+
recentNodesCount: data.recentNodes?.length || 0,
|
|
51
|
+
contextTokens: data.contextTokens || 0,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return {
|
|
56
|
+
label: n.label,
|
|
57
|
+
timestamp: "unknown",
|
|
58
|
+
sessionId: "unknown",
|
|
59
|
+
workingCacheSize: 0,
|
|
60
|
+
recentNodesCount: 0,
|
|
61
|
+
contextTokens: 0,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
return JSON.stringify(results, null, 2);
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { tool } from "@opencode-ai/plugin";
|
|
2
|
+
import { wrapWithContextWarning, wrapWithTracking } from "./shared";
|
|
3
|
+
export function MemoryPlaybookExecute(memoryStore) {
|
|
4
|
+
const t = tool({
|
|
5
|
+
description: "Execute a playbook — returns the ordered steps for the agent to follow. The agent should execute each step in sequence and report progress.",
|
|
6
|
+
args: {
|
|
7
|
+
playbook_id: tool.schema.string().describe("The playbook label (e.g. 'playbook:debug-workflow')"),
|
|
8
|
+
params: tool.schema.string().optional().describe("JSON object of parameter values to substitute into step params"),
|
|
9
|
+
},
|
|
10
|
+
async execute(args) {
|
|
11
|
+
let node;
|
|
12
|
+
try {
|
|
13
|
+
node = await memoryStore.getNodeByLabel("project", args.playbook_id);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return `Playbook not found: ${args.playbook_id}. List available playbooks with memory_search({ type: \"playbook" }).`;
|
|
17
|
+
}
|
|
18
|
+
const metadata = node.metadata || {};
|
|
19
|
+
const rawSteps = metadata.steps;
|
|
20
|
+
const steps = Array.isArray(rawSteps) ? rawSteps : [];
|
|
21
|
+
if (steps.length === 0) {
|
|
22
|
+
return `Playbook "${args.playbook_id}" has no steps defined in its metadata.`;
|
|
23
|
+
}
|
|
24
|
+
let params = {};
|
|
25
|
+
if (args.params) {
|
|
26
|
+
try {
|
|
27
|
+
params = JSON.parse(args.params);
|
|
28
|
+
if (typeof params !== "object" || Array.isArray(params))
|
|
29
|
+
throw new Error();
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return "Error: params must be a valid JSON object.";
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const resolvedSteps = steps.map((step, i) => {
|
|
36
|
+
const resolvedParams = {};
|
|
37
|
+
for (const [key, val] of Object.entries(step.params)) {
|
|
38
|
+
resolvedParams[key] = val.replace(/\{(\w+)\}/g, (_, name) => params[name] ?? `{${name}}`);
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
index: i + 1,
|
|
42
|
+
toolName: step.toolName,
|
|
43
|
+
description: step.description,
|
|
44
|
+
params: resolvedParams,
|
|
45
|
+
expectedOutcome: step.expectedOutcome,
|
|
46
|
+
critical: step.critical,
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
const executionCount = (metadata.executionCount ?? 0) + 1;
|
|
50
|
+
await memoryStore.updateNode(node.id, {
|
|
51
|
+
metadata: { ...metadata, executionCount, lastExecutedAt: Date.now() },
|
|
52
|
+
});
|
|
53
|
+
const stepsStr = resolvedSteps.map(s => `Step ${s.index}: ${s.toolName} — ${s.description}\n` +
|
|
54
|
+
` Params: ${JSON.stringify(s.params)}${s.expectedOutcome ? `\n Expected: ${s.expectedOutcome}` : ""}${s.critical ? " critical" : ""}`).join("\n\n");
|
|
55
|
+
const label = node.label || args.playbook_id;
|
|
56
|
+
return wrapWithContextWarning(`Executing playbook: ${label}\n` +
|
|
57
|
+
`Description: ${node.content?.slice(0, 200) || "No description"}\n` +
|
|
58
|
+
`Total steps: ${steps.length}\n\n` +
|
|
59
|
+
`${stepsStr}\n\n` +
|
|
60
|
+
`Execute each step above in order. Report completion after each step.`);
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
return wrapWithTracking(t, memoryStore, "memory_playbook_execute");
|
|
64
|
+
}
|