Skip to content

Fix schema-guided tool parameter coercion#1143

Merged
henrypark133 merged 7 commits intostagingfrom
fix/shared-tool-param-coercion
Mar 14, 2026
Merged

Fix schema-guided tool parameter coercion#1143
henrypark133 merged 7 commits intostagingfrom
fix/shared-tool-param-coercion

Conversation

@henrypark133
Copy link
Copy Markdown
Collaborator

Summary

  • add a shared schema-guided parameter normalization layer before approval and execution
  • remove the old WASM-only coercion path while preserving typed WASM discovery schema fallback
  • add unit and e2e regressions for Sheets-style values and Docs-style requests

Testing

  • cargo test --features libsql tools::coercion::tests
  • cargo test --features libsql test_execute_normalizes_stringified_array_params
  • cargo test --features libsql test_execute_tool_task_normalizes_params_before_approval
  • cargo test --features libsql test_untyped_override_preserves_extracted_discovery_schema
  • cargo test --features libsql --test e2e_tool_param_coercion

Copilot AI review requested due to automatic review settings March 13, 2026 19:03
@github-actions github-actions bot added scope: agent Agent core (agent loop, router, scheduler) scope: tool Tool infrastructure scope: tool/wasm WASM tool sandbox scope: tool/builder Dynamic tool builder scope: worker Container worker size: XL 500+ changed lines risk: medium Business logic, config, or moderate-risk modules contributor: core 20+ merged PRs labels Mar 13, 2026
@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a robust, centralized mechanism for coercing tool parameters based on their defined JSON schema. This enhancement addresses a common challenge where Large Language Models (LLMs) might provide parameters as stringified values (e.g., "5" instead of 5, or JSON strings for complex objects/arrays). By applying schema-guided normalization early in the tool execution lifecycle, it ensures that all tool inputs are correctly typed and formatted before they are approved or executed, improving reliability and reducing potential errors. The change refactors existing, more ad-hoc coercion logic into a unified system and includes extensive testing to cover various data types and structures.

Highlights

  • Shared Parameter Normalization: A new, shared schema-guided parameter normalization layer was added, ensuring consistent type coercion across tool approvals and executions.
  • WASM Coercion Refactor: The old WASM-only coercion path was removed, replaced by the new shared layer, while preserving typed WASM discovery schema fallback.
  • New Regression Tests: Comprehensive unit and end-to-end regression tests were added to validate the new coercion logic for common scenarios like Sheets-style values and Docs-style requests.
Changelog
  • src/agent/routine_engine.rs
    • Imported the new prepare_tool_params function.
    • Applied parameter preparation before tool approval checks, validation, and execution.
  • src/agent/scheduler.rs
    • Imported the new prepare_tool_params function.
    • Applied parameter preparation before tool approval checks.
    • Added a new test case test_execute_tool_task_normalizes_params_before_approval to verify pre-approval normalization.
  • src/tools/builder/core.rs
    • Imported the new prepare_tool_params function.
    • Applied parameter preparation before tool execution.
  • src/tools/coercion.rs
    • Added a new module containing core logic for schema-guided parameter coercion.
    • Implemented prepare_tool_params and prepare_params_for_schema functions.
    • Included helper functions coerce_value, coerce_string_value, and schema_allows_type for recursive type conversion.
    • Provided extensive unit tests covering scalar strings, stringified arrays and objects, nullable types, additional properties, and invalid JSON handling.
  • src/tools/execute.rs
    • Imported the new prepare_tool_params function.
    • Applied parameter preparation before tool validation and redaction.
    • Added ArrayEchoTool and test_execute_normalizes_stringified_array_params for testing array parameter normalization.
  • src/tools/mod.rs
    • Declared the new coercion module.
    • Exported prepare_tool_params for internal crate use.
  • src/tools/wasm/wrapper.rs
    • Updated comments to reflect the new role of discovery schema in runtime parameter preparation.
    • Added typed_property_count helper function.
    • Modified with_schema to intelligently preserve extracted WASM schemas for discovery/runtime preparation if an untyped override is provided.
    • Removed the deprecated effective_for_coercion function and the direct call to coerce_params_to_schema.
    • Enhanced build_tool_usage_hint to advise passing native JSON for container types.
    • Removed several old test_coerce_params_* unit tests, as their functionality is now covered by the new coercion.rs module.
    • Added test_untyped_override_preserves_extracted_discovery_schema to validate schema preservation logic.
  • src/worker/job.rs
    • Imported the new prepare_tool_params function.
    • Applied parameter preparation before tool approval checks and hook processing.
  • tests/e2e_tool_param_coercion.rs
    • Added a new end-to-end test file for schema-guided tool parameter normalization.
    • Introduced SheetsWriteFixtureTool and DocsBatchUpdateFixtureTool to simulate real-world API interactions.
    • Implemented e2e_normalizes_stringified_google_sheets_values and e2e_normalizes_stringified_google_docs_requests tests to verify correct coercion of stringified JSON parameters.
Activity
  • The author, henrypark133, provided detailed testing instructions, including specific cargo test commands for unit and e2e regressions, indicating thorough local validation.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a centralized and robust schema-guided parameter normalization layer. The new coercion.rs module provides a powerful recursive implementation that correctly handles scalar types, nested objects, arrays, and even stringified JSON values, which is a common issue with LLM-generated tool calls.

The changes are consistently applied across all tool execution paths, including routines, the scheduler, the builder, and the main job worker, which greatly improves code maintainability and correctness by removing duplicated and limited coercion logic. The old WASM-specific coercion path has been cleanly removed, and a clever fallback mechanism for WASM discovery schemas has been preserved.

The addition of comprehensive unit tests for the new coercion logic and end-to-end regression tests for common real-world scenarios (like Google Sheets and Docs API formats) provides strong confidence in the correctness of this fix.

Overall, this is an excellent and well-executed refactoring that significantly improves the reliability of tool parameter handling. I have no further comments.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces a shared, schema-guided parameter normalization step for tool calls so that stringified scalars (e.g., "true", "5") and quoted JSON containers (arrays/objects) are coerced into the correct JSON types before approval checks, validation, and execution. It centralizes behavior that previously lived in the WASM wrapper and adds regressions for Google Sheets/Docs-style payloads that models commonly send.

Changes:

  • Add prepare_tool_params (schema-guided coercion) and apply it across multiple tool execution entry points (worker, scheduler, routines, builder, shared execute pipeline).
  • Remove the WASM-only coercion path while preserving WASM discovery-schema fallback behavior and improving tool error hints around container params.
  • Add unit + E2E regressions covering stringified array/object payloads and approval behavior on normalized params.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
tests/e2e_tool_param_coercion.rs New E2E regressions for Sheets/Docs-style quoted JSON container arguments.
src/worker/job.rs Normalize params before approval/hooks/validation and again after hook modifications.
src/tools/wasm/wrapper.rs Remove WASM-specific coercion; preserve typed discovery schema when override is untyped; improve usage hints.
src/tools/mod.rs Wire in new coercion module and re-export prepare_tool_params internally.
src/tools/execute.rs Normalize params in shared execution pipeline; add unit regression for stringified arrays.
src/tools/coercion.rs New shared schema-guided coercion implementation + unit tests.
src/tools/builder/core.rs Normalize params before executing build tools.
src/agent/scheduler.rs Normalize params before approval in scheduler tool subtasks.
src/agent/routine_engine.rs Normalize params before approval/validation/execution for lightweight routine tools.
Comments suppressed due to low confidence (1)

src/agent/scheduler.rs:525

  • execute_tool_task normalizes params via prepare_tool_params and then calls execute_tool_with_safety, which normalizes again internally. This double-prepares/clones the params tree (potentially expensive for large payloads) and makes it harder to reason about where normalization happens. Consider either (a) passing the original params into execute_tool_with_safety and only using the prepared copy for the approval check, or (b) adding an execute_tool_with_prepared_params/flag so the shared pipeline can skip the second normalization when the caller already prepared them.
        let params = prepare_tool_params(tool.as_ref(), &params);

        // Scheduler-specific approval check
        let requirement = tool.requires_approval(&params);
        let blocked =
            ApprovalContext::is_blocked_or_default(&approval_context, tool_name, requirement);
        if blocked {
            return Err(crate::error::ToolError::AuthRequired {
                name: tool_name.to_string(),
            }
            .into());
        }

        // Delegate to shared tool execution pipeline
        let output_str = crate::tools::execute::execute_tool_with_safety(
            &tools, &safety, tool_name, &params, &job_ctx,
        )
        .await?;

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copilot AI review requested due to automatic review settings March 13, 2026 19:27
@henrypark133 henrypark133 requested a review from zmanian March 13, 2026 19:27
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces a shared, schema-guided tool-parameter normalization step that runs before approval checks, validation, and execution across the main tool execution entry points, and removes the prior WASM-only coercion path while preserving typed WASM discovery schema behavior.

Changes:

  • Add prepare_tool_params / schema-guided coercion module and integrate it into worker, scheduler, routine, builder, and generic tool execution paths.
  • Adjust WASM wrapper schema override behavior and update error hinting to reference tool_info and avoid quoted JSON container inputs.
  • Add unit + E2E regressions covering Sheets-style values and Docs-style requests payload shapes with stringified JSON containers.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tests/e2e_tool_param_coercion.rs New E2E trace tests validating container coercion for Sheets/Docs-like payloads.
src/worker/job.rs Normalize params prior to approval + hook handling; re-normalize hook-modified params.
src/tools/wasm/wrapper.rs Remove WASM-only coercion; preserve extracted typed discovery schema when sidecar override is untyped; improve error hints.
src/tools/mod.rs Wire in new internal coercion module and export prepare_tool_params within crate.
src/tools/execute.rs Normalize params before validation/logging/execution; add regression test.
src/tools/coercion.rs New schema-guided normalization implementation + unit tests.
src/tools/builder/core.rs Normalize params before executing build tools.
src/agent/scheduler.rs Normalize params before scheduler-specific approval gating; add regression test.
src/agent/routine_engine.rs Normalize params before approval + validation + execution in routine tool runs.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

.map(|props| {
props
.values()
.filter(|prop| prop.get("type").and_then(|t| t.as_str()).is_some())
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 7909289. typed_property_count now treats union/nullable type arrays as typed, and also recognizes other typed schema constructs ($ref, combinators, items, properties, typed additionalProperties) so we do not misclassify valid typed overrides as untyped.

Comment on lines +1286 to +1299
fn schema_contains_container_properties(schema: &serde_json::Value) -> bool {
schema
.get("properties")
.and_then(|p| p.as_object())
.map(|props| {
props.values().any(|prop| {
matches!(
prop.get("type").and_then(|t| t.as_str()),
Some("array" | "object")
)
})
})
.unwrap_or(false)
}
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 7909289. The container-hint check now uses the same schema type detection logic, so nullable container properties like {"type": ["array", "null"]} and object equivalents still trigger the native-JSON guidance.

Comment on lines +51 to +52
if let Some(current) = coerced.get(key).cloned() {
coerced.insert(key.clone(), coerce_value(&current, prop_schema));
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Also addressed in 7909289. I rewrote the object branch to coerce fields in place within the cloned map, which removes the extra get(...).cloned() / insert(...) churn while preserving the same behavior.

Copy link
Copy Markdown
Contributor

@G7CNF G7CNF left a comment

Choose a reason for hiding this comment

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

Reviewed for tool-calling blocker scope: schema-guided coercion is applied across scheduler/routine/worker paths and has targeted regressions. LGTM.

@G7CNF
Copy link
Copy Markdown
Contributor

G7CNF commented Mar 13, 2026

Tool-calling blocker triage: this appears ready from a reliability perspective (shared schema-guided param normalization across scheduler/routine/worker + regressions). Recommend maintainer merge once branch policy requirements are satisfied.

zmanian
zmanian previously approved these changes Mar 14, 2026
Copy link
Copy Markdown
Collaborator

@zmanian zmanian left a comment

Choose a reason for hiding this comment

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

Review

Solid, well-motivated change. Lifting parameter coercion from the WASM layer to the shared execution layer is the right architectural move -- LLMs send stringified values regardless of tool type.

The new coerce_value() is recursive and handles nested objects, stringified JSON arrays/objects, nullable union types, and additionalProperties. Good test coverage (10 unit tests in coercion.rs, integration tests in execute.rs and scheduler.rs, E2E trace tests for Sheets/Docs payloads).

The with_schema guard using typed_property_count correctly prevents untyped sidecar overrides from clobbering typed WASM-extracted discovery schemas. build_tool_usage_hint conditionally adding the array/object hint is a nice touch.

Minor notes (non-blocking)

  • Variable shadowing -- let params = prepare_tool_params(tool.as_ref(), &params) shadows the function parameter in several call sites. Consider normalized_params for readability.
  • Double normalization in worker/job.rs hook path -- hook-modified params get re-normalized while the fallback uses already-normalized params. Correct behavior but a comment explaining the asymmetry would help.
  • oneOf/anyOf/$ref schemas unsupported -- worth a comment in coerce_value() noting this limitation.
  • Cloning on every call -- prepare_tool_params always clones even when no coercion needed. Cow<'_, Value> could avoid this for hot paths in a follow-up.

Approve with the above as optional improvements.

Copilot AI review requested due to automatic review settings March 14, 2026 04:30
@henrypark133
Copy link
Copy Markdown
Collaborator Author

Addressed the non-blocking review notes in 0f36e4e.

  • Renamed the shadowing locals to normalized_params / effective_params across the shared execution call sites for readability.
  • Added a comment in worker/job.rs explaining why hook-modified params are re-normalized while the fallback path reuses the already-normalized input.
  • Added an explicit note in coerce_value() that anyOf / oneOf / allOf / $ref are not resolved yet.

I left the clone-avoidance idea as a follow-up; it changes the API shape more than I want in this PR.

Verification on this branch: cargo fmt --all and cargo clippy --all-targets --all-features -- -D warnings both pass.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes schema-guided tool parameter coercion by introducing a shared normalization step that runs before approval checks, validation, and execution, and by removing the prior WASM-only coercion logic while preserving typed discovery schema behavior for WASM tools.

Changes:

  • Added a shared prepare_tool_params normalization layer (schema-guided coercion of stringified scalars/containers).
  • Integrated normalization into major tool execution entrypoints (worker, scheduler, routines, shared execute pipeline, builder tools).
  • Added unit + E2E regression tests covering Sheets-style values and Docs-style requests payloads (quoted JSON arrays/objects).

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
tests/e2e_tool_param_coercion.rs New E2E tests exercising normalization through the real agent loop with Sheets/Docs-shaped payloads.
src/tools/coercion.rs Implements schema-guided parameter preparation/coercion used across execution paths.
src/tools/mod.rs Wires the new coercion module and re-exports prepare_tool_params for crate-internal use.
src/tools/execute.rs Normalizes params before validation/logging/execution in the shared execution pipeline; adds a regression unit test.
src/worker/job.rs Normalizes params before approval, hooks, validation, and execution in worker job execution.
src/agent/scheduler.rs Normalizes params before scheduler approval checks and forwards into shared execution.
src/agent/routine_engine.rs Normalizes params before approval/validation/execution for lightweight routine tool calls.
src/tools/builder/core.rs Normalizes params before executing build tools with a dummy context.
src/tools/wasm/wrapper.rs Removes WASM-only coercion path, improves schema override handling, and enhances tool-usage hinting.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 508 to 528
@@ -518,7 +520,11 @@ impl Scheduler {

// Delegate to shared tool execution pipeline
let output_str = crate::tools::execute::execute_tool_with_safety(
&tools, &safety, tool_name, &params, &job_ctx,
&tools,
&safety,
tool_name,
&normalized_params,
&job_ctx,
)
Copy link
Copy Markdown
Collaborator

@zmanian zmanian left a comment

Choose a reason for hiding this comment

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

Review: Extract schema-guided tool parameter coercion into shared module

Well-designed extraction. LLMs frequently send stringified values ("true", "42") that break tools expecting native types. This PR centralizes the coercion logic that was previously scattered/missing.

Positives:

  • New src/tools/coercion.rs provides prepare_tool_params() that coerces values guided by the tool's JSON Schema
  • Handles string->integer, string->boolean, string->number, string->array (JSON parse), string->object (JSON parse)
  • Applied consistently at all execution points: routine engine, scheduler, builder, direct execute
  • Extracted from WASM wrapper (which had ad-hoc coercion), reducing its complexity by ~70 lines
  • Schema-awareness means coercion only happens when the schema says the target type isn't string
  • Good test coverage in tests/e2e_tool_param_coercion.rs with edge cases
  • Test in scheduler verifies coercion happens before approval check (stringified "true" should resolve to true for approval)

Minor notes:

  • discovery_schema() is used as the coercion guide, which is the right choice (it includes full property definitions)
  • G7CNF already approved

Title check: accurate.

LGTM.

@henrypark133 henrypark133 merged commit c79754d into staging Mar 14, 2026
18 checks passed
@henrypark133 henrypark133 deleted the fix/shared-tool-param-coercion branch March 14, 2026 23:27
@ironclaw-ci ironclaw-ci bot mentioned this pull request Mar 17, 2026
bkutasi pushed a commit to bkutasi/ironclaw that referenced this pull request Mar 28, 2026
* Fix schema-guided tool parameter coercion

* Fix CI checks for coercion regression tests

* Finish panic-scan annotations

* Avoid redundant worker param preparation

* Keep panic-scan annotations rustfmt-stable

* Handle nullable WASM schema review feedback

* Address param coercion review notes
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

contributor: core 20+ merged PRs risk: medium Business logic, config, or moderate-risk modules scope: agent Agent core (agent loop, router, scheduler) scope: tool/builder Dynamic tool builder scope: tool/wasm WASM tool sandbox scope: tool Tool infrastructure scope: worker Container worker size: XL 500+ changed lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants