UNPKG

@tencentdb-agent-memory/memory-tencentdb

Version:

Four-layer local memory system plugin for OpenClaw — auto-captures, structures, and profiles conversational knowledge using local LLM + SQLite vector search (L0→L1→L2→L3 pipeline)

154 lines (153 loc) 5.48 kB
import { jsonSchema, tool } from "ai"; //#region src/adapters/standalone/storage-tools.ts /** * Storage-backed LLM tool definitions — drop-in replacement for the local-FS * sandboxed tools in `llm-runner.ts`. * * Used in service mode (COS) so that L2/L3 LLM agents read/write files via * StorageAdapter instead of the local filesystem. * * Tool names and schemas are **identical** to `createSandboxedTools`, so * LLM prompts work unchanged. */ const TAG = "[memory-tdai] [storage-tools]"; /** * Resolve a relative path within a storage prefix boundary (sandbox). * * Returns the full storage key (prefix + normalized path), or null if the * path escapes the prefix boundary (e.g. "../" traversal). */ function resolveStorageKey(prefix, relativePath) { const normalized = relativePath.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+/g, "/"); if (normalized.startsWith("/") || normalized.startsWith("..")) return null; const key = `${prefix}${normalized}`; if (normalized.split("/").includes("..")) return null; return key; } /** * Create storage-backed read/write/edit tools. * * @param storage - StorageAdapter instance (COS or local backend) * @param prefix - Key prefix acting as sandbox root (e.g. "scene_blocks/") * @param logger - Optional logger for diagnostics */ function createStorageTools(storage, prefix, logger) { return { read: tool({ description: "Read the contents of a file at the given relative path.", inputSchema: jsonSchema({ type: "object", properties: { path: { type: "string", description: "Relative file path to read." } }, required: ["path"] }), execute: (async (args) => { const key = resolveStorageKey(prefix, args.path); if (!key) return JSON.stringify({ error: `Path "${args.path}" escapes workspace boundary.` }); try { const content = await storage.readFile(key); if (content === null) { logger?.debug?.(`${TAG} read: "${args.path}" → not found`); return JSON.stringify({ error: `File not found: ${args.path}` }); } logger?.debug?.(`${TAG} read: "${args.path}" → ${content.length} chars`); return content; } catch (err) { const msg = err instanceof Error ? err.message : String(err); logger?.warn(`${TAG} read failed (key=${key}): ${msg}`); return JSON.stringify({ error: msg }); } }) }), write: tool({ description: "Write content to a file at the given relative path. Creates or overwrites.", inputSchema: jsonSchema({ type: "object", properties: { path: { type: "string", description: "Relative file path to write." }, content: { type: "string", description: "Content to write." } }, required: ["path", "content"] }), execute: (async (args) => { const key = resolveStorageKey(prefix, args.path); if (!key) return JSON.stringify({ error: `Path "${args.path}" escapes workspace boundary.` }); try { await storage.writeFile(key, args.content); logger?.debug?.(`${TAG} write: "${args.path}" → ${args.content.length} chars`); return JSON.stringify({ success: true }); } catch (err) { const msg = err instanceof Error ? err.message : String(err); logger?.warn(`${TAG} write failed (key=${key}): ${msg}`); return JSON.stringify({ error: msg }); } }) }), edit: tool({ description: "Apply one or more text replacements to a file. Each edit replaces an exact substring.", inputSchema: jsonSchema({ type: "object", properties: { path: { type: "string", description: "Relative file path." }, edits: { type: "array", description: "Array of replacements to apply sequentially.", items: { type: "object", properties: { oldText: { type: "string", description: "Exact string to find." }, newText: { type: "string", description: "Replacement string." } }, required: ["oldText", "newText"] } } }, required: ["path", "edits"] }), execute: (async (args) => { const key = resolveStorageKey(prefix, args.path); if (!key) return JSON.stringify({ error: `Path "${args.path}" escapes workspace boundary.` }); if (!args.edits || args.edits.length === 0) return JSON.stringify({ error: "edits array cannot be empty." }); try { const existing = await storage.readFile(key); if (existing === null) { logger?.debug?.(`${TAG} edit: "${args.path}" → not found`); return JSON.stringify({ error: `File not found: ${args.path}` }); } let content = existing; for (const edit of args.edits) { if (!edit.oldText) return JSON.stringify({ error: "oldText cannot be empty." }); if (!content.includes(edit.oldText)) return JSON.stringify({ error: `oldText not found in file "${args.path}": ${edit.oldText.slice(0, 80)}` }); content = content.replace(edit.oldText, edit.newText); } await storage.writeFile(key, content); logger?.debug?.(`${TAG} edit: "${args.path}" → ${args.edits.length} replacement(s), ${content.length} chars`); return JSON.stringify({ success: true }); } catch (err) { const msg = err instanceof Error ? err.message : String(err); logger?.warn(`${TAG} edit failed (key=${key}): ${msg}`); return JSON.stringify({ error: msg }); } }) }) }; } //#endregion export { createStorageTools };