From 2efc55d994ddf277465846f4c131a1d1cddf59b5 Mon Sep 17 00:00:00 2001 From: leilei3167 Date: Fri, 18 Sep 2026 13:53:09 +0000 Subject: [PATCH 1/2] .NET: include host-tool JsonSchema in Hyperlight execute_code descriptions BuildExecuteCodeDescription listed host AIFunction names and descriptions but never read JsonSchema, so models missed parameter names, requiredness, descriptions, enums, and nested shapes. Append each tool's existing schema to the model-facing description and cover it with a parameterized regression. --- .../Internal/InstructionBuilder.cs | 16 +++++++- .../InstructionBuilderTests.cs | 37 +++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/dotnet/src/Microsoft.Agents.AI.Hyperlight/Internal/InstructionBuilder.cs b/dotnet/src/Microsoft.Agents.AI.Hyperlight/Internal/InstructionBuilder.cs index a4c2a43266d..ded4835d480 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hyperlight/Internal/InstructionBuilder.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hyperlight/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.Hyperlight.Internal; @@ -42,7 +43,9 @@ public static string BuildContextInstructions(bool toolsVisibleToModel) /// /// Host-side filesystem paths are intentionally omitted from the /// description — only sandbox-visible mount paths are exposed to the - /// model. + /// model. Host-tool parameter shapes come from each tool's existing + /// JsonSchema and are documentation for the model only; they do + /// not change the execute_code input schema. /// public static string BuildExecuteCodeDescription( IReadOnlyList tools, @@ -72,6 +75,17 @@ public static string BuildExecuteCodeDescription( } sb.AppendLine(); + + // Surface the host tool's existing parameter schema so the model can see + // names, requiredness, descriptions, enums, and nested shapes. + JsonElement schema = tool.JsonSchema; + if (schema.ValueKind is JsonValueKind.Object or JsonValueKind.Array or JsonValueKind.String) + { + sb.AppendLine(" Parameters (JSON Schema):"); + sb.Append(" "); + sb.Append(schema.GetRawText()); + sb.AppendLine(); + } } } diff --git a/dotnet/tests/Microsoft.Agents.AI.Hyperlight.UnitTests/InstructionBuilderTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hyperlight.UnitTests/InstructionBuilderTests.cs index deab2b75e9b..1aff11822ec 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hyperlight.UnitTests/InstructionBuilderTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hyperlight.UnitTests/InstructionBuilderTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using System.Collections.Generic; +using System.ComponentModel; using Microsoft.Agents.AI.Hyperlight.Internal; using Microsoft.Extensions.AI; @@ -69,6 +70,42 @@ public void BuildExecuteCodeDescription_WithTools_IncludesToolNames() Assert.Contains("fetch docs", text); } + [Fact] + public void BuildExecuteCodeDescription_WithParameterizedTools_IncludesHostToolJsonSchema() + { + // Arrange — zero-parameter tools already have coverage above; this locks the + // host-tool JsonSchema gap tracked by microsoft/agent-framework#8446. + // Stick to reflection-friendly primitives so AOT/source-gen test hosts can build the schema. + 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."); + + // Act + var text = InstructionBuilder.BuildExecuteCodeDescription( + tools: [tool], + fileMounts: [], + allowedDomains: [], + hasHostInputDirectory: false); + + // Assert — model-facing description must carry the host tool parameter schema. + Assert.Contains("lookup", text); + Assert.Contains("Look up an item.", text); + Assert.Contains("Parameters (JSON Schema)", text); + Assert.Contains(tool.JsonSchema.GetRawText(), text); + Assert.Contains("query", text); + Assert.Contains("Search text", text); + Assert.Contains("limit", text); + Assert.Contains("Maximum results", text); + + // Host schemas are documentation only; they must not replace execute_code's code-only input. + Assert.DoesNotContain("\"code\"", tool.JsonSchema.GetRawText()); + } + [Fact] public void BuildExecuteCodeDescription_WithFilesystem_IncludesSandboxPathsOnly() { From 50d7266f70bf678fb1bec43779f713801ef2e6d7 Mon Sep 17 00:00:00 2001 From: leilei3167 Date: Fri, 18 Sep 2026 14:22:34 +0000 Subject: [PATCH 2/2] .NET: accept boolean JSON Schema roots for Hyperlight host tools Valid JSON Schema roots include boolean true/false; render those and stop accepting non-schema Array/String roots. Assert execute_code keeps its code-only input via HyperlightExecuteCodeFunction and the provider tool. --- .../Internal/InstructionBuilder.cs | 3 +- .../InstructionBuilderTests.cs | 71 ++++++++++++++++++- 2 files changed, 71 insertions(+), 3 deletions(-) diff --git a/dotnet/src/Microsoft.Agents.AI.Hyperlight/Internal/InstructionBuilder.cs b/dotnet/src/Microsoft.Agents.AI.Hyperlight/Internal/InstructionBuilder.cs index ded4835d480..e198987f8f9 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hyperlight/Internal/InstructionBuilder.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hyperlight/Internal/InstructionBuilder.cs @@ -78,8 +78,9 @@ public static string BuildExecuteCodeDescription( // Surface the host tool's existing parameter schema so the model can see // names, requiredness, descriptions, enums, and nested shapes. + // JSON Schema roots are objects or booleans (true/false); reject other kinds. JsonElement schema = tool.JsonSchema; - if (schema.ValueKind is JsonValueKind.Object or JsonValueKind.Array or JsonValueKind.String) + if (schema.ValueKind is JsonValueKind.Object or JsonValueKind.True or JsonValueKind.False) { sb.AppendLine(" Parameters (JSON Schema):"); sb.Append(" "); diff --git a/dotnet/tests/Microsoft.Agents.AI.Hyperlight.UnitTests/InstructionBuilderTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hyperlight.UnitTests/InstructionBuilderTests.cs index 1aff11822ec..d02dc1e622d 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hyperlight.UnitTests/InstructionBuilderTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hyperlight.UnitTests/InstructionBuilderTests.cs @@ -2,13 +2,19 @@ using System.Collections.Generic; using System.ComponentModel; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; using Microsoft.Agents.AI.Hyperlight.Internal; using Microsoft.Extensions.AI; +using Moq; namespace Microsoft.Agents.AI.Hyperlight.UnitTests; public sealed class InstructionBuilderTests { + private static readonly AIAgent s_mockAgent = new Mock().Object; + [Fact] public void BuildContextInstructions_HiddenTools_MentionsCallTool() { @@ -71,7 +77,7 @@ public void BuildExecuteCodeDescription_WithTools_IncludesToolNames() } [Fact] - public void BuildExecuteCodeDescription_WithParameterizedTools_IncludesHostToolJsonSchema() + public async Task BuildExecuteCodeDescription_WithParameterizedTools_IncludesHostToolJsonSchemaAsync() { // Arrange — zero-parameter tools already have coverage above; this locks the // host-tool JsonSchema gap tracked by microsoft/agent-framework#8446. @@ -103,7 +109,45 @@ static string Lookup( Assert.Contains("Maximum results", text); // Host schemas are documentation only; they must not replace execute_code's code-only input. - Assert.DoesNotContain("\"code\"", tool.JsonSchema.GetRawText()); + using var executeCode = new HyperlightExecuteCodeFunction(new HyperlightCodeActProviderOptions + { + Tools = [tool], + }); + Assert.Contains("\"code\"", executeCode.JsonSchema.GetRawText()); + Assert.DoesNotContain("\"query\"", executeCode.JsonSchema.GetRawText()); + Assert.DoesNotContain("\"limit\"", executeCode.JsonSchema.GetRawText()); + + using var provider = new HyperlightCodeActProvider(new HyperlightCodeActProviderOptions + { + Tools = [tool], + }); + var context = await provider.InvokingAsync( + new AIContextProvider.InvokingContext(s_mockAgent, session: null, new AIContext())); + var providerFn = Assert.IsAssignableFrom(context!.Tools!.First()); + Assert.Contains("\"code\"", providerFn.JsonSchema.GetRawText()); + Assert.DoesNotContain("\"query\"", providerFn.JsonSchema.GetRawText()); + Assert.DoesNotContain("\"limit\"", providerFn.JsonSchema.GetRawText()); + } + + [Theory] + [InlineData("true")] + [InlineData("false")] + public void BuildExecuteCodeDescription_WithBooleanJsonSchema_IncludesBooleanRoot(string schemaJson) + { + // Arrange — boolean JSON Schema roots are valid and must surface to the model. + var tool = new BooleanSchemaTool("gate", "Always-on gate.", schemaJson); + + // Act + var text = InstructionBuilder.BuildExecuteCodeDescription( + tools: [tool], + fileMounts: [], + allowedDomains: [], + hasHostInputDirectory: false); + + // Assert + Assert.Contains("gate", text); + Assert.Contains("Parameters (JSON Schema)", text); + Assert.Contains(schemaJson, text); } [Fact] @@ -142,4 +186,27 @@ public void BuildExecuteCodeDescription_WithAllowedDomains_IncludesNetworkSectio Assert.Contains("GET", text); Assert.Contains("POST", text); } + + private sealed class BooleanSchemaTool : AIFunction + { + private readonly JsonDocument _schemaDocument; + + public BooleanSchemaTool(string name, string description, string schemaJson) + { + this.Name = name; + this.Description = description; + this._schemaDocument = JsonDocument.Parse(schemaJson); + } + + public override string Name { get; } + + public override string Description { get; } + + public override JsonElement JsonSchema => this._schemaDocument.RootElement; + + protected override ValueTask InvokeCoreAsync( + AIFunctionArguments arguments, + System.Threading.CancellationToken cancellationToken) => + new((object?)null); + } }