mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 14:21:38 +00:00
Agents: preserve bootstrap warning dedupe across followup runs
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import type { AgentMessage } from "@mariozechner/pi-agent-core";
|
||||
import { resolveBootstrapWarningSignaturesSeen } from "../../agents/bootstrap-budget.js";
|
||||
import { estimateMessagesTokens } from "../../agents/compaction.js";
|
||||
import { runWithModelFallback } from "../../agents/model-fallback.js";
|
||||
import { isCliProvider } from "../../agents/model-selection.js";
|
||||
@@ -452,6 +453,10 @@ export async function runMemoryFlushIfNeeded(params: {
|
||||
|
||||
let activeSessionEntry = entry ?? params.sessionEntry;
|
||||
const activeSessionStore = params.sessionStore;
|
||||
let bootstrapPromptWarningSignaturesSeen = resolveBootstrapWarningSignaturesSeen(
|
||||
activeSessionEntry?.systemPromptReport ??
|
||||
(params.sessionKey ? activeSessionStore?.[params.sessionKey]?.systemPromptReport : undefined),
|
||||
);
|
||||
const flushRunId = crypto.randomUUID();
|
||||
if (params.sessionKey) {
|
||||
registerAgentRunContext(flushRunId, {
|
||||
@@ -469,7 +474,7 @@ export async function runMemoryFlushIfNeeded(params: {
|
||||
try {
|
||||
await runWithModelFallback({
|
||||
...resolveModelFallbackOptions(params.followupRun.run),
|
||||
run: (provider, model) => {
|
||||
run: async (provider, model) => {
|
||||
const { authProfile, embeddedContext, senderContext } = buildEmbeddedRunContexts({
|
||||
run: params.followupRun.run,
|
||||
sessionCtx: params.sessionCtx,
|
||||
@@ -483,7 +488,7 @@ export async function runMemoryFlushIfNeeded(params: {
|
||||
runId: flushRunId,
|
||||
authProfile,
|
||||
});
|
||||
return runEmbeddedPiAgent({
|
||||
const result = await runEmbeddedPiAgent({
|
||||
...embeddedContext,
|
||||
...senderContext,
|
||||
...runBaseParams,
|
||||
@@ -493,6 +498,9 @@ export async function runMemoryFlushIfNeeded(params: {
|
||||
cfg: params.cfg,
|
||||
}),
|
||||
extraSystemPrompt: flushSystemPrompt,
|
||||
bootstrapPromptWarningSignaturesSeen,
|
||||
bootstrapPromptWarningSignature:
|
||||
bootstrapPromptWarningSignaturesSeen[bootstrapPromptWarningSignaturesSeen.length - 1],
|
||||
onAgentEvent: (evt) => {
|
||||
if (evt.stream === "compaction") {
|
||||
const phase = typeof evt.data.phase === "string" ? evt.data.phase : "";
|
||||
@@ -502,6 +510,10 @@ export async function runMemoryFlushIfNeeded(params: {
|
||||
}
|
||||
},
|
||||
});
|
||||
bootstrapPromptWarningSignaturesSeen = resolveBootstrapWarningSignaturesSeen(
|
||||
result.meta?.systemPromptReport,
|
||||
);
|
||||
return result;
|
||||
},
|
||||
});
|
||||
let memoryFlushCompactionCount =
|
||||
|
||||
@@ -28,6 +28,8 @@ type AgentRunParams = {
|
||||
type EmbeddedRunParams = {
|
||||
prompt?: string;
|
||||
extraSystemPrompt?: string;
|
||||
bootstrapPromptWarningSignaturesSeen?: string[];
|
||||
bootstrapPromptWarningSignature?: string;
|
||||
onAgentEvent?: (evt: { stream?: string; data?: { phase?: string; willRetry?: boolean } }) => void;
|
||||
};
|
||||
|
||||
@@ -1114,7 +1116,7 @@ describe("runReplyAgent typing (heartbeat)", () => {
|
||||
const sessionId = "session";
|
||||
const storePath = path.join(stateDir, "sessions", "sessions.json");
|
||||
const transcriptPath = sessions.resolveSessionTranscriptPath(sessionId);
|
||||
const sessionEntry = {
|
||||
const sessionEntry: SessionEntry = {
|
||||
sessionId,
|
||||
updatedAt: Date.now(),
|
||||
sessionFile: transcriptPath,
|
||||
@@ -1478,7 +1480,7 @@ describe("runReplyAgent memory flush", () => {
|
||||
it("skips memory flush for CLI providers", async () => {
|
||||
await withTempStore(async (storePath) => {
|
||||
const sessionKey = "main";
|
||||
const sessionEntry = {
|
||||
const sessionEntry: SessionEntry = {
|
||||
sessionId: "session",
|
||||
updatedAt: Date.now(),
|
||||
totalTokens: 80_000,
|
||||
@@ -1577,6 +1579,77 @@ describe("runReplyAgent memory flush", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("passes stored bootstrap warning signatures to memory flush runs", async () => {
|
||||
await withTempStore(async (storePath) => {
|
||||
const sessionKey = "main";
|
||||
const sessionEntry: SessionEntry = {
|
||||
sessionId: "session",
|
||||
updatedAt: Date.now(),
|
||||
totalTokens: 80_000,
|
||||
compactionCount: 1,
|
||||
systemPromptReport: {
|
||||
source: "run",
|
||||
generatedAt: Date.now(),
|
||||
systemPrompt: {
|
||||
chars: 1,
|
||||
projectContextChars: 0,
|
||||
nonProjectContextChars: 1,
|
||||
},
|
||||
injectedWorkspaceFiles: [],
|
||||
skills: {
|
||||
promptChars: 0,
|
||||
entries: [],
|
||||
},
|
||||
tools: {
|
||||
listChars: 0,
|
||||
schemaChars: 0,
|
||||
entries: [],
|
||||
},
|
||||
bootstrapTruncation: {
|
||||
warningMode: "once",
|
||||
warningShown: true,
|
||||
promptWarningSignature: "sig-b",
|
||||
warningSignaturesSeen: ["sig-a", "sig-b"],
|
||||
truncatedFiles: 1,
|
||||
nearLimitFiles: 0,
|
||||
totalNearLimit: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
await seedSessionStore({ storePath, sessionKey, entry: sessionEntry });
|
||||
|
||||
const calls: Array<EmbeddedRunParams> = [];
|
||||
state.runEmbeddedPiAgentMock.mockImplementation(async (params: EmbeddedRunParams) => {
|
||||
calls.push(params);
|
||||
if (params.prompt?.includes("Pre-compaction memory flush.")) {
|
||||
return { payloads: [], meta: {} };
|
||||
}
|
||||
return {
|
||||
payloads: [{ text: "ok" }],
|
||||
meta: { agentMeta: { usage: { input: 1, output: 1 } } },
|
||||
};
|
||||
});
|
||||
|
||||
const baseRun = createBaseRun({
|
||||
storePath,
|
||||
sessionEntry,
|
||||
});
|
||||
|
||||
await runReplyAgentWithBase({
|
||||
baseRun,
|
||||
storePath,
|
||||
sessionKey,
|
||||
sessionEntry,
|
||||
commandBody: "hello",
|
||||
});
|
||||
|
||||
expect(calls).toHaveLength(2);
|
||||
expect(calls[0]?.bootstrapPromptWarningSignaturesSeen).toEqual(["sig-a", "sig-b"]);
|
||||
expect(calls[0]?.bootstrapPromptWarningSignature).toBe("sig-b");
|
||||
});
|
||||
});
|
||||
|
||||
it("runs a memory flush turn and updates session metadata", async () => {
|
||||
await withTempStore(async (storePath) => {
|
||||
const sessionKey = "main";
|
||||
|
||||
@@ -163,6 +163,70 @@ describe("createFollowupRunner compaction", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("createFollowupRunner bootstrap warning dedupe", () => {
|
||||
it("passes stored warning signature history to embedded followup runs", async () => {
|
||||
runEmbeddedPiAgentMock.mockResolvedValueOnce({
|
||||
payloads: [],
|
||||
meta: {},
|
||||
});
|
||||
|
||||
const sessionEntry: SessionEntry = {
|
||||
sessionId: "session",
|
||||
updatedAt: Date.now(),
|
||||
systemPromptReport: {
|
||||
source: "run",
|
||||
generatedAt: Date.now(),
|
||||
systemPrompt: {
|
||||
chars: 1,
|
||||
projectContextChars: 0,
|
||||
nonProjectContextChars: 1,
|
||||
},
|
||||
injectedWorkspaceFiles: [],
|
||||
skills: {
|
||||
promptChars: 0,
|
||||
entries: [],
|
||||
},
|
||||
tools: {
|
||||
listChars: 0,
|
||||
schemaChars: 0,
|
||||
entries: [],
|
||||
},
|
||||
bootstrapTruncation: {
|
||||
warningMode: "once",
|
||||
warningShown: true,
|
||||
promptWarningSignature: "sig-b",
|
||||
warningSignaturesSeen: ["sig-a", "sig-b"],
|
||||
truncatedFiles: 1,
|
||||
nearLimitFiles: 0,
|
||||
totalNearLimit: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
const sessionStore: Record<string, SessionEntry> = { main: sessionEntry };
|
||||
|
||||
const runner = createFollowupRunner({
|
||||
opts: { onBlockReply: vi.fn(async () => {}) },
|
||||
typing: createMockTypingController(),
|
||||
typingMode: "instant",
|
||||
sessionEntry,
|
||||
sessionStore,
|
||||
sessionKey: "main",
|
||||
defaultModel: "anthropic/claude-opus-4-5",
|
||||
});
|
||||
|
||||
await runner(baseQueuedRun());
|
||||
|
||||
const call = runEmbeddedPiAgentMock.mock.calls.at(-1)?.[0] as
|
||||
| {
|
||||
bootstrapPromptWarningSignaturesSeen?: string[];
|
||||
bootstrapPromptWarningSignature?: string;
|
||||
}
|
||||
| undefined;
|
||||
expect(call?.bootstrapPromptWarningSignaturesSeen).toEqual(["sig-a", "sig-b"]);
|
||||
expect(call?.bootstrapPromptWarningSignature).toBe("sig-b");
|
||||
});
|
||||
});
|
||||
|
||||
describe("createFollowupRunner messaging tool dedupe", () => {
|
||||
function createMessagingDedupeRunner(
|
||||
onBlockReply: (payload: unknown) => Promise<void>,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import crypto from "node:crypto";
|
||||
import { resolveRunModelFallbacksOverride } from "../../agents/agent-scope.js";
|
||||
import { resolveBootstrapWarningSignaturesSeen } from "../../agents/bootstrap-budget.js";
|
||||
import { lookupContextTokens } from "../../agents/context.js";
|
||||
import { DEFAULT_CONTEXT_TOKENS } from "../../agents/defaults.js";
|
||||
import { runWithModelFallback } from "../../agents/model-fallback.js";
|
||||
@@ -140,6 +141,11 @@ export function createFollowupRunner(params: {
|
||||
let runResult: Awaited<ReturnType<typeof runEmbeddedPiAgent>>;
|
||||
let fallbackProvider = queued.run.provider;
|
||||
let fallbackModel = queued.run.model;
|
||||
const activeSessionEntry =
|
||||
(sessionKey ? sessionStore?.[sessionKey] : undefined) ?? sessionEntry;
|
||||
let bootstrapPromptWarningSignaturesSeen = resolveBootstrapWarningSignaturesSeen(
|
||||
activeSessionEntry?.systemPromptReport,
|
||||
);
|
||||
try {
|
||||
const fallbackResult = await runWithModelFallback({
|
||||
cfg: queued.run.config,
|
||||
@@ -151,9 +157,9 @@ export function createFollowupRunner(params: {
|
||||
agentId: queued.run.agentId,
|
||||
sessionKey: queued.run.sessionKey,
|
||||
}),
|
||||
run: (provider, model) => {
|
||||
run: async (provider, model) => {
|
||||
const authProfile = resolveRunAuthProfile(queued.run, provider);
|
||||
return runEmbeddedPiAgent({
|
||||
const result = await runEmbeddedPiAgent({
|
||||
sessionId: queued.run.sessionId,
|
||||
sessionKey: queued.run.sessionKey,
|
||||
agentId: queued.run.agentId,
|
||||
@@ -195,6 +201,11 @@ export function createFollowupRunner(params: {
|
||||
timeoutMs: queued.run.timeoutMs,
|
||||
runId,
|
||||
blockReplyBreak: queued.run.blockReplyBreak,
|
||||
bootstrapPromptWarningSignaturesSeen,
|
||||
bootstrapPromptWarningSignature:
|
||||
bootstrapPromptWarningSignaturesSeen[
|
||||
bootstrapPromptWarningSignaturesSeen.length - 1
|
||||
],
|
||||
onAgentEvent: (evt) => {
|
||||
if (evt.stream !== "compaction") {
|
||||
return;
|
||||
@@ -205,6 +216,10 @@ export function createFollowupRunner(params: {
|
||||
}
|
||||
},
|
||||
});
|
||||
bootstrapPromptWarningSignaturesSeen = resolveBootstrapWarningSignaturesSeen(
|
||||
result.meta?.systemPromptReport,
|
||||
);
|
||||
return result;
|
||||
},
|
||||
});
|
||||
runResult = fallbackResult.result;
|
||||
@@ -235,6 +250,7 @@ export function createFollowupRunner(params: {
|
||||
modelUsed,
|
||||
providerUsed: fallbackProvider,
|
||||
contextTokensUsed,
|
||||
systemPromptReport: runResult.meta?.systemPromptReport,
|
||||
logLabel: "followup",
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user