Skip to content

fix(code): make tool arg validation errors recoverable - #5659

Merged
Mason Daugherty (mdrxy) merged 6 commits into
mainfrom
mdrxy/code/tool-arg-validation-recoverable
Aug 20, 2026
Merged

fix(code): make tool arg validation errors recoverable#5659
Mason Daugherty (mdrxy) merged 6 commits into
mainfrom
mdrxy/code/tool-arg-validation-recoverable

Conversation

@mdrxy

@mdrxy Mason Daugherty (mdrxy) commented Aug 19, 2026

Copy link
Copy Markdown
Member

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 raises ValueError, ToolNode never converted it, and the error bubbled up as a fatal Agent 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 error ToolMessage it can read and fix in one retry, and the run continues.


This wires langchain's ToolErrorMiddleware (available since langchain>=1.3.14; this package already requires >=1.3.15) into the agent's middleware stack, scoped to ask_user — the tool that validates model-authored arguments by raising ToolArgumentError, a ValueError subclass introduced here so recovery keys off intent rather than a shared built-in type. (read_file is deliberately out of scope: it already catches its own argument errors and returns an error ToolMessage, and its remaining ValueErrors are backend invariants that must stay fatal.) The on_error handler returns a message naming the tool and the validation detail (which the existing error text already carries) for ToolArgumentError, and returns None for everything else so unexpected errors still propagate and halt the run. ToolErrorMiddleware re-raises LangGraph control-flow signals (GraphBubbleUp / interrupts) unchanged, so ask_user's interrupt()-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.

@github-actions github-actions Bot added dcode Related to `deepagents-code` fix A bug fix (PATCH) internal User is a member of the `langchain-ai` GitHub organization size: S 50-199 LOC labels Aug 19, 2026

@open-swe open-swe Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Open SWE Review found 1 potential issue.

Open in WebView Open SWE trace

Comment thread libs/code/deepagents_code/agent.py Outdated
@github-actions github-actions Bot added size: M 200-499 LOC and removed size: S 50-199 LOC labels Aug 19, 2026
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.
@mdrxy
Mason Daugherty (mdrxy) merged commit a7027ed into main Aug 20, 2026
105 checks passed
@mdrxy
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:

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.

shouldn't the burden be on pydantic to validate the Question type, we shouldn't need a custom error?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dcode Related to `deepagents-code` fix A bug fix (PATCH) internal User is a member of the `langchain-ai` GitHub organization size: M 200-499 LOC

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants