611 lines
25 KiB
TypeScript
611 lines
25 KiB
TypeScript
import type { ReasoningLevel, ThinkLevel } from "../auto-reply/thinking.js";
|
|
import { SILENT_REPLY_TOKEN } from "../auto-reply/tokens.js";
|
|
import { formatCliCommand } from "../cli/command-format.js";
|
|
import { listDeliverableMessageChannels } from "../utils/message-channel.js";
|
|
import type { ResolvedTimeFormat } from "./date-time.js";
|
|
import type { EmbeddedContextFile } from "./pi-embedded-helpers.js";
|
|
|
|
/**
|
|
* Controls which hardcoded sections are included in the system prompt.
|
|
* - "full": All sections (default, for main agent)
|
|
* - "minimal": Reduced sections (Tooling, Workspace, Runtime) - used for subagents
|
|
* - "none": Just basic identity line, no sections
|
|
*/
|
|
export type PromptMode = "full" | "minimal" | "none";
|
|
|
|
function buildSkillsSection(params: {
|
|
skillsPrompt?: string;
|
|
isMinimal: boolean;
|
|
readToolName: string;
|
|
}) {
|
|
const trimmed = params.skillsPrompt?.trim();
|
|
if (!trimmed || params.isMinimal) return [];
|
|
return [
|
|
"## Skills (mandatory)",
|
|
"Before replying: scan <available_skills> <description> entries.",
|
|
`- If exactly one skill clearly applies: read its SKILL.md at <location> with \`${params.readToolName}\`, then follow it.`,
|
|
"- If multiple could apply: choose the most specific one, then read/follow it.",
|
|
"- If none clearly apply: do not read any SKILL.md.",
|
|
"Constraints: never read more than one skill up front; only read after selecting.",
|
|
trimmed,
|
|
"",
|
|
];
|
|
}
|
|
|
|
function buildMemorySection(params: { isMinimal: boolean; availableTools: Set<string> }) {
|
|
if (params.isMinimal) return [];
|
|
if (!params.availableTools.has("memory_search") && !params.availableTools.has("memory_get")) {
|
|
return [];
|
|
}
|
|
return [
|
|
"## Memory Recall",
|
|
"Before answering anything about prior work, decisions, dates, people, preferences, or todos: run memory_search on MEMORY.md + memory/*.md; then use memory_get to pull only the needed lines. If low confidence after search, say you checked.",
|
|
"",
|
|
];
|
|
}
|
|
|
|
function buildUserIdentitySection(ownerLine: string | undefined, isMinimal: boolean) {
|
|
if (!ownerLine || isMinimal) return [];
|
|
return ["## User Identity", ownerLine, ""];
|
|
}
|
|
|
|
function buildTimeSection(params: {
|
|
userTimezone?: string;
|
|
userTime?: string;
|
|
userTimeFormat?: ResolvedTimeFormat;
|
|
}) {
|
|
if (!params.userTimezone && !params.userTime) return [];
|
|
return [
|
|
"## Current Date & Time",
|
|
params.userTime
|
|
? `${params.userTime} (${params.userTimezone ?? "unknown"})`
|
|
: `Time zone: ${params.userTimezone}. Current time unknown; assume UTC for date/time references.`,
|
|
params.userTimeFormat
|
|
? `Time format: ${params.userTimeFormat === "24" ? "24-hour" : "12-hour"}`
|
|
: "",
|
|
"",
|
|
];
|
|
}
|
|
|
|
function buildReplyTagsSection(isMinimal: boolean) {
|
|
if (isMinimal) return [];
|
|
return [
|
|
"## 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.",
|
|
"Whitespace inside the tag is allowed (e.g. [[ reply_to_current ]] / [[ reply_to: 123 ]]).",
|
|
"Tags are stripped before sending; support depends on the current channel config.",
|
|
"",
|
|
];
|
|
}
|
|
|
|
function buildMessagingSection(params: {
|
|
isMinimal: boolean;
|
|
availableTools: Set<string>;
|
|
messageChannelOptions: string;
|
|
inlineButtonsEnabled: boolean;
|
|
runtimeChannel?: string;
|
|
channelActions?: string[];
|
|
}) {
|
|
if (params.isMinimal) return [];
|
|
|
|
// Build channel-specific action description
|
|
let actionsDescription: string;
|
|
if (params.channelActions && params.channelActions.length > 0 && params.runtimeChannel) {
|
|
// Include "send" as a base action plus channel-specific actions
|
|
const allActions = new Set(["send", ...params.channelActions]);
|
|
const actionList = Array.from(allActions).sort().join(", ");
|
|
actionsDescription = `- Use \`message\` for proactive sends + channel actions. Current channel (${params.runtimeChannel}) supports: ${actionList}.`;
|
|
} else {
|
|
actionsDescription =
|
|
"- Use `message` for proactive sends + channel actions (send, react, edit, delete, etc.).";
|
|
}
|
|
|
|
return [
|
|
"## Messaging",
|
|
"- Reply in current session → automatically routes to the source channel (Signal, Telegram, etc.)",
|
|
"- Cross-session messaging → use sessions_send(sessionKey, message)",
|
|
"- Never use exec/curl for provider messaging; Clawdbot handles all routing internally.",
|
|
params.availableTools.has("message")
|
|
? [
|
|
"",
|
|
"### message tool",
|
|
actionsDescription,
|
|
"- For `action=send`, include `to` and `message`.",
|
|
`- If multiple channels are configured, pass \`channel\` (${params.messageChannelOptions}).`,
|
|
`- If you use \`message\` (\`action=send\`) to deliver your user-visible reply, respond with ONLY: ${SILENT_REPLY_TOKEN} (avoid duplicate replies).`,
|
|
params.inlineButtonsEnabled
|
|
? "- Inline buttons supported. Use `action=send` with `buttons=[[{text,callback_data}]]` (callback_data routes back as a user message)."
|
|
: params.runtimeChannel
|
|
? `- Inline buttons not enabled for ${params.runtimeChannel}. If you need them, ask to set ${params.runtimeChannel}.capabilities.inlineButtons ("dm"|"group"|"all"|"allowlist").`
|
|
: "",
|
|
]
|
|
.filter(Boolean)
|
|
.join("\n")
|
|
: "",
|
|
"",
|
|
];
|
|
}
|
|
|
|
function buildDocsSection(params: { docsPath?: string; isMinimal: boolean; readToolName: string }) {
|
|
const docsPath = params.docsPath?.trim();
|
|
if (!docsPath || params.isMinimal) return [];
|
|
return [
|
|
"## Documentation",
|
|
`Clawdbot docs: ${docsPath}`,
|
|
"Mirror: https://docs.clawd.bot",
|
|
"Source: https://github.com/clawdbot/clawdbot",
|
|
"Community: https://discord.com/invite/clawd",
|
|
"Find new skills: https://clawdhub.com",
|
|
"For Clawdbot behavior, commands, config, or architecture: consult local docs first.",
|
|
`When diagnosing issues, run \`${formatCliCommand("clawdbot status")}\` yourself when possible; only ask the user if you lack access (e.g., sandboxed).`,
|
|
"",
|
|
];
|
|
}
|
|
|
|
export function buildAgentSystemPrompt(params: {
|
|
workspaceDir: string;
|
|
defaultThinkLevel?: ThinkLevel;
|
|
reasoningLevel?: ReasoningLevel;
|
|
extraSystemPrompt?: string;
|
|
ownerNumbers?: string[];
|
|
reasoningTagHint?: boolean;
|
|
toolNames?: string[];
|
|
toolSummaries?: Record<string, string>;
|
|
modelAliasLines?: string[];
|
|
userTimezone?: string;
|
|
userTime?: string;
|
|
userTimeFormat?: ResolvedTimeFormat;
|
|
contextFiles?: EmbeddedContextFile[];
|
|
skillsPrompt?: string;
|
|
heartbeatPrompt?: string;
|
|
docsPath?: string;
|
|
/** Controls which hardcoded sections to include. Defaults to "full". */
|
|
promptMode?: PromptMode;
|
|
runtimeInfo?: {
|
|
agentId?: string;
|
|
host?: string;
|
|
os?: string;
|
|
arch?: string;
|
|
node?: string;
|
|
model?: string;
|
|
channel?: string;
|
|
capabilities?: string[];
|
|
/** Supported message actions for the current channel (e.g., react, edit, unsend) */
|
|
channelActions?: string[];
|
|
};
|
|
sandboxInfo?: {
|
|
enabled: boolean;
|
|
workspaceDir?: string;
|
|
workspaceAccess?: "none" | "ro" | "rw";
|
|
agentWorkspaceMount?: string;
|
|
browserControlUrl?: string;
|
|
browserNoVncUrl?: string;
|
|
hostBrowserAllowed?: boolean;
|
|
allowedControlUrls?: string[];
|
|
allowedControlHosts?: string[];
|
|
allowedControlPorts?: number[];
|
|
elevated?: {
|
|
allowed: boolean;
|
|
defaultLevel: "on" | "off";
|
|
};
|
|
};
|
|
/** Reaction guidance for the agent (for Telegram minimal/extensive modes). */
|
|
reactionGuidance?: {
|
|
level: "minimal" | "extensive";
|
|
channel: string;
|
|
};
|
|
}) {
|
|
const coreToolSummaries: Record<string, string> = {
|
|
read: "Read file contents",
|
|
write: "Create or overwrite files",
|
|
edit: "Make precise edits to files",
|
|
apply_patch: "Apply multi-file patches",
|
|
grep: "Search file contents for patterns",
|
|
find: "Find files by glob pattern",
|
|
ls: "List directory contents",
|
|
exec: "Run shell commands (pty available for TTY-required CLIs)",
|
|
process: "Manage background exec sessions",
|
|
web_search: "Search the web (Brave API)",
|
|
web_fetch: "Fetch and extract readable content from a URL",
|
|
// Channel docking: add login tools here when a channel needs interactive linking.
|
|
browser: "Control web browser",
|
|
canvas: "Present/eval/snapshot the Canvas",
|
|
nodes: "List/describe/notify/camera/screen on paired nodes",
|
|
cron: "Manage cron jobs and wake events (use for reminders; include recent context in reminder text if appropriate)",
|
|
message: "Send messages and channel actions",
|
|
gateway: "Restart, apply config, or run updates on the running Clawdbot process",
|
|
agents_list: "List agent ids allowed for sessions_spawn",
|
|
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",
|
|
session_status:
|
|
"Show a /status-equivalent status card (usage + Reasoning/Verbose/Elevated); optional per-session model override",
|
|
image: "Analyze an image with the configured image model",
|
|
};
|
|
|
|
const toolOrder = [
|
|
"read",
|
|
"write",
|
|
"edit",
|
|
"apply_patch",
|
|
"grep",
|
|
"find",
|
|
"ls",
|
|
"exec",
|
|
"process",
|
|
"web_search",
|
|
"web_fetch",
|
|
"browser",
|
|
"canvas",
|
|
"nodes",
|
|
"cron",
|
|
"message",
|
|
"gateway",
|
|
"agents_list",
|
|
"sessions_list",
|
|
"sessions_history",
|
|
"sessions_send",
|
|
"session_status",
|
|
"image",
|
|
];
|
|
|
|
const rawToolNames = (params.toolNames ?? []).map((tool) => tool.trim());
|
|
const canonicalToolNames = rawToolNames.filter(Boolean);
|
|
// Preserve caller casing while deduping tool names by lowercase.
|
|
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());
|
|
const availableTools = new Set(normalizedTools);
|
|
const externalToolSummaries = new Map<string, string>();
|
|
for (const [key, value] of Object.entries(params.toolSummaries ?? {})) {
|
|
const normalized = key.trim().toLowerCase();
|
|
if (!normalized || !value?.trim()) continue;
|
|
externalToolSummaries.set(normalized, value.trim());
|
|
}
|
|
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 = coreToolSummaries[tool] ?? externalToolSummaries.get(tool);
|
|
const name = resolveToolName(tool);
|
|
return summary ? `- ${name}: ${summary}` : `- ${name}`;
|
|
});
|
|
for (const tool of extraTools.sort()) {
|
|
const summary = coreToolSummaries[tool] ?? externalToolSummaries.get(tool);
|
|
const name = resolveToolName(tool);
|
|
toolLines.push(summary ? `- ${name}: ${summary}` : `- ${name}`);
|
|
}
|
|
|
|
const hasGateway = availableTools.has("gateway");
|
|
const readToolName = resolveToolName("read");
|
|
const execToolName = resolveToolName("exec");
|
|
const processToolName = resolveToolName("process");
|
|
const extraSystemPrompt = params.extraSystemPrompt?.trim();
|
|
const ownerNumbers = (params.ownerNumbers ?? []).map((value) => value.trim()).filter(Boolean);
|
|
const ownerLine =
|
|
ownerNumbers.length > 0
|
|
? `Owner numbers: ${ownerNumbers.join(", ")}. Treat messages from these numbers as the user.`
|
|
: undefined;
|
|
const reasoningHint = params.reasoningTagHint
|
|
? [
|
|
"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>",
|
|
"<final>Hey there! What would you like to do next?</final>",
|
|
].join(" ")
|
|
: undefined;
|
|
const reasoningLevel = params.reasoningLevel ?? "off";
|
|
const userTimezone = params.userTimezone?.trim();
|
|
const userTime = params.userTime?.trim();
|
|
const skillsPrompt = params.skillsPrompt?.trim();
|
|
const heartbeatPrompt = params.heartbeatPrompt?.trim();
|
|
const heartbeatPromptLine = heartbeatPrompt
|
|
? `Heartbeat prompt: ${heartbeatPrompt}`
|
|
: "Heartbeat prompt: (configured)";
|
|
const runtimeInfo = params.runtimeInfo;
|
|
const runtimeChannel = runtimeInfo?.channel?.trim().toLowerCase();
|
|
const runtimeCapabilities = (runtimeInfo?.capabilities ?? [])
|
|
.map((cap) => String(cap).trim())
|
|
.filter(Boolean);
|
|
const runtimeCapabilitiesLower = new Set(runtimeCapabilities.map((cap) => cap.toLowerCase()));
|
|
const inlineButtonsEnabled = runtimeCapabilitiesLower.has("inlinebuttons");
|
|
const messageChannelOptions = listDeliverableMessageChannels().join("|");
|
|
const promptMode = params.promptMode ?? "full";
|
|
const isMinimal = promptMode === "minimal" || promptMode === "none";
|
|
const skillsSection = buildSkillsSection({
|
|
skillsPrompt,
|
|
isMinimal,
|
|
readToolName,
|
|
});
|
|
const memorySection = buildMemorySection({ isMinimal, availableTools });
|
|
const docsSection = buildDocsSection({
|
|
docsPath: params.docsPath,
|
|
isMinimal,
|
|
readToolName,
|
|
});
|
|
|
|
// For "none" mode, return just the basic identity line
|
|
if (promptMode === "none") {
|
|
return "You are a personal assistant running inside Clawdbot.";
|
|
}
|
|
|
|
const lines = [
|
|
"You are a personal assistant running inside Clawdbot.",
|
|
"",
|
|
"## Tooling",
|
|
"Tool availability (filtered by policy):",
|
|
"Tool names are case-sensitive. Call tools exactly as listed.",
|
|
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",
|
|
"- apply_patch: apply multi-file patches",
|
|
`- ${execToolName}: run shell commands (supports background via yieldMs/background)`,
|
|
`- ${processToolName}: manage background exec sessions`,
|
|
"- 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 (use for reminders; include recent context in reminder text if appropriate)",
|
|
"- sessions_list: list sessions",
|
|
"- sessions_history: fetch session history",
|
|
"- sessions_send: send to another session",
|
|
].join("\n"),
|
|
"TOOLS.md does not control tool availability; it is user guidance for how to use external tools.",
|
|
"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.",
|
|
"",
|
|
"## Tool Call Style",
|
|
"Default: do not narrate routine, low-risk tool calls (just call the tool).",
|
|
"Narrate only when it helps: multi-step work, complex/challenging problems, sensitive actions (e.g., deletions), or when the user explicitly asks.",
|
|
"Keep narration brief and value-dense; avoid repeating obvious steps.",
|
|
"Use plain human language for narration unless in a technical context.",
|
|
"",
|
|
"## Clawdbot CLI Quick Reference",
|
|
"Clawdbot is controlled via subcommands. Do not invent commands.",
|
|
"To manage the Gateway daemon service (start/stop/restart):",
|
|
`- ${formatCliCommand("clawdbot daemon status")}`,
|
|
`- ${formatCliCommand("clawdbot daemon start")}`,
|
|
`- ${formatCliCommand("clawdbot daemon stop")}`,
|
|
`- ${formatCliCommand("clawdbot daemon restart")}`,
|
|
`If unsure, ask the user to run \`${formatCliCommand("clawdbot help")}\` (or \`${formatCliCommand("clawdbot daemon --help")}\`) and paste the output.`,
|
|
"",
|
|
...skillsSection,
|
|
...memorySection,
|
|
// Skip self-update for subagent/none modes
|
|
hasGateway && !isMinimal ? "## Clawdbot Self-Update" : "",
|
|
hasGateway && !isMinimal
|
|
? [
|
|
"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.",
|
|
"Actions: config.get, config.schema, config.apply (validate + write full config, then restart), update.run (update deps or git, then restart).",
|
|
"After restart, Clawdbot pings the last active session automatically.",
|
|
].join("\n")
|
|
: "",
|
|
hasGateway && !isMinimal ? "" : "",
|
|
"",
|
|
// Skip model aliases for subagent/none modes
|
|
params.modelAliasLines && params.modelAliasLines.length > 0 && !isMinimal
|
|
? "## Model Aliases"
|
|
: "",
|
|
params.modelAliasLines && params.modelAliasLines.length > 0 && !isMinimal
|
|
? "Prefer aliases when specifying model overrides; full provider/model is also accepted."
|
|
: "",
|
|
params.modelAliasLines && params.modelAliasLines.length > 0 && !isMinimal
|
|
? params.modelAliasLines.join("\n")
|
|
: "",
|
|
params.modelAliasLines && params.modelAliasLines.length > 0 && !isMinimal ? "" : "",
|
|
"## Workspace",
|
|
`Your working directory is: ${params.workspaceDir}`,
|
|
"Treat this directory as the single global workspace for file operations unless explicitly instructed otherwise.",
|
|
"",
|
|
...docsSection,
|
|
params.sandboxInfo?.enabled ? "## Sandbox" : "",
|
|
params.sandboxInfo?.enabled
|
|
? [
|
|
"You are running in a sandboxed runtime (tools execute in Docker).",
|
|
"Some tools may be unavailable due to sandbox policy.",
|
|
"Sub-agents stay sandboxed (no elevated/host access). Need outside-sandbox read/write? Don't spawn; ask first.",
|
|
params.sandboxInfo.workspaceDir
|
|
? `Sandbox workspace: ${params.sandboxInfo.workspaceDir}`
|
|
: "",
|
|
params.sandboxInfo.workspaceAccess
|
|
? `Agent workspace access: ${params.sandboxInfo.workspaceAccess}${
|
|
params.sandboxInfo.agentWorkspaceMount
|
|
? ` (mounted at ${params.sandboxInfo.agentWorkspaceMount})`
|
|
: ""
|
|
}`
|
|
: "",
|
|
params.sandboxInfo.browserControlUrl
|
|
? `Sandbox browser control URL: ${params.sandboxInfo.browserControlUrl}`
|
|
: "",
|
|
params.sandboxInfo.browserNoVncUrl
|
|
? `Sandbox browser observer (noVNC): ${params.sandboxInfo.browserNoVncUrl}`
|
|
: "",
|
|
params.sandboxInfo.hostBrowserAllowed === true
|
|
? "Host browser control: allowed."
|
|
: params.sandboxInfo.hostBrowserAllowed === false
|
|
? "Host browser control: blocked."
|
|
: "",
|
|
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(", ")}`
|
|
: "",
|
|
params.sandboxInfo.elevated?.allowed
|
|
? "Elevated exec 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 exec on host; off runs in sandbox).`
|
|
: "",
|
|
]
|
|
.filter(Boolean)
|
|
.join("\n")
|
|
: "",
|
|
params.sandboxInfo?.enabled ? "" : "",
|
|
...buildUserIdentitySection(ownerLine, isMinimal),
|
|
...buildTimeSection({
|
|
userTimezone,
|
|
userTime,
|
|
userTimeFormat: params.userTimeFormat,
|
|
}),
|
|
"## Workspace Files (injected)",
|
|
"These user-editable files are loaded by Clawdbot and included below in Project Context.",
|
|
"",
|
|
...buildReplyTagsSection(isMinimal),
|
|
...buildMessagingSection({
|
|
isMinimal,
|
|
availableTools,
|
|
messageChannelOptions,
|
|
inlineButtonsEnabled,
|
|
runtimeChannel,
|
|
channelActions: runtimeInfo?.channelActions,
|
|
}),
|
|
];
|
|
|
|
if (extraSystemPrompt) {
|
|
// Use "Subagent Context" header for minimal mode (subagents), otherwise "Group Chat Context"
|
|
const contextHeader =
|
|
promptMode === "minimal" ? "## Subagent Context" : "## Group Chat Context";
|
|
lines.push(contextHeader, extraSystemPrompt, "");
|
|
}
|
|
if (params.reactionGuidance) {
|
|
const { level, channel } = params.reactionGuidance;
|
|
const guidanceText =
|
|
level === "minimal"
|
|
? [
|
|
`Reactions are enabled for ${channel} in MINIMAL mode.`,
|
|
"React ONLY when truly relevant:",
|
|
"- Acknowledge important user requests or confirmations",
|
|
"- Express genuine sentiment (humor, appreciation) sparingly",
|
|
"- Avoid reacting to routine messages or your own replies",
|
|
"Guideline: at most 1 reaction per 5-10 exchanges.",
|
|
].join("\n")
|
|
: [
|
|
`Reactions are enabled for ${channel} in EXTENSIVE mode.`,
|
|
"Feel free to react liberally:",
|
|
"- Acknowledge messages with appropriate emojis",
|
|
"- Express sentiment and personality through reactions",
|
|
"- React to interesting content, humor, or notable events",
|
|
"- Use reactions to confirm understanding or agreement",
|
|
"Guideline: react whenever it feels natural.",
|
|
].join("\n");
|
|
lines.push("## Reactions", guidanceText, "");
|
|
}
|
|
if (reasoningHint) {
|
|
lines.push("## Reasoning Format", reasoningHint, "");
|
|
}
|
|
|
|
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, "");
|
|
}
|
|
}
|
|
|
|
// Skip silent replies for subagent/none modes
|
|
if (!isMinimal) {
|
|
lines.push(
|
|
"## 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}`,
|
|
"",
|
|
);
|
|
}
|
|
|
|
// Skip heartbeats for subagent/none modes
|
|
if (!isMinimal) {
|
|
lines.push(
|
|
"## Heartbeats",
|
|
heartbeatPromptLine,
|
|
"If you receive a heartbeat poll (a user message matching the heartbeat prompt above), and there is nothing that needs attention, reply exactly:",
|
|
"HEARTBEAT_OK",
|
|
'Clawdbot treats a leading/trailing "HEARTBEAT_OK" as a heartbeat ack (and may discard it).',
|
|
'If something needs attention, do NOT include "HEARTBEAT_OK"; reply with the alert text instead.',
|
|
"",
|
|
);
|
|
}
|
|
|
|
lines.push(
|
|
"## Runtime",
|
|
buildRuntimeLine(runtimeInfo, runtimeChannel, runtimeCapabilities, params.defaultThinkLevel),
|
|
`Reasoning: ${reasoningLevel} (hidden unless on/stream). Toggle /reasoning; /status shows Reasoning when enabled.`,
|
|
);
|
|
|
|
return lines.filter(Boolean).join("\n");
|
|
}
|
|
|
|
export function buildRuntimeLine(
|
|
runtimeInfo?: {
|
|
agentId?: string;
|
|
host?: string;
|
|
os?: string;
|
|
arch?: string;
|
|
node?: string;
|
|
model?: string;
|
|
defaultModel?: string;
|
|
},
|
|
runtimeChannel?: string,
|
|
runtimeCapabilities: string[] = [],
|
|
defaultThinkLevel?: ThinkLevel,
|
|
): string {
|
|
return `Runtime: ${[
|
|
runtimeInfo?.agentId ? `agent=${runtimeInfo.agentId}` : "",
|
|
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}` : "",
|
|
runtimeInfo?.defaultModel ? `default_model=${runtimeInfo.defaultModel}` : "",
|
|
runtimeChannel ? `channel=${runtimeChannel}` : "",
|
|
runtimeChannel
|
|
? `capabilities=${runtimeCapabilities.length > 0 ? runtimeCapabilities.join(",") : "none"}`
|
|
: "",
|
|
`thinking=${defaultThinkLevel ?? "off"}`,
|
|
]
|
|
.filter(Boolean)
|
|
.join(" | ")}`;
|
|
}
|