Append install-wide instructions to every bot's prompt

Settings > General gains a prompt appendix the super admin sets once:
markdown every bot runs with, right after its own core instructions.

- The text lives in app_settings (bot_system_prompt_append, read through
  grabSystemPromptAppend, cached like every app setting), so it changes
  without a deploy and applies to every bot on the next run.
- composeBotPrompt takes an appendix and pushes it as
  # Install-wide instructions directly after system_prompt, before the
  workspace block and memory, so the bot's own more specific rules still
  win. grabBotPrompt feeds it in, so chat replies, tasks, scheduled runs
  and the coordinator all get it with no call site touched.
- api/admin/settings/system-prompt-append saves it (super admin only,
  capped at AppData.PromptMaxLength, empty clears it) and is audit
  logged under system.
- The section and its form are hidden from everyone but the super admin,
  save in place with a character count and no reload, and say what an
  empty text does.
This commit is contained in:
2026-10-22 11:12:38 +01:00
parent a88898d169
commit a8d8b680ad
13 changed files with 278 additions and 9 deletions
+25
View File
@@ -30,6 +30,31 @@ describe("composeBotPrompt", () => {
);
});
test("puts the install-wide appendix right after the core instructions", () => {
expect(
composeBotPrompt({
system_prompt: "Core.",
appendix: "Never invent sources.",
workspace: { name: "Launch", description: "The spring launch." },
memory: "- Cite sources.",
}),
).toBe(
[
"Core.",
"# Install-wide instructions\n\nNever invent sources.",
`# Workspace: Launch\n\nYou're working in the workspace "Launch". Follow its context and your instructions for it below, over your general habits where they differ.\n\nThe workspace's context, which the user sets:\n\nThe spring launch.`,
"# Memory\n\n- Cite sources.",
].join("\n\n"),
);
});
test("shows an appendix on its own and ignores a blank one", () => {
expect(composeBotPrompt({ appendix: "Be brief." })).toBe(
"# Install-wide instructions\n\nBe brief.",
);
expect(composeBotPrompt({ system_prompt: "Core.", appendix: " " })).toBe("Core.");
});
test("leaves out empty parts and has no workspace outside one", () => {
expect(
composeBotPrompt({
+15 -5
View File
@@ -11,6 +11,11 @@ type WorkspaceBotPrompt = {
type Params = {
system_prompt?: string | null;
/**
* The install-wide text every bot gets, right after its own core
* instructions (Settings > General).
*/
appendix?: string | null;
memory?: string | null;
/**
* What the bot remembers about the user it's working with.
@@ -28,14 +33,15 @@ type Params = {
/**
* The full prompt a bot runs with (markdown): its own core instructions,
* then, in a workspace, the workspace's context and the bot's
* instructions there, then its core memory, its memory about the user and
* its memory in the workspace (said to be the bot's own notes, not the
* workspace's context). Empty parts are left out; the workspace heading
* always shows, so the bot knows where it is.
* then the install-wide appendix, then, in a workspace, the workspace's
* context and the bot's instructions there, then its core memory, its
* memory about the user and its memory in the workspace (said to be the
* bot's own notes, not the workspace's context). Empty parts are left out;
* the workspace heading always shows, so the bot knows where it is.
*/
export default function composeBotPrompt({
system_prompt,
appendix,
memory,
user_memory,
workspace,
@@ -48,6 +54,10 @@ export default function composeBotPrompt({
parts.push(system_prompt.trim());
}
if (appendix?.trim()) {
parts.push(`# Install-wide instructions\n\n${appendix.trim()}`);
}
if (workspace) {
const context = [workspace.description?.trim(), workspace.system_prompt?.trim()].filter(
Boolean,