diff --git a/src/agents/pi-embedded-runner/run/attempt-bootstrap-routing.ts b/src/agents/pi-embedded-runner/run/attempt-bootstrap-routing.ts index 48541fa41ce..d73b4983b89 100644 --- a/src/agents/pi-embedded-runner/run/attempt-bootstrap-routing.ts +++ b/src/agents/pi-embedded-runner/run/attempt-bootstrap-routing.ts @@ -15,7 +15,8 @@ export type AttemptBootstrapRoutingInput = { export type AttemptBootstrapRouting = { bootstrapMode: BootstrapMode; - shouldStripBootstrapFromContext: boolean; + includeBootstrapInSystemContext: boolean; + includeBootstrapInRuntimeContext: boolean; }; export type AttemptWorkspaceBootstrapRoutingInput = Omit< @@ -25,10 +26,16 @@ export type AttemptWorkspaceBootstrapRoutingInput = Omit< isWorkspaceBootstrapPending: (workspaceDir: string) => Promise; }; -export function shouldStripBootstrapFromEmbeddedContext(_params: { +export function resolveBootstrapContextTargets(params: { bootstrapMode: BootstrapMode; -}): boolean { - return _params.bootstrapMode !== "full"; +}): Pick< + AttemptBootstrapRouting, + "includeBootstrapInSystemContext" | "includeBootstrapInRuntimeContext" +> { + return { + includeBootstrapInSystemContext: params.bootstrapMode === "full", + includeBootstrapInRuntimeContext: false, + }; } function resolveAttemptBootstrapRouting( @@ -47,9 +54,7 @@ function resolveAttemptBootstrapRouting( return { bootstrapMode, - shouldStripBootstrapFromContext: shouldStripBootstrapFromEmbeddedContext({ - bootstrapMode, - }), + ...resolveBootstrapContextTargets({ bootstrapMode }), }; } diff --git a/src/agents/pi-embedded-runner/run/attempt-system-prompt.test.ts b/src/agents/pi-embedded-runner/run/attempt-system-prompt.test.ts new file mode 100644 index 00000000000..889cc4ae206 --- /dev/null +++ b/src/agents/pi-embedded-runner/run/attempt-system-prompt.test.ts @@ -0,0 +1,90 @@ +import { describe, expect, it } from "vitest"; +import { buildAttemptSystemPrompt } from "./attempt-system-prompt.js"; + +const baseProviderTransform = { + provider: "openai", + workspaceDir: "/tmp/openclaw", + context: { + provider: "openai", + modelId: "gpt-5.5", + promptMode: "full" as const, + }, +}; + +describe("buildAttemptSystemPrompt", () => { + it("preserves bootstrap Project Context when a system prompt override is configured", () => { + const result = buildAttemptSystemPrompt({ + isRawModelRun: false, + systemPromptOverrideText: "Custom override prompt.", + embeddedSystemPrompt: { + workspaceDir: "/tmp/openclaw", + reasoningTagHint: false, + runtimeInfo: { + host: "test-host", + os: "Darwin", + arch: "arm64", + node: "v22.0.0", + model: "openai/gpt-5.5", + }, + tools: [], + modelAliasLines: [], + userTimezone: "UTC", + bootstrapMode: "full", + bootstrapTruncationNotice: "Bootstrap context was truncated.", + contextFiles: [ + { + path: "/tmp/openclaw/BOOTSTRAP.md", + content: "Reply with BOOTSTRAP_OK.", + }, + { + path: "/tmp/openclaw/USER.md", + content: "User profile should stay in normal prompt context only.", + }, + ], + }, + providerTransform: baseProviderTransform, + }); + + expect(result.systemPrompt).toContain("Custom override prompt."); + expect(result.systemPrompt).toContain("## Bootstrap Pending"); + expect(result.systemPrompt).toContain("BOOTSTRAP.md is included below in Project Context"); + expect(result.systemPrompt).toContain("## Bootstrap Context Notice"); + expect(result.systemPrompt).toContain("Bootstrap context was truncated."); + expect(result.systemPrompt).toContain("# Project Context"); + expect(result.systemPrompt).toContain("## /tmp/openclaw/BOOTSTRAP.md"); + expect(result.systemPrompt).toContain("Reply with BOOTSTRAP_OK."); + expect(result.systemPrompt).not.toContain("USER.md"); + }); + + it("omits system prompts for raw model probes", () => { + const result = buildAttemptSystemPrompt({ + isRawModelRun: true, + embeddedSystemPrompt: { + workspaceDir: "/tmp/openclaw", + reasoningTagHint: false, + runtimeInfo: { + host: "test-host", + os: "Darwin", + arch: "arm64", + node: "v22.0.0", + model: "openai/gpt-5.5", + }, + tools: [], + modelAliasLines: [], + userTimezone: "UTC", + bootstrapMode: "full", + contextFiles: [ + { + path: "/tmp/openclaw/BOOTSTRAP.md", + content: "Reply with BOOTSTRAP_OK.", + }, + ], + }, + providerTransform: baseProviderTransform, + }); + + expect(result.baseSystemPrompt).toContain("BOOTSTRAP.md is included below in Project Context"); + expect(result.systemPrompt).toBe(""); + expect(result.systemPromptOverride()).toBe(""); + }); +}); diff --git a/src/agents/pi-embedded-runner/run/attempt-system-prompt.ts b/src/agents/pi-embedded-runner/run/attempt-system-prompt.ts new file mode 100644 index 00000000000..85ffa422316 --- /dev/null +++ b/src/agents/pi-embedded-runner/run/attempt-system-prompt.ts @@ -0,0 +1,56 @@ +import type { OpenClawConfig } from "../../../config/types.openclaw.js"; +import { transformProviderSystemPrompt } from "../../../plugins/provider-runtime.js"; +import type { ProviderTransformSystemPromptContext } from "../../../plugins/types.js"; +import { appendAgentBootstrapSystemPromptSupplement } from "../../system-prompt.js"; +import { buildEmbeddedSystemPrompt, createSystemPromptOverride } from "../system-prompt.js"; + +type EmbeddedSystemPromptParams = Parameters[0]; + +export type BuildAttemptSystemPromptParams = { + isRawModelRun: boolean; + systemPromptOverrideText?: string; + embeddedSystemPrompt: EmbeddedSystemPromptParams; + providerTransform: { + provider: string; + config?: OpenClawConfig; + workspaceDir: string; + context: Omit; + }; +}; + +export type AttemptSystemPrompt = { + baseSystemPrompt: string; + systemPrompt: string; + systemPromptOverride: (defaultPrompt?: string) => string; +}; + +export function buildAttemptSystemPrompt( + params: BuildAttemptSystemPromptParams, +): AttemptSystemPrompt { + const baseSystemPrompt = params.systemPromptOverrideText + ? appendAgentBootstrapSystemPromptSupplement({ + systemPrompt: params.systemPromptOverrideText, + bootstrapMode: params.embeddedSystemPrompt.bootstrapMode, + bootstrapTruncationNotice: params.embeddedSystemPrompt.bootstrapTruncationNotice, + contextFiles: params.embeddedSystemPrompt.contextFiles, + }) + : buildEmbeddedSystemPrompt(params.embeddedSystemPrompt); + + const systemPrompt = params.isRawModelRun + ? "" + : transformProviderSystemPrompt({ + provider: params.providerTransform.provider, + config: params.providerTransform.config, + workspaceDir: params.providerTransform.workspaceDir, + context: { + ...params.providerTransform.context, + systemPrompt: baseSystemPrompt, + }, + }); + + return { + baseSystemPrompt, + systemPrompt, + systemPromptOverride: createSystemPromptOverride(systemPrompt), + }; +} diff --git a/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.bootstrap-routing.test.ts b/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.bootstrap-routing.test.ts index 92eacbca413..b5ddf161238 100644 --- a/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.bootstrap-routing.test.ts +++ b/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.bootstrap-routing.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it, vi } from "vitest"; import { + resolveBootstrapContextTargets, resolveAttemptWorkspaceBootstrapRouting, - shouldStripBootstrapFromEmbeddedContext, } from "./attempt-bootstrap-routing.js"; describe("runEmbeddedAttempt bootstrap routing", () => { @@ -26,7 +26,8 @@ describe("runEmbeddedAttempt bootstrap routing", () => { expect(isWorkspaceBootstrapPending).toHaveBeenCalledWith(canonicalWorkspace); expect(isWorkspaceBootstrapPending).not.toHaveBeenCalledWith(sandboxWorkspace); expect(routing.bootstrapMode).toBe("none"); - expect(routing.shouldStripBootstrapFromContext).toBe(true); + expect(routing.includeBootstrapInSystemContext).toBe(false); + expect(routing.includeBootstrapInRuntimeContext).toBe(false); }); it("falls back to limited bootstrap wording when a primary run cannot read files", async () => { @@ -41,15 +42,25 @@ describe("runEmbeddedAttempt bootstrap routing", () => { }); expect(routing.bootstrapMode).toBe("limited"); - expect(routing.shouldStripBootstrapFromContext).toBe(true); + expect(routing.includeBootstrapInSystemContext).toBe(false); + expect(routing.includeBootstrapInRuntimeContext).toBe(false); }); it("keeps BOOTSTRAP.md in Project Context for full bootstrap turns", () => { - expect(shouldStripBootstrapFromEmbeddedContext({ bootstrapMode: "full" })).toBe(false); + expect(resolveBootstrapContextTargets({ bootstrapMode: "full" })).toEqual({ + includeBootstrapInSystemContext: true, + includeBootstrapInRuntimeContext: false, + }); }); - it("strips BOOTSTRAP.md from Project Context outside full bootstrap turns", () => { - expect(shouldStripBootstrapFromEmbeddedContext({ bootstrapMode: "limited" })).toBe(true); - expect(shouldStripBootstrapFromEmbeddedContext({ bootstrapMode: "none" })).toBe(true); + it("excludes BOOTSTRAP.md from every context outside full bootstrap turns", () => { + expect(resolveBootstrapContextTargets({ bootstrapMode: "limited" })).toEqual({ + includeBootstrapInSystemContext: false, + includeBootstrapInRuntimeContext: false, + }); + expect(resolveBootstrapContextTargets({ bootstrapMode: "none" })).toEqual({ + includeBootstrapInSystemContext: false, + includeBootstrapInRuntimeContext: false, + }); }); }); diff --git a/src/agents/pi-embedded-runner/run/attempt.test.ts b/src/agents/pi-embedded-runner/run/attempt.test.ts index b571b96e1a7..eb98b3c36b2 100644 --- a/src/agents/pi-embedded-runner/run/attempt.test.ts +++ b/src/agents/pi-embedded-runner/run/attempt.test.ts @@ -3,6 +3,7 @@ import { describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../../../config/config.js"; import { SYSTEM_PROMPT_CACHE_BOUNDARY } from "../../system-prompt-cache-boundary.js"; import { buildAgentSystemPrompt } from "../../system-prompt.js"; +import { resolveBootstrapContextTargets } from "./attempt-bootstrap-routing.js"; import { buildContextEnginePromptCacheInfo, buildAfterTurnRuntimeContext, @@ -24,7 +25,6 @@ import { resolveAttemptToolPolicyMessageProvider, resolvePromptBuildHookResult, resolvePromptModeForSession, - shouldStripBootstrapFromEmbeddedContext, shouldWarnOnOrphanedUserRepair, wrapStreamFnRepairMalformedToolCallArguments, wrapStreamFnSanitizeMalformedToolCalls, @@ -404,11 +404,20 @@ describe("resolvePromptModeForSession", () => { }); }); -describe("shouldStripBootstrapFromEmbeddedContext", () => { +describe("resolveBootstrapContextTargets", () => { it("keeps BOOTSTRAP.md in system Project Context only for full bootstrap turns", () => { - expect(shouldStripBootstrapFromEmbeddedContext({ bootstrapMode: "full" })).toBe(false); - expect(shouldStripBootstrapFromEmbeddedContext({ bootstrapMode: "limited" })).toBe(true); - expect(shouldStripBootstrapFromEmbeddedContext({ bootstrapMode: "none" })).toBe(true); + expect(resolveBootstrapContextTargets({ bootstrapMode: "full" })).toEqual({ + includeBootstrapInSystemContext: true, + includeBootstrapInRuntimeContext: false, + }); + expect(resolveBootstrapContextTargets({ bootstrapMode: "limited" })).toEqual({ + includeBootstrapInSystemContext: false, + includeBootstrapInRuntimeContext: false, + }); + expect(resolveBootstrapContextTargets({ bootstrapMode: "none" })).toEqual({ + includeBootstrapInSystemContext: false, + includeBootstrapInRuntimeContext: false, + }); }); }); diff --git a/src/agents/pi-embedded-runner/run/attempt.ts b/src/agents/pi-embedded-runner/run/attempt.ts index f005c582746..27a8e11e9d1 100644 --- a/src/agents/pi-embedded-runner/run/attempt.ts +++ b/src/agents/pi-embedded-runner/run/attempt.ts @@ -32,7 +32,6 @@ import { import { resolveProviderSystemPromptContribution, resolveProviderTextTransforms, - transformProviderSystemPrompt, } from "../../../plugins/provider-runtime.js"; import { getPluginToolMeta } from "../../../plugins/tools.js"; import { isAcpSessionKey, isSubagentSessionKey } from "../../../routing/session-key.js"; @@ -155,7 +154,6 @@ import { import { resolveSystemPromptOverride } from "../../system-prompt-override.js"; import { buildSystemPromptParams } from "../../system-prompt-params.js"; import { buildSystemPromptReport } from "../../system-prompt-report.js"; -import { appendAgentBootstrapSystemPromptSupplement } from "../../system-prompt.js"; import { resolveAgentTimeoutMs } from "../../timeout.js"; import { buildEmptyExplicitToolAllowlistError, @@ -213,11 +211,7 @@ import { resolveEmbeddedAgentBaseStreamFn, resolveEmbeddedAgentStreamFn, } from "../stream-resolution.js"; -import { - applySystemPromptOverrideToSession, - buildEmbeddedSystemPrompt, - createSystemPromptOverride, -} from "../system-prompt.js"; +import { applySystemPromptOverrideToSession } from "../system-prompt.js"; import { dropReasoningFromHistory, dropThinkingBlocks } from "../thinking.js"; import { collectAllowedToolNames, @@ -239,21 +233,18 @@ import { flushPendingToolResultsAfterIdle } from "../wait-for-idle-before-flush. import { abortable as abortableWithSignal } from "./abortable.js"; import { createEmbeddedAgentSessionWithResourceLoader } from "./attempt-session.js"; export { buildContextEnginePromptCacheInfo } from "./attempt.context-engine-helpers.js"; -import { - resolveAttemptWorkspaceBootstrapRouting, - shouldStripBootstrapFromEmbeddedContext, -} from "./attempt-bootstrap-routing.js"; -export { shouldStripBootstrapFromEmbeddedContext } from "./attempt-bootstrap-routing.js"; import { rotateTranscriptAfterCompaction, shouldRotateCompactionTranscript, } from "../compaction-successor-transcript.js"; +import { resolveAttemptWorkspaceBootstrapRouting } from "./attempt-bootstrap-routing.js"; import { configureEmbeddedAttemptHttpRuntime } from "./attempt-http-runtime.js"; import { createEmbeddedRunStageTracker, formatEmbeddedRunStageSummary, shouldWarnEmbeddedRunStageSummary, } from "./attempt-stage-timing.js"; +import { buildAttemptSystemPrompt } from "./attempt-system-prompt.js"; import { assembleAttemptContextEngine, buildLoopPromptCacheInfo, @@ -958,7 +949,6 @@ export async function runEmbeddedAttempt( hasBootstrapFileAccess: bootstrapHasFileAccess, }); const bootstrapMode = bootstrapRouting.bootstrapMode; - const shouldStripBootstrapFromContext = bootstrapRouting.shouldStripBootstrapFromContext; const { bootstrapFiles: hookAdjustedBootstrapFiles, contextFiles: resolvedContextFiles, @@ -993,12 +983,12 @@ export async function runEmbeddedAttempt( sourceWorkspaceDir: resolvedWorkspace, targetWorkspaceDir: effectiveWorkspace, }); - const contextFiles = shouldStripBootstrapFromContext - ? remappedContextFiles.filter((file) => !/(^|[\\/])BOOTSTRAP\.md$/iu.test(file.path.trim())) - : remappedContextFiles; - const bootstrapFilesForInjectionStats = shouldStripBootstrapFromContext - ? hookAdjustedBootstrapFiles.filter((file) => file.name !== DEFAULT_BOOTSTRAP_FILENAME) - : hookAdjustedBootstrapFiles; + const contextFiles = bootstrapRouting.includeBootstrapInSystemContext + ? remappedContextFiles + : remappedContextFiles.filter((file) => !/(^|[\\/])BOOTSTRAP\.md$/iu.test(file.path.trim())); + const bootstrapFilesForInjectionStats = bootstrapRouting.includeBootstrapInSystemContext + ? hookAdjustedBootstrapFiles + : hookAdjustedBootstrapFiles.filter((file) => file.name !== DEFAULT_BOOTSTRAP_FILENAME); const bootstrapMaxChars = resolveBootstrapMaxChars(params.config); const bootstrapTotalMaxChars = resolveBootstrapTotalMaxChars(params.config); const bootstrapAnalysis = analyzeBootstrapBudget({ @@ -1291,71 +1281,66 @@ export async function runEmbeddedAttempt( config: params.config, agentId: sessionAgentId, }); - const builtAppendPrompt = systemPromptOverrideText - ? appendAgentBootstrapSystemPromptSupplement({ - systemPrompt: systemPromptOverrideText, - bootstrapMode, - bootstrapTruncationNotice, - contextFiles, - }) - : buildEmbeddedSystemPrompt({ - workspaceDir: effectiveWorkspace, - defaultThinkLevel: params.thinkLevel, - reasoningLevel: params.reasoningLevel ?? "off", - extraSystemPrompt: params.extraSystemPrompt, - ownerNumbers: params.ownerNumbers, - ownerDisplay: ownerDisplay.ownerDisplay, - ownerDisplaySecret: ownerDisplay.ownerDisplaySecret, - reasoningTagHint, - heartbeatPrompt, - skillsPrompt: effectiveSkillsPrompt, - docsPath: openClawReferences.docsPath ?? undefined, - sourcePath: openClawReferences.sourcePath ?? undefined, - ttsHint, - workspaceNotes: workspaceNotes?.length ? workspaceNotes : undefined, - reactionGuidance, - promptMode: effectivePromptMode, - sourceReplyDeliveryMode: params.sourceReplyDeliveryMode, - silentReplyPromptMode: params.silentReplyPromptMode, - acpEnabled: isAcpRuntimeSpawnAvailable({ - config: params.config, - sandboxed: sandboxInfo?.enabled === true, - }), - nativeCommandGuidanceLines: listRegisteredPluginAgentPromptGuidance(), - runtimeInfo, - messageToolHints, - sandboxInfo, - tools: effectiveTools, - modelAliasLines: buildModelAliasLines(params.config), - userTimezone, - userTime, - userTimeFormat, - contextFiles, - bootstrapMode, - bootstrapTruncationNotice, - includeMemorySection: !activeContextEngine || activeContextEngine.info.id === "legacy", - memoryCitationsMode: params.config?.memory?.citations, - promptContribution, - }); - const appendPrompt = isRawModelRun - ? "" - : transformProviderSystemPrompt({ - provider: params.provider, + const attemptSystemPrompt = buildAttemptSystemPrompt({ + isRawModelRun, + systemPromptOverrideText, + embeddedSystemPrompt: { + workspaceDir: effectiveWorkspace, + defaultThinkLevel: params.thinkLevel, + reasoningLevel: params.reasoningLevel ?? "off", + extraSystemPrompt: params.extraSystemPrompt, + ownerNumbers: params.ownerNumbers, + ownerDisplay: ownerDisplay.ownerDisplay, + ownerDisplaySecret: ownerDisplay.ownerDisplaySecret, + reasoningTagHint, + heartbeatPrompt, + skillsPrompt: effectiveSkillsPrompt, + docsPath: openClawReferences.docsPath ?? undefined, + sourcePath: openClawReferences.sourcePath ?? undefined, + ttsHint, + workspaceNotes: workspaceNotes?.length ? workspaceNotes : undefined, + reactionGuidance, + promptMode: effectivePromptMode, + sourceReplyDeliveryMode: params.sourceReplyDeliveryMode, + silentReplyPromptMode: params.silentReplyPromptMode, + acpEnabled: isAcpRuntimeSpawnAvailable({ config: params.config, + sandboxed: sandboxInfo?.enabled === true, + }), + nativeCommandGuidanceLines: listRegisteredPluginAgentPromptGuidance(), + runtimeInfo, + messageToolHints, + sandboxInfo, + tools: effectiveTools, + modelAliasLines: buildModelAliasLines(params.config), + userTimezone, + userTime, + userTimeFormat, + contextFiles, + bootstrapMode, + bootstrapTruncationNotice, + includeMemorySection: !activeContextEngine || activeContextEngine.info.id === "legacy", + memoryCitationsMode: params.config?.memory?.citations, + promptContribution, + }, + providerTransform: { + provider: params.provider, + config: params.config, + workspaceDir: effectiveWorkspace, + context: { + config: params.config, + agentDir: params.agentDir, workspaceDir: effectiveWorkspace, - context: { - config: params.config, - agentDir: params.agentDir, - workspaceDir: effectiveWorkspace, - provider: params.provider, - modelId: params.modelId, - promptMode: effectivePromptMode, - runtimeChannel, - runtimeCapabilities, - agentId: sessionAgentId, - systemPrompt: builtAppendPrompt, - }, - }); + provider: params.provider, + modelId: params.modelId, + promptMode: effectivePromptMode, + runtimeChannel, + runtimeCapabilities, + agentId: sessionAgentId, + }, + }, + }); + const appendPrompt = attemptSystemPrompt.systemPrompt; const systemPromptReport = buildSystemPromptReport({ source: "run", generatedAt: Date.now(), @@ -1384,7 +1369,7 @@ export async function runEmbeddedAttempt( skillsPrompt, tools: effectiveTools, }); - const systemPromptOverride = createSystemPromptOverride(appendPrompt); + const systemPromptOverride = attemptSystemPrompt.systemPromptOverride; let systemPromptText = systemPromptOverride(); prepStages.mark("system-prompt"); diff --git a/src/agents/system-prompt.test.ts b/src/agents/system-prompt.test.ts index 12510992595..0a36cbca762 100644 --- a/src/agents/system-prompt.test.ts +++ b/src/agents/system-prompt.test.ts @@ -6,6 +6,7 @@ import { SYSTEM_PROMPT_CACHE_BOUNDARY } from "./system-prompt-cache-boundary.js" import { appendAgentBootstrapSystemPromptSupplement, buildAgentBootstrapSystemContext, + buildAgentBootstrapSystemPromptSections, buildAgentBootstrapSystemPromptSupplement, buildAgentSystemPrompt, buildRuntimeLine, @@ -1111,6 +1112,22 @@ describe("buildAgentBootstrapSystemContext", () => { }); describe("buildAgentBootstrapSystemPromptSupplement", () => { + 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." }], + includeProjectContext: false, + }).join("\n"); + + expect(sections).toContain("## Bootstrap Pending"); + expect(sections).toContain("BOOTSTRAP.md is included below in Project Context"); + 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."); + }); + it("adds pending bootstrap guidance and BOOTSTRAP.md contents for override prompts", () => { const supplement = buildAgentBootstrapSystemPromptSupplement({ bootstrapMode: "full", diff --git a/src/agents/system-prompt.ts b/src/agents/system-prompt.ts index 82893f83e04..9d87d2a2b3f 100644 --- a/src/agents/system-prompt.ts +++ b/src/agents/system-prompt.ts @@ -264,6 +264,21 @@ export function buildAgentBootstrapSystemPromptSupplement(params: { bootstrapTruncationNotice?: string; contextFiles?: EmbeddedContextFile[]; }): string | undefined { + const supplement = buildAgentBootstrapSystemPromptSections({ + ...params, + includeProjectContext: true, + }) + .join("\n") + .trim(); + return supplement.length > 0 ? supplement : undefined; +} + +export function buildAgentBootstrapSystemPromptSections(params: { + bootstrapMode?: BootstrapMode; + bootstrapTruncationNotice?: string; + contextFiles?: EmbeddedContextFile[]; + includeProjectContext?: boolean; +}): string[] { const bootstrapFiles = params.bootstrapMode === "full" ? sortContextFilesForPrompt(params.contextFiles ?? []).filter((file) => @@ -280,7 +295,7 @@ export function buildAgentBootstrapSystemPromptSupplement(params: { if (bootstrapTruncationNotice) { lines.push("## Bootstrap Context Notice", bootstrapTruncationNotice, ""); } - if (bootstrapFiles.length > 0) { + if (params.includeProjectContext === true && bootstrapFiles.length > 0) { lines.push( ...buildProjectContextSection({ files: bootstrapFiles, @@ -289,8 +304,7 @@ export function buildAgentBootstrapSystemPromptSupplement(params: { }), ); } - const supplement = lines.join("\n").trim(); - return supplement.length > 0 ? supplement : undefined; + return lines; } export function appendAgentBootstrapSystemPromptSupplement(params: { @@ -819,14 +833,12 @@ export function buildAgentSystemPrompt(params: { const orderedContextFiles = sortContextFilesForPrompt(validContextFiles); const stableContextFiles = orderedContextFiles.filter((file) => !isDynamicContextFile(file.path)); const dynamicContextFiles = orderedContextFiles.filter((file) => isDynamicContextFile(file.path)); - const hasBootstrapFileInProjectContext = orderedContextFiles.some((file) => - isBootstrapContextFile(file.path), - ); - const bootstrapSystemContext = buildAgentBootstrapSystemContext({ + const bootstrapSystemPromptSections = buildAgentBootstrapSystemPromptSections({ bootstrapMode: params.bootstrapMode, - hasBootstrapFileInProjectContext, + bootstrapTruncationNotice: params.bootstrapTruncationNotice, + contextFiles: orderedContextFiles, + includeProjectContext: false, }); - const bootstrapTruncationNotice = params.bootstrapTruncationNotice?.trim(); const stablePrefixCacheKey = hashStablePromptInput({ workspaceDir: params.workspaceDir, promptMode, @@ -853,8 +865,7 @@ export function buildAgentSystemPrompt(params: { workspaceGuidance, workspaceNotes, bootstrapMode: params.bootstrapMode, - bootstrapSystemContext, - bootstrapTruncationNotice, + bootstrapSystemPromptSections, docsPath: params.docsPath, sourcePath: params.sourcePath, skillsPrompt, @@ -1063,10 +1074,7 @@ export function buildAgentSystemPrompt(params: { ...buildTimeSection({ userTimezone, }), - ...bootstrapSystemContext, - bootstrapTruncationNotice ? "## Bootstrap Context Notice" : "", - bootstrapTruncationNotice ?? "", - bootstrapTruncationNotice ? "" : "", + ...bootstrapSystemPromptSections, "## Workspace Files (injected)", "These user-editable files are loaded by OpenClaw and included below in Project Context.", "",