fix(code): make tool arg validation errors recoverable - #5659
Merged
Mason Daugherty (mdrxy) merged 6 commits intoAug 20, 2026
Conversation
Recovery keyed off `isinstance(exc, ValueError)` plus a tool-name filter. That is the wrong axis. It converted any `ValueError` raised while a listed tool ran, whatever the origin. Add `ToolArgumentError(ValueError)`, raised only by `_validate_questions` and `_validate_choices`, and key recovery off the type. A plain `ValueError` from a scoped tool is fatal again. Drop `read_file` from the scope list. It catches its own argument errors and returns an error `ToolMessage` already, and a negative `offset` is clamped rather than rejected, so no model-authored error ever reached the handler. The `ValueError`s that do escape it come from `ReadResult.__post_init__` and backend path resolution. The SDK raises those on purpose to stop a backend from silently skipping unshown source lines. Reporting them as "fix the input and retry" hid the fault and left the model retrying. Log the recovery with `exc_info`. The run no longer fails, so this is the only remaining record that the model sent bad arguments. Remove the subagent instance. `ask_user` is the only scoped tool and subagents never get `AskUserMiddleware`, so it could never fire.
`_chain_tool_call_wrappers` composes the middleware list first to outermost. The middleware was appended right after `AskUserMiddleware`, which left five middleware nested inside it, including `AsyncApprovalHITLMiddleware` and `ServerHooksMiddleware`. Both implement `wrap_tool_call`, so their exceptions reached the handler. `ServerHooksMiddleware` keeps a plain `ValueError` from `parse_hook_resume_value` fatal on purpose: it means the client answered a different request. Wrapping it reported a hook fault to the model as the model's own bad tool input. Move the append to the end of the list and pin the position with a test.
The unit tests build `ToolErrorMiddleware` directly, so they pass whether or not the composed graph recovers. Drive the real agent instead: a scripted `ask_user` call with no questions must yield an error `ToolMessage` and let the run continue. Verified by mutation: emptying `_TOOL_ARG_VALIDATION_TOOLS` makes the run raise and the test fail.
Mason Daugherty (mdrxy)
deleted the
mdrxy/code/tool-arg-validation-recoverable
branch
August 20, 2026 00:27
| raise ToolArgumentError(msg) | ||
|
|
||
|
|
||
| def _validate_questions(questions: list[Question]) -> None: |
Collaborator
There was a problem hiding this comment.
shouldn't the burden be on pydantic to validate the Question type, we shouldn't need a custom error?
Mason Daugherty (mdrxy)
added a commit
that referenced
this pull request
Aug 20, 2026
…tom error Prompted by review on #5659: the schema can carry the validation, so the bespoke recovery path is unnecessary. Move the `ask_user` argument rules out of the imperative `_validate_questions` body and onto the tool schema — `Literal`/`strict` bool/`min_length` for the per-field rules, `AfterValidator` for the cross-field ones (blank choice values, the `multi_select` comma ban, choice/non-choice `choices` consistency) and for the non-empty-list rule on `questions` itself. A rejection now surfaces as a pydantic `ValidationError` during argument parsing, which `ToolNode` already converts to an error `ToolMessage` — the same retry loop the model previously got only for schema-shape errors. The curated message text is preserved via the tool's `handle_validation_error` callback (`` `ask_user` failed: <loc>: <detail>. Fix the input and retry. ``). That makes the #5659 machinery redundant, so it is removed: the `ToolErrorMiddleware` instance, its `_tool_arg_validation_on_error` handler, the `_TOOL_ARG_VALIDATION_TOOLS` scope list, and the `ToolArgumentError` class. Recovery no longer keys off a custom exception type at all; anything pydantic rejects is model-fixable, and anything the tool body raises after parsing (a `ValueError` from an internal invariant, or the `interrupt()` control-flow signal) still propagates and halts the run. The raw-payload backstop in `auto_mode._ask_user_question_count` is unchanged: it reads the unvalidated tool-call args off message history and is independent of how the tool validates. Behavior is unchanged for the model-facing path; this is a refactor, so no changelog entry is warranted beyond the commit itself.
Mason Daugherty (mdrxy)
pushed a commit
that referenced
this pull request
Aug 20, 2026
> [!CAUTION] > Merging this PR will automatically publish to **PyPI** and create a **GitHub release**. For the full release process, see [`.github/RELEASING.md`](https://github.com/langchain-ai/deepagents/blob/main/.github/RELEASING.md). --- _Release notes preview: keep this section in sync with the package `CHANGELOG.md`. Publish reads the merged CHANGELOG via `release.yml`, not this PR description — keep them aligned anyway so the PR stays an accurate historical record for reviewers and anyone returning later._ --- ## [0.1.59](deepagents-code==0.1.58...deepagents-code==0.1.59) (2026-08-20) ### Features - Added support for `managed_config.toml` configuration ([#5604](#5604)) - Multi-select `ask_user` answers are now encoded as JSON arrays ([#5660](#5660)) - Made teardown usage stats configurable ([#5696](#5696)) - Footer pickers now open on click ([#5674](#5674)) - Replaced Gemini 3.6 Flash with Gemini 3.7 Flash ([#5681](#5681)) ### Bug fixes - Made tool argument validation errors recoverable ([#5659](#5659)) - Improved streaming performance for tool-call arguments to run in linear time ([#5712](#5712)) - Fixed durable-mask config resolution with ranked resolver behavior ([#5672](#5672)) - Skipped background sync in Apple Terminal ([#5666](#5666)) - Hid thread IDs when tracing is disabled ([#5692](#5692)) - Kept installed providers visible in `/auth` ([#5689](#5689)) - Preloaded the auth UI before notification handoff ([#5697](#5697)) - Updated and clarified UI copy across Auto mode, YOLO hints, classifier notices, `/tokens`, line-number toggles, review failures, onboarding Tavily cancellation, and OpenAI subscription login labels ([#5685](#5685), [#5694](#5694), [#5684](#5684), [#5687](#5687), [#5680](#5680), [#5688](#5688), [#5686](#5686), [#5691](#5691), [#5693](#5693)) - Removed the `Muse Spark 1.1` recommendation ([#5683](#5683)) _End release notes preview._ --- > [!NOTE] > A **community contributors** list and a **Special thanks** section (crediting the users who filed the issues this release's PRs closed) are appended to the GitHub release notes automatically at publish time (see [Release Pipeline](https://github.com/langchain-ai/deepagents/blob/main/.github/RELEASING.md#release-pipeline), step 3). --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: langchain-oss-automated-triage[bot] <248757908+langchain-oss-automated-triage[bot]@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A model that passes malformed arguments to
ask_user(for example an empty questions list, or a choice with a blank value) previously crashed the whole run: the tool raisesValueError,ToolNodenever converted it, and the error bubbled up as a fatalAgent error. The model never saw the failure and got no chance to reissue corrected arguments, so the user's turn dead-ended. Now those validation errors surface to the model as an errorToolMessageit can read and fix in one retry, and the run continues.This wires langchain's
ToolErrorMiddleware(available sincelangchain>=1.3.14; this package already requires>=1.3.15) into the agent's middleware stack, scoped toask_user— the tool that validates model-authored arguments by raisingToolArgumentError, aValueErrorsubclass introduced here so recovery keys off intent rather than a shared built-in type. (read_fileis deliberately out of scope: it already catches its own argument errors and returns an errorToolMessage, and its remainingValueErrors are backend invariants that must stay fatal.) Theon_errorhandler returns a message naming the tool and the validation detail (which the existing error text already carries) forToolArgumentError, and returnsNonefor everything else so unexpected errors still propagate and halt the run.ToolErrorMiddlewarere-raises LangGraph control-flow signals (GraphBubbleUp/ interrupts) unchanged, soask_user'sinterrupt()-based flow is unaffected.The tool itself still raises; only the run-level handling changes. This is the approach decided externally and discussed in langchain-ai/langchain#38781.