Skip to content

fix: Stop enforcing enum values dropped by input-schema truncation - #1258

Open
MQ37 wants to merge 5 commits into
masterfrom
fix/enum-truncation-false-rejection
Open

fix: Stop enforcing enum values dropped by input-schema truncation#1258
MQ37 wants to merge 5 commits into
masterfrom
fix/enum-truncation-false-rejection

Conversation

@MQ37

@MQ37 MQ37 commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

What

Reworked per review: instead of a separate AJV-only validation schema, the shared shortenProperties() now handles an oversized enum/items.enum itself — kept in full when it fits ACTOR_ENUM_MAX_LENGTH (no duplicate "Possible values" text, the enum field 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, and fetch-actor-details. When the session's tools/list happens to include fetch-actor-details, the dropped-enum note additionally points to it for more context on the Actor — via a new per-session buildInputSchema(ctx) render on ToolBase (mirrors the existing buildDescription(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:agents all clean — 101 test files, 1581 passed, 1 skipped. New/updated unit coverage: all-or-nothing shortenProperties/filterAndShortenEnum (kept-whole vs. dropped-whole, both enum and items.enum), findDroppedEnumProperties, the per-session buildInputSchema gating (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 wrap buildInputSchema, not only the static inputSchema, 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-places Actor (Google Maps Scraper) — the actual motivating case, a real 4,036-value / 71KB categoryFilterWords enum: confirmed via real tools/list that the enum is dropped with a generic note, additionally naming fetch-actor-details only in a session where it's actually loaded; confirmed fetch-actor-details itself 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 confirmed categoryFilterWords: ["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 via internals.js) gained a new field-dependent branch (buildInputSchema) — additive, same safe-fallback shape as the existing buildDescription, but flagging per this repo's own norm on changes to what the hosted server consumes.

Written with AI assistance (Claude).

@github-actions github-actions Bot added t-ai Issues owned by the AI team. tested Temporary label used only programatically for some analytics. labels Aug 14, 2026
@MQ37
MQ37 requested a review from jirispilka August 14, 2026 14:27
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).

@jirispilka jirispilka left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 enum keyword.
  • 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.

MQ37 added 3 commits September 4, 2026 15:32
…-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).
@MQ37

MQ37 commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

Reworked per your feedback — folded into shortenProperties(), one schema for display and AJV, no separate validation-only copy. An enum that fits stays whole; one that doesn't gets dropped entirely (not truncated), with a note and a few examples in the description instead. Covers fetch-actor-details too now, for free, since it shares the same function.

Verified live against compass/crawler-google-places's real 4,036-value categoryFilterWords: a bogus category is rejected by the platform pre-run at zero cost, and restaurant (the exact value class the original bug falsely rejected) is now accepted and actually runs.

One addition beyond your ask: when fetch-actor-details happens to be loaded in a session, the dropped-enum note also points to it — gated per-session so we never hardcode a tool name that might not be loaded.

@MQ37
MQ37 requested a review from jirispilka September 4, 2026 15:00
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).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

t-ai Issues owned by the AI team. tested Temporary label used only programatically for some analytics.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Enum truncation makes the validator reject valid Actor input, and the agent cannot recover (follow-up to #287)

3 participants