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
@@ -0,0 +1,27 @@
import { BotSystemPromptAppendSettingKey } from "@/src/dict/bot-dict";
import grabAppSetting from "./grab-app-setting";
/**
* The install-wide text appended to every bot's prompt, right after its own
* core instructions (`bot_system_prompt_append`, from Settings > General).
* Empty when the install never set one.
*/
export default async function grabSystemPromptAppend() {
const saved = await grabAppSetting({ key: BotSystemPromptAppendSettingKey });
return saved?.trim() || "";
}
/**
* The same text with its setting key, for the settings page.
*/
export async function grabSystemPromptAppendOverview() {
return {
setting_key: BotSystemPromptAppendSettingKey,
text: await grabSystemPromptAppend(),
};
}
export type SystemPromptAppendOverview = Awaited<
ReturnType<typeof grabSystemPromptAppendOverview>
>;
@@ -1,5 +1,6 @@
import composeBotPrompt from "@/src/utils/compose-bot-prompt";
import AppError from "@/src/utils/app-error";
import grabSystemPromptAppend from "../app-settings/grab-system-prompt-append";
import grabWorkspaceBot from "../workspaces/grab-workspace-bot";
import grabBotUserMemory from "./grab-bot-user-memory";
import grabUserBot from "./grab-user-bot";
@@ -19,8 +20,9 @@ type Params = {
/**
* The full prompt for one of the user's bots (`composeBotPrompt`): its
* core instructions and memory, what it remembers about this user, and in
* a workspace that workspace's context and what the bot has for it.
* core instructions, the install-wide appendix, its memory, what it
* remembers about this user, and in a workspace that workspace's context
* and what the bot has for it.
*/
export default async function grabBotPrompt({ bot_id, user_id, workspace_id }: Params) {
const bot = await grabUserBot({ bot_id, user_id });
@@ -39,6 +41,7 @@ export default async function grabBotPrompt({ bot_id, user_id, workspace_id }: P
return composeBotPrompt({
system_prompt: bot.system_prompt,
appendix: await grabSystemPromptAppend(),
memory: bot.memory,
user_memory: await grabBotUserMemory({ bot_id: bot.id, user_id }),
workspace: found?.workspace,