Agents: move bootstrap warnings out of system prompt (#48753)
Merged via squash. Prepared head SHA: dc1d4d075af7afdf4c143f1639cd49e129969f6c Co-authored-by: scoootscooob <167050519+scoootscooob@users.noreply.github.com> Reviewed-by: @scoootscooob
This commit is contained in:
parent
57204b4fa9
commit
80a2af1d65
@ -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.
|
- 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.
|
- 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
|
## 2026.3.13
|
||||||
|
|
||||||
### Changes
|
### Changes
|
||||||
|
|||||||
@ -6,8 +6,10 @@ import {
|
|||||||
buildBootstrapTruncationReportMeta,
|
buildBootstrapTruncationReportMeta,
|
||||||
buildBootstrapTruncationSignature,
|
buildBootstrapTruncationSignature,
|
||||||
formatBootstrapTruncationWarningLines,
|
formatBootstrapTruncationWarningLines,
|
||||||
|
prependBootstrapPromptWarning,
|
||||||
resolveBootstrapWarningSignaturesSeen,
|
resolveBootstrapWarningSignaturesSeen,
|
||||||
} from "./bootstrap-budget.js";
|
} from "./bootstrap-budget.js";
|
||||||
|
import { buildAgentSystemPrompt } from "./system-prompt.js";
|
||||||
import type { WorkspaceBootstrapFile } from "./workspace.js";
|
import type { WorkspaceBootstrapFile } from "./workspace.js";
|
||||||
|
|
||||||
describe("buildBootstrapInjectionStats", () => {
|
describe("buildBootstrapInjectionStats", () => {
|
||||||
@ -104,6 +106,34 @@ describe("analyzeBootstrapBudget", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("bootstrap prompt warnings", () => {
|
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", () => {
|
it("resolves seen signatures from report history or legacy single signature", () => {
|
||||||
expect(
|
expect(
|
||||||
resolveBootstrapWarningSignaturesSeen({
|
resolveBootstrapWarningSignaturesSeen({
|
||||||
@ -394,4 +424,35 @@ describe("bootstrap prompt warnings", () => {
|
|||||||
expect(meta.promptWarningSignature).toBeTruthy();
|
expect(meta.promptWarningSignature).toBeTruthy();
|
||||||
expect(meta.warningSignaturesSeen?.length).toBeGreaterThan(0);
|
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: {
|
export function buildBootstrapTruncationReportMeta(params: {
|
||||||
analysis: BootstrapBudgetAnalysis;
|
analysis: BootstrapBudgetAnalysis;
|
||||||
warningMode: BootstrapPromptWarningMode;
|
warningMode: BootstrapPromptWarningMode;
|
||||||
|
|||||||
@ -5,10 +5,25 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
|||||||
import type { OpenClawConfig } from "../config/config.js";
|
import type { OpenClawConfig } from "../config/config.js";
|
||||||
import { runCliAgent } from "./cli-runner.js";
|
import { runCliAgent } from "./cli-runner.js";
|
||||||
import { resolveCliNoOutputTimeoutMs } from "./cli-runner/helpers.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 supervisorSpawnMock = vi.fn();
|
||||||
const enqueueSystemEventMock = vi.fn();
|
const enqueueSystemEventMock = vi.fn();
|
||||||
const requestHeartbeatNowMock = 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", () => ({
|
vi.mock("../process/supervisor/index.js", () => ({
|
||||||
getProcessSupervisor: () => ({
|
getProcessSupervisor: () => ({
|
||||||
@ -28,6 +43,11 @@ vi.mock("../infra/heartbeat-wake.js", () => ({
|
|||||||
requestHeartbeatNow: (...args: unknown[]) => requestHeartbeatNowMock(...args),
|
requestHeartbeatNow: (...args: unknown[]) => requestHeartbeatNowMock(...args),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock("./bootstrap-files.js", () => ({
|
||||||
|
makeBootstrapWarn: () => () => {},
|
||||||
|
resolveBootstrapContextForRun: hoisted.resolveBootstrapContextForRunMock,
|
||||||
|
}));
|
||||||
|
|
||||||
type MockRunExit = {
|
type MockRunExit = {
|
||||||
reason:
|
reason:
|
||||||
| "manual-cancel"
|
| "manual-cancel"
|
||||||
@ -61,6 +81,10 @@ describe("runCliAgent with process supervisor", () => {
|
|||||||
supervisorSpawnMock.mockClear();
|
supervisorSpawnMock.mockClear();
|
||||||
enqueueSystemEventMock.mockClear();
|
enqueueSystemEventMock.mockClear();
|
||||||
requestHeartbeatNowMock.mockClear();
|
requestHeartbeatNowMock.mockClear();
|
||||||
|
hoisted.resolveBootstrapContextForRunMock.mockReset().mockResolvedValue({
|
||||||
|
bootstrapFiles: [],
|
||||||
|
contextFiles: [],
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("runs CLI through supervisor and returns payload", async () => {
|
it("runs CLI through supervisor and returns payload", async () => {
|
||||||
@ -107,6 +131,62 @@ describe("runCliAgent with process supervisor", () => {
|
|||||||
expect(input.scopeKey).toContain("thread-123");
|
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 () => {
|
it("fails with timeout when no-output watchdog trips", async () => {
|
||||||
supervisorSpawnMock.mockResolvedValueOnce(
|
supervisorSpawnMock.mockResolvedValueOnce(
|
||||||
createManagedRun({
|
createManagedRun({
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import {
|
|||||||
buildBootstrapInjectionStats,
|
buildBootstrapInjectionStats,
|
||||||
buildBootstrapPromptWarning,
|
buildBootstrapPromptWarning,
|
||||||
buildBootstrapTruncationReportMeta,
|
buildBootstrapTruncationReportMeta,
|
||||||
|
prependBootstrapPromptWarning,
|
||||||
} from "./bootstrap-budget.js";
|
} from "./bootstrap-budget.js";
|
||||||
import { makeBootstrapWarn, resolveBootstrapContextForRun } from "./bootstrap-files.js";
|
import { makeBootstrapWarn, resolveBootstrapContextForRun } from "./bootstrap-files.js";
|
||||||
import { resolveCliBackendConfig } from "./cli-backends.js";
|
import { resolveCliBackendConfig } from "./cli-backends.js";
|
||||||
@ -162,7 +163,6 @@ export async function runCliAgent(params: {
|
|||||||
docsPath: docsPath ?? undefined,
|
docsPath: docsPath ?? undefined,
|
||||||
tools: [],
|
tools: [],
|
||||||
contextFiles,
|
contextFiles,
|
||||||
bootstrapTruncationWarningLines: bootstrapPromptWarning.lines,
|
|
||||||
modelDisplay,
|
modelDisplay,
|
||||||
agentId: sessionAgentId,
|
agentId: sessionAgentId,
|
||||||
});
|
});
|
||||||
@ -218,7 +218,9 @@ export async function runCliAgent(params: {
|
|||||||
|
|
||||||
let imagePaths: string[] | undefined;
|
let imagePaths: string[] | undefined;
|
||||||
let cleanupImages: (() => Promise<void>) | 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) {
|
if (params.images && params.images.length > 0) {
|
||||||
const imagePayload = await writeCliImages(params.images);
|
const imagePayload = await writeCliImages(params.images);
|
||||||
imagePaths = imagePayload.paths;
|
imagePaths = imagePayload.paths;
|
||||||
|
|||||||
@ -48,7 +48,6 @@ export function buildSystemPrompt(params: {
|
|||||||
docsPath?: string;
|
docsPath?: string;
|
||||||
tools: AgentTool[];
|
tools: AgentTool[];
|
||||||
contextFiles?: EmbeddedContextFile[];
|
contextFiles?: EmbeddedContextFile[];
|
||||||
bootstrapTruncationWarningLines?: string[];
|
|
||||||
modelDisplay: string;
|
modelDisplay: string;
|
||||||
agentId?: string;
|
agentId?: string;
|
||||||
}) {
|
}) {
|
||||||
@ -92,7 +91,6 @@ export function buildSystemPrompt(params: {
|
|||||||
userTime,
|
userTime,
|
||||||
userTimeFormat,
|
userTimeFormat,
|
||||||
contextFiles: params.contextFiles,
|
contextFiles: params.contextFiles,
|
||||||
bootstrapTruncationWarningLines: params.bootstrapTruncationWarningLines,
|
|
||||||
ttsHint,
|
ttsHint,
|
||||||
memoryCitationsMode: params.config?.memory?.citations,
|
memoryCitationsMode: params.config?.memory?.citations,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -18,16 +18,27 @@ import type {
|
|||||||
IngestBatchResult,
|
IngestBatchResult,
|
||||||
IngestResult,
|
IngestResult,
|
||||||
} from "../../../context-engine/types.js";
|
} from "../../../context-engine/types.js";
|
||||||
|
import type { EmbeddedContextFile } from "../../pi-embedded-helpers.js";
|
||||||
import { createHostSandboxFsBridge } from "../../test-helpers/host-sandbox-fs-bridge.js";
|
import { createHostSandboxFsBridge } from "../../test-helpers/host-sandbox-fs-bridge.js";
|
||||||
import { createPiToolsSandboxContext } from "../../test-helpers/pi-tools-sandbox-context.js";
|
import { createPiToolsSandboxContext } from "../../test-helpers/pi-tools-sandbox-context.js";
|
||||||
|
import type { WorkspaceBootstrapFile } from "../../workspace.js";
|
||||||
|
|
||||||
const hoisted = vi.hoisted(() => {
|
const hoisted = vi.hoisted(() => {
|
||||||
|
type BootstrapContext = {
|
||||||
|
bootstrapFiles: WorkspaceBootstrapFile[];
|
||||||
|
contextFiles: EmbeddedContextFile[];
|
||||||
|
};
|
||||||
const spawnSubagentDirectMock = vi.fn();
|
const spawnSubagentDirectMock = vi.fn();
|
||||||
const createAgentSessionMock = vi.fn();
|
const createAgentSessionMock = vi.fn();
|
||||||
const sessionManagerOpenMock = vi.fn();
|
const sessionManagerOpenMock = vi.fn();
|
||||||
const resolveSandboxContextMock = vi.fn();
|
const resolveSandboxContextMock = vi.fn();
|
||||||
const subscribeEmbeddedPiSessionMock = vi.fn();
|
const subscribeEmbeddedPiSessionMock = vi.fn();
|
||||||
const acquireSessionWriteLockMock = vi.fn();
|
const acquireSessionWriteLockMock = vi.fn();
|
||||||
|
const resolveBootstrapContextForRunMock = vi.fn<() => Promise<BootstrapContext>>(async () => ({
|
||||||
|
bootstrapFiles: [],
|
||||||
|
contextFiles: [],
|
||||||
|
}));
|
||||||
|
const getGlobalHookRunnerMock = vi.fn<() => unknown>(() => undefined);
|
||||||
const sessionManager = {
|
const sessionManager = {
|
||||||
getLeafEntry: vi.fn(() => null),
|
getLeafEntry: vi.fn(() => null),
|
||||||
branch: vi.fn(),
|
branch: vi.fn(),
|
||||||
@ -42,6 +53,8 @@ const hoisted = vi.hoisted(() => {
|
|||||||
resolveSandboxContextMock,
|
resolveSandboxContextMock,
|
||||||
subscribeEmbeddedPiSessionMock,
|
subscribeEmbeddedPiSessionMock,
|
||||||
acquireSessionWriteLockMock,
|
acquireSessionWriteLockMock,
|
||||||
|
resolveBootstrapContextForRunMock,
|
||||||
|
getGlobalHookRunnerMock,
|
||||||
sessionManager,
|
sessionManager,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@ -80,7 +93,7 @@ vi.mock("../../pi-embedded-subscribe.js", () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("../../../plugins/hook-runner-global.js", () => ({
|
vi.mock("../../../plugins/hook-runner-global.js", () => ({
|
||||||
getGlobalHookRunner: () => undefined,
|
getGlobalHookRunner: hoisted.getGlobalHookRunnerMock,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("../../../infra/machine-name.js", () => ({
|
vi.mock("../../../infra/machine-name.js", () => ({
|
||||||
@ -94,7 +107,7 @@ vi.mock("../../../infra/net/undici-global-dispatcher.js", () => ({
|
|||||||
|
|
||||||
vi.mock("../../bootstrap-files.js", () => ({
|
vi.mock("../../bootstrap-files.js", () => ({
|
||||||
makeBootstrapWarn: () => () => {},
|
makeBootstrapWarn: () => () => {},
|
||||||
resolveBootstrapContextForRun: async () => ({ bootstrapFiles: [], contextFiles: [] }),
|
resolveBootstrapContextForRun: hoisted.resolveBootstrapContextForRunMock,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("../../skills.js", () => ({
|
vi.mock("../../skills.js", () => ({
|
||||||
@ -269,6 +282,11 @@ function resetEmbeddedAttemptHarness(
|
|||||||
hoisted.acquireSessionWriteLockMock.mockReset().mockResolvedValue({
|
hoisted.acquireSessionWriteLockMock.mockReset().mockResolvedValue({
|
||||||
release: async () => {},
|
release: async () => {},
|
||||||
});
|
});
|
||||||
|
hoisted.resolveBootstrapContextForRunMock.mockReset().mockResolvedValue({
|
||||||
|
bootstrapFiles: [],
|
||||||
|
contextFiles: [],
|
||||||
|
});
|
||||||
|
hoisted.getGlobalHookRunnerMock.mockReset().mockReturnValue(undefined);
|
||||||
hoisted.sessionManager.getLeafEntry.mockReset().mockReturnValue(null);
|
hoisted.sessionManager.getLeafEntry.mockReset().mockReturnValue(null);
|
||||||
hoisted.sessionManager.branch.mockReset();
|
hoisted.sessionManager.branch.mockReset();
|
||||||
hoisted.sessionManager.resetLeaf.mockReset();
|
hoisted.sessionManager.resetLeaf.mockReset();
|
||||||
@ -291,7 +309,11 @@ async function cleanupTempPaths(tempPaths: string[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createDefaultEmbeddedSession(params?: {
|
function createDefaultEmbeddedSession(params?: {
|
||||||
prompt?: (session: MutableSession) => Promise<void>;
|
prompt?: (
|
||||||
|
session: MutableSession,
|
||||||
|
prompt: string,
|
||||||
|
options?: { images?: unknown[] },
|
||||||
|
) => Promise<void>;
|
||||||
}): MutableSession {
|
}): MutableSession {
|
||||||
const session: MutableSession = {
|
const session: MutableSession = {
|
||||||
sessionId: "embedded-session",
|
sessionId: "embedded-session",
|
||||||
@ -303,9 +325,9 @@ function createDefaultEmbeddedSession(params?: {
|
|||||||
session.messages = [...messages];
|
session.messages = [...messages];
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
prompt: async () => {
|
prompt: async (prompt, options) => {
|
||||||
if (params?.prompt) {
|
if (params?.prompt) {
|
||||||
await params.prompt(session);
|
await params.prompt(session, prompt, options);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
session.messages = [
|
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", () => {
|
describe("runEmbeddedAttempt cache-ttl tracking after compaction", () => {
|
||||||
const tempPaths: string[] = [];
|
const tempPaths: string[] = [];
|
||||||
|
|
||||||
|
|||||||
@ -41,6 +41,7 @@ import {
|
|||||||
buildBootstrapPromptWarning,
|
buildBootstrapPromptWarning,
|
||||||
buildBootstrapTruncationReportMeta,
|
buildBootstrapTruncationReportMeta,
|
||||||
buildBootstrapInjectionStats,
|
buildBootstrapInjectionStats,
|
||||||
|
prependBootstrapPromptWarning,
|
||||||
} from "../../bootstrap-budget.js";
|
} from "../../bootstrap-budget.js";
|
||||||
import { makeBootstrapWarn, resolveBootstrapContextForRun } from "../../bootstrap-files.js";
|
import { makeBootstrapWarn, resolveBootstrapContextForRun } from "../../bootstrap-files.js";
|
||||||
import { createCacheTrace } from "../../cache-trace.js";
|
import { createCacheTrace } from "../../cache-trace.js";
|
||||||
@ -1665,6 +1666,9 @@ export async function runEmbeddedAttempt(
|
|||||||
});
|
});
|
||||||
const ttsHint = params.config ? buildTtsSystemPromptHint(params.config) : undefined;
|
const ttsHint = params.config ? buildTtsSystemPromptHint(params.config) : undefined;
|
||||||
const ownerDisplay = resolveOwnerDisplaySetting(params.config);
|
const ownerDisplay = resolveOwnerDisplaySetting(params.config);
|
||||||
|
const heartbeatPrompt = isDefaultAgent
|
||||||
|
? resolveHeartbeatPrompt(params.config?.agents?.defaults?.heartbeat?.prompt)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
const appendPrompt = buildEmbeddedSystemPrompt({
|
const appendPrompt = buildEmbeddedSystemPrompt({
|
||||||
workspaceDir: effectiveWorkspace,
|
workspaceDir: effectiveWorkspace,
|
||||||
@ -1675,9 +1679,7 @@ export async function runEmbeddedAttempt(
|
|||||||
ownerDisplay: ownerDisplay.ownerDisplay,
|
ownerDisplay: ownerDisplay.ownerDisplay,
|
||||||
ownerDisplaySecret: ownerDisplay.ownerDisplaySecret,
|
ownerDisplaySecret: ownerDisplay.ownerDisplaySecret,
|
||||||
reasoningTagHint,
|
reasoningTagHint,
|
||||||
heartbeatPrompt: isDefaultAgent
|
heartbeatPrompt,
|
||||||
? resolveHeartbeatPrompt(params.config?.agents?.defaults?.heartbeat?.prompt)
|
|
||||||
: undefined,
|
|
||||||
skillsPrompt,
|
skillsPrompt,
|
||||||
docsPath: docsPath ?? undefined,
|
docsPath: docsPath ?? undefined,
|
||||||
ttsHint,
|
ttsHint,
|
||||||
@ -1694,7 +1696,6 @@ export async function runEmbeddedAttempt(
|
|||||||
userTime,
|
userTime,
|
||||||
userTimeFormat,
|
userTimeFormat,
|
||||||
contextFiles,
|
contextFiles,
|
||||||
bootstrapTruncationWarningLines: bootstrapPromptWarning.lines,
|
|
||||||
memoryCitationsMode: params.config?.memory?.citations,
|
memoryCitationsMode: params.config?.memory?.citations,
|
||||||
});
|
});
|
||||||
const systemPromptReport = buildSystemPromptReport({
|
const systemPromptReport = buildSystemPromptReport({
|
||||||
@ -2378,7 +2379,13 @@ export async function runEmbeddedAttempt(
|
|||||||
|
|
||||||
// Run before_prompt_build hooks to allow plugins to inject prompt context.
|
// Run before_prompt_build hooks to allow plugins to inject prompt context.
|
||||||
// Legacy compatibility: before_agent_start is also checked for context fields.
|
// 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 = {
|
const hookCtx = {
|
||||||
agentId: hookAgentId,
|
agentId: hookAgentId,
|
||||||
sessionKey: params.sessionKey,
|
sessionKey: params.sessionKey,
|
||||||
@ -2397,7 +2404,7 @@ export async function runEmbeddedAttempt(
|
|||||||
});
|
});
|
||||||
{
|
{
|
||||||
if (hookResult?.prependContext) {
|
if (hookResult?.prependContext) {
|
||||||
effectivePrompt = `${hookResult.prependContext}\n\n${params.prompt}`;
|
effectivePrompt = `${hookResult.prependContext}\n\n${effectivePrompt}`;
|
||||||
log.debug(
|
log.debug(
|
||||||
`hooks: prepended context to prompt (${hookResult.prependContext.length} chars)`,
|
`hooks: prepended context to prompt (${hookResult.prependContext.length} chars)`,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -51,7 +51,6 @@ export function buildEmbeddedSystemPrompt(params: {
|
|||||||
userTime?: string;
|
userTime?: string;
|
||||||
userTimeFormat?: ResolvedTimeFormat;
|
userTimeFormat?: ResolvedTimeFormat;
|
||||||
contextFiles?: EmbeddedContextFile[];
|
contextFiles?: EmbeddedContextFile[];
|
||||||
bootstrapTruncationWarningLines?: string[];
|
|
||||||
memoryCitationsMode?: MemoryCitationsMode;
|
memoryCitationsMode?: MemoryCitationsMode;
|
||||||
}): string {
|
}): string {
|
||||||
return buildAgentSystemPrompt({
|
return buildAgentSystemPrompt({
|
||||||
@ -81,7 +80,6 @@ export function buildEmbeddedSystemPrompt(params: {
|
|||||||
userTime: params.userTime,
|
userTime: params.userTime,
|
||||||
userTimeFormat: params.userTimeFormat,
|
userTimeFormat: params.userTimeFormat,
|
||||||
contextFiles: params.contextFiles,
|
contextFiles: params.contextFiles,
|
||||||
bootstrapTruncationWarningLines: params.bootstrapTruncationWarningLines,
|
|
||||||
memoryCitationsMode: params.memoryCitationsMode,
|
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({
|
const prompt = buildAgentSystemPrompt({
|
||||||
workspaceDir: "/tmp/openclaw",
|
workspaceDir: "/tmp/openclaw",
|
||||||
bootstrapTruncationWarningLines: ["AGENTS.md: 200 raw -> 0 injected"],
|
|
||||||
contextFiles: [],
|
contextFiles: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(prompt).toContain("# Project Context");
|
expect(prompt).not.toContain("# Project Context");
|
||||||
expect(prompt).toContain("⚠ Bootstrap truncation warning:");
|
|
||||||
expect(prompt).toContain("- AGENTS.md: 200 raw -> 0 injected");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("summarizes the message tool when available", () => {
|
it("summarizes the message tool when available", () => {
|
||||||
|
|||||||
@ -202,7 +202,6 @@ export function buildAgentSystemPrompt(params: {
|
|||||||
userTime?: string;
|
userTime?: string;
|
||||||
userTimeFormat?: ResolvedTimeFormat;
|
userTimeFormat?: ResolvedTimeFormat;
|
||||||
contextFiles?: EmbeddedContextFile[];
|
contextFiles?: EmbeddedContextFile[];
|
||||||
bootstrapTruncationWarningLines?: string[];
|
|
||||||
skillsPrompt?: string;
|
skillsPrompt?: string;
|
||||||
heartbeatPrompt?: string;
|
heartbeatPrompt?: string;
|
||||||
docsPath?: string;
|
docsPath?: string;
|
||||||
@ -614,13 +613,10 @@ export function buildAgentSystemPrompt(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const contextFiles = params.contextFiles ?? [];
|
const contextFiles = params.contextFiles ?? [];
|
||||||
const bootstrapTruncationWarningLines = (params.bootstrapTruncationWarningLines ?? []).filter(
|
|
||||||
(line) => line.trim().length > 0,
|
|
||||||
);
|
|
||||||
const validContextFiles = contextFiles.filter(
|
const validContextFiles = contextFiles.filter(
|
||||||
(file) => typeof file.path === "string" && file.path.trim().length > 0,
|
(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", "");
|
lines.push("# Project Context", "");
|
||||||
if (validContextFiles.length > 0) {
|
if (validContextFiles.length > 0) {
|
||||||
const hasSoulFile = validContextFiles.some((file) => {
|
const hasSoulFile = validContextFiles.some((file) => {
|
||||||
@ -636,13 +632,6 @@ export function buildAgentSystemPrompt(params: {
|
|||||||
}
|
}
|
||||||
lines.push("");
|
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) {
|
for (const file of validContextFiles) {
|
||||||
lines.push(`## ${file.path}`, "", file.content, "");
|
lines.push(`## ${file.path}`, "", file.content, "");
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user