fix: avoid no reply prompt in message tool mode (#75779)

This commit is contained in:
pashpashpash
2026-05-01 13:02:47 -07:00
committed by GitHub
parent 5f3a17e2fd
commit 064d455fd8
6 changed files with 36 additions and 2 deletions

View File

@@ -6,6 +6,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Agents/Codex: stop prompting message-tool-only source turns to finish with `NO_REPLY`, so quiet turns are represented by not calling the visible message tool instead of conflicting final-text instructions. Thanks @pashpashpash.
- Gateway/config: report failed backup restores as failed in logs and config observe audit records instead of marking them valid. (#70515) Thanks @davidangularme.
- Compaction: use the active session model fallback chain for implicit summarization failures without persisting fallback model selection, so Azure content-filter 400s can recover. Fixes #64960. (#74470) Thanks @jalehman and @OpenCodeEngineer.

View File

@@ -803,6 +803,8 @@ describe("buildAgentSystemPrompt", () => {
expect(prompt).toContain("use `message(action=send)` for visible channel output");
expect(prompt).toContain("The target defaults to the current source channel");
expect(prompt).toContain("final answers are private in this mode");
expect(prompt).not.toContain("## Silent Replies");
expect(prompt).not.toContain(SILENT_REPLY_TOKEN);
expect(prompt).not.toContain(
`respond with ONLY: ${SILENT_REPLY_TOKEN} (avoid duplicate replies)`,
);

View File

@@ -351,6 +351,9 @@ function buildMessagingSection(params: {
const showGenericInlineButtonHint = params.runtimeChannel !== "slack";
const hasSessionsSpawn = params.availableTools.has("sessions_spawn");
const hasSubagents = params.availableTools.has("subagents");
const completionEventGuidance = messageToolOnly
? "- Runtime-generated completion events may ask for a user update. Rewrite those in your normal assistant voice and send the update (do not forward raw internal metadata or default to a silent placeholder)."
: `- Runtime-generated completion events may ask for a user update. Rewrite those in your normal assistant voice and send the update (do not forward raw internal metadata or default to ${SILENT_REPLY_TOKEN}).`;
const subagentOrchestrationGuidance = hasSessionsSpawn
? hasSubagents
? '- Sub-agent orchestration → use `sessions_spawn(...)` to start delegated work; omit `context` for isolated children, set `context:"fork"` only when the child needs the current transcript; use `subagents(action=list|steer|kill)` to manage already-spawned children.'
@@ -365,7 +368,7 @@ function buildMessagingSection(params: {
: "- Reply in current session → automatically routes to the source channel (Signal, Telegram, etc.)",
"- Cross-session messaging → use sessions_send(sessionKey, message)",
subagentOrchestrationGuidance,
`- Runtime-generated completion events may ask for a user update. Rewrite those in your normal assistant voice and send the update (do not forward raw internal metadata or default to ${SILENT_REPLY_TOKEN}).`,
completionEventGuidance,
"- Never use exec/curl for provider messaging; OpenClaw handles all routing internally.",
params.availableTools.has("message")
? [
@@ -665,7 +668,10 @@ export function buildAgentSystemPrompt(params: {
const messageChannelOptions = listDeliverableMessageChannels().join("|");
const promptMode = params.promptMode ?? "full";
const isMinimal = promptMode === "minimal" || promptMode === "none";
const silentReplyPromptMode = params.silentReplyPromptMode ?? "generic";
const sourceMessageToolOnly = params.sourceReplyDeliveryMode === "message_tool_only";
const silentReplyPromptMode = sourceMessageToolOnly
? "none"
: (params.silentReplyPromptMode ?? "generic");
const sandboxContainerWorkspace = params.sandboxInfo?.containerWorkspaceDir?.trim();
const sanitizedWorkspaceDir = sanitizeForPromptLiteral(params.workspaceDir);
const sanitizedSandboxContainerWorkspace = sandboxContainerWorkspace

View File

@@ -450,6 +450,7 @@ export async function runPreparedReply(
const directChatContext = isDirectChat
? buildDirectChatContext({
sessionCtx: promptSessionCtx,
sourceReplyDeliveryMode: opts?.sourceReplyDeliveryMode,
silentReplyPolicy: silentReplySettings.policy,
silentReplyRewrite: silentReplySettings.rewrite,
silentToken: SILENT_REPLY_TOKEN,

View File

@@ -87,6 +87,19 @@ describe("group runtime loading", () => {
silentToken: "NO_REPLY",
}),
).toContain('reply with exactly "NO_REPLY"');
const toolOnlyContext = groups.buildDirectChatContext({
sessionCtx: { ChatType: "direct", Provider: "telegram" },
sourceReplyDeliveryMode: "message_tool_only",
silentReplyPolicy: "allow",
silentReplyRewrite: true,
silentToken: "NO_REPLY",
});
expect(toolOnlyContext).toContain("Normal final replies are private");
expect(toolOnlyContext).toContain("message tool with action=send");
expect(toolOnlyContext).toContain("do not call message(action=send)");
expect(toolOnlyContext).not.toContain("NO_REPLY");
expect(toolOnlyContext).not.toContain("Your replies are automatically sent");
});
it("gates group silent-token instructions on the resolved silent reply policy", async () => {

View File

@@ -283,13 +283,24 @@ export function buildGroupChatContext(params: {
export function buildDirectChatContext(params: {
sessionCtx: TemplateContext;
sourceReplyDeliveryMode?: SourceReplyDeliveryMode;
silentReplyPolicy?: SilentReplyPolicy;
silentReplyRewrite?: boolean;
silentToken: string;
}): string {
const providerLabel = resolveProviderLabel(params.sessionCtx.Provider);
const messageToolOnly = params.sourceReplyDeliveryMode === "message_tool_only";
const lines: string[] = [];
lines.push(`You are in a ${providerLabel} direct conversation.`);
if (messageToolOnly) {
lines.push(
"Normal final replies are private and are not automatically sent to this conversation. To post visible output here, use the message tool with action=send; the target defaults to this conversation.",
);
lines.push(
"If no visible direct response is needed, do not call message(action=send). Your normal final answer stays private and will not be posted to the conversation.",
);
return lines.join(" ");
}
lines.push("Your replies are automatically sent to this conversation.");
if (params.silentReplyPolicy === "allow") {
lines.push(