refactor(agents): use silent token constant in prompts

This commit is contained in:
sebslight
2026-02-16 08:19:15 -05:00
parent e3e8046a93
commit 553d17f8af
7 changed files with 65 additions and 7 deletions

View File

@@ -1,4 +1,5 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { SILENT_REPLY_TOKEN } from "../auto-reply/tokens.js";
const agentSpy = vi.fn(async () => ({ runId: "run-main", status: "ok" }));
const sessionsDeleteSpy = vi.fn();
@@ -222,7 +223,7 @@ describe("subagent announce formatting", () => {
expect(msg).toContain("[sessionId: child-session-usage]");
expect(msg).toContain("A completed subagent task is ready for user delivery.");
expect(msg).toContain(
"Reply ONLY: NO_REPLY if this exact result was already delivered to the user in this same turn.",
`Reply ONLY: ${SILENT_REPLY_TOKEN} if this exact result was already delivered to the user in this same turn.`,
);
expect(msg).toContain("step-0");
expect(msg).toContain("step-139");

View File

@@ -1,4 +1,5 @@
import { resolveQueueSettings } from "../auto-reply/reply/queue.js";
import { SILENT_REPLY_TOKEN } from "../auto-reply/tokens.js";
import { loadConfig } from "../config/config.js";
import {
loadSessionStore,
@@ -364,9 +365,9 @@ function buildAnnounceReplyInstruction(params: {
return `There are still ${params.remainingActiveSubagentRuns} active subagent ${activeRunsLabel} for this session. If they are part of the same workflow, wait for the remaining results before sending a user update. If they are unrelated, respond normally using only the result above.`;
}
if (params.requesterIsSubagent) {
return "Convert this completion into a concise internal orchestration update for your parent agent in your own words. Keep this internal context private (don't mention system/log/stats/session details or announce type). If this result is duplicate or no update is needed, reply ONLY: NO_REPLY.";
return `Convert this completion into a concise internal orchestration update for your parent agent in your own words. Keep this internal context private (don't mention system/log/stats/session details or announce type). If this result is duplicate or no update is needed, reply ONLY: ${SILENT_REPLY_TOKEN}.`;
}
return `A completed ${params.announceType} is ready for user delivery. Convert the result above into your normal assistant voice and send that user-facing update now. Keep this internal context private (don't mention system/log/stats/session details or announce type), and do not copy the system message verbatim. Reply ONLY: NO_REPLY if this exact result was already delivered to the user in this same turn.`;
return `A completed ${params.announceType} is ready for user delivery. Convert the result above into your normal assistant voice and send that user-facing update now. Keep this internal context private (don't mention system/log/stats/session details or announce type), and do not copy the system message verbatim. Reply ONLY: ${SILENT_REPLY_TOKEN} if this exact result was already delivered to the user in this same turn.`;
}
export async function runSubagentAnnounceFlow(params: {

View File

@@ -1,4 +1,5 @@
import { describe, expect, it } from "vitest";
import { SILENT_REPLY_TOKEN } from "../auto-reply/tokens.js";
import { buildSubagentSystemPrompt } from "./subagent-announce.js";
import { buildAgentSystemPrompt, buildRuntimeLine } from "./system-prompt.js";
@@ -367,7 +368,7 @@ describe("buildAgentSystemPrompt", () => {
expect(prompt).toContain("message: Send messages and channel actions");
expect(prompt).toContain("### message tool");
expect(prompt).toContain("respond with ONLY: NO_REPLY");
expect(prompt).toContain(`respond with ONLY: ${SILENT_REPLY_TOKEN}`);
});
it("includes runtime provider capabilities when present", () => {

View File

@@ -112,7 +112,7 @@ function buildMessagingSection(params: {
"- Cross-session messaging → use sessions_send(sessionKey, message)",
"- Sub-agent orchestration → use subagents(action=list|steer|kill)",
"- `[System Message] ...` blocks are internal context and are not user-visible by default.",
"- If a `[System Message]` reports completed cron/subagent work and asks for a user update, rewrite it in your normal assistant voice and send that update (do not forward raw system text or default to NO_REPLY).",
`- If a \`[System Message]\` reports completed cron/subagent work and asks for a user update, rewrite it in your normal assistant voice and send that update (do not forward raw system text or default to ${SILENT_REPLY_TOKEN}).`,
"- Never use exec/curl for provider messaging; OpenClaw handles all routing internally.",
params.availableTools.has("message")
? [

View File

@@ -0,0 +1,16 @@
import { describe, expect, it, vi } from "vitest";
vi.mock("../../auto-reply/tokens.js", () => ({
SILENT_REPLY_TOKEN: "QUIET_TOKEN",
}));
const { createTtsTool } = await import("./tts-tool.js");
describe("createTtsTool", () => {
it("uses SILENT_REPLY_TOKEN in guidance text", () => {
const tool = createTtsTool();
expect(tool.description).toContain("QUIET_TOKEN");
expect(tool.description).not.toContain("NO_REPLY");
});
});

View File

@@ -2,6 +2,7 @@ import { Type } from "@sinclair/typebox";
import type { OpenClawConfig } from "../../config/config.js";
import type { GatewayMessageChannel } from "../../utils/message-channel.js";
import type { AnyAgentTool } from "./common.js";
import { SILENT_REPLY_TOKEN } from "../../auto-reply/tokens.js";
import { loadConfig } from "../../config/config.js";
import { textToSpeech } from "../../tts/tts.js";
import { readStringParam } from "./common.js";
@@ -20,8 +21,7 @@ export function createTtsTool(opts?: {
return {
label: "TTS",
name: "tts",
description:
"Convert text to speech. Audio is delivered automatically from the tool result — reply with NO_REPLY after a successful call to avoid duplicate messages.",
description: `Convert text to speech. Audio is delivered automatically from the tool result — reply with ${SILENT_REPLY_TOKEN} after a successful call to avoid duplicate messages.`,
parameters: TtsToolSchema,
execute: async (_toolCallId, args) => {
const params = args as Record<string, unknown>;

View File

@@ -138,6 +138,45 @@ describe("dispatchReplyFromConfig", () => {
);
});
it("routes media-only tool results when summaries are suppressed", async () => {
mocks.tryFastAbortFromMessage.mockResolvedValue({
handled: false,
aborted: false,
});
mocks.routeReply.mockClear();
const cfg = {} as OpenClawConfig;
const dispatcher = createDispatcher();
const ctx = buildTestCtx({
Provider: "slack",
ChatType: "group",
AccountId: "acc-1",
OriginatingChannel: "telegram",
OriginatingTo: "telegram:999",
});
const replyResolver = async (
_ctx: MsgContext,
opts: GetReplyOptions | undefined,
_cfg: OpenClawConfig,
) => {
expect(opts?.onToolResult).toBeDefined();
await opts?.onToolResult?.({
text: "NO_REPLY",
mediaUrls: ["https://example.com/tts-routed.opus"],
});
return undefined;
};
await dispatchReplyFromConfig({ ctx, cfg, dispatcher, replyResolver });
expect(dispatcher.sendToolResult).not.toHaveBeenCalled();
expect(dispatcher.sendFinalReply).not.toHaveBeenCalled();
expect(mocks.routeReply).toHaveBeenCalledTimes(1);
const routed = mocks.routeReply.mock.calls[0]?.[0] as { payload?: ReplyPayload } | undefined;
expect(routed?.payload?.mediaUrls).toEqual(["https://example.com/tts-routed.opus"]);
expect(routed?.payload?.text).toBeUndefined();
});
it("provides onToolResult in DM sessions", async () => {
mocks.tryFastAbortFromMessage.mockResolvedValue({
handled: false,