refactor(runtime): lift tool dispatch table to typed ToolError (#3576 slice 5)#6124
Merged
Conversation
…slice 5) The `tool_runner/dispatch.rs` match table previously narrowed every arm back to `Result<String, String>` via `.map_err(|e| e.to_string())` at the boundary, then stuffed the string into a hard-error `ToolResult` — discarding the `ToolError` kind that every submodule had already produced. The table now yields `Result<String, ToolError>` natively and converts once, at the end, through `tool_result_from_typed`. That routes each error's `execution_status()` onto the `ToolResult` for every tool, generalising the #5984 fix (previously only the ACL-gated memory/wiki tools): a tool-level `PermissionDenied` (e.g. agent_send depth-exceeded) now surfaces as the soft `Denied` status, reported to the model but not counted toward the consecutive-hard-failure abort, instead of the stringly path's hard default. Inline stringly arms map to their semantic variants: missing url/query -> `MissingParameter`, unknown tool -> `NotFound`, browser-subsystem-missing -> `Unavailable`. The handful of still-stringly downstreams (`browser_tools`, `web_fetch_to_file`, `channel::tool_channel_send`, MCP / skill providers) keep a `.map_err(ToolError::upstream_msg)` bridge pending their own slices. Public `execute_tool` / `execute_tool_raw` signatures are unchanged (still return the `ToolResult` struct), so there is no downstream API change. - Removes 67 boundary `.map_err(|e| e.to_string())` narrows - Drops dispatch.rs from 10 `Result<String, String>` sites to 0 - Adds converter-boundary tests pinning the soft/hard status mapping and the typed wire strings - Updates the now-stale "Unknown tool" assertion in tests/shell.rs to the typed `NotFound` phrasing - Refreshes the error-contracts RFC migration order (slice 5 done) and the error.rs module doc
…mment Comments referencing the current task slice number and superseded issue IDs will rot as the codebase evolves. Trim them: drop the module-level block comment in the test mod (test names are self-describing), shorten the test helper comment and the `typed_not_found` inline note, and remove the `shell.rs` change comment (assertions are already clear). The invariant comment before `tool_result_from_typed` is reworded to state the non-obvious WHY (soft vs hard abort counting) without the issue numbers.
houko
enabled auto-merge (squash)
June 16, 2026 03:24
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.
Summary
Slice 5 of the #3576 error-contract migration: lift the
tool_runner/dispatch.rsmatch table off the stringly boundary.The dispatch table previously narrowed every arm back to
Result<String, String>via.map_err(|e| e.to_string()), then stuffed the string into a hard-errorToolResult— discarding theToolErrorkind that each submodule had already produced. The table now yieldsResult<String, ToolError>natively and converts once, at the end, through the existingtool_result_from_typed.The payoff is the exact problem #3576 names: every tool's
ToolError::execution_status()now reaches theToolResult. A tool-levelPermissionDenied(e.g.agent_senddepth-exceeded) surfaces as the softDeniedstatus — reported to the model but not counted toward the consecutive-hard-failure abort — instead of the stringly path's hard default. This generalises the #5984 fix from the ACL-gated memory/wiki tools to every tool.Changes
.map_err(|e| e.to_string())narrows;dispatch.rsdrops from 10Result<String, String>sites to 0.MissingParameter, unknown tool →NotFound, browser-subsystem-missing →Unavailable..map_err(ToolError::upstream_msg)bridge only over the still-stringly downstreams (browser_tools,web_fetch_to_file,channel::tool_channel_send, MCP / skill providers), each pending its own slice.tests/shell.rs"Unknown tool" assertion to the typedNotFoundphrasing.error.rsmodule doc.Compatibility
Public
execute_tool/execute_tool_rawsignatures are unchanged (still return theToolResultstruct), so there is no downstream API change. The one intentional behaviour change is the soft-Deniedclassification described above.Verification
cargo check -p librefang-runtimewith default features and--no-default-featurescargo clippy -p librefang-runtime— zero warningscargo test -p librefang-runtime --lib— 1991 passed, 0 failed (incl. 4 new dispatch tests)librefang-api/librefang-clicheck clean; tool-runner forwarding integration tests passRefs #3576.