From 018ad37ded0fdc9fbc6abb0426ed7946bd336368 Mon Sep 17 00:00:00 2001
From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com>
Date: Fri, 25 Sep 2026 12:42:28 +0100
Subject: [PATCH 01/13] feat(dotnet): add GA computer tool factory
- Parameterless FoundryAITool.CreateComputerTool() emits the GA
{"type":"computer"} tool; the (env, w, h) overload stays preview
- OpenAI .NET 2.13/2.14 has no GA factory, so the tool is read from
its wire JSON via ModelReaderWriter as a stopgap
- Both overloads use OPENAICUA001 through a shared DiagnosticIds const
---
.../FoundryAITool.cs | 53 +++++++++++++-
.../Shared/DiagnosticIds/DiagnosticsIds.cs | 1 +
.../FoundryAIToolTests.cs | 73 +++++++++++++++++++
3 files changed, 124 insertions(+), 3 deletions(-)
diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry/FoundryAITool.cs b/dotnet/src/Microsoft.Agents.AI.Foundry/FoundryAITool.cs
index 9cffcc6fdee..54b0f46f4c3 100644
--- a/dotnet/src/Microsoft.Agents.AI.Foundry/FoundryAITool.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Foundry/FoundryAITool.cs
@@ -1,12 +1,15 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
+using System.ClientModel.Primitives;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Azure.AI.Extensions.OpenAI;
using Azure.AI.Projects.Agents;
using Microsoft.Extensions.AI;
+using Microsoft.Shared.DiagnosticIds;
using OpenAI.Responses;
+using OpenAIContext = OpenAI.OpenAIContext;
#pragma warning disable OPENAI001
@@ -29,6 +32,8 @@ namespace Microsoft.Agents.AI.Foundry;
///
public static class FoundryAITool
{
+ private static readonly BinaryData s_gaComputerToolJson = BinaryData.FromString("{\"type\":\"computer\"}");
+
///
/// Converts an existing into an .
///
@@ -128,13 +133,55 @@ public static AITool CreateHostedMcpToolbox(string toolboxName, string? version
// --- OpenAI SDK ResponseTool factories ---
///
- /// Creates an for computer use (screen interaction).
+ /// Creates an for the generally available (GA) OpenAI computer tool ({"type":"computer"}).
+ ///
+ ///
+ ///
+ /// The GA tool has no environment or display settings: the model infers the screen from the screenshots it receives.
+ /// Each computer_call it returns carries an ordered actions batch that the caller runs in order before
+ /// sending back a single computer_call_output screenshot.
+ ///
+ ///
+ /// To use the preview computer_use_preview tool, whose computer_call items carry a single action,
+ /// use the overload instead.
+ ///
+ ///
+ /// Known limitation: the OpenAI .NET SDK models only the preview computer_call item, so sending a GA
+ /// computer_call back to the model (for example with stored responses disabled and the history kept locally)
+ /// adds "action": null and "pending_safety_checks": [], which the Responses API rejects for the GA tool.
+ /// Continuing the conversation from the stored response (the default for a Responses agent session) only sends
+ /// the new computer_call_output and is not affected.
+ ///
+ ///
+ /// An for the GA computer tool.
+ [Experimental(DiagnosticIds.Experiments.AIOpenAIComputerUse)]
+ public static AITool CreateComputerTool()
+ {
+ // OpenAI .NET (2.13.0 and 2.14.0) has no factory for the GA tool: ResponseTool.CreateComputerTool always emits
+ // computer_use_preview. Reading the GA wire shape yields an unknown-type ResponseTool that serializes back
+ // exactly as {"type":"computer"}, so it can be sent today. Switch to the typed OpenAI API once it ships.
+ ResponseTool tool = ModelReaderWriter.Read(s_gaComputerToolJson, ModelReaderWriterOptions.Json, OpenAIContext.Default)!;
+ return tool.AsAITool();
+ }
+
+ ///
+ /// Creates an for the preview OpenAI computer use tool (computer_use_preview).
///
+ ///
+ ///
+ /// The preview tool is used with the computer-use-preview model and declares the environment and display size.
+ /// Each computer_call it returns carries a single action.
+ ///
+ ///
+ /// To use the generally available (GA) computer tool, which takes no settings and returns batched actions,
+ /// use the parameterless overload instead.
+ ///
+ ///
/// The computer tool environment type.
/// The display width in pixels.
/// The display height in pixels.
- /// An for computer use.
- [Experimental("OPENAICUA001")]
+ /// An for the preview computer use tool.
+ [Experimental(DiagnosticIds.Experiments.AIOpenAIComputerUse)]
public static AITool CreateComputerTool(ComputerToolEnvironment environment, int displayWidth, int displayHeight)
=> ResponseTool.CreateComputerTool(environment, displayWidth, displayHeight).AsAITool();
diff --git a/dotnet/src/Shared/DiagnosticIds/DiagnosticsIds.cs b/dotnet/src/Shared/DiagnosticIds/DiagnosticsIds.cs
index f9e0d4b15e3..e9020aec629 100644
--- a/dotnet/src/Shared/DiagnosticIds/DiagnosticsIds.cs
+++ b/dotnet/src/Shared/DiagnosticIds/DiagnosticsIds.cs
@@ -30,6 +30,7 @@ internal static class Experiments
// We use the same IDs so consumers do not need to suppress additional diagnostics
// when using the experimental OpenAI APIs.
internal const string AIOpenAIResponses = "OPENAI001";
+ internal const string AIOpenAIComputerUse = "OPENAICUA001";
private const string MEAIExperiments = "MEAI001";
}
diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/FoundryAIToolTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/FoundryAIToolTests.cs
index 473e7cdbb21..b3c21a4665f 100644
--- a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/FoundryAIToolTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/FoundryAIToolTests.cs
@@ -1,18 +1,91 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
+using System.ClientModel;
using System.ClientModel.Primitives;
using System.Collections.Generic;
+using System.IO;
+using System.Net;
+using System.Net.Http;
+using System.Text;
+using System.Text.Json;
+using System.Threading.Tasks;
using Azure.AI.Projects.Agents;
using Microsoft.Extensions.AI;
+using OpenAI;
using OpenAI.Responses;
#pragma warning disable OPENAI001
+#pragma warning disable OPENAICUA001
namespace Microsoft.Agents.AI.Foundry.UnitTests;
public class FoundryAIToolTests
{
+ [Fact]
+ public void CreateComputerTool_Parameterless_SerializesAsGaComputerTool()
+ {
+ // Arrange & Act
+ AITool tool = FoundryAITool.CreateComputerTool();
+
+ // Assert
+ var responseTool = Assert.IsAssignableFrom(tool.GetService(typeof(ResponseTool)));
+ string json = ModelReaderWriter.Write(responseTool, ModelReaderWriterOptions.Json).ToString();
+
+ // The GA tool has no environment or display settings; anything besides the type would be a preview field.
+ Assert.Equal("{\"type\":\"computer\"}", json);
+ }
+
+ [Fact]
+ public async Task CreateComputerTool_Parameterless_IsSentAsGaComputerToolOnTheWireAsync()
+ {
+ // Arrange
+ string? requestBody = null;
+ using var handler = new HttpHandlerAssert(async request =>
+ {
+ requestBody = await request.Content!.ReadAsStringAsync();
+ return new HttpResponseMessage(HttpStatusCode.OK)
+ {
+ Content = new StringContent(
+ File.ReadAllText(Path.Combine("TestData", "OpenAIDefaultResponse.json")),
+ Encoding.UTF8,
+ "application/json"),
+ };
+ });
+#pragma warning disable CA5399
+ using var httpClient = new HttpClient(handler);
+#pragma warning restore CA5399
+ IChatClient chatClient = new OpenAIClient(
+ new ApiKeyCredential("test-key"),
+ new OpenAIClientOptions { Transport = new HttpClientPipelineTransport(httpClient) })
+ .GetResponsesClient()
+ .AsIChatClient("test-model");
+
+ // Act
+ await chatClient.GetResponseAsync("Take a screenshot.", new ChatOptions { Tools = [FoundryAITool.CreateComputerTool()] });
+
+ // Assert
+ Assert.NotNull(requestBody);
+ using JsonDocument body = JsonDocument.Parse(requestBody!);
+ JsonElement tool = Assert.Single(body.RootElement.GetProperty("tools").EnumerateArray());
+ Assert.Equal("{\"type\":\"computer\"}", tool.GetRawText());
+ }
+
+ [Fact]
+ public void CreateComputerTool_WithEnvironmentAndDisplay_SerializesAsPreviewTool()
+ {
+ // Arrange & Act
+ AITool tool = FoundryAITool.CreateComputerTool(ComputerToolEnvironment.Browser, 1024, 768);
+
+ // Assert
+ var responseTool = Assert.IsAssignableFrom(tool.GetService(typeof(ResponseTool)));
+ string json = ModelReaderWriter.Write(responseTool, ModelReaderWriterOptions.Json).ToString();
+ Assert.Contains("\"type\":\"computer_use_preview\"", json);
+ Assert.Contains("\"environment\":\"browser\"", json);
+ Assert.Contains("\"display_width\":1024", json);
+ Assert.Contains("\"display_height\":768", json);
+ }
+
[Fact]
public void CreateMcpTool_WithProjectConnectionId_SetsProjectConnectionId()
{
From e4a76fc63602eb414f4c0afe0c83a1df19cf16e0 Mon Sep 17 00:00:00 2001
From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com>
Date: Fri, 25 Sep 2026 12:42:29 +0100
Subject: [PATCH 02/13] feat(dotnet): map computer items in Foundry hosting
Hosting dropped computer_call and computer_call_output from input and
history, and never emitted the model's computer_call to the caller.
- Convert items between AgentServer and OpenAI models through JSON,
stripping AgentServer envelope fields and using builder item ids
- Rebuild AgentServer calls without pending_safety_checks, which
otherwise throw NullReferenceException on serialization (beta.8)
Known blocker, not worked around: OpenAI .NET replays a GA call with
"action": null and "pending_safety_checks": [], both rejected by the
Responses API, so hosted turns after a computer call fail.
---
.../ComputerToolItemConverter.cs | 200 +++++++++++
.../InputConverter.cs | 4 +
.../OutputConverter.cs | 61 ++++
.../ComputerToolHostingLoopTests.cs | 322 ++++++++++++++++++
.../InputConverterTests.cs | 176 +++++++++-
.../OutputConverterTests.cs | 169 +++++++++
6 files changed, 931 insertions(+), 1 deletion(-)
create mode 100644 dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/ComputerToolItemConverter.cs
create mode 100644 dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/ComputerToolHostingLoopTests.cs
diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/ComputerToolItemConverter.cs b/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/ComputerToolItemConverter.cs
new file mode 100644
index 00000000000..20c20175c03
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/ComputerToolItemConverter.cs
@@ -0,0 +1,200 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Buffers;
+using System.ClientModel.Primitives;
+using System.Collections.Generic;
+using System.Text.Json;
+using Azure.AI.AgentServer.Responses;
+using Azure.AI.AgentServer.Responses.Models;
+using Microsoft.Extensions.AI;
+using ChatMessage = Microsoft.Extensions.AI.ChatMessage;
+using OpenAIComputerCallOutputResponseItem = OpenAI.Responses.ComputerCallOutputResponseItem;
+using OpenAIComputerCallResponseItem = OpenAI.Responses.ComputerCallResponseItem;
+using OpenAIContext = OpenAI.OpenAIContext;
+using OpenAIResponseItem = OpenAI.Responses.ResponseItem;
+
+#pragma warning disable OPENAICUA001 // OpenAI .NET marks its computer use types as experimental.
+
+namespace Microsoft.Agents.AI.Foundry.Hosting;
+
+///
+/// Converts computer tool items (computer_call and computer_call_output) between the AgentServer
+/// Responses models and the OpenAI .NET models that the MEAI OpenAI Responses client sends to the model.
+///
+///
+///
+/// Both SDKs generate their models from the same OpenAI wire schema, but neither can build the other's item, and
+/// OpenAI .NET (2.13.0 and 2.14.0) has no typed GA actions batch. The item is therefore converted through its
+/// JSON: fields one SDK does not type (such as actions or the screenshot detail) travel as additional
+/// data. Replace this with typed conversions once OpenAI .NET models the GA computer items.
+///
+///
+/// The OpenAI item is carried as the of a or
+/// . The MEAI OpenAI Responses client sends any content whose raw representation is a
+/// to the model unchanged, which is what keeps the batch intact on replay.
+///
+///
+internal static class ComputerToolItemConverter
+{
+ /// Converts a computer_call sent as request input into an assistant message.
+ public static ChatMessage ToChatMessage(ItemComputerToolCall item) =>
+ ToToolCallMessage(Write(WithSafetyCheckList(item)), item.CallId);
+
+ /// Converts a computer_call loaded from AgentServer history into an assistant message.
+ public static ChatMessage ToChatMessage(OutputItemComputerToolCall item) =>
+ ToToolCallMessage(Write(WithSafetyCheckList(item)), item.CallId);
+
+ /// Converts a computer_call_output sent as request input into a tool message.
+ public static ChatMessage ToChatMessage(ComputerCallOutputItemParam item) =>
+ ToToolResultMessage(Write(item), item.CallId);
+
+ /// Converts a computer_call_output loaded from AgentServer history into a tool message.
+ public static ChatMessage ToChatMessage(OutputItemComputerToolCallOutput item) =>
+ ToToolResultMessage(Write(item), item.CallId);
+
+ /// Converts a model computer_call into the output item emitted to the caller.
+ /// The computer call returned by the model.
+ /// The id generated by the AgentServer output item builder.
+ public static OutputItemComputerToolCall ToOutputItem(OpenAIComputerCallResponseItem item, string itemId) =>
+ ReadAgentServerItem(WriteOpenAI(item), itemId, "computer_call", item.CallId);
+
+ /// Converts a computer_call_output produced by the agent into the output item emitted to the caller.
+ /// The computer call output produced by the agent.
+ /// The id generated by the AgentServer output item builder.
+ public static OutputItemComputerToolCallOutput ToOutputItem(OpenAIComputerCallOutputResponseItem item, string itemId) =>
+ ReadAgentServerItem(WriteOpenAI(item), itemId, "computer_call_output", item.CallId);
+
+ private static BinaryData WriteOpenAI(T item)
+ where T : IPersistableModel =>
+ ModelReaderWriter.Write(item, ModelReaderWriterOptions.Json, OpenAIContext.Default);
+
+ private static T ReadAgentServerItem(BinaryData openAIJson, string itemId, string itemType, string callId)
+ where T : OutputItem
+ {
+ // The builder id replaces the model's item id: AgentServer requires its own {prefix}_{50 chars} id format and
+ // does not stamp one on EmitAdded. A null "action" is how OpenAI .NET writes a GA call; leaving it out keeps
+ // the emitted item in the GA shape (actions only). A populated preview "action" is kept.
+ BinaryData agentServerJson = RewriteTopLevelProperties(
+ openAIJson,
+ static property => property.NameEquals("action"u8) && property.Value.ValueKind == JsonValueKind.Null,
+ replacementId: itemId);
+
+ return ModelReaderWriter.Read(agentServerJson, ModelReaderWriterOptions.Json, AzureAIAgentServerResponsesContext.Default) as T
+ ?? throw new InvalidOperationException(
+ $"The {itemType} item for call '{callId}' could not be converted to the AgentServer {typeof(T).Name} representation.");
+ }
+
+ private static BinaryData Write(T item)
+ where T : IPersistableModel =>
+ ModelReaderWriter.Write(item, ModelReaderWriterOptions.Json, AzureAIAgentServerResponsesContext.Default);
+
+ // The GA computer_call has no "pending_safety_checks". AgentServer (1.0.0-beta.8) then leaves PendingSafetyChecks
+ // null and its serializer throws NullReferenceException, so such an item is rebuilt with an empty list first. The
+ // rebuilt item keeps every typed field; the list has no public setter, which is why a copy is needed.
+ private static ItemComputerToolCall WithSafetyCheckList(ItemComputerToolCall item)
+ {
+ if (item.PendingSafetyChecks is not null)
+ {
+ return item;
+ }
+
+ var copy = new ItemComputerToolCall(item.Id, item.CallId, [], item.Status) { Action = item.Action };
+ CopyActions(item.Actions, copy.Actions);
+ return copy;
+ }
+
+ private static OutputItemComputerToolCall WithSafetyCheckList(OutputItemComputerToolCall item)
+ {
+ if (item.PendingSafetyChecks is not null)
+ {
+ return item;
+ }
+
+ // The AgentServer envelope (response id, agent reference) is not copied: it is dropped before replay anyway.
+ var copy = new OutputItemComputerToolCall(item.Id, item.CallId, [], item.Status) { Action = item.Action };
+ CopyActions(item.Actions, copy.Actions);
+ return copy;
+ }
+
+ private static void CopyActions(IList? source, IList target)
+ {
+ if (source is null)
+ {
+ return;
+ }
+
+ foreach (ComputerAction action in source)
+ {
+ target.Add(action);
+ }
+ }
+
+ private static ChatMessage ToToolCallMessage(BinaryData agentServerJson, string callId)
+ {
+ // Known blocker (OpenAI .NET 2.13.0 and 2.14.0): ComputerCallResponseItem models the preview item and always
+ // writes "action" and "pending_safety_checks", so a GA call replayed from here reaches the model with
+ // "action": null and "pending_safety_checks": [] next to "actions". The Responses API rejects both keys for the
+ // GA tool. Its Patch API cannot remove "action" (NullReferenceException), and hosting does not own the agent's
+ // HTTP pipeline, so the item is left as OpenAI .NET writes it until the SDK models the GA item.
+ var item = ReadOpenAIItem(agentServerJson, "computer_call", callId);
+ return new ChatMessage(ChatRole.Assistant, [new ToolCallContent(callId) { RawRepresentation = item }]);
+ }
+
+ private static ChatMessage ToToolResultMessage(BinaryData agentServerJson, string callId)
+ {
+ var item = ReadOpenAIItem(agentServerJson, "computer_call_output", callId);
+ return new ChatMessage(ChatRole.Tool, [new ToolResultContent(callId) { RawRepresentation = item }]);
+ }
+
+ private static T ReadOpenAIItem(BinaryData agentServerJson, string itemType, string callId)
+ where T : OpenAIResponseItem
+ {
+ BinaryData openAIJson = RewriteTopLevelProperties(agentServerJson, IsAgentServerEnvelopeProperty);
+
+ return ModelReaderWriter.Read(openAIJson, ModelReaderWriterOptions.Json, OpenAIContext.Default) as T
+ ?? throw new InvalidOperationException(
+ $"The {itemType} item for call '{callId}' could not be converted to the OpenAI {typeof(T).Name} representation.");
+ }
+
+ ///
+ /// AgentServer adds these fields to stored output items. They are not part of the model's item schema, and the
+ /// model API rejects unknown input parameters, so they must not be replayed.
+ ///
+ private static bool IsAgentServerEnvelopeProperty(JsonProperty property) =>
+ property.NameEquals("response_id"u8) ||
+ property.NameEquals("agent_reference"u8) ||
+ property.NameEquals("created_by"u8);
+
+ ///
+ /// Copies a JSON object, leaving out the top-level properties selected by and, when
+ /// is set, replacing its id.
+ ///
+ private static BinaryData RewriteTopLevelProperties(BinaryData json, Func skip, string? replacementId = null)
+ {
+ using JsonDocument document = JsonDocument.Parse(json);
+ var buffer = new ArrayBufferWriter();
+ using (var writer = new Utf8JsonWriter(buffer))
+ {
+ writer.WriteStartObject();
+ if (replacementId is not null)
+ {
+ writer.WriteString("id"u8, replacementId);
+ }
+
+ foreach (JsonProperty property in document.RootElement.EnumerateObject())
+ {
+ if (skip(property) || (replacementId is not null && property.NameEquals("id"u8)))
+ {
+ continue;
+ }
+
+ property.WriteTo(writer);
+ }
+
+ writer.WriteEndObject();
+ }
+
+ return new BinaryData(buffer.WrittenMemory.ToArray());
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/InputConverter.cs b/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/InputConverter.cs
index 1275ec750e2..fd6f1242960 100644
--- a/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/InputConverter.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/InputConverter.cs
@@ -231,6 +231,8 @@ static CreateResponseOptions LocalDisableStoredOutput(CreateResponseOptions resp
ItemFunctionToolCall funcCall => ConvertItemFunctionToolCall(funcCall),
ItemMcpApprovalRequest approvalRequest => ConvertMcpApprovalRequest(approvalRequest.Id, approvalRequest.Name, approvalRequest.Arguments, stateBag),
MCPApprovalResponse approvalResponse => ConvertMcpApprovalResponse(approvalResponse.ApprovalRequestId, approvalResponse.Approve, stateBag),
+ ItemComputerToolCall computerCall => ComputerToolItemConverter.ToChatMessage(computerCall),
+ ComputerCallOutputItemParam computerCallOutput => ComputerToolItemConverter.ToChatMessage(computerCallOutput),
ItemReferenceParam => null,
_ => null
};
@@ -384,6 +386,8 @@ private static ChatMessage ConvertMcpApprovalResponse(string approvalRequestId,
OutputItemFunctionToolCallOutput funcOutput => ConvertFunctionToolCallOutput(funcOutput),
OutputItemMcpApprovalRequest approvalRequest => ConvertMcpApprovalRequest(approvalRequest.Id, approvalRequest.Name, approvalRequest.Arguments, stateBag),
OutputItemMcpApprovalResponseResource approvalResponse => ConvertMcpApprovalResponse(approvalResponse.ApprovalRequestId, approvalResponse.Approve, stateBag),
+ OutputItemComputerToolCall computerCall => ComputerToolItemConverter.ToChatMessage(computerCall),
+ OutputItemComputerToolCallOutput computerCallOutput => ComputerToolItemConverter.ToChatMessage(computerCallOutput),
OutputItemReasoningItem => null,
_ => null
};
diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/OutputConverter.cs b/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/OutputConverter.cs
index 8cbb0aa2ce5..4fe6166515b 100644
--- a/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/OutputConverter.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/OutputConverter.cs
@@ -15,6 +15,8 @@
using Microsoft.Agents.AI.Workflows;
using Microsoft.Extensions.AI;
using MeaiTextContent = Microsoft.Extensions.AI.TextContent;
+using OpenAIComputerCallOutputResponseItem = OpenAI.Responses.ComputerCallOutputResponseItem;
+using OpenAIComputerCallResponseItem = OpenAI.Responses.ComputerCallResponseItem;
using OpenAIContainerFileCitationMessageAnnotation = OpenAI.Responses.ContainerFileCitationMessageAnnotation;
using OpenAIFileCitationMessageAnnotation = OpenAI.Responses.FileCitationMessageAnnotation;
using OpenAIFilePathMessageAnnotation = OpenAI.Responses.FilePathMessageAnnotation;
@@ -24,6 +26,8 @@
using OpenAIStreamingResponseOutputTextAnnotationAddedUpdate = OpenAI.Responses.StreamingResponseOutputTextAnnotationAddedUpdate;
using OpenAIStreamingResponseOutputTextDeltaUpdate = OpenAI.Responses.StreamingResponseOutputTextDeltaUpdate;
+#pragma warning disable OPENAICUA001 // OpenAI .NET marks its computer use types as experimental.
+
namespace Microsoft.Agents.AI.Foundry.Hosting;
internal abstract record OutputConverterItem
@@ -212,6 +216,63 @@ public static async IAsyncEnumerable ConvertUpdatesToItemsA
break;
}
+ // The MEAI OpenAI Responses client has no dedicated content type for computer calls: it yields a
+ // base ToolCallContent carrying the OpenAI item. Without this case the caller would never see the
+ // call it is expected to execute.
+ case ToolCallContent { RawRepresentation: OpenAIComputerCallResponseItem computerCall } computerCallContent:
+ {
+ if (computerCallContent.CallId is not { Length: > 0 })
+ {
+ break;
+ }
+
+ foreach (var evt in CloseCurrentMessage(currentMessageBuilder, currentTextBuilder, accumulatedText, accumulatedAnnotations))
+ {
+ yield return evt;
+ }
+
+ currentTextBuilder = null;
+ currentMessageBuilder = null;
+ accumulatedText = null;
+ accumulatedAnnotations = null;
+ previousMessageId = null;
+
+ // Emit a fully populated item through the generic builder. The SDK's OutputItemComputerCall
+ // convenience method takes a single action and cannot carry the GA actions batch.
+ var computerCallBuilder = stream.AddOutputItemComputerCall();
+ var computerCallItem = ComputerToolItemConverter.ToOutputItem(computerCall, computerCallBuilder.ItemId);
+ yield return computerCallBuilder.EmitAdded(computerCallItem);
+ yield return computerCallBuilder.EmitDone(computerCallItem);
+ break;
+ }
+
+ // Only an agent that runs the actions itself produces this; the usual caller-executed loop receives
+ // the output as request input instead. Emitting it keeps the AgentServer history complete.
+ case ToolResultContent { RawRepresentation: OpenAIComputerCallOutputResponseItem computerCallOutput } computerCallOutputContent:
+ {
+ if (computerCallOutputContent.CallId is not { Length: > 0 })
+ {
+ break;
+ }
+
+ foreach (var evt in CloseCurrentMessage(currentMessageBuilder, currentTextBuilder, accumulatedText, accumulatedAnnotations))
+ {
+ yield return evt;
+ }
+
+ currentTextBuilder = null;
+ currentMessageBuilder = null;
+ accumulatedText = null;
+ accumulatedAnnotations = null;
+ previousMessageId = null;
+
+ var computerCallOutputBuilder = stream.AddOutputItemComputerCallOutput();
+ var computerCallOutputItem = ComputerToolItemConverter.ToOutputItem(computerCallOutput, computerCallOutputBuilder.ItemId);
+ yield return computerCallOutputBuilder.EmitAdded(computerCallOutputItem);
+ yield return computerCallOutputBuilder.EmitDone(computerCallOutputItem);
+ break;
+ }
+
case TextReasoningContent reasoningContent:
{
foreach (var evt in CloseCurrentMessage(currentMessageBuilder, currentTextBuilder, accumulatedText, accumulatedAnnotations))
diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/ComputerToolHostingLoopTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/ComputerToolHostingLoopTests.cs
new file mode 100644
index 00000000000..7dc0d23dfcb
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/ComputerToolHostingLoopTests.cs
@@ -0,0 +1,322 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.ClientModel;
+using System.ClientModel.Primitives;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Text;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+using Azure.AI.AgentServer.Responses;
+using Azure.AI.AgentServer.Responses.Models;
+using Microsoft.Extensions.AI;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging.Abstractions;
+using Moq;
+using OpenAIClient = OpenAI.OpenAIClient;
+using OpenAIClientOptions = OpenAI.OpenAIClientOptions;
+
+#pragma warning disable OPENAI001 // Experimental Responses API surfaces
+#pragma warning disable OPENAICUA001 // Experimental OpenAI computer use surfaces
+
+namespace Microsoft.Agents.AI.Foundry.Hosting.UnitTests;
+
+///
+/// End-to-end tests for the hosted GA computer tool loop. They run the real hosting handler, a real
+/// and the real MEAI OpenAI Responses client over a fake HTTP transport, so both
+/// the items the caller receives and the exact request body sent to the model are checked.
+///
+public sealed class ComputerToolHostingLoopTests
+{
+ private const string ModelCallId = "call_ga_1";
+ private const string ModelItemId = "cu_model_item_1";
+ private const string ScreenshotUrl = "data:image/png;base64,iVBORw0KGgo=";
+
+ // Shape the service returns for the GA tool: an ordered actions batch, and neither "action" nor "pending_safety_checks".
+ private const string BatchedComputerCallJson =
+ "{\"type\":\"computer_call\",\"id\":\"" + ModelItemId + "\",\"call_id\":\"" + ModelCallId + "\"," +
+ "\"actions\":[{\"type\":\"click\",\"button\":\"left\",\"x\":405,\"y\":157},{\"type\":\"type\",\"text\":\"penguin\"}]," +
+ "\"status\":\"completed\"}";
+
+ [Fact]
+ public async Task CreateAsync_ModelReturnsBatchedComputerCall_EmitsComputerCallWithActionsAsync()
+ {
+ // Arrange
+ using var model = new FakeResponsesModel(ComputerCallTurn());
+ var handler = BuildHandler(model);
+ var request = NewRequest(UserMessageInput("Search for penguin."));
+
+ // Act
+ var events = await DrainAsync(handler.CreateAsync(request, NewContext("resp_" + new string('1', 46), request, []), CancellationToken.None));
+
+ // Assert: the caller receives the model's computer call, with the batch intact.
+ var added = Assert.Single(events.OfType().Select(e => e.Item).OfType());
+ var done = Assert.Single(events.OfType().Select(e => e.Item).OfType());
+ Assert.Equal(added.Id, done.Id);
+ Assert.StartsWith("cu_", done.Id, StringComparison.Ordinal);
+ Assert.NotEqual(ModelItemId, done.Id);
+ Assert.Equal(ModelCallId, done.CallId);
+ Assert.Equal(2, done.Actions.Count);
+
+ string doneJson = ModelReaderWriter.Write(done, ModelReaderWriterOptions.Json).ToString();
+ Assert.Contains("\"actions\":[{\"type\":\"click\"", doneJson);
+ Assert.DoesNotContain("\"action\":", doneJson);
+ Assert.IsType(events[^1]);
+
+ // Assert: the model was offered the GA tool, and nothing was stored on the model service.
+ using var sent = JsonDocument.Parse(Assert.Single(model.RequestBodies));
+ Assert.Equal("{\"type\":\"computer\"}", Assert.Single(sent.RootElement.GetProperty("tools").EnumerateArray()).GetRawText());
+ Assert.False(sent.RootElement.GetProperty("store").GetBoolean());
+ }
+
+ [Fact]
+ public async Task CreateAsync_ComputerCallOutputAfterStoredComputerCall_ReplaysBothItemsToModelAsync()
+ {
+ // Arrange: history as AgentServer returns it, including the envelope fields it adds to output items.
+ const string HistoryItemId = "cu_" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
+ var storedCall = ReadOutputItem(
+ "{\"type\":\"computer_call\",\"id\":\"" + HistoryItemId + "\",\"call_id\":\"" + ModelCallId + "\"," +
+ "\"actions\":[{\"type\":\"click\",\"button\":\"left\",\"x\":405,\"y\":157},{\"type\":\"type\",\"text\":\"penguin\"}]," +
+ "\"pending_safety_checks\":[],\"status\":\"completed\"," +
+ "\"response_id\":\"resp_previous\",\"agent_reference\":{\"type\":\"agent_reference\",\"name\":\"computer-agent\"}}");
+ IReadOnlyList history = [UserHistoryMessage("Search for penguin."), storedCall];
+
+ using var model = new FakeResponsesModel(TextTurn("Done."));
+ var handler = BuildHandler(model);
+ var request = NewRequest(ComputerCallOutputInput(ModelCallId));
+
+ // Act
+ await DrainAsync(handler.CreateAsync(request, NewContext("resp_" + new string('2', 46), request, history), CancellationToken.None));
+
+ // Assert
+ using var sent = JsonDocument.Parse(Assert.Single(model.RequestBodies));
+ var input = sent.RootElement.GetProperty("input").EnumerateArray().ToList();
+ int callIndex = input.FindIndex(i => i.GetProperty("type").GetString() == "computer_call");
+ int outputIndex = input.FindIndex(i => i.GetProperty("type").GetString() == "computer_call_output");
+ Assert.True(callIndex >= 0, "The stored computer_call was not replayed to the model.");
+ Assert.True(outputIndex > callIndex, "The computer_call_output must follow its computer_call.");
+
+ AssertReplayedComputerCall(input[callIndex], HistoryItemId);
+ AssertReplayedComputerCallOutput(input[outputIndex]);
+ }
+
+ [Fact]
+ public async Task CreateAsync_TwoTurnComputerLoop_ReplaysEmittedCallWithCallerOutputAsync()
+ {
+ // Arrange: turn 1 returns a computer call, turn 2 answers with text.
+ using var model = new FakeResponsesModel(ComputerCallTurn(), TextTurn("Done."));
+ var handler = BuildHandler(model);
+
+ var firstRequest = NewRequest(UserMessageInput("Search for penguin."));
+ var firstEvents = await DrainAsync(handler.CreateAsync(firstRequest, NewContext("resp_" + new string('3', 46), firstRequest, []), CancellationToken.None));
+ var emittedCall = Assert.Single(firstEvents.OfType().Select(e => e.Item).OfType());
+
+ // The caller runs the actions and returns a screenshot; AgentServer supplies turn 1 as history.
+ IReadOnlyList history = [UserHistoryMessage("Search for penguin."), emittedCall];
+ var secondRequest = NewRequest(ComputerCallOutputInput(emittedCall.CallId));
+
+ // Act
+ await DrainAsync(handler.CreateAsync(secondRequest, NewContext("resp_" + new string('4', 46), secondRequest, history), CancellationToken.None));
+
+ // Assert: the second model request carries the call the caller saw, then the caller's output.
+ Assert.Equal(2, model.RequestBodies.Count);
+ using var sent = JsonDocument.Parse(model.RequestBodies[1]);
+ var input = sent.RootElement.GetProperty("input").EnumerateArray().ToList();
+ int callIndex = input.FindIndex(i => i.GetProperty("type").GetString() == "computer_call");
+ int outputIndex = input.FindIndex(i => i.GetProperty("type").GetString() == "computer_call_output");
+ Assert.True(callIndex >= 0, "The emitted computer_call was not replayed to the model.");
+ Assert.True(outputIndex > callIndex, "The computer_call_output must follow its computer_call.");
+
+ AssertReplayedComputerCall(input[callIndex], emittedCall.Id);
+ AssertReplayedComputerCallOutput(input[outputIndex]);
+ }
+
+ private static void AssertReplayedComputerCall(JsonElement call, string expectedId)
+ {
+ Assert.Equal(expectedId, call.GetProperty("id").GetString());
+ Assert.Equal(ModelCallId, call.GetProperty("call_id").GetString());
+
+ var actions = call.GetProperty("actions").EnumerateArray().ToList();
+ Assert.Equal(2, actions.Count);
+ Assert.Equal("click", actions[0].GetProperty("type").GetString());
+ Assert.Equal("penguin", actions[1].GetProperty("text").GetString());
+
+ // AgentServer envelope fields are not part of the model's item schema and must not reach the model.
+ Assert.False(call.TryGetProperty("response_id", out _));
+ Assert.False(call.TryGetProperty("agent_reference", out _));
+
+ // OpenAI .NET 2.13.0 re-serializes the GA call with "action": null and "pending_safety_checks": [], and the
+ // service rejects both on replay (see the known-issue test in InputConverterTests). These tests check what MAF
+ // controls: a populated single action here would mean the batch was rewritten.
+ if (call.TryGetProperty("action", out JsonElement action))
+ {
+ Assert.Equal(JsonValueKind.Null, action.ValueKind);
+ }
+ }
+
+ private static void AssertReplayedComputerCallOutput(JsonElement output)
+ {
+ Assert.Equal(ModelCallId, output.GetProperty("call_id").GetString());
+ JsonElement screenshot = output.GetProperty("output");
+ Assert.Equal("computer_screenshot", screenshot.GetProperty("type").GetString());
+ Assert.Equal(ScreenshotUrl, screenshot.GetProperty("image_url").GetString());
+ Assert.Equal("original", screenshot.GetProperty("detail").GetString());
+ Assert.False(output.TryGetProperty("response_id", out _));
+ Assert.False(output.TryGetProperty("agent_reference", out _));
+ }
+
+ private static AgentFrameworkResponseHandler BuildHandler(FakeResponsesModel model)
+ {
+ IChatClient chatClient = new OpenAIClient(
+ new ApiKeyCredential("test-key"),
+ new OpenAIClientOptions { Transport = new HttpClientPipelineTransport(model.HttpClient) })
+ .GetResponsesClient()
+ .AsIChatClient("test-model");
+
+ var agent = new ChatClientAgent(chatClient, new ChatClientAgentOptions
+ {
+ ChatOptions = new ChatOptions { Tools = [FoundryAITool.CreateComputerTool()] },
+ });
+
+ var services = new ServiceCollection();
+ services.AddSingleton(new InMemoryAgentSessionStore());
+ services.AddSingleton(agent);
+ services.AddSingleton(new FakeHostedSessionIsolationKeyProvider());
+ return new AgentFrameworkResponseHandler(services.BuildServiceProvider(), NullLogger.Instance);
+ }
+
+ private static CreateResponse NewRequest(object[] input) =>
+ new() { Model = "test", Input = BinaryData.FromObjectAsJson(input) };
+
+ private static object[] UserMessageInput(string text) =>
+ [
+ new
+ {
+ type = "message",
+ id = "msg_1",
+ status = "completed",
+ role = "user",
+ content = new[] { new { type = "input_text", text } },
+ },
+ ];
+
+ private static object[] ComputerCallOutputInput(string callId) =>
+ [
+ new
+ {
+ type = "computer_call_output",
+ call_id = callId,
+ output = new { type = "computer_screenshot", image_url = ScreenshotUrl, detail = "original" },
+ },
+ ];
+
+ private static ResponseContext NewContext(string responseId, CreateResponse request, IReadOnlyList history)
+ {
+ IReadOnlyList- inputItems = request.GetInputExpanded().ToList();
+ var context = new Mock(responseId) { CallBase = true };
+ context.Setup(x => x.PlatformContext).Returns(new PlatformContext("alice", null));
+ context.Setup(x => x.GetHistoryAsync(It.IsAny())).ReturnsAsync(history);
+ context.Setup(x => x.GetInputItemsAsync(It.IsAny(), It.IsAny())).ReturnsAsync(inputItems);
+ return context.Object;
+ }
+
+ private static OutputItemMessage UserHistoryMessage(string text) =>
+ new(
+ id: "msg_history_1",
+ role: MessageRole.User,
+ content: new MessageContent[] { new MessageContentInputTextContent(text) },
+ status: MessageStatus.Completed);
+
+ private static OutputItem ReadOutputItem(string json) =>
+ ModelReaderWriter.Read(BinaryData.FromString(json), ModelReaderWriterOptions.Json, AzureAIAgentServerResponsesContext.Default)!;
+
+ private static async Task
> DrainAsync(IAsyncEnumerable events)
+ {
+ var list = new List();
+ await foreach (var evt in events)
+ {
+ list.Add(evt);
+ }
+
+ return list;
+ }
+
+ private static string ComputerCallTurn()
+ {
+ string inProgressCall = BatchedComputerCallJson.Replace("\"status\":\"completed\"", "\"status\":\"in_progress\"");
+ return Sse(
+ ("response.created", "{\"type\":\"response.created\",\"sequence_number\":0,\"response\":" + ResponseJson("in_progress", string.Empty) + "}"),
+ ("response.output_item.added", "{\"type\":\"response.output_item.added\",\"sequence_number\":1,\"output_index\":0,\"item\":" + inProgressCall + "}"),
+ ("response.output_item.done", "{\"type\":\"response.output_item.done\",\"sequence_number\":2,\"output_index\":0,\"item\":" + BatchedComputerCallJson + "}"),
+ ("response.completed", "{\"type\":\"response.completed\",\"sequence_number\":3,\"response\":" + ResponseJson("completed", BatchedComputerCallJson) + "}"));
+ }
+
+ private static string TextTurn(string text)
+ {
+ string message = "{\"type\":\"message\",\"id\":\"msg_model_2\",\"status\":\"completed\",\"role\":\"assistant\",\"content\":[{\"type\":\"output_text\",\"text\":\"" + text + "\",\"annotations\":[]}]}";
+ return Sse(
+ ("response.created", "{\"type\":\"response.created\",\"sequence_number\":0,\"response\":" + ResponseJson("in_progress", string.Empty) + "}"),
+ ("response.output_text.delta", "{\"type\":\"response.output_text.delta\",\"sequence_number\":1,\"item_id\":\"msg_model_2\",\"output_index\":0,\"content_index\":0,\"delta\":\"" + text + "\"}"),
+ ("response.output_item.done", "{\"type\":\"response.output_item.done\",\"sequence_number\":2,\"output_index\":0,\"item\":" + message + "}"),
+ ("response.completed", "{\"type\":\"response.completed\",\"sequence_number\":3,\"response\":" + ResponseJson("completed", message) + "}"));
+ }
+
+ private static string ResponseJson(string status, string outputItemJson) =>
+ "{\"id\":\"resp_model_1\",\"object\":\"response\",\"created_at\":1762941294,\"status\":\"" + status + "\"," +
+ "\"model\":\"test-model\",\"output\":[" + outputItemJson + "],\"parallel_tool_calls\":true,\"tool_choice\":\"auto\"," +
+ "\"tools\":[{\"type\":\"computer\"}],\"store\":false,\"text\":{\"format\":{\"type\":\"text\"}},\"truncation\":\"disabled\"}";
+
+ private static string Sse(params (string EventType, string Data)[] events)
+ {
+ var builder = new StringBuilder();
+ foreach (var (eventType, data) in events)
+ {
+ builder.Append("event: ").Append(eventType).Append('\n');
+ builder.Append("data: ").Append(data).Append("\n\n");
+ }
+
+ return builder.ToString();
+ }
+
+ /// Serves one canned server-sent-event stream per model request and records each request body.
+ private sealed class FakeResponsesModel : HttpMessageHandler
+ {
+ private readonly Queue _turns;
+
+ public FakeResponsesModel(params string[] turns)
+ {
+ this._turns = new Queue(turns);
+#pragma warning disable CA5399 // Fake transport, no certificate revocation involved.
+ this.HttpClient = new HttpClient(this, disposeHandler: false);
+#pragma warning restore CA5399
+ }
+
+ public HttpClient HttpClient { get; }
+
+ public List RequestBodies { get; } = [];
+
+ protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ this.RequestBodies.Add(await request.Content!.ReadAsStringAsync(cancellationToken).ConfigureAwait(false));
+ return new HttpResponseMessage(HttpStatusCode.OK)
+ {
+ Content = new StringContent(this._turns.Dequeue(), Encoding.UTF8, "text/event-stream"),
+ RequestMessage = request,
+ };
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ this.HttpClient.Dispose();
+ }
+
+ base.Dispose(disposing);
+ }
+ }
+}
diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/InputConverterTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/InputConverterTests.cs
index 158eee6f62a..a2e5678adf3 100644
--- a/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/InputConverterTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/InputConverterTests.cs
@@ -1,10 +1,19 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
+using System.ClientModel.Primitives;
using System.Linq;
+using System.Text.Json;
+using Azure.AI.AgentServer.Responses;
using Azure.AI.AgentServer.Responses.Models;
using Microsoft.Extensions.AI;
using MeaiTextContent = Microsoft.Extensions.AI.TextContent;
+using OpenAIComputerCallActionKind = OpenAI.Responses.ComputerCallActionKind;
+using OpenAIComputerCallOutputResponseItem = OpenAI.Responses.ComputerCallOutputResponseItem;
+using OpenAIComputerCallResponseItem = OpenAI.Responses.ComputerCallResponseItem;
+
+#pragma warning disable OPENAI001 // Experimental Responses API surfaces
+#pragma warning disable OPENAICUA001 // Experimental OpenAI computer use surfaces
namespace Microsoft.Agents.AI.Foundry.Hosting.UnitTests;
@@ -886,7 +895,7 @@ public void ConvertItemsToMessages_McpApprovalResponse_ResolvesAfRequestIdFromSt
Assert.Equal("call_xyz", fcc.CallId);
Assert.Equal("issue_refund", fcc.Name);
Assert.NotNull(fcc.Arguments);
- Assert.Equal(123, ((System.Text.Json.JsonElement)fcc.Arguments!["order_id"]!).GetInt32());
+ Assert.Equal(123, ((JsonElement)fcc.Arguments!["order_id"]!).GetInt32());
}
[Fact]
@@ -1453,4 +1462,169 @@ public void ConvertInputToMessages_OversizedTextDataUri_FallsBackToDataContent()
// Should have fallen back to DataContent (carrying the original opaque blob).
Assert.Contains(messages[0].Contents, c => c is DataContent);
}
+
+ // --- Computer tool items (GA batched actions and preview single action) ---
+
+ // Shape the service returns for the GA tool: an ordered actions batch, and neither "action" nor "pending_safety_checks".
+ private const string GaComputerCallJson =
+ "{\"type\":\"computer_call\",\"id\":\"cu_input_1\",\"call_id\":\"call_cu_1\"," +
+ "\"actions\":[{\"type\":\"click\",\"button\":\"left\",\"x\":10,\"y\":20},{\"type\":\"type\",\"text\":\"penguin\"}]," +
+ "\"status\":\"completed\"}";
+
+ private const string ComputerCallOutputJson =
+ "{\"type\":\"computer_call_output\",\"call_id\":\"call_cu_1\"," +
+ "\"output\":{\"type\":\"computer_screenshot\",\"image_url\":\"data:image/png;base64,iVBORw0KGgo=\",\"detail\":\"original\"}}";
+
+ [Fact]
+ public void ConvertInputToMessages_GaComputerCall_ProducesAssistantToolCallCarryingOpenAIItem()
+ {
+ // Arrange
+ var request = new CreateResponse { Input = BinaryData.FromString("[" + GaComputerCallJson + "]") };
+
+ // Act
+ var messages = InputConverter.ConvertInputToMessages(request);
+
+ // Assert
+ var message = Assert.Single(messages);
+ Assert.Equal(ChatRole.Assistant, message.Role);
+ var toolCall = Assert.Single(message.Contents);
+ Assert.Equal(typeof(ToolCallContent), toolCall.GetType());
+ Assert.Equal("call_cu_1", ((ToolCallContent)toolCall).CallId);
+
+ var item = Assert.IsType(toolCall.RawRepresentation);
+ Assert.Equal("cu_input_1", item.Id);
+ Assert.Equal("call_cu_1", item.CallId);
+
+ using var json = JsonDocument.Parse(ModelReaderWriter.Write(item, ModelReaderWriterOptions.Json).ToString());
+ var actions = json.RootElement.GetProperty("actions").EnumerateArray().ToList();
+ Assert.Equal(2, actions.Count);
+ Assert.Equal("click", actions[0].GetProperty("type").GetString());
+ Assert.Equal("penguin", actions[1].GetProperty("text").GetString());
+ }
+
+ [Fact]
+ public void ConvertInputToMessages_ComputerCallOutput_ProducesToolResultCarryingOpenAIItem()
+ {
+ // Arrange
+ var request = new CreateResponse { Input = BinaryData.FromString("[" + ComputerCallOutputJson + "]") };
+
+ // Act
+ var messages = InputConverter.ConvertInputToMessages(request);
+
+ // Assert
+ var message = Assert.Single(messages);
+ Assert.Equal(ChatRole.Tool, message.Role);
+ var toolResult = Assert.Single(message.Contents);
+ Assert.Equal(typeof(ToolResultContent), toolResult.GetType());
+ Assert.Equal("call_cu_1", ((ToolResultContent)toolResult).CallId);
+
+ var item = Assert.IsType(toolResult.RawRepresentation);
+ Assert.Equal("call_cu_1", item.CallId);
+
+ using var json = JsonDocument.Parse(ModelReaderWriter.Write(item, ModelReaderWriterOptions.Json).ToString());
+ var screenshot = json.RootElement.GetProperty("output");
+ Assert.Equal("computer_screenshot", screenshot.GetProperty("type").GetString());
+ Assert.Equal("data:image/png;base64,iVBORw0KGgo=", screenshot.GetProperty("image_url").GetString());
+
+ // "detail" is not typed by either SDK yet; it must still survive as additional data.
+ Assert.Equal("original", screenshot.GetProperty("detail").GetString());
+ }
+
+ [Fact]
+ public void ConvertInputToMessages_PreviewComputerCall_KeepsSingleAction()
+ {
+ // Arrange
+ var request = new CreateResponse
+ {
+ Input = BinaryData.FromString(
+ "[{\"type\":\"computer_call\",\"id\":\"cu_preview_1\",\"call_id\":\"call_preview_1\"," +
+ "\"action\":{\"type\":\"click\",\"button\":\"left\",\"x\":10,\"y\":20}," +
+ "\"pending_safety_checks\":[],\"status\":\"completed\"}]"),
+ };
+
+ // Act
+ var messages = InputConverter.ConvertInputToMessages(request);
+
+ // Assert
+ var toolCall = Assert.Single(Assert.Single(messages).Contents);
+ var item = Assert.IsType(toolCall.RawRepresentation);
+ Assert.Equal("call_preview_1", item.CallId);
+ Assert.Equal(OpenAIComputerCallActionKind.Click, item.Action.Kind);
+ }
+
+ [Fact]
+ public void ConvertOutputItemsToMessages_HistoryComputerCall_DropsAgentServerEnvelopeFields()
+ {
+ // Arrange: AgentServer adds response_id and agent_reference to stored output items. They are not part of the
+ // model's item schema, and the model API rejects unknown input parameters.
+ var history = ReadOutputItem(
+ GaComputerCallJson.Replace(
+ "\"status\":\"completed\"",
+ "\"status\":\"completed\",\"response_id\":\"resp_previous\",\"agent_reference\":{\"type\":\"agent_reference\",\"name\":\"computer-agent\"}"));
+ Assert.IsType(history);
+
+ // Act
+ var messages = InputConverter.ConvertOutputItemsToMessages([history]);
+
+ // Assert
+ var message = Assert.Single(messages);
+ Assert.Equal(ChatRole.Assistant, message.Role);
+ var item = Assert.IsType(Assert.Single(message.Contents).RawRepresentation);
+ Assert.Equal("cu_input_1", item.Id);
+
+ string json = ModelReaderWriter.Write(item, ModelReaderWriterOptions.Json).ToString();
+ Assert.DoesNotContain("response_id", json);
+ Assert.DoesNotContain("agent_reference", json);
+ Assert.Contains("\"actions\":[{\"type\":\"click\"", json);
+ }
+
+ [Fact]
+ public void ConvertOutputItemsToMessages_HistoryComputerCallOutput_ProducesToolResultWithoutEnvelopeFields()
+ {
+ // Arrange
+ var history = ReadOutputItem(
+ ComputerCallOutputJson.Replace(
+ "\"type\":\"computer_call_output\",",
+ "\"type\":\"computer_call_output\",\"id\":\"cuo_history_1\",\"response_id\":\"resp_previous\","));
+ Assert.IsType(history);
+
+ // Act
+ var messages = InputConverter.ConvertOutputItemsToMessages([history]);
+
+ // Assert
+ var message = Assert.Single(messages);
+ Assert.Equal(ChatRole.Tool, message.Role);
+ var toolResult = Assert.Single(message.Contents);
+ Assert.Equal("call_cu_1", Assert.IsType(toolResult).CallId);
+ var item = Assert.IsType(toolResult.RawRepresentation);
+
+ string json = ModelReaderWriter.Write(item, ModelReaderWriterOptions.Json).ToString();
+ Assert.DoesNotContain("response_id", json);
+ Assert.Contains("\"detail\":\"original\"", json);
+ }
+
+ [Fact]
+ public void ConvertOutputItemsToMessages_GaComputerCall_ReplaysWithPreviewOnlyKeysKnownIssue()
+ {
+ // Arrange
+ var history = ReadOutputItem(GaComputerCallJson);
+
+ // Act
+ var messages = InputConverter.ConvertOutputItemsToMessages([history]);
+
+ // Assert: documents a known OpenAI .NET 2.13.0 behavior, not a desired one. ComputerCallResponseItem models the
+ // preview item: it has no typed "actions" and always writes "action" and "pending_safety_checks", so a replayed
+ // GA call carries both keys next to "actions". The Responses API rejects each of them for the GA tool:
+ // "action": null -> "Computer call input must include exactly one of `action` or `actions`."
+ // "pending_safety_checks" -> "`pending_safety_checks` is not supported for the "computer" tool."
+ // When this test starts failing, OpenAI .NET changed the shape: revisit the caveat in ComputerToolItemConverter.
+ var item = Assert.IsType(Assert.Single(Assert.Single(messages).Contents).RawRepresentation);
+ string json = ModelReaderWriter.Write(item, ModelReaderWriterOptions.Json).ToString();
+ Assert.Contains("\"action\":null", json);
+ Assert.Contains("\"pending_safety_checks\":[]", json);
+ Assert.Contains("\"actions\":[", json);
+ }
+
+ private static OutputItem ReadOutputItem(string json) =>
+ ModelReaderWriter.Read(BinaryData.FromString(json), ModelReaderWriterOptions.Json, AzureAIAgentServerResponsesContext.Default)!;
}
diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/OutputConverterTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/OutputConverterTests.cs
index 0aca1b238eb..be6c353c500 100644
--- a/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/OutputConverterTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/OutputConverterTests.cs
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
+using System.ClientModel.Primitives;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
@@ -14,12 +15,15 @@
using FileCitationMessageAnnotation = OpenAI.Responses.FileCitationMessageAnnotation;
using FilePathMessageAnnotation = OpenAI.Responses.FilePathMessageAnnotation;
using MeaiTextContent = Microsoft.Extensions.AI.TextContent;
+using OpenAIComputerCallOutputResponseItem = OpenAI.Responses.ComputerCallOutputResponseItem;
+using OpenAIComputerCallResponseItem = OpenAI.Responses.ComputerCallResponseItem;
using OpenAIResponseItem = OpenAI.Responses.ResponseItem;
using OpenAIStreamingResponseOutputItemDoneUpdate = OpenAI.Responses.StreamingResponseOutputItemDoneUpdate;
using OpenAIStreamingResponseOutputTextAnnotationAddedUpdate = OpenAI.Responses.StreamingResponseOutputTextAnnotationAddedUpdate;
using OpenAIStreamingResponseOutputTextDeltaUpdate = OpenAI.Responses.StreamingResponseOutputTextDeltaUpdate;
#pragma warning disable OPENAI001 // Experimental Responses API surfaces
+#pragma warning disable OPENAICUA001 // Experimental OpenAI computer use surfaces
namespace Microsoft.Agents.AI.Foundry.Hosting.UnitTests;
@@ -2228,6 +2232,171 @@ public async Task ConvertUpdatesToEventsAsync_NonCitationAnnotation_IsSkippedAsy
#endregion
+ #region Computer tool items
+
+ private const string ModelComputerCallId = "cu_model_item_1";
+
+ // Shape the service returns for the GA tool: an ordered actions batch, and neither "action" nor "pending_safety_checks".
+ private const string GaComputerCallJson =
+ "{\"type\":\"computer_call\",\"id\":\"" + ModelComputerCallId + "\",\"call_id\":\"call_cu_1\"," +
+ "\"actions\":[{\"type\":\"click\",\"button\":\"left\",\"x\":10,\"y\":20},{\"type\":\"type\",\"text\":\"penguin\"}]," +
+ "\"status\":\"completed\"}";
+
+ [Fact]
+ public async Task ConvertUpdatesToEventsAsync_GaComputerCall_EmitsComputerCallWithActionsAsync()
+ {
+ // Arrange
+ var (stream, _) = CreateTestStream();
+ var update = new AgentResponseUpdate { Contents = [ModelComputerCall(GaComputerCallJson)] };
+
+ // Act
+ var events = await CollectAsync(stream, update);
+
+ // Assert
+ var added = Assert.IsType(Assert.Single(events.OfType()).Item);
+ var done = Assert.IsType(Assert.Single(events.OfType()).Item);
+
+ // AgentServer requires its own item id format and does not stamp one on EmitAdded, so the builder id is used
+ // while the model's call_id is kept for the caller to answer with.
+ Assert.Equal(added.Id, done.Id);
+ Assert.StartsWith("cu_", done.Id, StringComparison.Ordinal);
+ Assert.NotEqual(ModelComputerCallId, done.Id);
+ Assert.Equal("call_cu_1", done.CallId);
+ Assert.Equal(2, done.Actions.Count);
+ Assert.Null(done.Action);
+ Assert.Equal(ItemComputerToolCallStatus.Completed, done.Status);
+
+ string json = ModelReaderWriter.Write(done, ModelReaderWriterOptions.Json).ToString();
+ Assert.Contains("\"actions\":[{\"type\":\"click\"", json);
+ Assert.DoesNotContain("\"action\":", json);
+ Assert.IsType(events[^1]);
+ }
+
+ [Fact]
+ public async Task ConvertUpdatesToEventsAsync_PreviewComputerCall_EmitsSingleActionAsync()
+ {
+ // Arrange
+ var (stream, _) = CreateTestStream();
+ var update = new AgentResponseUpdate
+ {
+ Contents =
+ [
+ ModelComputerCall(
+ "{\"type\":\"computer_call\",\"id\":\"cu_preview_1\",\"call_id\":\"call_preview_1\"," +
+ "\"action\":{\"type\":\"click\",\"button\":\"left\",\"x\":10,\"y\":20}," +
+ "\"pending_safety_checks\":[],\"status\":\"completed\"}")
+ ],
+ };
+
+ // Act
+ var events = await CollectAsync(stream, update);
+
+ // Assert
+ var done = Assert.IsType(Assert.Single(events.OfType()).Item);
+ Assert.Equal("call_preview_1", done.CallId);
+ Assert.NotNull(done.Action);
+ Assert.Empty(done.Actions);
+ Assert.Contains("\"action\":{\"type\":\"click\"", ModelReaderWriter.Write(done, ModelReaderWriterOptions.Json).ToString());
+ }
+
+ [Fact]
+ public async Task ConvertUpdatesToEventsAsync_ComputerCallClosesOpenMessageAsync()
+ {
+ // Arrange
+ var (stream, _) = CreateTestStream();
+ var updates = new[]
+ {
+ new AgentResponseUpdate { MessageId = "msg_1", Contents = [new MeaiTextContent("Let me look.")] },
+ new AgentResponseUpdate { Contents = [ModelComputerCall(GaComputerCallJson)] },
+ };
+
+ // Act
+ var events = await CollectAsync(stream, updates);
+
+ // Assert: the message is completed before the computer call starts.
+ int messageDone = events.FindIndex(e => e is ResponseOutputItemDoneEvent { Item: OutputItemMessage });
+ int computerAdded = events.FindIndex(e => e is ResponseOutputItemAddedEvent { Item: OutputItemComputerToolCall });
+ Assert.True(messageDone >= 0, "The open message was not closed.");
+ Assert.True(computerAdded > messageDone, "The computer call must start after the open message is closed.");
+ }
+
+ [Fact]
+ public async Task ConvertUpdatesToEventsAsync_AgentProducedComputerCallOutput_EmitsComputerCallOutputAsync()
+ {
+ // Arrange: an agent that runs the actions itself yields the output alongside the call.
+ var (stream, _) = CreateTestStream();
+ var output = Assert.IsType(ReadOpenAIItem(
+ "{\"type\":\"computer_call_output\",\"call_id\":\"call_cu_1\"," +
+ "\"output\":{\"type\":\"computer_screenshot\",\"image_url\":\"data:image/png;base64,iVBORw0KGgo=\",\"detail\":\"original\"}}"));
+ var update = new AgentResponseUpdate { Contents = [new ToolResultContent("call_cu_1") { RawRepresentation = output }] };
+
+ // Act
+ var events = await CollectAsync(stream, update);
+
+ // Assert
+ var added = Assert.IsType(Assert.Single(events.OfType()).Item);
+ var done = Assert.IsType(Assert.Single(events.OfType()).Item);
+ Assert.Equal(added.Id, done.Id);
+ Assert.False(string.IsNullOrEmpty(done.Id));
+ Assert.Equal("call_cu_1", done.CallId);
+ Assert.Equal("data:image/png;base64,iVBORw0KGgo=", done.Output.ImageUrl?.ToString());
+ Assert.Contains("\"detail\":\"original\"", ModelReaderWriter.Write(done, ModelReaderWriterOptions.Json).ToString());
+ }
+
+ [Fact]
+ public async Task ConvertUpdatesToEventsAsync_ComputerCallWithEmptyCallId_IsSkippedAsync()
+ {
+ // Arrange
+ var (stream, _) = CreateTestStream();
+ var item = ReadOpenAIItem(GaComputerCallJson);
+ var update = new AgentResponseUpdate { Contents = [new ToolCallContent(string.Empty) { RawRepresentation = item }] };
+
+ // Act
+ var events = await CollectAsync(stream, update);
+
+ // Assert: same rule as function calls, a call without an id cannot be answered.
+ Assert.Single(events);
+ Assert.IsType(events[0]);
+ }
+
+ [Fact]
+ public async Task ConvertUpdatesToEventsAsync_ToolCallWithoutComputerItem_IsSkippedAsync()
+ {
+ // Arrange
+ var (stream, _) = CreateTestStream();
+ var update = new AgentResponseUpdate { Contents = [new ToolCallContent("call_other")] };
+
+ // Act
+ var events = await CollectAsync(stream, update);
+
+ // Assert: only computer calls are mapped; other hosted tool calls keep being skipped.
+ Assert.Single(events);
+ Assert.IsType(events[0]);
+ }
+
+ private static ToolCallContent ModelComputerCall(string json)
+ {
+ // Mirrors what the MEAI OpenAI Responses client yields for a streamed computer_call.
+ var item = Assert.IsType(ReadOpenAIItem(json));
+ return new ToolCallContent(item.CallId) { RawRepresentation = item };
+ }
+
+ private static OpenAIResponseItem ReadOpenAIItem(string json) =>
+ ModelReaderWriter.Read(BinaryData.FromString(json), ModelReaderWriterOptions.Json)!;
+
+ private static async Task> CollectAsync(ResponseEventStream stream, params AgentResponseUpdate[] updates)
+ {
+ var events = new List();
+ await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(updates), stream))
+ {
+ events.Add(evt);
+ }
+
+ return events;
+ }
+
+ #endregion
+
private sealed class RawToolCallContent : ToolCallContent
{
public RawToolCallContent(string callId) : base(callId) { }
From 70afa939a1fb8d6f22b843b0690ef3ec3a832403 Mon Sep 17 00:00:00 2001
From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com>
Date: Fri, 25 Sep 2026 12:42:29 +0100
Subject: [PATCH 03/13] docs(dotnet): add GA computer use sample
Foundry project endpoint rejects {"type":"computer"} today, so the
sample targets public OpenAI with the Foundry setup commented out.
---
dotnet/agent-framework-dotnet.slnx | 1 +
dotnet/eng/verify-samples/AgentsSamples.cs | 13 ++
.../Agent_Step15_ComputerUse/README.md | 4 +-
.../Agent_Step27_ComputerUseGA.csproj | 26 ++++
.../ComputerUseUtil.cs | 105 ++++++++++++++++
.../Agent_Step27_ComputerUseGA/Program.cs | 116 ++++++++++++++++++
.../Agent_Step27_ComputerUseGA/README.md | 65 ++++++++++
.../AgentProviders/foundry/README.md | 3 +-
8 files changed, 331 insertions(+), 2 deletions(-)
create mode 100644 dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step27_ComputerUseGA/Agent_Step27_ComputerUseGA.csproj
create mode 100644 dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step27_ComputerUseGA/ComputerUseUtil.cs
create mode 100644 dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step27_ComputerUseGA/Program.cs
create mode 100644 dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step27_ComputerUseGA/README.md
diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx
index b614d196a46..12e236f7034 100644
--- a/dotnet/agent-framework-dotnet.slnx
+++ b/dotnet/agent-framework-dotnet.slnx
@@ -169,6 +169,7 @@
+
diff --git a/dotnet/eng/verify-samples/AgentsSamples.cs b/dotnet/eng/verify-samples/AgentsSamples.cs
index 3caac4bb037..3438b0e0070 100644
--- a/dotnet/eng/verify-samples/AgentsSamples.cs
+++ b/dotnet/eng/verify-samples/AgentsSamples.cs
@@ -1195,6 +1195,19 @@ internal static class AgentsSamples
ExpectedOutputDescription = ["The output should show a computer automation session processing simulated browser screenshots with iteration steps and a final response describing search results."],
},
+ new SampleDefinition
+ {
+ Name = "FoundryAgent_Step27_ComputerUseGA",
+ ProjectPath = "samples/02-agents/AgentProviders/foundry/Agent_Step27_ComputerUseGA",
+ RequiredEnvironmentVariables = ["OPENAI_API_KEY"],
+ OptionalEnvironmentVariables = ["OPENAI_CHAT_MODEL_NAME"],
+ ExpectedOutputDescription =
+ [
+ "The output should show a computer automation session running batched actions (such as click, type or keypress) against simulated browser screenshots, followed by a final response describing search results.",
+ "The output should not contain error messages or stack traces.",
+ ],
+ },
+
new SampleDefinition
{
Name = "FoundryAgent_Step18_BingCustomSearch",
diff --git a/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step15_ComputerUse/README.md b/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step15_ComputerUse/README.md
index 414bd55522c..0cb9cf9df91 100644
--- a/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step15_ComputerUse/README.md
+++ b/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step15_ComputerUse/README.md
@@ -4,10 +4,12 @@ This sample shows how to use the Computer Use tool with `AIProjectClient.AsAIAge
## What this sample demonstrates
-- Using `FoundryAITool.CreateComputerTool()` to add computer use capabilities
+- Using `FoundryAITool.CreateComputerTool(environment, width, height)` to add the preview `computer_use_preview` tool
- Processing computer call actions (click, type, key press)
- Managing the computer use interaction loop with screenshots
+For the generally available (GA) `computer` tool, created with the parameterless `FoundryAITool.CreateComputerTool()` and returning batched actions, see [Agent_Step27_ComputerUseGA](../Agent_Step27_ComputerUseGA/).
+
For more information, see [Use the computer tool](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/tools/computer-use?pivots=csharp).
## How the simulation works
diff --git a/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step27_ComputerUseGA/Agent_Step27_ComputerUseGA.csproj b/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step27_ComputerUseGA/Agent_Step27_ComputerUseGA.csproj
new file mode 100644
index 00000000000..55eda1a877e
--- /dev/null
+++ b/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step27_ComputerUseGA/Agent_Step27_ComputerUseGA.csproj
@@ -0,0 +1,26 @@
+
+
+
+ Exe
+ net10.0
+
+ enable
+ enable
+ $(NoWarn);OPENAICUA001;MEAI001
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step27_ComputerUseGA/ComputerUseUtil.cs b/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step27_ComputerUseGA/ComputerUseUtil.cs
new file mode 100644
index 00000000000..25a54ee55ef
--- /dev/null
+++ b/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step27_ComputerUseGA/ComputerUseUtil.cs
@@ -0,0 +1,105 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.ClientModel.Primitives;
+using System.Text.Json;
+using OpenAI.Responses;
+
+namespace Demo.ComputerUseGA;
+
+///
+/// Enum for tracking the state of the simulated web search flow.
+///
+internal enum SearchState
+{
+ Initial, // Browser search page
+ Typed, // Text entered in search box
+ PressedEnter // Search submitted, showing results
+}
+
+///
+/// One action of a computer call batch, reduced to the fields this simulation needs.
+///
+internal sealed record ComputerActionRequest(string Type, string? Text, IReadOnlyList Keys);
+
+internal static class ComputerUseUtil
+{
+ internal static Dictionary LoadScreenshots()
+ {
+ string assetsDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Assets");
+
+ return new Dictionary
+ {
+ ["browser_search"] = BinaryData.FromBytes(File.ReadAllBytes(Path.Combine(assetsDir, "cua_browser_search.jpg"))),
+ ["search_typed"] = BinaryData.FromBytes(File.ReadAllBytes(Path.Combine(assetsDir, "cua_search_typed.jpg"))),
+ ["search_results"] = BinaryData.FromBytes(File.ReadAllBytes(Path.Combine(assetsDir, "cua_search_results.jpg"))),
+ };
+ }
+
+ ///
+ /// Reads the ordered actions of a computer call.
+ ///
+ ///
+ /// The OpenAI .NET SDK does not expose the GA actions batch as a typed property yet, so the call is
+ /// re-serialized and the batch is read from its JSON. A preview call carries a single action instead.
+ ///
+ internal static IReadOnlyList GetActions(ComputerCallResponseItem computerCall)
+ {
+ using JsonDocument json = JsonDocument.Parse(ModelReaderWriter.Write(computerCall, ModelReaderWriterOptions.Json));
+
+ if (json.RootElement.TryGetProperty("actions", out JsonElement actions) && actions.ValueKind == JsonValueKind.Array)
+ {
+ return [.. actions.EnumerateArray().Select(ToActionRequest)];
+ }
+
+ if (json.RootElement.TryGetProperty("action", out JsonElement action) && action.ValueKind == JsonValueKind.Object)
+ {
+ return [ToActionRequest(action)];
+ }
+
+ return [];
+ }
+
+ ///
+ /// Simulates executing a computer action by advancing the state.
+ ///
+ internal static async Task ApplyAsync(ComputerActionRequest action, SearchState currentState)
+ {
+ if (action.Type == "wait")
+ {
+ await Task.Delay(TimeSpan.FromSeconds(1));
+ }
+
+ return action.Type switch
+ {
+ "click" when currentState == SearchState.Typed => SearchState.PressedEnter,
+ "type" when action.Text is not null => SearchState.Typed,
+ "keypress" when IsEnterKey(action) => SearchState.PressedEnter,
+ _ => currentState
+ };
+ }
+
+ ///
+ /// Returns the key of the screenshot that shows the given state.
+ ///
+ internal static string GetScreenshotKey(SearchState state) => state switch
+ {
+ SearchState.PressedEnter => "search_results",
+ SearchState.Typed => "search_typed",
+ _ => "browser_search"
+ };
+
+ private static ComputerActionRequest ToActionRequest(JsonElement action)
+ {
+ string type = action.TryGetProperty("type", out JsonElement typeElement) ? typeElement.GetString() ?? string.Empty : string.Empty;
+ string? text = action.TryGetProperty("text", out JsonElement textElement) ? textElement.GetString() : null;
+ IReadOnlyList keys = action.TryGetProperty("keys", out JsonElement keysElement) && keysElement.ValueKind == JsonValueKind.Array
+ ? [.. keysElement.EnumerateArray().Select(k => k.GetString() ?? string.Empty)]
+ : [];
+
+ return new ComputerActionRequest(type, text, keys);
+ }
+
+ private static bool IsEnterKey(ComputerActionRequest action) =>
+ action.Keys.Contains("Return", StringComparer.OrdinalIgnoreCase) ||
+ action.Keys.Contains("Enter", StringComparer.OrdinalIgnoreCase);
+}
diff --git a/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step27_ComputerUseGA/Program.cs b/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step27_ComputerUseGA/Program.cs
new file mode 100644
index 00000000000..b58393d1bd0
--- /dev/null
+++ b/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step27_ComputerUseGA/Program.cs
@@ -0,0 +1,116 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+// This sample shows how to use the generally available (GA) computer tool, created with the parameterless
+// FoundryAITool.CreateComputerTool(), with an AIAgent. Each computer call returned by the model carries an ordered
+// batch of actions; the application runs all of them and then sends back a single screenshot.
+//
+// Microsoft Foundry does not accept the GA "computer" tool type yet, so the sample currently runs against the public
+// OpenAI Responses API. The Foundry setup is kept below, commented out: once Foundry supports the GA tool, swap the
+// two agent setups and the rest of the sample works unchanged.
+
+using Demo.ComputerUseGA;
+using Microsoft.Agents.AI;
+using Microsoft.Agents.AI.Foundry;
+using Microsoft.Extensions.AI;
+using OpenAI;
+using OpenAI.Responses;
+
+// Microsoft Foundry setup (see below): uncomment once Foundry supports the GA computer tool.
+// using Azure.AI.Projects;
+// using Azure.Identity;
+
+// Public OpenAI Responses API (active until Microsoft Foundry supports the GA computer tool).
+string apiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY") ?? throw new InvalidOperationException("OPENAI_API_KEY is not set.");
+string model = Environment.GetEnvironmentVariable("OPENAI_CHAT_MODEL_NAME") ?? "gpt-5.4";
+
+AIAgent agent = new OpenAIClient(apiKey)
+ .GetResponsesClient()
+ .AsIChatClient(model)
+ .AsAIAgent(
+ name: "ComputerAgent",
+ instructions: "You are a computer automation assistant.",
+ tools: [FoundryAITool.CreateComputerTool()]);
+
+// Microsoft Foundry: uncomment this block (and the Azure usings at the top) once Foundry supports the GA computer
+// tool, then remove the OpenAI block above.
+//
+// string endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
+// string deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_COMPUTER_USE_DEPLOYMENT_NAME") ?? "gpt-5.4";
+//
+// // WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
+// // In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
+// // latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
+// AIProjectClient projectClient = new(new Uri(endpoint), new DefaultAzureCredential());
+//
+// AIAgent agent = projectClient.AsAIAgent(
+// model: deploymentName,
+// name: "ComputerAgent",
+// instructions: "You are a computer automation assistant.",
+// tools: [FoundryAITool.CreateComputerTool()]);
+
+Dictionary screenshots = ComputerUseUtil.LoadScreenshots();
+
+// Send the initial request with a screenshot of the browser.
+ChatMessage message = new(ChatRole.User, [
+ new TextContent("Search for 'OpenAI news'. Type it and submit. Once you see results, the task is complete."),
+ new DataContent(screenshots["browser_search"].ToMemory(), "image/jpeg")
+]);
+
+Console.WriteLine("Starting computer use session...");
+
+AgentSession session = await agent.CreateSessionAsync();
+AgentResponse response = await agent.RunAsync(message, session: session);
+
+SearchState currentState = SearchState.Initial;
+
+for (int i = 0; i < 10; i++)
+{
+ // Find the next computer call.
+ ComputerCallResponseItem? computerCall = response.Messages
+ .SelectMany(m => m.Contents)
+ .Select(c => c.RawRepresentation as ComputerCallResponseItem)
+ .FirstOrDefault(item => item is not null);
+
+ if (computerCall is null)
+ {
+ if (currentState == SearchState.PressedEnter)
+ {
+ Console.WriteLine("No more computer actions. Done.");
+ Console.WriteLine(response);
+ break;
+ }
+
+ // The model may ask for confirmation before acting. Answer and continue.
+ if (response.Text.Contains('?'))
+ {
+ Console.WriteLine($"Model asked: {response.Text}");
+ response = await agent.RunAsync("Please proceed.", session);
+ continue;
+ }
+
+ Console.WriteLine(response);
+ break;
+ }
+
+ // Run every action of the batch in order, then capture a single screenshot.
+ foreach (ComputerActionRequest action in ComputerUseUtil.GetActions(computerCall))
+ {
+ Console.WriteLine($"[{i + 1}] Action: {action.Type}");
+ currentState = await ComputerUseUtil.ApplyAsync(action, currentState);
+ }
+
+ ComputerCallOutputResponseItem callOutput = new(
+ computerCall.CallId,
+ ComputerCallOutput.CreateScreenshotOutput(screenshots[ComputerUseUtil.GetScreenshotKey(currentState)], "image/jpeg"));
+
+ // The model can flag an action as risky with pending safety checks. A real application must show them to the user
+ // and acknowledge them only after the user confirms; this simulation acknowledges them automatically.
+ foreach (ComputerCallSafetyCheck safetyCheck in computerCall.PendingSafetyChecks)
+ {
+ Console.WriteLine($"Acknowledging safety check: {safetyCheck.Code}");
+ callOutput.AcknowledgedSafetyChecks.Add(safetyCheck);
+ }
+
+ // Send the screenshot back as the computer call output.
+ response = await agent.RunAsync([new ChatMessage(ChatRole.User, [new AIContent { RawRepresentation = callOutput }])], session);
+}
diff --git a/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step27_ComputerUseGA/README.md b/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step27_ComputerUseGA/README.md
new file mode 100644
index 00000000000..edf0d20eb14
--- /dev/null
+++ b/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step27_ComputerUseGA/README.md
@@ -0,0 +1,65 @@
+# Computer use with the GA computer tool
+
+This sample shows how to use the generally available (GA) computer tool with an `AIAgent`.
+
+## What this sample demonstrates
+
+- Using the parameterless `FoundryAITool.CreateComputerTool()` to add the GA `computer` tool (`{"type":"computer"}`)
+- Running every action of a batched `computer_call` (the GA call returns an ordered `actions` list) before sending back one screenshot
+- Returning screenshots as inline image data, so no file upload or cleanup is needed
+- Acknowledging pending safety checks in the `computer_call_output`
+
+For the preview `computer_use_preview` tool, which takes an environment and display size and returns a single `action` per call, see [Agent_Step15_ComputerUse](../Agent_Step15_ComputerUse/).
+
+| | GA tool (this sample) | Preview tool (Step15) |
+| --- | --- | --- |
+| Factory | `FoundryAITool.CreateComputerTool()` | `FoundryAITool.CreateComputerTool(environment, width, height)` |
+| Wire tool | `{"type":"computer"}` | `{"type":"computer_use_preview", ...}` |
+| Actions per call | ordered `actions` batch | single `action` |
+
+For more information, see the OpenAI [computer use guide](https://developers.openai.com/api/docs/guides/tools-computer-use).
+
+## Microsoft Foundry support
+
+Microsoft Foundry does not accept the GA `computer` tool type yet. Until it does, the sample runs against the public OpenAI Responses API. The Foundry setup is already in `Program.cs`, commented out: once Foundry supports the GA tool, uncomment it (and the `Azure.*` usings at the top), remove the OpenAI setup, and set the Foundry environment variables below.
+
+## Known limitation
+
+The sample continues each turn from the stored response, so only the new `computer_call_output` is sent. Sending a GA `computer_call` back to the model instead (for example with stored responses disabled and the history kept locally) currently fails. The OpenAI .NET SDK models only the preview item and adds `"action": null` and `"pending_safety_checks": []`, which the Responses API rejects for the GA tool.
+
+## How the simulation works
+
+**This sample does not connect to a real browser.** It intercepts the model's actions and returns pre-captured screenshots (shared with the Step15 sample) as if the actions were performed:
+
+| State | Reached by | Screenshot sent back |
+| --- | --- | --- |
+| Initial | Session start | `cua_browser_search.jpg`, an empty search page |
+| Typed | A `type` action | `cua_search_typed.jpg`, search text in the box |
+| Search submitted | A `keypress` of Enter, or a `click` after typing | `cua_search_results.jpg`, the results page |
+
+Each loop iteration runs all actions of the call in order, then sends the screenshot for the resulting state. The loop ends when the model stops returning computer calls after the search was submitted, or after 10 iterations.
+
+## Prerequisites
+
+- .NET 10 SDK or later
+- An OpenAI API key with access to a model that supports the GA computer tool (for example `gpt-5.4`)
+
+Set the following environment variables:
+
+```powershell
+$env:OPENAI_API_KEY="sk-..."
+$env:OPENAI_CHAT_MODEL_NAME="gpt-5.4" # Optional, defaults to gpt-5.4
+```
+
+When switching to Microsoft Foundry, set these instead:
+
+```powershell
+$env:FOUNDRY_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project"
+$env:AZURE_AI_COMPUTER_USE_DEPLOYMENT_NAME="gpt-5.4"
+```
+
+## Run the sample
+
+```powershell
+dotnet run
+```
diff --git a/dotnet/samples/02-agents/AgentProviders/foundry/README.md b/dotnet/samples/02-agents/AgentProviders/foundry/README.md
index eaae6be594f..837c351ba34 100644
--- a/dotnet/samples/02-agents/AgentProviders/foundry/README.md
+++ b/dotnet/samples/02-agents/AgentProviders/foundry/README.md
@@ -63,7 +63,7 @@ Some samples require extra tool-specific environment variables. See each sample
| [Middleware](./Agent_Step12_Middleware/) | Multiple middleware layers |
| [Plugins](./Agent_Step13_Plugins/) | Plugins with dependency injection |
| [Code interpreter](./Agent_Step14_CodeInterpreter/) | Code interpreter tool |
-| [Computer use](./Agent_Step15_ComputerUse/) | Computer use tool |
+| [Computer use](./Agent_Step15_ComputerUse/) | Computer use tool (preview `computer_use_preview`) |
| [File search](./Agent_Step16_FileSearch/) | File search tool |
| [OpenAPI tools](./Agent_Step17_OpenAPITools/) | OpenAPI tools |
| [Bing custom search](./Agent_Step18_BingCustomSearch/) | Bing Custom Search tool |
@@ -75,6 +75,7 @@ Some samples require extra tool-specific environment variables. See each sample
| [Code interpreter file download](./Agent_Step24_CodeInterpreterFileDownload/) | Download container files generated by code interpreter |
| [Foundry toolbox via MCP](./Agent_Step25_FoundryToolboxMcp/README.md) | Use a Foundry Toolbox from a non-hosted agent via its MCP endpoint |
| [Foundry toolbox MCP skills](./Agent_Step26_FoundryToolboxMcpSkills/README.md) | Use a Foundry Toolbox with MCP-based skills discovery (SEP-2640) via AIContextProviders |
+| [Computer use (GA)](./Agent_Step27_ComputerUseGA/) | GA `computer` tool with batched actions (runs on public OpenAI until Foundry supports it) |
## Running the samples
From 77f6aad5eac936711e52ce76390ff72e801e2da5 Mon Sep 17 00:00:00 2001
From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com>
Date: Fri, 25 Sep 2026 14:08:56 +0100
Subject: [PATCH 04/13] chore(dotnet): drop stale OPENAICUA001 pragmas
OpenAI .NET 2.13.0 gates its computer use types with OPENAI001
(openai/openai-dotnet#1245), so the pragmas in hosting code and its
converter tests were no-ops. OPENAICUA001 now only marks MAF's own
computer tool factories.
---
.../ComputerToolItemConverter.cs | 2 --
.../Microsoft.Agents.AI.Foundry.Hosting/OutputConverter.cs | 2 --
dotnet/src/Shared/DiagnosticIds/DiagnosticsIds.cs | 4 ++++
.../ComputerToolHostingLoopTests.cs | 2 +-
.../InputConverterTests.cs | 1 -
.../OutputConverterTests.cs | 1 -
6 files changed, 5 insertions(+), 7 deletions(-)
diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/ComputerToolItemConverter.cs b/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/ComputerToolItemConverter.cs
index 20c20175c03..273ec2c1d97 100644
--- a/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/ComputerToolItemConverter.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/ComputerToolItemConverter.cs
@@ -14,8 +14,6 @@
using OpenAIContext = OpenAI.OpenAIContext;
using OpenAIResponseItem = OpenAI.Responses.ResponseItem;
-#pragma warning disable OPENAICUA001 // OpenAI .NET marks its computer use types as experimental.
-
namespace Microsoft.Agents.AI.Foundry.Hosting;
///
diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/OutputConverter.cs b/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/OutputConverter.cs
index 4fe6166515b..01b4fe63b08 100644
--- a/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/OutputConverter.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/OutputConverter.cs
@@ -26,8 +26,6 @@
using OpenAIStreamingResponseOutputTextAnnotationAddedUpdate = OpenAI.Responses.StreamingResponseOutputTextAnnotationAddedUpdate;
using OpenAIStreamingResponseOutputTextDeltaUpdate = OpenAI.Responses.StreamingResponseOutputTextDeltaUpdate;
-#pragma warning disable OPENAICUA001 // OpenAI .NET marks its computer use types as experimental.
-
namespace Microsoft.Agents.AI.Foundry.Hosting;
internal abstract record OutputConverterItem
diff --git a/dotnet/src/Shared/DiagnosticIds/DiagnosticsIds.cs b/dotnet/src/Shared/DiagnosticIds/DiagnosticsIds.cs
index e9020aec629..ea73ec19ec7 100644
--- a/dotnet/src/Shared/DiagnosticIds/DiagnosticsIds.cs
+++ b/dotnet/src/Shared/DiagnosticIds/DiagnosticsIds.cs
@@ -30,6 +30,10 @@ internal static class Experiments
// We use the same IDs so consumers do not need to suppress additional diagnostics
// when using the experimental OpenAI APIs.
internal const string AIOpenAIResponses = "OPENAI001";
+
+ // The OpenAI package used OPENAICUA001 for its computer use APIs until 2.13.0, which folded
+ // them into OPENAI001 (openai/openai-dotnet#1245). The ID now only gates the computer tool
+ // factories in this repo; it is kept so existing suppressions of the preview factory keep working.
internal const string AIOpenAIComputerUse = "OPENAICUA001";
private const string MEAIExperiments = "MEAI001";
diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/ComputerToolHostingLoopTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/ComputerToolHostingLoopTests.cs
index 7dc0d23dfcb..7b79dd82375 100644
--- a/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/ComputerToolHostingLoopTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/ComputerToolHostingLoopTests.cs
@@ -21,7 +21,7 @@
using OpenAIClientOptions = OpenAI.OpenAIClientOptions;
#pragma warning disable OPENAI001 // Experimental Responses API surfaces
-#pragma warning disable OPENAICUA001 // Experimental OpenAI computer use surfaces
+#pragma warning disable OPENAICUA001 // FoundryAITool.CreateComputerTool is experimental.
namespace Microsoft.Agents.AI.Foundry.Hosting.UnitTests;
diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/InputConverterTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/InputConverterTests.cs
index a2e5678adf3..31074c3bef4 100644
--- a/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/InputConverterTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/InputConverterTests.cs
@@ -13,7 +13,6 @@
using OpenAIComputerCallResponseItem = OpenAI.Responses.ComputerCallResponseItem;
#pragma warning disable OPENAI001 // Experimental Responses API surfaces
-#pragma warning disable OPENAICUA001 // Experimental OpenAI computer use surfaces
namespace Microsoft.Agents.AI.Foundry.Hosting.UnitTests;
diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/OutputConverterTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/OutputConverterTests.cs
index be6c353c500..ce23e233638 100644
--- a/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/OutputConverterTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/OutputConverterTests.cs
@@ -23,7 +23,6 @@
using OpenAIStreamingResponseOutputTextDeltaUpdate = OpenAI.Responses.StreamingResponseOutputTextDeltaUpdate;
#pragma warning disable OPENAI001 // Experimental Responses API surfaces
-#pragma warning disable OPENAICUA001 // Experimental OpenAI computer use surfaces
namespace Microsoft.Agents.AI.Foundry.Hosting.UnitTests;
From 4cc75602620f14c18ea6111f4ac50f98eeef9bcd Mon Sep 17 00:00:00 2001
From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com>
Date: Fri, 25 Sep 2026 14:16:12 +0100
Subject: [PATCH 05/13] refactor(dotnet): gate computer tool factories with
OPENAI001
OpenAI .NET 2.13.0 retired OPENAICUA001 in favor of OPENAI001
(openai/openai-dotnet#1245). Both FoundryAITool.CreateComputerTool
overloads now use the shared OPENAI001 id, so callers need no extra
suppression; remaining OPENAICUA001 pragmas and NoWarn entries go.
---
.../Agent_Step15_ComputerUse/Agent_Step15_ComputerUse.csproj | 2 +-
.../Agent_Step27_ComputerUseGA.csproj | 2 +-
dotnet/src/Microsoft.Agents.AI.Foundry/FoundryAITool.cs | 4 ++--
dotnet/src/Shared/DiagnosticIds/DiagnosticsIds.cs | 5 -----
dotnet/src/Shared/Workflows/Execution/WorkflowRunner.cs | 1 -
.../ComputerToolHostingLoopTests.cs | 3 +--
.../FoundryAIToolTests.cs | 1 -
7 files changed, 5 insertions(+), 13 deletions(-)
diff --git a/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step15_ComputerUse/Agent_Step15_ComputerUse.csproj b/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step15_ComputerUse/Agent_Step15_ComputerUse.csproj
index a3f494a1b46..eeefebab121 100644
--- a/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step15_ComputerUse/Agent_Step15_ComputerUse.csproj
+++ b/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step15_ComputerUse/Agent_Step15_ComputerUse.csproj
@@ -6,7 +6,7 @@
enable
enable
- $(NoWarn);OPENAICUA001;MEAI001
+ $(NoWarn);MEAI001
diff --git a/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step27_ComputerUseGA/Agent_Step27_ComputerUseGA.csproj b/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step27_ComputerUseGA/Agent_Step27_ComputerUseGA.csproj
index 55eda1a877e..cd45bdbe091 100644
--- a/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step27_ComputerUseGA/Agent_Step27_ComputerUseGA.csproj
+++ b/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step27_ComputerUseGA/Agent_Step27_ComputerUseGA.csproj
@@ -6,7 +6,7 @@
enable
enable
- $(NoWarn);OPENAICUA001;MEAI001
+ $(NoWarn);MEAI001
diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry/FoundryAITool.cs b/dotnet/src/Microsoft.Agents.AI.Foundry/FoundryAITool.cs
index 54b0f46f4c3..bdb61a6b773 100644
--- a/dotnet/src/Microsoft.Agents.AI.Foundry/FoundryAITool.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Foundry/FoundryAITool.cs
@@ -154,7 +154,7 @@ public static AITool CreateHostedMcpToolbox(string toolboxName, string? version
///
///
/// An for the GA computer tool.
- [Experimental(DiagnosticIds.Experiments.AIOpenAIComputerUse)]
+ [Experimental(DiagnosticIds.Experiments.AIOpenAIResponses)]
public static AITool CreateComputerTool()
{
// OpenAI .NET (2.13.0 and 2.14.0) has no factory for the GA tool: ResponseTool.CreateComputerTool always emits
@@ -181,7 +181,7 @@ public static AITool CreateComputerTool()
/// The display width in pixels.
/// The display height in pixels.
/// An for the preview computer use tool.
- [Experimental(DiagnosticIds.Experiments.AIOpenAIComputerUse)]
+ [Experimental(DiagnosticIds.Experiments.AIOpenAIResponses)]
public static AITool CreateComputerTool(ComputerToolEnvironment environment, int displayWidth, int displayHeight)
=> ResponseTool.CreateComputerTool(environment, displayWidth, displayHeight).AsAITool();
diff --git a/dotnet/src/Shared/DiagnosticIds/DiagnosticsIds.cs b/dotnet/src/Shared/DiagnosticIds/DiagnosticsIds.cs
index ea73ec19ec7..f9e0d4b15e3 100644
--- a/dotnet/src/Shared/DiagnosticIds/DiagnosticsIds.cs
+++ b/dotnet/src/Shared/DiagnosticIds/DiagnosticsIds.cs
@@ -31,11 +31,6 @@ internal static class Experiments
// when using the experimental OpenAI APIs.
internal const string AIOpenAIResponses = "OPENAI001";
- // The OpenAI package used OPENAICUA001 for its computer use APIs until 2.13.0, which folded
- // them into OPENAI001 (openai/openai-dotnet#1245). The ID now only gates the computer tool
- // factories in this repo; it is kept so existing suppressions of the preview factory keep working.
- internal const string AIOpenAIComputerUse = "OPENAICUA001";
-
private const string MEAIExperiments = "MEAI001";
}
}
diff --git a/dotnet/src/Shared/Workflows/Execution/WorkflowRunner.cs b/dotnet/src/Shared/Workflows/Execution/WorkflowRunner.cs
index c0c3f882104..97266106351 100644
--- a/dotnet/src/Shared/Workflows/Execution/WorkflowRunner.cs
+++ b/dotnet/src/Shared/Workflows/Execution/WorkflowRunner.cs
@@ -17,7 +17,6 @@ namespace Shared.Workflows;
// Types are for evaluation purposes only and is subject to change or removal in future updates.
#pragma warning disable OPENAI001
-#pragma warning disable OPENAICUA001
#pragma warning disable MEAI001
internal sealed class WorkflowRunner
diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/ComputerToolHostingLoopTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/ComputerToolHostingLoopTests.cs
index 7b79dd82375..36b8a5caf8e 100644
--- a/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/ComputerToolHostingLoopTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/ComputerToolHostingLoopTests.cs
@@ -20,8 +20,7 @@
using OpenAIClient = OpenAI.OpenAIClient;
using OpenAIClientOptions = OpenAI.OpenAIClientOptions;
-#pragma warning disable OPENAI001 // Experimental Responses API surfaces
-#pragma warning disable OPENAICUA001 // FoundryAITool.CreateComputerTool is experimental.
+#pragma warning disable OPENAI001 // Experimental Responses API surfaces, including FoundryAITool.CreateComputerTool
namespace Microsoft.Agents.AI.Foundry.Hosting.UnitTests;
diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/FoundryAIToolTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/FoundryAIToolTests.cs
index b3c21a4665f..dd1fe11b4ac 100644
--- a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/FoundryAIToolTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/FoundryAIToolTests.cs
@@ -16,7 +16,6 @@
using OpenAI.Responses;
#pragma warning disable OPENAI001
-#pragma warning disable OPENAICUA001
namespace Microsoft.Agents.AI.Foundry.UnitTests;
From de8740b62fb34e9eee4b7290b39ee3a06ab60a41 Mon Sep 17 00:00:00 2001
From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com>
Date: Fri, 25 Sep 2026 14:22:18 +0100
Subject: [PATCH 06/13] docs(dotnet): rename computer use samples
GA computer tool is the default going forward, so it takes the
unsuffixed name (Agent_Step27_ComputerUse) and the existing preview
sample becomes Agent_Step15_ComputerUsePreview.
---
dotnet/agent-framework-dotnet.slnx | 4 ++--
dotnet/eng/verify-samples/AgentsSamples.cs | 8 ++++----
.../Agent_Step15_ComputerUsePreview.csproj} | 0
.../Assets/cua_browser_search.jpg | Bin
.../Assets/cua_search_results.jpg | Bin
.../Assets/cua_search_typed.jpg | Bin
.../ComputerUseUtil.cs | 2 +-
.../Program.cs | 4 ++--
.../README.md | 6 +++---
.../Agent_Step27_ComputerUse.csproj} | 2 +-
.../ComputerUseUtil.cs | 2 +-
.../Program.cs | 2 +-
.../README.md | 2 +-
.../02-agents/AgentProviders/foundry/README.md | 4 ++--
14 files changed, 18 insertions(+), 18 deletions(-)
rename dotnet/samples/02-agents/AgentProviders/foundry/{Agent_Step15_ComputerUse/Agent_Step15_ComputerUse.csproj => Agent_Step15_ComputerUsePreview/Agent_Step15_ComputerUsePreview.csproj} (100%)
rename dotnet/samples/02-agents/AgentProviders/foundry/{Agent_Step15_ComputerUse => Agent_Step15_ComputerUsePreview}/Assets/cua_browser_search.jpg (100%)
rename dotnet/samples/02-agents/AgentProviders/foundry/{Agent_Step15_ComputerUse => Agent_Step15_ComputerUsePreview}/Assets/cua_search_results.jpg (100%)
rename dotnet/samples/02-agents/AgentProviders/foundry/{Agent_Step15_ComputerUse => Agent_Step15_ComputerUsePreview}/Assets/cua_search_typed.jpg (100%)
rename dotnet/samples/02-agents/AgentProviders/foundry/{Agent_Step15_ComputerUse => Agent_Step15_ComputerUsePreview}/ComputerUseUtil.cs (98%)
rename dotnet/samples/02-agents/AgentProviders/foundry/{Agent_Step15_ComputerUse => Agent_Step15_ComputerUsePreview}/Program.cs (96%)
rename dotnet/samples/02-agents/AgentProviders/foundry/{Agent_Step15_ComputerUse => Agent_Step15_ComputerUsePreview}/README.md (93%)
rename dotnet/samples/02-agents/AgentProviders/foundry/{Agent_Step27_ComputerUseGA/Agent_Step27_ComputerUseGA.csproj => Agent_Step27_ComputerUse/Agent_Step27_ComputerUse.csproj} (83%)
rename dotnet/samples/02-agents/AgentProviders/foundry/{Agent_Step27_ComputerUseGA => Agent_Step27_ComputerUse}/ComputerUseUtil.cs (99%)
rename dotnet/samples/02-agents/AgentProviders/foundry/{Agent_Step27_ComputerUseGA => Agent_Step27_ComputerUse}/Program.cs (99%)
rename dotnet/samples/02-agents/AgentProviders/foundry/{Agent_Step27_ComputerUseGA => Agent_Step27_ComputerUse}/README.md (97%)
diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx
index 12e236f7034..a45c4f26231 100644
--- a/dotnet/agent-framework-dotnet.slnx
+++ b/dotnet/agent-framework-dotnet.slnx
@@ -157,7 +157,7 @@
-
+
@@ -169,7 +169,7 @@
-
+
diff --git a/dotnet/eng/verify-samples/AgentsSamples.cs b/dotnet/eng/verify-samples/AgentsSamples.cs
index 3438b0e0070..5965e8f2a10 100644
--- a/dotnet/eng/verify-samples/AgentsSamples.cs
+++ b/dotnet/eng/verify-samples/AgentsSamples.cs
@@ -1188,8 +1188,8 @@ internal static class AgentsSamples
new SampleDefinition
{
- Name = "FoundryAgent_Step15_ComputerUse",
- ProjectPath = "samples/02-agents/AgentProviders/foundry/Agent_Step15_ComputerUse",
+ Name = "FoundryAgent_Step15_ComputerUsePreview",
+ ProjectPath = "samples/02-agents/AgentProviders/foundry/Agent_Step15_ComputerUsePreview",
RequiredEnvironmentVariables = ["FOUNDRY_PROJECT_ENDPOINT", "AZURE_AI_COMPUTER_USE_DEPLOYMENT_NAME"],
OptionalEnvironmentVariables = [],
ExpectedOutputDescription = ["The output should show a computer automation session processing simulated browser screenshots with iteration steps and a final response describing search results."],
@@ -1197,8 +1197,8 @@ internal static class AgentsSamples
new SampleDefinition
{
- Name = "FoundryAgent_Step27_ComputerUseGA",
- ProjectPath = "samples/02-agents/AgentProviders/foundry/Agent_Step27_ComputerUseGA",
+ Name = "FoundryAgent_Step27_ComputerUse",
+ ProjectPath = "samples/02-agents/AgentProviders/foundry/Agent_Step27_ComputerUse",
RequiredEnvironmentVariables = ["OPENAI_API_KEY"],
OptionalEnvironmentVariables = ["OPENAI_CHAT_MODEL_NAME"],
ExpectedOutputDescription =
diff --git a/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step15_ComputerUse/Agent_Step15_ComputerUse.csproj b/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step15_ComputerUsePreview/Agent_Step15_ComputerUsePreview.csproj
similarity index 100%
rename from dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step15_ComputerUse/Agent_Step15_ComputerUse.csproj
rename to dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step15_ComputerUsePreview/Agent_Step15_ComputerUsePreview.csproj
diff --git a/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step15_ComputerUse/Assets/cua_browser_search.jpg b/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step15_ComputerUsePreview/Assets/cua_browser_search.jpg
similarity index 100%
rename from dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step15_ComputerUse/Assets/cua_browser_search.jpg
rename to dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step15_ComputerUsePreview/Assets/cua_browser_search.jpg
diff --git a/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step15_ComputerUse/Assets/cua_search_results.jpg b/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step15_ComputerUsePreview/Assets/cua_search_results.jpg
similarity index 100%
rename from dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step15_ComputerUse/Assets/cua_search_results.jpg
rename to dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step15_ComputerUsePreview/Assets/cua_search_results.jpg
diff --git a/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step15_ComputerUse/Assets/cua_search_typed.jpg b/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step15_ComputerUsePreview/Assets/cua_search_typed.jpg
similarity index 100%
rename from dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step15_ComputerUse/Assets/cua_search_typed.jpg
rename to dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step15_ComputerUsePreview/Assets/cua_search_typed.jpg
diff --git a/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step15_ComputerUse/ComputerUseUtil.cs b/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step15_ComputerUsePreview/ComputerUseUtil.cs
similarity index 98%
rename from dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step15_ComputerUse/ComputerUseUtil.cs
rename to dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step15_ComputerUsePreview/ComputerUseUtil.cs
index d1df3e7ccd3..b8f3e77b77e 100644
--- a/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step15_ComputerUse/ComputerUseUtil.cs
+++ b/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step15_ComputerUsePreview/ComputerUseUtil.cs
@@ -3,7 +3,7 @@
using Microsoft.Extensions.AI;
using OpenAI.Responses;
-namespace Demo.ComputerUse;
+namespace Demo.ComputerUsePreview;
///
/// Enum for tracking the state of the simulated web search flow.
diff --git a/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step15_ComputerUse/Program.cs b/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step15_ComputerUsePreview/Program.cs
similarity index 96%
rename from dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step15_ComputerUse/Program.cs
rename to dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step15_ComputerUsePreview/Program.cs
index 865dfac806d..65788e79f24 100644
--- a/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step15_ComputerUse/Program.cs
+++ b/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step15_ComputerUsePreview/Program.cs
@@ -1,10 +1,10 @@
// Copyright (c) Microsoft. All rights reserved.
-// This sample shows how to use the Computer Use tool with AIProjectClient.AsAIAgent(...).
+// This sample shows how to use the preview Computer Use tool (computer_use_preview) with AIProjectClient.AsAIAgent(...).
using Azure.AI.Projects;
using Azure.Identity;
-using Demo.ComputerUse;
+using Demo.ComputerUsePreview;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Foundry;
using Microsoft.Extensions.AI;
diff --git a/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step15_ComputerUse/README.md b/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step15_ComputerUsePreview/README.md
similarity index 93%
rename from dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step15_ComputerUse/README.md
rename to dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step15_ComputerUsePreview/README.md
index 0cb9cf9df91..fdc01825163 100644
--- a/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step15_ComputerUse/README.md
+++ b/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step15_ComputerUsePreview/README.md
@@ -1,6 +1,6 @@
-# Computer Use with the Responses API
+# Computer Use (preview) with the Responses API
-This sample shows how to use the Computer Use tool with `AIProjectClient.AsAIAgent(...)`.
+This sample shows how to use the preview Computer Use tool with `AIProjectClient.AsAIAgent(...)`.
## What this sample demonstrates
@@ -8,7 +8,7 @@ This sample shows how to use the Computer Use tool with `AIProjectClient.AsAIAge
- Processing computer call actions (click, type, key press)
- Managing the computer use interaction loop with screenshots
-For the generally available (GA) `computer` tool, created with the parameterless `FoundryAITool.CreateComputerTool()` and returning batched actions, see [Agent_Step27_ComputerUseGA](../Agent_Step27_ComputerUseGA/).
+For the generally available (GA) `computer` tool, created with the parameterless `FoundryAITool.CreateComputerTool()` and returning batched actions, see [Agent_Step27_ComputerUse](../Agent_Step27_ComputerUse/).
For more information, see [Use the computer tool](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/tools/computer-use?pivots=csharp).
diff --git a/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step27_ComputerUseGA/Agent_Step27_ComputerUseGA.csproj b/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step27_ComputerUse/Agent_Step27_ComputerUse.csproj
similarity index 83%
rename from dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step27_ComputerUseGA/Agent_Step27_ComputerUseGA.csproj
rename to dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step27_ComputerUse/Agent_Step27_ComputerUse.csproj
index cd45bdbe091..a80bec83f01 100644
--- a/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step27_ComputerUseGA/Agent_Step27_ComputerUseGA.csproj
+++ b/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step27_ComputerUse/Agent_Step27_ComputerUse.csproj
@@ -20,7 +20,7 @@
-
+
diff --git a/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step27_ComputerUseGA/ComputerUseUtil.cs b/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step27_ComputerUse/ComputerUseUtil.cs
similarity index 99%
rename from dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step27_ComputerUseGA/ComputerUseUtil.cs
rename to dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step27_ComputerUse/ComputerUseUtil.cs
index 25a54ee55ef..261d56bee12 100644
--- a/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step27_ComputerUseGA/ComputerUseUtil.cs
+++ b/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step27_ComputerUse/ComputerUseUtil.cs
@@ -4,7 +4,7 @@
using System.Text.Json;
using OpenAI.Responses;
-namespace Demo.ComputerUseGA;
+namespace Demo.ComputerUse;
///
/// Enum for tracking the state of the simulated web search flow.
diff --git a/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step27_ComputerUseGA/Program.cs b/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step27_ComputerUse/Program.cs
similarity index 99%
rename from dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step27_ComputerUseGA/Program.cs
rename to dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step27_ComputerUse/Program.cs
index b58393d1bd0..4317f636b42 100644
--- a/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step27_ComputerUseGA/Program.cs
+++ b/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step27_ComputerUse/Program.cs
@@ -8,7 +8,7 @@
// OpenAI Responses API. The Foundry setup is kept below, commented out: once Foundry supports the GA tool, swap the
// two agent setups and the rest of the sample works unchanged.
-using Demo.ComputerUseGA;
+using Demo.ComputerUse;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Foundry;
using Microsoft.Extensions.AI;
diff --git a/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step27_ComputerUseGA/README.md b/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step27_ComputerUse/README.md
similarity index 97%
rename from dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step27_ComputerUseGA/README.md
rename to dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step27_ComputerUse/README.md
index edf0d20eb14..c261e5eea96 100644
--- a/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step27_ComputerUseGA/README.md
+++ b/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step27_ComputerUse/README.md
@@ -9,7 +9,7 @@ This sample shows how to use the generally available (GA) computer tool with an
- Returning screenshots as inline image data, so no file upload or cleanup is needed
- Acknowledging pending safety checks in the `computer_call_output`
-For the preview `computer_use_preview` tool, which takes an environment and display size and returns a single `action` per call, see [Agent_Step15_ComputerUse](../Agent_Step15_ComputerUse/).
+For the preview `computer_use_preview` tool, which takes an environment and display size and returns a single `action` per call, see [Agent_Step15_ComputerUsePreview](../Agent_Step15_ComputerUsePreview/).
| | GA tool (this sample) | Preview tool (Step15) |
| --- | --- | --- |
diff --git a/dotnet/samples/02-agents/AgentProviders/foundry/README.md b/dotnet/samples/02-agents/AgentProviders/foundry/README.md
index 837c351ba34..dec2bfac863 100644
--- a/dotnet/samples/02-agents/AgentProviders/foundry/README.md
+++ b/dotnet/samples/02-agents/AgentProviders/foundry/README.md
@@ -63,7 +63,7 @@ Some samples require extra tool-specific environment variables. See each sample
| [Middleware](./Agent_Step12_Middleware/) | Multiple middleware layers |
| [Plugins](./Agent_Step13_Plugins/) | Plugins with dependency injection |
| [Code interpreter](./Agent_Step14_CodeInterpreter/) | Code interpreter tool |
-| [Computer use](./Agent_Step15_ComputerUse/) | Computer use tool (preview `computer_use_preview`) |
+| [Computer use (preview)](./Agent_Step15_ComputerUsePreview/) | Preview `computer_use_preview` tool |
| [File search](./Agent_Step16_FileSearch/) | File search tool |
| [OpenAPI tools](./Agent_Step17_OpenAPITools/) | OpenAPI tools |
| [Bing custom search](./Agent_Step18_BingCustomSearch/) | Bing Custom Search tool |
@@ -75,7 +75,7 @@ Some samples require extra tool-specific environment variables. See each sample
| [Code interpreter file download](./Agent_Step24_CodeInterpreterFileDownload/) | Download container files generated by code interpreter |
| [Foundry toolbox via MCP](./Agent_Step25_FoundryToolboxMcp/README.md) | Use a Foundry Toolbox from a non-hosted agent via its MCP endpoint |
| [Foundry toolbox MCP skills](./Agent_Step26_FoundryToolboxMcpSkills/README.md) | Use a Foundry Toolbox with MCP-based skills discovery (SEP-2640) via AIContextProviders |
-| [Computer use (GA)](./Agent_Step27_ComputerUseGA/) | GA `computer` tool with batched actions (runs on public OpenAI until Foundry supports it) |
+| [Computer use](./Agent_Step27_ComputerUse/) | GA `computer` tool with batched actions (runs on public OpenAI until Foundry supports it) |
## Running the samples
From b2370618d383c8e8f689a40e2a6284ffb84c406e Mon Sep 17 00:00:00 2001
From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com>
Date: Fri, 25 Sep 2026 15:37:00 +0100
Subject: [PATCH 07/13] docs(dotnet): renumber GA computer use sample to
Step15.2
Keeps the GA sample next to its preview counterpart, following the
existing Step02.1/Step02.2 numbering.
---
dotnet/agent-framework-dotnet.slnx | 2 +-
dotnet/eng/verify-samples/AgentsSamples.cs | 4 ++--
.../Agent_Step15.2_ComputerUse.csproj} | 0
.../ComputerUseUtil.cs | 0
.../Program.cs | 0
.../README.md | 0
.../foundry/Agent_Step15_ComputerUsePreview/README.md | 2 +-
dotnet/samples/02-agents/AgentProviders/foundry/README.md | 2 +-
8 files changed, 5 insertions(+), 5 deletions(-)
rename dotnet/samples/02-agents/AgentProviders/foundry/{Agent_Step27_ComputerUse/Agent_Step27_ComputerUse.csproj => Agent_Step15.2_ComputerUse/Agent_Step15.2_ComputerUse.csproj} (100%)
rename dotnet/samples/02-agents/AgentProviders/foundry/{Agent_Step27_ComputerUse => Agent_Step15.2_ComputerUse}/ComputerUseUtil.cs (100%)
rename dotnet/samples/02-agents/AgentProviders/foundry/{Agent_Step27_ComputerUse => Agent_Step15.2_ComputerUse}/Program.cs (100%)
rename dotnet/samples/02-agents/AgentProviders/foundry/{Agent_Step27_ComputerUse => Agent_Step15.2_ComputerUse}/README.md (100%)
diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx
index a45c4f26231..d37af2bc006 100644
--- a/dotnet/agent-framework-dotnet.slnx
+++ b/dotnet/agent-framework-dotnet.slnx
@@ -158,6 +158,7 @@
+
@@ -169,7 +170,6 @@
-
diff --git a/dotnet/eng/verify-samples/AgentsSamples.cs b/dotnet/eng/verify-samples/AgentsSamples.cs
index 5965e8f2a10..3749f1c2ca2 100644
--- a/dotnet/eng/verify-samples/AgentsSamples.cs
+++ b/dotnet/eng/verify-samples/AgentsSamples.cs
@@ -1197,8 +1197,8 @@ internal static class AgentsSamples
new SampleDefinition
{
- Name = "FoundryAgent_Step27_ComputerUse",
- ProjectPath = "samples/02-agents/AgentProviders/foundry/Agent_Step27_ComputerUse",
+ Name = "FoundryAgent_Step15.2_ComputerUse",
+ ProjectPath = "samples/02-agents/AgentProviders/foundry/Agent_Step15.2_ComputerUse",
RequiredEnvironmentVariables = ["OPENAI_API_KEY"],
OptionalEnvironmentVariables = ["OPENAI_CHAT_MODEL_NAME"],
ExpectedOutputDescription =
diff --git a/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step27_ComputerUse/Agent_Step27_ComputerUse.csproj b/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step15.2_ComputerUse/Agent_Step15.2_ComputerUse.csproj
similarity index 100%
rename from dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step27_ComputerUse/Agent_Step27_ComputerUse.csproj
rename to dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step15.2_ComputerUse/Agent_Step15.2_ComputerUse.csproj
diff --git a/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step27_ComputerUse/ComputerUseUtil.cs b/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step15.2_ComputerUse/ComputerUseUtil.cs
similarity index 100%
rename from dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step27_ComputerUse/ComputerUseUtil.cs
rename to dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step15.2_ComputerUse/ComputerUseUtil.cs
diff --git a/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step27_ComputerUse/Program.cs b/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step15.2_ComputerUse/Program.cs
similarity index 100%
rename from dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step27_ComputerUse/Program.cs
rename to dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step15.2_ComputerUse/Program.cs
diff --git a/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step27_ComputerUse/README.md b/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step15.2_ComputerUse/README.md
similarity index 100%
rename from dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step27_ComputerUse/README.md
rename to dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step15.2_ComputerUse/README.md
diff --git a/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step15_ComputerUsePreview/README.md b/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step15_ComputerUsePreview/README.md
index fdc01825163..ecab6ae76a4 100644
--- a/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step15_ComputerUsePreview/README.md
+++ b/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step15_ComputerUsePreview/README.md
@@ -8,7 +8,7 @@ This sample shows how to use the preview Computer Use tool with `AIProjectClient
- Processing computer call actions (click, type, key press)
- Managing the computer use interaction loop with screenshots
-For the generally available (GA) `computer` tool, created with the parameterless `FoundryAITool.CreateComputerTool()` and returning batched actions, see [Agent_Step27_ComputerUse](../Agent_Step27_ComputerUse/).
+For the generally available (GA) `computer` tool, created with the parameterless `FoundryAITool.CreateComputerTool()` and returning batched actions, see [Agent_Step15.2_ComputerUse](../Agent_Step15.2_ComputerUse/).
For more information, see [Use the computer tool](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/tools/computer-use?pivots=csharp).
diff --git a/dotnet/samples/02-agents/AgentProviders/foundry/README.md b/dotnet/samples/02-agents/AgentProviders/foundry/README.md
index dec2bfac863..45adb36b3e3 100644
--- a/dotnet/samples/02-agents/AgentProviders/foundry/README.md
+++ b/dotnet/samples/02-agents/AgentProviders/foundry/README.md
@@ -64,6 +64,7 @@ Some samples require extra tool-specific environment variables. See each sample
| [Plugins](./Agent_Step13_Plugins/) | Plugins with dependency injection |
| [Code interpreter](./Agent_Step14_CodeInterpreter/) | Code interpreter tool |
| [Computer use (preview)](./Agent_Step15_ComputerUsePreview/) | Preview `computer_use_preview` tool |
+| [Computer use](./Agent_Step15.2_ComputerUse/) | GA `computer` tool with batched actions (runs on public OpenAI until Foundry supports it) |
| [File search](./Agent_Step16_FileSearch/) | File search tool |
| [OpenAPI tools](./Agent_Step17_OpenAPITools/) | OpenAPI tools |
| [Bing custom search](./Agent_Step18_BingCustomSearch/) | Bing Custom Search tool |
@@ -75,7 +76,6 @@ Some samples require extra tool-specific environment variables. See each sample
| [Code interpreter file download](./Agent_Step24_CodeInterpreterFileDownload/) | Download container files generated by code interpreter |
| [Foundry toolbox via MCP](./Agent_Step25_FoundryToolboxMcp/README.md) | Use a Foundry Toolbox from a non-hosted agent via its MCP endpoint |
| [Foundry toolbox MCP skills](./Agent_Step26_FoundryToolboxMcpSkills/README.md) | Use a Foundry Toolbox with MCP-based skills discovery (SEP-2640) via AIContextProviders |
-| [Computer use](./Agent_Step27_ComputerUse/) | GA `computer` tool with batched actions (runs on public OpenAI until Foundry supports it) |
## Running the samples
From adec663aace161f5277e27baadae8deac91684fc Mon Sep 17 00:00:00 2001
From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com>
Date: Fri, 25 Sep 2026 16:07:43 +0100
Subject: [PATCH 08/13] docs(dotnet): narrow computer call replay caveat to
action
The service rejects a resent GA computer_call for its "action": null,
which is what the known limitation now describes; the known-issue test
asserts only that key.
---
.../foundry/Agent_Step15.2_ComputerUse/README.md | 3 +--
.../ComputerToolItemConverter.cs | 8 ++++----
.../src/Microsoft.Agents.AI.Foundry/FoundryAITool.cs | 2 +-
.../ComputerToolHostingLoopTests.cs | 6 +++---
.../InputConverterTests.cs | 12 +++++-------
5 files changed, 14 insertions(+), 17 deletions(-)
diff --git a/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step15.2_ComputerUse/README.md b/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step15.2_ComputerUse/README.md
index c261e5eea96..60c4c987869 100644
--- a/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step15.2_ComputerUse/README.md
+++ b/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step15.2_ComputerUse/README.md
@@ -7,7 +7,6 @@ This sample shows how to use the generally available (GA) computer tool with an
- Using the parameterless `FoundryAITool.CreateComputerTool()` to add the GA `computer` tool (`{"type":"computer"}`)
- Running every action of a batched `computer_call` (the GA call returns an ordered `actions` list) before sending back one screenshot
- Returning screenshots as inline image data, so no file upload or cleanup is needed
-- Acknowledging pending safety checks in the `computer_call_output`
For the preview `computer_use_preview` tool, which takes an environment and display size and returns a single `action` per call, see [Agent_Step15_ComputerUsePreview](../Agent_Step15_ComputerUsePreview/).
@@ -25,7 +24,7 @@ Microsoft Foundry does not accept the GA `computer` tool type yet. Until it does
## Known limitation
-The sample continues each turn from the stored response, so only the new `computer_call_output` is sent. Sending a GA `computer_call` back to the model instead (for example with stored responses disabled and the history kept locally) currently fails. The OpenAI .NET SDK models only the preview item and adds `"action": null` and `"pending_safety_checks": []`, which the Responses API rejects for the GA tool.
+The sample continues each turn from the stored response, so only the new `computer_call_output` is sent. Sending a GA `computer_call` back to the model instead (for example with stored responses disabled and the history kept locally) currently fails. The OpenAI .NET SDK models only the preview item and adds `"action": null`, which the Responses API rejects for the GA tool.
## How the simulation works
diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/ComputerToolItemConverter.cs b/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/ComputerToolItemConverter.cs
index 273ec2c1d97..2899bcb3abd 100644
--- a/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/ComputerToolItemConverter.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/ComputerToolItemConverter.cs
@@ -131,10 +131,10 @@ private static void CopyActions(IList? source, IList(agentServerJson, "computer_call", callId);
return new ChatMessage(ChatRole.Assistant, [new ToolCallContent(callId) { RawRepresentation = item }]);
}
diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry/FoundryAITool.cs b/dotnet/src/Microsoft.Agents.AI.Foundry/FoundryAITool.cs
index bdb61a6b773..de01d784f55 100644
--- a/dotnet/src/Microsoft.Agents.AI.Foundry/FoundryAITool.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Foundry/FoundryAITool.cs
@@ -148,7 +148,7 @@ public static AITool CreateHostedMcpToolbox(string toolboxName, string? version
///
/// Known limitation: the OpenAI .NET SDK models only the preview computer_call item, so sending a GA
/// computer_call back to the model (for example with stored responses disabled and the history kept locally)
- /// adds "action": null and "pending_safety_checks": [], which the Responses API rejects for the GA tool.
+ /// adds "action": null, which the Responses API rejects for the GA tool.
/// Continuing the conversation from the stored response (the default for a Responses agent session) only sends
/// the new computer_call_output and is not affected.
///
diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/ComputerToolHostingLoopTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/ComputerToolHostingLoopTests.cs
index 36b8a5caf8e..3f0861c1fa0 100644
--- a/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/ComputerToolHostingLoopTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/ComputerToolHostingLoopTests.cs
@@ -148,9 +148,9 @@ private static void AssertReplayedComputerCall(JsonElement call, string expected
Assert.False(call.TryGetProperty("response_id", out _));
Assert.False(call.TryGetProperty("agent_reference", out _));
- // OpenAI .NET 2.13.0 re-serializes the GA call with "action": null and "pending_safety_checks": [], and the
- // service rejects both on replay (see the known-issue test in InputConverterTests). These tests check what MAF
- // controls: a populated single action here would mean the batch was rewritten.
+ // OpenAI .NET 2.13.0 re-serializes the GA call with "action": null, which the service rejects on replay (see the
+ // known-issue test in InputConverterTests). These tests check what MAF controls: a populated single action here
+ // would mean the batch was rewritten.
if (call.TryGetProperty("action", out JsonElement action))
{
Assert.Equal(JsonValueKind.Null, action.ValueKind);
diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/InputConverterTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/InputConverterTests.cs
index 31074c3bef4..71f541bbaa4 100644
--- a/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/InputConverterTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/InputConverterTests.cs
@@ -1603,7 +1603,7 @@ public void ConvertOutputItemsToMessages_HistoryComputerCallOutput_ProducesToolR
}
[Fact]
- public void ConvertOutputItemsToMessages_GaComputerCall_ReplaysWithPreviewOnlyKeysKnownIssue()
+ public void ConvertOutputItemsToMessages_GaComputerCall_ReplaysWithNullActionKnownIssue()
{
// Arrange
var history = ReadOutputItem(GaComputerCallJson);
@@ -1612,15 +1612,13 @@ public void ConvertOutputItemsToMessages_GaComputerCall_ReplaysWithPreviewOnlyKe
var messages = InputConverter.ConvertOutputItemsToMessages([history]);
// Assert: documents a known OpenAI .NET 2.13.0 behavior, not a desired one. ComputerCallResponseItem models the
- // preview item: it has no typed "actions" and always writes "action" and "pending_safety_checks", so a replayed
- // GA call carries both keys next to "actions". The Responses API rejects each of them for the GA tool:
- // "action": null -> "Computer call input must include exactly one of `action` or `actions`."
- // "pending_safety_checks" -> "`pending_safety_checks` is not supported for the "computer" tool."
- // When this test starts failing, OpenAI .NET changed the shape: revisit the caveat in ComputerToolItemConverter.
+ // preview item: it has no typed "actions" and always writes "action", so a replayed GA call carries
+ // "action": null next to "actions". The Responses API rejects that with "Computer call input must include
+ // exactly one of `action` or `actions`." When this test starts failing, OpenAI .NET changed the shape: revisit
+ // the caveat in ComputerToolItemConverter.
var item = Assert.IsType(Assert.Single(Assert.Single(messages).Contents).RawRepresentation);
string json = ModelReaderWriter.Write(item, ModelReaderWriterOptions.Json).ToString();
Assert.Contains("\"action\":null", json);
- Assert.Contains("\"pending_safety_checks\":[]", json);
Assert.Contains("\"actions\":[", json);
}
From b729ccbe50f07432d78c8e7a5ff323ecfc9cb5ca Mon Sep 17 00:00:00 2001
From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com>
Date: Fri, 25 Sep 2026 17:30:54 +0100
Subject: [PATCH 09/13] fix(dotnet): resend GA computer calls without action
OpenAI .NET models only the preview computer_call and writes
"action": null next to "actions" when a GA call is sent back, which
the Responses API rejects. Foundry hosting resends history on every
turn, so the turn after a computer call failed.
Replayed calls now use a ComputerCallResponseItem subclass that writes
the call JSON as received; typed properties stay available.
---
.../ComputerToolItemConverter.cs | 84 ++++++++++++++-----
.../FoundryAITool.cs | 10 +--
.../ComputerToolHostingLoopTests.cs | 11 +--
.../InputConverterTests.cs | 33 +++++---
4 files changed, 92 insertions(+), 46 deletions(-)
diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/ComputerToolItemConverter.cs b/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/ComputerToolItemConverter.cs
index 2899bcb3abd..d547c1a92c4 100644
--- a/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/ComputerToolItemConverter.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/ComputerToolItemConverter.cs
@@ -9,6 +9,7 @@
using Azure.AI.AgentServer.Responses.Models;
using Microsoft.Extensions.AI;
using ChatMessage = Microsoft.Extensions.AI.ChatMessage;
+using ComputerCallAction = OpenAI.Responses.ComputerCallAction;
using OpenAIComputerCallOutputResponseItem = OpenAI.Responses.ComputerCallOutputResponseItem;
using OpenAIComputerCallResponseItem = OpenAI.Responses.ComputerCallResponseItem;
using OpenAIContext = OpenAI.OpenAIContext;
@@ -30,7 +31,8 @@ namespace Microsoft.Agents.AI.Foundry.Hosting;
///
/// The OpenAI item is carried as the of a or
/// . The MEAI OpenAI Responses client sends any content whose raw representation is a
-/// to the model unchanged, which is what keeps the batch intact on replay.
+/// to the model unchanged, which is what keeps the batch intact on replay. Computer
+/// calls sent back to the model are a , which writes the GA shape.
///
///
internal static class ComputerToolItemConverter
@@ -71,12 +73,9 @@ private static T ReadAgentServerItem(BinaryData openAIJson, string itemId, st
where T : OutputItem
{
// The builder id replaces the model's item id: AgentServer requires its own {prefix}_{50 chars} id format and
- // does not stamp one on EmitAdded. A null "action" is how OpenAI .NET writes a GA call; leaving it out keeps
- // the emitted item in the GA shape (actions only). A populated preview "action" is kept.
- BinaryData agentServerJson = RewriteTopLevelProperties(
- openAIJson,
- static property => property.NameEquals("action"u8) && property.Value.ValueKind == JsonValueKind.Null,
- replacementId: itemId);
+ // does not stamp one on EmitAdded. Leaving out a null "action" keeps the emitted item in the GA shape (actions
+ // only).
+ BinaryData agentServerJson = RewriteTopLevelProperties(openAIJson, IsNullAction, replacementId: itemId);
return ModelReaderWriter.Read(agentServerJson, ModelReaderWriterOptions.Json, AzureAIAgentServerResponsesContext.Default) as T
?? throw new InvalidOperationException(
@@ -130,30 +129,31 @@ private static void CopyActions(IList? source, IList(agentServerJson, "computer_call", callId);
- return new ChatMessage(ChatRole.Assistant, [new ToolCallContent(callId) { RawRepresentation = item }]);
+ // OpenAI .NET (2.13.0 and 2.14.0) models only the preview item: ComputerCallResponseItem always writes "action",
+ // so a GA call sent back as that type would carry "action": null next to "actions", which the Responses API
+ // rejects. Its Patch API cannot remove "action" (NullReferenceException), so the item is wrapped in a subclass
+ // that writes this JSON as is. The JSON also drops a null "action" in case a caller sent one.
+ BinaryData openAIJson = RewriteTopLevelProperties(
+ agentServerJson,
+ static property => IsAgentServerEnvelopeProperty(property) || IsNullAction(property));
+ var item = ReadOpenAIItem(openAIJson, "computer_call", callId);
+ return new ChatMessage(
+ ChatRole.Assistant,
+ [new ToolCallContent(callId) { RawRepresentation = new ResentComputerCallResponseItem(item, openAIJson) }]);
}
private static ChatMessage ToToolResultMessage(BinaryData agentServerJson, string callId)
{
- var item = ReadOpenAIItem(agentServerJson, "computer_call_output", callId);
+ BinaryData openAIJson = RewriteTopLevelProperties(agentServerJson, IsAgentServerEnvelopeProperty);
+ var item = ReadOpenAIItem(openAIJson, "computer_call_output", callId);
return new ChatMessage(ChatRole.Tool, [new ToolResultContent(callId) { RawRepresentation = item }]);
}
- private static T ReadOpenAIItem(BinaryData agentServerJson, string itemType, string callId)
- where T : OpenAIResponseItem
- {
- BinaryData openAIJson = RewriteTopLevelProperties(agentServerJson, IsAgentServerEnvelopeProperty);
-
- return ModelReaderWriter.Read(openAIJson, ModelReaderWriterOptions.Json, OpenAIContext.Default) as T
+ private static T ReadOpenAIItem(BinaryData openAIJson, string itemType, string callId)
+ where T : OpenAIResponseItem =>
+ ModelReaderWriter.Read(openAIJson, ModelReaderWriterOptions.Json, OpenAIContext.Default) as T
?? throw new InvalidOperationException(
$"The {itemType} item for call '{callId}' could not be converted to the OpenAI {typeof(T).Name} representation.");
- }
///
/// AgentServer adds these fields to stored output items. They are not part of the model's item schema, and the
@@ -164,6 +164,12 @@ private static bool IsAgentServerEnvelopeProperty(JsonProperty property) =>
property.NameEquals("agent_reference"u8) ||
property.NameEquals("created_by"u8);
+ ///
+ /// A null "action" is how OpenAI .NET writes a GA call. A populated preview "action" is kept.
+ ///
+ private static bool IsNullAction(JsonProperty property) =>
+ property.NameEquals("action"u8) && property.Value.ValueKind == JsonValueKind.Null;
+
///
/// Copies a JSON object, leaving out the top-level properties selected by and, when
/// is set, replacing its id.
@@ -195,4 +201,38 @@ private static BinaryData RewriteTopLevelProperties(BinaryData json, Func
+ /// A that is sent back to the model with the JSON it was read from.
+ ///
+ ///
+ /// Callers still see the typed OpenAI item (CallId, Id, Status, Action), but the item is
+ /// written as the computer call it was read from, so a GA call keeps its actions batch without the
+ /// "action": null the base type would add. Changes to the typed properties are not written. Remove this
+ /// once OpenAI .NET models the GA computer call.
+ ///
+ private sealed class ResentComputerCallResponseItem : OpenAIComputerCallResponseItem
+ {
+ private readonly BinaryData _json;
+
+ public ResentComputerCallResponseItem(OpenAIComputerCallResponseItem item, BinaryData json)
+ // The only public constructor requires an action, which a GA call does not have. A placeholder satisfies
+ // it and is replaced right away, so Action reads null for a GA call as it does on the parsed item.
+ : base(item.CallId, item.Action ?? ComputerCallAction.CreateScreenshotAction(), item.PendingSafetyChecks)
+ {
+ this._json = json;
+ this.Action = item.Action;
+ this.Status = item.Status;
+ this.Id = item.Id;
+ }
+
+ protected override void JsonModelWriteCore(Utf8JsonWriter writer, ModelReaderWriterOptions options)
+ {
+ using JsonDocument document = JsonDocument.Parse(this._json);
+ foreach (JsonProperty property in document.RootElement.EnumerateObject())
+ {
+ property.WriteTo(writer);
+ }
+ }
+ }
}
diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry/FoundryAITool.cs b/dotnet/src/Microsoft.Agents.AI.Foundry/FoundryAITool.cs
index de01d784f55..000cb11deb4 100644
--- a/dotnet/src/Microsoft.Agents.AI.Foundry/FoundryAITool.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Foundry/FoundryAITool.cs
@@ -146,11 +146,11 @@ public static AITool CreateHostedMcpToolbox(string toolboxName, string? version
/// use the overload instead.
///
///
- /// Known limitation: the OpenAI .NET SDK models only the preview computer_call item, so sending a GA
- /// computer_call back to the model (for example with stored responses disabled and the history kept locally)
- /// adds "action": null, which the Responses API rejects for the GA tool.
- /// Continuing the conversation from the stored response (the default for a Responses agent session) only sends
- /// the new computer_call_output and is not affected.
+ /// Known limitation: the OpenAI .NET SDK models only the preview computer_call item, so an agent that keeps its
+ /// history locally and sends a GA computer_call back to the model (for example with stored responses disabled)
+ /// adds "action": null, which the Responses API rejects for the GA tool. Continuing the conversation from the
+ /// stored response (the default for a Responses agent session) only sends the new computer_call_output and is
+ /// not affected, and Foundry hosting sends computer calls back in the GA shape.
///
///
/// An for the GA computer tool.
diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/ComputerToolHostingLoopTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/ComputerToolHostingLoopTests.cs
index 3f0861c1fa0..2e05ccc7e64 100644
--- a/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/ComputerToolHostingLoopTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/ComputerToolHostingLoopTests.cs
@@ -148,13 +148,10 @@ private static void AssertReplayedComputerCall(JsonElement call, string expected
Assert.False(call.TryGetProperty("response_id", out _));
Assert.False(call.TryGetProperty("agent_reference", out _));
- // OpenAI .NET 2.13.0 re-serializes the GA call with "action": null, which the service rejects on replay (see the
- // known-issue test in InputConverterTests). These tests check what MAF controls: a populated single action here
- // would mean the batch was rewritten.
- if (call.TryGetProperty("action", out JsonElement action))
- {
- Assert.Equal(JsonValueKind.Null, action.ValueKind);
- }
+ // OpenAI .NET 2.13.0 models only the preview item and would write "action": null next to "actions", which the
+ // Responses API rejects ("Computer call input must include exactly one of `action` or `actions`."). The resent
+ // GA call must carry the batch only.
+ Assert.False(call.TryGetProperty("action", out _), "A resent GA computer_call must not carry an \"action\" key.");
}
private static void AssertReplayedComputerCallOutput(JsonElement output)
diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/InputConverterTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/InputConverterTests.cs
index 71f541bbaa4..453a5327664 100644
--- a/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/InputConverterTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/InputConverterTests.cs
@@ -11,6 +11,7 @@
using OpenAIComputerCallActionKind = OpenAI.Responses.ComputerCallActionKind;
using OpenAIComputerCallOutputResponseItem = OpenAI.Responses.ComputerCallOutputResponseItem;
using OpenAIComputerCallResponseItem = OpenAI.Responses.ComputerCallResponseItem;
+using OpenAIComputerCallStatus = OpenAI.Responses.ComputerCallStatus;
#pragma warning disable OPENAI001 // Experimental Responses API surfaces
@@ -1490,7 +1491,7 @@ public void ConvertInputToMessages_GaComputerCall_ProducesAssistantToolCallCarry
Assert.Equal(typeof(ToolCallContent), toolCall.GetType());
Assert.Equal("call_cu_1", ((ToolCallContent)toolCall).CallId);
- var item = Assert.IsType(toolCall.RawRepresentation);
+ var item = Assert.IsAssignableFrom(toolCall.RawRepresentation);
Assert.Equal("cu_input_1", item.Id);
Assert.Equal("call_cu_1", item.CallId);
@@ -1546,9 +1547,14 @@ public void ConvertInputToMessages_PreviewComputerCall_KeepsSingleAction()
// Assert
var toolCall = Assert.Single(Assert.Single(messages).Contents);
- var item = Assert.IsType(toolCall.RawRepresentation);
+ var item = Assert.IsAssignableFrom(toolCall.RawRepresentation);
Assert.Equal("call_preview_1", item.CallId);
Assert.Equal(OpenAIComputerCallActionKind.Click, item.Action.Kind);
+
+ // A preview call is resent with its single action, not rewritten to the GA shape.
+ string json = ModelReaderWriter.Write(item, ModelReaderWriterOptions.Json).ToString();
+ Assert.Contains("\"action\":{\"type\":\"click\"", json);
+ Assert.DoesNotContain("\"actions\"", json);
}
[Fact]
@@ -1568,7 +1574,7 @@ public void ConvertOutputItemsToMessages_HistoryComputerCall_DropsAgentServerEnv
// Assert
var message = Assert.Single(messages);
Assert.Equal(ChatRole.Assistant, message.Role);
- var item = Assert.IsType(Assert.Single(message.Contents).RawRepresentation);
+ var item = Assert.IsAssignableFrom(Assert.Single(message.Contents).RawRepresentation);
Assert.Equal("cu_input_1", item.Id);
string json = ModelReaderWriter.Write(item, ModelReaderWriterOptions.Json).ToString();
@@ -1603,7 +1609,7 @@ public void ConvertOutputItemsToMessages_HistoryComputerCallOutput_ProducesToolR
}
[Fact]
- public void ConvertOutputItemsToMessages_GaComputerCall_ReplaysWithNullActionKnownIssue()
+ public void ConvertOutputItemsToMessages_GaComputerCall_ReplaysWithoutNullAction()
{
// Arrange
var history = ReadOutputItem(GaComputerCallJson);
@@ -1611,15 +1617,18 @@ public void ConvertOutputItemsToMessages_GaComputerCall_ReplaysWithNullActionKno
// Act
var messages = InputConverter.ConvertOutputItemsToMessages([history]);
- // Assert: documents a known OpenAI .NET 2.13.0 behavior, not a desired one. ComputerCallResponseItem models the
- // preview item: it has no typed "actions" and always writes "action", so a replayed GA call carries
- // "action": null next to "actions". The Responses API rejects that with "Computer call input must include
- // exactly one of `action` or `actions`." When this test starts failing, OpenAI .NET changed the shape: revisit
- // the caveat in ComputerToolItemConverter.
- var item = Assert.IsType(Assert.Single(Assert.Single(messages).Contents).RawRepresentation);
+ // Assert: OpenAI .NET 2.13.0 models only the preview item and would write "action": null next to "actions",
+ // which the Responses API rejects ("Computer call input must include exactly one of `action` or `actions`.").
+ // The resent item keeps the typed surface of the SDK item but writes the GA shape.
+ var item = Assert.IsAssignableFrom(Assert.Single(Assert.Single(messages).Contents).RawRepresentation);
+ Assert.Equal("cu_input_1", item.Id);
+ Assert.Equal("call_cu_1", item.CallId);
+ Assert.Equal(OpenAIComputerCallStatus.Completed, item.Status);
+ Assert.Null(item.Action);
+
string json = ModelReaderWriter.Write(item, ModelReaderWriterOptions.Json).ToString();
- Assert.Contains("\"action\":null", json);
- Assert.Contains("\"actions\":[", json);
+ Assert.DoesNotContain("\"action\"", json);
+ Assert.Contains("\"actions\":[{\"type\":\"click\"", json);
}
private static OutputItem ReadOutputItem(string json) =>
From 639a3a2f877987fb64e6146805b4ca9acfa96b3f Mon Sep 17 00:00:00 2001
From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com>
Date: Mon, 28 Sep 2026 15:55:51 +0100
Subject: [PATCH 10/13] docs(dotnet): move GA computer use sample to Azure
providers
The Foundry project Responses endpoint does not accept the GA
computer tool yet, so the GA sample now targets the Azure OpenAI
endpoint with Entra ID as Agent_With_AzureOpenAIResponses_ComputerUse.
The Foundry sample keeps the preview tool as
Agent_Step15_ComputerUsePreview.
---
dotnet/agent-framework-dotnet.slnx | 2 +-
dotnet/eng/verify-samples/AgentsSamples.cs | 26 +++++-----
.../02-agents/AgentProviders/README.md | 1 +
...h_AzureOpenAIResponses_ComputerUse.csproj} | 4 +-
.../ComputerUseUtil.cs | 0
.../Program.cs | 50 +++++++------------
.../README.md | 31 +++++-------
.../Agent_Step15_ComputerUsePreview/README.md | 2 +-
.../AgentProviders/foundry/README.md | 1 -
9 files changed, 47 insertions(+), 70 deletions(-)
rename dotnet/samples/02-agents/AgentProviders/{foundry/Agent_Step15.2_ComputerUse/Agent_Step15.2_ComputerUse.csproj => azure/Agent_With_AzureOpenAIResponses_ComputerUse/Agent_With_AzureOpenAIResponses_ComputerUse.csproj} (73%)
rename dotnet/samples/02-agents/AgentProviders/{foundry/Agent_Step15.2_ComputerUse => azure/Agent_With_AzureOpenAIResponses_ComputerUse}/ComputerUseUtil.cs (100%)
rename dotnet/samples/02-agents/AgentProviders/{foundry/Agent_Step15.2_ComputerUse => azure/Agent_With_AzureOpenAIResponses_ComputerUse}/Program.cs (60%)
rename dotnet/samples/02-agents/AgentProviders/{foundry/Agent_Step15.2_ComputerUse => azure/Agent_With_AzureOpenAIResponses_ComputerUse}/README.md (63%)
diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx
index d37af2bc006..891b0c562a1 100644
--- a/dotnet/agent-framework-dotnet.slnx
+++ b/dotnet/agent-framework-dotnet.slnx
@@ -27,6 +27,7 @@
+
@@ -158,7 +159,6 @@
-
diff --git a/dotnet/eng/verify-samples/AgentsSamples.cs b/dotnet/eng/verify-samples/AgentsSamples.cs
index 3749f1c2ca2..a4c32d13d50 100644
--- a/dotnet/eng/verify-samples/AgentsSamples.cs
+++ b/dotnet/eng/verify-samples/AgentsSamples.cs
@@ -50,6 +50,19 @@ internal static class AgentsSamples
],
},
+ new SampleDefinition
+ {
+ Name = "Agent_With_AzureOpenAIResponses_ComputerUse",
+ ProjectPath = "samples/02-agents/AgentProviders/azure/Agent_With_AzureOpenAIResponses_ComputerUse",
+ RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
+ ExpectedOutputDescription =
+ [
+ "The output should show a computer automation session running batched actions (such as click, type or keypress) against simulated browser screenshots, followed by a final response describing search results.",
+ "The output should not contain error messages or stack traces.",
+ ],
+ },
+
new SampleDefinition
{
Name = "Agent_With_AzureAIProject",
@@ -1195,19 +1208,6 @@ internal static class AgentsSamples
ExpectedOutputDescription = ["The output should show a computer automation session processing simulated browser screenshots with iteration steps and a final response describing search results."],
},
- new SampleDefinition
- {
- Name = "FoundryAgent_Step15.2_ComputerUse",
- ProjectPath = "samples/02-agents/AgentProviders/foundry/Agent_Step15.2_ComputerUse",
- RequiredEnvironmentVariables = ["OPENAI_API_KEY"],
- OptionalEnvironmentVariables = ["OPENAI_CHAT_MODEL_NAME"],
- ExpectedOutputDescription =
- [
- "The output should show a computer automation session running batched actions (such as click, type or keypress) against simulated browser screenshots, followed by a final response describing search results.",
- "The output should not contain error messages or stack traces.",
- ],
- },
-
new SampleDefinition
{
Name = "FoundryAgent_Step18_BingCustomSearch",
diff --git a/dotnet/samples/02-agents/AgentProviders/README.md b/dotnet/samples/02-agents/AgentProviders/README.md
index 407c4e8e4d9..2925e681d46 100644
--- a/dotnet/samples/02-agents/AgentProviders/README.md
+++ b/dotnet/samples/02-agents/AgentProviders/README.md
@@ -37,6 +37,7 @@ See the README.md for each sample for the prerequisites for that sample.
| [Azure Foundry Model](./azure/Agent_With_AzureFoundryModel/) | Use any model deployed to Microsoft Foundry |
| [Azure OpenAI ChatCompletion](./azure/Agent_With_AzureOpenAIChatCompletion/) | Create an AIAgent using Azure OpenAI ChatCompletion |
| [Azure OpenAI Responses](./azure/Agent_With_AzureOpenAIResponses/) | Create an AIAgent using Azure OpenAI Responses |
+| [Azure OpenAI Responses computer use](./azure/Agent_With_AzureOpenAIResponses_ComputerUse/) | Use the GA computer tool with an Azure OpenAI Responses agent |
### [Custom](./custom/)
diff --git a/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step15.2_ComputerUse/Agent_Step15.2_ComputerUse.csproj b/dotnet/samples/02-agents/AgentProviders/azure/Agent_With_AzureOpenAIResponses_ComputerUse/Agent_With_AzureOpenAIResponses_ComputerUse.csproj
similarity index 73%
rename from dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step15.2_ComputerUse/Agent_Step15.2_ComputerUse.csproj
rename to dotnet/samples/02-agents/AgentProviders/azure/Agent_With_AzureOpenAIResponses_ComputerUse/Agent_With_AzureOpenAIResponses_ComputerUse.csproj
index a80bec83f01..5d01a371f2c 100644
--- a/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step15.2_ComputerUse/Agent_Step15.2_ComputerUse.csproj
+++ b/dotnet/samples/02-agents/AgentProviders/azure/Agent_With_AzureOpenAIResponses_ComputerUse/Agent_With_AzureOpenAIResponses_ComputerUse.csproj
@@ -10,17 +10,17 @@
-
+
-
+
diff --git a/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step15.2_ComputerUse/ComputerUseUtil.cs b/dotnet/samples/02-agents/AgentProviders/azure/Agent_With_AzureOpenAIResponses_ComputerUse/ComputerUseUtil.cs
similarity index 100%
rename from dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step15.2_ComputerUse/ComputerUseUtil.cs
rename to dotnet/samples/02-agents/AgentProviders/azure/Agent_With_AzureOpenAIResponses_ComputerUse/ComputerUseUtil.cs
diff --git a/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step15.2_ComputerUse/Program.cs b/dotnet/samples/02-agents/AgentProviders/azure/Agent_With_AzureOpenAIResponses_ComputerUse/Program.cs
similarity index 60%
rename from dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step15.2_ComputerUse/Program.cs
rename to dotnet/samples/02-agents/AgentProviders/azure/Agent_With_AzureOpenAIResponses_ComputerUse/Program.cs
index 4317f636b42..b323c591cd7 100644
--- a/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step15.2_ComputerUse/Program.cs
+++ b/dotnet/samples/02-agents/AgentProviders/azure/Agent_With_AzureOpenAIResponses_ComputerUse/Program.cs
@@ -1,13 +1,12 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample shows how to use the generally available (GA) computer tool, created with the parameterless
-// FoundryAITool.CreateComputerTool(), with an AIAgent. Each computer call returned by the model carries an ordered
-// batch of actions; the application runs all of them and then sends back a single screenshot.
-//
-// Microsoft Foundry does not accept the GA "computer" tool type yet, so the sample currently runs against the public
-// OpenAI Responses API. The Foundry setup is kept below, commented out: once Foundry supports the GA tool, swap the
-// two agent setups and the rest of the sample works unchanged.
+// FoundryAITool.CreateComputerTool(), with an AIAgent backed by Azure OpenAI Responses. Each computer call returned by
+// the model carries an ordered batch of actions; the application runs all of them and then sends back a single
+// screenshot.
+using System.ClientModel.Primitives;
+using Azure.Identity;
using Demo.ComputerUse;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Foundry;
@@ -15,39 +14,24 @@
using OpenAI;
using OpenAI.Responses;
-// Microsoft Foundry setup (see below): uncomment once Foundry supports the GA computer tool.
-// using Azure.AI.Projects;
-// using Azure.Identity;
-
-// Public OpenAI Responses API (active until Microsoft Foundry supports the GA computer tool).
-string apiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY") ?? throw new InvalidOperationException("OPENAI_API_KEY is not set.");
-string model = Environment.GetEnvironmentVariable("OPENAI_CHAT_MODEL_NAME") ?? "gpt-5.4";
-
-AIAgent agent = new OpenAIClient(apiKey)
+Uri endpoint = AzureOpenAIEndpoint.From(
+ Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT"))
+ ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
+string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5.4";
+
+// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
+// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
+// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
+AIAgent agent = new OpenAIClient(
+ new BearerTokenPolicy(new DefaultAzureCredential(), "https://ai.azure.com/.default"),
+ new OpenAIClientOptions { Endpoint = endpoint })
.GetResponsesClient()
- .AsIChatClient(model)
+ .AsIChatClient(deploymentName)
.AsAIAgent(
name: "ComputerAgent",
instructions: "You are a computer automation assistant.",
tools: [FoundryAITool.CreateComputerTool()]);
-// Microsoft Foundry: uncomment this block (and the Azure usings at the top) once Foundry supports the GA computer
-// tool, then remove the OpenAI block above.
-//
-// string endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
-// string deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_COMPUTER_USE_DEPLOYMENT_NAME") ?? "gpt-5.4";
-//
-// // WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
-// // In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
-// // latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
-// AIProjectClient projectClient = new(new Uri(endpoint), new DefaultAzureCredential());
-//
-// AIAgent agent = projectClient.AsAIAgent(
-// model: deploymentName,
-// name: "ComputerAgent",
-// instructions: "You are a computer automation assistant.",
-// tools: [FoundryAITool.CreateComputerTool()]);
-
Dictionary screenshots = ComputerUseUtil.LoadScreenshots();
// Send the initial request with a screenshot of the browser.
diff --git a/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step15.2_ComputerUse/README.md b/dotnet/samples/02-agents/AgentProviders/azure/Agent_With_AzureOpenAIResponses_ComputerUse/README.md
similarity index 63%
rename from dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step15.2_ComputerUse/README.md
rename to dotnet/samples/02-agents/AgentProviders/azure/Agent_With_AzureOpenAIResponses_ComputerUse/README.md
index 60c4c987869..a499be446ba 100644
--- a/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step15.2_ComputerUse/README.md
+++ b/dotnet/samples/02-agents/AgentProviders/azure/Agent_With_AzureOpenAIResponses_ComputerUse/README.md
@@ -1,6 +1,6 @@
-# Computer use with the GA computer tool
+# Computer use with Azure OpenAI Responses
-This sample shows how to use the generally available (GA) computer tool with an `AIAgent`.
+This sample shows how to use the generally available (GA) computer tool with an `AIAgent` backed by Azure OpenAI Responses.
## What this sample demonstrates
@@ -8,9 +8,9 @@ This sample shows how to use the generally available (GA) computer tool with an
- Running every action of a batched `computer_call` (the GA call returns an ordered `actions` list) before sending back one screenshot
- Returning screenshots as inline image data, so no file upload or cleanup is needed
-For the preview `computer_use_preview` tool, which takes an environment and display size and returns a single `action` per call, see [Agent_Step15_ComputerUsePreview](../Agent_Step15_ComputerUsePreview/).
+For the preview `computer_use_preview` tool with a Microsoft Foundry project, which takes an environment and display size and returns a single `action` per call, see [Agent_Step15_ComputerUsePreview](../../foundry/Agent_Step15_ComputerUsePreview/).
-| | GA tool (this sample) | Preview tool (Step15) |
+| | GA tool (this sample) | Preview tool (Agent_Step15_ComputerUsePreview) |
| --- | --- | --- |
| Factory | `FoundryAITool.CreateComputerTool()` | `FoundryAITool.CreateComputerTool(environment, width, height)` |
| Wire tool | `{"type":"computer"}` | `{"type":"computer_use_preview", ...}` |
@@ -18,9 +18,8 @@ For the preview `computer_use_preview` tool, which takes an environment and disp
For more information, see the OpenAI [computer use guide](https://developers.openai.com/api/docs/guides/tools-computer-use).
-## Microsoft Foundry support
-
-Microsoft Foundry does not accept the GA `computer` tool type yet. Until it does, the sample runs against the public OpenAI Responses API. The Foundry setup is already in `Program.cs`, commented out: once Foundry supports the GA tool, uncomment it (and the `Azure.*` usings at the top), remove the OpenAI setup, and set the Foundry environment variables below.
+> [!NOTE]
+> The Microsoft Foundry project Responses endpoint does not accept the GA `computer` tool yet, so this sample uses the Azure OpenAI endpoint of the resource.
## Known limitation
@@ -28,7 +27,7 @@ The sample continues each turn from the stored response, so only the new `comput
## How the simulation works
-**This sample does not connect to a real browser.** It intercepts the model's actions and returns pre-captured screenshots (shared with the Step15 sample) as if the actions were performed:
+**This sample does not connect to a real browser.** It intercepts the model's actions and returns pre-captured screenshots (shared with the preview sample) as if the actions were performed:
| State | Reached by | Screenshot sent back |
| --- | --- | --- |
@@ -41,24 +40,18 @@ Each loop iteration runs all actions of the call in order, then sends the screen
## Prerequisites
- .NET 10 SDK or later
-- An OpenAI API key with access to a model that supports the GA computer tool (for example `gpt-5.4`)
+- An Azure OpenAI deployment of a model that supports the GA computer tool (for example `gpt-5.4`)
+- An authenticated Azure identity with access to the resource (for example, sign in with `az login`)
Set the following environment variables:
```powershell
-$env:OPENAI_API_KEY="sk-..."
-$env:OPENAI_CHAT_MODEL_NAME="gpt-5.4" # Optional, defaults to gpt-5.4
-```
-
-When switching to Microsoft Foundry, set these instead:
-
-```powershell
-$env:FOUNDRY_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project"
-$env:AZURE_AI_COMPUTER_USE_DEPLOYMENT_NAME="gpt-5.4"
+$env:AZURE_OPENAI_ENDPOINT="https://your-resource.openai.azure.com/"
+$env:AZURE_OPENAI_DEPLOYMENT_NAME="gpt-5.4" # Optional, defaults to gpt-5.4
```
## Run the sample
```powershell
dotnet run
-```
+```
\ No newline at end of file
diff --git a/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step15_ComputerUsePreview/README.md b/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step15_ComputerUsePreview/README.md
index ecab6ae76a4..749980606ef 100644
--- a/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step15_ComputerUsePreview/README.md
+++ b/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step15_ComputerUsePreview/README.md
@@ -8,7 +8,7 @@ This sample shows how to use the preview Computer Use tool with `AIProjectClient
- Processing computer call actions (click, type, key press)
- Managing the computer use interaction loop with screenshots
-For the generally available (GA) `computer` tool, created with the parameterless `FoundryAITool.CreateComputerTool()` and returning batched actions, see [Agent_Step15.2_ComputerUse](../Agent_Step15.2_ComputerUse/).
+For the generally available (GA) `computer` tool, created with the parameterless `FoundryAITool.CreateComputerTool()` and returning batched actions, see [Agent_With_AzureOpenAIResponses_ComputerUse](../../azure/Agent_With_AzureOpenAIResponses_ComputerUse/).
For more information, see [Use the computer tool](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/tools/computer-use?pivots=csharp).
diff --git a/dotnet/samples/02-agents/AgentProviders/foundry/README.md b/dotnet/samples/02-agents/AgentProviders/foundry/README.md
index 45adb36b3e3..00c2981945d 100644
--- a/dotnet/samples/02-agents/AgentProviders/foundry/README.md
+++ b/dotnet/samples/02-agents/AgentProviders/foundry/README.md
@@ -64,7 +64,6 @@ Some samples require extra tool-specific environment variables. See each sample
| [Plugins](./Agent_Step13_Plugins/) | Plugins with dependency injection |
| [Code interpreter](./Agent_Step14_CodeInterpreter/) | Code interpreter tool |
| [Computer use (preview)](./Agent_Step15_ComputerUsePreview/) | Preview `computer_use_preview` tool |
-| [Computer use](./Agent_Step15.2_ComputerUse/) | GA `computer` tool with batched actions (runs on public OpenAI until Foundry supports it) |
| [File search](./Agent_Step16_FileSearch/) | File search tool |
| [OpenAPI tools](./Agent_Step17_OpenAPITools/) | OpenAPI tools |
| [Bing custom search](./Agent_Step18_BingCustomSearch/) | Bing Custom Search tool |
From 710c2c39e53370b1f8b18a9403a304145991be80 Mon Sep 17 00:00:00 2001
From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com>
Date: Tue, 20 Oct 2026 16:07:08 -0700
Subject: [PATCH 11/13] style: Simplify JsonElement qualification
---
.../InputConverterTests.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/InputConverterTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/InputConverterTests.cs
index 453a5327664..ddc7f3247ed 100644
--- a/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/InputConverterTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/InputConverterTests.cs
@@ -958,7 +958,7 @@ public void ConvertOutputItemsToMessages_McpApprovalRequest_ResolvesAfRequestFro
Assert.Equal("call_history", fcc.CallId);
Assert.Equal("delete_file", fcc.Name);
Assert.NotNull(fcc.Arguments);
- Assert.Equal("/tmp/x", ((System.Text.Json.JsonElement)fcc.Arguments!["path"]!).GetString());
+ Assert.Equal("/tmp/x", ((JsonElement)fcc.Arguments!["path"]!).GetString());
}
[Fact]
From 3f4b2410e55f47d6a483a93ba5fd0605f0f23c7f Mon Sep 17 00:00:00 2001
From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com>
Date: Thu, 29 Oct 2026 18:47:15 -0700
Subject: [PATCH 12/13] fix(dotnet): run GA computer sample through Foundry
---
.../02-agents/AgentProviders/README.md | 2 +-
...th_AzureOpenAIResponses_ComputerUse.csproj | 2 +-
.../ComputerUseUtil.cs | 43 +++++-
.../Program.cs | 137 ++++++++++--------
.../README.md | 18 +--
5 files changed, 124 insertions(+), 78 deletions(-)
diff --git a/dotnet/samples/02-agents/AgentProviders/README.md b/dotnet/samples/02-agents/AgentProviders/README.md
index 2925e681d46..32c0afb1025 100644
--- a/dotnet/samples/02-agents/AgentProviders/README.md
+++ b/dotnet/samples/02-agents/AgentProviders/README.md
@@ -37,7 +37,7 @@ See the README.md for each sample for the prerequisites for that sample.
| [Azure Foundry Model](./azure/Agent_With_AzureFoundryModel/) | Use any model deployed to Microsoft Foundry |
| [Azure OpenAI ChatCompletion](./azure/Agent_With_AzureOpenAIChatCompletion/) | Create an AIAgent using Azure OpenAI ChatCompletion |
| [Azure OpenAI Responses](./azure/Agent_With_AzureOpenAIResponses/) | Create an AIAgent using Azure OpenAI Responses |
-| [Azure OpenAI Responses computer use](./azure/Agent_With_AzureOpenAIResponses_ComputerUse/) | Use the GA computer tool with an Azure OpenAI Responses agent |
+| [Microsoft Foundry Responses computer use](./azure/Agent_With_AzureOpenAIResponses_ComputerUse/) | Use the GA computer tool with a Microsoft Foundry project Responses agent |
### [Custom](./custom/)
diff --git a/dotnet/samples/02-agents/AgentProviders/azure/Agent_With_AzureOpenAIResponses_ComputerUse/Agent_With_AzureOpenAIResponses_ComputerUse.csproj b/dotnet/samples/02-agents/AgentProviders/azure/Agent_With_AzureOpenAIResponses_ComputerUse/Agent_With_AzureOpenAIResponses_ComputerUse.csproj
index 5d01a371f2c..8630c6ff7e1 100644
--- a/dotnet/samples/02-agents/AgentProviders/azure/Agent_With_AzureOpenAIResponses_ComputerUse/Agent_With_AzureOpenAIResponses_ComputerUse.csproj
+++ b/dotnet/samples/02-agents/AgentProviders/azure/Agent_With_AzureOpenAIResponses_ComputerUse/Agent_With_AzureOpenAIResponses_ComputerUse.csproj
@@ -14,7 +14,7 @@
-
+
diff --git a/dotnet/samples/02-agents/AgentProviders/azure/Agent_With_AzureOpenAIResponses_ComputerUse/ComputerUseUtil.cs b/dotnet/samples/02-agents/AgentProviders/azure/Agent_With_AzureOpenAIResponses_ComputerUse/ComputerUseUtil.cs
index 261d56bee12..0b8105d2a44 100644
--- a/dotnet/samples/02-agents/AgentProviders/azure/Agent_With_AzureOpenAIResponses_ComputerUse/ComputerUseUtil.cs
+++ b/dotnet/samples/02-agents/AgentProviders/azure/Agent_With_AzureOpenAIResponses_ComputerUse/ComputerUseUtil.cs
@@ -2,6 +2,7 @@
using System.ClientModel.Primitives;
using System.Text.Json;
+using Microsoft.Extensions.AI;
using OpenAI.Responses;
namespace Demo.ComputerUse;
@@ -23,16 +24,46 @@ internal sealed record ComputerActionRequest(string Type, string? Text, IReadOnl
internal static class ComputerUseUtil
{
- internal static Dictionary LoadScreenshots()
+ internal static async Task> UploadScreenshotAssetsAsync(IHostedFileClient fileClient)
{
string assetsDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Assets");
- return new Dictionary
+ (string Key, string FileName)[] files =
+ [
+ ("browser_search", "cua_browser_search.jpg"),
+ ("search_typed", "cua_search_typed.jpg"),
+ ("search_results", "cua_search_results.jpg")
+ ];
+
+ Dictionary screenshots = [];
+
+ foreach ((string key, string fileName) in files)
{
- ["browser_search"] = BinaryData.FromBytes(File.ReadAllBytes(Path.Combine(assetsDir, "cua_browser_search.jpg"))),
- ["search_typed"] = BinaryData.FromBytes(File.ReadAllBytes(Path.Combine(assetsDir, "cua_search_typed.jpg"))),
- ["search_results"] = BinaryData.FromBytes(File.ReadAllBytes(Path.Combine(assetsDir, "cua_search_results.jpg"))),
- };
+ HostedFileContent result = await fileClient.UploadAsync(
+ Path.Combine(assetsDir, fileName),
+ new HostedFileClientOptions { Purpose = "assistants" });
+ screenshots[key] = result.FileId;
+ }
+
+ return screenshots;
+ }
+
+ internal static async Task EnsureDeleteScreenshotAssetsAsync(
+ IHostedFileClient fileClient,
+ Dictionary screenshots)
+ {
+ foreach ((string key, string fileId) in screenshots)
+ {
+ try
+ {
+ await fileClient.DeleteAsync(fileId);
+ }
+ catch (Exception exception)
+ {
+ // Continue so one failed deletion does not leave every remaining sample asset behind.
+ Console.Error.WriteLine($"Failed to delete screenshot '{key}' ({fileId}): {exception.Message}");
+ }
+ }
}
///
diff --git a/dotnet/samples/02-agents/AgentProviders/azure/Agent_With_AzureOpenAIResponses_ComputerUse/Program.cs b/dotnet/samples/02-agents/AgentProviders/azure/Agent_With_AzureOpenAIResponses_ComputerUse/Program.cs
index b323c591cd7..720a14b6a2c 100644
--- a/dotnet/samples/02-agents/AgentProviders/azure/Agent_With_AzureOpenAIResponses_ComputerUse/Program.cs
+++ b/dotnet/samples/02-agents/AgentProviders/azure/Agent_With_AzureOpenAIResponses_ComputerUse/Program.cs
@@ -1,30 +1,30 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample shows how to use the generally available (GA) computer tool, created with the parameterless
-// FoundryAITool.CreateComputerTool(), with an AIAgent backed by Azure OpenAI Responses. Each computer call returned by
-// the model carries an ordered batch of actions; the application runs all of them and then sends back a single
-// screenshot.
+// FoundryAITool.CreateComputerTool(), with an AIAgent backed by the Microsoft Foundry project Responses endpoint.
+// Each computer call returned by the model carries an ordered batch of actions; the application runs all of them and
+// then sends back a single screenshot.
-using System.ClientModel.Primitives;
+using Azure.AI.Projects;
using Azure.Identity;
using Demo.ComputerUse;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Foundry;
using Microsoft.Extensions.AI;
-using OpenAI;
using OpenAI.Responses;
-Uri endpoint = AzureOpenAIEndpoint.From(
- Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT"))
- ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
-string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5.4";
+string endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT")
+ ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
+string deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4";
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
-AIAgent agent = new OpenAIClient(
- new BearerTokenPolicy(new DefaultAzureCredential(), "https://ai.azure.com/.default"),
- new OpenAIClientOptions { Endpoint = endpoint })
+AIProjectClient projectClient = new(new Uri(endpoint), new DefaultAzureCredential());
+using IHostedFileClient fileClient = projectClient.GetProjectOpenAIClient().AsIHostedFileClient();
+
+AIAgent agent = projectClient
+ .GetProjectOpenAIClient()
.GetResponsesClient()
.AsIChatClient(deploymentName)
.AsAIAgent(
@@ -32,69 +32,86 @@
instructions: "You are a computer automation assistant.",
tools: [FoundryAITool.CreateComputerTool()]);
-Dictionary screenshots = ComputerUseUtil.LoadScreenshots();
+Dictionary screenshots = [];
-// Send the initial request with a screenshot of the browser.
-ChatMessage message = new(ChatRole.User, [
- new TextContent("Search for 'OpenAI news'. Type it and submit. Once you see results, the task is complete."),
- new DataContent(screenshots["browser_search"].ToMemory(), "image/jpeg")
-]);
+try
+{
+ // Foundry rejects large inline data URIs in computer_call_output, so upload the simulated screenshots once and
+ // reference their file IDs throughout the loop.
+ screenshots = await ComputerUseUtil.UploadScreenshotAssetsAsync(fileClient);
+
+ // Send the initial request with a screenshot of the browser.
+ ChatMessage message = new(ChatRole.User, [
+ new TextContent("Search for 'OpenAI news'. Type it and submit. Once you see results, the task is complete."),
+ new AIContent
+ {
+ RawRepresentation = ResponseContentPart.CreateInputImagePart(
+ imageFileId: screenshots["browser_search"],
+ imageDetailLevel: ResponseImageDetailLevel.High)
+ }
+ ]);
-Console.WriteLine("Starting computer use session...");
+ Console.WriteLine("Starting computer use session...");
-AgentSession session = await agent.CreateSessionAsync();
-AgentResponse response = await agent.RunAsync(message, session: session);
+ AgentSession session = await agent.CreateSessionAsync();
+ AgentResponse response = await agent.RunAsync(message, session: session);
-SearchState currentState = SearchState.Initial;
+ SearchState currentState = SearchState.Initial;
-for (int i = 0; i < 10; i++)
-{
- // Find the next computer call.
- ComputerCallResponseItem? computerCall = response.Messages
- .SelectMany(m => m.Contents)
- .Select(c => c.RawRepresentation as ComputerCallResponseItem)
- .FirstOrDefault(item => item is not null);
-
- if (computerCall is null)
+ for (int i = 0; i < 10; i++)
{
- if (currentState == SearchState.PressedEnter)
+ // Find the next computer call.
+ ComputerCallResponseItem? computerCall = response.Messages
+ .SelectMany(m => m.Contents)
+ .Select(c => c.RawRepresentation as ComputerCallResponseItem)
+ .FirstOrDefault(item => item is not null);
+
+ if (computerCall is null)
{
- Console.WriteLine("No more computer actions. Done.");
+ if (currentState == SearchState.PressedEnter)
+ {
+ Console.WriteLine("No more computer actions. Done.");
+ Console.WriteLine(response);
+ break;
+ }
+
+ // The model may ask for confirmation before acting. Answer and continue.
+ if (response.Text.Contains('?'))
+ {
+ Console.WriteLine($"Model asked: {response.Text}");
+ response = await agent.RunAsync("Please proceed.", session);
+ continue;
+ }
+
Console.WriteLine(response);
break;
}
- // The model may ask for confirmation before acting. Answer and continue.
- if (response.Text.Contains('?'))
+ // Run every action of the batch in order, then capture a single screenshot.
+ foreach (ComputerActionRequest action in ComputerUseUtil.GetActions(computerCall))
{
- Console.WriteLine($"Model asked: {response.Text}");
- response = await agent.RunAsync("Please proceed.", session);
- continue;
+ Console.WriteLine($"[{i + 1}] Action: {action.Type}");
+ currentState = await ComputerUseUtil.ApplyAsync(action, currentState);
}
- Console.WriteLine(response);
- break;
- }
-
- // Run every action of the batch in order, then capture a single screenshot.
- foreach (ComputerActionRequest action in ComputerUseUtil.GetActions(computerCall))
- {
- Console.WriteLine($"[{i + 1}] Action: {action.Type}");
- currentState = await ComputerUseUtil.ApplyAsync(action, currentState);
- }
+ ComputerCallOutputResponseItem callOutput = new(
+ computerCall.CallId,
+ ComputerCallOutput.CreateScreenshotOutput(
+ screenshotImageFileId: screenshots[ComputerUseUtil.GetScreenshotKey(currentState)]));
- ComputerCallOutputResponseItem callOutput = new(
- computerCall.CallId,
- ComputerCallOutput.CreateScreenshotOutput(screenshots[ComputerUseUtil.GetScreenshotKey(currentState)], "image/jpeg"));
+ // The model can flag an action as risky with pending safety checks. A real application must show them to the
+ // user and acknowledge them only after the user confirms; this simulation acknowledges them automatically.
+ foreach (ComputerCallSafetyCheck safetyCheck in computerCall.PendingSafetyChecks)
+ {
+ Console.WriteLine($"Acknowledging safety check: {safetyCheck.Code}");
+ callOutput.AcknowledgedSafetyChecks.Add(safetyCheck);
+ }
- // The model can flag an action as risky with pending safety checks. A real application must show them to the user
- // and acknowledge them only after the user confirms; this simulation acknowledges them automatically.
- foreach (ComputerCallSafetyCheck safetyCheck in computerCall.PendingSafetyChecks)
- {
- Console.WriteLine($"Acknowledging safety check: {safetyCheck.Code}");
- callOutput.AcknowledgedSafetyChecks.Add(safetyCheck);
+ // Send the screenshot back as the computer call output.
+ response = await agent.RunAsync([new ChatMessage(ChatRole.User, [new AIContent { RawRepresentation = callOutput }])], session);
}
-
- // Send the screenshot back as the computer call output.
- response = await agent.RunAsync([new ChatMessage(ChatRole.User, [new AIContent { RawRepresentation = callOutput }])], session);
+}
+finally
+{
+ await ComputerUseUtil.EnsureDeleteScreenshotAssetsAsync(fileClient, screenshots);
}
diff --git a/dotnet/samples/02-agents/AgentProviders/azure/Agent_With_AzureOpenAIResponses_ComputerUse/README.md b/dotnet/samples/02-agents/AgentProviders/azure/Agent_With_AzureOpenAIResponses_ComputerUse/README.md
index a499be446ba..8c2edfde4eb 100644
--- a/dotnet/samples/02-agents/AgentProviders/azure/Agent_With_AzureOpenAIResponses_ComputerUse/README.md
+++ b/dotnet/samples/02-agents/AgentProviders/azure/Agent_With_AzureOpenAIResponses_ComputerUse/README.md
@@ -1,12 +1,12 @@
-# Computer use with Azure OpenAI Responses
+# Computer use with Microsoft Foundry Responses
-This sample shows how to use the generally available (GA) computer tool with an `AIAgent` backed by Azure OpenAI Responses.
+This sample shows how to use the generally available (GA) computer tool with an `AIAgent` backed by a Microsoft Foundry project Responses endpoint.
## What this sample demonstrates
- Using the parameterless `FoundryAITool.CreateComputerTool()` to add the GA `computer` tool (`{"type":"computer"}`)
- Running every action of a batched `computer_call` (the GA call returns an ordered `actions` list) before sending back one screenshot
-- Returning screenshots as inline image data, so no file upload or cleanup is needed
+- Uploading screenshots to Foundry Files and returning file IDs from the computer loop
For the preview `computer_use_preview` tool with a Microsoft Foundry project, which takes an environment and display size and returns a single `action` per call, see [Agent_Step15_ComputerUsePreview](../../foundry/Agent_Step15_ComputerUsePreview/).
@@ -18,9 +18,6 @@ For the preview `computer_use_preview` tool with a Microsoft Foundry project, wh
For more information, see the OpenAI [computer use guide](https://developers.openai.com/api/docs/guides/tools-computer-use).
-> [!NOTE]
-> The Microsoft Foundry project Responses endpoint does not accept the GA `computer` tool yet, so this sample uses the Azure OpenAI endpoint of the resource.
-
## Known limitation
The sample continues each turn from the stored response, so only the new `computer_call_output` is sent. Sending a GA `computer_call` back to the model instead (for example with stored responses disabled and the history kept locally) currently fails. The OpenAI .NET SDK models only the preview item and adds `"action": null`, which the Responses API rejects for the GA tool.
@@ -40,14 +37,15 @@ Each loop iteration runs all actions of the call in order, then sends the screen
## Prerequisites
- .NET 10 SDK or later
-- An Azure OpenAI deployment of a model that supports the GA computer tool (for example `gpt-5.4`)
-- An authenticated Azure identity with access to the resource (for example, sign in with `az login`)
+- A Microsoft Foundry project with a deployment of a model that supports the GA computer tool (for example `gpt-5.4`)
+- Foundry Files configured for the project, including storage access for the project's managed identity
+- An authenticated Azure identity with access to the project (for example, sign in with `az login`)
Set the following environment variables:
```powershell
-$env:AZURE_OPENAI_ENDPOINT="https://your-resource.openai.azure.com/"
-$env:AZURE_OPENAI_DEPLOYMENT_NAME="gpt-5.4" # Optional, defaults to gpt-5.4
+$env:FOUNDRY_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project"
+$env:FOUNDRY_MODEL="gpt-5.4" # Optional, defaults to gpt-5.4
```
## Run the sample
From e25c54fd3ac343c5eb41aa65a8c2360c9ee13e53 Mon Sep 17 00:00:00 2001
From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com>
Date: Fri, 30 Oct 2026 09:33:04 -0700
Subject: [PATCH 13/13] fix(dotnet): isolate computer sample assets
---
...ith_AzureOpenAIResponses_ComputerUse.csproj | 3 +--
.../Assets/cua_browser_search.jpg | Bin 0 -> 411712 bytes
.../Assets/cua_search_results.jpg | Bin 0 -> 87473 bytes
.../Assets/cua_search_typed.jpg | Bin 0 -> 365122 bytes
.../ComputerUseUtil.cs | 2 +-
.../Program.cs | 2 +-
.../README.md | 2 +-
.../ComputerUseUtil.cs | 2 +-
.../Agent_Step15_ComputerUsePreview/Program.cs | 2 +-
9 files changed, 6 insertions(+), 7 deletions(-)
create mode 100644 dotnet/samples/02-agents/AgentProviders/azure/Agent_With_AzureOpenAIResponses_ComputerUse/Assets/cua_browser_search.jpg
create mode 100644 dotnet/samples/02-agents/AgentProviders/azure/Agent_With_AzureOpenAIResponses_ComputerUse/Assets/cua_search_results.jpg
create mode 100644 dotnet/samples/02-agents/AgentProviders/azure/Agent_With_AzureOpenAIResponses_ComputerUse/Assets/cua_search_typed.jpg
diff --git a/dotnet/samples/02-agents/AgentProviders/azure/Agent_With_AzureOpenAIResponses_ComputerUse/Agent_With_AzureOpenAIResponses_ComputerUse.csproj b/dotnet/samples/02-agents/AgentProviders/azure/Agent_With_AzureOpenAIResponses_ComputerUse/Agent_With_AzureOpenAIResponses_ComputerUse.csproj
index 8630c6ff7e1..5bfe68fa9e4 100644
--- a/dotnet/samples/02-agents/AgentProviders/azure/Agent_With_AzureOpenAIResponses_ComputerUse/Agent_With_AzureOpenAIResponses_ComputerUse.csproj
+++ b/dotnet/samples/02-agents/AgentProviders/azure/Agent_With_AzureOpenAIResponses_ComputerUse/Agent_With_AzureOpenAIResponses_ComputerUse.csproj
@@ -19,8 +19,7 @@
-
-
+
diff --git a/dotnet/samples/02-agents/AgentProviders/azure/Agent_With_AzureOpenAIResponses_ComputerUse/Assets/cua_browser_search.jpg b/dotnet/samples/02-agents/AgentProviders/azure/Agent_With_AzureOpenAIResponses_ComputerUse/Assets/cua_browser_search.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..372916a298b13852f8192959c442160d0fa59810
GIT binary patch
literal 411712
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
z
ZIGS47~*Df_#iQKCpt@=Ak7GS
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