diff --git a/dotnet/src/Microsoft.Agents.AI.Hyperlight/Internal/InstructionBuilder.cs b/dotnet/src/Microsoft.Agents.AI.Hyperlight/Internal/InstructionBuilder.cs index a4c2a43266d..e198987f8f9 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,18 @@ 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. + // JSON Schema roots are objects or booleans (true/false); reject other kinds. + JsonElement schema = tool.JsonSchema; + if (schema.ValueKind is JsonValueKind.Object or JsonValueKind.True or JsonValueKind.False) + { + 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..d02dc1e622d 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hyperlight.UnitTests/InstructionBuilderTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hyperlight.UnitTests/InstructionBuilderTests.cs @@ -1,13 +1,20 @@ // Copyright (c) Microsoft. All rights reserved. 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() { @@ -69,6 +76,80 @@ public void BuildExecuteCodeDescription_WithTools_IncludesToolNames() Assert.Contains("fetch docs", text); } + [Fact] + 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. + // 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. + 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] public void BuildExecuteCodeDescription_WithFilesystem_IncludesSandboxPathsOnly() { @@ -105,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); + } }