feat: return input schema on start() invalid-input errors - #1275
Conversation
call-actor/call-actor-widget fell through to a generic 'verify the Actor name' message on a platform invalid-input 400 from start() — pointing the agent at the wrong problem. Branch that path: for a confirmed platform input-validation error, return the input-specific message and the Actor's input schema, not the Actor-name/existence hint. No extra network call — the schema was already fetched during resolveAndValidateActor. Discriminator: apify-core throws every start()-body validation failure (bad JSON, wrong type, real-schema mismatch) as ApifyApiError type 'invalid-input', status 400 — distinct from 'invalid-input-schema' (a broken schema on the Actor itself, not a user input problem). Written with AI assistance (Claude).
…ols too Direct per-Actor tools (actor_executor.ts) hit the same platform invalid-input 400 from actor.start() as call-actor, but errors there fell through to the fully generic tool-call error mapper — no schema, no input-specific message. Wrap just the start() call and intercept the confirmed platform-validation case; everything else rethrows unchanged, unaffected. Inline, not a shared helper with call_actor.ts: verified LOC — a shared module costs 17 more net lines and a new file to guard a duplication that's structurally capped at these two call sites (the only two places in this codebase that start() an Actor against user-supplied input).
Same AJV-vs-platform-schema explanation was written out at 3 call sites; kept it at its one canonical home (isActorInputValidationError's JSDoc), call sites now just point at it.
05c7b1b to
87865c6
Compare
expect(spies.startInput).toBeUndefined() checked a variable the test's own apifyClient override never wires up — always undefined regardless of code behavior.
…ut-error-schema # Conflicts: # src/const.ts
Same respondUserError message array was duplicated in actor_executor.ts and call_actor.ts for the isActorInputValidationError branch. Single-source it next to isActorInputValidationError, mirroring the existing buildPermissionApprovalTexts/buildActorNotFoundHint pattern.
jirispilka
left a comment
There was a problem hiding this comment.
Nice, thanks!
I made the two comments inline that I would like to have fixed.
Pre-approving.
One more thing: I re-ran the telemetry with Claude (details in #1266, reopened — #1257 closed it without the fix landing). 94.8% of platform schema rejections name a constraint that filterSchemaProperties strips, so the echoed schema mostly won't show why the input was rejected until #1266 lands. The platform's message is what carries the information here — still a clear improvement over "verify the Actor name…". FWIW: call-actor is 89% of these rejections, direct tools 5.4%.
Things I checked and won't nitpick: the as ActorExecutionResult cast (existing idiom there), the predicate scoping vs invalid-input-schema (matches API docs, tested), response size (same schema we already serve in tools/list).
| export function buildInvalidInputTexts(actorName: string, errMsg: string, inputSchema?: ToolInputSchema): string[] { | ||
| return [ | ||
| `Failed to call Actor '${actorName}': ${errMsg}`, | ||
| `Please ensure the input is correct and matches the Actor's input schema.`, | ||
| ...(inputSchema ? [`Input schema:\n${wrapJsonText(inputSchema)}`] : []), | ||
| ]; | ||
| } | ||
|
|
There was a problem hiding this comment.
Move buildInvalidInputTexts out of apify_errors.ts. The module's own header says "Kept in one leaf module (imports only const + apify-client) so logging, telemetry, payments, and the tool layer can share them without import cycles." The PR adds import { wrapJsonText } from './encode_text.js' (runtime) and import type { ToolInputSchema } from '../types.js', so that comment is now false — and a text formatter isn't an error-classification predicate anyway.
Move it to src/utils/mcp.ts, next to respondUserError and getToolCallErrorUserText. That file already imports both ./apify_errors.js (line 5) and wrapJsonText (line 6), so it's zero new imports and no cycle. Move its test from tests/unit/utils.apify_errors.test.ts to tests/unit/utils.mcp.test.ts; the isActorInputValidationError predicate test stays put.
| const executorResult = await actorExecutor.executeActorTool({ | ||
| actorFullName: tool.actorFullName, | ||
| actorId: tool.actorId, | ||
| inputSchema: tool.inputSchema, |
There was a problem hiding this comment.
For the direct Actor calls, the schema is returned but the LLM already has schema in the context. This will only duplicate it.
Drop the schema echo on the direct-actor-tool path. getToolPublicFieldOnly (src/utils/tools.ts:113) serves tool.inputSchema verbatim in tools/list, and MCP clients re-send tool definitions every turn — so on that path the echoed schema is guaranteed to already be in the agent's context at the moment it reads the error. It's duplicate tokens, not redundancy-as-insurance.
src/mcp/tool_dispatch.ts:244-245 — drop inputSchema: tool.inputSchema, keep actorId (telemetry needs it).
src/types.ts — drop inputSchema from ActorExecutionParams, keep actorId.
src/tools/actors/actor_executor.ts — call buildInvalidInputTexts(actorFullName, error.message) with no schema.
The direct path keeps both real wins: no more uncaught throw, plus the platform's own message. This also retires the reviewer's dead-branch question, and shrinks the diff.
call-actor/call-actor-widget keep the echo — there the schema genuinely isn't in context, since the tool takes an Actor name and a free-form input object.
| /** Actor's platform ID — echoed into telemetry on a platform input-validation error. */ | ||
| actorId?: string; | ||
| /** Actor's input schema — echoed back verbatim on a platform input-validation error. */ | ||
| inputSchema?: ToolInputSchema; |
There was a problem hiding this comment.
If we drop schema that is returned for direct tool calls:
Fix the two doc comments that overstate what's returned. src/types.ts says the schema is "echoed back verbatim" and the PR body says "the Actor's input schema".
What
When
actor.start()is rejected by the platform with a confirmedinvalid-input400 (real-schema validation, not our own AJV copy),call-actor/call-actor-widgetand direct per-Actor tools now return the Actor's input schema and the platform's own message instead of falling through to the generic "verify the Actor name, input parameters, and ensure the Actor exists" text. No extra network call — the schema was already fetched duringresolveAndValidateActor(or already known at tools/list time for direct actor tools).Why
Split out from #1256 per review: a remote pre-flight validate-input call can't reduce failures (same validator either way), but the existing fallback message on a real start()-time input rejection points the agent at the wrong problem. This fixes just that response shape.
Direct actor tools (
actor_executor.ts) hit the same platform rejection via their ownactor.start()call but had no interception at all — extended after the initialcall-actorfix landed. Kept as independent inline logic in each of the two call sites rather than a shared helper: measured a shared module at +17 net lines to guard a duplication that's structurally capped at exactly these two places (the only two spots in this codebase thatstart()an Actor against user-supplied input).Testing
pnpm run type-check/lint/test:unit/format/check:agentsclean (93 files, 1361 passed, 1 skipped, zero regressions). Unit tests cover: schema+message returned (not the generic fallback) wheninputSchemais available, the platform message alone when it isn't, the direct-actor-tool executor delegating instead of throwing, any otherstart()error rethrown unchanged, and theisActorInputValidationErrorpredicate matching only the confirmed error type (not the siblinginvalid-input-schema).