From 2bba5af6d63d37f1e80920a19d7ebc93e01d1085 Mon Sep 17 00:00:00 2001 From: Pasindu Madusha Date: Mon, 21 Sep 2026 12:07:45 +0530 Subject: [PATCH 1/2] .NET: expose host-tool JsonSchema in LocalCodeAct --- .../Internal/InstructionBuilder.cs | 19 ++++++++ .../README.md | 16 +++++++ .../InstructionBuilderTests.cs | 43 +++++++++++++++++++ .../LocalCodeActProviderTests.cs | 30 +++++++++++++ .../LocalExecuteCodeFunctionTests.cs | 37 ++++++++++++++++ 5 files changed, 145 insertions(+) create mode 100644 dotnet/tests/Microsoft.Agents.AI.LocalCodeAct.UnitTests/LocalExecuteCodeFunctionTests.cs diff --git a/dotnet/src/Microsoft.Agents.AI.LocalCodeAct/Internal/InstructionBuilder.cs b/dotnet/src/Microsoft.Agents.AI.LocalCodeAct/Internal/InstructionBuilder.cs index c54cb6dc49d..fc6b1d62115 100644 --- a/dotnet/src/Microsoft.Agents.AI.LocalCodeAct/Internal/InstructionBuilder.cs +++ b/dotnet/src/Microsoft.Agents.AI.LocalCodeAct/Internal/InstructionBuilder.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Text; +using System.Text.Json; using Microsoft.Extensions.AI; namespace Microsoft.Agents.AI.LocalCodeAct.Internal; @@ -40,6 +41,24 @@ public static string BuildExecuteCodeDescription( } sb.AppendLine(); + + JsonElement schema = tool.JsonSchema; + if (schema.ValueKind is JsonValueKind.Object + && schema.TryGetProperty("properties", out var properties) + && properties.ValueKind is JsonValueKind.Object) + { + if (properties.EnumerateObject().MoveNext()) + { + sb.AppendLine(" Parameters (JSON Schema):"); + sb.Append(" "); + sb.Append(schema.GetRawText()); + sb.AppendLine(); + } + else + { + sb.AppendLine(" Parameters: none."); + } + } } } diff --git a/dotnet/src/Microsoft.Agents.AI.LocalCodeAct/README.md b/dotnet/src/Microsoft.Agents.AI.LocalCodeAct/README.md index 4bcd95720fc..eb8249264fb 100644 --- a/dotnet/src/Microsoft.Agents.AI.LocalCodeAct/README.md +++ b/dotnet/src/Microsoft.Agents.AI.LocalCodeAct/README.md @@ -94,6 +94,22 @@ total = await call_tool("add", a=2, b=3) print(total) ``` +The `execute_code` description lists each registered tool with its existing +`AIFunction.JsonSchema`, so the model can see parameter names, requiredness, +descriptions, enums, and nested shapes and call `call_tool(...)` correctly. +Tools that do not describe their input are listed by name only. + +Two things to keep in mind: + +* Parameter names, descriptions, defaults, and enum values are **model-visible**. + Do not put credentials, tenant identifiers, or other secrets in them. +* C# enums serialize as integers by default, so their member names do not reach + the schema. Apply `JsonStringEnumConverter` to the enum if the model should see + the allowed values. + +This does not change `execute_code`'s own input schema, which remains the single +`code` parameter. + ## Tool Approval Generated code reaches registered tools through `call_tool(...)`, which invokes diff --git a/dotnet/tests/Microsoft.Agents.AI.LocalCodeAct.UnitTests/InstructionBuilderTests.cs b/dotnet/tests/Microsoft.Agents.AI.LocalCodeAct.UnitTests/InstructionBuilderTests.cs index 3795ab39aee..4754db1b54b 100644 --- a/dotnet/tests/Microsoft.Agents.AI.LocalCodeAct.UnitTests/InstructionBuilderTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.LocalCodeAct.UnitTests/InstructionBuilderTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using System.Collections.Generic; +using System.ComponentModel; using Microsoft.Agents.AI.LocalCodeAct.Internal; using Microsoft.Extensions.AI; @@ -24,6 +25,48 @@ public void BuildExecuteCodeDescription_MentionsToolsWhenProvided() Assert.Contains("get_weather", description); } + [Fact] + public void BuildExecuteCodeDescription_WithToolParameters_IncludesParameterMetadata() + { + static string Lookup( + [Description("Search text")] string query, + [Description("Maximum results")] int limit = 10) => $"{query}:{limit}"; + + var tool = AIFunctionFactory.Create(Lookup, name: "lookup", description: "Look up an item."); + var description = InstructionBuilder.BuildExecuteCodeDescription( + new List { tool }, + new List()); + + Assert.Contains("lookup", description); + Assert.Contains("query", description); + Assert.Contains("Search text", description); + Assert.Contains("Maximum results", description); + Assert.Contains("\"required\":[\"query\"]", description); + Assert.Contains("\"default\":10", description); + } + + [Fact] + public void BuildExecuteCodeDescription_WithZeroParameterTool_ReportsNoParameters() + { + var tool = AIFunctionFactory.Create(() => "ok", name: "ping", description: "Pings."); + var description = InstructionBuilder.BuildExecuteCodeDescription( + new List { tool }, + new List()); + + Assert.Contains("Parameters: none.", description); + Assert.DoesNotContain("Parameters (JSON Schema)", description); + } + + [Fact] + public void BuildExecuteCodeDescription_WithUnconstrainedSchema_OmitsParameterBlock() + { + var tools = new List { new TestTool("legacy_tool", "Does not describe its input.") }; + var description = InstructionBuilder.BuildExecuteCodeDescription(tools, new List()); + + Assert.Contains("legacy_tool", description); + Assert.DoesNotContain("Parameters", description); + } + [Fact] public void BuildExecuteCodeDescription_MentionsMountsWhenProvided() { diff --git a/dotnet/tests/Microsoft.Agents.AI.LocalCodeAct.UnitTests/LocalCodeActProviderTests.cs b/dotnet/tests/Microsoft.Agents.AI.LocalCodeAct.UnitTests/LocalCodeActProviderTests.cs index d6435f314ec..11996a7b30e 100644 --- a/dotnet/tests/Microsoft.Agents.AI.LocalCodeAct.UnitTests/LocalCodeActProviderTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.LocalCodeAct.UnitTests/LocalCodeActProviderTests.cs @@ -36,6 +36,36 @@ public async Task ProvideAIContextAsync_ReturnsExecuteCodeToolAndInstructionsAsy Assert.False(string.IsNullOrWhiteSpace(context.Instructions)); } + [Fact] + public async Task ProvideAIContextAsync_WithParameterizedTool_CarriesHostToolSchemaAsync() + { + var tool = AIFunctionFactory.Create((string query) => "ok", name: "lookup"); + var options = Options(); + options.Tools = [tool]; + using var provider = new LocalCodeActProvider("/usr/bin/python3", options); + + var context = await provider.InvokingAsync(NewInvokingContext()); + var function = Assert.IsAssignableFrom(context!.Tools!.First()); + + Assert.Contains("query", function.Description); + } + + [Fact] + public async Task ProvideAIContextAsync_WithParameterizedTool_KeepsCodeOnlyInputSchemaAsync() + { + var tool = AIFunctionFactory.Create((string query) => "ok", name: "lookup"); + var options = Options(); + options.Tools = [tool]; + using var provider = new LocalCodeActProvider("/usr/bin/python3", options); + + var context = await provider.InvokingAsync(NewInvokingContext()); + var function = Assert.IsAssignableFrom(context!.Tools!.First()); + var inputSchema = function.JsonSchema.GetRawText(); + + Assert.Contains("\"code\"", inputSchema); + Assert.DoesNotContain("query", inputSchema); + } + [Fact] public void AddAndRemoveTools_RoundTrips() { diff --git a/dotnet/tests/Microsoft.Agents.AI.LocalCodeAct.UnitTests/LocalExecuteCodeFunctionTests.cs b/dotnet/tests/Microsoft.Agents.AI.LocalCodeAct.UnitTests/LocalExecuteCodeFunctionTests.cs new file mode 100644 index 00000000000..0dac179b3db --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.LocalCodeAct.UnitTests/LocalExecuteCodeFunctionTests.cs @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.Extensions.AI; + +namespace Microsoft.Agents.AI.LocalCodeAct.UnitTests; + +public sealed class LocalExecuteCodeFunctionTests +{ + private static LocalCodeActProviderOptions Options(AIFunction tool) => + new() + { + ValidationDisabled = true, // No subprocess will be launched in these tests + Tools = [tool], + }; + + [Fact] + public void Description_WithParameterizedTool_CarriesHostToolSchema() + { + var tool = AIFunctionFactory.Create((string query) => "ok", name: "lookup"); + + var function = new LocalExecuteCodeFunction("/usr/bin/python3", Options(tool)); + + Assert.Contains("query", function.Description); + } + + [Fact] + public void JsonSchema_WithParameterizedTool_StaysCodeOnly() + { + var tool = AIFunctionFactory.Create((string query) => "ok", name: "lookup"); + + var function = new LocalExecuteCodeFunction("/usr/bin/python3", Options(tool)); + var inputSchema = function.JsonSchema.GetRawText(); + + Assert.Contains("\"code\"", inputSchema); + Assert.DoesNotContain("query", inputSchema); + } +} From 31c4c1fb71c37c84b3e42b90655ac051953963bc Mon Sep 17 00:00:00 2001 From: Pasindu Madusha Date: Sat, 26 Sep 2026 15:17:16 +0530 Subject: [PATCH 2/2] .NET: add UTF-8 BOM to LocalExecuteCodeFunctionTests --- .../LocalExecuteCodeFunctionTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/tests/Microsoft.Agents.AI.LocalCodeAct.UnitTests/LocalExecuteCodeFunctionTests.cs b/dotnet/tests/Microsoft.Agents.AI.LocalCodeAct.UnitTests/LocalExecuteCodeFunctionTests.cs index 0dac179b3db..44172504a5a 100644 --- a/dotnet/tests/Microsoft.Agents.AI.LocalCodeAct.UnitTests/LocalExecuteCodeFunctionTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.LocalCodeAct.UnitTests/LocalExecuteCodeFunctionTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using Microsoft.Extensions.AI;