mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 09:39:25 +00:00
refactor(agents): share presentation record sanitization (#147851)
This commit is contained in:
parent
dba4da1f8b
commit
da39dbae88
3 changed files with 99 additions and 37 deletions
|
|
@ -1967,7 +1967,7 @@ src/agents/tools/in-process-gateway.ts 1
|
|||
src/agents/tools/media-generate-tool-actions-shared.ts 1
|
||||
src/agents/tools/message-tool-discovery.ts 3
|
||||
src/agents/tools/message-tool-execution.ts 3
|
||||
src/agents/tools/message-tool-visible-content.ts 9
|
||||
src/agents/tools/message-tool-visible-content.ts 6
|
||||
src/agents/tools/mobile-ui-tool.ts 5
|
||||
src/agents/tools/music-generate-tool.ts 2
|
||||
src/agents/tools/nodes-tool.ts 2
|
||||
|
|
|
|||
|
|
@ -106,6 +106,18 @@ function sanitizePresentationTextFieldsResult(
|
|||
return { value };
|
||||
}
|
||||
let suppressionReason: VisibleTextSuppressionReason | undefined;
|
||||
const sanitizeRecordArray = (entries: unknown[], field: "label" | "name") =>
|
||||
entries.map((entry) => {
|
||||
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
||||
return entry;
|
||||
}
|
||||
// SAFETY: The guard establishes a non-null, non-array record before cloning.
|
||||
const sanitized = { ...(entry as Record<string, unknown>) };
|
||||
// Keep sanitizing after suppression; the first reason only labels the outcome.
|
||||
const reason = sanitizeStringParam(sanitized, field, bootPrompt);
|
||||
suppressionReason ??= reason;
|
||||
return sanitized;
|
||||
});
|
||||
const presentation = { ...(value as Record<string, unknown>) };
|
||||
if (typeof presentation.title === "string") {
|
||||
const sanitized = sanitizeUserVisibleToolTextResult(presentation.title, bootPrompt);
|
||||
|
|
@ -229,18 +241,7 @@ function sanitizePresentationTextFieldsResult(
|
|||
});
|
||||
}
|
||||
if (Array.isArray(sanitizedBlock.options)) {
|
||||
sanitizedBlock.options = sanitizedBlock.options.map((option) => {
|
||||
if (!option || typeof option !== "object" || Array.isArray(option)) {
|
||||
return option;
|
||||
}
|
||||
const sanitizedOption = { ...(option as Record<string, unknown>) };
|
||||
if (typeof sanitizedOption.label === "string") {
|
||||
const sanitized = sanitizeUserVisibleToolTextResult(sanitizedOption.label, bootPrompt);
|
||||
sanitizedOption.label = sanitized.text;
|
||||
suppressionReason ??= sanitized.suppressionReason;
|
||||
}
|
||||
return sanitizedOption;
|
||||
});
|
||||
sanitizedBlock.options = sanitizeRecordArray(sanitizedBlock.options, "label");
|
||||
}
|
||||
if (Array.isArray(sanitizedBlock.categories)) {
|
||||
sanitizedBlock.categories = sanitizedBlock.categories.map((category) => {
|
||||
|
|
@ -253,32 +254,10 @@ function sanitizePresentationTextFieldsResult(
|
|||
});
|
||||
}
|
||||
if (Array.isArray(sanitizedBlock.segments)) {
|
||||
sanitizedBlock.segments = sanitizedBlock.segments.map((segment) => {
|
||||
if (!segment || typeof segment !== "object" || Array.isArray(segment)) {
|
||||
return segment;
|
||||
}
|
||||
const sanitizedSegment = { ...(segment as Record<string, unknown>) };
|
||||
if (typeof sanitizedSegment.label === "string") {
|
||||
const sanitized = sanitizeUserVisibleToolTextResult(sanitizedSegment.label, bootPrompt);
|
||||
sanitizedSegment.label = sanitized.text;
|
||||
suppressionReason ??= sanitized.suppressionReason;
|
||||
}
|
||||
return sanitizedSegment;
|
||||
});
|
||||
sanitizedBlock.segments = sanitizeRecordArray(sanitizedBlock.segments, "label");
|
||||
}
|
||||
if (Array.isArray(sanitizedBlock.series)) {
|
||||
sanitizedBlock.series = sanitizedBlock.series.map((series) => {
|
||||
if (!series || typeof series !== "object" || Array.isArray(series)) {
|
||||
return series;
|
||||
}
|
||||
const sanitizedSeries = { ...(series as Record<string, unknown>) };
|
||||
if (typeof sanitizedSeries.name === "string") {
|
||||
const sanitized = sanitizeUserVisibleToolTextResult(sanitizedSeries.name, bootPrompt);
|
||||
sanitizedSeries.name = sanitized.text;
|
||||
suppressionReason ??= sanitized.suppressionReason;
|
||||
}
|
||||
return sanitizedSeries;
|
||||
});
|
||||
sanitizedBlock.series = sanitizeRecordArray(sanitizedBlock.series, "name");
|
||||
}
|
||||
return sanitizedBlock;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ import { readEmbeddedMessageDeliveryFact } from "../embedded-agent-message-deliv
|
|||
import { createOpenClawTools } from "../openclaw-tools.js";
|
||||
import { withGatewayToolCallerIdentity } from "./gateway-caller-context.js";
|
||||
import { createMessageTool } from "./message-tool-execution.js";
|
||||
import { sanitizeMessageToolVisiblePayload } from "./message-tool-visible-content.js";
|
||||
import { runSessionsSendA2AFlow } from "./sessions-send-tool.a2a.js";
|
||||
|
||||
type CreateMessageTool = typeof createMessageTool;
|
||||
|
|
@ -4553,6 +4554,12 @@ describe("message tool reasoning tag sanitization", () => {
|
|||
xLabel: "<think>axis rationale</think>Day",
|
||||
yLabel: "<think>axis rationale</think>Milliseconds",
|
||||
},
|
||||
{
|
||||
type: "chart",
|
||||
chartType: "pie",
|
||||
title: "Traffic",
|
||||
segments: [{ label: "<think>segment rationale</think>Primary", value: 1 }],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
|
@ -4586,10 +4593,86 @@ describe("message tool reasoning tag sanitization", () => {
|
|||
xLabel: "Day",
|
||||
yLabel: "Milliseconds",
|
||||
},
|
||||
{
|
||||
type: "chart",
|
||||
chartType: "pie",
|
||||
title: "Traffic",
|
||||
segments: [{ label: "Primary", value: 1 }],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it.each([true, false])(
|
||||
"sanitizes every presentation record array while retaining the first reason (option suppressed: %s)",
|
||||
(suppressOption) => {
|
||||
const internalContext =
|
||||
"<<<BEGIN_OPENCLAW_INTERNAL_CONTEXT>>>\nBOOT.md:\nWake up and report.\n<<<END_OPENCLAW_INTERNAL_CONTEXT>>>";
|
||||
const inboundContext = [
|
||||
markInboundContextLabel("Conversation info:"),
|
||||
"```json",
|
||||
'{"chat_id":"group:test","sender_id":"test-sender"}',
|
||||
"```",
|
||||
].join("\n");
|
||||
const metadata = { retained: true };
|
||||
const option = { label: suppressOption ? internalContext : " Choice ", metadata };
|
||||
const nonString = { label: 7 };
|
||||
const invalidArray = ["<think>unchanged</think>"];
|
||||
const presentation = {
|
||||
blocks: [
|
||||
{
|
||||
options: [option, null, invalidArray, nonString],
|
||||
categories: [inboundContext],
|
||||
segments: [
|
||||
{ label: internalContext, value: 1 },
|
||||
{ label: "<think>segment rationale</think>Slice", value: 2 },
|
||||
],
|
||||
series: [
|
||||
{ name: internalContext, values: [1] },
|
||||
{ name: "<think>series rationale</think>Trend", values: [2] },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
const original = structuredClone(presentation);
|
||||
const params = { presentation };
|
||||
|
||||
expect(sanitizeMessageToolVisiblePayload(params)).toBe(
|
||||
suppressOption ? "internal_runtime_context_echo" : "inbound_metadata_echo",
|
||||
);
|
||||
|
||||
const block = params.presentation.blocks[0];
|
||||
expect(block).toEqual({
|
||||
options: [
|
||||
{ label: suppressOption ? "" : " Choice ", metadata },
|
||||
null,
|
||||
invalidArray,
|
||||
nonString,
|
||||
],
|
||||
categories: [""],
|
||||
segments: [
|
||||
{ label: "", value: 1 },
|
||||
{ label: "Slice", value: 2 },
|
||||
],
|
||||
series: [
|
||||
{ name: "", values: [1] },
|
||||
{ name: "Trend", values: [2] },
|
||||
],
|
||||
});
|
||||
expect(params.presentation).not.toBe(presentation);
|
||||
expect(block).not.toBe(presentation.blocks[0]);
|
||||
for (const field of ["options", "segments", "series"] as const) {
|
||||
expect(block?.[field]).not.toBe(presentation.blocks[0]?.[field]);
|
||||
}
|
||||
expect(block?.options[0]).not.toBe(option);
|
||||
expect(block?.options[2]).toBe(invalidArray);
|
||||
expect(block?.options[3]).not.toBe(nonString);
|
||||
expect(block?.segments[0]).not.toBe(presentation.blocks[0]?.segments[0]);
|
||||
expect(block?.series[0]).not.toBe(presentation.blocks[0]?.series[0]);
|
||||
expect(presentation).toEqual(original);
|
||||
},
|
||||
);
|
||||
|
||||
it("strips internal runtime context from visible presentation fields before sending (#53732)", async () => {
|
||||
mockSendResult({ channel: "slack", to: "slack:C123" });
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue