mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 17:20:45 +00:00
fix: cache stable system prompt prep
This commit is contained in:
@@ -123,4 +123,25 @@ describe("buildSystemPromptReport", () => {
|
||||
|
||||
expect(report.injectedWorkspaceFiles[0]?.injectedChars).toBe("trimmed".length);
|
||||
});
|
||||
|
||||
it("does not count injected files as project context when the rendered prompt omits them", () => {
|
||||
const file = makeBootstrapFile({
|
||||
path: "/tmp/workspace/AGENTS.md",
|
||||
content: "raw bootstrap context",
|
||||
});
|
||||
const report = buildSystemPromptReport({
|
||||
source: "run",
|
||||
generatedAt: 0,
|
||||
bootstrapMaxChars: 20_000,
|
||||
systemPrompt: "custom override",
|
||||
bootstrapFiles: [file],
|
||||
injectedFiles: [{ path: "/tmp/workspace/AGENTS.md", content: "rendered context" }],
|
||||
skillsPrompt: "",
|
||||
tools: [],
|
||||
});
|
||||
|
||||
expect(report.systemPrompt.chars).toBe("custom override".length);
|
||||
expect(report.systemPrompt.projectContextChars).toBe(0);
|
||||
expect(report.systemPrompt.nonProjectContextChars).toBe("custom override".length);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,20 +4,21 @@ import { buildBootstrapInjectionStats } from "./bootstrap-budget.js";
|
||||
import type { EmbeddedContextFile } from "./pi-embedded-helpers.js";
|
||||
import type { WorkspaceBootstrapFile } from "./workspace.js";
|
||||
|
||||
function extractBetween(
|
||||
input: string,
|
||||
startMarker: string,
|
||||
endMarker: string,
|
||||
): { text: string; found: boolean } {
|
||||
type ToolReportEntry = SessionSystemPromptReport["tools"]["entries"][number];
|
||||
|
||||
const toolReportEntryCache = new WeakMap<AgentTool, ToolReportEntry>();
|
||||
const toolSchemaStatsCache = new WeakMap<
|
||||
object,
|
||||
Pick<ToolReportEntry, "propertiesCount" | "schemaChars">
|
||||
>();
|
||||
|
||||
function extractBetween(input: string, startMarker: string, endMarker: string): string {
|
||||
const start = input.indexOf(startMarker);
|
||||
if (start === -1) {
|
||||
return { text: "", found: false };
|
||||
return "";
|
||||
}
|
||||
const end = input.indexOf(endMarker, start + startMarker.length);
|
||||
if (end === -1) {
|
||||
return { text: input.slice(start), found: true };
|
||||
}
|
||||
return { text: input.slice(start, end), found: true };
|
||||
return end === -1 ? input.slice(start) : input.slice(start, end);
|
||||
}
|
||||
|
||||
function parseSkillBlocks(skillsPrompt: string): Array<{ name: string; blockChars: number }> {
|
||||
@@ -36,36 +37,57 @@ function parseSkillBlocks(skillsPrompt: string): Array<{ name: string; blockChar
|
||||
.filter((b) => b.blockChars > 0);
|
||||
}
|
||||
|
||||
function buildToolsEntries(tools: AgentTool[]): SessionSystemPromptReport["tools"]["entries"] {
|
||||
return tools.map((tool) => {
|
||||
const name = tool.name;
|
||||
const summary = tool.description?.trim() || tool.label?.trim() || "";
|
||||
const summaryChars = summary.length;
|
||||
const schemaChars = (() => {
|
||||
if (!tool.parameters || typeof tool.parameters !== "object") {
|
||||
return 0;
|
||||
}
|
||||
function buildToolSchemaStats(
|
||||
parameters: AgentTool["parameters"],
|
||||
): Pick<ToolReportEntry, "propertiesCount" | "schemaChars"> {
|
||||
if (!parameters || typeof parameters !== "object") {
|
||||
return { schemaChars: 0, propertiesCount: null };
|
||||
}
|
||||
const cached = toolSchemaStatsCache.get(parameters);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
const stats = {
|
||||
schemaChars: (() => {
|
||||
try {
|
||||
return JSON.stringify(tool.parameters).length;
|
||||
return JSON.stringify(parameters).length;
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
})();
|
||||
const propertiesCount = (() => {
|
||||
const schema =
|
||||
tool.parameters && typeof tool.parameters === "object"
|
||||
? (tool.parameters as Record<string, unknown>)
|
||||
: null;
|
||||
const props = schema && typeof schema.properties === "object" ? schema.properties : null;
|
||||
})(),
|
||||
propertiesCount: (() => {
|
||||
const schema = parameters as Record<string, unknown>;
|
||||
const props = typeof schema.properties === "object" ? schema.properties : null;
|
||||
if (!props || typeof props !== "object") {
|
||||
return null;
|
||||
}
|
||||
return Object.keys(props as Record<string, unknown>).length;
|
||||
})();
|
||||
return { name, summaryChars, schemaChars, propertiesCount };
|
||||
})(),
|
||||
};
|
||||
toolSchemaStatsCache.set(parameters, stats);
|
||||
return stats;
|
||||
}
|
||||
|
||||
function buildToolsEntries(tools: AgentTool[]): SessionSystemPromptReport["tools"]["entries"] {
|
||||
return tools.map((tool) => {
|
||||
const cached = toolReportEntryCache.get(tool);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
const name = tool.name;
|
||||
const summary = tool.description?.trim() || tool.label?.trim() || "";
|
||||
const summaryChars = summary.length;
|
||||
const schemaStats = buildToolSchemaStats(tool.parameters);
|
||||
const entry = { name, summaryChars, ...schemaStats };
|
||||
toolReportEntryCache.set(tool, entry);
|
||||
return entry;
|
||||
});
|
||||
}
|
||||
|
||||
function measureRenderedProjectContextChars(systemPrompt: string): number {
|
||||
return extractBetween(systemPrompt, "\n# Project Context\n", "\n## Silent Replies\n").length;
|
||||
}
|
||||
|
||||
export function buildSystemPromptReport(params: {
|
||||
source: SessionSystemPromptReport["source"];
|
||||
generatedAt: number;
|
||||
@@ -84,13 +106,8 @@ export function buildSystemPromptReport(params: {
|
||||
skillsPrompt: string;
|
||||
tools: AgentTool[];
|
||||
}): SessionSystemPromptReport {
|
||||
const systemPrompt = params.systemPrompt.trim();
|
||||
const projectContext = extractBetween(
|
||||
systemPrompt,
|
||||
"\n# Project Context\n",
|
||||
"\n## Silent Replies\n",
|
||||
);
|
||||
const projectContextChars = projectContext.text.length;
|
||||
const systemPromptChars = params.systemPrompt.length;
|
||||
const projectContextChars = measureRenderedProjectContextChars(params.systemPrompt);
|
||||
const toolsEntries = buildToolsEntries(params.tools);
|
||||
const toolsSchemaChars = toolsEntries.reduce((sum, t) => sum + (t.schemaChars ?? 0), 0);
|
||||
const skillsEntries = parseSkillBlocks(params.skillsPrompt);
|
||||
@@ -108,9 +125,9 @@ export function buildSystemPromptReport(params: {
|
||||
...(params.bootstrapTruncation ? { bootstrapTruncation: params.bootstrapTruncation } : {}),
|
||||
sandbox: params.sandbox,
|
||||
systemPrompt: {
|
||||
chars: systemPrompt.length,
|
||||
chars: systemPromptChars,
|
||||
projectContextChars,
|
||||
nonProjectContextChars: Math.max(0, systemPrompt.length - projectContextChars),
|
||||
nonProjectContextChars: Math.max(0, systemPromptChars - projectContextChars),
|
||||
},
|
||||
injectedWorkspaceFiles: buildBootstrapInjectionStats({
|
||||
bootstrapFiles: params.bootstrapFiles,
|
||||
|
||||
@@ -57,6 +57,40 @@ const CONTEXT_FILE_ORDER = new Map<string, number>([
|
||||
const DYNAMIC_CONTEXT_FILE_BASENAMES = new Set(["heartbeat.md"]);
|
||||
const DEFAULT_HEARTBEAT_PROMPT_CONTEXT_BLOCK =
|
||||
"Default heartbeat prompt:\n`Read HEARTBEAT.md if it exists (workspace context). Follow it strictly. Do not infer or repeat old tasks from prior chats. If nothing needs attention, reply HEARTBEAT_OK.`";
|
||||
const SYSTEM_PROMPT_STABLE_PREFIX_CACHE_LIMIT = 64;
|
||||
|
||||
type StablePromptPrefixCacheEntry = {
|
||||
value: string;
|
||||
};
|
||||
|
||||
const stablePromptPrefixCache = new Map<string, StablePromptPrefixCacheEntry>();
|
||||
|
||||
function cacheStablePromptPrefix(key: string, build: () => string): string {
|
||||
const cached = stablePromptPrefixCache.get(key);
|
||||
if (cached) {
|
||||
stablePromptPrefixCache.delete(key);
|
||||
stablePromptPrefixCache.set(key, cached);
|
||||
return cached.value;
|
||||
}
|
||||
|
||||
const value = build();
|
||||
stablePromptPrefixCache.set(key, { value });
|
||||
while (stablePromptPrefixCache.size > SYSTEM_PROMPT_STABLE_PREFIX_CACHE_LIMIT) {
|
||||
const oldestKey = stablePromptPrefixCache.keys().next().value;
|
||||
if (oldestKey === undefined) {
|
||||
break;
|
||||
}
|
||||
stablePromptPrefixCache.delete(oldestKey);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function hashStablePromptInput(value: unknown): string {
|
||||
const hash = createHash("sha256");
|
||||
hash.update(JSON.stringify(value));
|
||||
return hash.digest("hex");
|
||||
}
|
||||
|
||||
function normalizeContextFilePath(pathValue: string): string {
|
||||
return pathValue.trim().replace(/\\/g, "/");
|
||||
}
|
||||
@@ -721,215 +755,6 @@ export function buildAgentSystemPrompt(params: {
|
||||
return "You are a personal assistant running inside OpenClaw.";
|
||||
}
|
||||
|
||||
const lines = [
|
||||
"You are a personal assistant running inside OpenClaw.",
|
||||
"",
|
||||
"## 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 OpenClaw'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; when scheduling a reminder, write the systemEvent text as something that will read like a reminder when it fires, and mention that it is a reminder depending on the time gap between setting and firing; include recent context in reminder text if appropriate)",
|
||||
"- sessions_list: list sessions",
|
||||
"- sessions_history: fetch session history",
|
||||
"- sessions_send: send to another session",
|
||||
"- subagents: list/steer/kill sub-agent runs",
|
||||
'- session_status: show usage/time/model state and answer "what model are we using?"',
|
||||
].join("\n"),
|
||||
"TOOLS.md does not control tool availability; it is user guidance for how to use external tools.",
|
||||
`For long waits, avoid rapid poll loops: use ${execToolName} with enough yieldMs or ${processToolName}(action=poll, timeout=<ms>).`,
|
||||
"If a task is more complex or takes longer, spawn a sub-agent. Completion is push-based: it will auto-announce when done.",
|
||||
'Sub-agents start isolated by default. Use `sessions_spawn` with `context:"fork"` only when the child needs the current transcript context; otherwise omit `context` or use `context:"isolated"`.',
|
||||
...nativeCommandGuidanceLines,
|
||||
...(acpHarnessSpawnAllowed
|
||||
? [
|
||||
'For requests like "do this in claude code/cursor/gemini/opencode" or similar ACP harnesses, treat it as ACP harness intent and call `sessions_spawn` with `runtime: "acp"`.',
|
||||
...(runtimeChannel === "discord" && threadBoundAcpSpawnEnabled
|
||||
? [
|
||||
'On Discord, default ACP harness requests to thread-bound persistent sessions (`thread: true`, `mode: "session"`) unless the user asks otherwise.',
|
||||
]
|
||||
: []),
|
||||
"Set `agentId` explicitly unless `acp.defaultAgent` is configured, and do not route ACP harness requests through `subagents`/`agents_list` or local PTY exec flows.",
|
||||
...(threadBoundAcpSpawnEnabled
|
||||
? [
|
||||
'For ACP harness thread spawns, do not call `message` with `action=thread-create`; use `sessions_spawn` (`runtime: "acp"`, `thread: true`) as the single thread creation path.',
|
||||
]
|
||||
: []),
|
||||
]
|
||||
: []),
|
||||
"Do not poll `subagents list` / `sessions_list` in a loop; only check status on-demand (for intervention, debugging, or when explicitly asked).",
|
||||
"",
|
||||
...buildOverridablePromptSection({
|
||||
override: providerSectionOverrides.interaction_style,
|
||||
fallback: [],
|
||||
}),
|
||||
...buildOverridablePromptSection({
|
||||
override: providerSectionOverrides.tool_call_style,
|
||||
fallback: [
|
||||
"## 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.",
|
||||
"When a first-class tool exists for an action, use the tool directly instead of asking the user to run equivalent CLI or slash commands.",
|
||||
buildExecApprovalPromptGuidance({
|
||||
runtimeChannel: params.runtimeInfo?.channel,
|
||||
inlineButtonsEnabled,
|
||||
runtimeCapabilities,
|
||||
}),
|
||||
"Never execute /approve through exec or any other shell/tool path; /approve is a user-facing approval command, not a shell command.",
|
||||
"Treat allow-once as single-command only: if another elevated command needs approval, request a fresh /approve and do not claim prior approval covered it.",
|
||||
"When approvals are required, preserve and show the full command/script exactly as provided (including chained operators like &&, ||, |, ;, or multiline shells) so the user can approve what will actually run, but keep command/script previews separate from the /approve command and never substitute the shell command/script for the approval id or slug.",
|
||||
"",
|
||||
],
|
||||
}),
|
||||
...buildOverridablePromptSection({
|
||||
override: providerSectionOverrides.execution_bias,
|
||||
fallback: buildExecutionBiasSection({
|
||||
isMinimal,
|
||||
}),
|
||||
}),
|
||||
...buildOverridablePromptSection({
|
||||
override: providerStablePrefix,
|
||||
fallback: [],
|
||||
}),
|
||||
...safetySection,
|
||||
"## OpenClaw CLI Quick Reference",
|
||||
"OpenClaw is controlled via subcommands. Do not invent commands.",
|
||||
"For config changes, use the first-class `gateway` tool (`config.schema.lookup`, `config.get`, `config.patch`, `config.apply`) instead of editing config through exec; the gateway tool hot-reloads config when possible and uses a safe restart only when required.",
|
||||
"Use the `gateway` tool action `restart` for Gateway restarts. Only use CLI service lifecycle commands when the user explicitly asks for them.",
|
||||
"Gateway service lifecycle quick reference:",
|
||||
"- openclaw gateway status",
|
||||
"- openclaw gateway restart",
|
||||
"Operator-only, explicit user request:",
|
||||
"- openclaw gateway start",
|
||||
"- openclaw gateway stop",
|
||||
"Do not chain `openclaw gateway stop` and `openclaw gateway start` as a restart substitute.",
|
||||
"If unsure, ask the user to run `openclaw help` (or `openclaw gateway --help`) and paste the output.",
|
||||
"",
|
||||
...skillsSection,
|
||||
...memorySection,
|
||||
// Skip self-update for subagent/none modes
|
||||
hasGateway && !isMinimal ? "## OpenClaw 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.",
|
||||
"Use config.schema.lookup with a specific dot path to inspect only the relevant config subtree before making config changes or answering config-field questions; avoid guessing field names/types.",
|
||||
"Actions: config.schema.lookup, config.get, config.patch (partial update, merges with existing), config.apply (validate + write full config), update.run (update deps or git, then restart). Config writes hot-reload when possible and use a safe restart only when required.",
|
||||
"After restart, OpenClaw 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 ? "" : "",
|
||||
userTimezone
|
||||
? "If you need the current date, time, or day of week, run session_status (📊 session_status)."
|
||||
: "",
|
||||
"## Workspace",
|
||||
`Your working directory is: ${displayWorkspaceDir}`,
|
||||
workspaceGuidance,
|
||||
...workspaceNotes,
|
||||
"",
|
||||
...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.",
|
||||
hasSessionsSpawn && acpEnabled
|
||||
? 'ACP harness spawns are blocked from sandboxed sessions (`sessions_spawn` with `runtime: "acp"`). Use `runtime: "subagent"` instead.'
|
||||
: "",
|
||||
params.sandboxInfo.containerWorkspaceDir
|
||||
? `Sandbox container workdir: ${sanitizeForPromptLiteral(params.sandboxInfo.containerWorkspaceDir)}`
|
||||
: "",
|
||||
params.sandboxInfo.workspaceDir
|
||||
? `Sandbox host mount source (file tools bridge only; not valid inside sandbox exec): ${sanitizeForPromptLiteral(params.sandboxInfo.workspaceDir)}`
|
||||
: "",
|
||||
params.sandboxInfo.workspaceAccess
|
||||
? `Agent workspace access: ${params.sandboxInfo.workspaceAccess}${
|
||||
params.sandboxInfo.agentWorkspaceMount
|
||||
? ` (mounted at ${sanitizeForPromptLiteral(params.sandboxInfo.agentWorkspaceMount)})`
|
||||
: ""
|
||||
}`
|
||||
: "",
|
||||
params.sandboxInfo.browserBridgeUrl ? "Sandbox browser: enabled." : "",
|
||||
params.sandboxInfo.hostBrowserAllowed === true
|
||||
? "Host browser control: allowed."
|
||||
: params.sandboxInfo.hostBrowserAllowed === false
|
||||
? "Host browser control: blocked."
|
||||
: "",
|
||||
elevated?.allowed
|
||||
? "Elevated exec is available for this session."
|
||||
: elevated
|
||||
? "Elevated exec is unavailable for this session."
|
||||
: "",
|
||||
elevated?.allowed && elevated.fullAccessAvailable
|
||||
? "User can toggle with /elevated on|off|ask|full."
|
||||
: "",
|
||||
elevated?.allowed && !elevated.fullAccessAvailable
|
||||
? "User can toggle with /elevated on|off|ask."
|
||||
: "",
|
||||
elevated?.allowed && elevated.fullAccessAvailable
|
||||
? "You may also send /elevated on|off|ask|full when needed."
|
||||
: "",
|
||||
elevated?.allowed && !elevated.fullAccessAvailable
|
||||
? "You may also send /elevated on|off|ask when needed."
|
||||
: "",
|
||||
elevated?.fullAccessAvailable === false
|
||||
? `Auto-approved /elevated full is unavailable here (${fullAccessBlockedReasonLabel}).`
|
||||
: "",
|
||||
elevated?.allowed && elevated.fullAccessAvailable
|
||||
? `Current elevated level: ${elevated.defaultLevel} (ask runs exec on host with approvals; full auto-approves).`
|
||||
: elevated?.allowed
|
||||
? `Current elevated level: ${elevated.defaultLevel} (full auto-approval unavailable here; use ask/on instead).`
|
||||
: elevated
|
||||
? "Current elevated level: off (elevated exec unavailable)."
|
||||
: "",
|
||||
elevated && !elevated.allowed
|
||||
? "Do not tell the user to switch to /elevated full in this session."
|
||||
: "",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n")
|
||||
: "",
|
||||
params.sandboxInfo?.enabled ? "" : "",
|
||||
...buildUserIdentitySection(ownerLine, isMinimal),
|
||||
...buildTimeSection({
|
||||
userTimezone,
|
||||
}),
|
||||
"## Workspace Files (injected)",
|
||||
"These user-editable files are loaded by OpenClaw and included below in Project Context.",
|
||||
"",
|
||||
...buildAssistantOutputDirectivesSection(isMinimal),
|
||||
];
|
||||
|
||||
if (reasoningHint) {
|
||||
lines.push("## Reasoning Format", reasoningHint, "");
|
||||
}
|
||||
|
||||
const contextFiles = params.contextFiles ?? [];
|
||||
const validContextFiles = contextFiles.filter(
|
||||
(file) => typeof file.path === "string" && file.path.trim().length > 0,
|
||||
@@ -937,36 +762,279 @@ export function buildAgentSystemPrompt(params: {
|
||||
const orderedContextFiles = sortContextFilesForPrompt(validContextFiles);
|
||||
const stableContextFiles = orderedContextFiles.filter((file) => !isDynamicContextFile(file.path));
|
||||
const dynamicContextFiles = orderedContextFiles.filter((file) => isDynamicContextFile(file.path));
|
||||
lines.push(
|
||||
...buildProjectContextSection({
|
||||
files: stableContextFiles,
|
||||
heading: "# Project Context",
|
||||
dynamic: false,
|
||||
}),
|
||||
);
|
||||
const stablePrefixCacheKey = hashStablePromptInput({
|
||||
workspaceDir: params.workspaceDir,
|
||||
promptMode,
|
||||
toolLines,
|
||||
hasGateway,
|
||||
readToolName,
|
||||
execToolName,
|
||||
processToolName,
|
||||
nativeCommandGuidanceLines,
|
||||
providerSectionOverrides,
|
||||
providerStablePrefix,
|
||||
ownerLine,
|
||||
reasoningHint,
|
||||
reasoningLevel,
|
||||
userTimezone,
|
||||
runtimeChannel,
|
||||
runtimeCapabilities,
|
||||
inlineButtonsEnabled,
|
||||
threadBoundAcpSpawnEnabled,
|
||||
sourceMessageToolOnly,
|
||||
silentReplyPromptMode,
|
||||
sandboxInfo: params.sandboxInfo,
|
||||
displayWorkspaceDir,
|
||||
workspaceGuidance,
|
||||
workspaceNotes,
|
||||
docsPath: params.docsPath,
|
||||
sourcePath: params.sourcePath,
|
||||
skillsPrompt,
|
||||
modelAliasLines: params.modelAliasLines,
|
||||
includeMemorySection: params.includeMemorySection,
|
||||
memoryCitationsMode: params.memoryCitationsMode,
|
||||
memorySection,
|
||||
acpEnabled,
|
||||
stableContextFiles,
|
||||
});
|
||||
const stablePrefix = cacheStablePromptPrefix(stablePrefixCacheKey, () => {
|
||||
const lines = [
|
||||
"You are a personal assistant running inside OpenClaw.",
|
||||
"",
|
||||
"## 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 OpenClaw'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; when scheduling a reminder, write the systemEvent text as something that will read like a reminder when it fires, and mention that it is a reminder depending on the time gap between setting and firing; include recent context in reminder text if appropriate)",
|
||||
"- sessions_list: list sessions",
|
||||
"- sessions_history: fetch session history",
|
||||
"- sessions_send: send to another session",
|
||||
"- subagents: list/steer/kill sub-agent runs",
|
||||
'- session_status: show usage/time/model state and answer "what model are we using?"',
|
||||
].join("\n"),
|
||||
"TOOLS.md does not control tool availability; it is user guidance for how to use external tools.",
|
||||
`For long waits, avoid rapid poll loops: use ${execToolName} with enough yieldMs or ${processToolName}(action=poll, timeout=<ms>).`,
|
||||
"If a task is more complex or takes longer, spawn a sub-agent. Completion is push-based: it will auto-announce when done.",
|
||||
'Sub-agents start isolated by default. Use `sessions_spawn` with `context:"fork"` only when the child needs the current transcript context; otherwise omit `context` or use `context:"isolated"`.',
|
||||
...nativeCommandGuidanceLines,
|
||||
...(acpHarnessSpawnAllowed
|
||||
? [
|
||||
'For requests like "do this in claude code/cursor/gemini/opencode" or similar ACP harnesses, treat it as ACP harness intent and call `sessions_spawn` with `runtime: "acp"`.',
|
||||
...(runtimeChannel === "discord" && threadBoundAcpSpawnEnabled
|
||||
? [
|
||||
'On Discord, default ACP harness requests to thread-bound persistent sessions (`thread: true`, `mode: "session"`) unless the user asks otherwise.',
|
||||
]
|
||||
: []),
|
||||
"Set `agentId` explicitly unless `acp.defaultAgent` is configured, and do not route ACP harness requests through `subagents`/`agents_list` or local PTY exec flows.",
|
||||
...(threadBoundAcpSpawnEnabled
|
||||
? [
|
||||
'For ACP harness thread spawns, do not call `message` with `action=thread-create`; use `sessions_spawn` (`runtime: "acp"`, `thread: true`) as the single thread creation path.',
|
||||
]
|
||||
: []),
|
||||
]
|
||||
: []),
|
||||
"Do not poll `subagents list` / `sessions_list` in a loop; only check status on-demand (for intervention, debugging, or when explicitly asked).",
|
||||
"",
|
||||
...buildOverridablePromptSection({
|
||||
override: providerSectionOverrides.interaction_style,
|
||||
fallback: [],
|
||||
}),
|
||||
...buildOverridablePromptSection({
|
||||
override: providerSectionOverrides.tool_call_style,
|
||||
fallback: [
|
||||
"## 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.",
|
||||
"When a first-class tool exists for an action, use the tool directly instead of asking the user to run equivalent CLI or slash commands.",
|
||||
buildExecApprovalPromptGuidance({
|
||||
runtimeChannel: params.runtimeInfo?.channel,
|
||||
inlineButtonsEnabled,
|
||||
runtimeCapabilities,
|
||||
}),
|
||||
"Never execute /approve through exec or any other shell/tool path; /approve is a user-facing approval command, not a shell command.",
|
||||
"Treat allow-once as single-command only: if another elevated command needs approval, request a fresh /approve and do not claim prior approval covered it.",
|
||||
"When approvals are required, preserve and show the full command/script exactly as provided (including chained operators like &&, ||, |, ;, or multiline shells) so the user can approve what will actually run, but keep command/script previews separate from the /approve command and never substitute the shell command/script for the approval id or slug.",
|
||||
"",
|
||||
],
|
||||
}),
|
||||
...buildOverridablePromptSection({
|
||||
override: providerSectionOverrides.execution_bias,
|
||||
fallback: buildExecutionBiasSection({
|
||||
isMinimal,
|
||||
}),
|
||||
}),
|
||||
...buildOverridablePromptSection({
|
||||
override: providerStablePrefix,
|
||||
fallback: [],
|
||||
}),
|
||||
...safetySection,
|
||||
"## OpenClaw CLI Quick Reference",
|
||||
"OpenClaw is controlled via subcommands. Do not invent commands.",
|
||||
"For config changes, use the first-class `gateway` tool (`config.schema.lookup`, `config.get`, `config.patch`, `config.apply`) instead of editing config through exec; the gateway tool hot-reloads config when possible and uses a safe restart only when required.",
|
||||
"Use the `gateway` tool action `restart` for Gateway restarts. Only use CLI service lifecycle commands when the user explicitly asks for them.",
|
||||
"Gateway service lifecycle quick reference:",
|
||||
"- openclaw gateway status",
|
||||
"- openclaw gateway restart",
|
||||
"Operator-only, explicit user request:",
|
||||
"- openclaw gateway start",
|
||||
"- openclaw gateway stop",
|
||||
"Do not chain `openclaw gateway stop` and `openclaw gateway start` as a restart substitute.",
|
||||
"If unsure, ask the user to run `openclaw help` (or `openclaw gateway --help`) and paste the output.",
|
||||
"",
|
||||
...skillsSection,
|
||||
...memorySection,
|
||||
hasGateway && !isMinimal ? "## OpenClaw 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.",
|
||||
"Use config.schema.lookup with a specific dot path to inspect only the relevant config subtree before making config changes or answering config-field questions; avoid guessing field names/types.",
|
||||
"Actions: config.schema.lookup, config.get, config.patch (partial update, merges with existing), config.apply (validate + write full config), update.run (update deps or git, then restart). Config writes hot-reload when possible and use a safe restart only when required.",
|
||||
"After restart, OpenClaw pings the last active session automatically.",
|
||||
].join("\n")
|
||||
: "",
|
||||
hasGateway && !isMinimal ? "" : "",
|
||||
"",
|
||||
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 ? "" : "",
|
||||
userTimezone
|
||||
? "If you need the current date, time, or day of week, run session_status (📊 session_status)."
|
||||
: "",
|
||||
"## Workspace",
|
||||
`Your working directory is: ${displayWorkspaceDir}`,
|
||||
workspaceGuidance,
|
||||
...workspaceNotes,
|
||||
"",
|
||||
...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.",
|
||||
hasSessionsSpawn && acpEnabled
|
||||
? 'ACP harness spawns are blocked from sandboxed sessions (`sessions_spawn` with `runtime: "acp"`). Use `runtime: "subagent"` instead.'
|
||||
: "",
|
||||
params.sandboxInfo.containerWorkspaceDir
|
||||
? `Sandbox container workdir: ${sanitizeForPromptLiteral(params.sandboxInfo.containerWorkspaceDir)}`
|
||||
: "",
|
||||
params.sandboxInfo.workspaceDir
|
||||
? `Sandbox host mount source (file tools bridge only; not valid inside sandbox exec): ${sanitizeForPromptLiteral(params.sandboxInfo.workspaceDir)}`
|
||||
: "",
|
||||
params.sandboxInfo.workspaceAccess
|
||||
? `Agent workspace access: ${params.sandboxInfo.workspaceAccess}${
|
||||
params.sandboxInfo.agentWorkspaceMount
|
||||
? ` (mounted at ${sanitizeForPromptLiteral(params.sandboxInfo.agentWorkspaceMount)})`
|
||||
: ""
|
||||
}`
|
||||
: "",
|
||||
params.sandboxInfo.browserBridgeUrl ? "Sandbox browser: enabled." : "",
|
||||
params.sandboxInfo.hostBrowserAllowed === true
|
||||
? "Host browser control: allowed."
|
||||
: params.sandboxInfo.hostBrowserAllowed === false
|
||||
? "Host browser control: blocked."
|
||||
: "",
|
||||
elevated?.allowed
|
||||
? "Elevated exec is available for this session."
|
||||
: elevated
|
||||
? "Elevated exec is unavailable for this session."
|
||||
: "",
|
||||
elevated?.allowed && elevated.fullAccessAvailable
|
||||
? "User can toggle with /elevated on|off|ask|full."
|
||||
: "",
|
||||
elevated?.allowed && !elevated.fullAccessAvailable
|
||||
? "User can toggle with /elevated on|off|ask."
|
||||
: "",
|
||||
elevated?.allowed && elevated.fullAccessAvailable
|
||||
? "You may also send /elevated on|off|ask|full when needed."
|
||||
: "",
|
||||
elevated?.allowed && !elevated.fullAccessAvailable
|
||||
? "You may also send /elevated on|off|ask when needed."
|
||||
: "",
|
||||
elevated?.fullAccessAvailable === false
|
||||
? `Auto-approved /elevated full is unavailable here (${fullAccessBlockedReasonLabel}).`
|
||||
: "",
|
||||
elevated?.allowed && elevated.fullAccessAvailable
|
||||
? `Current elevated level: ${elevated.defaultLevel} (ask runs exec on host with approvals; full auto-approves).`
|
||||
: elevated?.allowed
|
||||
? `Current elevated level: ${elevated.defaultLevel} (full auto-approval unavailable here; use ask/on instead).`
|
||||
: elevated
|
||||
? "Current elevated level: off (elevated exec unavailable)."
|
||||
: "",
|
||||
elevated && !elevated.allowed
|
||||
? "Do not tell the user to switch to /elevated full in this session."
|
||||
: "",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n")
|
||||
: "",
|
||||
params.sandboxInfo?.enabled ? "" : "",
|
||||
...buildUserIdentitySection(ownerLine, isMinimal),
|
||||
...buildTimeSection({
|
||||
userTimezone,
|
||||
}),
|
||||
"## Workspace Files (injected)",
|
||||
"These user-editable files are loaded by OpenClaw and included below in Project Context.",
|
||||
"",
|
||||
...buildAssistantOutputDirectivesSection(isMinimal),
|
||||
];
|
||||
|
||||
if (reasoningHint) {
|
||||
lines.push("## Reasoning Format", reasoningHint, "");
|
||||
}
|
||||
|
||||
// Skip silent replies for subagent/none modes
|
||||
if (!isMinimal && silentReplyPromptMode !== "none") {
|
||||
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}`,
|
||||
"",
|
||||
...buildProjectContextSection({
|
||||
files: stableContextFiles,
|
||||
heading: "# Project Context",
|
||||
dynamic: false,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// Keep large stable prompt context above this seam so Anthropic-family
|
||||
// transports can reuse it across labs and turns. Dynamic group/session
|
||||
// additions and volatile project context below it are the primary cache invalidators.
|
||||
lines.push(SYSTEM_PROMPT_CACHE_BOUNDARY);
|
||||
if (!isMinimal && silentReplyPromptMode !== "none") {
|
||||
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}`,
|
||||
"",
|
||||
);
|
||||
}
|
||||
|
||||
lines.push(SYSTEM_PROMPT_CACHE_BOUNDARY);
|
||||
return lines.filter(Boolean).join("\n");
|
||||
});
|
||||
|
||||
const lines = [stablePrefix];
|
||||
|
||||
lines.push(
|
||||
...buildProjectContextSection({
|
||||
|
||||
Reference in New Issue
Block a user