mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-17 21:10:54 +00:00
Agents: move bootstrap warnings out of system prompt (#48753)
Merged via squash.
Prepared head SHA: dc1d4d075a
Co-authored-by: scoootscooob <167050519+scoootscooob@users.noreply.github.com>
Reviewed-by: @scoootscooob
This commit is contained in:
@@ -115,6 +115,10 @@ Docs: https://docs.openclaw.ai
|
||||
- macOS/exec approvals: harden exec-host request HMAC verification to use a timing-safe compare and keep malformed or truncated signatures fail-closed in focused IPC auth coverage.
|
||||
- Gateway/exec approvals: surface requested env override keys in gateway-host approval prompts so operators can review surviving env context without inheriting noisy base host env.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Agents/bootstrap warnings: move bootstrap truncation warnings out of the system prompt and into the per-turn prompt body so prompt-cache reuse stays stable when truncation warnings appear or disappear. (#48753) Thanks @scoootscooob and @obviyus.
|
||||
|
||||
## 2026.3.13
|
||||
|
||||
### Changes
|
||||
|
||||
@@ -6,8 +6,10 @@ import {
|
||||
buildBootstrapTruncationReportMeta,
|
||||
buildBootstrapTruncationSignature,
|
||||
formatBootstrapTruncationWarningLines,
|
||||
prependBootstrapPromptWarning,
|
||||
resolveBootstrapWarningSignaturesSeen,
|
||||
} from "./bootstrap-budget.js";
|
||||
import { buildAgentSystemPrompt } from "./system-prompt.js";
|
||||
import type { WorkspaceBootstrapFile } from "./workspace.js";
|
||||
|
||||
describe("buildBootstrapInjectionStats", () => {
|
||||
@@ -104,6 +106,34 @@ describe("analyzeBootstrapBudget", () => {
|
||||
});
|
||||
|
||||
describe("bootstrap prompt warnings", () => {
|
||||
it("prepends warning details to the turn prompt instead of mutating the system prompt", () => {
|
||||
const prompt = prependBootstrapPromptWarning("Please continue.", [
|
||||
"AGENTS.md: 200 raw -> 0 injected",
|
||||
]);
|
||||
expect(prompt).toContain("[Bootstrap truncation warning]");
|
||||
expect(prompt).toContain("Treat Project Context as partial");
|
||||
expect(prompt).toContain("- AGENTS.md: 200 raw -> 0 injected");
|
||||
expect(prompt).toContain("Please continue.");
|
||||
});
|
||||
|
||||
it("preserves raw prompt whitespace when prepending warning details", () => {
|
||||
const prompt = prependBootstrapPromptWarning(" indented\nkeep tail ", [
|
||||
"AGENTS.md: 200 raw -> 0 injected",
|
||||
]);
|
||||
|
||||
expect(prompt.endsWith(" indented\nkeep tail ")).toBe(true);
|
||||
});
|
||||
|
||||
it("preserves exact heartbeat prompts without warning prefixes", () => {
|
||||
const heartbeatPrompt = "Read HEARTBEAT.md. Reply HEARTBEAT_OK.";
|
||||
|
||||
expect(
|
||||
prependBootstrapPromptWarning(heartbeatPrompt, ["AGENTS.md: 200 raw -> 0 injected"], {
|
||||
preserveExactPrompt: heartbeatPrompt,
|
||||
}),
|
||||
).toBe(heartbeatPrompt);
|
||||
});
|
||||
|
||||
it("resolves seen signatures from report history or legacy single signature", () => {
|
||||
expect(
|
||||
resolveBootstrapWarningSignaturesSeen({
|
||||
@@ -394,4 +424,35 @@ describe("bootstrap prompt warnings", () => {
|
||||
expect(meta.promptWarningSignature).toBeTruthy();
|
||||
expect(meta.warningSignaturesSeen?.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("improves cache-relevant system prompt stability versus legacy warning injection", () => {
|
||||
const contextFiles = [{ path: "AGENTS.md", content: "Follow AGENTS guidance." }];
|
||||
const warningLines = ["AGENTS.md: 200 raw -> 0 injected"];
|
||||
const stableSystemPrompt = buildAgentSystemPrompt({
|
||||
workspaceDir: "/tmp/openclaw",
|
||||
contextFiles,
|
||||
});
|
||||
const optimizedTurns = [stableSystemPrompt, stableSystemPrompt, stableSystemPrompt];
|
||||
const injectLegacyWarning = (prompt: string, lines: string[]) => {
|
||||
const warningBlock = [
|
||||
"⚠ Bootstrap truncation warning:",
|
||||
...lines.map((line) => `- ${line}`),
|
||||
"",
|
||||
].join("\n");
|
||||
return prompt.replace("## AGENTS.md", `${warningBlock}## AGENTS.md`);
|
||||
};
|
||||
const legacyTurns = [
|
||||
injectLegacyWarning(optimizedTurns[0] ?? "", warningLines),
|
||||
optimizedTurns[1] ?? "",
|
||||
injectLegacyWarning(optimizedTurns[2] ?? "", warningLines),
|
||||
];
|
||||
const cacheHitRate = (turns: string[]) => {
|
||||
const hits = turns.slice(1).filter((turn, index) => turn === turns[index]).length;
|
||||
return hits / Math.max(1, turns.length - 1);
|
||||
};
|
||||
|
||||
expect(cacheHitRate(legacyTurns)).toBe(0);
|
||||
expect(cacheHitRate(optimizedTurns)).toBe(1);
|
||||
expect(optimizedTurns[0]).not.toContain("⚠ Bootstrap truncation warning:");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -330,6 +330,29 @@ export function buildBootstrapPromptWarning(params: {
|
||||
};
|
||||
}
|
||||
|
||||
export function prependBootstrapPromptWarning(
|
||||
prompt: string,
|
||||
warningLines?: string[],
|
||||
options?: {
|
||||
preserveExactPrompt?: string;
|
||||
},
|
||||
): string {
|
||||
const normalizedLines = (warningLines ?? []).map((line) => line.trim()).filter(Boolean);
|
||||
if (normalizedLines.length === 0) {
|
||||
return prompt;
|
||||
}
|
||||
if (options?.preserveExactPrompt && prompt === options.preserveExactPrompt) {
|
||||
return prompt;
|
||||
}
|
||||
const warningBlock = [
|
||||
"[Bootstrap truncation warning]",
|
||||
"Some workspace bootstrap files were truncated before injection.",
|
||||
"Treat Project Context as partial and read the relevant files directly if details seem missing.",
|
||||
...normalizedLines.map((line) => `- ${line}`),
|
||||
].join("\n");
|
||||
return prompt ? `${warningBlock}\n\n${prompt}` : warningBlock;
|
||||
}
|
||||
|
||||
export function buildBootstrapTruncationReportMeta(params: {
|
||||
analysis: BootstrapBudgetAnalysis;
|
||||
warningMode: BootstrapPromptWarningMode;
|
||||
|
||||
@@ -5,10 +5,25 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { runCliAgent } from "./cli-runner.js";
|
||||
import { resolveCliNoOutputTimeoutMs } from "./cli-runner/helpers.js";
|
||||
import type { EmbeddedContextFile } from "./pi-embedded-helpers.js";
|
||||
import type { WorkspaceBootstrapFile } from "./workspace.js";
|
||||
|
||||
const supervisorSpawnMock = vi.fn();
|
||||
const enqueueSystemEventMock = vi.fn();
|
||||
const requestHeartbeatNowMock = vi.fn();
|
||||
const hoisted = vi.hoisted(() => {
|
||||
type BootstrapContext = {
|
||||
bootstrapFiles: WorkspaceBootstrapFile[];
|
||||
contextFiles: EmbeddedContextFile[];
|
||||
};
|
||||
|
||||
return {
|
||||
resolveBootstrapContextForRunMock: vi.fn<() => Promise<BootstrapContext>>(async () => ({
|
||||
bootstrapFiles: [],
|
||||
contextFiles: [],
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../process/supervisor/index.js", () => ({
|
||||
getProcessSupervisor: () => ({
|
||||
@@ -28,6 +43,11 @@ vi.mock("../infra/heartbeat-wake.js", () => ({
|
||||
requestHeartbeatNow: (...args: unknown[]) => requestHeartbeatNowMock(...args),
|
||||
}));
|
||||
|
||||
vi.mock("./bootstrap-files.js", () => ({
|
||||
makeBootstrapWarn: () => () => {},
|
||||
resolveBootstrapContextForRun: hoisted.resolveBootstrapContextForRunMock,
|
||||
}));
|
||||
|
||||
type MockRunExit = {
|
||||
reason:
|
||||
| "manual-cancel"
|
||||
@@ -61,6 +81,10 @@ describe("runCliAgent with process supervisor", () => {
|
||||
supervisorSpawnMock.mockClear();
|
||||
enqueueSystemEventMock.mockClear();
|
||||
requestHeartbeatNowMock.mockClear();
|
||||
hoisted.resolveBootstrapContextForRunMock.mockReset().mockResolvedValue({
|
||||
bootstrapFiles: [],
|
||||
contextFiles: [],
|
||||
});
|
||||
});
|
||||
|
||||
it("runs CLI through supervisor and returns payload", async () => {
|
||||
@@ -107,6 +131,62 @@ describe("runCliAgent with process supervisor", () => {
|
||||
expect(input.scopeKey).toContain("thread-123");
|
||||
});
|
||||
|
||||
it("prepends bootstrap warnings to the CLI prompt body", async () => {
|
||||
supervisorSpawnMock.mockResolvedValueOnce(
|
||||
createManagedRun({
|
||||
reason: "exit",
|
||||
exitCode: 0,
|
||||
exitSignal: null,
|
||||
durationMs: 50,
|
||||
stdout: "ok",
|
||||
stderr: "",
|
||||
timedOut: false,
|
||||
noOutputTimedOut: false,
|
||||
}),
|
||||
);
|
||||
hoisted.resolveBootstrapContextForRunMock.mockResolvedValueOnce({
|
||||
bootstrapFiles: [
|
||||
{
|
||||
name: "AGENTS.md",
|
||||
path: "/tmp/AGENTS.md",
|
||||
content: "A".repeat(200),
|
||||
missing: false,
|
||||
},
|
||||
],
|
||||
contextFiles: [{ path: "AGENTS.md", content: "A".repeat(20) }],
|
||||
});
|
||||
|
||||
await runCliAgent({
|
||||
sessionId: "s1",
|
||||
sessionFile: "/tmp/session.jsonl",
|
||||
workspaceDir: "/tmp",
|
||||
config: {
|
||||
agents: {
|
||||
defaults: {
|
||||
bootstrapMaxChars: 50,
|
||||
bootstrapTotalMaxChars: 50,
|
||||
},
|
||||
},
|
||||
} satisfies OpenClawConfig,
|
||||
prompt: "hi",
|
||||
provider: "codex-cli",
|
||||
model: "gpt-5.2-codex",
|
||||
timeoutMs: 1_000,
|
||||
runId: "run-warning",
|
||||
cliSessionId: "thread-123",
|
||||
});
|
||||
|
||||
const input = supervisorSpawnMock.mock.calls[0]?.[0] as {
|
||||
argv?: string[];
|
||||
input?: string;
|
||||
};
|
||||
const promptCarrier = [input.input ?? "", ...(input.argv ?? [])].join("\n");
|
||||
|
||||
expect(promptCarrier).toContain("[Bootstrap truncation warning]");
|
||||
expect(promptCarrier).toContain("- AGENTS.md: 200 raw -> 20 injected");
|
||||
expect(promptCarrier).toContain("hi");
|
||||
});
|
||||
|
||||
it("fails with timeout when no-output watchdog trips", async () => {
|
||||
supervisorSpawnMock.mockResolvedValueOnce(
|
||||
createManagedRun({
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
buildBootstrapInjectionStats,
|
||||
buildBootstrapPromptWarning,
|
||||
buildBootstrapTruncationReportMeta,
|
||||
prependBootstrapPromptWarning,
|
||||
} from "./bootstrap-budget.js";
|
||||
import { makeBootstrapWarn, resolveBootstrapContextForRun } from "./bootstrap-files.js";
|
||||
import { resolveCliBackendConfig } from "./cli-backends.js";
|
||||
@@ -162,7 +163,6 @@ export async function runCliAgent(params: {
|
||||
docsPath: docsPath ?? undefined,
|
||||
tools: [],
|
||||
contextFiles,
|
||||
bootstrapTruncationWarningLines: bootstrapPromptWarning.lines,
|
||||
modelDisplay,
|
||||
agentId: sessionAgentId,
|
||||
});
|
||||
@@ -218,7 +218,9 @@ export async function runCliAgent(params: {
|
||||
|
||||
let imagePaths: string[] | undefined;
|
||||
let cleanupImages: (() => Promise<void>) | undefined;
|
||||
let prompt = params.prompt;
|
||||
let prompt = prependBootstrapPromptWarning(params.prompt, bootstrapPromptWarning.lines, {
|
||||
preserveExactPrompt: heartbeatPrompt,
|
||||
});
|
||||
if (params.images && params.images.length > 0) {
|
||||
const imagePayload = await writeCliImages(params.images);
|
||||
imagePaths = imagePayload.paths;
|
||||
|
||||
@@ -48,7 +48,6 @@ export function buildSystemPrompt(params: {
|
||||
docsPath?: string;
|
||||
tools: AgentTool[];
|
||||
contextFiles?: EmbeddedContextFile[];
|
||||
bootstrapTruncationWarningLines?: string[];
|
||||
modelDisplay: string;
|
||||
agentId?: string;
|
||||
}) {
|
||||
@@ -92,7 +91,6 @@ export function buildSystemPrompt(params: {
|
||||
userTime,
|
||||
userTimeFormat,
|
||||
contextFiles: params.contextFiles,
|
||||
bootstrapTruncationWarningLines: params.bootstrapTruncationWarningLines,
|
||||
ttsHint,
|
||||
memoryCitationsMode: params.config?.memory?.citations,
|
||||
});
|
||||
|
||||
@@ -18,16 +18,27 @@ import type {
|
||||
IngestBatchResult,
|
||||
IngestResult,
|
||||
} from "../../../context-engine/types.js";
|
||||
import type { EmbeddedContextFile } from "../../pi-embedded-helpers.js";
|
||||
import { createHostSandboxFsBridge } from "../../test-helpers/host-sandbox-fs-bridge.js";
|
||||
import { createPiToolsSandboxContext } from "../../test-helpers/pi-tools-sandbox-context.js";
|
||||
import type { WorkspaceBootstrapFile } from "../../workspace.js";
|
||||
|
||||
const hoisted = vi.hoisted(() => {
|
||||
type BootstrapContext = {
|
||||
bootstrapFiles: WorkspaceBootstrapFile[];
|
||||
contextFiles: EmbeddedContextFile[];
|
||||
};
|
||||
const spawnSubagentDirectMock = vi.fn();
|
||||
const createAgentSessionMock = vi.fn();
|
||||
const sessionManagerOpenMock = vi.fn();
|
||||
const resolveSandboxContextMock = vi.fn();
|
||||
const subscribeEmbeddedPiSessionMock = vi.fn();
|
||||
const acquireSessionWriteLockMock = vi.fn();
|
||||
const resolveBootstrapContextForRunMock = vi.fn<() => Promise<BootstrapContext>>(async () => ({
|
||||
bootstrapFiles: [],
|
||||
contextFiles: [],
|
||||
}));
|
||||
const getGlobalHookRunnerMock = vi.fn<() => unknown>(() => undefined);
|
||||
const sessionManager = {
|
||||
getLeafEntry: vi.fn(() => null),
|
||||
branch: vi.fn(),
|
||||
@@ -42,6 +53,8 @@ const hoisted = vi.hoisted(() => {
|
||||
resolveSandboxContextMock,
|
||||
subscribeEmbeddedPiSessionMock,
|
||||
acquireSessionWriteLockMock,
|
||||
resolveBootstrapContextForRunMock,
|
||||
getGlobalHookRunnerMock,
|
||||
sessionManager,
|
||||
};
|
||||
});
|
||||
@@ -80,7 +93,7 @@ vi.mock("../../pi-embedded-subscribe.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("../../../plugins/hook-runner-global.js", () => ({
|
||||
getGlobalHookRunner: () => undefined,
|
||||
getGlobalHookRunner: hoisted.getGlobalHookRunnerMock,
|
||||
}));
|
||||
|
||||
vi.mock("../../../infra/machine-name.js", () => ({
|
||||
@@ -94,7 +107,7 @@ vi.mock("../../../infra/net/undici-global-dispatcher.js", () => ({
|
||||
|
||||
vi.mock("../../bootstrap-files.js", () => ({
|
||||
makeBootstrapWarn: () => () => {},
|
||||
resolveBootstrapContextForRun: async () => ({ bootstrapFiles: [], contextFiles: [] }),
|
||||
resolveBootstrapContextForRun: hoisted.resolveBootstrapContextForRunMock,
|
||||
}));
|
||||
|
||||
vi.mock("../../skills.js", () => ({
|
||||
@@ -269,6 +282,11 @@ function resetEmbeddedAttemptHarness(
|
||||
hoisted.acquireSessionWriteLockMock.mockReset().mockResolvedValue({
|
||||
release: async () => {},
|
||||
});
|
||||
hoisted.resolveBootstrapContextForRunMock.mockReset().mockResolvedValue({
|
||||
bootstrapFiles: [],
|
||||
contextFiles: [],
|
||||
});
|
||||
hoisted.getGlobalHookRunnerMock.mockReset().mockReturnValue(undefined);
|
||||
hoisted.sessionManager.getLeafEntry.mockReset().mockReturnValue(null);
|
||||
hoisted.sessionManager.branch.mockReset();
|
||||
hoisted.sessionManager.resetLeaf.mockReset();
|
||||
@@ -291,7 +309,11 @@ async function cleanupTempPaths(tempPaths: string[]) {
|
||||
}
|
||||
|
||||
function createDefaultEmbeddedSession(params?: {
|
||||
prompt?: (session: MutableSession) => Promise<void>;
|
||||
prompt?: (
|
||||
session: MutableSession,
|
||||
prompt: string,
|
||||
options?: { images?: unknown[] },
|
||||
) => Promise<void>;
|
||||
}): MutableSession {
|
||||
const session: MutableSession = {
|
||||
sessionId: "embedded-session",
|
||||
@@ -303,9 +325,9 @@ function createDefaultEmbeddedSession(params?: {
|
||||
session.messages = [...messages];
|
||||
},
|
||||
},
|
||||
prompt: async () => {
|
||||
prompt: async (prompt, options) => {
|
||||
if (params?.prompt) {
|
||||
await params.prompt(session);
|
||||
await params.prompt(session, prompt, options);
|
||||
return;
|
||||
}
|
||||
session.messages = [
|
||||
@@ -450,6 +472,90 @@ describe("runEmbeddedAttempt sessions_spawn workspace inheritance", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("runEmbeddedAttempt bootstrap warning prompt assembly", () => {
|
||||
const tempPaths: string[] = [];
|
||||
|
||||
beforeEach(() => {
|
||||
resetEmbeddedAttemptHarness({
|
||||
subscribeImpl: createSubscriptionMock,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await cleanupTempPaths(tempPaths);
|
||||
});
|
||||
|
||||
it("keeps bootstrap warnings in the sent prompt after hook prepend context", async () => {
|
||||
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-warning-workspace-"));
|
||||
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-warning-agent-dir-"));
|
||||
const sessionFile = path.join(workspaceDir, "session.jsonl");
|
||||
tempPaths.push(workspaceDir, agentDir);
|
||||
await fs.writeFile(sessionFile, "", "utf8");
|
||||
|
||||
hoisted.resolveBootstrapContextForRunMock.mockResolvedValue({
|
||||
bootstrapFiles: [
|
||||
{
|
||||
name: "AGENTS.md",
|
||||
path: path.join(workspaceDir, "AGENTS.md"),
|
||||
content: "A".repeat(200),
|
||||
missing: false,
|
||||
},
|
||||
],
|
||||
contextFiles: [{ path: "AGENTS.md", content: "A".repeat(20) }],
|
||||
});
|
||||
hoisted.getGlobalHookRunnerMock.mockReturnValue({
|
||||
hasHooks: (hookName: string) => hookName === "before_prompt_build",
|
||||
runBeforePromptBuild: async () => ({ prependContext: "hook context" }),
|
||||
});
|
||||
|
||||
let seenPrompt = "";
|
||||
hoisted.createAgentSessionMock.mockImplementation(async () => ({
|
||||
session: createDefaultEmbeddedSession({
|
||||
prompt: async (session, prompt) => {
|
||||
seenPrompt = prompt;
|
||||
session.messages = [
|
||||
...session.messages,
|
||||
{ role: "assistant", content: "done", timestamp: 2 },
|
||||
];
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
const result = await runEmbeddedAttempt({
|
||||
sessionId: "embedded-session",
|
||||
sessionKey: "agent:main:main",
|
||||
sessionFile,
|
||||
workspaceDir,
|
||||
agentDir,
|
||||
config: {
|
||||
agents: {
|
||||
defaults: {
|
||||
bootstrapMaxChars: 50,
|
||||
bootstrapTotalMaxChars: 50,
|
||||
},
|
||||
},
|
||||
},
|
||||
prompt: "hello",
|
||||
timeoutMs: 10_000,
|
||||
runId: "run-warning",
|
||||
provider: "openai",
|
||||
modelId: "gpt-test",
|
||||
model: testModel,
|
||||
authStorage: {} as AuthStorage,
|
||||
modelRegistry: {} as ModelRegistry,
|
||||
thinkLevel: "off",
|
||||
senderIsOwner: true,
|
||||
disableMessageTool: true,
|
||||
});
|
||||
|
||||
expect(result.promptError).toBeNull();
|
||||
expect(seenPrompt).toContain("hook context");
|
||||
expect(seenPrompt).toContain("[Bootstrap truncation warning]");
|
||||
expect(seenPrompt).toContain("- AGENTS.md: 200 raw -> 20 injected");
|
||||
expect(seenPrompt).toContain("hello");
|
||||
});
|
||||
});
|
||||
|
||||
describe("runEmbeddedAttempt cache-ttl tracking after compaction", () => {
|
||||
const tempPaths: string[] = [];
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ import {
|
||||
buildBootstrapPromptWarning,
|
||||
buildBootstrapTruncationReportMeta,
|
||||
buildBootstrapInjectionStats,
|
||||
prependBootstrapPromptWarning,
|
||||
} from "../../bootstrap-budget.js";
|
||||
import { makeBootstrapWarn, resolveBootstrapContextForRun } from "../../bootstrap-files.js";
|
||||
import { createCacheTrace } from "../../cache-trace.js";
|
||||
@@ -1665,6 +1666,9 @@ export async function runEmbeddedAttempt(
|
||||
});
|
||||
const ttsHint = params.config ? buildTtsSystemPromptHint(params.config) : undefined;
|
||||
const ownerDisplay = resolveOwnerDisplaySetting(params.config);
|
||||
const heartbeatPrompt = isDefaultAgent
|
||||
? resolveHeartbeatPrompt(params.config?.agents?.defaults?.heartbeat?.prompt)
|
||||
: undefined;
|
||||
|
||||
const appendPrompt = buildEmbeddedSystemPrompt({
|
||||
workspaceDir: effectiveWorkspace,
|
||||
@@ -1675,9 +1679,7 @@ export async function runEmbeddedAttempt(
|
||||
ownerDisplay: ownerDisplay.ownerDisplay,
|
||||
ownerDisplaySecret: ownerDisplay.ownerDisplaySecret,
|
||||
reasoningTagHint,
|
||||
heartbeatPrompt: isDefaultAgent
|
||||
? resolveHeartbeatPrompt(params.config?.agents?.defaults?.heartbeat?.prompt)
|
||||
: undefined,
|
||||
heartbeatPrompt,
|
||||
skillsPrompt,
|
||||
docsPath: docsPath ?? undefined,
|
||||
ttsHint,
|
||||
@@ -1694,7 +1696,6 @@ export async function runEmbeddedAttempt(
|
||||
userTime,
|
||||
userTimeFormat,
|
||||
contextFiles,
|
||||
bootstrapTruncationWarningLines: bootstrapPromptWarning.lines,
|
||||
memoryCitationsMode: params.config?.memory?.citations,
|
||||
});
|
||||
const systemPromptReport = buildSystemPromptReport({
|
||||
@@ -2378,7 +2379,13 @@ export async function runEmbeddedAttempt(
|
||||
|
||||
// Run before_prompt_build hooks to allow plugins to inject prompt context.
|
||||
// Legacy compatibility: before_agent_start is also checked for context fields.
|
||||
let effectivePrompt = params.prompt;
|
||||
let effectivePrompt = prependBootstrapPromptWarning(
|
||||
params.prompt,
|
||||
bootstrapPromptWarning.lines,
|
||||
{
|
||||
preserveExactPrompt: heartbeatPrompt,
|
||||
},
|
||||
);
|
||||
const hookCtx = {
|
||||
agentId: hookAgentId,
|
||||
sessionKey: params.sessionKey,
|
||||
@@ -2397,7 +2404,7 @@ export async function runEmbeddedAttempt(
|
||||
});
|
||||
{
|
||||
if (hookResult?.prependContext) {
|
||||
effectivePrompt = `${hookResult.prependContext}\n\n${params.prompt}`;
|
||||
effectivePrompt = `${hookResult.prependContext}\n\n${effectivePrompt}`;
|
||||
log.debug(
|
||||
`hooks: prepended context to prompt (${hookResult.prependContext.length} chars)`,
|
||||
);
|
||||
|
||||
@@ -51,7 +51,6 @@ export function buildEmbeddedSystemPrompt(params: {
|
||||
userTime?: string;
|
||||
userTimeFormat?: ResolvedTimeFormat;
|
||||
contextFiles?: EmbeddedContextFile[];
|
||||
bootstrapTruncationWarningLines?: string[];
|
||||
memoryCitationsMode?: MemoryCitationsMode;
|
||||
}): string {
|
||||
return buildAgentSystemPrompt({
|
||||
@@ -81,7 +80,6 @@ export function buildEmbeddedSystemPrompt(params: {
|
||||
userTime: params.userTime,
|
||||
userTimeFormat: params.userTimeFormat,
|
||||
contextFiles: params.contextFiles,
|
||||
bootstrapTruncationWarningLines: params.bootstrapTruncationWarningLines,
|
||||
memoryCitationsMode: params.memoryCitationsMode,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -534,16 +534,13 @@ describe("buildAgentSystemPrompt", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("renders bootstrap truncation warning even when no context files are injected", () => {
|
||||
it("omits project context when no context files are injected", () => {
|
||||
const prompt = buildAgentSystemPrompt({
|
||||
workspaceDir: "/tmp/openclaw",
|
||||
bootstrapTruncationWarningLines: ["AGENTS.md: 200 raw -> 0 injected"],
|
||||
contextFiles: [],
|
||||
});
|
||||
|
||||
expect(prompt).toContain("# Project Context");
|
||||
expect(prompt).toContain("⚠ Bootstrap truncation warning:");
|
||||
expect(prompt).toContain("- AGENTS.md: 200 raw -> 0 injected");
|
||||
expect(prompt).not.toContain("# Project Context");
|
||||
});
|
||||
|
||||
it("summarizes the message tool when available", () => {
|
||||
|
||||
@@ -202,7 +202,6 @@ export function buildAgentSystemPrompt(params: {
|
||||
userTime?: string;
|
||||
userTimeFormat?: ResolvedTimeFormat;
|
||||
contextFiles?: EmbeddedContextFile[];
|
||||
bootstrapTruncationWarningLines?: string[];
|
||||
skillsPrompt?: string;
|
||||
heartbeatPrompt?: string;
|
||||
docsPath?: string;
|
||||
@@ -614,13 +613,10 @@ export function buildAgentSystemPrompt(params: {
|
||||
}
|
||||
|
||||
const contextFiles = params.contextFiles ?? [];
|
||||
const bootstrapTruncationWarningLines = (params.bootstrapTruncationWarningLines ?? []).filter(
|
||||
(line) => line.trim().length > 0,
|
||||
);
|
||||
const validContextFiles = contextFiles.filter(
|
||||
(file) => typeof file.path === "string" && file.path.trim().length > 0,
|
||||
);
|
||||
if (validContextFiles.length > 0 || bootstrapTruncationWarningLines.length > 0) {
|
||||
if (validContextFiles.length > 0) {
|
||||
lines.push("# Project Context", "");
|
||||
if (validContextFiles.length > 0) {
|
||||
const hasSoulFile = validContextFiles.some((file) => {
|
||||
@@ -636,13 +632,6 @@ export function buildAgentSystemPrompt(params: {
|
||||
}
|
||||
lines.push("");
|
||||
}
|
||||
if (bootstrapTruncationWarningLines.length > 0) {
|
||||
lines.push("⚠ Bootstrap truncation warning:");
|
||||
for (const warningLine of bootstrapTruncationWarningLines) {
|
||||
lines.push(`- ${warningLine}`);
|
||||
}
|
||||
lines.push("");
|
||||
}
|
||||
for (const file of validContextFiles) {
|
||||
lines.push(`## ${file.path}`, "", file.content, "");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user