fix: Stop enforcing enum values dropped by input-schema truncation - #1258
fix: Stop enforcing enum values dropped by input-schema truncation#1258MQ37 wants to merge 5 commits into
Conversation
shortenProperties() truncates a large enum for display, but the same truncated array was also compiled straight into the AJV validator — rejecting Actor input values that were cut only because the display list has a character cap, not because the Actor's real schema forbids them (e.g. compass/crawler-google-places' categoryFilterWords). Strip enum/items.enum from the AJV-compiled schema only for properties where truncation actually happened, comparing against the raw Actor schema. The schema shown to the LLM keeps the truncated list unchanged — only local enforcement of the incomplete list is removed. Closes #1253. Written with AI assistance (Claude).
61f9788 to
98e0330
Compare
There was a problem hiding this comment.
Thank you for this PR. When we discussed the fix, I understood that the enum keyword would be removed completely when the full list does not fit. I still think that is the correct fix because an incomplete enum is not a valid representation of the Actor's input.
What do you think? The enum truncation was good fix for large schema. But, still, like your idea with removing it completely and do not pretend it is an enumeration of all values (this is what LLM understands imo).
The current PR:
- Keeps a truncated enum in the public schema.
- Creates a separate private schema without that enum for AJV.
- Fixes false rejection in direct tools and
call-actor. - Does not fix
fetch-actor-details. - Still tells the model that the incomplete list is exhaustive.
- Adds a helper, clone, comparison logic, factory wiring, and two conflicting schema representations.
Please handle this in the shared shortenProperties() path instead:
- Keep the enum when every non-empty value fits.
- When it does not fit, remove the
enumkeyword. - Add examples and a note that the complete list is not shown.
This gives us one honest schema for display and validation, and it covers direct tools, call-actor, and fetch-actor-details.
Removing enum does not remove type validation. AJV will still enforce constraints such as type: "array" and items.type: "string". It will reject a wrong container type or non-string items, but it will not check membership in the omitted enum. The Actor receives the value and may reject unsupported values according to its own validation or runtime behavior.
That is the intended trade-off: the MCP server must not reject a valid Actor input based on a list it knows is incomplete.
…-false-rejection # Conflicts: # src/tools/actors/actor_tools_factory.ts
Reworks #1258 per review (jirispilka): fold the fix into the single shared shortenProperties() instead of a separate AJV-only schema. An enum that fits ACTOR_ENUM_MAX_LENGTH whole is kept as-is; one that doesn't is dropped entirely (not partially truncated) from both display and validation, with a generic note + a few examples appended to the description instead. - Kept enum: no more duplicate "Possible values" line (the enum field already carries it). - Dropped enum: description stays generic (no tool name) by default; only when fetch-actor-details is present in the session's tools/list does it additionally name it as where to find the Actor's full input schema. New buildInputSchema(ctx) on ToolBase renders that per-session, mirroring buildDescription, since Actor input-schema field text isn't otherwise session-rendered (src/tools/AGENTS.md). - fetch-actor-details inherits the fix for free (same shared function). - Fixes a regression this rework introduced along the way: Skyfire's decorateToolSchema() only patched the static inputSchema, so an injected skyfire-pay-id vanished whenever the gated render was used instead — now composes with buildInputSchema. actor_tools_factory's buildInputSchema also now always clones its render, never handing back the live schema object shared across sessions. Written with AI assistance (Claude).
Shorter note text (3 examples instead of 5, tighter phrasing), and the gated fetch-actor-details addendum no longer claims 'full input schema' — fetch-actor-details shares the same shortenProperties() cap, so it drops the same oversized enum and doesn't actually return the complete list either (confirmed live against a real oversized-enum Actor). Rewords to 'more on this Actor' — still a genuinely useful pointer (README, other properties, defaults), without overclaiming enum completeness. Written with AI assistance (Claude).
|
Reworked per your feedback — folded into Verified live against One addition beyond your ask: when |
findDroppedEnumProperties's doc still called it the "see full schema" addendum after the addendum was reworded (9c77ea7) precisely because fetch-actor-details doesn't return the full schema/enum either. Written with AI assistance (Claude).
What
Reworked per review: instead of a separate AJV-only validation schema, the shared
shortenProperties()now handles an oversizedenum/items.enumitself — kept in full when it fitsACTOR_ENUM_MAX_LENGTH(no duplicate "Possible values" text, theenumfield already carries it), dropped entirely (not partially truncated) when it doesn't, with a note + a few example values appended to the description instead. One schema, no divergence between what the LLM sees and what AJV validates, across all three consumers that share this path: direct Actor tools,call-actor, andfetch-actor-details. When the session'stools/listhappens to includefetch-actor-details, the dropped-enum note additionally points to it for more context on the Actor — via a new per-sessionbuildInputSchema(ctx)render onToolBase(mirrors the existingbuildDescription(ctx)convention), since a bare tool name can't be hardcoded into Actor input-schema text without knowing whether that tool is actually loaded in a given session.Why
Closes #1253. Reviewer (jirispilka) asked for the enum keyword to be removed entirely rather than partially truncated, folded into the one shared path so display and validation never diverge, and covering all three consumers instead of two. AJV still enforces
type, just not enum membership, on a dropped field — the real Actor/platform holds the true, complete list and is left to reject an actually-invalid value on its own, confirmed live end-to-end (see Testing).Testing
pnpm run type-check/lint/test:unit/format/check:agentsall clean — 101 test files, 1581 passed, 1 skipped. New/updated unit coverage: all-or-nothingshortenProperties/filterAndShortenEnum(kept-whole vs. dropped-whole, bothenumanditems.enum),findDroppedEnumProperties, the per-sessionbuildInputSchemagating (tools.actor_tools_factory.enum_gating.test.ts), and a Skyfire-payment composition regression (payments.skyfire.build_input_schema.test.ts) — a payment decorator that clones a tool must wrapbuildInputSchema, not only the staticinputSchema, or an injected field silently disappears whenever the gated render is used; caught this live before it shipped.Live e2e against the real, currently-deployed
compass/crawler-google-placesActor (Google Maps Scraper) — the actual motivating case, a real 4,036-value / 71KBcategoryFilterWordsenum: confirmed via realtools/listthat the enum is dropped with a generic note, additionally namingfetch-actor-detailsonly in a session where it's actually loaded; confirmedfetch-actor-detailsitself doesn't expose the complete enum either (same drop, worded accordingly rather than overclaiming); confirmed a genuinely invalid category value is rejected by the real platform pre-run at zero cost; and confirmedcategoryFilterWords: ["restaurant"]— a real category that was in the exact range the old code falsely rejected — is now accepted, a real run starts, and the filter actually works end to end (1 place scraped before the run was aborted).getToolPublicFieldOnly(exported viainternals.js) gained a new field-dependent branch (buildInputSchema) — additive, same safe-fallback shape as the existingbuildDescription, but flagging per this repo's own norm on changes to what the hosted server consumes.Written with AI assistance (Claude).