.NET: Add shell support to the HarnessAgent (#6005)

* Add shell support to the HarnessAgent

* Address PR comments

* Address PR comments
This commit is contained in:
westey
2026-05-21 18:25:33 +01:00
committed by GitHub
Unverified
parent 46ed66cfd5
commit bda40ba0e1
9 changed files with 208 additions and 8 deletions
@@ -6,6 +6,9 @@ using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using Microsoft.Agents.AI.Compaction;
#if NET
using Microsoft.Agents.AI.Tools.Shell;
#endif
using Microsoft.Extensions.AI;
using Microsoft.Shared.DiagnosticIds;
using Microsoft.Shared.Diagnostics;
@@ -201,6 +204,14 @@ public sealed class HarnessAgent : DelegatingAIAgent
result.Tools.Add(new HostedWebSearchTool());
}
#if NET
if (options?.ShellExecutor is ShellExecutor shellExecutor)
{
result.Tools ??= [];
result.Tools.Add(shellExecutor.AsAIFunction());
}
#endif
return result;
}
@@ -259,6 +270,13 @@ public sealed class HarnessAgent : DelegatingAIAgent
}
}
#if NET
if (options?.ShellExecutor is ShellExecutor shellExecutor)
{
providers.Add(new ShellEnvironmentProvider(shellExecutor, options.ShellEnvironmentProviderOptions));
}
#endif
if (options?.AIContextProviders is IEnumerable<AIContextProvider> userProviders)
{
providers.AddRange(userProviders);
@@ -2,6 +2,9 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
#if NET
using Microsoft.Agents.AI.Tools.Shell;
#endif
using Microsoft.Extensions.AI;
using Microsoft.Shared.DiagnosticIds;
@@ -240,4 +243,27 @@ public sealed class HarnessAgentOptions
/// This property is ignored when <see cref="BackgroundAgents"/> is <see langword="null"/> or empty.
/// </remarks>
public BackgroundAgentsProviderOptions? BackgroundAgentsProviderOptions { get; set; }
#if NET
/// <summary>
/// Gets or sets the shell executor used to enable shell tool and environment probing via <see cref="ShellEnvironmentProvider"/>.
/// </summary>
/// <remarks>
/// When non-null, a <see cref="ShellEnvironmentProvider"/> is automatically included in the agent's context
/// providers (injecting OS/shell/CWD information into the system prompt), and the executor's
/// <see cref="ShellExecutor.AsAIFunction"/> is registered as a callable tool.
/// When <see langword="null"/> (the default), no shell features are enabled.
/// </remarks>
public ShellExecutor? ShellExecutor { get; set; }
/// <summary>
/// Gets or sets optional configuration for the <see cref="ShellEnvironmentProvider"/>.
/// </summary>
/// <remarks>
/// Use this to customize which tools are probed, the probe timeout, shell family override,
/// or the instructions formatter.
/// This property is ignored when <see cref="ShellExecutor"/> is <see langword="null"/>.
/// </remarks>
public ShellEnvironmentProviderOptions? ShellEnvironmentProviderOptions { get; set; }
#endif
}
@@ -15,6 +15,10 @@
<ProjectReference Include="..\Microsoft.Agents.AI\Microsoft.Agents.AI.csproj" />
</ItemGroup>
<ItemGroup Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net8.0'))">
<ProjectReference Include="..\Microsoft.Agents.AI.Tools.Shell\Microsoft.Agents.AI.Tools.Shell.csproj" />
</ItemGroup>
<PropertyGroup>
<!-- NuGet Package Settings -->
<Title>Microsoft Agent Framework Harness</Title>
@@ -248,7 +248,7 @@ public sealed class DockerShellExecutor : ShellExecutor
/// Build the AIFunction for this tool.
/// </summary>
/// <remarks>
/// When <paramref name="requireApproval"/> is <see langword="null"/>
/// When <paramref name="requireApproval"/> is <see langword="true"/>
/// (the default), the returned function is wrapped in
/// <see cref="ApprovalRequiredAIFunction"/>. The caller must
/// explicitly pass <see langword="false"/> to opt out of approval
@@ -259,14 +259,12 @@ public sealed class DockerShellExecutor : ShellExecutor
/// <param name="name">Function name surfaced to the model.</param>
/// <param name="description">Function description for the model.</param>
/// <param name="requireApproval">
/// <see langword="true"/> or <see langword="null"/> (the default)
/// wraps the function in <see cref="ApprovalRequiredAIFunction"/>;
/// <see langword="true"/> (the default) wraps the function in
/// <see cref="ApprovalRequiredAIFunction"/>;
/// <see langword="false"/> opts out and returns the raw function.
/// </param>
public AIFunction AsAIFunction(string name = "run_shell", string? description = null, bool? requireApproval = null)
public override AIFunction AsAIFunction(string name = "run_shell", string? description = null, bool requireApproval = true)
{
var effectiveRequireApproval = requireApproval ?? true;
description ??=
"Execute a single shell command inside an isolated Docker container and return its " +
"stdout, stderr, and exit code. The container has no network, no host filesystem access " +
@@ -292,7 +290,7 @@ public sealed class DockerShellExecutor : ShellExecutor
},
new AIFunctionFactoryOptions { Name = name, Description = description });
return effectiveRequireApproval ? new ApprovalRequiredAIFunction(fn) : fn;
return requireApproval ? new ApprovalRequiredAIFunction(fn) : fn;
}
/// <summary>
@@ -325,7 +325,7 @@ public sealed class LocalShellExecutor : ShellExecutor
/// container where the tool itself is the boundary).
/// </param>
/// <returns>An <see cref="AIFunction"/> wrapping <see cref="RunAsync"/>.</returns>
public AIFunction AsAIFunction(string name = "run_shell", string? description = null, bool requireApproval = true)
public override AIFunction AsAIFunction(string name = "run_shell", string? description = null, bool requireApproval = true)
{
if (!requireApproval && !this._acknowledgeUnsafe)
{
@@ -3,6 +3,7 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI.Tools.Shell;
@@ -65,6 +66,20 @@ public abstract class ShellExecutor : IAsyncDisposable
/// <param name="cancellationToken">Cancellation token.</param>
public abstract Task<ShellResult> RunAsync(string command, CancellationToken cancellationToken = default);
/// <summary>
/// Build an <see cref="AIFunction"/> bound to this executor, suitable for
/// registering with an agent as a callable tool.
/// </summary>
/// <param name="name">Function name visible to the model.</param>
/// <param name="description">Function description for the model.</param>
/// <param name="requireApproval">
/// When <see langword="true"/> (the default), wraps the function in
/// <see cref="ApprovalRequiredAIFunction"/> so every invocation requires
/// explicit user approval before executing.
/// </param>
/// <returns>An <see cref="AIFunction"/> wrapping <see cref="RunAsync"/>.</returns>
public abstract AIFunction AsAIFunction(string name = "run_shell", string? description = null, bool requireApproval = true);
/// <inheritdoc />
public abstract ValueTask DisposeAsync();
}