mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-12 01:31:08 +00:00
feat(memory-wiki): add shared memory search bridge
This commit is contained in:
@@ -296,6 +296,7 @@ Current bundled provider examples:
|
||||
| `plugin-sdk/memory-host-events` | Memory host event journal alias | Vendor-neutral alias for memory host event journal helpers |
|
||||
| `plugin-sdk/memory-host-files` | Memory host file/runtime alias | Vendor-neutral alias for memory host file/runtime helpers |
|
||||
| `plugin-sdk/memory-host-markdown` | Managed markdown helpers | Shared managed-markdown helpers for memory-adjacent plugins |
|
||||
| `plugin-sdk/memory-host-search` | Active memory search facade | Lazy active-memory search-manager runtime facade |
|
||||
| `plugin-sdk/memory-host-status` | Memory host status alias | Vendor-neutral alias for memory host status helpers |
|
||||
| `plugin-sdk/memory-lancedb` | Bundled memory-lancedb helpers | Memory-lancedb helper surface |
|
||||
| `plugin-sdk/testing` | Test utilities | Test helpers and mocks |
|
||||
|
||||
@@ -262,6 +262,7 @@ explicitly promotes one as public.
|
||||
| `plugin-sdk/memory-host-events` | Vendor-neutral alias for memory host event journal helpers |
|
||||
| `plugin-sdk/memory-host-files` | Vendor-neutral alias for memory host file/runtime helpers |
|
||||
| `plugin-sdk/memory-host-markdown` | Shared managed-markdown helpers for memory-adjacent plugins |
|
||||
| `plugin-sdk/memory-host-search` | Active memory runtime facade for search-manager access |
|
||||
| `plugin-sdk/memory-host-status` | Vendor-neutral alias for memory host status helpers |
|
||||
| `plugin-sdk/memory-lancedb` | Bundled memory-lancedb helper surface |
|
||||
</Accordion>
|
||||
|
||||
@@ -249,17 +249,18 @@ export async function runWikiSearch(params: {
|
||||
await syncMemoryWikiImportedSources({ config: params.config, appConfig: params.appConfig });
|
||||
const results = await searchMemoryWiki({
|
||||
config: params.config,
|
||||
appConfig: params.appConfig,
|
||||
query: params.query,
|
||||
maxResults: params.maxResults,
|
||||
});
|
||||
const summary = params.json
|
||||
? JSON.stringify(results, null, 2)
|
||||
: results.length === 0
|
||||
? "No wiki results."
|
||||
? "No wiki or memory results."
|
||||
: results
|
||||
.map(
|
||||
(result, index) =>
|
||||
`${index + 1}. ${result.title} (${result.kind})\nPath: ${result.path}\nSnippet: ${result.snippet}`,
|
||||
`${index + 1}. ${result.title} (${result.corpus}/${result.kind})\nPath: ${result.path}${typeof result.startLine === "number" && typeof result.endLine === "number" ? `\nLines: ${result.startLine}-${result.endLine}` : ""}\nSnippet: ${result.snippet}`,
|
||||
)
|
||||
.join("\n\n");
|
||||
writeOutput(summary, params.stdout);
|
||||
@@ -278,6 +279,7 @@ export async function runWikiGet(params: {
|
||||
await syncMemoryWikiImportedSources({ config: params.config, appConfig: params.appConfig });
|
||||
const result = await getMemoryWikiPage({
|
||||
config: params.config,
|
||||
appConfig: params.appConfig,
|
||||
lookup: params.lookup,
|
||||
fromLine: params.fromLine,
|
||||
lineCount: params.lineCount,
|
||||
@@ -540,7 +542,7 @@ export function registerWikiCli(
|
||||
|
||||
wiki
|
||||
.command("search")
|
||||
.description("Search wiki pages")
|
||||
.description("Search wiki pages and, when configured, the active memory corpus")
|
||||
.argument("<query>", "Search query")
|
||||
.option("--max-results <n>", "Maximum results", (value: string) => Number(value))
|
||||
.option("--json", "Print JSON")
|
||||
@@ -556,7 +558,7 @@ export function registerWikiCli(
|
||||
|
||||
wiki
|
||||
.command("get")
|
||||
.description("Read a wiki page by id or relative path")
|
||||
.description("Read a wiki page by id or relative path, with optional active-memory fallback")
|
||||
.argument("<lookup>", "Relative path or page id")
|
||||
.option("--from <n>", "Start line", (value: string) => Number(value))
|
||||
.option("--lines <n>", "Number of lines", (value: string) => Number(value))
|
||||
|
||||
@@ -207,6 +207,7 @@ export function registerMemoryWikiGatewayMethods(params: {
|
||||
true,
|
||||
await searchMemoryWiki({
|
||||
config,
|
||||
appConfig,
|
||||
query,
|
||||
maxResults,
|
||||
}),
|
||||
@@ -249,6 +250,7 @@ export function registerMemoryWikiGatewayMethods(params: {
|
||||
true,
|
||||
await getMemoryWikiPage({
|
||||
config,
|
||||
appConfig,
|
||||
lookup,
|
||||
fromLine,
|
||||
lineCount,
|
||||
|
||||
@@ -1,20 +1,69 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { OpenClawConfig } from "../api.js";
|
||||
import { resolveMemoryWikiConfig } from "./config.js";
|
||||
import { renderWikiMarkdown } from "./markdown.js";
|
||||
import { getMemoryWikiPage, searchMemoryWiki } from "./query.js";
|
||||
import { initializeMemoryWikiVault } from "./vault.js";
|
||||
|
||||
const { getActiveMemorySearchManagerMock } = vi.hoisted(() => ({
|
||||
getActiveMemorySearchManagerMock: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/memory-host-search", () => ({
|
||||
getActiveMemorySearchManager: getActiveMemorySearchManagerMock,
|
||||
}));
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(tempDirs.splice(0).map((dir) => fs.rm(dir, { recursive: true, force: true })));
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
getActiveMemorySearchManagerMock.mockReset();
|
||||
getActiveMemorySearchManagerMock.mockResolvedValue({ manager: null, error: "unavailable" });
|
||||
});
|
||||
|
||||
function createAppConfig(): OpenClawConfig {
|
||||
return {
|
||||
agents: {
|
||||
list: [{ id: "main", default: true }],
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
}
|
||||
|
||||
function createMemoryManager(overrides?: {
|
||||
searchResults?: Array<{
|
||||
path: string;
|
||||
startLine: number;
|
||||
endLine: number;
|
||||
score: number;
|
||||
snippet: string;
|
||||
source: "memory" | "sessions";
|
||||
citation?: string;
|
||||
}>;
|
||||
readResult?: { text: string; path: string };
|
||||
}) {
|
||||
return {
|
||||
search: vi.fn().mockResolvedValue(overrides?.searchResults ?? []),
|
||||
readFile: vi.fn().mockImplementation(async () => {
|
||||
if (!overrides?.readResult) {
|
||||
throw new Error("missing");
|
||||
}
|
||||
return overrides.readResult;
|
||||
}),
|
||||
status: vi.fn().mockReturnValue({ backend: "builtin", provider: "builtin" }),
|
||||
probeEmbeddingAvailability: vi.fn().mockResolvedValue({ ok: true }),
|
||||
probeVectorAvailability: vi.fn().mockResolvedValue(false),
|
||||
close: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
}
|
||||
|
||||
describe("searchMemoryWiki", () => {
|
||||
it("finds pages by title and body", async () => {
|
||||
it("finds wiki pages by title and body", async () => {
|
||||
const rootDir = await fs.mkdtemp(path.join(os.tmpdir(), "memory-wiki-query-"));
|
||||
tempDirs.push(rootDir);
|
||||
const config = resolveMemoryWikiConfig(
|
||||
@@ -34,12 +83,105 @@ describe("searchMemoryWiki", () => {
|
||||
const results = await searchMemoryWiki({ config, query: "alpha" });
|
||||
|
||||
expect(results).toHaveLength(1);
|
||||
expect(results[0]?.corpus).toBe("wiki");
|
||||
expect(results[0]?.path).toBe("sources/alpha.md");
|
||||
expect(getActiveMemorySearchManagerMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("includes active memory results when shared search and all corpora are enabled", async () => {
|
||||
const rootDir = await fs.mkdtemp(path.join(os.tmpdir(), "memory-wiki-query-"));
|
||||
tempDirs.push(rootDir);
|
||||
const config = resolveMemoryWikiConfig(
|
||||
{
|
||||
vault: { path: rootDir },
|
||||
search: { backend: "shared", corpus: "all" },
|
||||
},
|
||||
{ homedir: "/Users/tester" },
|
||||
);
|
||||
await initializeMemoryWikiVault(config);
|
||||
await fs.writeFile(
|
||||
path.join(rootDir, "sources", "alpha.md"),
|
||||
renderWikiMarkdown({
|
||||
frontmatter: { pageType: "source", id: "source.alpha", title: "Alpha Source" },
|
||||
body: "# Alpha Source\n\nalpha body text\n",
|
||||
}),
|
||||
"utf8",
|
||||
);
|
||||
const manager = createMemoryManager({
|
||||
searchResults: [
|
||||
{
|
||||
path: "MEMORY.md",
|
||||
startLine: 4,
|
||||
endLine: 8,
|
||||
score: 42,
|
||||
snippet: "alpha durable memory",
|
||||
source: "memory",
|
||||
citation: "MEMORY.md#L4-L8",
|
||||
},
|
||||
],
|
||||
});
|
||||
getActiveMemorySearchManagerMock.mockResolvedValue({ manager });
|
||||
|
||||
const results = await searchMemoryWiki({
|
||||
config,
|
||||
appConfig: createAppConfig(),
|
||||
query: "alpha",
|
||||
maxResults: 5,
|
||||
});
|
||||
|
||||
expect(results).toHaveLength(2);
|
||||
expect(results.some((result) => result.corpus === "wiki")).toBe(true);
|
||||
expect(results.some((result) => result.corpus === "memory")).toBe(true);
|
||||
expect(manager.search).toHaveBeenCalledWith("alpha", { maxResults: 5 });
|
||||
});
|
||||
|
||||
it("keeps memory search disabled when the backend is local", async () => {
|
||||
const rootDir = await fs.mkdtemp(path.join(os.tmpdir(), "memory-wiki-query-"));
|
||||
tempDirs.push(rootDir);
|
||||
const config = resolveMemoryWikiConfig(
|
||||
{
|
||||
vault: { path: rootDir },
|
||||
search: { backend: "local", corpus: "all" },
|
||||
},
|
||||
{ homedir: "/Users/tester" },
|
||||
);
|
||||
await initializeMemoryWikiVault(config);
|
||||
await fs.writeFile(
|
||||
path.join(rootDir, "sources", "alpha.md"),
|
||||
renderWikiMarkdown({
|
||||
frontmatter: { pageType: "source", id: "source.alpha", title: "Alpha Source" },
|
||||
body: "# Alpha Source\n\nalpha only wiki\n",
|
||||
}),
|
||||
"utf8",
|
||||
);
|
||||
const manager = createMemoryManager({
|
||||
searchResults: [
|
||||
{
|
||||
path: "MEMORY.md",
|
||||
startLine: 1,
|
||||
endLine: 2,
|
||||
score: 50,
|
||||
snippet: "alpha memory",
|
||||
source: "memory",
|
||||
},
|
||||
],
|
||||
});
|
||||
getActiveMemorySearchManagerMock.mockResolvedValue({ manager });
|
||||
|
||||
const results = await searchMemoryWiki({
|
||||
config,
|
||||
appConfig: createAppConfig(),
|
||||
query: "alpha",
|
||||
});
|
||||
|
||||
expect(results).toHaveLength(1);
|
||||
expect(results[0]?.corpus).toBe("wiki");
|
||||
expect(manager.search).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getMemoryWikiPage", () => {
|
||||
it("reads pages by relative path and slices line ranges", async () => {
|
||||
it("reads wiki pages by relative path and slices line ranges", async () => {
|
||||
const rootDir = await fs.mkdtemp(path.join(os.tmpdir(), "memory-wiki-query-"));
|
||||
tempDirs.push(rootDir);
|
||||
const config = resolveMemoryWikiConfig(
|
||||
@@ -63,9 +205,53 @@ describe("getMemoryWikiPage", () => {
|
||||
lineCount: 2,
|
||||
});
|
||||
|
||||
expect(result?.corpus).toBe("wiki");
|
||||
expect(result?.path).toBe("sources/alpha.md");
|
||||
expect(result?.content).toContain("line one");
|
||||
expect(result?.content).toContain("line two");
|
||||
expect(result?.content).not.toContain("line three");
|
||||
});
|
||||
|
||||
it("falls back to active memory reads when memory corpus is selected", async () => {
|
||||
const rootDir = await fs.mkdtemp(path.join(os.tmpdir(), "memory-wiki-query-"));
|
||||
tempDirs.push(rootDir);
|
||||
const config = resolveMemoryWikiConfig(
|
||||
{
|
||||
vault: { path: rootDir },
|
||||
search: { backend: "shared", corpus: "memory" },
|
||||
},
|
||||
{ homedir: "/Users/tester" },
|
||||
);
|
||||
await initializeMemoryWikiVault(config);
|
||||
const manager = createMemoryManager({
|
||||
readResult: {
|
||||
path: "MEMORY.md",
|
||||
text: "durable alpha memory\nline two",
|
||||
},
|
||||
});
|
||||
getActiveMemorySearchManagerMock.mockResolvedValue({ manager });
|
||||
|
||||
const result = await getMemoryWikiPage({
|
||||
config,
|
||||
appConfig: createAppConfig(),
|
||||
lookup: "MEMORY.md",
|
||||
fromLine: 2,
|
||||
lineCount: 2,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
corpus: "memory",
|
||||
path: "MEMORY.md",
|
||||
title: "MEMORY",
|
||||
kind: "memory",
|
||||
content: "durable alpha memory\nline two",
|
||||
fromLine: 2,
|
||||
lineCount: 2,
|
||||
});
|
||||
expect(manager.readFile).toHaveBeenCalledWith({
|
||||
relPath: "MEMORY.md",
|
||||
from: 2,
|
||||
lines: 2,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { resolveDefaultAgentId } from "openclaw/plugin-sdk/config-runtime";
|
||||
import type { MemorySearchResult } from "openclaw/plugin-sdk/memory-host-files";
|
||||
import { getActiveMemorySearchManager } from "openclaw/plugin-sdk/memory-host-search";
|
||||
import type { OpenClawConfig } from "../api.js";
|
||||
import type { ResolvedMemoryWikiConfig } from "./config.js";
|
||||
import { parseWikiMarkdown, toWikiPageSummary, type WikiPageSummary } from "./markdown.js";
|
||||
import { initializeMemoryWikiVault } from "./vault.js";
|
||||
@@ -7,18 +11,24 @@ import { initializeMemoryWikiVault } from "./vault.js";
|
||||
const QUERY_DIRS = ["entities", "concepts", "sources", "syntheses", "reports"] as const;
|
||||
|
||||
export type WikiSearchResult = {
|
||||
corpus: "wiki" | "memory";
|
||||
path: string;
|
||||
title: string;
|
||||
kind: WikiPageSummary["kind"];
|
||||
kind: WikiPageSummary["kind"] | "memory";
|
||||
score: number;
|
||||
snippet: string;
|
||||
id?: string;
|
||||
startLine?: number;
|
||||
endLine?: number;
|
||||
citation?: string;
|
||||
memorySource?: MemorySearchResult["source"];
|
||||
};
|
||||
|
||||
export type WikiGetResult = {
|
||||
corpus: "wiki" | "memory";
|
||||
path: string;
|
||||
title: string;
|
||||
kind: WikiPageSummary["kind"];
|
||||
kind: WikiPageSummary["kind"] | "memory";
|
||||
content: string;
|
||||
fromLine: number;
|
||||
lineCount: number;
|
||||
@@ -113,6 +123,74 @@ function normalizeLookupKey(value: string): string {
|
||||
return normalized.endsWith(".md") ? normalized : normalized.replace(/\/+$/, "");
|
||||
}
|
||||
|
||||
function buildLookupCandidates(lookup: string): string[] {
|
||||
const normalized = normalizeLookupKey(lookup);
|
||||
const withExtension = normalized.endsWith(".md") ? normalized : `${normalized}.md`;
|
||||
return [...new Set([normalized, withExtension])];
|
||||
}
|
||||
|
||||
function shouldSearchWiki(config: ResolvedMemoryWikiConfig): boolean {
|
||||
return config.search.corpus === "wiki" || config.search.corpus === "all";
|
||||
}
|
||||
|
||||
function shouldSearchSharedMemory(
|
||||
config: ResolvedMemoryWikiConfig,
|
||||
appConfig?: OpenClawConfig,
|
||||
): boolean {
|
||||
return (
|
||||
config.search.backend === "shared" &&
|
||||
appConfig !== undefined &&
|
||||
(config.search.corpus === "memory" || config.search.corpus === "all")
|
||||
);
|
||||
}
|
||||
|
||||
async function resolveActiveMemoryManager(appConfig?: OpenClawConfig) {
|
||||
if (!appConfig) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const { manager } = await getActiveMemorySearchManager({
|
||||
cfg: appConfig,
|
||||
agentId: resolveDefaultAgentId(appConfig),
|
||||
});
|
||||
return manager;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function buildMemorySearchTitle(resultPath: string): string {
|
||||
const basename = path.basename(resultPath, path.extname(resultPath));
|
||||
return basename.length > 0 ? basename : resultPath;
|
||||
}
|
||||
|
||||
function toWikiSearchResult(page: QueryableWikiPage, query: string): WikiSearchResult {
|
||||
return {
|
||||
corpus: "wiki",
|
||||
path: page.relativePath,
|
||||
title: page.title,
|
||||
kind: page.kind,
|
||||
score: scorePage(page, query),
|
||||
snippet: buildSnippet(page.raw, query),
|
||||
...(page.id ? { id: page.id } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function toMemoryWikiSearchResult(result: MemorySearchResult): WikiSearchResult {
|
||||
return {
|
||||
corpus: "memory",
|
||||
path: result.path,
|
||||
title: buildMemorySearchTitle(result.path),
|
||||
kind: "memory",
|
||||
score: result.score,
|
||||
snippet: result.snippet,
|
||||
startLine: result.startLine,
|
||||
endLine: result.endLine,
|
||||
memorySource: result.source,
|
||||
...(result.citation ? { citation: result.citation } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveQueryableWikiPageByLookup(
|
||||
pages: QueryableWikiPage[],
|
||||
lookup: string,
|
||||
@@ -131,22 +209,29 @@ export function resolveQueryableWikiPageByLookup(
|
||||
|
||||
export async function searchMemoryWiki(params: {
|
||||
config: ResolvedMemoryWikiConfig;
|
||||
appConfig?: OpenClawConfig;
|
||||
query: string;
|
||||
maxResults?: number;
|
||||
}): Promise<WikiSearchResult[]> {
|
||||
await initializeMemoryWikiVault(params.config);
|
||||
const pages = await readQueryableWikiPages(params.config.vault.path);
|
||||
const maxResults = Math.max(1, params.maxResults ?? 10);
|
||||
return pages
|
||||
.map((page) => ({
|
||||
path: page.relativePath,
|
||||
title: page.title,
|
||||
kind: page.kind,
|
||||
score: scorePage(page, params.query),
|
||||
snippet: buildSnippet(page.raw, params.query),
|
||||
...(page.id ? { id: page.id } : {}),
|
||||
}))
|
||||
.filter((page) => page.score > 0)
|
||||
|
||||
const wikiResults = shouldSearchWiki(params.config)
|
||||
? (await readQueryableWikiPages(params.config.vault.path))
|
||||
.map((page) => toWikiSearchResult(page, params.query))
|
||||
.filter((page) => page.score > 0)
|
||||
: [];
|
||||
|
||||
const sharedMemoryManager = shouldSearchSharedMemory(params.config, params.appConfig)
|
||||
? await resolveActiveMemoryManager(params.appConfig)
|
||||
: null;
|
||||
const memoryResults = sharedMemoryManager
|
||||
? (await sharedMemoryManager.search(params.query, { maxResults })).map((result) =>
|
||||
toMemoryWikiSearchResult(result),
|
||||
)
|
||||
: [];
|
||||
|
||||
return [...wikiResults, ...memoryResults]
|
||||
.toSorted((left, right) => {
|
||||
if (left.score !== right.score) {
|
||||
return right.score - left.score;
|
||||
@@ -158,30 +243,65 @@ export async function searchMemoryWiki(params: {
|
||||
|
||||
export async function getMemoryWikiPage(params: {
|
||||
config: ResolvedMemoryWikiConfig;
|
||||
appConfig?: OpenClawConfig;
|
||||
lookup: string;
|
||||
fromLine?: number;
|
||||
lineCount?: number;
|
||||
}): Promise<WikiGetResult | null> {
|
||||
await initializeMemoryWikiVault(params.config);
|
||||
const pages = await readQueryableWikiPages(params.config.vault.path);
|
||||
const page = resolveQueryableWikiPageByLookup(pages, params.lookup);
|
||||
if (!page) {
|
||||
const fromLine = Math.max(1, params.fromLine ?? 1);
|
||||
const lineCount = Math.max(1, params.lineCount ?? 200);
|
||||
|
||||
if (shouldSearchWiki(params.config)) {
|
||||
const pages = await readQueryableWikiPages(params.config.vault.path);
|
||||
const page = resolveQueryableWikiPageByLookup(pages, params.lookup);
|
||||
if (page) {
|
||||
const parsed = parseWikiMarkdown(page.raw);
|
||||
const lines = parsed.body.split(/\r?\n/);
|
||||
const slice = lines.slice(fromLine - 1, fromLine - 1 + lineCount).join("\n");
|
||||
|
||||
return {
|
||||
corpus: "wiki",
|
||||
path: page.relativePath,
|
||||
title: page.title,
|
||||
kind: page.kind,
|
||||
content: slice,
|
||||
fromLine,
|
||||
lineCount,
|
||||
...(page.id ? { id: page.id } : {}),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (!shouldSearchSharedMemory(params.config, params.appConfig)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parsed = parseWikiMarkdown(page.raw);
|
||||
const lines = parsed.body.split(/\r?\n/);
|
||||
const fromLine = Math.max(1, params.fromLine ?? 1);
|
||||
const lineCount = Math.max(1, params.lineCount ?? 200);
|
||||
const slice = lines.slice(fromLine - 1, fromLine - 1 + lineCount).join("\n");
|
||||
const manager = await resolveActiveMemoryManager(params.appConfig);
|
||||
if (!manager) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
path: page.relativePath,
|
||||
title: page.title,
|
||||
kind: page.kind,
|
||||
content: slice,
|
||||
fromLine,
|
||||
lineCount,
|
||||
...(page.id ? { id: page.id } : {}),
|
||||
};
|
||||
for (const relPath of buildLookupCandidates(params.lookup)) {
|
||||
try {
|
||||
const result = await manager.readFile({
|
||||
relPath,
|
||||
from: fromLine,
|
||||
lines: lineCount,
|
||||
});
|
||||
return {
|
||||
corpus: "memory",
|
||||
path: result.path,
|
||||
title: buildMemorySearchTitle(result.path),
|
||||
kind: "memory",
|
||||
content: result.text,
|
||||
fromLine,
|
||||
lineCount,
|
||||
};
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -74,23 +74,25 @@ export function createWikiSearchTool(
|
||||
return {
|
||||
name: "wiki_search",
|
||||
label: "Wiki Search",
|
||||
description: "Search wiki pages by title, path, id, or body text.",
|
||||
description:
|
||||
"Search wiki pages and, when shared search is enabled, the active memory corpus by title, path, id, or body text.",
|
||||
parameters: WikiSearchSchema,
|
||||
execute: async (_toolCallId, rawParams) => {
|
||||
const params = rawParams as { query: string; maxResults?: number };
|
||||
await syncImportedSourcesIfNeeded(config, appConfig);
|
||||
const results = await searchMemoryWiki({
|
||||
config,
|
||||
appConfig,
|
||||
query: params.query,
|
||||
maxResults: params.maxResults,
|
||||
});
|
||||
const text =
|
||||
results.length === 0
|
||||
? "No wiki results."
|
||||
? "No wiki or memory results."
|
||||
: results
|
||||
.map(
|
||||
(result, index) =>
|
||||
`${index + 1}. ${result.title} (${result.kind})\nPath: ${result.path}\nSnippet: ${result.snippet}`,
|
||||
`${index + 1}. ${result.title} (${result.corpus}/${result.kind})\nPath: ${result.path}${typeof result.startLine === "number" && typeof result.endLine === "number" ? `\nLines: ${result.startLine}-${result.endLine}` : ""}\nSnippet: ${result.snippet}`,
|
||||
)
|
||||
.join("\n\n");
|
||||
return {
|
||||
@@ -176,13 +178,15 @@ export function createWikiGetTool(
|
||||
return {
|
||||
name: "wiki_get",
|
||||
label: "Wiki Get",
|
||||
description: "Read a wiki page by id or relative path.",
|
||||
description:
|
||||
"Read a wiki page by id or relative path, or fall back to the active memory corpus when shared search is enabled.",
|
||||
parameters: WikiGetSchema,
|
||||
execute: async (_toolCallId, rawParams) => {
|
||||
const params = rawParams as { lookup: string; fromLine?: number; lineCount?: number };
|
||||
await syncImportedSourcesIfNeeded(config, appConfig);
|
||||
const result = await getMemoryWikiPage({
|
||||
config,
|
||||
appConfig,
|
||||
lookup: params.lookup,
|
||||
fromLine: params.fromLine,
|
||||
lineCount: params.lineCount,
|
||||
|
||||
@@ -715,6 +715,10 @@
|
||||
"types": "./dist/plugin-sdk/memory-host-markdown.d.ts",
|
||||
"default": "./dist/plugin-sdk/memory-host-markdown.js"
|
||||
},
|
||||
"./plugin-sdk/memory-host-search": {
|
||||
"types": "./dist/plugin-sdk/memory-host-search.d.ts",
|
||||
"default": "./dist/plugin-sdk/memory-host-search.js"
|
||||
},
|
||||
"./plugin-sdk/memory-host-status": {
|
||||
"types": "./dist/plugin-sdk/memory-host-status.d.ts",
|
||||
"default": "./dist/plugin-sdk/memory-host-status.js"
|
||||
|
||||
@@ -168,6 +168,7 @@
|
||||
"memory-host-events",
|
||||
"memory-host-files",
|
||||
"memory-host-markdown",
|
||||
"memory-host-search",
|
||||
"memory-host-status",
|
||||
"memory-lancedb",
|
||||
"msteams",
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
export { listMemoryFiles, normalizeExtraMemoryPaths } from "./host/internal.js";
|
||||
export { readAgentMemoryFile } from "./host/read-file.js";
|
||||
export { resolveMemoryBackendConfig } from "./host/backend-config.js";
|
||||
export type { MemorySearchResult } from "./host/types.js";
|
||||
export type { MemorySearchManager, MemorySearchResult } from "./host/types.js";
|
||||
|
||||
5
src/plugin-sdk/memory-host-search.runtime.ts
Normal file
5
src/plugin-sdk/memory-host-search.runtime.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export {
|
||||
closeActiveMemorySearchManagers,
|
||||
getActiveMemorySearchManager,
|
||||
resolveActiveMemoryBackendConfig,
|
||||
} from "../plugins/memory-runtime.js";
|
||||
42
src/plugin-sdk/memory-host-search.test.ts
Normal file
42
src/plugin-sdk/memory-host-search.test.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import {
|
||||
closeActiveMemorySearchManagers,
|
||||
getActiveMemorySearchManager,
|
||||
} from "./memory-host-search.js";
|
||||
|
||||
const { closeActiveMemorySearchManagersMock, getActiveMemorySearchManagerMock } = vi.hoisted(
|
||||
() => ({
|
||||
closeActiveMemorySearchManagersMock: vi.fn(),
|
||||
getActiveMemorySearchManagerMock: vi.fn(),
|
||||
}),
|
||||
);
|
||||
|
||||
vi.mock("./memory-host-search.runtime.js", () => ({
|
||||
closeActiveMemorySearchManagers: closeActiveMemorySearchManagersMock,
|
||||
getActiveMemorySearchManager: getActiveMemorySearchManagerMock,
|
||||
}));
|
||||
|
||||
describe("memory-host-search facade", () => {
|
||||
beforeEach(() => {
|
||||
closeActiveMemorySearchManagersMock.mockReset();
|
||||
getActiveMemorySearchManagerMock.mockReset();
|
||||
});
|
||||
|
||||
it("delegates active manager lookup to the lazy runtime module", async () => {
|
||||
const cfg = { agents: { list: [{ id: "main", default: true }] } } as OpenClawConfig;
|
||||
const expected = { manager: null, error: "unavailable" };
|
||||
getActiveMemorySearchManagerMock.mockResolvedValue(expected);
|
||||
|
||||
await expect(getActiveMemorySearchManager({ cfg, agentId: "main" })).resolves.toEqual(expected);
|
||||
expect(getActiveMemorySearchManagerMock).toHaveBeenCalledWith({ cfg, agentId: "main" });
|
||||
});
|
||||
|
||||
it("delegates runtime cleanup to the lazy runtime module", async () => {
|
||||
const cfg = { agents: { list: [{ id: "main", default: true }] } } as OpenClawConfig;
|
||||
|
||||
await closeActiveMemorySearchManagers(cfg);
|
||||
|
||||
expect(closeActiveMemorySearchManagersMock).toHaveBeenCalledWith(cfg);
|
||||
});
|
||||
});
|
||||
29
src/plugin-sdk/memory-host-search.ts
Normal file
29
src/plugin-sdk/memory-host-search.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import type { RegisteredMemorySearchManager } from "../plugins/memory-state.js";
|
||||
|
||||
type ActiveMemorySearchPurpose = "default" | "status";
|
||||
|
||||
export type ActiveMemorySearchManagerResult = {
|
||||
manager: RegisteredMemorySearchManager | null;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
type MemoryHostSearchRuntimeModule = typeof import("./memory-host-search.runtime.js");
|
||||
|
||||
async function loadMemoryHostSearchRuntime(): Promise<MemoryHostSearchRuntimeModule> {
|
||||
return await import("./memory-host-search.runtime.js");
|
||||
}
|
||||
|
||||
export async function getActiveMemorySearchManager(params: {
|
||||
cfg: OpenClawConfig;
|
||||
agentId: string;
|
||||
purpose?: ActiveMemorySearchPurpose;
|
||||
}): Promise<ActiveMemorySearchManagerResult> {
|
||||
const runtime = await loadMemoryHostSearchRuntime();
|
||||
return await runtime.getActiveMemorySearchManager(params);
|
||||
}
|
||||
|
||||
export async function closeActiveMemorySearchManagers(cfg?: OpenClawConfig): Promise<void> {
|
||||
const runtime = await loadMemoryHostSearchRuntime();
|
||||
await runtime.closeActiveMemorySearchManagers(cfg);
|
||||
}
|
||||
@@ -1,10 +1,6 @@
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import type { MemoryCitationsMode } from "../config/types.memory.js";
|
||||
import type {
|
||||
MemoryEmbeddingProbeResult,
|
||||
MemoryProviderStatus,
|
||||
MemorySyncProgressUpdate,
|
||||
} from "../memory-host-sdk/engine-storage.js";
|
||||
import type { MemorySearchManager } from "../memory-host-sdk/runtime-files.js";
|
||||
|
||||
export type MemoryPromptSectionBuilder = (params: {
|
||||
availableTools: Set<string>;
|
||||
@@ -25,18 +21,7 @@ export type MemoryFlushPlanResolver = (params: {
|
||||
nowMs?: number;
|
||||
}) => MemoryFlushPlan | null;
|
||||
|
||||
export type RegisteredMemorySearchManager = {
|
||||
status(): MemoryProviderStatus;
|
||||
probeEmbeddingAvailability(): Promise<MemoryEmbeddingProbeResult>;
|
||||
probeVectorAvailability(): Promise<boolean>;
|
||||
sync?(params?: {
|
||||
reason?: string;
|
||||
force?: boolean;
|
||||
sessionFiles?: string[];
|
||||
progress?: (update: MemorySyncProgressUpdate) => void;
|
||||
}): Promise<void>;
|
||||
close?(): Promise<void>;
|
||||
};
|
||||
export type RegisteredMemorySearchManager = MemorySearchManager;
|
||||
|
||||
export type MemoryRuntimeQmdConfig = {
|
||||
command?: string;
|
||||
|
||||
Reference in New Issue
Block a user