mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-14 10:00:29 +00:00
fix(auto-reply): guard context report tool entries
This commit is contained in:
@@ -16,6 +16,7 @@ function makeParams(
|
||||
sessionKey?: string;
|
||||
agentId?: string;
|
||||
currentTurn?: NonNullable<SessionEntry["systemPromptReport"]>["currentTurn"];
|
||||
toolEntries?: NonNullable<SessionEntry["systemPromptReport"]>["tools"]["entries"];
|
||||
},
|
||||
): HandleCommandsParams {
|
||||
return {
|
||||
@@ -67,7 +68,9 @@ function makeParams(
|
||||
tools: {
|
||||
listChars: 10,
|
||||
schemaChars: 20,
|
||||
entries: [{ name: "read", summaryChars: 10, schemaChars: 20, propertiesCount: 1 }],
|
||||
entries: options?.toolEntries ?? [
|
||||
{ name: "read", summaryChars: 10, schemaChars: 20, propertiesCount: 1 },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -159,6 +162,30 @@ describe("buildContextReply", () => {
|
||||
expect(result.text).not.toContain("~645 tok");
|
||||
});
|
||||
|
||||
it("omits unreadable tool report entries from detail output", async () => {
|
||||
const malformedTool = {
|
||||
get name(): string {
|
||||
throw new Error("fuzzed unreadable report tool name");
|
||||
},
|
||||
summaryChars: 10,
|
||||
schemaChars: 20,
|
||||
propertiesCount: 1,
|
||||
};
|
||||
|
||||
const result = await buildContextReply(
|
||||
makeParams("/context detail", false, {
|
||||
toolEntries: [
|
||||
{ name: "read", summaryChars: 10, schemaChars: 20, propertiesCount: 1 },
|
||||
malformedTool,
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.text).toContain("Tools: read");
|
||||
expect(result.text).toContain("- read: 20 chars (~5 tok)");
|
||||
expect(result.text).not.toContain("fuzzed unreadable");
|
||||
});
|
||||
|
||||
it("prefers the target session entry from sessionStore for cached context stats", async () => {
|
||||
const params = makeParams("/context detail", false, {
|
||||
contextTokens: 8_192,
|
||||
@@ -217,6 +244,38 @@ describe("buildContextReply", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("omits unreadable tool report entries from context maps", async () => {
|
||||
const malformedTool = {
|
||||
get name(): string {
|
||||
throw new Error("fuzzed unreadable report tool name");
|
||||
},
|
||||
summaryChars: 10,
|
||||
schemaChars: 20,
|
||||
propertiesCount: 1,
|
||||
};
|
||||
const result = await buildContextReply(
|
||||
makeParams("/context map", false, {
|
||||
contextTokens: 8_192,
|
||||
totalTokens: 900,
|
||||
toolEntries: [
|
||||
{ name: "read", summaryChars: 10, schemaChars: 20, propertiesCount: 1 },
|
||||
malformedTool,
|
||||
],
|
||||
}),
|
||||
);
|
||||
if (!result.mediaUrl) {
|
||||
throw new Error("missing context map media path");
|
||||
}
|
||||
try {
|
||||
const png = await readFile(result.mediaUrl);
|
||||
expect(result.text).toContain("Context treemap");
|
||||
expect(result.text).toContain("Tracked: 10,520 chars");
|
||||
expect(png.subarray(0, 8)).toEqual(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]));
|
||||
} finally {
|
||||
await unlink(result.mediaUrl);
|
||||
}
|
||||
});
|
||||
|
||||
it("counts room events as event context in context maps", async () => {
|
||||
const result = await buildContextReply(
|
||||
makeParams("/context map", false, {
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
import { estimateTokensFromChars } from "../../utils/cjk-chars.js";
|
||||
import type { ReplyPayload } from "../types.js";
|
||||
import type { HandleCommandsParams } from "./commands-types.js";
|
||||
import { readContextReportToolEntries } from "./context-report-tools.js";
|
||||
import { renderContextTreemapPng } from "./context-treemap.js";
|
||||
|
||||
function formatInt(n: number): string {
|
||||
@@ -178,7 +179,8 @@ export async function buildContextReply(params: HandleCommandsParams): Promise<R
|
||||
const toolListLine = `Tool list (system prompt text): ${formatCharsAndTokens(report.tools.listChars)}`;
|
||||
const skillNameSet = new Set(report.skills.entries.map((s) => s.name));
|
||||
const skillNames = Array.from(skillNameSet);
|
||||
const toolNames = report.tools.entries.map((t) => t.name);
|
||||
const toolEntries = readContextReportToolEntries(report.tools.entries);
|
||||
const toolNames = toolEntries.map((t) => t.name);
|
||||
const formatNameList = (names: string[], cap: number) =>
|
||||
names.length <= cap
|
||||
? names.join(", ")
|
||||
@@ -267,14 +269,14 @@ export async function buildContextReply(params: HandleCommandsParams): Promise<R
|
||||
30,
|
||||
);
|
||||
const perToolSchema = formatListTop(
|
||||
report.tools.entries.map((t) => ({ name: t.name, value: t.schemaChars })),
|
||||
toolEntries.map((t) => ({ name: t.name, value: t.schemaChars })),
|
||||
30,
|
||||
);
|
||||
const perToolSummary = formatListTop(
|
||||
report.tools.entries.map((t) => ({ name: t.name, value: t.summaryChars })),
|
||||
toolEntries.map((t) => ({ name: t.name, value: t.summaryChars })),
|
||||
30,
|
||||
);
|
||||
const toolPropsLines = report.tools.entries
|
||||
const toolPropsLines = toolEntries
|
||||
.filter((t) => t.propertiesCount != null)
|
||||
.toSorted((a, b) => (b.propertiesCount ?? 0) - (a.propertiesCount ?? 0))
|
||||
.slice(0, 30)
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import type { SessionSystemPromptReport } from "../../config/sessions/types.js";
|
||||
|
||||
type SessionToolReportEntry = SessionSystemPromptReport["tools"]["entries"][number];
|
||||
|
||||
export type ReadableContextReportToolEntry = {
|
||||
readonly name: string;
|
||||
readonly summaryChars: number;
|
||||
readonly schemaChars: number;
|
||||
readonly propertiesCount?: number | null;
|
||||
};
|
||||
|
||||
function readNonNegativeNumber(read: () => number | null | undefined): number {
|
||||
try {
|
||||
const value = read();
|
||||
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
function readOptionalCount(read: () => number | null | undefined): number | null | undefined {
|
||||
try {
|
||||
const value = read();
|
||||
if (value == null) {
|
||||
return value;
|
||||
}
|
||||
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function readToolName(entry: SessionToolReportEntry): string | undefined {
|
||||
try {
|
||||
return typeof entry.name === "string" && entry.name ? entry.name : undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function readContextReportToolEntries(
|
||||
entries: readonly SessionToolReportEntry[],
|
||||
): ReadableContextReportToolEntry[] {
|
||||
return entries.flatMap((entry) => {
|
||||
const name = readToolName(entry);
|
||||
if (!name) {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
{
|
||||
name,
|
||||
summaryChars: readNonNegativeNumber(() => entry.summaryChars),
|
||||
schemaChars: readNonNegativeNumber(() => entry.schemaChars),
|
||||
propertiesCount: readOptionalCount(() => entry.propertiesCount),
|
||||
},
|
||||
];
|
||||
});
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import zlib from "node:zlib";
|
||||
import type { SessionSystemPromptReport } from "../../config/sessions/types.js";
|
||||
import { resolvePreferredOpenClawTmpDir } from "../../infra/tmp-openclaw-dir.js";
|
||||
import { estimateTokensFromChars } from "../../utils/cjk-chars.js";
|
||||
import { readContextReportToolEntries } from "./context-report-tools.js";
|
||||
|
||||
type Rect = {
|
||||
x: number;
|
||||
@@ -339,8 +340,8 @@ function buildGroups(report: SessionSystemPromptReport): TreemapGroup[] {
|
||||
const projectFrameChars = Math.max(0, report.systemPrompt.projectContextChars - injectedTotal);
|
||||
const skillTotal = report.skills.entries.reduce((sum, skill) => sum + skill.blockChars, 0);
|
||||
const systemBaseChars = Math.max(0, report.systemPrompt.nonProjectContextChars - skillTotal);
|
||||
const tools = report.tools.entries
|
||||
.map((tool) => ({ name: tool.name, value: tool.schemaChars ?? 0 }))
|
||||
const tools = readContextReportToolEntries(report.tools.entries)
|
||||
.map((tool) => ({ name: tool.name, value: tool.schemaChars }))
|
||||
.filter((tool) => tool.value > 0);
|
||||
const currentTurnLeaves = report.currentTurn
|
||||
? [
|
||||
|
||||
Reference in New Issue
Block a user