From 9d57eb4078f6b62d90c8638a09c727c94720ec4d Mon Sep 17 00:00:00 2001 From: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> Date: Tue, 24 Feb 2026 13:00:12 -0800 Subject: [PATCH 1/3] Added shell classes --- .../Contents/AIContent.cs | 2 + .../Contents/ShellCallContent.cs | 60 +++++++++ .../Contents/ShellCommandOutput.cs | 41 ++++++ .../Contents/ShellResultContent.cs | 43 ++++++ .../Tools/HostedShellTool.cs | 38 ++++++ .../Tools/ShellTool.cs | 49 +++++++ .../Utilities/AIJsonUtilities.Defaults.cs | 5 + src/Shared/DiagnosticIds/DiagnosticIds.cs | 1 + .../Contents/ShellCallContentTests.cs | 115 ++++++++++++++++ .../Contents/ShellCommandOutputTests.cs | 81 +++++++++++ .../Contents/ShellResultContentTests.cs | 126 ++++++++++++++++++ .../Tools/HostedShellToolTests.cs | 38 ++++++ .../Tools/ShellToolTests.cs | 75 +++++++++++ 13 files changed, 674 insertions(+) create mode 100644 src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ShellCallContent.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ShellCommandOutput.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ShellResultContent.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedShellTool.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/ShellTool.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ShellCallContentTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ShellCommandOutputTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ShellResultContentTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedShellToolTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/ShellToolTests.cs diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs index af8b19c8d84..9cb56036440 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs @@ -32,6 +32,8 @@ namespace Microsoft.Extensions.AI; // [JsonDerivedType(typeof(McpServerToolApprovalResponseContent), typeDiscriminator: "mcpServerToolApprovalResponse")] // [JsonDerivedType(typeof(CodeInterpreterToolCallContent), typeDiscriminator: "codeInterpreterToolCall")] // [JsonDerivedType(typeof(CodeInterpreterToolResultContent), typeDiscriminator: "codeInterpreterToolResult")] +// [JsonDerivedType(typeof(ShellCallContent), typeDiscriminator: "shellCall")] +// [JsonDerivedType(typeof(ShellResultContent), typeDiscriminator: "shellResult")] public class AIContent { diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ShellCallContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ShellCallContent.cs new file mode 100644 index 00000000000..134afbc7b4e --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ShellCallContent.cs @@ -0,0 +1,60 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization; +using Microsoft.Shared.DiagnosticIds; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.AI; + +/// +/// Represents a shell tool call request from a service. +/// +/// +/// +/// This content type is produced by implementations that have native shell tool support. +/// It extends with shell-specific properties such as the list of commands, +/// timeout, and output length constraints. +/// +/// +/// For implementations without native shell support, the standard +/// is used instead. +/// +/// +[Experimental(DiagnosticIds.Experiments.AIShell, UrlFormat = DiagnosticIds.UrlFormat)] +public sealed class ShellCallContent : FunctionCallContent +{ + /// + /// Initializes a new instance of the class. + /// + /// The function call ID. + /// The tool name. + /// The function arguments. + [JsonConstructor] + public ShellCallContent(string callId, string name, IDictionary? arguments = null) + : base(callId, Throw.IfNull(name), arguments) + { + } + + /// + /// Gets or sets the list of commands to execute. + /// + public IList? Commands { get; set; } + + /// + /// Gets or sets the timeout in milliseconds for the shell command execution. + /// + public int? TimeoutMs { get; set; } + + /// + /// Gets or sets the maximum output length in characters. + /// + public int? MaxOutputLength { get; set; } + + /// + /// Gets or sets the status of the shell call. + /// + public string? Status { get; set; } +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ShellCommandOutput.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ShellCommandOutput.cs new file mode 100644 index 00000000000..a746310dc20 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ShellCommandOutput.cs @@ -0,0 +1,41 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.Shared.DiagnosticIds; + +namespace Microsoft.Extensions.AI; + +/// +/// Represents the output of a single shell command execution. +/// +[Experimental(DiagnosticIds.Experiments.AIShell, UrlFormat = DiagnosticIds.UrlFormat)] +public class ShellCommandOutput +{ + /// + /// Initializes a new instance of the class. + /// + public ShellCommandOutput() + { + } + + /// + /// Gets or sets the standard output of the command. + /// + public string? Stdout { get; set; } + + /// + /// Gets or sets the standard error output of the command. + /// + public string? Stderr { get; set; } + + /// + /// Gets or sets the exit code of the command, or if the command timed out. + /// + public int? ExitCode { get; set; } + + /// + /// Gets or sets a value indicating whether the command execution timed out. + /// + public bool TimedOut { get; set; } +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ShellResultContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ShellResultContent.cs new file mode 100644 index 00000000000..1ce535e3900 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ShellResultContent.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization; +using Microsoft.Shared.DiagnosticIds; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.AI; + +/// +/// Represents the result of a shell tool invocation. +/// +/// +/// This content type extends with structured shell output data. +/// It can be produced by subclasses for local execution or by +/// implementations for hosted shell execution results. +/// +[Experimental(DiagnosticIds.Experiments.AIShell, UrlFormat = DiagnosticIds.UrlFormat)] +public sealed class ShellResultContent : FunctionResultContent +{ + /// + /// Initializes a new instance of the class. + /// + /// The function call ID for which this is the result. + /// The result of the shell tool invocation. + [JsonConstructor] + public ShellResultContent(string callId, object? result = null) + : base(Throw.IfNull(callId), result) + { + } + + /// + /// Gets or sets the output of the shell command execution. + /// + public IList? Output { get; set; } + + /// + /// Gets or sets the maximum output length in characters. + /// + public int? MaxOutputLength { get; set; } +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedShellTool.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedShellTool.cs new file mode 100644 index 00000000000..415bdeb15cf --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedShellTool.cs @@ -0,0 +1,38 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Shared.DiagnosticIds; + +namespace Microsoft.Extensions.AI; + +/// Represents a hosted tool that can be specified to an AI service to enable it to execute shell commands. +/// +/// This tool does not itself implement shell command execution. It is a marker that can be used to inform a service +/// that the service is allowed to execute shell commands if the service is capable of doing so. +/// +[Experimental(DiagnosticIds.Experiments.AIShell, UrlFormat = DiagnosticIds.UrlFormat)] +public class HostedShellTool : AITool +{ + /// Any additional properties associated with the tool. + private IReadOnlyDictionary? _additionalProperties; + + /// Initializes a new instance of the class. + public HostedShellTool() + { + } + + /// Initializes a new instance of the class. + /// Any additional properties associated with the tool. + public HostedShellTool(IReadOnlyDictionary? additionalProperties) + { + _additionalProperties = additionalProperties; + } + + /// + public override string Name => "shell"; + + /// + public override IReadOnlyDictionary AdditionalProperties => _additionalProperties ?? base.AdditionalProperties; +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/ShellTool.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/ShellTool.cs new file mode 100644 index 00000000000..5d7e60d2ea9 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/ShellTool.cs @@ -0,0 +1,49 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Shared.DiagnosticIds; + +namespace Microsoft.Extensions.AI; + +/// Represents a shell tool that can execute commands and be described to an AI service. +/// +/// +/// This is an abstract base class for shell tools that execute commands locally. +/// Subclasses must implement to provide the actual command execution logic. +/// +/// +/// implementations backed by a service that has its own notion of a shell tool +/// can special-case this type, translating it into usage of the service's native shell tool. +/// For implementations without such special-casing, the tool functions as +/// a standard that can be invoked via . +/// +/// +[Experimental(DiagnosticIds.Experiments.AIShell, UrlFormat = DiagnosticIds.UrlFormat)] +public abstract class ShellTool : AIFunction +{ + /// Any additional properties associated with the tool. + private IReadOnlyDictionary? _additionalProperties; + + /// Initializes a new instance of the class. + protected ShellTool() + { + } + + /// Initializes a new instance of the class. + /// Any additional properties associated with the tool. + protected ShellTool(IReadOnlyDictionary? additionalProperties) + { + _additionalProperties = additionalProperties; + } + + /// + public override string Name => "local_shell"; + + /// + public override string Description => "Executes a shell command and returns stdout, stderr, and exit code."; + + /// + public override IReadOnlyDictionary AdditionalProperties => _additionalProperties ?? base.AdditionalProperties; +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs index 0f2e4340358..22bf257e58c 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs @@ -61,6 +61,8 @@ private static JsonSerializerOptions CreateDefaultOptions() AddAIContentType(options, typeof(CodeInterpreterToolResultContent), typeDiscriminatorId: "codeInterpreterToolResult", checkBuiltIn: false); AddAIContentType(options, typeof(ImageGenerationToolCallContent), typeDiscriminatorId: "imageGenerationToolCall", checkBuiltIn: false); AddAIContentType(options, typeof(ImageGenerationToolResultContent), typeDiscriminatorId: "imageGenerationToolResult", checkBuiltIn: false); + AddAIContentType(options, typeof(ShellCallContent), typeDiscriminatorId: "shellCall", checkBuiltIn: false); + AddAIContentType(options, typeof(ShellResultContent), typeDiscriminatorId: "shellResult", checkBuiltIn: false); if (JsonSerializer.IsReflectionEnabledByDefault) { @@ -137,6 +139,9 @@ private static JsonSerializerOptions CreateDefaultOptions() [JsonSerializable(typeof(CodeInterpreterToolResultContent))] [JsonSerializable(typeof(ImageGenerationToolCallContent))] [JsonSerializable(typeof(ImageGenerationToolResultContent))] + [JsonSerializable(typeof(ShellCallContent))] + [JsonSerializable(typeof(ShellResultContent))] + [JsonSerializable(typeof(ShellCommandOutput))] [JsonSerializable(typeof(ResponseContinuationToken))] // IEmbeddingGenerator diff --git a/src/Shared/DiagnosticIds/DiagnosticIds.cs b/src/Shared/DiagnosticIds/DiagnosticIds.cs index 0b11c260d10..3d2bcddd528 100644 --- a/src/Shared/DiagnosticIds/DiagnosticIds.cs +++ b/src/Shared/DiagnosticIds/DiagnosticIds.cs @@ -56,6 +56,7 @@ internal static class Experiments internal const string AIChatReduction = AIExperiments; internal const string AIResponseContinuations = AIExperiments; internal const string AICodeInterpreter = AIExperiments; + internal const string AIShell = AIExperiments; internal const string AIRealTime = AIExperiments; // These diagnostic IDs are defined by the OpenAI package for its experimental APIs. diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ShellCallContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ShellCallContentTests.cs new file mode 100644 index 00000000000..bff9ec3152c --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ShellCallContentTests.cs @@ -0,0 +1,115 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Text.Json; +using Xunit; + +namespace Microsoft.Extensions.AI; + +public class ShellCallContentTests +{ + [Fact] + public void Constructor_PropsDefault() + { + ShellCallContent content = new("call123", "shell"); + Assert.Equal("call123", content.CallId); + Assert.Equal("shell", content.Name); + Assert.Null(content.Arguments); + Assert.Null(content.Commands); + Assert.Null(content.TimeoutMs); + Assert.Null(content.MaxOutputLength); + Assert.Null(content.Status); + } + + [Fact] + public void Constructor_WithArguments() + { + var args = new Dictionary { ["command"] = "ls" }; + ShellCallContent content = new("call123", "local_shell", args); + + Assert.Equal("call123", content.CallId); + Assert.Equal("local_shell", content.Name); + Assert.Same(args, content.Arguments); + } + + [Fact] + public void Properties_Roundtrip() + { + ShellCallContent content = new("call123", "shell"); + + Assert.Null(content.Commands); + IList commands = ["ls -la", "pwd"]; + content.Commands = commands; + Assert.Same(commands, content.Commands); + + Assert.Null(content.TimeoutMs); + content.TimeoutMs = 120000; + Assert.Equal(120000, content.TimeoutMs); + + Assert.Null(content.MaxOutputLength); + content.MaxOutputLength = 4096; + Assert.Equal(4096, content.MaxOutputLength); + + Assert.Null(content.Status); + content.Status = "completed"; + Assert.Equal("completed", content.Status); + + Assert.Null(content.RawRepresentation); + object raw = new(); + content.RawRepresentation = raw; + Assert.Same(raw, content.RawRepresentation); + + Assert.Null(content.AdditionalProperties); + AdditionalPropertiesDictionary props = new() { { "key", "value" } }; + content.AdditionalProperties = props; + Assert.Same(props, content.AdditionalProperties); + } + + [Fact] + public void Serialization_Roundtrips() + { + ShellCallContent content = new("call123", "shell") + { + Commands = ["ls -la", "pwd"], + TimeoutMs = 60000, + MaxOutputLength = 4096, + Status = "completed", + }; + + var json = JsonSerializer.Serialize(content, AIJsonUtilities.DefaultOptions); + var deserializedSut = JsonSerializer.Deserialize(json, AIJsonUtilities.DefaultOptions); + + Assert.NotNull(deserializedSut); + Assert.Equal("call123", deserializedSut.CallId); + Assert.Equal("shell", deserializedSut.Name); + Assert.NotNull(deserializedSut.Commands); + Assert.Equal(2, deserializedSut.Commands.Count); + Assert.Equal("ls -la", deserializedSut.Commands[0]); + Assert.Equal("pwd", deserializedSut.Commands[1]); + Assert.Equal(60000, deserializedSut.TimeoutMs); + Assert.Equal(4096, deserializedSut.MaxOutputLength); + Assert.Equal("completed", deserializedSut.Status); + } + + [Fact] + public void Serialization_PolymorphicAsAIContent_Roundtrips() + { + AIContent content = new ShellCallContent("call123", "shell") + { + Commands = ["echo hello"], + }; + + var json = JsonSerializer.Serialize(content, AIJsonUtilities.DefaultOptions); + Assert.Contains("\"$type\"", json); + Assert.Contains("\"shellCall\"", json); + + var deserializedSut = JsonSerializer.Deserialize(json, AIJsonUtilities.DefaultOptions); + var shellCall = Assert.IsType(deserializedSut); + Assert.Equal("call123", shellCall.CallId); + Assert.Equal("shell", shellCall.Name); + Assert.NotNull(shellCall.Commands); + Assert.Single(shellCall.Commands); + Assert.Equal("echo hello", shellCall.Commands[0]); + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ShellCommandOutputTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ShellCommandOutputTests.cs new file mode 100644 index 00000000000..4c5bd0b356e --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ShellCommandOutputTests.cs @@ -0,0 +1,81 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json; +using Xunit; + +namespace Microsoft.Extensions.AI; + +public class ShellCommandOutputTests +{ + [Fact] + public void Constructor_PropsDefault() + { + ShellCommandOutput output = new(); + Assert.Null(output.Stdout); + Assert.Null(output.Stderr); + Assert.Null(output.ExitCode); + Assert.False(output.TimedOut); + } + + [Fact] + public void Properties_Roundtrip() + { + ShellCommandOutput output = new(); + + Assert.Null(output.Stdout); + output.Stdout = "hello"; + Assert.Equal("hello", output.Stdout); + + Assert.Null(output.Stderr); + output.Stderr = "error message"; + Assert.Equal("error message", output.Stderr); + + Assert.Null(output.ExitCode); + output.ExitCode = 42; + Assert.Equal(42, output.ExitCode); + + Assert.False(output.TimedOut); + output.TimedOut = true; + Assert.True(output.TimedOut); + } + + [Fact] + public void Serialization_Roundtrips() + { + ShellCommandOutput output = new() + { + Stdout = "hello world", + Stderr = "warning: something", + ExitCode = 0, + TimedOut = false, + }; + + var json = JsonSerializer.Serialize(output, AIJsonUtilities.DefaultOptions); + var deserializedSut = JsonSerializer.Deserialize(json, AIJsonUtilities.DefaultOptions); + + Assert.NotNull(deserializedSut); + Assert.Equal("hello world", deserializedSut.Stdout); + Assert.Equal("warning: something", deserializedSut.Stderr); + Assert.Equal(0, deserializedSut.ExitCode); + Assert.False(deserializedSut.TimedOut); + } + + [Fact] + public void Serialization_TimedOut_Roundtrips() + { + ShellCommandOutput output = new() + { + Stdout = "partial output", + TimedOut = true, + }; + + var json = JsonSerializer.Serialize(output, AIJsonUtilities.DefaultOptions); + var deserializedSut = JsonSerializer.Deserialize(json, AIJsonUtilities.DefaultOptions); + + Assert.NotNull(deserializedSut); + Assert.Equal("partial output", deserializedSut.Stdout); + Assert.Null(deserializedSut.ExitCode); + Assert.True(deserializedSut.TimedOut); + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ShellResultContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ShellResultContentTests.cs new file mode 100644 index 00000000000..f52d9e5d571 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ShellResultContentTests.cs @@ -0,0 +1,126 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Text.Json; +using Xunit; + +namespace Microsoft.Extensions.AI; + +public class ShellResultContentTests +{ + [Fact] + public void Constructor_PropsDefault() + { + ShellResultContent content = new("call123"); + Assert.Equal("call123", content.CallId); + Assert.Null(content.Result); + Assert.Null(content.Output); + Assert.Null(content.MaxOutputLength); + } + + [Fact] + public void Constructor_WithResult() + { + ShellResultContent content = new("call123", "some result"); + Assert.Equal("call123", content.CallId); + Assert.Equal("some result", content.Result); + } + + [Fact] + public void Properties_Roundtrip() + { + ShellResultContent content = new("call123"); + + Assert.Null(content.Output); + IList output = + [ + new ShellCommandOutput { Stdout = "hello", ExitCode = 0 } + ]; + content.Output = output; + Assert.Same(output, content.Output); + + Assert.Null(content.MaxOutputLength); + content.MaxOutputLength = 4096; + Assert.Equal(4096, content.MaxOutputLength); + + Assert.Null(content.RawRepresentation); + object raw = new(); + content.RawRepresentation = raw; + Assert.Same(raw, content.RawRepresentation); + + Assert.Null(content.AdditionalProperties); + AdditionalPropertiesDictionary props = new() { { "key", "value" } }; + content.AdditionalProperties = props; + Assert.Same(props, content.AdditionalProperties); + } + + [Fact] + public void Output_SupportsMultipleItems() + { + ShellResultContent content = new("call123") + { + Output = + [ + new ShellCommandOutput { Stdout = "output1", ExitCode = 0 }, + new ShellCommandOutput { Stderr = "error", ExitCode = 1 }, + new ShellCommandOutput { TimedOut = true }, + ] + }; + + Assert.NotNull(content.Output); + Assert.Equal(3, content.Output.Count); + Assert.Equal("output1", content.Output[0].Stdout); + Assert.Equal(0, content.Output[0].ExitCode); + Assert.Equal("error", content.Output[1].Stderr); + Assert.Equal(1, content.Output[1].ExitCode); + Assert.True(content.Output[2].TimedOut); + } + + [Fact] + public void Serialization_Roundtrips() + { + ShellResultContent content = new("call123") + { + Output = + [ + new ShellCommandOutput { Stdout = "hello\n", ExitCode = 0 }, + ], + MaxOutputLength = 4096, + }; + + var json = JsonSerializer.Serialize(content, AIJsonUtilities.DefaultOptions); + var deserializedSut = JsonSerializer.Deserialize(json, AIJsonUtilities.DefaultOptions); + + Assert.NotNull(deserializedSut); + Assert.Equal("call123", deserializedSut.CallId); + Assert.NotNull(deserializedSut.Output); + Assert.Single(deserializedSut.Output); + Assert.Equal("hello\n", deserializedSut.Output[0].Stdout); + Assert.Equal(0, deserializedSut.Output[0].ExitCode); + Assert.Equal(4096, deserializedSut.MaxOutputLength); + } + + [Fact] + public void Serialization_PolymorphicAsAIContent_Roundtrips() + { + AIContent content = new ShellResultContent("call123") + { + Output = + [ + new ShellCommandOutput { Stdout = "done", ExitCode = 0 }, + ], + }; + + var json = JsonSerializer.Serialize(content, AIJsonUtilities.DefaultOptions); + Assert.Contains("\"$type\"", json); + Assert.Contains("\"shellResult\"", json); + + var deserializedSut = JsonSerializer.Deserialize(json, AIJsonUtilities.DefaultOptions); + var shellResult = Assert.IsType(deserializedSut); + Assert.Equal("call123", shellResult.CallId); + Assert.NotNull(shellResult.Output); + Assert.Single(shellResult.Output); + Assert.Equal("done", shellResult.Output[0].Stdout); + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedShellToolTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedShellToolTests.cs new file mode 100644 index 00000000000..a6815fcb08e --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedShellToolTests.cs @@ -0,0 +1,38 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using Xunit; + +namespace Microsoft.Extensions.AI; + +public class HostedShellToolTests +{ + [Fact] + public void Constructor_Roundtrips() + { + var tool = new HostedShellTool(); + Assert.Equal("shell", tool.Name); + Assert.Empty(tool.Description); + Assert.Empty(tool.AdditionalProperties); + Assert.Equal(tool.Name, tool.ToString()); + } + + [Fact] + public void Constructor_AdditionalProperties_Roundtrips() + { + var props = new Dictionary { ["key"] = "value" }; + var tool = new HostedShellTool(props); + + Assert.Equal("shell", tool.Name); + Assert.Same(props, tool.AdditionalProperties); + } + + [Fact] + public void Constructor_NullAdditionalProperties_UsesEmpty() + { + var tool = new HostedShellTool(null); + + Assert.Empty(tool.AdditionalProperties); + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/ShellToolTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/ShellToolTests.cs new file mode 100644 index 00000000000..f6aefc80d06 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/ShellToolTests.cs @@ -0,0 +1,75 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.Extensions.AI; + +public class ShellToolTests +{ + [Fact] + public void Constructor_Roundtrips() + { + var tool = new TestShellTool(); + Assert.Equal("local_shell", tool.Name); + Assert.Equal("Executes a shell command and returns stdout, stderr, and exit code.", tool.Description); + Assert.Empty(tool.AdditionalProperties); + Assert.Equal(tool.Name, tool.ToString()); + } + + [Fact] + public void Constructor_AdditionalProperties_Roundtrips() + { + var props = new Dictionary { ["key"] = "value" }; + var tool = new TestShellTool(props); + + Assert.Equal("local_shell", tool.Name); + Assert.Same(props, tool.AdditionalProperties); + } + + [Fact] + public async Task InvokeCoreAsync_ReturnsShellResultContent() + { + var tool = new TestShellTool(); + var arguments = new AIFunctionArguments { ["command"] = "echo hello" }; + + var result = await tool.InvokeAsync(arguments); + + var shellResult = Assert.IsType(result); + Assert.Equal("test-call-id", shellResult.CallId); + Assert.NotNull(shellResult.Output); + Assert.Single(shellResult.Output); + Assert.Equal("hello\n", shellResult.Output[0].Stdout); + Assert.Equal(0, shellResult.Output[0].ExitCode); + } + + private sealed class TestShellTool : ShellTool + { + public TestShellTool() + { + } + + public TestShellTool(IReadOnlyDictionary? additionalProperties) + : base(additionalProperties) + { + } + + protected override ValueTask InvokeCoreAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) + { + return new ValueTask(new ShellResultContent("test-call-id") + { + Output = + [ + new ShellCommandOutput + { + Stdout = "hello\n", + ExitCode = 0, + } + ] + }); + } + } +} From 78e7230a1a351fa6e07e08bc49a606bda85807b0 Mon Sep 17 00:00:00 2001 From: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> Date: Thu, 5 Mar 2026 18:32:04 -0800 Subject: [PATCH 2/3] Addressed PR comments --- .../Contents/ShellCallContent.cs | 29 +++---- .../Contents/ShellCommandOutput.cs | 8 +- .../Contents/ShellResultContent.cs | 19 ++--- .../Contents/ToolCallContent.cs | 1 + .../Contents/ToolResultContent.cs | 1 + .../Tools/ShellTool.cs | 49 ------------ .../Utilities/AIJsonUtilities.Defaults.cs | 2 + .../Contents/ShellCallContentTests.cs | 36 +++------ .../Contents/ShellResultContentTests.cs | 9 --- .../Tools/ShellToolTests.cs | 75 ------------------- 10 files changed, 30 insertions(+), 199 deletions(-) delete mode 100644 src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/ShellTool.cs delete mode 100644 test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/ShellToolTests.cs diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ShellCallContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ShellCallContent.cs index 134afbc7b4e..60587150880 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ShellCallContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ShellCallContent.cs @@ -1,40 +1,29 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Text.Json.Serialization; using Microsoft.Shared.DiagnosticIds; -using Microsoft.Shared.Diagnostics; namespace Microsoft.Extensions.AI; /// -/// Represents a shell tool call request from a service. +/// Represents a shell tool call invocation by a hosted service. /// /// -/// /// This content type is produced by implementations that have native shell tool support. -/// It extends with shell-specific properties such as the list of commands, -/// timeout, and output length constraints. -/// -/// -/// For implementations without native shell support, the standard -/// is used instead. -/// +/// It is informational only and represents the call itself, not the result. /// [Experimental(DiagnosticIds.Experiments.AIShell, UrlFormat = DiagnosticIds.UrlFormat)] -public sealed class ShellCallContent : FunctionCallContent +public sealed class ShellCallContent : ToolCallContent { /// /// Initializes a new instance of the class. /// - /// The function call ID. - /// The tool name. - /// The function arguments. - [JsonConstructor] - public ShellCallContent(string callId, string name, IDictionary? arguments = null) - : base(callId, Throw.IfNull(name), arguments) + /// The tool call ID. + public ShellCallContent(string callId) + : base(callId) { } @@ -44,9 +33,9 @@ public ShellCallContent(string callId, string name, IDictionary public IList? Commands { get; set; } /// - /// Gets or sets the timeout in milliseconds for the shell command execution. + /// Gets or sets the timeout for the shell command execution. /// - public int? TimeoutMs { get; set; } + public TimeSpan? Timeout { get; set; } /// /// Gets or sets the maximum output length in characters. diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ShellCommandOutput.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ShellCommandOutput.cs index a746310dc20..5bab9cd1c1e 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ShellCommandOutput.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ShellCommandOutput.cs @@ -10,14 +10,8 @@ namespace Microsoft.Extensions.AI; /// Represents the output of a single shell command execution. /// [Experimental(DiagnosticIds.Experiments.AIShell, UrlFormat = DiagnosticIds.UrlFormat)] -public class ShellCommandOutput +public class ShellCommandOutput : AIContent { - /// - /// Initializes a new instance of the class. - /// - public ShellCommandOutput() - { - } /// /// Gets or sets the standard output of the command. diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ShellResultContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ShellResultContent.cs index 1ce535e3900..00280a87b1f 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ShellResultContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ShellResultContent.cs @@ -3,31 +3,22 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Text.Json.Serialization; using Microsoft.Shared.DiagnosticIds; -using Microsoft.Shared.Diagnostics; namespace Microsoft.Extensions.AI; /// -/// Represents the result of a shell tool invocation. +/// Represents the result of a shell tool invocation by a hosted service. /// -/// -/// This content type extends with structured shell output data. -/// It can be produced by subclasses for local execution or by -/// implementations for hosted shell execution results. -/// [Experimental(DiagnosticIds.Experiments.AIShell, UrlFormat = DiagnosticIds.UrlFormat)] -public sealed class ShellResultContent : FunctionResultContent +public sealed class ShellResultContent : ToolResultContent { /// /// Initializes a new instance of the class. /// - /// The function call ID for which this is the result. - /// The result of the shell tool invocation. - [JsonConstructor] - public ShellResultContent(string callId, object? result = null) - : base(Throw.IfNull(callId), result) + /// The tool call ID. + public ShellResultContent(string callId) + : base(callId) { } diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ToolCallContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ToolCallContent.cs index ecdba681b0d..7b2e8039924 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ToolCallContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ToolCallContent.cs @@ -22,6 +22,7 @@ namespace Microsoft.Extensions.AI; // [JsonDerivedType(typeof(CodeInterpreterToolCallContent), "codeInterpreterToolCall")] // [JsonDerivedType(typeof(ImageGenerationToolCallContent), "imageGenerationToolCall")] // [JsonDerivedType(typeof(WebSearchToolCallContent), "webSearchToolCall")] +// [JsonDerivedType(typeof(ShellCallContent), "shellCall")] public class ToolCallContent : AIContent { /// diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ToolResultContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ToolResultContent.cs index 63a99476bb7..aeeb21fa189 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ToolResultContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ToolResultContent.cs @@ -22,6 +22,7 @@ namespace Microsoft.Extensions.AI; // [JsonDerivedType(typeof(CodeInterpreterToolResultContent), "codeInterpreterToolResult")] // [JsonDerivedType(typeof(ImageGenerationToolResultContent), "imageGenerationToolResult")] // [JsonDerivedType(typeof(WebSearchToolResultContent), "webSearchToolResult")] +// [JsonDerivedType(typeof(ShellResultContent), "shellResult")] public class ToolResultContent : AIContent { /// diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/ShellTool.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/ShellTool.cs deleted file mode 100644 index 5d7e60d2ea9..00000000000 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/ShellTool.cs +++ /dev/null @@ -1,49 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using Microsoft.Shared.DiagnosticIds; - -namespace Microsoft.Extensions.AI; - -/// Represents a shell tool that can execute commands and be described to an AI service. -/// -/// -/// This is an abstract base class for shell tools that execute commands locally. -/// Subclasses must implement to provide the actual command execution logic. -/// -/// -/// implementations backed by a service that has its own notion of a shell tool -/// can special-case this type, translating it into usage of the service's native shell tool. -/// For implementations without such special-casing, the tool functions as -/// a standard that can be invoked via . -/// -/// -[Experimental(DiagnosticIds.Experiments.AIShell, UrlFormat = DiagnosticIds.UrlFormat)] -public abstract class ShellTool : AIFunction -{ - /// Any additional properties associated with the tool. - private IReadOnlyDictionary? _additionalProperties; - - /// Initializes a new instance of the class. - protected ShellTool() - { - } - - /// Initializes a new instance of the class. - /// Any additional properties associated with the tool. - protected ShellTool(IReadOnlyDictionary? additionalProperties) - { - _additionalProperties = additionalProperties; - } - - /// - public override string Name => "local_shell"; - - /// - public override string Description => "Executes a shell command and returns stdout, stderr, and exit code."; - - /// - public override IReadOnlyDictionary AdditionalProperties => _additionalProperties ?? base.AdditionalProperties; -} diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs index d8c12d87d30..69dee1059dc 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs @@ -64,9 +64,11 @@ private static JsonSerializerOptions CreateDefaultOptions() AddAIContentType(options, typeof(ToolCallContent), typeof(CodeInterpreterToolCallContent), "codeInterpreterToolCall", checkBuiltIn: false); AddAIContentType(options, typeof(ToolCallContent), typeof(ImageGenerationToolCallContent), "imageGenerationToolCall", checkBuiltIn: false); AddAIContentType(options, typeof(ToolCallContent), typeof(WebSearchToolCallContent), "webSearchToolCall", checkBuiltIn: false); + AddAIContentType(options, typeof(ToolCallContent), typeof(ShellCallContent), "shellCall", checkBuiltIn: false); AddAIContentType(options, typeof(ToolResultContent), typeof(CodeInterpreterToolResultContent), "codeInterpreterToolResult", checkBuiltIn: false); AddAIContentType(options, typeof(ToolResultContent), typeof(ImageGenerationToolResultContent), "imageGenerationToolResult", checkBuiltIn: false); AddAIContentType(options, typeof(ToolResultContent), typeof(WebSearchToolResultContent), "webSearchToolResult", checkBuiltIn: false); + AddAIContentType(options, typeof(ToolResultContent), typeof(ShellResultContent), "shellResult", checkBuiltIn: false); if (JsonSerializer.IsReflectionEnabledByDefault) { diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ShellCallContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ShellCallContentTests.cs index bff9ec3152c..c79d99a6501 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ShellCallContentTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ShellCallContentTests.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; using System.Collections.Generic; using System.Text.Json; using Xunit; @@ -12,40 +13,27 @@ public class ShellCallContentTests [Fact] public void Constructor_PropsDefault() { - ShellCallContent content = new("call123", "shell"); + ShellCallContent content = new("call123"); Assert.Equal("call123", content.CallId); - Assert.Equal("shell", content.Name); - Assert.Null(content.Arguments); Assert.Null(content.Commands); - Assert.Null(content.TimeoutMs); + Assert.Null(content.Timeout); Assert.Null(content.MaxOutputLength); Assert.Null(content.Status); } - [Fact] - public void Constructor_WithArguments() - { - var args = new Dictionary { ["command"] = "ls" }; - ShellCallContent content = new("call123", "local_shell", args); - - Assert.Equal("call123", content.CallId); - Assert.Equal("local_shell", content.Name); - Assert.Same(args, content.Arguments); - } - [Fact] public void Properties_Roundtrip() { - ShellCallContent content = new("call123", "shell"); + ShellCallContent content = new("call123"); Assert.Null(content.Commands); IList commands = ["ls -la", "pwd"]; content.Commands = commands; Assert.Same(commands, content.Commands); - Assert.Null(content.TimeoutMs); - content.TimeoutMs = 120000; - Assert.Equal(120000, content.TimeoutMs); + Assert.Null(content.Timeout); + content.Timeout = TimeSpan.FromMinutes(2); + Assert.Equal(TimeSpan.FromMinutes(2), content.Timeout); Assert.Null(content.MaxOutputLength); content.MaxOutputLength = 4096; @@ -69,10 +57,10 @@ public void Properties_Roundtrip() [Fact] public void Serialization_Roundtrips() { - ShellCallContent content = new("call123", "shell") + ShellCallContent content = new("call123") { Commands = ["ls -la", "pwd"], - TimeoutMs = 60000, + Timeout = TimeSpan.FromSeconds(60), MaxOutputLength = 4096, Status = "completed", }; @@ -82,12 +70,11 @@ public void Serialization_Roundtrips() Assert.NotNull(deserializedSut); Assert.Equal("call123", deserializedSut.CallId); - Assert.Equal("shell", deserializedSut.Name); Assert.NotNull(deserializedSut.Commands); Assert.Equal(2, deserializedSut.Commands.Count); Assert.Equal("ls -la", deserializedSut.Commands[0]); Assert.Equal("pwd", deserializedSut.Commands[1]); - Assert.Equal(60000, deserializedSut.TimeoutMs); + Assert.Equal(TimeSpan.FromSeconds(60), deserializedSut.Timeout); Assert.Equal(4096, deserializedSut.MaxOutputLength); Assert.Equal("completed", deserializedSut.Status); } @@ -95,7 +82,7 @@ public void Serialization_Roundtrips() [Fact] public void Serialization_PolymorphicAsAIContent_Roundtrips() { - AIContent content = new ShellCallContent("call123", "shell") + AIContent content = new ShellCallContent("call123") { Commands = ["echo hello"], }; @@ -107,7 +94,6 @@ public void Serialization_PolymorphicAsAIContent_Roundtrips() var deserializedSut = JsonSerializer.Deserialize(json, AIJsonUtilities.DefaultOptions); var shellCall = Assert.IsType(deserializedSut); Assert.Equal("call123", shellCall.CallId); - Assert.Equal("shell", shellCall.Name); Assert.NotNull(shellCall.Commands); Assert.Single(shellCall.Commands); Assert.Equal("echo hello", shellCall.Commands[0]); diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ShellResultContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ShellResultContentTests.cs index f52d9e5d571..7fd43c95442 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ShellResultContentTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ShellResultContentTests.cs @@ -14,19 +14,10 @@ public void Constructor_PropsDefault() { ShellResultContent content = new("call123"); Assert.Equal("call123", content.CallId); - Assert.Null(content.Result); Assert.Null(content.Output); Assert.Null(content.MaxOutputLength); } - [Fact] - public void Constructor_WithResult() - { - ShellResultContent content = new("call123", "some result"); - Assert.Equal("call123", content.CallId); - Assert.Equal("some result", content.Result); - } - [Fact] public void Properties_Roundtrip() { diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/ShellToolTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/ShellToolTests.cs deleted file mode 100644 index f6aefc80d06..00000000000 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/ShellToolTests.cs +++ /dev/null @@ -1,75 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Xunit; - -namespace Microsoft.Extensions.AI; - -public class ShellToolTests -{ - [Fact] - public void Constructor_Roundtrips() - { - var tool = new TestShellTool(); - Assert.Equal("local_shell", tool.Name); - Assert.Equal("Executes a shell command and returns stdout, stderr, and exit code.", tool.Description); - Assert.Empty(tool.AdditionalProperties); - Assert.Equal(tool.Name, tool.ToString()); - } - - [Fact] - public void Constructor_AdditionalProperties_Roundtrips() - { - var props = new Dictionary { ["key"] = "value" }; - var tool = new TestShellTool(props); - - Assert.Equal("local_shell", tool.Name); - Assert.Same(props, tool.AdditionalProperties); - } - - [Fact] - public async Task InvokeCoreAsync_ReturnsShellResultContent() - { - var tool = new TestShellTool(); - var arguments = new AIFunctionArguments { ["command"] = "echo hello" }; - - var result = await tool.InvokeAsync(arguments); - - var shellResult = Assert.IsType(result); - Assert.Equal("test-call-id", shellResult.CallId); - Assert.NotNull(shellResult.Output); - Assert.Single(shellResult.Output); - Assert.Equal("hello\n", shellResult.Output[0].Stdout); - Assert.Equal(0, shellResult.Output[0].ExitCode); - } - - private sealed class TestShellTool : ShellTool - { - public TestShellTool() - { - } - - public TestShellTool(IReadOnlyDictionary? additionalProperties) - : base(additionalProperties) - { - } - - protected override ValueTask InvokeCoreAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) - { - return new ValueTask(new ShellResultContent("test-call-id") - { - Output = - [ - new ShellCommandOutput - { - Stdout = "hello\n", - ExitCode = 0, - } - ] - }); - } - } -} From 7783106c07c3b90d50ff8b056b0ecd7bd3b7ab03 Mon Sep 17 00:00:00 2001 From: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> Date: Thu, 5 Mar 2026 19:35:28 -0800 Subject: [PATCH 3/3] Fixed warnings --- .../Contents/ShellCommandOutput.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ShellCommandOutput.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ShellCommandOutput.cs index 5bab9cd1c1e..dffcf8bcd9c 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ShellCommandOutput.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ShellCommandOutput.cs @@ -12,7 +12,6 @@ namespace Microsoft.Extensions.AI; [Experimental(DiagnosticIds.Experiments.AIShell, UrlFormat = DiagnosticIds.UrlFormat)] public class ShellCommandOutput : AIContent { - /// /// Gets or sets the standard output of the command. ///