Skip to content

Support non-object arguments for external tools - #2496

Merged
SteveSandersonMS merged 1 commit into
mainfrom
stephentoub-apply-patch-override-research
Sep 3, 2026
Merged

Support non-object arguments for external tools#2496
SteveSandersonMS merged 1 commit into
mainfrom
stephentoub-apply-patch-override-research

Conversation

@stephentoub

Copy link
Copy Markdown
Collaborator

External tool overrides such as apply_patch can receive freeform JSON values, but the .NET SDK previously assumed every invocation root was an object and failed before entering the host handler.

This change preserves named-property expansion for object roots and binds any non-object JSON value to the function's sole schema parameter. When additional parameters are optional, it selects the sole required parameter; ambiguous schemas return a clear tool error instead of guessing. Fake-server coverage exercises string and numeric values, optional parameters, successful responses, and ambiguous rejection.

Tests

  • dotnet test dotnet\test\GitHub.Copilot.SDK.Test.csproj --filter "FullyQualifiedName~GitHub.Copilot.Test.Unit.ClientSessionLifetimeTests" --no-restore

Generated by Copilot

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@stephentoub
stephentoub requested a review from a team as a code owner September 3, 2026 03:50
Copilot AI balanced review requested due to automatic review settings September 3, 2026 03:50

Copilot AI 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.

Copilot review overview

🟢 Approval recommended

The reviewed implementation and tests have no unresolved approval-blocking issues.

Review tier: Balanced
Findings: None

What changed in this PR

Adds .NET support for external tools receiving non-object JSON arguments while preserving object-property binding.

Changes:

  • Binds non-object JSON to the sole or sole-required schema parameter.
  • Rejects ambiguous schemas with a clear error.
  • Adds coverage for string, numeric, optional-parameter, successful, and rejected invocations.
File Description
dotnet/​src/​Session.cs Implements non-object argument binding and schema validation.
dotnet/​test/​Unit/​ClientSessionLifetimeTests.cs Adds fake-server coverage and tool-response handling.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Cross-SDK Consistency Review

Reviewed the change to dotnet/src/Session.cs (ExecuteToolAndRespondAsync / GetSingleParameterName), which fixes a bug where an ExternalToolRequestedEvent carrying non-object arguments (e.g., a bare string or number, as built-in tools like apply_patch can send) would throw when the code tried to EnumerateObject() on a non-object JsonElement. The fix now special-cases non-object arguments and binds them to the tool's single (or sole-required) parameter, matching how Microsoft.Extensions.AI's AIFunctionFactory-generated schemas expect a single named property.

Assessment: no cross-SDK parity gap.

This bug is unique to the .NET SDK's API design: CopilotTool.DefineTool uses reflection (AIFunctionFactory) to turn a strongly-typed C# lambda/method into a tool with a generated JSON schema, then reconstructs a Dictionary<string, object> of named arguments from the incoming JSON before invoking the underlying delegate. That reconstruction step (aiFunctionArgs[prop.Name] = ...) is what previously assumed the incoming arguments were always a JSON object.

The other SDKs don't have this failure mode because they don't reflectively bind incoming arguments to individual function parameters:

  • Node.js/TypeScript: ToolHandler receives args: unknown (the raw parsed JSON value) directly — no property enumeration.
  • Python: tools.py's wrapped_handler passes invocation.arguments straight through (or validates it as a whole via a Pydantic model) rather than fanning it out into positional parameters.
  • Go: tool invocations carry raw json.RawMessage/any arguments handed to the user's handler as-is.
  • Java: executeToolAndRespondAsync puts arguments directly onto ToolInvocation as a JsonNode, with no per-parameter unpacking.
  • Rust: ToolInvocation.arguments is a raw serde_json::Value that handlers deserialize themselves (e.g., via serde_json::from_value).

So a non-object arguments payload (string/number) would already flow through unchanged in all of these — only .NET's function-signature-based binding needed the guard added here. This looks like a good, self-contained language-specific fix and doesn't require mirroring elsewhere. The accompanying unit tests (single-parameter binding, ambiguous multi-parameter rejection, numeric arguments, sole-required-parameter binding) look thorough for the .NET-specific code path.

No action needed in other SDKs.

Generated by SDK Consistency Review Agent for #2496 · sonnet50 32.3 AIC · ⌖ 5.19 AIC · ⊞ 8.8K ·

@SteveSandersonMS SteveSandersonMS 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.

Approving — this is a well-scoped, verified fix for a real crash when overriding apply_patch (and any other built-in tool that sends a bare string/number argument) in the .NET SDK. I reverted the source change locally and confirmed the new tests fail with the exact EnumerateObject-on-non-object exception this PR fixes, then confirmed they pass with the fix restored and the rest of the 57-test suite is unaffected. Confirmed this is .NET-specific (other SDKs pass raw JSON through without reflective binding), so no other-language changes are needed.

@SteveSandersonMS
SteveSandersonMS added this pull request to the merge queue Sep 3, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Sep 3, 2026
@stephentoub
stephentoub added this pull request to the merge queue Sep 3, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Sep 3, 2026
@SteveSandersonMS
SteveSandersonMS merged commit e2af6c4 into main Sep 3, 2026
54 checks passed
@SteveSandersonMS
SteveSandersonMS deleted the stephentoub-apply-patch-override-research branch September 3, 2026 21:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants