mirror of
https://github.com/morluto/rea.git
synced 2026-11-05 14:26:07 +00:00
fix(mcp): keep detail guidance in compact input schemas (#8034)
Compact input schemas are the default advertisement since #5975, and the profile drops every property description. On 23 of the 31 tools with a guided `detail` option, including inspect_artifact and inspect_macho, `detail` is advertised as a bare complete|summary enum, so callers take the complete default. On a universal macOS app that reply was about 699k characters for inspect_artifact and 336k for inspect_macho, against about 4.4k and 3.2k with summary. Keep the canonical description of a root `detail` property in the inline compact input form. The reduced form already keeps first-level guidance, and compact output schemas are unchanged. Advertised compact input schemas grow from 127,616 to 133,358 bytes across 233 tools; the largest schema stays at 4,107 bytes, within the 13,312-byte budget. The new boundary test compares `detail` guidance between the full and compact tools/list and fails before the change.
This commit is contained in:
1 parent
a56cc93cab
commit
5b953e13c4
3 files changed
+62
-2
No files matched your search
@@ -1707,7 +1707,9 @@ JSON Schema:
|
||||
|
||||
- Annotation prose leaves the advertised form: nested property descriptions,
|
||||
literal examples, defaults, and titles are dropped. The root keeps its
|
||||
description because it states the accepted input groups.
|
||||
description because it states the accepted input groups. A root `detail`
|
||||
property keeps its description because it selects between complete Evidence
|
||||
and a bounded summary.
|
||||
- Schema-local references are inlined within the budget. Kimi Code expands
|
||||
references before sending provider requests, so sharing definitions cannot
|
||||
establish the provider-facing size. Expansion stops at the budget before
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
COMPACT_SCHEMA_MAX_DEPTH,
|
||||
hasGatewayIncompatibleShape,
|
||||
inlineWithinBudget,
|
||||
isSchemaObject,
|
||||
reduceToShallowProperties,
|
||||
schemaDepth,
|
||||
serializedBytes,
|
||||
@@ -28,6 +29,34 @@ export interface CompactedInputSchema {
|
||||
const INPUT_REDUCED_ANNOTATION =
|
||||
"Advertised in reduced form by the compact input schema profile; the server still validates complete canonical inputs.";
|
||||
|
||||
// `detail` selects between complete Evidence and a bounded summary. Without
|
||||
// its guidance, callers default to complete replies that can exceed context.
|
||||
const retainedRootPropertyDescriptions = ["detail"];
|
||||
|
||||
/** Restore the description of root properties whose choice bounds the reply. */
|
||||
const retainRootPropertyGuidance = (
|
||||
schema: Record<string, unknown>,
|
||||
stripped: Record<string, unknown>,
|
||||
): Record<string, unknown> => {
|
||||
if (
|
||||
!isSchemaObject(schema.properties) ||
|
||||
!isSchemaObject(stripped.properties)
|
||||
)
|
||||
return stripped;
|
||||
const properties = { ...stripped.properties };
|
||||
for (const name of retainedRootPropertyDescriptions) {
|
||||
const source = schema.properties[name];
|
||||
const kept = properties[name];
|
||||
if (
|
||||
isSchemaObject(source) &&
|
||||
isSchemaObject(kept) &&
|
||||
typeof source.description === "string"
|
||||
)
|
||||
properties[name] = { ...kept, description: source.description };
|
||||
}
|
||||
return { ...stripped, properties };
|
||||
};
|
||||
|
||||
/**
|
||||
* Render an advertised input schema within a per-schema byte budget for
|
||||
* providers that validate the literal serialized schema size. Validation
|
||||
@@ -41,7 +70,9 @@ export const compactAdvertisedInputSchema = (
|
||||
): CompactedInputSchema => {
|
||||
const expanded = inlineWithinBudget(stripAnnotations(schema), budgetBytes);
|
||||
const inline =
|
||||
expanded === undefined ? undefined : typeArrayItemUnions(expanded);
|
||||
expanded === undefined
|
||||
? undefined
|
||||
: typeArrayItemUnions(retainRootPropertyGuidance(schema, expanded));
|
||||
if (
|
||||
inline !== undefined &&
|
||||
serializedBytes(inline) <= budgetBytes &&
|
||||
|
||||
@@ -644,3 +644,30 @@ it.each(["full", "compact"] as const)(
|
||||
expect(calls).toBe(1);
|
||||
},
|
||||
);
|
||||
|
||||
it("keeps root detail guidance so callers can choose bounded summaries", async () => {
|
||||
const full = fullServer();
|
||||
const compact = compactServer();
|
||||
registerCatalog(full);
|
||||
registerCatalog(compact);
|
||||
const [fullTools, compactTools] = await Promise.all([
|
||||
listToolsThroughTransport(full),
|
||||
listToolsThroughTransport(compact),
|
||||
]);
|
||||
const detailGuidance = (tools: readonly AdvertisedTool[]) =>
|
||||
Object.fromEntries(
|
||||
tools.flatMap(({ name, inputSchema }) => {
|
||||
const detail = (
|
||||
inputSchema.properties as Record<string, Record<string, unknown>>
|
||||
)?.detail;
|
||||
return typeof detail?.description === "string"
|
||||
? [[name, detail.description]]
|
||||
: [];
|
||||
}),
|
||||
);
|
||||
const expected = detailGuidance(fullTools);
|
||||
expect(Object.keys(expected)).toEqual(
|
||||
expect.arrayContaining(["inspect_artifact", "inspect_macho"]),
|
||||
);
|
||||
expect(detailGuidance(compactTools)).toEqual(expected);
|
||||
});
|
||||
Reference in new issue
Block a user