mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 10:16:44 +00:00
fix(discord): suppress internal agent failure traces
This commit is contained in:
committed by
Ayaan Zaidi
parent
a9f014e9df
commit
a8bf14da84
@@ -2504,7 +2504,7 @@ describe("processDiscordMessage draft streaming", () => {
|
||||
expect(deliverDiscordReply).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("delivers tool warning finals when no recovered reply is available", async () => {
|
||||
it("suppresses pure tool warning finals when no recovered reply is available", async () => {
|
||||
const draftStream = createMockDraftStreamForTest();
|
||||
dispatchInboundMessage.mockImplementationOnce(async (params?: DispatchInboundParams) => {
|
||||
await params?.dispatcher.sendFinalReply(createNonTerminalToolWarningPayload());
|
||||
@@ -2519,18 +2519,10 @@ describe("processDiscordMessage draft streaming", () => {
|
||||
|
||||
expect(editMessageDiscord).not.toHaveBeenCalled();
|
||||
expect(draftStream.clear).toHaveBeenCalledTimes(1);
|
||||
expect(deliverDiscordReply).toHaveBeenCalledTimes(1);
|
||||
expect(firstMockArg(deliverDiscordReply, "deliverDiscordReply")).toMatchObject({
|
||||
replies: [
|
||||
{
|
||||
text: "⚠️ 🛠️ `run openclaw definitely-not-a-real-subcommand (agent)` failed",
|
||||
isError: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(deliverDiscordReply).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("delivers tool warning finals when the recovered reply fails to send", async () => {
|
||||
it("suppresses tool warning finals when the recovered reply fails to send", async () => {
|
||||
deliverDiscordReply.mockRejectedValueOnce(new Error("send failed"));
|
||||
dispatchInboundMessage.mockImplementationOnce(async (params?: DispatchInboundParams) => {
|
||||
await params?.dispatcher.sendFinalReply({ text: "delivery failed" });
|
||||
@@ -2549,21 +2541,13 @@ describe("processDiscordMessage draft streaming", () => {
|
||||
|
||||
await runProcessDiscordMessage(ctx);
|
||||
|
||||
expect(deliverDiscordReply).toHaveBeenCalledTimes(2);
|
||||
expect(deliverDiscordReply).toHaveBeenCalledTimes(1);
|
||||
expect(firstMockArg(deliverDiscordReply, "deliverDiscordReply")).toMatchObject({
|
||||
replies: [{ text: "delivery failed" }],
|
||||
});
|
||||
expect(deliverDiscordReply.mock.calls[1]?.[0]).toMatchObject({
|
||||
replies: [
|
||||
{
|
||||
text: "⚠️ 🛠️ `run openclaw definitely-not-a-real-subcommand (agent)` failed",
|
||||
isError: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps mutating tool warning finals after successful-looking replies", async () => {
|
||||
it("suppresses mutating tool warning finals after successful-looking replies", async () => {
|
||||
const draftStream = createMockDraftStreamForTest();
|
||||
dispatchInboundMessage.mockImplementationOnce(async (params?: DispatchInboundParams) => {
|
||||
await params?.dispatcher.sendFinalReply({ text: "Done." });
|
||||
@@ -2582,15 +2566,7 @@ describe("processDiscordMessage draft streaming", () => {
|
||||
|
||||
expectPreviewEditContent("Done.");
|
||||
expect(draftStream.clear).not.toHaveBeenCalled();
|
||||
expect(deliverDiscordReply).toHaveBeenCalledTimes(1);
|
||||
expect(firstMockArg(deliverDiscordReply, "deliverDiscordReply")).toMatchObject({
|
||||
replies: [
|
||||
{
|
||||
text: "⚠️ 🛠️ `write file (agent)` failed",
|
||||
isError: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(deliverDiscordReply).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("suppresses reasoning payload delivery to Discord", async () => {
|
||||
|
||||
@@ -66,6 +66,7 @@ import { createDiscordDraftPreviewController } from "./message-handler.draft-pre
|
||||
import type { DiscordMessagePreflightContext } from "./message-handler.preflight.js";
|
||||
import { resolveForwardedMediaList, resolveMediaList } from "./message-utils.js";
|
||||
import { deliverDiscordReply } from "./reply-delivery.js";
|
||||
import { sanitizeDiscordFrontChannelReplyPayloads } from "./reply-safety.js";
|
||||
import { createDiscordReplyTypingFeedback } from "./reply-typing-feedback.js";
|
||||
import {
|
||||
DISCORD_ATTACHMENT_IDLE_TIMEOUT_MS,
|
||||
@@ -111,7 +112,10 @@ function isFallbackOnlyToolWarningFinal(payload: ReplyPayload): boolean {
|
||||
return !resolveSendableOutboundReplyParts(payload).hasMedia;
|
||||
}
|
||||
|
||||
type DiscordReplySkipReason = "aborted before delivery" | "reasoning payload";
|
||||
type DiscordReplySkipReason =
|
||||
| "aborted before delivery"
|
||||
| "reasoning payload"
|
||||
| "internal-only payload";
|
||||
|
||||
export function formatDiscordReplySkip(params: {
|
||||
kind: "tool" | "block" | "final";
|
||||
@@ -669,10 +673,24 @@ async function processDiscordMessageInner(
|
||||
})
|
||||
: payload.text;
|
||||
const effectivePayload = finalText !== payload.text ? { ...payload, text: finalText } : payload;
|
||||
const [deliverablePayload] = sanitizeDiscordFrontChannelReplyPayloads([effectivePayload], {
|
||||
kind: info.kind,
|
||||
});
|
||||
if (!deliverablePayload) {
|
||||
logVerbose(
|
||||
formatDiscordReplySkip({
|
||||
kind: info.kind,
|
||||
reason: "internal-only payload",
|
||||
target: deliverTarget,
|
||||
sessionKey: ctxPayload.SessionKey,
|
||||
}),
|
||||
);
|
||||
return { visibleReplySent: false };
|
||||
}
|
||||
const draftStream = draftPreview.draftStream;
|
||||
if (draftStream && draftPreview.isProgressMode && info.kind === "block") {
|
||||
const reply = resolveSendableOutboundReplyParts(effectivePayload);
|
||||
if (!reply.hasMedia && !payload.isError) {
|
||||
const reply = resolveSendableOutboundReplyParts(deliverablePayload);
|
||||
if (!reply.hasMedia && !deliverablePayload.isError) {
|
||||
return { visibleReplySent: false };
|
||||
}
|
||||
}
|
||||
@@ -680,22 +698,22 @@ async function processDiscordMessageInner(
|
||||
draftStream &&
|
||||
isFinal &&
|
||||
(!draftPreview.isProgressMode || draftPreview.hasProgressDraftStarted) &&
|
||||
!payload.isError;
|
||||
!deliverablePayload.isError;
|
||||
if (shouldFinalizeDraftPreview) {
|
||||
const reply = resolveSendableOutboundReplyParts(effectivePayload);
|
||||
const reply = resolveSendableOutboundReplyParts(deliverablePayload);
|
||||
const hasMedia = reply.hasMedia;
|
||||
const ttsSupplement = getReplyPayloadTtsSupplement(effectivePayload);
|
||||
const previewSourceText = finalText ?? ttsSupplement?.spokenText;
|
||||
const ttsSupplement = getReplyPayloadTtsSupplement(deliverablePayload);
|
||||
const previewSourceText = deliverablePayload.text ?? ttsSupplement?.spokenText;
|
||||
const previewFinalText = draftPreview.resolvePreviewFinalText(previewSourceText);
|
||||
const previewReplyToId = replyReference.peek();
|
||||
const hasExplicitReplyDirective =
|
||||
Boolean(effectivePayload.replyToTag || effectivePayload.replyToCurrent) ||
|
||||
Boolean(deliverablePayload.replyToTag || deliverablePayload.replyToCurrent) ||
|
||||
(typeof previewSourceText === "string" &&
|
||||
/\[\[\s*reply_to(?:_current|\s*:)/i.test(previewSourceText));
|
||||
|
||||
const result = await deliverWithFinalizableLivePreviewAdapter({
|
||||
kind: info.kind,
|
||||
payload: effectivePayload,
|
||||
payload: deliverablePayload,
|
||||
adapter: defineFinalizableLivePreviewAdapter({
|
||||
draft: {
|
||||
flush: () => draftPreview.flush(),
|
||||
@@ -710,7 +728,7 @@ async function processDiscordMessageInner(
|
||||
(hasMedia && !ttsSupplement) ||
|
||||
typeof previewFinalText !== "string" ||
|
||||
hasExplicitReplyDirective ||
|
||||
payload.isError
|
||||
deliverablePayload.isError
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -747,7 +765,7 @@ async function processDiscordMessageInner(
|
||||
replyReference.markSent();
|
||||
},
|
||||
buildSupplementalPayload: () =>
|
||||
ttsSupplement ? buildTtsSupplementMediaPayload(effectivePayload) : undefined,
|
||||
ttsSupplement ? buildTtsSupplementMediaPayload(deliverablePayload) : undefined,
|
||||
deliverSupplemental: async (supplementalPayload) => {
|
||||
if (isProcessAborted(abortSignal)) {
|
||||
return false;
|
||||
@@ -794,9 +812,9 @@ async function processDiscordMessageInner(
|
||||
const fallbackPayload =
|
||||
ttsSupplement &&
|
||||
ttsSupplement.visibleTextAlreadyDelivered !== true &&
|
||||
!effectivePayload.text?.trim()
|
||||
? { ...effectivePayload, text: ttsSupplement.spokenText }
|
||||
: effectivePayload;
|
||||
!deliverablePayload.text?.trim()
|
||||
? { ...deliverablePayload, text: ttsSupplement.spokenText }
|
||||
: deliverablePayload;
|
||||
const replyToId = replyReference.use();
|
||||
notifyFinalReplyStart();
|
||||
await deliverDiscordReply({
|
||||
@@ -849,7 +867,7 @@ async function processDiscordMessageInner(
|
||||
}
|
||||
await deliverDiscordReply({
|
||||
cfg,
|
||||
replies: [effectivePayload],
|
||||
replies: [deliverablePayload],
|
||||
target: deliverTarget,
|
||||
token,
|
||||
accountId,
|
||||
@@ -867,7 +885,7 @@ async function processDiscordMessageInner(
|
||||
kind: info.kind,
|
||||
});
|
||||
replyReference.markSent();
|
||||
if (isFinal && payload.isError !== true) {
|
||||
if (isFinal && deliverablePayload.isError !== true) {
|
||||
markUserFacingFinalDelivered();
|
||||
}
|
||||
return { visibleReplySent: true };
|
||||
|
||||
@@ -183,6 +183,7 @@ describe("deliverDiscordReply", () => {
|
||||
text: [
|
||||
"📊 Session Status: current",
|
||||
"🛠️ run git status",
|
||||
"⚠️ 🛠️ `run openclaw definitely-not-a-real-subcommand (agent)` failed",
|
||||
"🛠️ `gh pr view`",
|
||||
"🛠️ `docker compose up`",
|
||||
"🛠️ elevated · `cd /tmp && pnpm test`",
|
||||
@@ -204,6 +205,26 @@ describe("deliverDiscordReply", () => {
|
||||
expect(firstDeliverParams().payloads).toEqual([{ text: "Visible reply." }]);
|
||||
});
|
||||
|
||||
it("drops pure internal tool failure warnings at the final Discord send boundary", async () => {
|
||||
await deliverDiscordReply({
|
||||
replies: [
|
||||
{
|
||||
text: "⚠️ 🛠️ `run openclaw definitely-not-a-real-subcommand (agent)` failed",
|
||||
isError: true,
|
||||
},
|
||||
],
|
||||
target: "channel:101",
|
||||
token: "token",
|
||||
accountId: "default",
|
||||
runtime,
|
||||
cfg,
|
||||
textLimit: 2000,
|
||||
kind: "final",
|
||||
});
|
||||
|
||||
expect(sendDurableMessageBatchMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("strips serialized tool call blocks at the final Discord send boundary", async () => {
|
||||
await deliverDiscordReply({
|
||||
replies: [
|
||||
@@ -243,7 +264,7 @@ describe("deliverDiscordReply", () => {
|
||||
await deliverDiscordReply({
|
||||
replies: [
|
||||
{
|
||||
text: "commentary: calling tool\nanalysis: inspect private state",
|
||||
text: "tool_call: calling tool\ntool_result: inspect private state",
|
||||
mediaUrl: "https://example.com/result.png",
|
||||
},
|
||||
],
|
||||
@@ -283,7 +304,7 @@ describe("deliverDiscordReply", () => {
|
||||
await deliverDiscordReply({
|
||||
replies: [
|
||||
{
|
||||
text: "analysis: internal only",
|
||||
text: "tool_result: internal only",
|
||||
channelData,
|
||||
},
|
||||
],
|
||||
@@ -313,7 +334,7 @@ describe("deliverDiscordReply", () => {
|
||||
await deliverDiscordReply({
|
||||
replies: [
|
||||
{
|
||||
text: "commentary: hidden",
|
||||
text: "function_call: hidden",
|
||||
presentation,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -3,13 +3,6 @@ import { resolveSendableOutboundReplyParts } from "openclaw/plugin-sdk/reply-pay
|
||||
import { sanitizeAssistantVisibleText } from "openclaw/plugin-sdk/text-chunking";
|
||||
import { stripPlainTextToolCallBlocks } from "openclaw/plugin-sdk/tool-payload";
|
||||
|
||||
const DISCORD_INTERNAL_TRACE_LINE_RE =
|
||||
/^(?:>\s*)?(?:📊|🛠️|📖|📝|🔍|🔎|⚙️)\s*(?:Session Status|Exec|Read|Edit|Write|Patch|Search|Open|Click|Find|Screenshot|Update Plan|Tool Call|Tool Result|Function Call|Shell|Command)\s*:/i;
|
||||
const DISCORD_INTERNAL_COMPACT_COMMAND_TRACE_LINE_RE =
|
||||
/^(?:>\s*)?🛠️\s*(?:(?:(?:elevated|pty)\b\s*(?:·|,)\s*)+)?(?:`{1,2}\s*\S|(?:run|check|fetch|pull|push|view|show|list|switch|create|merge|rebase|stage|restore|reset|stash|search|find|print|copy|move|remove|install|start|cd|git|pnpm|npm|yarn|bun|node|python|python3|bash|sh)\b)/i;
|
||||
const DISCORD_INTERNAL_CHANNEL_LINE_RE =
|
||||
/^(?:>\s*)?(?:analysis|commentary|tool[-_ ]?call|tool[-_ ]?result|function[-_ ]?call|thinking|reasoning)\s*[:=]/i;
|
||||
|
||||
function hasNonEmptyRecord(value: unknown): value is Record<string, unknown> {
|
||||
return Boolean(
|
||||
value && typeof value === "object" && !Array.isArray(value) && Object.keys(value).length > 0,
|
||||
@@ -36,30 +29,6 @@ function hasNonTextReplyPayloadContent(payload: ReplyPayload): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
function stripDiscordInternalTraceLines(text: string): string {
|
||||
let inFence = false;
|
||||
const kept: string[] = [];
|
||||
for (const line of text.split(/\r?\n/)) {
|
||||
if (/^\s*```/.test(line)) {
|
||||
inFence = !inFence;
|
||||
kept.push(line);
|
||||
continue;
|
||||
}
|
||||
if (!inFence) {
|
||||
const trimmed = line.trim();
|
||||
if (
|
||||
DISCORD_INTERNAL_TRACE_LINE_RE.test(trimmed) ||
|
||||
DISCORD_INTERNAL_COMPACT_COMMAND_TRACE_LINE_RE.test(trimmed) ||
|
||||
DISCORD_INTERNAL_CHANNEL_LINE_RE.test(trimmed)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
kept.push(line);
|
||||
}
|
||||
return kept.join("\n");
|
||||
}
|
||||
|
||||
function collapseExcessBlankLines(text: string): string {
|
||||
return text.replace(/[ \t]+\n/g, "\n").replace(/\n{3,}/g, "\n\n");
|
||||
}
|
||||
@@ -68,8 +37,7 @@ export function sanitizeDiscordFrontChannelText(text: string): string {
|
||||
const withoutToolCallBlocks = stripPlainTextToolCallBlocks(text);
|
||||
const withoutAssistantScaffolding = sanitizeAssistantVisibleText(withoutToolCallBlocks);
|
||||
const withoutResidualToolCallBlocks = stripPlainTextToolCallBlocks(withoutAssistantScaffolding);
|
||||
const withoutTraceLines = stripDiscordInternalTraceLines(withoutResidualToolCallBlocks);
|
||||
return collapseExcessBlankLines(withoutTraceLines).trim();
|
||||
return collapseExcessBlankLines(withoutResidualToolCallBlocks).trim();
|
||||
}
|
||||
|
||||
export function sanitizeDiscordFrontChannelReplyPayloads(
|
||||
@@ -82,7 +50,7 @@ export function sanitizeDiscordFrontChannelReplyPayloads(
|
||||
const safeText =
|
||||
typeof payload.text === "string"
|
||||
? preserveVerboseToolProgress
|
||||
? collapseExcessBlankLines(sanitizeAssistantVisibleText(payload.text)).trim()
|
||||
? collapseExcessBlankLines(payload.text).trim()
|
||||
: sanitizeDiscordFrontChannelText(payload.text)
|
||||
: payload.text;
|
||||
const nextPayload =
|
||||
|
||||
@@ -304,6 +304,37 @@ describe("sanitizeUserFacingText", () => {
|
||||
expect(sanitizeUserFacingText(input)).toBe("Before\n\nAfter");
|
||||
});
|
||||
|
||||
it("strips internal tool trace warning lines from error-context delivery text", () => {
|
||||
const input = [
|
||||
"Visible intro.",
|
||||
"⚠️ 🛠️ `run openclaw definitely-not-a-real-subcommand (agent)` failed",
|
||||
"🛠️ run git status",
|
||||
"📖 Read: lines 1-40 from secret.md",
|
||||
"Visible outro.",
|
||||
].join("\n");
|
||||
|
||||
expect(sanitizeUserFacingText(input, { errorContext: true })).toBe(
|
||||
"Visible intro.\nVisible outro.",
|
||||
);
|
||||
});
|
||||
|
||||
it("preserves explicit tool progress outside error-context delivery text", () => {
|
||||
const input = "🛠️ Exec: echo queued-progress";
|
||||
|
||||
expect(sanitizeUserFacingText(input)).toBe(input);
|
||||
});
|
||||
|
||||
it("preserves internal trace examples inside fenced code", () => {
|
||||
const input = [
|
||||
"Example:",
|
||||
"```",
|
||||
"⚠️ 🛠️ `run openclaw definitely-not-a-real-subcommand (agent)` failed",
|
||||
"```",
|
||||
].join("\n");
|
||||
|
||||
expect(sanitizeUserFacingText(input)).toBe(input);
|
||||
});
|
||||
|
||||
it("strips plural XML function-call wrappers before user-facing delivery", () => {
|
||||
const input = [
|
||||
"Before",
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import {
|
||||
normalizeLowercaseStringOrEmpty,
|
||||
normalizeOptionalLowercaseString,
|
||||
} from "../../../packages/normalization-core/src/string-coerce.js";
|
||||
import { stripPlainTextToolCallBlocks } from "../../../packages/tool-call-repair/src/index.js";
|
||||
import { stripInboundMetadata } from "../../auto-reply/reply/strip-inbound-meta.js";
|
||||
import {
|
||||
@@ -11,10 +15,7 @@ import {
|
||||
} from "../../shared/assistant-error-format.js";
|
||||
import { coerceChatContentText } from "../../shared/chat-content.js";
|
||||
import {
|
||||
normalizeLowercaseStringOrEmpty,
|
||||
normalizeOptionalLowercaseString,
|
||||
} from "../../../packages/normalization-core/src/string-coerce.js";
|
||||
import {
|
||||
stripAssistantInternalTraceLines,
|
||||
stripLegacyBracketToolCallBlocks,
|
||||
stripMinimaxToolCallXml,
|
||||
stripToolCallXmlTags,
|
||||
@@ -414,8 +415,11 @@ export function sanitizeUserFacingText(text: unknown, opts?: { errorContext?: bo
|
||||
// It is internal scaffolding, so drop standalone placeholder lines before delivery
|
||||
// while preserving ordinary inline mentions a user may be discussing.
|
||||
const withoutPlaceholder = stripToolCallsOmittedPlaceholderLines(withoutToolCallXml);
|
||||
const withoutInternalTraceLines = errorContext
|
||||
? stripAssistantInternalTraceLines(withoutPlaceholder)
|
||||
: withoutPlaceholder;
|
||||
const withoutToolCallBlocks = stripPlainTextToolCallBlocks(
|
||||
stripLegacyBracketToolCallBlocks(withoutPlaceholder),
|
||||
stripLegacyBracketToolCallBlocks(withoutInternalTraceLines),
|
||||
);
|
||||
const trimmed = withoutToolCallBlocks.trim();
|
||||
if (!trimmed) {
|
||||
|
||||
@@ -830,6 +830,34 @@ describe("sanitizeAssistantVisibleText", () => {
|
||||
expect(sanitizeAssistantVisibleText(input)).toBe("Visible answer");
|
||||
});
|
||||
|
||||
it("strips internal tool trace warning lines on the delivery path", () => {
|
||||
const input = [
|
||||
"Visible intro.",
|
||||
"⚠️ 🛠️ `run openclaw definitely-not-a-real-subcommand (agent)` failed",
|
||||
"🛠️ run git status",
|
||||
"Visible outro.",
|
||||
].join("\n");
|
||||
|
||||
expect(sanitizeAssistantVisibleText(input)).toBe("Visible intro.\nVisible outro.");
|
||||
});
|
||||
|
||||
it("preserves internal tool trace examples inside fenced code", () => {
|
||||
const input = [
|
||||
"Example:",
|
||||
"```",
|
||||
"⚠️ 🛠️ `run openclaw definitely-not-a-real-subcommand (agent)` failed",
|
||||
"```",
|
||||
].join("\n");
|
||||
|
||||
expect(sanitizeAssistantVisibleText(input)).toBe(input);
|
||||
});
|
||||
|
||||
it("preserves ordinary analysis headings", () => {
|
||||
const input = ["Analysis:", "This is user-visible reasoning about the result."].join("\n");
|
||||
|
||||
expect(sanitizeAssistantVisibleText(input)).toBe(input);
|
||||
});
|
||||
|
||||
it("drops malformed reasoning before orphan close tags when final text follows", () => {
|
||||
expect(sanitizeAssistantVisibleText("private chain of thought </think> Visible answer")).toBe(
|
||||
"Visible answer",
|
||||
|
||||
@@ -11,6 +11,14 @@ import {
|
||||
const MEMORY_TAG_RE = /<\s*(\/?)\s*relevant[-_]memories\b[^<>]*>/gi;
|
||||
const MEMORY_TAG_QUICK_RE = /<\s*\/?\s*relevant[-_]memories\b/i;
|
||||
const LEGACY_BRACKET_TOOL_BLOCK_QUICK_RE = /\[\s*\/?\s*TOOL_(?:CALL|RESULT)\s*\]/i;
|
||||
const INTERNAL_TRACE_LINE_QUICK_RE =
|
||||
/(?:📊|🛠️|📖|📝|🔍|🔎|⚙️|tool[-_ ]?call|tool[-_ ]?result|function[-_ ]?call)/i;
|
||||
const INTERNAL_TRACE_LINE_RE =
|
||||
/^(?:>\s*)?(?:⚠️\s*)?(?:📊|🛠️|📖|📝|🔍|🔎|⚙️)\s*(?:Session Status|Exec|Read|Edit|Write|Patch|Search|Open|Click|Find|Screenshot|Update Plan|Tool Call|Tool Result|Function Call|Shell|Command)\s*:/i;
|
||||
const INTERNAL_COMPACT_COMMAND_TRACE_LINE_RE =
|
||||
/^(?:>\s*)?(?:⚠️\s*)?🛠️\s*(?:(?:(?:elevated|pty)\b\s*(?:·|,)\s*)+)?(?:`{1,2}\s*\S|(?:run|check|fetch|pull|push|view|show|list|switch|create|merge|rebase|stage|restore|reset|stash|search|find|print|copy|move|remove|install|start|cd|git|pnpm|npm|yarn|bun|node|python|python3|bash|sh)\b)/i;
|
||||
const INTERNAL_CHANNEL_TRACE_LINE_RE =
|
||||
/^(?:>\s*)?(?:tool[-_ ]?call|tool[-_ ]?result|function[-_ ]?call)\s*[:=]/i;
|
||||
|
||||
/**
|
||||
* Strip XML-style tool call tags that models sometimes emit as plain text.
|
||||
@@ -760,6 +768,33 @@ function stripRelevantMemoriesTags(text: string): string {
|
||||
return result;
|
||||
}
|
||||
|
||||
export function stripAssistantInternalTraceLines(text: string): string {
|
||||
if (!text || !INTERNAL_TRACE_LINE_QUICK_RE.test(text)) {
|
||||
return text;
|
||||
}
|
||||
|
||||
const codeRegions = findCodeRegions(text);
|
||||
let result = "";
|
||||
let lineStart = 0;
|
||||
while (lineStart < text.length) {
|
||||
const newlineIndex = text.indexOf("\n", lineStart);
|
||||
const lineEnd = newlineIndex === -1 ? text.length : newlineIndex + 1;
|
||||
const rawLine = text.slice(lineStart, lineEnd);
|
||||
const line = rawLine.endsWith("\n") ? rawLine.slice(0, -1).replace(/\r$/, "") : rawLine;
|
||||
const trimmed = line.trim();
|
||||
const shouldStrip =
|
||||
!isInsideCode(lineStart, codeRegions) &&
|
||||
(INTERNAL_TRACE_LINE_RE.test(trimmed) ||
|
||||
INTERNAL_COMPACT_COMMAND_TRACE_LINE_RE.test(trimmed) ||
|
||||
INTERNAL_CHANNEL_TRACE_LINE_RE.test(trimmed));
|
||||
if (!shouldStrip) {
|
||||
result += rawLine;
|
||||
}
|
||||
lineStart = lineEnd;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export type AssistantVisibleTextSanitizerProfile = "delivery" | "history" | "internal-scaffolding";
|
||||
|
||||
type AssistantVisibleTextPipelineOptions = {
|
||||
@@ -833,6 +868,7 @@ function applyAssistantVisibleTextStagePipeline(
|
||||
stripFunctionCallsXmlPayloads: options.stripFunctionCallsXmlPayloads,
|
||||
stripFunctionResponseAfterPluralToolCalls: options.stripFunctionResponseAfterPluralToolCalls,
|
||||
});
|
||||
cleaned = stripAssistantInternalTraceLines(cleaned);
|
||||
cleaned = stripLegacyBracketToolCallBlocks(cleaned);
|
||||
cleaned = stripPlainTextToolCallBlocks(cleaned);
|
||||
if (!options.preserveDowngradedToolText) {
|
||||
|
||||
Reference in New Issue
Block a user