fix(agents): guard system prompt tool schema stats

This commit is contained in:
Vincent Koc
2026-06-04 10:59:41 +02:00
parent f4d2748ca5
commit 511e7eba51
2 changed files with 103 additions and 10 deletions

View File

@@ -216,4 +216,83 @@ describe("buildSystemPromptReport", () => {
});
expect(report.tools.entries[0]?.schemaHash).toMatch(/^[a-f0-9]{64}$/u);
});
it("keeps reporting when a tool schema getter throws", () => {
const file = makeBootstrapFile({ path: "/tmp/workspace/AGENTS.md" });
const brokenTool = {
name: "broken",
description: "Broken schema",
};
Object.defineProperty(brokenTool, "parameters", {
get() {
throw new Error("schema getter exploded");
},
});
const report = buildSystemPromptReport({
source: "run",
generatedAt: 0,
bootstrapMaxChars: 20_000,
systemPrompt: "system",
bootstrapFiles: [file],
injectedFiles: [],
skillsPrompt: "",
tools: [
brokenTool,
{
name: "healthy",
description: "Healthy schema",
parameters: {
type: "object",
properties: { path: { type: "string" } },
},
},
] as never,
});
expect(report.tools.entries).toHaveLength(2);
expect(report.tools.entries[0]).toMatchObject({
name: "broken",
schemaChars: 0,
propertiesCount: null,
});
expect(report.tools.entries[1]).toMatchObject({
name: "healthy",
propertiesCount: 1,
});
});
it("keeps reporting when a tool schema properties getter throws", () => {
const file = makeBootstrapFile({ path: "/tmp/workspace/AGENTS.md" });
const schema = { type: "object" };
Object.defineProperty(schema, "properties", {
get() {
throw new Error("properties getter exploded");
},
enumerable: true,
});
const report = buildSystemPromptReport({
source: "run",
generatedAt: 0,
bootstrapMaxChars: 20_000,
systemPrompt: "system",
bootstrapFiles: [file],
injectedFiles: [],
skillsPrompt: "",
tools: [
{
name: "broken",
description: "Broken schema",
parameters: schema,
},
] as never,
});
expect(report.tools.entries[0]).toMatchObject({
name: "broken",
schemaChars: 0,
propertiesCount: null,
});
});
});

View File

@@ -49,7 +49,7 @@ function parseSkillBlocks(skillsPrompt: string): Array<{ name: string; blockChar
}
function buildToolSchemaStats(
parameters: AgentTool["parameters"],
parameters: AgentTool["parameters"] | undefined,
): Pick<ToolReportEntry, "propertiesCount" | "schemaChars" | "schemaHash"> {
if (!parameters || typeof parameters !== "object") {
return { schemaChars: 0, schemaHash: sha256(""), propertiesCount: null };
@@ -67,14 +67,7 @@ function buildToolSchemaStats(
const stats = {
schemaChars: schemaJson.length,
schemaHash: sha256(schemaJson),
propertiesCount: (() => {
const schema = parameters as Record<string, unknown>;
const props = typeof schema.properties === "object" ? schema.properties : null;
if (!props || typeof props !== "object") {
return null;
}
return Object.keys(props as Record<string, unknown>).length;
})(),
propertiesCount: countToolSchemaProperties(parameters),
};
// Tool parameter objects are reused across runs; cache their stable size/hash
// so report generation stays cheap during frequent prompt rebuilds.
@@ -82,6 +75,27 @@ function buildToolSchemaStats(
return stats;
}
function countToolSchemaProperties(parameters: object): number | null {
try {
const schema = parameters as Record<string, unknown>;
const props = typeof schema.properties === "object" ? schema.properties : null;
if (!props || typeof props !== "object") {
return null;
}
return Object.keys(props as Record<string, unknown>).length;
} catch {
return null;
}
}
function readToolParameters(tool: AgentTool): AgentTool["parameters"] | undefined {
try {
return tool.parameters;
} catch {
return undefined;
}
}
function buildToolsEntries(tools: AgentTool[]): SessionSystemPromptReport["tools"]["entries"] {
return tools.map((tool) => {
const cached = toolReportEntryCache.get(tool);
@@ -91,7 +105,7 @@ function buildToolsEntries(tools: AgentTool[]): SessionSystemPromptReport["tools
const name = tool.name;
const summary = tool.description?.trim() || tool.label?.trim() || "";
const summaryChars = summary.length;
const schemaStats = buildToolSchemaStats(tool.parameters);
const schemaStats = buildToolSchemaStats(readToolParameters(tool));
const entry = { name, summaryChars, summaryHash: sha256(summary), ...schemaStats };
toolReportEntryCache.set(tool, entry);
return entry;