mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-12 09:41:11 +00:00
feat(memory-wiki): add wiki apply mutation tool
This commit is contained in:
@@ -24,10 +24,11 @@ describe("memory-wiki plugin", () => {
|
||||
|
||||
await plugin.register(api);
|
||||
|
||||
expect(registerTool).toHaveBeenCalledTimes(4);
|
||||
expect(registerTool).toHaveBeenCalledTimes(5);
|
||||
expect(registerTool.mock.calls.map((call) => call[1]?.name)).toEqual([
|
||||
"wiki_status",
|
||||
"wiki_lint",
|
||||
"wiki_apply",
|
||||
"wiki_search",
|
||||
"wiki_get",
|
||||
]);
|
||||
|
||||
@@ -2,6 +2,7 @@ import { definePluginEntry } from "./api.js";
|
||||
import { registerWikiCli } from "./src/cli.js";
|
||||
import { memoryWikiConfigSchema, resolveMemoryWikiConfig } from "./src/config.js";
|
||||
import {
|
||||
createWikiApplyTool,
|
||||
createWikiGetTool,
|
||||
createWikiLintTool,
|
||||
createWikiSearchTool,
|
||||
@@ -18,6 +19,7 @@ export default definePluginEntry({
|
||||
|
||||
api.registerTool(createWikiStatusTool(config, api.config), { name: "wiki_status" });
|
||||
api.registerTool(createWikiLintTool(config, api.config), { name: "wiki_lint" });
|
||||
api.registerTool(createWikiApplyTool(config, api.config), { name: "wiki_apply" });
|
||||
api.registerTool(createWikiSearchTool(config, api.config), { name: "wiki_search" });
|
||||
api.registerTool(createWikiGetTool(config, api.config), { name: "wiki_get" });
|
||||
api.registerCli(
|
||||
|
||||
@@ -7,6 +7,7 @@ Use this skill when working inside a memory-wiki vault.
|
||||
|
||||
- Prefer `wiki_status` first when you need to understand the vault mode, path, or Obsidian CLI availability.
|
||||
- Use `wiki_search` to discover candidate pages, then `wiki_get` to inspect the exact page before editing or citing it.
|
||||
- Use `wiki_apply` for narrow synthesis filing and metadata updates when a tool-level mutation is enough.
|
||||
- Run `wiki_lint` after meaningful wiki updates so contradictions, provenance gaps, and open questions get surfaced before you trust the vault.
|
||||
- Use `openclaw wiki ingest`, `openclaw wiki compile`, and `openclaw wiki lint` as the default maintenance loop.
|
||||
- In `bridge` mode, run `openclaw wiki bridge import` before relying on search results if you need the latest public memory-core artifacts pulled in.
|
||||
|
||||
133
extensions/memory-wiki/src/apply.test.ts
Normal file
133
extensions/memory-wiki/src/apply.test.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { applyMemoryWikiMutation } from "./apply.js";
|
||||
import { resolveMemoryWikiConfig } from "./config.js";
|
||||
import { parseWikiMarkdown, renderWikiMarkdown } from "./markdown.js";
|
||||
import { initializeMemoryWikiVault } from "./vault.js";
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(tempDirs.splice(0).map((dir) => fs.rm(dir, { recursive: true, force: true })));
|
||||
});
|
||||
|
||||
describe("applyMemoryWikiMutation", () => {
|
||||
it("creates synthesis pages with managed summary blocks and refreshed indexes", async () => {
|
||||
const rootDir = await fs.mkdtemp(path.join(os.tmpdir(), "memory-wiki-apply-"));
|
||||
tempDirs.push(rootDir);
|
||||
const config = resolveMemoryWikiConfig(
|
||||
{ vault: { path: rootDir } },
|
||||
{ homedir: "/Users/tester" },
|
||||
);
|
||||
|
||||
const result = await applyMemoryWikiMutation({
|
||||
config,
|
||||
mutation: {
|
||||
op: "create_synthesis",
|
||||
title: "Alpha Synthesis",
|
||||
body: "Alpha summary body.",
|
||||
sourceIds: ["source.alpha", "source.beta"],
|
||||
contradictions: ["Needs a better primary source"],
|
||||
questions: ["What changed after launch?"],
|
||||
confidence: 0.7,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.changed).toBe(true);
|
||||
expect(result.pagePath).toBe("syntheses/alpha-synthesis.md");
|
||||
expect(result.pageId).toBe("synthesis.alpha-synthesis");
|
||||
expect(result.compile.pageCounts.synthesis).toBe(1);
|
||||
|
||||
const page = await fs.readFile(path.join(rootDir, result.pagePath), "utf8");
|
||||
const parsed = parseWikiMarkdown(page);
|
||||
|
||||
expect(parsed.frontmatter).toMatchObject({
|
||||
pageType: "synthesis",
|
||||
id: "synthesis.alpha-synthesis",
|
||||
title: "Alpha Synthesis",
|
||||
sourceIds: ["source.alpha", "source.beta"],
|
||||
contradictions: ["Needs a better primary source"],
|
||||
questions: ["What changed after launch?"],
|
||||
confidence: 0.7,
|
||||
status: "active",
|
||||
});
|
||||
expect(parsed.body).toContain("## Summary");
|
||||
expect(parsed.body).toContain("<!-- openclaw:wiki:generated:start -->");
|
||||
expect(parsed.body).toContain("Alpha summary body.");
|
||||
expect(parsed.body).toContain("## Notes");
|
||||
expect(parsed.body).toContain("<!-- openclaw:human:start -->");
|
||||
await expect(fs.readFile(path.join(rootDir, "index.md"), "utf8")).resolves.toContain(
|
||||
"[Alpha Synthesis](syntheses/alpha-synthesis.md)",
|
||||
);
|
||||
});
|
||||
|
||||
it("updates page metadata without overwriting existing human notes", async () => {
|
||||
const rootDir = await fs.mkdtemp(path.join(os.tmpdir(), "memory-wiki-apply-"));
|
||||
tempDirs.push(rootDir);
|
||||
const config = resolveMemoryWikiConfig(
|
||||
{ vault: { path: rootDir } },
|
||||
{ homedir: "/Users/tester" },
|
||||
);
|
||||
await initializeMemoryWikiVault(config);
|
||||
|
||||
const targetPath = path.join(rootDir, "entities", "alpha.md");
|
||||
await fs.writeFile(
|
||||
targetPath,
|
||||
renderWikiMarkdown({
|
||||
frontmatter: {
|
||||
pageType: "entity",
|
||||
id: "entity.alpha",
|
||||
title: "Alpha",
|
||||
sourceIds: ["source.old"],
|
||||
confidence: 0.3,
|
||||
},
|
||||
body: `# Alpha
|
||||
|
||||
## Notes
|
||||
<!-- openclaw:human:start -->
|
||||
keep this note
|
||||
<!-- openclaw:human:end -->
|
||||
`,
|
||||
}),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const result = await applyMemoryWikiMutation({
|
||||
config,
|
||||
mutation: {
|
||||
op: "update_metadata",
|
||||
lookup: "entity.alpha",
|
||||
sourceIds: ["source.new"],
|
||||
contradictions: ["Conflicts with source.beta"],
|
||||
questions: ["Is Alpha still active?"],
|
||||
confidence: null,
|
||||
status: "review",
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.changed).toBe(true);
|
||||
expect(result.pagePath).toBe("entities/alpha.md");
|
||||
expect(result.compile.pageCounts.entity).toBe(1);
|
||||
|
||||
const updated = await fs.readFile(targetPath, "utf8");
|
||||
const parsed = parseWikiMarkdown(updated);
|
||||
|
||||
expect(parsed.frontmatter).toMatchObject({
|
||||
pageType: "entity",
|
||||
id: "entity.alpha",
|
||||
title: "Alpha",
|
||||
sourceIds: ["source.new"],
|
||||
contradictions: ["Conflicts with source.beta"],
|
||||
questions: ["Is Alpha still active?"],
|
||||
status: "review",
|
||||
});
|
||||
expect(parsed.frontmatter).not.toHaveProperty("confidence");
|
||||
expect(parsed.body).toContain("keep this note");
|
||||
expect(parsed.body).toContain("<!-- openclaw:human:start -->");
|
||||
await expect(
|
||||
fs.readFile(path.join(rootDir, "entities", "index.md"), "utf8"),
|
||||
).resolves.toContain("[Alpha](entities/alpha.md)");
|
||||
});
|
||||
});
|
||||
255
extensions/memory-wiki/src/apply.ts
Normal file
255
extensions/memory-wiki/src/apply.ts
Normal file
@@ -0,0 +1,255 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import {
|
||||
replaceManagedMarkdownBlock,
|
||||
withTrailingNewline,
|
||||
} from "openclaw/plugin-sdk/memory-host-markdown";
|
||||
import { compileMemoryWikiVault, type CompileMemoryWikiResult } from "./compile.js";
|
||||
import type { ResolvedMemoryWikiConfig } from "./config.js";
|
||||
import {
|
||||
parseWikiMarkdown,
|
||||
renderWikiMarkdown,
|
||||
slugifyWikiSegment,
|
||||
normalizeSourceIds,
|
||||
} from "./markdown.js";
|
||||
import {
|
||||
readQueryableWikiPages,
|
||||
resolveQueryableWikiPageByLookup,
|
||||
type QueryableWikiPage,
|
||||
} from "./query.js";
|
||||
import { initializeMemoryWikiVault } from "./vault.js";
|
||||
|
||||
const GENERATED_START = "<!-- openclaw:wiki:generated:start -->";
|
||||
const GENERATED_END = "<!-- openclaw:wiki:generated:end -->";
|
||||
const HUMAN_START = "<!-- openclaw:human:start -->";
|
||||
const HUMAN_END = "<!-- openclaw:human:end -->";
|
||||
|
||||
export type CreateSynthesisMemoryWikiMutation = {
|
||||
op: "create_synthesis";
|
||||
title: string;
|
||||
body: string;
|
||||
sourceIds: string[];
|
||||
contradictions?: string[];
|
||||
questions?: string[];
|
||||
confidence?: number;
|
||||
status?: string;
|
||||
};
|
||||
|
||||
export type UpdateMetadataMemoryWikiMutation = {
|
||||
op: "update_metadata";
|
||||
lookup: string;
|
||||
sourceIds?: string[];
|
||||
contradictions?: string[];
|
||||
questions?: string[];
|
||||
confidence?: number | null;
|
||||
status?: string;
|
||||
};
|
||||
|
||||
export type ApplyMemoryWikiMutation =
|
||||
| CreateSynthesisMemoryWikiMutation
|
||||
| UpdateMetadataMemoryWikiMutation;
|
||||
|
||||
export type ApplyMemoryWikiMutationResult = {
|
||||
changed: boolean;
|
||||
operation: ApplyMemoryWikiMutation["op"];
|
||||
pagePath: string;
|
||||
pageId?: string;
|
||||
compile: CompileMemoryWikiResult;
|
||||
};
|
||||
|
||||
function normalizeUniqueStrings(values: string[] | undefined): string[] | undefined {
|
||||
if (!values) {
|
||||
return undefined;
|
||||
}
|
||||
const normalized = values
|
||||
.map((value) => value.trim())
|
||||
.filter(Boolean)
|
||||
.filter((value, index, all) => all.indexOf(value) === index);
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function ensureHumanNotesBlock(body: string): string {
|
||||
if (body.includes(HUMAN_START) && body.includes(HUMAN_END)) {
|
||||
return body;
|
||||
}
|
||||
const trimmed = body.trimEnd();
|
||||
const prefix = trimmed.length > 0 ? `${trimmed}\n\n` : "";
|
||||
return `${prefix}## Notes\n${HUMAN_START}\n${HUMAN_END}\n`;
|
||||
}
|
||||
|
||||
function buildSynthesisBody(params: {
|
||||
title: string;
|
||||
originalBody?: string;
|
||||
generatedBody: string;
|
||||
}): string {
|
||||
const base = params.originalBody?.trim().length
|
||||
? params.originalBody
|
||||
: `# ${params.title}\n\n## Notes\n${HUMAN_START}\n${HUMAN_END}\n`;
|
||||
const withGenerated = replaceManagedMarkdownBlock({
|
||||
original: base,
|
||||
heading: "## Summary",
|
||||
startMarker: GENERATED_START,
|
||||
endMarker: GENERATED_END,
|
||||
body: params.generatedBody,
|
||||
});
|
||||
return ensureHumanNotesBlock(withGenerated);
|
||||
}
|
||||
|
||||
async function writeWikiPage(params: {
|
||||
absolutePath: string;
|
||||
frontmatter: Record<string, unknown>;
|
||||
body: string;
|
||||
}): Promise<boolean> {
|
||||
const rendered = withTrailingNewline(
|
||||
renderWikiMarkdown({
|
||||
frontmatter: params.frontmatter,
|
||||
body: params.body,
|
||||
}),
|
||||
);
|
||||
const existing = await fs.readFile(params.absolutePath, "utf8").catch(() => "");
|
||||
if (existing === rendered) {
|
||||
return false;
|
||||
}
|
||||
await fs.mkdir(path.dirname(params.absolutePath), { recursive: true });
|
||||
await fs.writeFile(params.absolutePath, rendered, "utf8");
|
||||
return true;
|
||||
}
|
||||
|
||||
async function resolveWritablePage(params: {
|
||||
config: ResolvedMemoryWikiConfig;
|
||||
lookup: string;
|
||||
}): Promise<QueryableWikiPage | null> {
|
||||
const pages = await readQueryableWikiPages(params.config.vault.path);
|
||||
return resolveQueryableWikiPageByLookup(pages, params.lookup);
|
||||
}
|
||||
|
||||
async function applyCreateSynthesisMutation(params: {
|
||||
config: ResolvedMemoryWikiConfig;
|
||||
mutation: CreateSynthesisMemoryWikiMutation;
|
||||
}): Promise<{ changed: boolean; pagePath: string; pageId: string }> {
|
||||
const slug = slugifyWikiSegment(params.mutation.title);
|
||||
const pagePath = path.join("syntheses", `${slug}.md`).replace(/\\/g, "/");
|
||||
const absolutePath = path.join(params.config.vault.path, pagePath);
|
||||
const existing = await fs.readFile(absolutePath, "utf8").catch(() => "");
|
||||
const parsed = parseWikiMarkdown(existing);
|
||||
const pageId =
|
||||
(typeof parsed.frontmatter.id === "string" && parsed.frontmatter.id.trim()) ||
|
||||
`synthesis.${slug}`;
|
||||
const changed = await writeWikiPage({
|
||||
absolutePath,
|
||||
frontmatter: {
|
||||
...parsed.frontmatter,
|
||||
pageType: "synthesis",
|
||||
id: pageId,
|
||||
title: params.mutation.title,
|
||||
sourceIds: normalizeSourceIds(params.mutation.sourceIds),
|
||||
...(normalizeUniqueStrings(params.mutation.contradictions)
|
||||
? { contradictions: normalizeUniqueStrings(params.mutation.contradictions) }
|
||||
: {}),
|
||||
...(normalizeUniqueStrings(params.mutation.questions)
|
||||
? { questions: normalizeUniqueStrings(params.mutation.questions) }
|
||||
: {}),
|
||||
...(typeof params.mutation.confidence === "number"
|
||||
? { confidence: params.mutation.confidence }
|
||||
: {}),
|
||||
status: params.mutation.status?.trim() || "active",
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
body: buildSynthesisBody({
|
||||
title: params.mutation.title,
|
||||
originalBody: parsed.body,
|
||||
generatedBody: params.mutation.body.trim(),
|
||||
}),
|
||||
});
|
||||
return { changed, pagePath, pageId };
|
||||
}
|
||||
|
||||
function buildUpdatedFrontmatter(params: {
|
||||
original: Record<string, unknown>;
|
||||
mutation: UpdateMetadataMemoryWikiMutation;
|
||||
}): Record<string, unknown> {
|
||||
const frontmatter: Record<string, unknown> = {
|
||||
...params.original,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
if (params.mutation.sourceIds) {
|
||||
frontmatter.sourceIds = normalizeSourceIds(params.mutation.sourceIds);
|
||||
}
|
||||
if (params.mutation.contradictions) {
|
||||
const contradictions = normalizeUniqueStrings(params.mutation.contradictions) ?? [];
|
||||
if (contradictions.length > 0) {
|
||||
frontmatter.contradictions = contradictions;
|
||||
} else {
|
||||
delete frontmatter.contradictions;
|
||||
}
|
||||
}
|
||||
if (params.mutation.questions) {
|
||||
const questions = normalizeUniqueStrings(params.mutation.questions) ?? [];
|
||||
if (questions.length > 0) {
|
||||
frontmatter.questions = questions;
|
||||
} else {
|
||||
delete frontmatter.questions;
|
||||
}
|
||||
}
|
||||
if (params.mutation.confidence === null) {
|
||||
delete frontmatter.confidence;
|
||||
} else if (typeof params.mutation.confidence === "number") {
|
||||
frontmatter.confidence = params.mutation.confidence;
|
||||
}
|
||||
if (params.mutation.status?.trim()) {
|
||||
frontmatter.status = params.mutation.status.trim();
|
||||
}
|
||||
return frontmatter;
|
||||
}
|
||||
|
||||
async function applyUpdateMetadataMutation(params: {
|
||||
config: ResolvedMemoryWikiConfig;
|
||||
mutation: UpdateMetadataMemoryWikiMutation;
|
||||
}): Promise<{ changed: boolean; pagePath: string; pageId?: string }> {
|
||||
const page = await resolveWritablePage({
|
||||
config: params.config,
|
||||
lookup: params.mutation.lookup,
|
||||
});
|
||||
if (!page) {
|
||||
throw new Error(`Wiki page not found: ${params.mutation.lookup}`);
|
||||
}
|
||||
const parsed = parseWikiMarkdown(page.raw);
|
||||
const changed = await writeWikiPage({
|
||||
absolutePath: page.absolutePath,
|
||||
frontmatter: buildUpdatedFrontmatter({
|
||||
original: parsed.frontmatter,
|
||||
mutation: params.mutation,
|
||||
}),
|
||||
body: parsed.body,
|
||||
});
|
||||
return {
|
||||
changed,
|
||||
pagePath: page.relativePath,
|
||||
...(page.id ? { pageId: page.id } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
export async function applyMemoryWikiMutation(params: {
|
||||
config: ResolvedMemoryWikiConfig;
|
||||
mutation: ApplyMemoryWikiMutation;
|
||||
}): Promise<ApplyMemoryWikiMutationResult> {
|
||||
await initializeMemoryWikiVault(params.config);
|
||||
const result =
|
||||
params.mutation.op === "create_synthesis"
|
||||
? await applyCreateSynthesisMutation({
|
||||
config: params.config,
|
||||
mutation: params.mutation,
|
||||
})
|
||||
: await applyUpdateMetadataMutation({
|
||||
config: params.config,
|
||||
mutation: params.mutation,
|
||||
});
|
||||
const compile = await compileMemoryWikiVault(params.config);
|
||||
return {
|
||||
changed: result.changed,
|
||||
operation: params.mutation.op,
|
||||
pagePath: result.pagePath,
|
||||
...(result.pageId ? { pageId: result.pageId } : {}),
|
||||
compile,
|
||||
};
|
||||
}
|
||||
@@ -25,7 +25,7 @@ export type WikiGetResult = {
|
||||
id?: string;
|
||||
};
|
||||
|
||||
type QueryableWikiPage = WikiPageSummary & {
|
||||
export type QueryableWikiPage = WikiPageSummary & {
|
||||
raw: string;
|
||||
};
|
||||
|
||||
@@ -46,7 +46,7 @@ async function listWikiMarkdownFiles(rootDir: string): Promise<string[]> {
|
||||
return files.toSorted((left, right) => left.localeCompare(right));
|
||||
}
|
||||
|
||||
async function readQueryablePages(rootDir: string): Promise<QueryableWikiPage[]> {
|
||||
export async function readQueryableWikiPages(rootDir: string): Promise<QueryableWikiPage[]> {
|
||||
const files = await listWikiMarkdownFiles(rootDir);
|
||||
const pages = await Promise.all(
|
||||
files.map(async (relativePath) => {
|
||||
@@ -113,7 +113,10 @@ function normalizeLookupKey(value: string): string {
|
||||
return normalized.endsWith(".md") ? normalized : normalized.replace(/\/+$/, "");
|
||||
}
|
||||
|
||||
function resolvePageByLookup(pages: QueryableWikiPage[], lookup: string): QueryableWikiPage | null {
|
||||
export function resolveQueryableWikiPageByLookup(
|
||||
pages: QueryableWikiPage[],
|
||||
lookup: string,
|
||||
): QueryableWikiPage | null {
|
||||
const key = normalizeLookupKey(lookup);
|
||||
const withExtension = key.endsWith(".md") ? key : `${key}.md`;
|
||||
return (
|
||||
@@ -132,7 +135,7 @@ export async function searchMemoryWiki(params: {
|
||||
maxResults?: number;
|
||||
}): Promise<WikiSearchResult[]> {
|
||||
await initializeMemoryWikiVault(params.config);
|
||||
const pages = await readQueryablePages(params.config.vault.path);
|
||||
const pages = await readQueryableWikiPages(params.config.vault.path);
|
||||
const maxResults = Math.max(1, params.maxResults ?? 10);
|
||||
return pages
|
||||
.map((page) => ({
|
||||
@@ -160,8 +163,8 @@ export async function getMemoryWikiPage(params: {
|
||||
lineCount?: number;
|
||||
}): Promise<WikiGetResult | null> {
|
||||
await initializeMemoryWikiVault(params.config);
|
||||
const pages = await readQueryablePages(params.config.vault.path);
|
||||
const page = resolvePageByLookup(pages, params.lookup);
|
||||
const pages = await readQueryableWikiPages(params.config.vault.path);
|
||||
const page = resolveQueryableWikiPageByLookup(pages, params.lookup);
|
||||
if (!page) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import type { AnyAgentTool, OpenClawConfig } from "../api.js";
|
||||
import { applyMemoryWikiMutation, type ApplyMemoryWikiMutation } from "./apply.js";
|
||||
import type { ResolvedMemoryWikiConfig } from "./config.js";
|
||||
import { lintMemoryWikiVault } from "./lint.js";
|
||||
import { getMemoryWikiPage, searchMemoryWiki } from "./query.js";
|
||||
@@ -23,6 +24,20 @@ const WikiGetSchema = Type.Object(
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
const WikiApplySchema = Type.Object(
|
||||
{
|
||||
op: Type.Union([Type.Literal("create_synthesis"), Type.Literal("update_metadata")]),
|
||||
title: Type.Optional(Type.String({ minLength: 1 })),
|
||||
body: Type.Optional(Type.String({ minLength: 1 })),
|
||||
lookup: Type.Optional(Type.String({ minLength: 1 })),
|
||||
sourceIds: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
|
||||
contradictions: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
|
||||
questions: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
|
||||
confidence: Type.Optional(Type.Union([Type.Number({ minimum: 0, maximum: 1 }), Type.Null()])),
|
||||
status: Type.Optional(Type.String({ minLength: 1 })),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
async function syncImportedSourcesIfNeeded(
|
||||
config: ResolvedMemoryWikiConfig,
|
||||
@@ -31,6 +46,53 @@ async function syncImportedSourcesIfNeeded(
|
||||
await syncMemoryWikiImportedSources({ config, appConfig });
|
||||
}
|
||||
|
||||
function normalizeWikiApplyMutation(rawParams: unknown): ApplyMemoryWikiMutation {
|
||||
const params = rawParams as {
|
||||
op: ApplyMemoryWikiMutation["op"];
|
||||
title?: string;
|
||||
body?: string;
|
||||
lookup?: string;
|
||||
sourceIds?: string[];
|
||||
contradictions?: string[];
|
||||
questions?: string[];
|
||||
confidence?: number | null;
|
||||
status?: string;
|
||||
};
|
||||
if (params.op === "create_synthesis") {
|
||||
if (!params.title?.trim()) {
|
||||
throw new Error("wiki_apply requires title for create_synthesis.");
|
||||
}
|
||||
if (!params.body?.trim()) {
|
||||
throw new Error("wiki_apply requires body for create_synthesis.");
|
||||
}
|
||||
if (!params.sourceIds || params.sourceIds.length === 0) {
|
||||
throw new Error("wiki_apply requires at least one sourceId for create_synthesis.");
|
||||
}
|
||||
return {
|
||||
op: "create_synthesis",
|
||||
title: params.title,
|
||||
body: params.body,
|
||||
sourceIds: params.sourceIds,
|
||||
...(params.contradictions ? { contradictions: params.contradictions } : {}),
|
||||
...(params.questions ? { questions: params.questions } : {}),
|
||||
...(typeof params.confidence === "number" ? { confidence: params.confidence } : {}),
|
||||
...(params.status ? { status: params.status } : {}),
|
||||
};
|
||||
}
|
||||
if (!params.lookup?.trim()) {
|
||||
throw new Error("wiki_apply requires lookup for update_metadata.");
|
||||
}
|
||||
return {
|
||||
op: "update_metadata",
|
||||
lookup: params.lookup,
|
||||
...(params.sourceIds ? { sourceIds: params.sourceIds } : {}),
|
||||
...(params.contradictions ? { contradictions: params.contradictions } : {}),
|
||||
...(params.questions ? { questions: params.questions } : {}),
|
||||
...(params.confidence !== undefined ? { confidence: params.confidence } : {}),
|
||||
...(params.status ? { status: params.status } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
export function createWikiStatusTool(
|
||||
config: ResolvedMemoryWikiConfig,
|
||||
appConfig?: OpenClawConfig,
|
||||
@@ -122,6 +184,38 @@ export function createWikiLintTool(
|
||||
};
|
||||
}
|
||||
|
||||
export function createWikiApplyTool(
|
||||
config: ResolvedMemoryWikiConfig,
|
||||
appConfig?: OpenClawConfig,
|
||||
): AnyAgentTool {
|
||||
return {
|
||||
name: "wiki_apply",
|
||||
label: "Wiki Apply",
|
||||
description:
|
||||
"Apply narrow wiki mutations for syntheses and page metadata without freeform markdown surgery.",
|
||||
parameters: WikiApplySchema,
|
||||
execute: async (_toolCallId, rawParams) => {
|
||||
const mutation = normalizeWikiApplyMutation(rawParams);
|
||||
await syncImportedSourcesIfNeeded(config, appConfig);
|
||||
const result = await applyMemoryWikiMutation({ config, mutation });
|
||||
const action = result.changed ? "Updated" : "No changes for";
|
||||
const compileSummary =
|
||||
result.compile.updatedFiles.length > 0
|
||||
? `Refreshed ${result.compile.updatedFiles.length} index file${result.compile.updatedFiles.length === 1 ? "" : "s"}.`
|
||||
: "Indexes unchanged.";
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `${action} ${result.pagePath} via ${result.operation}. ${compileSummary}`,
|
||||
},
|
||||
],
|
||||
details: result,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function createWikiGetTool(
|
||||
config: ResolvedMemoryWikiConfig,
|
||||
appConfig?: OpenClawConfig,
|
||||
|
||||
Reference in New Issue
Block a user