mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:20:43 +00:00
perf(prompt): stabilize channel prompt suffix
This commit is contained in:
@@ -62,6 +62,7 @@ Docs: https://docs.openclaw.ai
|
||||
- CLI/model probes: request trusted operator scope for `infer model run --gateway --model <provider/model>` so Gateway raw model smokes can use one-off provider/model overrides instead of being rejected before provider auth resolution. Fixes #73759. Thanks @chrislro.
|
||||
- CLI/image describe: pass `--prompt` and `--timeout-ms` through `infer image describe` and `describe-many`, so custom vision instructions and slow local model budgets reach media-understanding providers such as Ollama, OpenAI, Google, and OpenRouter. Refs #63700. Thanks @cedricjanssens.
|
||||
- Model selection: include the rejected provider/model ref and allowlist recovery hint when a stored session override is cleared, so local model selections such as Gemma GGUF variants do not fall back to the default with a generic message. Refs #71069. Thanks @CyberRaccoonTeam.
|
||||
- Local model prompt caching: keep stable Project Context above volatile channel/session prompt guidance and stop embedding current channel names in the message tool description, so Ollama, MLX, llama.cpp, and other prefix-cache backends avoid avoidable full prompt reprocessing across channel turns. Fixes #40256; supersedes #40296. Thanks @rhclaw and @sriram369.
|
||||
- WhatsApp/Web: pass explicit Baileys socket timings into every WhatsApp Web socket and expose `web.whatsapp.*` keepalive, connect, and query timeout settings so unstable networks can avoid repeated 408 disconnect and opening-handshake timeout loops. Fixes #56365. (#73580) Thanks @velvet-shark.
|
||||
- Channels/Telegram: persist native command metadata on target sessions so topic, helper, and ACP-bound slash commands keep their session metadata attached to the routed conversation. (#57548) Thanks @GaosCode.
|
||||
- Channels/native commands: keep validated native slash command replies visible in group chats while preserving explicit owner allowlists for command authorization. (#73672) Thanks @obviyus.
|
||||
|
||||
@@ -53,6 +53,14 @@ The prompt is intentionally compact and uses fixed sections:
|
||||
- **Runtime**: host, OS, node, model, repo root (when detected), thinking level (one line).
|
||||
- **Reasoning**: current visibility level + /reasoning toggle hint.
|
||||
|
||||
OpenClaw keeps large stable content, including **Project Context**, above the
|
||||
internal prompt cache boundary. Volatile channel/session sections such as
|
||||
Control UI embed guidance, **Messaging**, **Voice**, **Group Chat Context**,
|
||||
**Reactions**, **Heartbeats**, and **Runtime** are appended below that boundary
|
||||
so local backends with prefix caches can reuse the stable workspace prefix
|
||||
across channel turns. Tool descriptions should likewise avoid embedding current
|
||||
channel names when the accepted schema already carries that runtime detail.
|
||||
|
||||
The Tooling section also includes runtime guidance for long-running work:
|
||||
|
||||
- use cron for future follow-up (`check back later`, reminders, recurring work)
|
||||
|
||||
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
|
||||
import { SILENT_REPLY_TOKEN } from "../auto-reply/tokens.js";
|
||||
import { typedCases } from "../test-utils/typed-cases.js";
|
||||
import { buildSubagentSystemPrompt } from "./subagent-system-prompt.js";
|
||||
import { SYSTEM_PROMPT_CACHE_BOUNDARY } from "./system-prompt-cache-boundary.js";
|
||||
import {
|
||||
buildAgentSystemPrompt,
|
||||
buildAgentUserPromptPrefix,
|
||||
@@ -952,6 +953,41 @@ describe("buildAgentSystemPrompt", () => {
|
||||
expect(prompt).toContain("## Reactions");
|
||||
expect(prompt).toContain("Reactions are enabled for Telegram in MINIMAL mode.");
|
||||
});
|
||||
|
||||
it("keeps stable project context before volatile channel guidance for prefix-cache reuse", () => {
|
||||
const prompt = buildAgentSystemPrompt({
|
||||
workspaceDir: "/tmp/openclaw",
|
||||
toolNames: ["message"],
|
||||
runtimeInfo: {
|
||||
channel: "telegram",
|
||||
capabilities: ["inlineButtons"],
|
||||
canvasRootDir: "/tmp/canvas",
|
||||
},
|
||||
contextFiles: [
|
||||
{
|
||||
path: "AGENTS.md",
|
||||
content: "Project rules mention ## Messaging, ## Group Chat Context, and ## Reactions.",
|
||||
},
|
||||
],
|
||||
extraSystemPrompt: "Current group-chat facts",
|
||||
reactionGuidance: { level: "minimal", channel: "Telegram" },
|
||||
ttsHint: "Use short voice-friendly replies.",
|
||||
});
|
||||
|
||||
const projectContextPos = prompt.indexOf("# Project Context");
|
||||
const boundaryPos = prompt.indexOf(SYSTEM_PROMPT_CACHE_BOUNDARY);
|
||||
const messagingPos = prompt.lastIndexOf("## Messaging");
|
||||
const groupChatPos = prompt.lastIndexOf("## Group Chat Context");
|
||||
const reactionsPos = prompt.lastIndexOf("## Reactions");
|
||||
const voicePos = prompt.lastIndexOf("## Voice (TTS)");
|
||||
|
||||
expect(projectContextPos).toBeGreaterThan(-1);
|
||||
expect(boundaryPos).toBeGreaterThan(projectContextPos);
|
||||
expect(messagingPos).toBeGreaterThan(boundaryPos);
|
||||
expect(groupChatPos).toBeGreaterThan(boundaryPos);
|
||||
expect(reactionsPos).toBeGreaterThan(boundaryPos);
|
||||
expect(voicePos).toBeGreaterThan(boundaryPos);
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildAgentUserPromptPrefix", () => {
|
||||
|
||||
@@ -903,46 +903,8 @@ export function buildAgentSystemPrompt(params: {
|
||||
"These user-editable files are loaded by OpenClaw and included below in Project Context.",
|
||||
"",
|
||||
...buildAssistantOutputDirectivesSection(isMinimal),
|
||||
...buildWebchatCanvasSection({
|
||||
isMinimal,
|
||||
runtimeChannel,
|
||||
canvasRootDir: params.runtimeInfo?.canvasRootDir,
|
||||
}),
|
||||
...buildMessagingSection({
|
||||
isMinimal,
|
||||
availableTools,
|
||||
messageChannelOptions,
|
||||
inlineButtonsEnabled,
|
||||
runtimeChannel,
|
||||
messageToolHints: params.messageToolHints,
|
||||
sourceReplyDeliveryMode: params.sourceReplyDeliveryMode,
|
||||
}),
|
||||
...buildVoiceSection({ isMinimal, ttsHint: params.ttsHint }),
|
||||
];
|
||||
|
||||
if (params.reactionGuidance) {
|
||||
const { level, channel } = params.reactionGuidance;
|
||||
const guidanceText =
|
||||
level === "minimal"
|
||||
? [
|
||||
`Reactions are enabled for ${channel} in MINIMAL mode.`,
|
||||
"React ONLY when truly relevant:",
|
||||
"- Acknowledge important user requests or confirmations",
|
||||
"- Express genuine sentiment (humor, appreciation) sparingly",
|
||||
"- Avoid reacting to routine messages or your own replies",
|
||||
"Guideline: at most 1 reaction per 5-10 exchanges.",
|
||||
].join("\n")
|
||||
: [
|
||||
`Reactions are enabled for ${channel} in EXTENSIVE mode.`,
|
||||
"Feel free to react liberally:",
|
||||
"- Acknowledge messages with appropriate emojis",
|
||||
"- Express sentiment and personality through reactions",
|
||||
"- React to interesting content, humor, or notable events",
|
||||
"- Use reactions to confirm understanding or agreement",
|
||||
"Guideline: react whenever it feels natural.",
|
||||
].join("\n");
|
||||
lines.push("## Reactions", guidanceText, "");
|
||||
}
|
||||
if (reasoningHint) {
|
||||
lines.push("## Reasoning Format", reasoningHint, "");
|
||||
}
|
||||
@@ -993,12 +955,55 @@ export function buildAgentSystemPrompt(params: {
|
||||
}),
|
||||
);
|
||||
|
||||
// Channel/session-specific guidance lives below the cache boundary so large
|
||||
// stable workspace context can remain a byte-identical prefix across turns.
|
||||
lines.push(
|
||||
...buildWebchatCanvasSection({
|
||||
isMinimal,
|
||||
runtimeChannel,
|
||||
canvasRootDir: params.runtimeInfo?.canvasRootDir,
|
||||
}),
|
||||
...buildMessagingSection({
|
||||
isMinimal,
|
||||
availableTools,
|
||||
messageChannelOptions,
|
||||
inlineButtonsEnabled,
|
||||
runtimeChannel,
|
||||
messageToolHints: params.messageToolHints,
|
||||
sourceReplyDeliveryMode: params.sourceReplyDeliveryMode,
|
||||
}),
|
||||
...buildVoiceSection({ isMinimal, ttsHint: params.ttsHint }),
|
||||
);
|
||||
|
||||
if (extraSystemPrompt) {
|
||||
// Use "Subagent Context" header for minimal mode (subagents), otherwise "Group Chat Context"
|
||||
const contextHeader =
|
||||
promptMode === "minimal" ? "## Subagent Context" : "## Group Chat Context";
|
||||
lines.push(contextHeader, extraSystemPrompt, "");
|
||||
}
|
||||
if (params.reactionGuidance) {
|
||||
const { level, channel } = params.reactionGuidance;
|
||||
const guidanceText =
|
||||
level === "minimal"
|
||||
? [
|
||||
`Reactions are enabled for ${channel} in MINIMAL mode.`,
|
||||
"React ONLY when truly relevant:",
|
||||
"- Acknowledge important user requests or confirmations",
|
||||
"- Express genuine sentiment (humor, appreciation) sparingly",
|
||||
"- Avoid reacting to routine messages or your own replies",
|
||||
"Guideline: at most 1 reaction per 5-10 exchanges.",
|
||||
].join("\n")
|
||||
: [
|
||||
`Reactions are enabled for ${channel} in EXTENSIVE mode.`,
|
||||
"Feel free to react liberally:",
|
||||
"- Acknowledge messages with appropriate emojis",
|
||||
"- Express sentiment and personality through reactions",
|
||||
"- React to interesting content, humor, or notable events",
|
||||
"- Use reactions to confirm understanding or agreement",
|
||||
"Guideline: react whenever it feels natural.",
|
||||
].join("\n");
|
||||
lines.push("## Reactions", guidanceText, "");
|
||||
}
|
||||
if (providerDynamicSuffix) {
|
||||
lines.push(providerDynamicSuffix, "");
|
||||
}
|
||||
|
||||
@@ -601,8 +601,10 @@ describe("message tool schema scoping", () => {
|
||||
|
||||
expect(getActionEnum(getToolProperties(scopedTool))).toContain("react");
|
||||
expect(getActionEnum(getToolProperties(unscopedTool))).not.toContain("react");
|
||||
expect(scopedTool.description).toContain("telegram (react, send)");
|
||||
expect(unscopedTool.description).not.toContain("telegram (react, send)");
|
||||
expect(scopedTool.description).toContain("Supports actions: react, send.");
|
||||
expect(unscopedTool.description).toContain("Supports actions: send.");
|
||||
expect(scopedTool.description).not.toContain("telegram (");
|
||||
expect(unscopedTool.description).not.toContain("telegram (");
|
||||
});
|
||||
|
||||
it("routes full discovery context into plugin action discovery", () => {
|
||||
@@ -779,7 +781,7 @@ describe("message tool description", () => {
|
||||
expect(tool.description).not.toContain("leaveGroup");
|
||||
});
|
||||
|
||||
it("includes other configured channels when currentChannel is set", () => {
|
||||
it("describes accepted actions without channel-specific wording when currentChannel is set", () => {
|
||||
const signalPlugin = createChannelPlugin({
|
||||
id: "signal",
|
||||
label: "Signal",
|
||||
@@ -808,11 +810,12 @@ describe("message tool description", () => {
|
||||
currentChannelProvider: "signal",
|
||||
});
|
||||
|
||||
// Current channel actions are listed
|
||||
expect(tool.description).toContain("Current channel (signal) supports: react, send.");
|
||||
// Other configured channels are also listed
|
||||
expect(tool.description).toContain("Other configured channels:");
|
||||
expect(tool.description).toContain("telegram (delete, edit, react, send, topic-create)");
|
||||
expect(tool.description).toContain(
|
||||
"Supports actions: delete, edit, react, send, topic-create.",
|
||||
);
|
||||
expect(tool.description).not.toContain("Current channel");
|
||||
expect(tool.description).not.toContain("Other configured channels");
|
||||
expect(tool.description).not.toContain("telegram (");
|
||||
});
|
||||
|
||||
it("does not advertise cross-channel actions whose params are hidden by current-channel schema", () => {
|
||||
@@ -885,10 +888,11 @@ describe("message tool description", () => {
|
||||
currentChannelProvider: "sig",
|
||||
});
|
||||
|
||||
expect(tool.description).toContain("Current channel (signal) supports: react, send.");
|
||||
expect(tool.description).toContain("Supports actions: react, send.");
|
||||
expect(tool.description).not.toContain("Current channel");
|
||||
});
|
||||
|
||||
it("does not include 'Other configured channels' when only one channel is configured", () => {
|
||||
it("keeps the current-channel description stable when only one channel is configured", () => {
|
||||
setActivePluginRegistry(
|
||||
createTestRegistry([{ pluginId: "bluebubbles", source: "test", plugin: bluebubblesPlugin }]),
|
||||
);
|
||||
@@ -898,7 +902,8 @@ describe("message tool description", () => {
|
||||
currentChannelProvider: "bluebubbles",
|
||||
});
|
||||
|
||||
expect(tool.description).toContain("Current channel (bluebubbles) supports:");
|
||||
expect(tool.description).toContain("Supports actions:");
|
||||
expect(tool.description).not.toContain("Current channel");
|
||||
expect(tool.description).not.toContain("Other configured channels");
|
||||
});
|
||||
|
||||
@@ -970,7 +975,7 @@ describe("message tool description", () => {
|
||||
config: {} as never,
|
||||
});
|
||||
|
||||
expect(tool.description).toContain("Supports actions: send, broadcast.");
|
||||
expect(tool.description).toContain("Supports actions: broadcast, send.");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -587,49 +587,17 @@ function buildMessageToolDescription(options?: {
|
||||
}
|
||||
: undefined;
|
||||
|
||||
// If we have a current channel, show its actions and list other configured channels
|
||||
if (currentChannel && messageToolDiscoveryParams) {
|
||||
const channelActions = listChannelSupportedActions(
|
||||
buildMessageActionDiscoveryInput(messageToolDiscoveryParams, currentChannel),
|
||||
);
|
||||
if (channelActions.length > 0) {
|
||||
// Always include "send" as a base action
|
||||
const allActions = new Set<ChannelMessageActionName | "send">(["send", ...channelActions]);
|
||||
const actionList = Array.from(allActions).toSorted().join(", ");
|
||||
let desc = `${baseDescription} Current channel (${currentChannel}) supports: ${actionList}.`;
|
||||
|
||||
// Include other configured channels so cron/isolated agents can discover them
|
||||
const otherChannels: string[] = [];
|
||||
for (const plugin of listChannelPlugins()) {
|
||||
if (plugin.id === currentChannel) {
|
||||
continue;
|
||||
}
|
||||
const actions = listCrossChannelSchemaSupportedMessageActions(
|
||||
buildMessageActionDiscoveryInput(messageToolDiscoveryParams, plugin.id),
|
||||
);
|
||||
if (actions.length > 0) {
|
||||
const all = new Set<ChannelMessageActionName | "send">(["send", ...actions]);
|
||||
otherChannels.push(`${plugin.id} (${Array.from(all).toSorted().join(", ")})`);
|
||||
}
|
||||
}
|
||||
if (otherChannels.length > 0) {
|
||||
desc += ` Other configured channels: ${otherChannels.join(", ")}.`;
|
||||
}
|
||||
|
||||
return appendMessageToolReadHint(
|
||||
desc,
|
||||
Array.from(allActions) as Iterable<ChannelMessageActionName | "send">,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to generic description with all configured actions
|
||||
if (messageToolDiscoveryParams) {
|
||||
const actions = listAllMessageToolActions(messageToolDiscoveryParams);
|
||||
const actions = currentChannel
|
||||
? resolveMessageToolSchemaActions(messageToolDiscoveryParams)
|
||||
: listAllMessageToolActions(messageToolDiscoveryParams);
|
||||
if (actions.length > 0) {
|
||||
const sortedActions = Array.from(new Set(actions)).toSorted() as Array<
|
||||
ChannelMessageActionName | "send"
|
||||
>;
|
||||
return appendMessageToolReadHint(
|
||||
`${baseDescription} Supports actions: ${actions.join(", ")}.`,
|
||||
actions,
|
||||
`${baseDescription} Supports actions: ${sortedActions.join(", ")}.`,
|
||||
sortedActions,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user