2025-12-17 11:29:04 +01:00
import type { ThinkLevel } from "../auto-reply/thinking.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 ;
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-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 ;
} ;
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 :
"Show a /status-equivalent status card (includes usage + cost when available); 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" ,
] ;
const normalizedTools = ( params . toolNames ? ? [ ] )
. map ( ( tool ) = > tool . trim ( ) . toLowerCase ( ) )
. filter ( Boolean ) ;
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 ] ;
return summary ? ` - ${ tool } : ${ summary } ` : ` - ${ tool } ` ;
} ) ;
for ( const tool of extraTools . sort ( ) ) {
toolLines . push ( ` - ${ tool } ` ) ;
}
2026-01-08 01:36:55 +01:00
const hasGateway = availableTools . has ( "gateway" ) ;
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-05 23:02:13 +00:00
const userTimezone = params . userTimezone ? . trim ( ) ;
const userTime = params . userTime ? . 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" ) ;
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):" ,
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" ,
"- bash: run shell commands (supports background via yieldMs/background)" ,
"- process: manage background bash sessions" ,
"- 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-08 02:20:18 +01:00
"## Skills" ,
` Skills provide task-specific instructions. Use \` read \` to load from ${ params . workspaceDir } /skills/<name>/SKILL.md when needed. ` ,
"" ,
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
? [
"Tool execution is isolated in a Docker sandbox." ,
"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 } `
: "" ,
]
. 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 (
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 ( " | " ) } ` ,
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
}