2026-01-10 22:24:13 +01:00
import type { ReasoningLevel , ThinkLevel } from "../auto-reply/thinking.js" ;
2026-01-09 23:29:01 +00:00
import { SILENT_REPLY_TOKEN } from "../auto-reply/tokens.js" ;
2026-01-08 02:20:18 +01:00
import type { EmbeddedContextFile } from "./pi-embedded-helpers.js" ;
2025-12-17 11:29:04 +01:00
2026-01-08 02:20:18 +01:00
export function buildAgentSystemPrompt ( params : {
2025-12-17 11:29:04 +01:00
workspaceDir : string ;
defaultThinkLevel? : ThinkLevel ;
2026-01-10 22:24:13 +01:00
reasoningLevel? : ReasoningLevel ;
2025-12-23 13:32:07 +00:00
extraSystemPrompt? : string ;
2025-12-23 14:19:41 +00:00
ownerNumbers? : string [ ] ;
2025-12-23 14:34:56 +00:00
reasoningTagHint? : boolean ;
2026-01-05 06:32:44 +00:00
toolNames? : string [ ] ;
2026-01-07 06:53:01 +01:00
modelAliasLines? : string [ ] ;
2026-01-05 23:02:13 +00:00
userTimezone? : string ;
userTime? : string ;
2026-01-08 02:20:18 +01:00
contextFiles? : EmbeddedContextFile [ ] ;
2026-01-09 21:20:38 +01:00
skillsPrompt? : string ;
2026-01-06 21:54:19 +00:00
heartbeatPrompt? : string ;
2025-12-23 14:05:43 +00:00
runtimeInfo ? : {
host? : string ;
os? : string ;
arch? : string ;
node? : string ;
model? : string ;
2026-01-09 20:46:11 +01:00
provider? : string ;
capabilities? : string [ ] ;
2025-12-23 14:05:43 +00:00
} ;
2026-01-03 22:11:43 +01:00
sandboxInfo ? : {
enabled : boolean ;
workspaceDir? : string ;
2026-01-07 09:32:49 +00:00
workspaceAccess ? : "none" | "ro" | "rw" ;
agentWorkspaceMount? : string ;
2026-01-03 22:11:43 +01:00
browserControlUrl? : string ;
browserNoVncUrl? : string ;
2026-01-11 01:24:02 +01:00
hostBrowserAllowed? : boolean ;
2026-01-11 02:42:14 +01:00
allowedControlUrls? : string [ ] ;
allowedControlHosts? : string [ ] ;
allowedControlPorts? : number [ ] ;
2026-01-10 21:37:04 +01:00
elevated ? : {
allowed : boolean ;
defaultLevel : "on" | "off" ;
} ;
2026-01-03 22:11:43 +01:00
} ;
2025-12-17 11:29:04 +01:00
} ) {
2026-01-05 06:32:44 +00:00
const toolSummaries : Record < string , string > = {
read : "Read file contents" ,
write : "Create or overwrite files" ,
edit : "Make precise edits to files" ,
grep : "Search file contents for patterns" ,
find : "Find files by glob pattern" ,
ls : "List directory contents" ,
bash : "Run shell commands" ,
process : "Manage background bash sessions" ,
whatsapp_login : "Generate and wait for WhatsApp QR login" ,
2026-01-08 02:20:18 +01:00
browser : "Control web browser" ,
2026-01-05 06:32:44 +00:00
canvas : "Present/eval/snapshot the Canvas" ,
nodes : "List/describe/notify/camera/screen on paired nodes" ,
cron : "Manage cron jobs and wake events" ,
2026-01-09 20:46:11 +01:00
message : "Send messages and provider actions" ,
2026-01-08 01:36:55 +01:00
gateway :
2026-01-08 18:24:00 +01:00
"Restart, apply config, or run updates on the running Clawdbot process" ,
2026-01-08 07:06:36 +00:00
agents_list : "List agent ids allowed for sessions_spawn" ,
2026-01-08 02:20:18 +01:00
sessions_list : "List other sessions (incl. sub-agents) with filters/last" ,
sessions_history : "Fetch history for another session/sub-agent" ,
sessions_send : "Send a message to another session/sub-agent" ,
sessions_spawn : "Spawn a sub-agent session" ,
2026-01-09 21:09:34 +01:00
session_status :
2026-01-10 22:24:13 +01:00
"Show a /status-equivalent status card (usage/cost + Reasoning/Verbose/Elevated); optional per-session model override" ,
2026-01-05 06:32:44 +00:00
image : "Analyze an image with the configured image model" ,
} ;
const toolOrder = [
"read" ,
"write" ,
"edit" ,
"grep" ,
"find" ,
"ls" ,
"bash" ,
"process" ,
"whatsapp_login" ,
"browser" ,
"canvas" ,
"nodes" ,
"cron" ,
2026-01-09 20:46:11 +01:00
"message" ,
2026-01-05 06:32:44 +00:00
"gateway" ,
2026-01-08 07:06:36 +00:00
"agents_list" ,
2026-01-05 06:32:44 +00:00
"sessions_list" ,
"sessions_history" ,
"sessions_send" ,
2026-01-09 21:09:34 +01:00
"session_status" ,
2026-01-05 06:32:44 +00:00
"image" ,
] ;
2026-01-10 04:01:00 +01:00
const rawToolNames = ( params . toolNames ? ? [ ] ) . map ( ( tool ) = > tool . trim ( ) ) ;
const canonicalToolNames = rawToolNames . filter ( Boolean ) ;
const canonicalByNormalized = new Map < string , string > ( ) ;
for ( const name of canonicalToolNames ) {
const normalized = name . toLowerCase ( ) ;
if ( ! canonicalByNormalized . has ( normalized ) ) {
canonicalByNormalized . set ( normalized , name ) ;
}
}
const resolveToolName = ( normalized : string ) = >
canonicalByNormalized . get ( normalized ) ? ? normalized ;
const normalizedTools = canonicalToolNames . map ( ( tool ) = > tool . toLowerCase ( ) ) ;
2026-01-05 06:32:44 +00:00
const availableTools = new Set ( normalizedTools ) ;
const extraTools = Array . from (
new Set ( normalizedTools . filter ( ( tool ) = > ! toolOrder . includes ( tool ) ) ) ,
) ;
const enabledTools = toolOrder . filter ( ( tool ) = > availableTools . has ( tool ) ) ;
const toolLines = enabledTools . map ( ( tool ) = > {
const summary = toolSummaries [ tool ] ;
2026-01-10 04:01:00 +01:00
const name = resolveToolName ( tool ) ;
return summary ? ` - ${ name } : ${ summary } ` : ` - ${ name } ` ;
2026-01-05 06:32:44 +00:00
} ) ;
for ( const tool of extraTools . sort ( ) ) {
2026-01-10 04:01:00 +01:00
toolLines . push ( ` - ${ resolveToolName ( tool ) } ` ) ;
2026-01-05 06:32:44 +00:00
}
2026-01-08 01:36:55 +01:00
const hasGateway = availableTools . has ( "gateway" ) ;
2026-01-10 04:01:00 +01:00
const readToolName = resolveToolName ( "read" ) ;
const bashToolName = resolveToolName ( "bash" ) ;
const processToolName = resolveToolName ( "process" ) ;
2025-12-23 13:32:07 +00:00
const extraSystemPrompt = params . extraSystemPrompt ? . trim ( ) ;
2025-12-23 14:19:41 +00:00
const ownerNumbers = ( params . ownerNumbers ? ? [ ] )
. map ( ( value ) = > value . trim ( ) )
. filter ( Boolean ) ;
const ownerLine =
ownerNumbers . length > 0
2026-01-02 15:03:38 +01:00
? ` Owner numbers: ${ ownerNumbers . join ( ", " ) } . Treat messages from these numbers as the user. `
2025-12-23 14:19:41 +00:00
: undefined ;
2025-12-23 14:34:56 +00:00
const reasoningHint = params . reasoningTagHint
2025-12-24 00:52:30 +00:00
? [
"ALL internal reasoning MUST be inside <think>...</think>." ,
"Do not output any analysis outside <think>." ,
"Format every reply as <think>...</think> then <final>...</final>, with no other text." ,
"Only the final user-visible reply may appear inside <final>." ,
"Only text inside <final> is shown to the user; everything else is discarded and never seen by the user." ,
"Example:" ,
"<think>Short internal reasoning.</think>" ,
2026-01-02 15:03:38 +01:00
"<final>Hey there! What would you like to do next?</final>" ,
2025-12-24 00:52:30 +00:00
] . join ( " " )
2025-12-23 14:34:56 +00:00
: undefined ;
2026-01-10 22:24:13 +01:00
const reasoningLevel = params . reasoningLevel ? ? "off" ;
2026-01-05 23:02:13 +00:00
const userTimezone = params . userTimezone ? . trim ( ) ;
const userTime = params . userTime ? . trim ( ) ;
2026-01-09 21:20:38 +01:00
const skillsPrompt = params . skillsPrompt ? . trim ( ) ;
2026-01-06 21:54:19 +00:00
const heartbeatPrompt = params . heartbeatPrompt ? . trim ( ) ;
const heartbeatPromptLine = heartbeatPrompt
? ` Heartbeat prompt: ${ heartbeatPrompt } `
: "Heartbeat prompt: (configured)" ;
2025-12-23 14:05:43 +00:00
const runtimeInfo = params . runtimeInfo ;
2026-01-09 20:46:11 +01:00
const runtimeProvider = runtimeInfo ? . provider ? . trim ( ) . toLowerCase ( ) ;
const runtimeCapabilities = ( runtimeInfo ? . capabilities ? ? [ ] )
. map ( ( cap ) = > String ( cap ) . trim ( ) )
. filter ( Boolean ) ;
const runtimeCapabilitiesLower = new Set (
runtimeCapabilities . map ( ( cap ) = > cap . toLowerCase ( ) ) ,
) ;
const telegramInlineButtonsEnabled =
runtimeProvider === "telegram" &&
runtimeCapabilitiesLower . has ( "inlinebuttons" ) ;
2026-01-09 21:20:38 +01:00
const skillsLines = skillsPrompt ? [ skillsPrompt , "" ] : [ ] ;
2026-01-09 21:27:11 +01:00
const skillsSection = skillsPrompt
? [
"## Skills" ,
2026-01-10 04:01:00 +01:00
` Skills provide task-specific instructions. Use \` ${ readToolName } \` to load the SKILL.md at the location listed for that skill. ` ,
2026-01-09 21:27:11 +01:00
. . . skillsLines ,
"" ,
]
: [ ] ;
2025-12-23 13:32:07 +00:00
const lines = [
2026-01-08 18:24:00 +01:00
"You are a personal assistant running inside Clawdbot." ,
2025-12-17 11:29:04 +01:00
"" ,
2025-12-22 18:05:44 +01:00
"## Tooling" ,
2026-01-05 06:32:44 +00:00
"Tool availability (filtered by policy):" ,
2026-01-10 04:01:00 +01:00
"Tool names are case-sensitive. Call tools exactly as listed." ,
2026-01-05 06:32:44 +00:00
toolLines . length > 0
? toolLines . join ( "\n" )
: [
"Pi lists the standard tools above. This runtime enables:" ,
"- grep: search file contents for patterns" ,
"- find: find files by glob pattern" ,
"- ls: list directory contents" ,
2026-01-10 04:01:00 +01:00
` - ${ bashToolName } : run shell commands (supports background via yieldMs/background) ` ,
` - ${ processToolName } : manage background bash sessions ` ,
2026-01-05 06:32:44 +00:00
"- whatsapp_login: generate a WhatsApp QR code and wait for linking" ,
"- browser: control clawd's dedicated browser" ,
"- canvas: present/eval/snapshot the Canvas" ,
"- nodes: list/describe/notify/camera/screen on paired nodes" ,
"- cron: manage cron jobs and wake events" ,
"- sessions_list: list sessions" ,
"- sessions_history: fetch session history" ,
"- sessions_send: send to another session" ,
] . join ( "\n" ) ,
2025-12-22 18:05:44 +01:00
"TOOLS.md does not control tool availability; it is user guidance for how to use external tools." ,
2026-01-08 02:20:18 +01:00
"If a task is more complex or takes longer, spawn a sub-agent. It will do the work for you and ping you when it's done. You can always check up on it." ,
2025-12-17 11:29:04 +01:00
"" ,
2026-01-09 21:27:11 +01:00
. . . skillsSection ,
2026-01-08 18:24:00 +01:00
hasGateway ? "## Clawdbot Self-Update" : "" ,
2026-01-08 01:36:55 +01:00
hasGateway
? [
2026-01-08 12:04:29 +01:00
"Get Updates (self-update) is ONLY allowed when the user explicitly asks for it." ,
"Do not run config.apply or update.run unless the user explicitly requests an update or config change; if it's not explicit, ask first." ,
2026-01-08 01:36:55 +01:00
"Actions: config.get, config.schema, config.apply (validate + write full config, then restart), update.run (update deps or git, then restart)." ,
2026-01-08 18:24:00 +01:00
"After restart, Clawdbot pings the last active session automatically." ,
2026-01-08 01:36:55 +01:00
] . join ( "\n" )
: "" ,
hasGateway ? "" : "" ,
"" ,
2026-01-07 06:53:01 +01:00
params . modelAliasLines && params . modelAliasLines . length > 0
? "## Model Aliases"
: "" ,
params . modelAliasLines && params . modelAliasLines . length > 0
? "Prefer aliases when specifying model overrides; full provider/model is also accepted."
: "" ,
params . modelAliasLines && params . modelAliasLines . length > 0
? params . modelAliasLines . join ( "\n" )
: "" ,
params . modelAliasLines && params . modelAliasLines . length > 0 ? "" : "" ,
2025-12-17 11:29:04 +01:00
"## Workspace" ,
` Your working directory is: ${ params . workspaceDir } ` ,
"Treat this directory as the single global workspace for file operations unless explicitly instructed otherwise." ,
"" ,
2026-01-03 22:11:43 +01:00
params . sandboxInfo ? . enabled ? "## Sandbox" : "" ,
params . sandboxInfo ? . enabled
? [
2026-01-10 21:37:04 +01:00
"You are running in a sandboxed runtime (tools execute in Docker)." ,
2026-01-03 22:11:43 +01:00
"Some tools may be unavailable due to sandbox policy." ,
params . sandboxInfo . workspaceDir
? ` Sandbox workspace: ${ params . sandboxInfo . workspaceDir } `
: "" ,
2026-01-07 09:32:49 +00:00
params . sandboxInfo . workspaceAccess
? ` Agent workspace access: ${ params . sandboxInfo . workspaceAccess } ${
params . sandboxInfo . agentWorkspaceMount
? ` (mounted at ${ params . sandboxInfo . agentWorkspaceMount } ) `
: ""
} `
: "" ,
2026-01-03 22:11:43 +01:00
params . sandboxInfo . browserControlUrl
? ` Sandbox browser control URL: ${ params . sandboxInfo . browserControlUrl } `
: "" ,
params . sandboxInfo . browserNoVncUrl
? ` Sandbox browser observer (noVNC): ${ params . sandboxInfo . browserNoVncUrl } `
: "" ,
2026-01-11 01:24:02 +01:00
params . sandboxInfo . hostBrowserAllowed === true
? "Host browser control: allowed."
: params . sandboxInfo . hostBrowserAllowed === false
? "Host browser control: blocked."
: "" ,
2026-01-11 01:52:23 +01:00
params . sandboxInfo . allowedControlUrls ? . length
? ` Browser control URL allowlist: ${ params . sandboxInfo . allowedControlUrls . join (
", " ,
) } `
: "" ,
params . sandboxInfo . allowedControlHosts ? . length
? ` Browser control host allowlist: ${ params . sandboxInfo . allowedControlHosts . join (
", " ,
) } `
: "" ,
params . sandboxInfo . allowedControlPorts ? . length
? ` Browser control port allowlist: ${ params . sandboxInfo . allowedControlPorts . join (
", " ,
) } `
: "" ,
2026-01-10 21:37:04 +01:00
params . sandboxInfo . elevated ? . allowed
? "Elevated bash is available for this session."
: "" ,
params . sandboxInfo . elevated ? . allowed
? "User can toggle with /elevated on|off."
: "" ,
params . sandboxInfo . elevated ? . allowed
? "You may also send /elevated on|off when needed."
: "" ,
params . sandboxInfo . elevated ? . allowed
? ` Current elevated level: ${
params . sandboxInfo . elevated . defaultLevel
} ( on runs bash on host ; off runs in sandbox ) . `
: "" ,
2026-01-03 22:11:43 +01:00
]
. filter ( Boolean )
. join ( "\n" )
: "" ,
params . sandboxInfo ? . enabled ? "" : "" ,
2025-12-23 14:19:41 +00:00
ownerLine ? "## User Identity" : "" ,
ownerLine ? ? "" ,
ownerLine ? "" : "" ,
2025-12-17 11:29:04 +01:00
"## Workspace Files (injected)" ,
2026-01-08 18:24:00 +01:00
"These user-editable files are loaded by Clawdbot and included below in Project Context." ,
2025-12-17 11:29:04 +01:00
"" ,
2026-01-08 02:20:18 +01:00
userTimezone || userTime
? ` Time: assume UTC unless stated. User TZ= ${ userTimezone ? ? "unknown" } . Current user time (converted)= ${ userTime ? ? "unknown" } . `
: "" ,
2026-01-05 23:02:13 +00:00
userTimezone || userTime ? "" : "" ,
2026-01-02 23:18:41 +01:00
"## Reply Tags" ,
"To request a native reply/quote on supported surfaces, include one tag in your reply:" ,
"- [[reply_to_current]] replies to the triggering message." ,
"- [[reply_to:<id>]] replies to a specific message id when you have it." ,
2026-01-09 17:14:36 +01:00
"Whitespace inside the tag is allowed (e.g. [[ reply_to_current ]] / [[ reply_to: 123 ]])." ,
2026-01-02 23:18:41 +01:00
"Tags are stripped before sending; support depends on the current provider config." ,
"" ,
2026-01-08 22:02:42 +01:00
"## Messaging" ,
"- Reply in current session → automatically routes to the source provider (Signal, Telegram, etc.)" ,
"- Cross-session messaging → use sessions_send(sessionKey, message)" ,
"- Never use bash/curl for provider messaging; Clawdbot handles all routing internally." ,
2026-01-09 20:46:11 +01:00
availableTools . has ( "message" )
? [
"" ,
"### message tool" ,
"- Use `message` for proactive sends + provider actions (polls, reactions, etc.)." ,
"- If multiple providers are configured, pass `provider` (whatsapp|telegram|discord|slack|signal|imessage|msteams)." ,
telegramInlineButtonsEnabled
? "- Telegram: inline buttons supported. Use `action=send` with `buttons=[[{text,callback_data}]]` (callback_data routes back as a user message)."
: runtimeProvider === "telegram"
? '- Telegram: inline buttons NOT enabled. If you need them, ask to add "inlineButtons" to telegram.capabilities or telegram.accounts.<id>.capabilities.'
: "" ,
]
. filter ( Boolean )
. join ( "\n" )
: "" ,
2026-01-08 22:02:42 +01:00
"" ,
2025-12-23 13:32:07 +00:00
] ;
if ( extraSystemPrompt ) {
lines . push ( "## Group Chat Context" , extraSystemPrompt , "" ) ;
}
2025-12-23 14:34:56 +00:00
if ( reasoningHint ) {
lines . push ( "## Reasoning Format" , reasoningHint , "" ) ;
}
2025-12-23 13:32:07 +00:00
2026-01-08 02:20:18 +01:00
const contextFiles = params . contextFiles ? ? [ ] ;
if ( contextFiles . length > 0 ) {
lines . push (
"# Project Context" ,
"" ,
"The following project context files have been loaded:" ,
"" ,
) ;
for ( const file of contextFiles ) {
lines . push ( ` ## ${ file . path } ` , "" , file . content , "" ) ;
}
}
2025-12-23 13:32:07 +00:00
lines . push (
2026-01-09 23:29:01 +00:00
"## Silent Replies" ,
` When you have nothing to say, respond with ONLY: ${ SILENT_REPLY_TOKEN } ` ,
"" ,
"⚠️ Rules:" ,
"- It must be your ENTIRE message — nothing else" ,
` - Never append it to an actual response (never include " ${ SILENT_REPLY_TOKEN } " in real replies) ` ,
"- Never wrap it in markdown or code blocks" ,
"" ,
` ❌ Wrong: "Here's help... ${ SILENT_REPLY_TOKEN } " ` ,
` ❌ Wrong: " ${ SILENT_REPLY_TOKEN } " ` ,
` ✅ Right: ${ SILENT_REPLY_TOKEN } ` ,
"" ,
2025-12-17 11:29:04 +01:00
"## Heartbeats" ,
2026-01-06 21:54:19 +00:00
heartbeatPromptLine ,
"If you receive a heartbeat poll (a user message matching the heartbeat prompt above), and there is nothing that needs attention, reply exactly:" ,
2025-12-17 11:29:04 +01:00
"HEARTBEAT_OK" ,
2026-01-08 18:24:00 +01:00
'Clawdbot treats a leading/trailing "HEARTBEAT_OK" as a heartbeat ack (and may discard it).' ,
2025-12-17 11:29:04 +01:00
'If something needs attention, do NOT include "HEARTBEAT_OK"; reply with the alert text instead.' ,
"" ,
"## Runtime" ,
2026-01-08 02:20:18 +01:00
` Runtime: ${ [
runtimeInfo ? . host ? ` host= ${ runtimeInfo . host } ` : "" ,
runtimeInfo ? . os
? ` os= ${ runtimeInfo . os } ${ runtimeInfo ? . arch ? ` ( ${ runtimeInfo . arch } ) ` : "" } `
: runtimeInfo ? . arch
? ` arch= ${ runtimeInfo . arch } `
: "" ,
runtimeInfo ? . node ? ` node= ${ runtimeInfo . node } ` : "" ,
runtimeInfo ? . model ? ` model= ${ runtimeInfo . model } ` : "" ,
2026-01-09 20:46:11 +01:00
runtimeProvider ? ` provider= ${ runtimeProvider } ` : "" ,
runtimeProvider
? ` capabilities= ${
runtimeCapabilities . length > 0
? runtimeCapabilities . join ( "," )
: "none"
} `
: "" ,
2026-01-08 02:20:18 +01:00
` thinking= ${ params . defaultThinkLevel ? ? "off" } ` ,
]
. filter ( Boolean )
. join ( " | " ) } ` ,
2026-01-10 22:24:13 +01:00
` Reasoning: ${ reasoningLevel } (hidden unless on/stream). Toggle /reasoning; /status shows Reasoning when enabled. ` ,
2025-12-23 13:32:07 +00:00
) ;
2025-12-24 00:52:30 +00:00
return lines . filter ( Boolean ) . join ( "\n" ) ;
2025-12-17 11:29:04 +01:00
}