import fs from "node:fs/promises" ;
import path from "node:path" ;
import {
appendBootstrapPromptWarning,
analyzeBootstrapBudget,
buildBootstrapInjectionStats,
buildBootstrapPromptWarning,
} from "../../../src/agents/bootstrap-budget.js" ;
import { resolveBootstrapContextForRun } from "../../../src/agents/bootstrap-files.js" ;
import { buildEmbeddedSystemPrompt } from "../../../src/agents/pi-embedded-runner/system-prompt.js" ;
import { buildAgentSystemPrompt } from "../../../src/agents/system-prompt.js" ;
import { createStubTool } from "../../../src/agents/test-helpers/pi-tool-stubs.js" ;
import {
buildDirectChatContext,
buildGroupChatContext,
buildGroupIntro,
} from "../../../src/auto-reply/reply/groups.js" ;
import {
buildInboundMetaSystemPrompt,
buildInboundUserContextPrefix,
} from "../../../src/auto-reply/reply/inbound-meta.js" ;
import type { TemplateContext } from "../../../src/auto-reply/templating.js" ;
import { SILENT_REPLY_TOKEN } from "../../../src/auto-reply/tokens.js" ;
import type { OpenClawConfig } from "../../../src/config/config.js" ;
import { makeTempWorkspace, writeWorkspaceFile } from "../../../src/test-helpers/workspace.js" ;
export type PromptScenarioTurn = {
id: string;
label: string;
systemPrompt: string;
bodyPrompt: string;
notes: string[];
};
export type PromptScenario = {
scenario: string;
focus: string;
expectedStableSystemAfterTurnIds: string[];
turns: PromptScenarioTurn[];
};
function buildCommonSystemParams(workspaceDir: string) {
const toolNames = [
"bash" ,
"read" ,
"edit" ,
"grep" ,
"glob" ,
"message" ,
"memory_search" ,
"memory_get" ,
"web_search" ,
"x_search" ,
"web_fetch" ,
];
return {
runtimeInfo: {
agentId: "main" ,
host: "cache-lab" ,
repoRoot: workspaceDir,
os: "Darwin 24.0.0" ,
arch: "arm64" ,
node: process.version,
model: "anthropic/claude-sonnet-4-6" ,
defaultModel: "anthropic/claude-sonnet-4-6" ,
shell: "zsh" ,
},
userTimezone: "America/Los_Angeles" ,
userTime: "Monday, March 16th, 2026 - 9:00 PM" ,
userTimeFormat: "12" as const ,
toolNames,
};
}
function buildSystemPrompt(params: {
workspaceDir: string;
extraSystemPrompt?: string;
skillsPrompt?: string;
reactionGuidance?: { level: "minimal" | "extensive" ; channel: string };
contextFiles?: Array<{ path: string; content: string }>;
}) {
const { runtimeInfo, userTimezone, userTime, userTimeFormat, toolNames } =
buildCommonSystemParams(params.workspaceDir);
return buildAgentSystemPrompt({
workspaceDir: params.workspaceDir,
extraSystemPrompt: params.extraSystemPrompt,
runtimeInfo,
userTimezone,
userTime,
userTimeFormat,
toolNames,
modelAliasLines: [],
promptMode: "full" ,
acpEnabled: true ,
skillsPrompt: params.skillsPrompt,
reactionGuidance: params.reactionGuidance,
contextFiles: params.contextFiles,
});
}
function buildAutoReplyBody(params: { ctx: TemplateContext; body: string; eventLine?: string }) {
return [params.eventLine, buildInboundUserContextPrefix(params.ctx), params.body]
.filter(Boolean )
.join("\n\n" );
}
async function readContextFiles(workspaceDir: string, fileNames: string[]) {
return Promise.all(
fileNames.map(async (fileName) => ({
path: fileName,
content: await fs.readFile(path.join(workspaceDir, fileName), "utf-8" ),
})),
);
}
function buildAutoReplySystemPrompt(params: {
workspaceDir: string;
sessionCtx: TemplateContext;
includeGroupChatContext?: boolean ;
includeGroupIntro?: boolean ;
groupSystemPrompt?: string;
}) {
const extraSystemPromptParts = [
buildInboundMetaSystemPrompt(params.sessionCtx),
params.sessionCtx.ChatType === "direct" || params.sessionCtx.ChatType === "dm"
? buildDirectChatContext({
sessionCtx: params.sessionCtx,
silentToken: SILENT_REPLY_TOKEN,
silentReplyPolicy: "disallow" ,
silentReplyRewrite: true ,
})
: "" ,
params.includeGroupChatContext ? buildGroupChatContext({ sessionCtx: params.sessionCtx }) : "" ,
params.includeGroupIntro
? buildGroupIntro({
cfg: {} as OpenClawConfig,
sessionCtx: params.sessionCtx,
defaultActivation: "mention" ,
silentToken: SILENT_REPLY_TOKEN,
})
: "" ,
params.groupSystemPrompt?.trim() ?? "" ,
].filter(Boolean );
return buildSystemPrompt({
workspaceDir: params.workspaceDir,
extraSystemPrompt: extraSystemPromptParts.join("\n\n" ) || undefined,
});
}
function buildToolRichSystemPrompt(params: {
workspaceDir: string;
skillsPrompt: string;
contextFiles: Array<{ path: string; content: string }>;
}) {
const { runtimeInfo, userTimezone, userTime, userTimeFormat } = buildCommonSystemParams(
params.workspaceDir,
);
const tools = [
"bash" ,
"read" ,
"edit" ,
"grep" ,
"glob" ,
"message" ,
"memory_search" ,
"memory_get" ,
"web_search" ,
"x_search" ,
"web_fetch" ,
].map((name) => Object.assign({}, createStubTool(name), { description: `${name} tool` }));
return buildEmbeddedSystemPrompt({
workspaceDir: params.workspaceDir,
reasoningTagHint: false ,
runtimeInfo,
tools,
modelAliasLines: [],
userTimezone,
userTime,
userTimeFormat,
acpEnabled: true ,
skillsPrompt: params.skillsPrompt,
reactionGuidance: { level: "extensive" , channel: "Telegram" },
contextFiles: params.contextFiles,
});
}
function createDirectScenario(workspaceDir: string): PromptScenario {
const baseCtx: TemplateContext = {
Provider: "slack" ,
Surface: "slack" ,
OriginatingChannel: "slack" ,
OriginatingTo: "D123" ,
AccountId: "A1" ,
ChatType: "dm" ,
SenderId: "U1" ,
SenderName: "Alice" ,
Body: "hi" ,
BodyStripped: "hi" ,
};
return {
scenario: "auto-reply-direct" ,
focus:
"Normal direct-chat turns with ids, reply context, think hint, and runtime event body injection" ,
expectedStableSystemAfterTurnIds: ["t2" , "t3" , "t4" ],
turns: [
{
id: "t1" ,
label: "Direct turn with reply context" ,
systemPrompt: buildAutoReplySystemPrompt({
workspaceDir,
sessionCtx: {
...baseCtx,
MessageSid: "m1" ,
ReplyToId: "r1" ,
ReplyToBody: "prior message" ,
WasMentioned: true ,
},
}),
bodyPrompt: buildAutoReplyBody({
ctx: {
...baseCtx,
MessageSid: "m1" ,
ReplyToId: "r1" ,
ReplyToBody: "prior message" ,
WasMentioned: true ,
},
body: "Please summarize yesterday's decision." ,
}),
notes: ["Direct chat baseline" , "Per-message ids and reply context change in body only" ],
},
{
id: "t2" ,
label: "Direct turn with new message id" ,
systemPrompt: buildAutoReplySystemPrompt({
workspaceDir,
sessionCtx: {
...baseCtx,
MessageSid: "m2" ,
ReplyToId: "r2" ,
},
}),
bodyPrompt: buildAutoReplyBody({
ctx: {
...baseCtx,
MessageSid: "m2" ,
ReplyToId: "r2" ,
},
body: "Now open the read tool and inspect AGENTS.md." ,
}),
notes: ["Steady-state direct turn" , "No runtime event" ],
},
{
id: "t3" ,
label: "Direct turn with runtime event and think hint" ,
systemPrompt: buildAutoReplySystemPrompt({
workspaceDir,
sessionCtx: {
...baseCtx,
MessageSid: "m3" ,
ReplyToId: "r3" ,
},
}),
bodyPrompt: buildAutoReplyBody({
ctx: {
...baseCtx,
MessageSid: "m3" ,
ReplyToId: "r3" ,
},
eventLine: "System: [t] Model switched." ,
body: "low use tools if needed and tell me which file controls startup behavior" ,
}),
notes: ["Touches runtime event body path" , "Touches think-hint parsing path" ],
},
{
id: "t4" ,
label: "Direct turn after runtime event" ,
systemPrompt: buildAutoReplySystemPrompt({
workspaceDir,
sessionCtx: {
...baseCtx,
MessageSid: "m4" ,
ReplyToId: "r4" ,
},
}),
bodyPrompt: buildAutoReplyBody({
ctx: {
...baseCtx,
MessageSid: "m4" ,
ReplyToId: "r4" ,
},
body: "Repeat the startup file path only." ,
}),
notes: ["Checks steady-state after event turn" ],
},
],
};
}
function createGroupScenario(workspaceDir: string): PromptScenario {
const baseCtx: TemplateContext = {
Provider: "slack" ,
Surface: "slack" ,
OriginatingChannel: "slack" ,
OriginatingTo: "C123" ,
AccountId: "A1" ,
ChatType: "group" ,
GroupSubject: "ops" ,
GroupChannel: "#ops" ,
GroupMembers: "Bob, Cara, Dan, Eve" ,
SenderId: "U2" ,
SenderName: "Bob" ,
Body: "hi" ,
BodyStripped: "hi" ,
};
return {
scenario: "auto-reply-group" ,
focus: "Group chat bootstrap, steady state, and runtime event turns" ,
expectedStableSystemAfterTurnIds: ["t3" ],
turns: [
{
id: "t1" ,
label: "First group turn with one-time intro" ,
systemPrompt: buildAutoReplySystemPrompt({
workspaceDir,
sessionCtx: {
...baseCtx,
MessageSid: "g1" ,
WasMentioned: true ,
InboundHistory: [{ sender: "Cara" , timestamp: 1 , body: "status?" }],
},
includeGroupChatContext: true ,
includeGroupIntro: true ,
}),
bodyPrompt: buildAutoReplyBody({
ctx: {
...baseCtx,
MessageSid: "g1" ,
WasMentioned: true ,
InboundHistory: [{ sender: "Cara" , timestamp: 1 , body: "status?" }],
},
body: "Can you investigate this issue?" ,
}),
notes: ["Expected first-turn bootstrap churn" , "Not steady-state" ],
},
{
id: "t2" ,
label: "Steady-state group turn" ,
systemPrompt: buildAutoReplySystemPrompt({
workspaceDir,
sessionCtx: {
...baseCtx,
MessageSid: "g2" ,
WasMentioned: false ,
InboundHistory: [
{ sender: "Cara" , timestamp: 1 , body: "status?" },
{ sender: "Dan" , timestamp: 2 , body: "please help" },
],
},
includeGroupChatContext: true ,
}),
bodyPrompt: buildAutoReplyBody({
ctx: {
...baseCtx,
MessageSid: "g2" ,
WasMentioned: false ,
InboundHistory: [
{ sender: "Cara" , timestamp: 1 , body: "status?" },
{ sender: "Dan" , timestamp: 2 , body: "please help" },
],
},
body: "Give a short update." ,
}),
notes: ["One-time intro gone" , "Should settle afterward" ],
},
{
id: "t3" ,
label: "Group turn with runtime event" ,
systemPrompt: buildAutoReplySystemPrompt({
workspaceDir,
sessionCtx: {
...baseCtx,
MessageSid: "g2" ,
WasMentioned: false ,
InboundHistory: [
{ sender: "Cara" , timestamp: 1 , body: "status?" },
{ sender: "Dan" , timestamp: 2 , body: "please help" },
],
},
includeGroupChatContext: true ,
}),
bodyPrompt: buildAutoReplyBody({
ctx: {
...baseCtx,
MessageSid: "g3" ,
WasMentioned: true ,
InboundHistory: [
{ sender: "Cara" , timestamp: 1 , body: "status?" },
{ sender: "Dan" , timestamp: 2 , body: "please help" },
{ sender: "Eve" , timestamp: 3 , body: "what changed?" },
],
},
eventLine: "System: [t] Node connected." ,
body: "Tell the room whether tools are available." ,
}),
notes: ["Runtime event lands in body" , "System prompt should stay stable vs t2" ],
},
],
};
}
async function createToolRichScenario(workspaceDir: string): Promise<PromptScenario> {
const skillsPrompt = [
"<available_skills>" ,
"<skill><name>checks</name><description>Run checks before landing changes.</description><location>/skills/checks/SKILL.md</location></skill>" ,
"<skill><name>release</name><description>Release OpenClaw safely.</description><location>/skills/release/SKILL.md</location></skill>" ,
"</available_skills>" ,
].join("\n" );
const contextFiles = await readContextFiles(workspaceDir, ["AGENTS.md" , "TOOLS.md" , "SOUL.md" ]);
const systemPrompt = buildToolRichSystemPrompt({
workspaceDir,
skillsPrompt,
contextFiles,
});
return {
scenario: "tool-rich-agent-run" ,
focus:
"Tool-enabled system prompt with skills, reactions, workspace bootstrap, and a follow-up after fictional tool calls" ,
expectedStableSystemAfterTurnIds: ["t2" ],
turns: [
{
id: "t1" ,
label: "Tool-rich turn asking for search, read, and file edits" ,
systemPrompt,
bodyPrompt: [
"Conversation info (untrusted metadata):" ,
"```json" ,
JSON.stringify({ message_id: "tool-1" , sender_id: "U9" , was_mentioned: true }, null , 2 ),
"```" ,
"" ,
"high Search the workspace, read AGENTS.md, inspect the failing test, and propose a patch." ,
].join("\n" ),
notes: ["Touches tool list in system prompt" , "Touches high-thinking hint in body" ],
},
{
id: "t2" ,
label: "Follow-up after a fictional tool call" ,
systemPrompt,
bodyPrompt: [
"Conversation info (untrusted metadata):" ,
"```json" ,
JSON.stringify({ message_id: "tool-2" , sender_id: "U9" }, null , 2 ),
"```" ,
"" ,
"Tool transcript summary (untrusted, for context):" ,
"```json" ,
JSON.stringify(
[
{ role: "assistant" , action: "tool_use" , name: "read" , target: "AGENTS.md" },
{ role: "tool" , name: "read" , result: "Loaded AGENTS.md" },
{ role: "assistant" , action: "tool_use" , name: "grep" , target: "failing test" },
{ role: "tool" , name: "grep" , result: "Matched src/foo.ts:42" },
],
null ,
2 ,
),
"```" ,
"" ,
"Continue and explain the root cause." ,
].join("\n" ),
notes: ["Simulates tool-call-heavy conversation" , "System prompt should stay stable" ],
},
],
};
}
async function createBootstrapWarningScenario(workspaceDir: string): Promise<PromptScenario> {
const bootstrapConfig = {
agents: {
defaults: {
bootstrapMaxChars: 1 _500 ,
bootstrapTotalMaxChars: 2 _200 ,
},
},
} satisfies OpenClawConfig;
const largeAgents = "# AGENTS.md\n\n" + "Rules.\n" .repeat(5 _000 );
const largeTools = "# TOOLS.md\n\n" + "Notes.\n" .repeat(3 _000 );
await writeWorkspaceFile({ dir: workspaceDir, name: "AGENTS.md" , content: largeAgents });
await writeWorkspaceFile({ dir: workspaceDir, name: "TOOLS.md" , content: largeTools });
const { bootstrapFiles, contextFiles } = await resolveBootstrapContextForRun({
workspaceDir,
config: bootstrapConfig,
});
const analysis = analyzeBootstrapBudget({
files: buildBootstrapInjectionStats({
bootstrapFiles,
injectedFiles: contextFiles,
}),
bootstrapMaxChars: bootstrapConfig.agents.defaults.bootstrapMaxChars,
bootstrapTotalMaxChars: bootstrapConfig.agents.defaults.bootstrapTotalMaxChars,
});
if (!analysis.hasTruncation) {
throw new Error("bootstrap-warning scenario expected truncated bootstrap context" );
}
const warningFirst = buildBootstrapPromptWarning({
analysis,
mode: "once" ,
seenSignatures: [],
});
const warningSeen = buildBootstrapPromptWarning({
analysis,
mode: "once" ,
seenSignatures: warningFirst.warningSignaturesSeen,
previousSignature: warningFirst.signature,
});
const warningAlways = buildBootstrapPromptWarning({
analysis,
mode: "always" ,
seenSignatures: warningFirst.warningSignaturesSeen,
previousSignature: warningFirst.signature,
});
return {
scenario: "bootstrap-warning" ,
focus: "Workspace bootstrap truncation warnings inside # Project Context" ,
expectedStableSystemAfterTurnIds: ["t2" , "t3" ],
turns: [
{
id: "t1" ,
label: "First warning emission" ,
systemPrompt: buildSystemPrompt({
workspaceDir,
contextFiles,
}),
bodyPrompt: appendBootstrapPromptWarning("hello" , warningFirst.lines),
notes: ["Warning is appended to the turn body" , "System prompt should stay stable" ],
},
{
id: "t2" ,
label: "Same truncation signature after once-mode dedupe" ,
systemPrompt: buildSystemPrompt({
workspaceDir,
contextFiles,
}),
bodyPrompt: appendBootstrapPromptWarning("hello again" , warningSeen.lines),
notes: ["Once-mode removes warning lines" , "Only the body tail changes now" ],
},
{
id: "t3" ,
label: "Always-mode warning" ,
systemPrompt: buildSystemPrompt({
workspaceDir,
contextFiles,
}),
bodyPrompt: appendBootstrapPromptWarning("one more turn" , warningAlways.lines),
notes: [
"Always-mode keeps warning in the body prompt tail" ,
"System prompt remains stable" ,
],
},
],
};
}
async function createMaintenanceScenario(workspaceDir: string): Promise<PromptScenario> {
await writeWorkspaceFile({
dir: workspaceDir,
name: "AGENTS.md" ,
content: [
"## Session Startup" ,
"Read AGENTS.md and MEMORY.md before responding." ,
"" ,
"## Red Lines" ,
"Do not delete production data." ,
"" ,
"## Safety" ,
"Never reveal secrets." ,
].join("\n" ),
});
const memoryFlushPrompt = [
"Pre-compaction memory flush." ,
"Store durable memories only in memory/2026-03-15.md (create memory/ if needed)." ,
"Treat workspace bootstrap/reference files such as MEMORY.md, SOUL.md, TOOLS.md, and AGENTS.md as read-only during this flush; never overwrite, replace, or edit them." ,
"If nothing to store, reply with NO_REPLY." ,
"Current time: Sunday, March 15th, 2026 - 9:30 PM (America/Los_Angeles) / 2026-03-16 04:30 UTC" ,
].join("\n" );
const memoryFlushSystemPrompt = buildSystemPrompt({
workspaceDir,
extraSystemPrompt: [
"Pre-compaction memory flush turn." ,
"The session is near auto-compaction; capture durable memories to disk." ,
"Store durable memories only in memory/YYYY-MM-DD.md (create memory/ if needed)." ,
"You may reply, but usually NO_REPLY is correct." ,
].join(" " ),
});
const postCompaction = [
"[Post-compaction context refresh]" ,
"" ,
"Session was just compacted. The conversation summary above is a hint, NOT a substitute for your startup sequence." ,
"" ,
"Critical rules from AGENTS.md:" ,
"" ,
"## Session Startup" ,
"Read AGENTS.md and MEMORY.md before responding." ,
"" ,
"## Red Lines" ,
"Do not delete production data." ,
"" ,
"Current time: Sunday, March 15th, 2026 - 9:30 PM (America/Los_Angeles) / 2026-03-16 04:30 UTC" ,
].join("\n" );
const postCompactionSystemPrompt = buildSystemPrompt({
workspaceDir,
extraSystemPrompt: buildInboundMetaSystemPrompt({
Provider: "slack" ,
Surface: "slack" ,
OriginatingChannel: "slack" ,
OriginatingTo: "D123" ,
AccountId: "A1" ,
ChatType: "direct" ,
}),
});
return {
scenario: "maintenance-prompts" ,
focus: "Memory flush and post-compaction maintenance prompts" ,
expectedStableSystemAfterTurnIds: [],
turns: [
{
id: "t1" ,
label: "Pre-compaction memory flush run" ,
systemPrompt: memoryFlushSystemPrompt,
bodyPrompt: memoryFlushPrompt,
notes: [
"Writes to memory/2026-03-15.md" ,
"Separate maintenance run; expected to differ from normal user turns" ,
],
},
{
id: "t2" ,
label: "Post-compaction refresh context run" ,
systemPrompt: postCompactionSystemPrompt,
bodyPrompt: postCompaction,
notes: [
"Separate maintenance context payload" ,
"Expected to differ from normal user turns" ,
],
},
],
};
}
export async function createWorkspaceWithPromptCompositionFiles(): Promise<string> {
const workspaceDir = await makeTempWorkspace("openclaw-prompt-cache-" );
await writeWorkspaceFile({
dir: workspaceDir,
name: "AGENTS.md" ,
content: [
"# AGENTS.md" ,
"" ,
"## Session Startup" ,
"Read AGENTS.md and TOOLS.md before making changes." ,
"" ,
"## Red Lines" ,
"Do not rewrite user commits." ,
].join("\n" ),
});
await writeWorkspaceFile({
dir: workspaceDir,
name: "TOOLS.md" ,
content: "# TOOLS.md\n\nUse rg before grep.\n" ,
});
await writeWorkspaceFile({
dir: workspaceDir,
name: "SOUL.md" ,
content: "# SOUL.md\n\nBe concise but kind.\n" ,
});
return workspaceDir;
}
export async function createPromptCompositionScenarios(): Promise<{
workspaceDir: string;
warningWorkspaceDir: string;
scenarios: PromptScenario[];
cleanup: () => Promise<void >;
}> {
const workspaceDir = await createWorkspaceWithPromptCompositionFiles();
const warningWorkspaceDir = await makeTempWorkspace("openclaw-prompt-cache-warning-" );
const scenarios = [
createDirectScenario(workspaceDir),
createGroupScenario(workspaceDir),
await createToolRichScenario(workspaceDir),
await createBootstrapWarningScenario(warningWorkspaceDir),
await createMaintenanceScenario(workspaceDir),
];
return {
workspaceDir,
warningWorkspaceDir,
scenarios,
cleanup: async () => {
await fs.rm(workspaceDir, { recursive: true , force: true });
await fs.rm(warningWorkspaceDir, { recursive: true , force: true });
},
};
}
Messung V0.5 in Prozent C=100 H=100 G=100
¤ Dauer der Verarbeitung: 0.12 Sekunden
(vorverarbeitet am 2026-06-10)
¤
*© Formatika GbR, Deutschland