From e330e4a17d3c9705ff79da123efe259acd9bd0f3 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Tue, 14 Jul 2026 09:27:10 +0800 Subject: [PATCH] refactor(agents): privatize system prompt helpers (#106965) --- scripts/deadcode-exports.baseline.mjs | 3 - src/agents/system-prompt.test.ts | 171 ++++++++++++-------------- src/agents/system-prompt.ts | 6 +- 3 files changed, 80 insertions(+), 100 deletions(-) diff --git a/scripts/deadcode-exports.baseline.mjs b/scripts/deadcode-exports.baseline.mjs index a1d2fa093f6c..10fa9127206a 100644 --- a/scripts/deadcode-exports.baseline.mjs +++ b/scripts/deadcode-exports.baseline.mjs @@ -770,9 +770,6 @@ export const KNIP_UNUSED_EXPORT_BASELINE = [ "src/agents/subagent-registry.ts: testing", "src/agents/subagent-spawn.ts: testing", "src/agents/system-prompt-config.ts: resolveAgentSystemPromptConfig", - "src/agents/system-prompt.ts: buildAgentBootstrapSystemContext", - "src/agents/system-prompt.ts: buildAgentBootstrapSystemPromptSections", - "src/agents/system-prompt.ts: buildRuntimeLine", "src/agents/tool-call-id.ts: isValidCloudCodeAssistToolId", "src/agents/tool-call-id.ts: sanitizeToolCallId", "src/agents/tool-loop-detection.ts: CRITICAL_THRESHOLD", diff --git a/src/agents/system-prompt.test.ts b/src/agents/system-prompt.test.ts index 96857ad749b5..29647e6488ea 100644 --- a/src/agents/system-prompt.test.ts +++ b/src/agents/system-prompt.test.ts @@ -8,12 +8,7 @@ import { listDeliverableMessageChannels } from "../utils/message-channel.js"; import { resolveAgentPromptSurfaceForSessionKey } from "./prompt-surface.js"; import { buildSkillWorkshopPromptSection } from "./skill-workshop-prompt.js"; import { buildSubagentSystemPrompt } from "./subagent-system-prompt.js"; -import { - buildAgentBootstrapSystemContext, - buildAgentBootstrapSystemPromptSections, - buildAgentSystemPrompt, - buildRuntimeLine, -} from "./system-prompt.js"; +import { buildAgentSystemPrompt } from "./system-prompt.js"; describe("buildAgentSystemPrompt", () => { it("resolves helper session keys to scoped prompt surfaces", () => { @@ -603,9 +598,39 @@ describe("buildAgentSystemPrompt", () => { expect(prompt).toContain("## Bootstrap Pending"); expect(prompt).toContain("BOOTSTRAP.md below; follow before normal reply."); + expect(prompt).toContain("Can finish BOOTSTRAP.md here: do it."); + expect(prompt).toContain("brief blocker"); + expect(prompt).toContain("simplest next step"); + expect(prompt).toContain("Never claim completion early"); expect(prompt).toContain("First visible reply must follow BOOTSTRAP.md"); expect(prompt).toContain("## /tmp/openclaw/BOOTSTRAP.md"); expect(prompt).toContain("Ask who I am."); + expect(prompt.match(/## \/tmp\/openclaw\/BOOTSTRAP\.md/g)).toHaveLength(1); + expect(prompt.match(/Ask who I am\./g)).toHaveLength(1); + }); + + it("uses limited bootstrap wording for constrained user-facing runs", () => { + const prompt = buildAgentSystemPrompt({ + workspaceDir: "/tmp/openclaw", + bootstrapMode: "limited", + }); + + expect(prompt).toContain("## Bootstrap Pending"); + expect(prompt).toContain("cannot safely finish full BOOTSTRAP.md"); + expect(prompt).toContain("Never claim complete"); + expect(prompt).toContain("no generic first greeting"); + expect(prompt).toContain("primary interactive run with normal workspace access"); + }); + + it("omits bootstrap instructions when bootstrap is not pending", () => { + for (const bootstrapMode of ["none", undefined] as const) { + const prompt = buildAgentSystemPrompt({ + workspaceDir: "/tmp/openclaw", + ...(bootstrapMode ? { bootstrapMode } : {}), + }); + + expect(prompt).not.toContain("## Bootstrap Pending"); + } }); it("includes bootstrap truncation notice in system prompt without raw diagnostics", () => { @@ -1316,8 +1341,9 @@ describe("buildAgentSystemPrompt", () => { }); it("builds runtime line with agent and channel details", () => { - const line = buildRuntimeLine( - { + const prompt = buildAgentSystemPrompt({ + workspaceDir: "/tmp/openclaw", + runtimeInfo: { agentId: "work", sessionKey: "agent:main:subagent:runtime-check", sessionId: "23ae7fce-3c27-4a51-b58e-d800d8ca091f", @@ -1329,59 +1355,65 @@ describe("buildAgentSystemPrompt", () => { model: "anthropic/claude", defaultModel: "anthropic/claude-opus-4-5", activeNode: "mac-123", + channel: "telegram", + capabilities: ["inlineButtons"], }, - "telegram", - ["inlineButtons"], - "low", - ); + defaultThinkLevel: "low", + }); - expect(line).toContain("agent=work"); - expect(line).toContain("session=agent:main:subagent:runtime-check"); - expect(line).toContain("sessionId=23ae7fce-3c27-4a51-b58e-d800d8ca091f"); - expect(line).toContain("host=host"); - expect(line).toContain("repo=/repo"); - expect(line).toContain("os=macOS (arm64)"); - expect(line).toContain("node=v20"); - expect(line).toContain("model=anthropic/claude"); - expect(line).toContain("default_model=anthropic/claude-opus-4-5"); - expect(line).toContain("active_node=mac-123"); - expect(line).toContain("channel=telegram"); - expect(line).toContain("capabilities=inlinebuttons"); - expect(line).toContain("thinking=low"); + expect(prompt).toContain("agent=work"); + expect(prompt).toContain("session=agent:main:subagent:runtime-check"); + expect(prompt).toContain("sessionId=23ae7fce-3c27-4a51-b58e-d800d8ca091f"); + expect(prompt).toContain("host=host"); + expect(prompt).toContain("repo=/repo"); + expect(prompt).toContain("os=macOS (arm64)"); + expect(prompt).toContain("node=v20"); + expect(prompt).toContain("model=anthropic/claude"); + expect(prompt).toContain("default_model=anthropic/claude-opus-4-5"); + expect(prompt).toContain("active_node=mac-123"); + expect(prompt).toContain("channel=telegram"); + expect(prompt).toContain("capabilities=inlinebuttons"); + expect(prompt).toContain("thinking=low"); }); it("keeps the runtime line cache-stable across isolated cron runs", () => { // Isolated cron run-scoped keys carry a fresh per-run id every run (forceNew). Rendering it // verbatim re-busts byte-exact prefix caching for the tool catalog after it (#96677 / #43148). const buildForRun = (runId: string) => - buildRuntimeLine({ - agentId: "work", - sessionKey: `agent:work:cron:nightly-job:run:${runId}`, - sessionId: runId, - host: "host", - os: "linux", + buildAgentSystemPrompt({ + workspaceDir: "/tmp/openclaw", + runtimeInfo: { + agentId: "work", + sessionKey: `agent:work:cron:nightly-job:run:${runId}`, + sessionId: runId, + host: "host", + os: "linux", + }, }); - const lineA = buildForRun("11111111-1111-1111-1111-111111111111"); - const lineB = buildForRun("22222222-2222-2222-2222-222222222222"); + const promptA = buildForRun("11111111-1111-1111-1111-111111111111"); + const promptB = buildForRun("22222222-2222-2222-2222-222222222222"); - expect(lineA).toContain("session=agent:work:cron:nightly-job"); - expect(lineA).not.toContain(":run:"); - expect(lineA).not.toContain("sessionId="); + expect(promptA).toContain("session=agent:work:cron:nightly-job"); + expect(promptA).not.toContain(":run:"); + expect(promptA).not.toContain("sessionId="); // Two runs of the same job render identical bytes, so the cached prefix is reused. - expect(lineA).toBe(lineB); + expect(promptA).toBe(promptB); }); it("preserves a stable session id that is not the run-scope id", () => { - const line = buildRuntimeLine({ - agentId: "work", - sessionKey: "agent:work:cron:nightly-job:run:run-id", - sessionId: "stable-session-id", - host: "host", - os: "linux", + const prompt = buildAgentSystemPrompt({ + workspaceDir: "/tmp/openclaw", + runtimeInfo: { + agentId: "work", + sessionKey: "agent:work:cron:nightly-job:run:run-id", + sessionId: "stable-session-id", + host: "host", + os: "linux", + }, }); - expect(line).toContain("session=agent:work:cron:nightly-job"); - expect(line).toContain("sessionId=stable-session-id"); + expect(prompt).toContain("session=agent:work:cron:nightly-job"); + expect(prompt).toContain("sessionId=stable-session-id"); }); it("renders extra system prompt exactly once", () => { @@ -1523,55 +1555,6 @@ describe("buildAgentSystemPrompt", () => { }); }); -describe("buildAgentBootstrapSystemContext", () => { - it("uses friendly full bootstrap wording that is truthful about completion blockers", () => { - const prompt = buildAgentBootstrapSystemContext({ - bootstrapMode: "full", - hasBootstrapFileInProjectContext: true, - }).join("\n"); - - expect(prompt).toContain("## Bootstrap Pending"); - expect(prompt).toContain("BOOTSTRAP.md below; follow before normal reply."); - expect(prompt).toContain("Can finish BOOTSTRAP.md here: do it."); - expect(prompt).toContain("brief blocker"); - expect(prompt).toContain("simplest next step"); - expect(prompt).toContain("Never claim completion early"); - expect(prompt).toContain("First visible reply must follow BOOTSTRAP.md"); - }); - - it("uses limited bootstrap wording for constrained user-facing runs", () => { - const prompt = buildAgentBootstrapSystemContext({ bootstrapMode: "limited" }).join("\n"); - - expect(prompt).toContain("## Bootstrap Pending"); - expect(prompt).toContain("cannot safely finish full BOOTSTRAP.md"); - expect(prompt).toContain("Never claim complete"); - expect(prompt).toContain("no generic first greeting"); - expect(prompt).toContain("primary interactive run with normal workspace access"); - }); - - it("returns nothing when bootstrap is not pending", () => { - expect(buildAgentBootstrapSystemContext({ bootstrapMode: "none" })).toStrictEqual([]); - expect(buildAgentBootstrapSystemContext({})).toStrictEqual([]); - }); -}); - -describe("buildAgentBootstrapSystemPromptSections", () => { - it("can render bootstrap guidance without duplicating Project Context", () => { - const sections = buildAgentBootstrapSystemPromptSections({ - bootstrapMode: "full", - bootstrapTruncationNotice: "Bootstrap context was truncated.", - contextFiles: [{ path: "/tmp/openclaw/BOOTSTRAP.md", content: "Ask who I am." }], - }).join("\n"); - - expect(sections).toContain("## Bootstrap Pending"); - expect(sections).toContain("BOOTSTRAP.md below; follow before normal reply."); - expect(sections).toContain("## Bootstrap Context Notice"); - expect(sections).toContain("Bootstrap context was truncated."); - expect(sections).not.toContain("## /tmp/openclaw/BOOTSTRAP.md"); - expect(sections).not.toContain("Ask who I am."); - }); -}); - describe("buildSubagentSystemPrompt", () => { it("renders depth-1 orchestrator guidance, labels, and recovery notes", () => { const prompt = buildSubagentSystemPrompt({ diff --git a/src/agents/system-prompt.ts b/src/agents/system-prompt.ts index ed5bb0782a5e..14e93f1ad6a1 100644 --- a/src/agents/system-prompt.ts +++ b/src/agents/system-prompt.ts @@ -319,7 +319,7 @@ function buildMemorySection(params: { }); } -export function buildAgentBootstrapSystemContext(params: { +function buildAgentBootstrapSystemContext(params: { bootstrapMode?: BootstrapMode; hasBootstrapFileInProjectContext?: boolean; }): string[] { @@ -349,7 +349,7 @@ export function buildAgentBootstrapSystemContext(params: { ]; } -export function buildAgentBootstrapSystemPromptSections(params: { +function buildAgentBootstrapSystemPromptSections(params: { bootstrapMode?: BootstrapMode; bootstrapTruncationNotice?: string; contextFiles?: EmbeddedContextFile[]; @@ -1384,7 +1384,7 @@ function buildActiveProcessSessionReferenceLines( ]; } -export function buildRuntimeLine( +function buildRuntimeLine( runtimeInfo?: { agentId?: string; sessionKey?: string;