Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -42,7 +43,9 @@ public static string BuildContextInstructions(bool toolsVisibleToModel)
/// <remarks>
/// 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
/// <c>JsonSchema</c> and are documentation for the model only; they do
/// not change the <c>execute_code</c> input schema.
/// </remarks>
public static string BuildExecuteCodeDescription(
IReadOnlyList<AIFunction> tools,
Expand Down Expand Up @@ -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();
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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<AIAgent>().Object;

[Fact]
public void BuildContextInstructions_HiddenTools_MentionsCallTool()
{
Expand Down Expand Up @@ -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<AIFunction>(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()
{
Expand Down Expand Up @@ -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<object?> InvokeCoreAsync(
AIFunctionArguments arguments,
System.Threading.CancellationToken cancellationToken) =>
new((object?)null);
}
}
Loading