diff --git a/openclaw.plugin.json b/openclaw.plugin.json index a3a62702..2c858428 100644 --- a/openclaw.plugin.json +++ b/openclaw.plugin.json @@ -44,6 +44,10 @@ "expansionProvider": { "label": "Expansion Provider", "help": "Provider override for lcm_expand_query sub-agent (e.g., 'anthropic')" + }, + "customInstructions": { + "label": "Custom Instructions", + "help": "Natural language instructions injected into all summarization prompts (e.g., formatting rules, tone control)" } }, "configSchema": { @@ -111,6 +115,9 @@ }, "expansionProvider": { "type": "string" + }, + "customInstructions": { + "type": "string" } } } diff --git a/src/db/config.ts b/src/db/config.ts index b1188c14..bf9e6bab 100644 --- a/src/db/config.ts +++ b/src/db/config.ts @@ -42,6 +42,8 @@ export type LcmConfig = { timezone: string; /** When true, retroactively delete HEARTBEAT_OK turn cycles from LCM storage. */ pruneHeartbeatOk: boolean; + /** Custom instructions injected into all summarization prompts. */ + customInstructions: string; }; /** Safely coerce an unknown value to a finite number, or return undefined. */ @@ -185,5 +187,7 @@ export function resolveLcmConfig( env.LCM_PRUNE_HEARTBEAT_OK !== undefined ? env.LCM_PRUNE_HEARTBEAT_OK === "true" : toBool(pc.pruneHeartbeatOk) ?? false, + customInstructions: + env.LCM_CUSTOM_INSTRUCTIONS?.trim() ?? toStr(pc.customInstructions) ?? "", }; } diff --git a/src/engine.ts b/src/engine.ts index 8ff86742..6530a09a 100644 --- a/src/engine.ts +++ b/src/engine.ts @@ -1234,7 +1234,7 @@ export class LcmContextEngine implements ContextEngine { const runtimeSummarizer = await createLcmSummarizeFromLegacyParams({ deps: this.deps, legacyParams: lp, - customInstructions: params.customInstructions, + customInstructions: params.customInstructions || this.config.customInstructions || undefined, }); if (runtimeSummarizer) { return { summarize: runtimeSummarizer.fn, summaryModel: runtimeSummarizer.model }; diff --git a/test/custom-instructions.test.ts b/test/custom-instructions.test.ts new file mode 100644 index 00000000..a02f5b35 --- /dev/null +++ b/test/custom-instructions.test.ts @@ -0,0 +1,58 @@ +import { describe, it, expect } from "vitest"; +import manifest from "../openclaw.plugin.json" with { type: "json" }; +import { resolveLcmConfig } from "../src/db/config.js"; + +describe("customInstructions config", () => { + it("defaults customInstructions to empty string", () => { + const config = resolveLcmConfig({}, {}); + expect(config.customInstructions).toBe(""); + }); + + it("reads customInstructions from plugin config", () => { + const config = resolveLcmConfig({}, { + customInstructions: "Write as a neutral documenter. Use third person.", + }); + expect(config.customInstructions).toBe("Write as a neutral documenter. Use third person."); + }); + + it("env var overrides plugin config for customInstructions", () => { + const config = resolveLcmConfig( + { LCM_CUSTOM_INSTRUCTIONS: "env instructions" } as NodeJS.ProcessEnv, + { customInstructions: "plugin instructions" }, + ); + expect(config.customInstructions).toBe("env instructions"); + }); + + it("trims whitespace from env var customInstructions", () => { + const config = resolveLcmConfig( + { LCM_CUSTOM_INSTRUCTIONS: " trimmed " } as NodeJS.ProcessEnv, + {}, + ); + expect(config.customInstructions).toBe("trimmed"); + }); + + it("trims whitespace from plugin config customInstructions", () => { + const config = resolveLcmConfig({}, { + customInstructions: " trimmed ", + }); + expect(config.customInstructions).toBe("trimmed"); + }); + + it("falls through to default when plugin config value is empty string", () => { + const config = resolveLcmConfig({}, { + customInstructions: " ", + }); + expect(config.customInstructions).toBe(""); + }); + + it("ignores non-string plugin config values", () => { + const config = resolveLcmConfig({}, { + customInstructions: 42, + }); + expect(config.customInstructions).toBe(""); + }); + + it("ships a manifest with customInstructions in schema", () => { + expect(manifest.configSchema.properties.customInstructions).toEqual({ type: "string" }); + }); +});