@basicmemory/openclaw-basic-memory 0.2.2 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -18
- package/commands/cli.ts +1 -1
- package/commands/skills.ts +22 -10
- package/commands/slash.ts +4 -2
- package/config.ts +1 -0
- package/context-engine/basic-memory-context-engine.ts +38 -57
- package/dist/bm-client.d.ts +177 -0
- package/dist/bm-client.js +572 -0
- package/dist/commands/cli.d.ts +4 -0
- package/dist/commands/cli.js +117 -0
- package/dist/commands/skills.d.ts +2 -0
- package/dist/commands/skills.js +47 -0
- package/dist/commands/slash.d.ts +3 -0
- package/dist/commands/slash.js +92 -0
- package/dist/config.d.ts +22 -0
- package/dist/config.js +109 -0
- package/dist/context-engine/basic-memory-context-engine.d.ts +94 -0
- package/dist/context-engine/basic-memory-context-engine.js +176 -0
- package/dist/hooks/capture.d.ts +24 -0
- package/dist/hooks/capture.js +88 -0
- package/dist/hooks/recall.d.ts +24 -0
- package/dist/hooks/recall.js +56 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +125 -0
- package/dist/logger.d.ts +13 -0
- package/dist/logger.js +34 -0
- package/dist/schema/conversation-schema.d.ts +5 -0
- package/dist/schema/conversation-schema.js +33 -0
- package/dist/schema/task-schema.d.ts +5 -0
- package/dist/schema/task-schema.js +34 -0
- package/dist/tools/build-context.d.ts +3 -0
- package/dist/tools/build-context.js +100 -0
- package/dist/tools/delete-note.d.ts +3 -0
- package/dist/tools/delete-note.js +52 -0
- package/dist/tools/edit-note.d.ts +3 -0
- package/dist/tools/edit-note.js +81 -0
- package/dist/tools/list-memory-projects.d.ts +3 -0
- package/dist/tools/list-memory-projects.js +85 -0
- package/dist/tools/list-workspaces.d.ts +3 -0
- package/dist/tools/list-workspaces.js +64 -0
- package/dist/tools/memory-provider.d.ts +15 -0
- package/dist/tools/memory-provider.js +274 -0
- package/dist/tools/move-note.d.ts +3 -0
- package/dist/tools/move-note.js +56 -0
- package/dist/tools/read-note.d.ts +3 -0
- package/dist/tools/read-note.js +57 -0
- package/dist/tools/schema-diff.d.ts +3 -0
- package/dist/tools/schema-diff.js +81 -0
- package/dist/tools/schema-infer.d.ts +3 -0
- package/dist/tools/schema-infer.js +66 -0
- package/dist/tools/schema-validate.d.ts +3 -0
- package/dist/tools/schema-validate.js +73 -0
- package/dist/tools/search-notes.d.ts +3 -0
- package/dist/tools/search-notes.js +97 -0
- package/dist/tools/write-note.d.ts +3 -0
- package/dist/tools/write-note.js +80 -0
- package/index.ts +4 -5
- package/openclaw.plugin.json +28 -8
- package/package.json +24 -6
- package/tools/build-context.ts +1 -1
- package/tools/delete-note.ts +1 -1
- package/tools/edit-note.ts +1 -1
- package/tools/list-memory-projects.ts +1 -1
- package/tools/list-workspaces.ts +1 -1
- package/tools/memory-provider.ts +1 -1
- package/tools/move-note.ts +1 -1
- package/tools/read-note.ts +1 -1
- package/tools/schema-diff.ts +1 -1
- package/tools/schema-infer.ts +1 -1
- package/tools/schema-validate.ts +1 -1
- package/tools/search-notes.ts +9 -5
- package/tools/write-note.ts +1 -1
- package/context-engine/basic-memory-context-engine.test.ts +0 -398
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
5
|
+
function resolveSkillsDir(api) {
|
|
6
|
+
if (api.resolvePath) {
|
|
7
|
+
return api.resolvePath("skills");
|
|
8
|
+
}
|
|
9
|
+
const sourceRootSkills = resolve(__dirname, "..", "skills");
|
|
10
|
+
if (existsSync(sourceRootSkills)) {
|
|
11
|
+
return sourceRootSkills;
|
|
12
|
+
}
|
|
13
|
+
return resolve(__dirname, "..", "..", "skills");
|
|
14
|
+
}
|
|
15
|
+
function loadManifest(skillsDir) {
|
|
16
|
+
try {
|
|
17
|
+
const raw = readFileSync(resolve(skillsDir, "manifest.json"), "utf-8");
|
|
18
|
+
return JSON.parse(raw);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
throw new Error("skills/manifest.json not found. Run `bun scripts/fetch-skills.ts` first.");
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function loadSkill(skillsDir, dir) {
|
|
25
|
+
return readFileSync(resolve(skillsDir, dir, "SKILL.md"), "utf-8");
|
|
26
|
+
}
|
|
27
|
+
export function registerSkillCommands(api) {
|
|
28
|
+
const skillsDir = resolveSkillsDir(api);
|
|
29
|
+
const manifest = loadManifest(skillsDir);
|
|
30
|
+
for (const entry of manifest) {
|
|
31
|
+
const commandName = entry.dir.replace(/^memory-/, "");
|
|
32
|
+
const content = loadSkill(skillsDir, entry.dir);
|
|
33
|
+
api.registerCommand({
|
|
34
|
+
name: commandName,
|
|
35
|
+
description: entry.description,
|
|
36
|
+
acceptsArgs: true,
|
|
37
|
+
requireAuth: true,
|
|
38
|
+
handler: async (ctx) => {
|
|
39
|
+
const args = ctx.args?.trim();
|
|
40
|
+
const prefix = args
|
|
41
|
+
? `User request: ${args}\n\nFollow this workflow:\n\n`
|
|
42
|
+
: "Follow this workflow:\n\n";
|
|
43
|
+
return { text: prefix + content };
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { log } from "../logger.js";
|
|
5
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
export function registerCommands(api, client) {
|
|
7
|
+
api.registerCommand({
|
|
8
|
+
name: "bm-setup",
|
|
9
|
+
description: "Install or update the Basic Memory CLI (requires uv)",
|
|
10
|
+
requireAuth: true,
|
|
11
|
+
handler: async () => {
|
|
12
|
+
const scriptPath = api.resolvePath
|
|
13
|
+
? api.resolvePath("scripts/setup-bm.sh")
|
|
14
|
+
: resolve(__dirname, "..", "scripts", "setup-bm.sh");
|
|
15
|
+
log.info(`/bm-setup: running ${scriptPath}`);
|
|
16
|
+
try {
|
|
17
|
+
const output = execSync(`bash "${scriptPath}"`, {
|
|
18
|
+
encoding: "utf-8",
|
|
19
|
+
timeout: 180_000,
|
|
20
|
+
stdio: "pipe",
|
|
21
|
+
env: { ...process.env },
|
|
22
|
+
});
|
|
23
|
+
return { text: output.trim() };
|
|
24
|
+
}
|
|
25
|
+
catch (err) {
|
|
26
|
+
const execErr = err;
|
|
27
|
+
const detail = execErr.stderr || execErr.stdout || String(err);
|
|
28
|
+
log.error("/bm-setup failed", err);
|
|
29
|
+
return {
|
|
30
|
+
text: `Setup failed:\n${detail.trim()}`,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
api.registerCommand({
|
|
36
|
+
name: "remember",
|
|
37
|
+
description: "Save something to the Basic Memory knowledge graph",
|
|
38
|
+
acceptsArgs: true,
|
|
39
|
+
requireAuth: true,
|
|
40
|
+
handler: async (ctx) => {
|
|
41
|
+
const text = ctx.args?.trim();
|
|
42
|
+
if (!text) {
|
|
43
|
+
return { text: "Usage: /remember <text to save as a note>" };
|
|
44
|
+
}
|
|
45
|
+
log.debug(`/remember: "${text.slice(0, 50)}"`);
|
|
46
|
+
try {
|
|
47
|
+
const title = text.length > 60 ? text.slice(0, 60) : text;
|
|
48
|
+
await client.writeNote(title, text, "agent/memories");
|
|
49
|
+
const preview = text.length > 60 ? `${text.slice(0, 60)}...` : text;
|
|
50
|
+
return { text: `Remembered: "${preview}"` };
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
log.error("/remember failed", err);
|
|
54
|
+
return {
|
|
55
|
+
text: "Failed to save memory. Is Basic Memory running?",
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
api.registerCommand({
|
|
61
|
+
name: "recall",
|
|
62
|
+
description: "Search the Basic Memory knowledge graph",
|
|
63
|
+
acceptsArgs: true,
|
|
64
|
+
requireAuth: true,
|
|
65
|
+
handler: async (ctx) => {
|
|
66
|
+
const query = ctx.args?.trim();
|
|
67
|
+
if (!query) {
|
|
68
|
+
return { text: "Usage: /recall <search query>" };
|
|
69
|
+
}
|
|
70
|
+
log.debug(`/recall: "${query}"`);
|
|
71
|
+
try {
|
|
72
|
+
const results = await client.search(query, 5);
|
|
73
|
+
if (results.length === 0) {
|
|
74
|
+
return { text: `No notes found for: "${query}"` };
|
|
75
|
+
}
|
|
76
|
+
const lines = results.map((r, i) => {
|
|
77
|
+
const score = r.score ? ` (${(r.score * 100).toFixed(0)}%)` : "";
|
|
78
|
+
return `${i + 1}. ${r.title}${score}`;
|
|
79
|
+
});
|
|
80
|
+
return {
|
|
81
|
+
text: `Found ${results.length} notes:\n\n${lines.join("\n")}`,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
log.error("/recall failed", err);
|
|
86
|
+
return {
|
|
87
|
+
text: "Failed to search. Is Basic Memory running?",
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export type CloudConfig = {
|
|
2
|
+
url: string;
|
|
3
|
+
api_key: string;
|
|
4
|
+
};
|
|
5
|
+
export type BasicMemoryConfig = {
|
|
6
|
+
project: string;
|
|
7
|
+
bmPath: string;
|
|
8
|
+
memoryDir: string;
|
|
9
|
+
memoryFile: string;
|
|
10
|
+
projectPath: string;
|
|
11
|
+
autoCapture: boolean;
|
|
12
|
+
captureMinChars: number;
|
|
13
|
+
autoRecall: boolean;
|
|
14
|
+
recallPrompt: string;
|
|
15
|
+
debug: boolean;
|
|
16
|
+
cloud?: CloudConfig;
|
|
17
|
+
};
|
|
18
|
+
export declare function resolveProjectPath(projectPath: string, workspaceDir: string): string;
|
|
19
|
+
export declare function parseConfig(raw: unknown): BasicMemoryConfig;
|
|
20
|
+
export declare const basicMemoryConfigSchema: {
|
|
21
|
+
parse: typeof parseConfig;
|
|
22
|
+
};
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { homedir, hostname } from "node:os";
|
|
2
|
+
import { isAbsolute, resolve } from "node:path";
|
|
3
|
+
const ALLOWED_KEYS = [
|
|
4
|
+
"project",
|
|
5
|
+
"bmPath",
|
|
6
|
+
"memoryDir",
|
|
7
|
+
"memory_dir",
|
|
8
|
+
"memoryFile",
|
|
9
|
+
"memory_file",
|
|
10
|
+
"projectPath",
|
|
11
|
+
"autoCapture",
|
|
12
|
+
"captureMinChars",
|
|
13
|
+
"capture_min_chars",
|
|
14
|
+
"autoRecall",
|
|
15
|
+
"auto_recall",
|
|
16
|
+
"recallPrompt",
|
|
17
|
+
"recall_prompt",
|
|
18
|
+
"debug",
|
|
19
|
+
"cloud",
|
|
20
|
+
];
|
|
21
|
+
function assertAllowedKeys(value, allowed, label) {
|
|
22
|
+
const unknown = Object.keys(value).filter((k) => !allowed.includes(k));
|
|
23
|
+
if (unknown.length > 0) {
|
|
24
|
+
throw new Error(`${label} has unknown keys: ${unknown.join(", ")}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function defaultProject() {
|
|
28
|
+
return `openclaw-${hostname()
|
|
29
|
+
.replace(/[^a-zA-Z0-9-]/g, "-")
|
|
30
|
+
.toLowerCase()}`;
|
|
31
|
+
}
|
|
32
|
+
function expandUserPath(path) {
|
|
33
|
+
if (path === "~")
|
|
34
|
+
return homedir();
|
|
35
|
+
if (path.startsWith("~/"))
|
|
36
|
+
return `${homedir()}/${path.slice(2)}`;
|
|
37
|
+
return path;
|
|
38
|
+
}
|
|
39
|
+
export function resolveProjectPath(projectPath, workspaceDir) {
|
|
40
|
+
const expanded = expandUserPath(projectPath);
|
|
41
|
+
if (isAbsolute(expanded))
|
|
42
|
+
return expanded;
|
|
43
|
+
return resolve(workspaceDir, expanded);
|
|
44
|
+
}
|
|
45
|
+
export function parseConfig(raw) {
|
|
46
|
+
const cfg = raw && typeof raw === "object" && !Array.isArray(raw)
|
|
47
|
+
? raw
|
|
48
|
+
: {};
|
|
49
|
+
if (Object.keys(cfg).length > 0) {
|
|
50
|
+
assertAllowedKeys(cfg, ALLOWED_KEYS, "basic-memory config");
|
|
51
|
+
}
|
|
52
|
+
// Support both camelCase and snake_case for memory_dir / memory_file
|
|
53
|
+
const memoryDir = typeof cfg.memoryDir === "string" && cfg.memoryDir.length > 0
|
|
54
|
+
? cfg.memoryDir
|
|
55
|
+
: typeof cfg.memory_dir === "string" &&
|
|
56
|
+
cfg.memory_dir.length > 0
|
|
57
|
+
? cfg.memory_dir
|
|
58
|
+
: "memory/";
|
|
59
|
+
const memoryFile = typeof cfg.memoryFile === "string" && cfg.memoryFile.length > 0
|
|
60
|
+
? cfg.memoryFile
|
|
61
|
+
: typeof cfg.memory_file === "string" &&
|
|
62
|
+
cfg.memory_file.length > 0
|
|
63
|
+
? cfg.memory_file
|
|
64
|
+
: "MEMORY.md";
|
|
65
|
+
let cloud;
|
|
66
|
+
if (cfg.cloud && typeof cfg.cloud === "object" && !Array.isArray(cfg.cloud)) {
|
|
67
|
+
const c = cfg.cloud;
|
|
68
|
+
assertAllowedKeys(c, ["url", "api_key"], "basic-memory cloud config");
|
|
69
|
+
if (typeof c.url === "string" && typeof c.api_key === "string") {
|
|
70
|
+
cloud = { url: c.url, api_key: c.api_key };
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
project: typeof cfg.project === "string" && cfg.project.length > 0
|
|
75
|
+
? cfg.project
|
|
76
|
+
: defaultProject(),
|
|
77
|
+
projectPath: typeof cfg.projectPath === "string" && cfg.projectPath.length > 0
|
|
78
|
+
? cfg.projectPath
|
|
79
|
+
: ".",
|
|
80
|
+
bmPath: typeof cfg.bmPath === "string" && cfg.bmPath.length > 0
|
|
81
|
+
? cfg.bmPath
|
|
82
|
+
: "bm",
|
|
83
|
+
memoryDir,
|
|
84
|
+
memoryFile,
|
|
85
|
+
autoCapture: typeof cfg.autoCapture === "boolean" ? cfg.autoCapture : true,
|
|
86
|
+
captureMinChars: typeof cfg.captureMinChars === "number" && cfg.captureMinChars >= 0
|
|
87
|
+
? cfg.captureMinChars
|
|
88
|
+
: typeof cfg.capture_min_chars === "number" &&
|
|
89
|
+
cfg.capture_min_chars >= 0
|
|
90
|
+
? cfg.capture_min_chars
|
|
91
|
+
: 10,
|
|
92
|
+
autoRecall: typeof cfg.autoRecall === "boolean"
|
|
93
|
+
? cfg.autoRecall
|
|
94
|
+
: typeof cfg.auto_recall === "boolean"
|
|
95
|
+
? cfg.auto_recall
|
|
96
|
+
: true,
|
|
97
|
+
recallPrompt: typeof cfg.recallPrompt === "string" && cfg.recallPrompt.length > 0
|
|
98
|
+
? cfg.recallPrompt
|
|
99
|
+
: typeof cfg.recall_prompt === "string" &&
|
|
100
|
+
cfg.recall_prompt.length > 0
|
|
101
|
+
? cfg.recall_prompt
|
|
102
|
+
: "Check for active tasks and recent activity. Summarize anything relevant to the current session.",
|
|
103
|
+
debug: typeof cfg.debug === "boolean" ? cfg.debug : false,
|
|
104
|
+
cloud,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
export const basicMemoryConfigSchema = {
|
|
108
|
+
parse: parseConfig,
|
|
109
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { AgentMessage } from "@mariozechner/pi-agent-core";
|
|
2
|
+
import type { BmClient } from "../bm-client.ts";
|
|
3
|
+
import type { BasicMemoryConfig } from "../config.ts";
|
|
4
|
+
export declare const MAX_ASSEMBLE_RECALL_CHARS = 1200;
|
|
5
|
+
type AssembleResult = {
|
|
6
|
+
messages: AgentMessage[];
|
|
7
|
+
estimatedTokens: number;
|
|
8
|
+
systemPromptAddition?: string;
|
|
9
|
+
};
|
|
10
|
+
type BootstrapResult = {
|
|
11
|
+
bootstrapped: boolean;
|
|
12
|
+
importedMessages?: number;
|
|
13
|
+
reason?: string;
|
|
14
|
+
};
|
|
15
|
+
type CompactResult = {
|
|
16
|
+
ok: boolean;
|
|
17
|
+
compacted: boolean;
|
|
18
|
+
reason?: string;
|
|
19
|
+
result?: {
|
|
20
|
+
summary?: string;
|
|
21
|
+
firstKeptEntryId?: string;
|
|
22
|
+
tokensBefore: number;
|
|
23
|
+
tokensAfter?: number;
|
|
24
|
+
details?: unknown;
|
|
25
|
+
sessionId?: string;
|
|
26
|
+
sessionFile?: string;
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
interface ContextEngine {
|
|
30
|
+
readonly info: {
|
|
31
|
+
id: string;
|
|
32
|
+
name: string;
|
|
33
|
+
version?: string;
|
|
34
|
+
ownsCompaction?: boolean;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export declare class BasicMemoryContextEngine implements ContextEngine {
|
|
38
|
+
private readonly client;
|
|
39
|
+
private readonly cfg;
|
|
40
|
+
readonly info: {
|
|
41
|
+
readonly id: "openclaw-basic-memory";
|
|
42
|
+
readonly name: "Basic Memory Context Engine";
|
|
43
|
+
readonly version: "0.1.5";
|
|
44
|
+
readonly ownsCompaction: false;
|
|
45
|
+
};
|
|
46
|
+
private readonly sessionState;
|
|
47
|
+
private readonly subagentState;
|
|
48
|
+
constructor(client: BmClient, cfg: BasicMemoryConfig);
|
|
49
|
+
bootstrap(params: {
|
|
50
|
+
sessionId: string;
|
|
51
|
+
sessionFile: string;
|
|
52
|
+
}): Promise<BootstrapResult>;
|
|
53
|
+
ingest(): Promise<{
|
|
54
|
+
ingested: boolean;
|
|
55
|
+
}>;
|
|
56
|
+
assemble(params: {
|
|
57
|
+
sessionId: string;
|
|
58
|
+
messages: AgentMessage[];
|
|
59
|
+
tokenBudget?: number;
|
|
60
|
+
}): Promise<AssembleResult>;
|
|
61
|
+
afterTurn(params: {
|
|
62
|
+
sessionId: string;
|
|
63
|
+
sessionFile: string;
|
|
64
|
+
messages: AgentMessage[];
|
|
65
|
+
prePromptMessageCount: number;
|
|
66
|
+
autoCompactionSummary?: string;
|
|
67
|
+
isHeartbeat?: boolean;
|
|
68
|
+
tokenBudget?: number;
|
|
69
|
+
runtimeContext?: Record<string, unknown>;
|
|
70
|
+
}): Promise<void>;
|
|
71
|
+
compact(params: {
|
|
72
|
+
sessionId: string;
|
|
73
|
+
sessionFile: string;
|
|
74
|
+
tokenBudget?: number;
|
|
75
|
+
force?: boolean;
|
|
76
|
+
currentTokenCount?: number;
|
|
77
|
+
compactionTarget?: "budget" | "threshold";
|
|
78
|
+
customInstructions?: string;
|
|
79
|
+
runtimeContext?: Record<string, unknown>;
|
|
80
|
+
}): Promise<CompactResult>;
|
|
81
|
+
prepareSubagentSpawn(params: {
|
|
82
|
+
parentSessionKey: string;
|
|
83
|
+
childSessionKey: string;
|
|
84
|
+
ttlMs?: number;
|
|
85
|
+
}): Promise<{
|
|
86
|
+
rollback: () => Promise<void>;
|
|
87
|
+
} | undefined>;
|
|
88
|
+
onSubagentEnded(params: {
|
|
89
|
+
childSessionKey: string;
|
|
90
|
+
reason: "deleted" | "completed" | "swept" | "released";
|
|
91
|
+
}): Promise<void>;
|
|
92
|
+
dispose(): Promise<void>;
|
|
93
|
+
}
|
|
94
|
+
export {};
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { delegateCompactionToRuntime } from "openclaw/plugin-sdk/core";
|
|
2
|
+
import { selectCaptureTurn } from "../hooks/capture.js";
|
|
3
|
+
import { loadRecallState } from "../hooks/recall.js";
|
|
4
|
+
import { log } from "../logger.js";
|
|
5
|
+
export const MAX_ASSEMBLE_RECALL_CHARS = 1200;
|
|
6
|
+
const TRUNCATED_RECALL_SUFFIX = "\n\n[Basic Memory recall truncated]";
|
|
7
|
+
const SUBAGENT_HANDOFF_FOLDER = "agent/subagents";
|
|
8
|
+
const MAX_SUBAGENT_RECALL_CHARS = 800;
|
|
9
|
+
function boundRecallContext(context) {
|
|
10
|
+
if (context.length <= MAX_ASSEMBLE_RECALL_CHARS) {
|
|
11
|
+
return context;
|
|
12
|
+
}
|
|
13
|
+
const trimmed = context
|
|
14
|
+
.slice(0, Math.max(0, MAX_ASSEMBLE_RECALL_CHARS - TRUNCATED_RECALL_SUFFIX.length))
|
|
15
|
+
.trimEnd();
|
|
16
|
+
return `${trimmed}${TRUNCATED_RECALL_SUFFIX}`;
|
|
17
|
+
}
|
|
18
|
+
function slugifySessionKey(sessionKey) {
|
|
19
|
+
return sessionKey
|
|
20
|
+
.toLowerCase()
|
|
21
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
22
|
+
.replace(/^-+|-+$/g, "")
|
|
23
|
+
.slice(0, 80);
|
|
24
|
+
}
|
|
25
|
+
function buildSubagentNoteTitle(childSessionKey) {
|
|
26
|
+
return `subagent-handoff-${slugifySessionKey(childSessionKey)}`;
|
|
27
|
+
}
|
|
28
|
+
function buildSubagentHandoffContent(params) {
|
|
29
|
+
const sections = [
|
|
30
|
+
"# Subagent Handoff",
|
|
31
|
+
"",
|
|
32
|
+
"## Sessions",
|
|
33
|
+
`- Parent: ${params.parentSessionKey}`,
|
|
34
|
+
`- Child: ${params.childSessionKey}`,
|
|
35
|
+
"",
|
|
36
|
+
"## Lifecycle",
|
|
37
|
+
`- Spawned: ${new Date().toISOString()}`,
|
|
38
|
+
];
|
|
39
|
+
if (params.recallContext) {
|
|
40
|
+
sections.push("", "## Parent Basic Memory Context", params.recallContext.slice(0, MAX_SUBAGENT_RECALL_CHARS).trimEnd());
|
|
41
|
+
}
|
|
42
|
+
return sections.join("\n");
|
|
43
|
+
}
|
|
44
|
+
function buildSubagentCompletionUpdate(params) {
|
|
45
|
+
const statusLine = params.reason === "completed"
|
|
46
|
+
? "Child run completed. Durable conversation capture continues through the normal afterTurn path."
|
|
47
|
+
: `Child run ended with reason: ${params.reason}.`;
|
|
48
|
+
return [
|
|
49
|
+
"",
|
|
50
|
+
"## Completion",
|
|
51
|
+
`- Child: ${params.childSessionKey}`,
|
|
52
|
+
`- Ended: ${new Date().toISOString()}`,
|
|
53
|
+
`- Reason: ${params.reason}`,
|
|
54
|
+
"",
|
|
55
|
+
statusLine,
|
|
56
|
+
].join("\n");
|
|
57
|
+
}
|
|
58
|
+
export class BasicMemoryContextEngine {
|
|
59
|
+
client;
|
|
60
|
+
cfg;
|
|
61
|
+
info = {
|
|
62
|
+
id: "openclaw-basic-memory",
|
|
63
|
+
name: "Basic Memory Context Engine",
|
|
64
|
+
version: "0.1.5",
|
|
65
|
+
ownsCompaction: false,
|
|
66
|
+
};
|
|
67
|
+
sessionState = new Map();
|
|
68
|
+
subagentState = new Map();
|
|
69
|
+
constructor(client, cfg) {
|
|
70
|
+
this.client = client;
|
|
71
|
+
this.cfg = cfg;
|
|
72
|
+
}
|
|
73
|
+
async bootstrap(params) {
|
|
74
|
+
if (!this.cfg.autoRecall) {
|
|
75
|
+
this.sessionState.delete(params.sessionId);
|
|
76
|
+
return { bootstrapped: false, reason: "autoRecall disabled" };
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
const recall = await loadRecallState(this.client, this.cfg);
|
|
80
|
+
if (!recall) {
|
|
81
|
+
this.sessionState.delete(params.sessionId);
|
|
82
|
+
return { bootstrapped: false, reason: "no recall context found" };
|
|
83
|
+
}
|
|
84
|
+
this.sessionState.set(params.sessionId, {
|
|
85
|
+
recallContext: boundRecallContext(recall.context),
|
|
86
|
+
});
|
|
87
|
+
log.debug(`context-engine bootstrap: session=${params.sessionId} tasks=${recall.tasks.length} recent=${recall.recent.length}`);
|
|
88
|
+
return { bootstrapped: true };
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
this.sessionState.delete(params.sessionId);
|
|
92
|
+
log.error("context-engine bootstrap failed", err);
|
|
93
|
+
return { bootstrapped: false, reason: "recall failed" };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
async ingest() {
|
|
97
|
+
return { ingested: false };
|
|
98
|
+
}
|
|
99
|
+
async assemble(params) {
|
|
100
|
+
const state = this.sessionState.get(params.sessionId);
|
|
101
|
+
return {
|
|
102
|
+
messages: params.messages,
|
|
103
|
+
estimatedTokens: 0,
|
|
104
|
+
systemPromptAddition: state?.recallContext,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
async afterTurn(params) {
|
|
108
|
+
if (!this.cfg.autoCapture)
|
|
109
|
+
return;
|
|
110
|
+
const newMessages = params.messages.slice(params.prePromptMessageCount);
|
|
111
|
+
const turn = selectCaptureTurn(newMessages, this.cfg.captureMinChars) ??
|
|
112
|
+
selectCaptureTurn(params.messages, this.cfg.captureMinChars);
|
|
113
|
+
if (!turn)
|
|
114
|
+
return;
|
|
115
|
+
log.debug(`context-engine afterTurn: session=${params.sessionId} user=${turn.userText.length} assistant=${turn.assistantText.length}`);
|
|
116
|
+
try {
|
|
117
|
+
await this.client.indexConversation(turn.userText, turn.assistantText);
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
log.error("context-engine capture failed", err);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
async compact(params) {
|
|
124
|
+
return delegateCompactionToRuntime(params);
|
|
125
|
+
}
|
|
126
|
+
async prepareSubagentSpawn(params) {
|
|
127
|
+
const parentState = this.sessionState.get(params.parentSessionKey);
|
|
128
|
+
const noteTitle = buildSubagentNoteTitle(params.childSessionKey);
|
|
129
|
+
try {
|
|
130
|
+
const note = await this.client.writeNote(noteTitle, buildSubagentHandoffContent({
|
|
131
|
+
parentSessionKey: params.parentSessionKey,
|
|
132
|
+
childSessionKey: params.childSessionKey,
|
|
133
|
+
recallContext: parentState?.recallContext,
|
|
134
|
+
}), SUBAGENT_HANDOFF_FOLDER);
|
|
135
|
+
this.subagentState.set(params.childSessionKey, {
|
|
136
|
+
noteIdentifier: note.permalink,
|
|
137
|
+
noteTitle: note.title,
|
|
138
|
+
parentSessionKey: params.parentSessionKey,
|
|
139
|
+
});
|
|
140
|
+
return {
|
|
141
|
+
rollback: async () => {
|
|
142
|
+
const handoff = this.subagentState.get(params.childSessionKey);
|
|
143
|
+
this.subagentState.delete(params.childSessionKey);
|
|
144
|
+
if (!handoff)
|
|
145
|
+
return;
|
|
146
|
+
try {
|
|
147
|
+
await this.client.deleteNote(handoff.noteIdentifier);
|
|
148
|
+
}
|
|
149
|
+
catch (err) {
|
|
150
|
+
log.error("context-engine subagent rollback failed", err);
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
catch (err) {
|
|
156
|
+
log.error("context-engine prepareSubagentSpawn failed", err);
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
async onSubagentEnded(params) {
|
|
161
|
+
const handoff = this.subagentState.get(params.childSessionKey);
|
|
162
|
+
if (!handoff)
|
|
163
|
+
return;
|
|
164
|
+
this.subagentState.delete(params.childSessionKey);
|
|
165
|
+
try {
|
|
166
|
+
await this.client.editNote(handoff.noteIdentifier, "append", buildSubagentCompletionUpdate(params));
|
|
167
|
+
}
|
|
168
|
+
catch (err) {
|
|
169
|
+
log.error("context-engine onSubagentEnded failed", err);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
async dispose() {
|
|
173
|
+
this.sessionState.clear();
|
|
174
|
+
this.subagentState.clear();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { BmClient } from "../bm-client.ts";
|
|
2
|
+
import type { BasicMemoryConfig } from "../config.ts";
|
|
3
|
+
/**
|
|
4
|
+
* Extract text content from a message object.
|
|
5
|
+
*/
|
|
6
|
+
export declare function extractText(msg: Record<string, unknown>): string;
|
|
7
|
+
/**
|
|
8
|
+
* Find the last user+assistant turn from the messages array.
|
|
9
|
+
*/
|
|
10
|
+
export declare function getLastTurn(messages: unknown[]): {
|
|
11
|
+
userText: string;
|
|
12
|
+
assistantText: string;
|
|
13
|
+
};
|
|
14
|
+
export declare function selectCaptureTurn(messages: unknown[], minChars: number): {
|
|
15
|
+
userText: string;
|
|
16
|
+
assistantText: string;
|
|
17
|
+
} | null;
|
|
18
|
+
/**
|
|
19
|
+
* Build the post-turn capture handler for Mode B.
|
|
20
|
+
*
|
|
21
|
+
* After each agent turn, extracts the conversation content and indexes it
|
|
22
|
+
* into the Basic Memory knowledge graph as a conversation note.
|
|
23
|
+
*/
|
|
24
|
+
export declare function buildCaptureHandler(client: BmClient, cfg: BasicMemoryConfig): (event: Record<string, unknown>) => Promise<void>;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { log } from "../logger.js";
|
|
2
|
+
/**
|
|
3
|
+
* Extract text content from a message object.
|
|
4
|
+
*/
|
|
5
|
+
export function extractText(msg) {
|
|
6
|
+
const content = msg.content;
|
|
7
|
+
if (typeof content === "string")
|
|
8
|
+
return content;
|
|
9
|
+
if (Array.isArray(content)) {
|
|
10
|
+
const parts = [];
|
|
11
|
+
for (const block of content) {
|
|
12
|
+
if (!block || typeof block !== "object")
|
|
13
|
+
continue;
|
|
14
|
+
const b = block;
|
|
15
|
+
if (b.type === "text" && typeof b.text === "string") {
|
|
16
|
+
parts.push(b.text);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return parts.join("\n");
|
|
20
|
+
}
|
|
21
|
+
return "";
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Find the last user+assistant turn from the messages array.
|
|
25
|
+
*/
|
|
26
|
+
export function getLastTurn(messages) {
|
|
27
|
+
let lastUserIdx = -1;
|
|
28
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
29
|
+
const msg = messages[i];
|
|
30
|
+
if (msg &&
|
|
31
|
+
typeof msg === "object" &&
|
|
32
|
+
msg.role === "user") {
|
|
33
|
+
lastUserIdx = i;
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (lastUserIdx < 0)
|
|
38
|
+
return { userText: "", assistantText: "" };
|
|
39
|
+
let userText = "";
|
|
40
|
+
let assistantText = "";
|
|
41
|
+
for (let i = lastUserIdx; i < messages.length; i++) {
|
|
42
|
+
const msg = messages[i];
|
|
43
|
+
if (!msg?.role)
|
|
44
|
+
continue;
|
|
45
|
+
const text = extractText(msg);
|
|
46
|
+
if (msg.role === "user")
|
|
47
|
+
userText = text;
|
|
48
|
+
else if (msg.role === "assistant")
|
|
49
|
+
assistantText = text;
|
|
50
|
+
}
|
|
51
|
+
return { userText, assistantText };
|
|
52
|
+
}
|
|
53
|
+
export function selectCaptureTurn(messages, minChars) {
|
|
54
|
+
const turn = getLastTurn(messages);
|
|
55
|
+
if (!turn.userText && !turn.assistantText)
|
|
56
|
+
return null;
|
|
57
|
+
if (turn.userText.length < minChars && turn.assistantText.length < minChars) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
return turn;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Build the post-turn capture handler for Mode B.
|
|
64
|
+
*
|
|
65
|
+
* After each agent turn, extracts the conversation content and indexes it
|
|
66
|
+
* into the Basic Memory knowledge graph as a conversation note.
|
|
67
|
+
*/
|
|
68
|
+
export function buildCaptureHandler(client, cfg) {
|
|
69
|
+
const minChars = cfg.captureMinChars;
|
|
70
|
+
return async (event) => {
|
|
71
|
+
if (!event.success ||
|
|
72
|
+
!Array.isArray(event.messages) ||
|
|
73
|
+
event.messages.length === 0) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const turn = selectCaptureTurn(event.messages, minChars);
|
|
77
|
+
if (!turn)
|
|
78
|
+
return;
|
|
79
|
+
const { userText, assistantText } = turn;
|
|
80
|
+
log.debug(`capturing conversation: user=${userText.length} chars, assistant=${assistantText.length} chars`);
|
|
81
|
+
try {
|
|
82
|
+
await client.indexConversation(userText, assistantText);
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
log.error("capture failed", err);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { BmClient, RecentResult, SearchResult } from "../bm-client.ts";
|
|
2
|
+
import type { BasicMemoryConfig } from "../config.ts";
|
|
3
|
+
export interface RecallState {
|
|
4
|
+
tasks: SearchResult[];
|
|
5
|
+
recent: RecentResult[];
|
|
6
|
+
context: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Format recalled context from active tasks and recent activity.
|
|
10
|
+
* Returns empty string if nothing was found.
|
|
11
|
+
*/
|
|
12
|
+
export declare function formatRecallContext(tasks: SearchResult[], recent: RecentResult[], prompt: string): string;
|
|
13
|
+
export declare function loadRecallState(client: BmClient, cfg: BasicMemoryConfig): Promise<RecallState | null>;
|
|
14
|
+
/**
|
|
15
|
+
* Build the pre-turn recall handler.
|
|
16
|
+
*
|
|
17
|
+
* On agent_start, queries active tasks and recent activity from Basic Memory,
|
|
18
|
+
* then returns formatted context for injection into the conversation.
|
|
19
|
+
*/
|
|
20
|
+
export declare function buildRecallHandler(client: BmClient, cfg: BasicMemoryConfig): (_event: Record<string, unknown>) => Promise<{
|
|
21
|
+
context?: undefined;
|
|
22
|
+
} | {
|
|
23
|
+
context: string;
|
|
24
|
+
}>;
|