diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx
index 00a1882018..69c5698a88 100644
--- a/dotnet/agent-framework-dotnet.slnx
+++ b/dotnet/agent-framework-dotnet.slnx
@@ -152,6 +152,7 @@
+
@@ -173,6 +174,7 @@
+
diff --git a/dotnet/samples/02-agents/AgentWithOpenAI/Agent_OpenAI_Step06_CodeInterpreterFileDownload/Agent_OpenAI_Step06_CodeInterpreterFileDownload.csproj b/dotnet/samples/02-agents/AgentWithOpenAI/Agent_OpenAI_Step06_CodeInterpreterFileDownload/Agent_OpenAI_Step06_CodeInterpreterFileDownload.csproj
new file mode 100644
index 0000000000..06380e8016
--- /dev/null
+++ b/dotnet/samples/02-agents/AgentWithOpenAI/Agent_OpenAI_Step06_CodeInterpreterFileDownload/Agent_OpenAI_Step06_CodeInterpreterFileDownload.csproj
@@ -0,0 +1,15 @@
+
+
+
+ Exe
+ net10.0
+
+ enable
+ enable
+
+
+
+
+
+
+
diff --git a/dotnet/samples/02-agents/AgentWithOpenAI/Agent_OpenAI_Step06_CodeInterpreterFileDownload/Program.cs b/dotnet/samples/02-agents/AgentWithOpenAI/Agent_OpenAI_Step06_CodeInterpreterFileDownload/Program.cs
new file mode 100644
index 0000000000..c01ff4304e
--- /dev/null
+++ b/dotnet/samples/02-agents/AgentWithOpenAI/Agent_OpenAI_Step06_CodeInterpreterFileDownload/Program.cs
@@ -0,0 +1,89 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+// This sample shows how to download files generated by Code Interpreter using the Containers API.
+// Code Interpreter generates files inside containers (cfile_ / cntr_ IDs) which cannot be
+// downloaded via the standard Files API. Use ContainerClient instead.
+
+#pragma warning disable OPENAI001
+
+using System.ClientModel;
+using Microsoft.Agents.AI;
+using Microsoft.Extensions.AI;
+using OpenAI;
+using OpenAI.Containers;
+using OpenAI.Responses;
+
+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-4o-mini";
+
+var openAIClient = new OpenAIClient(new ApiKeyCredential(apiKey));
+
+// Create an agent with Code Interpreter tool enabled
+AIAgent agent = openAIClient
+ .GetResponsesClient()
+ .AsAIAgent(
+ model: model,
+ instructions: "You are a helpful assistant that can generate files using code.",
+ name: "CodeInterpreterAgent",
+ tools: [new HostedCodeInterpreterTool()]);
+
+// Ask the agent to generate a file
+AgentResponse response = await agent.RunAsync(
+ "Create a CSV file with the multiplication times tables from 1 to 12. Include headers.");
+
+// Display the text response
+foreach (TextContent textContent in response.Messages.SelectMany(x => x.Contents).OfType())
+{
+ Console.WriteLine(textContent.Text);
+}
+
+// Extract container file citations from response annotations and download
+ContainerClient containerClient = openAIClient.GetContainerClient();
+
+HashSet downloadedFiles = [];
+bool foundContainerFiles = false;
+
+foreach (AIContent content in response.Messages.SelectMany(x => x.Contents))
+{
+ if (content.Annotations is null)
+ {
+ continue;
+ }
+
+ foreach (AIAnnotation annotation in content.Annotations)
+ {
+ // Container files from Code Interpreter have ContainerFileCitationMessageAnnotation as raw representation
+ if (annotation is CitationAnnotation citation
+ && citation.RawRepresentation is ContainerFileCitationMessageAnnotation containerCitation)
+ {
+ foundContainerFiles = true;
+
+ // Deduplicate by container+file ID in case the same file is cited multiple times
+ string key = $"{containerCitation.ContainerId}/{containerCitation.FileId}";
+ if (!downloadedFiles.Add(key))
+ {
+ continue;
+ }
+
+ Console.WriteLine($"\nDownloading container file: {containerCitation.Filename}");
+ Console.WriteLine($" Container ID: {containerCitation.ContainerId}");
+ Console.WriteLine($" File ID: {containerCitation.FileId}");
+
+ BinaryData fileData = await containerClient.DownloadContainerFileAsync(
+ containerCitation.ContainerId,
+ containerCitation.FileId);
+
+ // Sanitize filename to prevent path traversal
+ string safeFilename = Path.GetFileName(containerCitation.Filename);
+ string outputPath = Path.Combine(Directory.GetCurrentDirectory(), safeFilename);
+ await File.WriteAllBytesAsync(outputPath, fileData.ToArray());
+ Console.WriteLine($" Saved to: {outputPath}");
+ }
+ }
+}
+
+if (!foundContainerFiles)
+{
+ Console.WriteLine("\nNo container file citations found in the response.");
+ Console.WriteLine("The model may not have generated a downloadable file for this prompt.");
+}
diff --git a/dotnet/samples/02-agents/AgentWithOpenAI/Agent_OpenAI_Step06_CodeInterpreterFileDownload/README.md b/dotnet/samples/02-agents/AgentWithOpenAI/Agent_OpenAI_Step06_CodeInterpreterFileDownload/README.md
new file mode 100644
index 0000000000..4ba457d0f8
--- /dev/null
+++ b/dotnet/samples/02-agents/AgentWithOpenAI/Agent_OpenAI_Step06_CodeInterpreterFileDownload/README.md
@@ -0,0 +1,51 @@
+# Code Interpreter File Download (OpenAI)
+
+This sample demonstrates how to download files generated by Code Interpreter when using the OpenAI Responses API.
+
+## What this sample demonstrates
+
+- Creating an agent with Code Interpreter tool using `ResponsesClient.AsAIAgent()`
+- Generating files through Code Interpreter (e.g., CSV, Excel, images)
+- Extracting container file citations from agent response annotations
+- Downloading container files using the `ContainerClient` API
+
+## Container files vs regular files
+
+When Code Interpreter generates a file, the file is stored inside a **container** with a `cntr_` prefixed ID. The file itself gets a `cfile_` prefixed ID.
+
+These container files **cannot** be downloaded using the standard Files API (`GetOpenAIFileClient`), which returns 404 for `cfile_` IDs. Instead, you must use the **Containers API** (`GetContainerClient`) to download them:
+
+```csharp
+// ❌ This does NOT work for container files
+var filesClient = openAIClient.GetOpenAIFileClient();
+await filesClient.DownloadFileAsync("cfile_..."); // Returns 404
+
+// ✅ Use ContainerClient instead
+var containerClient = openAIClient.GetContainerClient();
+await containerClient.DownloadContainerFileAsync("cntr_...", "cfile_...");
+```
+
+The container ID and file ID are available from the `ContainerFileCitationMessageAnnotation` annotation in the response, accessible via `CitationAnnotation.RawRepresentation`.
+
+## Prerequisites
+
+- .NET 10 SDK or later
+- OpenAI API key with access to a model that supports Code Interpreter
+
+Set the following environment variables:
+
+```powershell
+$env:OPENAI_API_KEY="sk-..."
+$env:OPENAI_CHAT_MODEL_NAME="gpt-4o-mini" # Optional, defaults to gpt-4o-mini
+```
+
+## Run the sample
+
+```powershell
+dotnet run
+```
+
+## See also
+
+- [Code Interpreter File Download with Foundry](../../../02-agents/AgentsWithFoundry/Agent_Step24_CodeInterpreterFileDownload/) — same scenario using Microsoft Foundry
+- [Code Interpreter](../../../02-agents/AgentsWithFoundry/Agent_Step14_CodeInterpreter/) — Code Interpreter without file download
diff --git a/dotnet/samples/02-agents/AgentWithOpenAI/README.md b/dotnet/samples/02-agents/AgentWithOpenAI/README.md
index 74a44600bf..78955a72af 100644
--- a/dotnet/samples/02-agents/AgentWithOpenAI/README.md
+++ b/dotnet/samples/02-agents/AgentWithOpenAI/README.md
@@ -14,4 +14,5 @@ Agent Framework provides additional support to allow OpenAI developers to use th
|[Using Reasoning Capabilities](./Agent_OpenAI_Step02_Reasoning/)|This sample demonstrates how to create an AI agent with reasoning capabilities using OpenAI's reasoning models and response types.|
|[Creating an Agent from a ChatClient](./Agent_OpenAI_Step03_CreateFromChatClient/)|This sample demonstrates how to create an AI agent directly from an OpenAI.Chat.ChatClient instance using OpenAIChatClientAgent.|
|[Creating an Agent from an OpenAIResponseClient](./Agent_OpenAI_Step04_CreateFromOpenAIResponseClient/)|This sample demonstrates how to create an AI agent directly from an OpenAI.Responses.OpenAIResponseClient instance using OpenAIResponseClientAgent.|
-|[Managing Conversation State](./Agent_OpenAI_Step05_Conversation/)|This sample demonstrates how to maintain conversation state across multiple turns using the AgentSession for context continuity.|
\ No newline at end of file
+|[Managing Conversation State](./Agent_OpenAI_Step05_Conversation/)|This sample demonstrates how to maintain conversation state across multiple turns using the AgentSession for context continuity.|
+|[Code Interpreter File Download](./Agent_OpenAI_Step06_CodeInterpreterFileDownload/)|This sample demonstrates how to download files generated by Code Interpreter using the Containers API (`cfile_`/`cntr_` IDs).|
\ No newline at end of file
diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step24_CodeInterpreterFileDownload/Agent_Step24_CodeInterpreterFileDownload.csproj b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step24_CodeInterpreterFileDownload/Agent_Step24_CodeInterpreterFileDownload.csproj
new file mode 100644
index 0000000000..129c9026a2
--- /dev/null
+++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step24_CodeInterpreterFileDownload/Agent_Step24_CodeInterpreterFileDownload.csproj
@@ -0,0 +1,19 @@
+
+
+
+ Exe
+ net10.0
+
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step24_CodeInterpreterFileDownload/Program.cs b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step24_CodeInterpreterFileDownload/Program.cs
new file mode 100644
index 0000000000..79fac0d5d4
--- /dev/null
+++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step24_CodeInterpreterFileDownload/Program.cs
@@ -0,0 +1,91 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+// This sample shows how to download files generated by Code Interpreter using Microsoft Foundry.
+// Code Interpreter generates files inside containers (cfile_ / cntr_ IDs) which cannot be
+// downloaded via the standard Files API. Use ContainerClient from the project's OpenAI client instead.
+
+#pragma warning disable OPENAI001
+
+using Azure.AI.Projects;
+using Azure.Identity;
+using Microsoft.Agents.AI;
+using Microsoft.Extensions.AI;
+using OpenAI.Responses;
+
+string endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.");
+string deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
+
+// 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 aiProjectClient = new(new Uri(endpoint), new DefaultAzureCredential());
+
+// Create an agent with Code Interpreter tool enabled
+AIAgent agent = aiProjectClient.AsAIAgent(
+ deploymentName,
+ instructions: "You are a helpful assistant that can generate files using code.",
+ name: "CodeInterpreterAgent",
+ tools: [new HostedCodeInterpreterTool()]);
+
+// Ask the agent to generate a file
+AgentResponse response = await agent.RunAsync(
+ "Create a CSV file with the multiplication times tables from 1 to 12. Include headers.");
+
+// Display the text response
+foreach (TextContent textContent in response.Messages.SelectMany(x => x.Contents).OfType())
+{
+ Console.WriteLine(textContent.Text);
+}
+
+// Extract container file citations from response annotations and download.
+// AIProjectClient.GetProjectOpenAIClient() returns a ProjectOpenAIClient (inherits from OpenAI.OpenAIClient)
+// which supports GetContainerClient(), unlike AzureOpenAIClient which does not.
+var containerClient = aiProjectClient.GetProjectOpenAIClient().GetContainerClient();
+
+HashSet downloadedFiles = [];
+bool foundContainerFiles = false;
+
+foreach (AIContent content in response.Messages.SelectMany(x => x.Contents))
+{
+ if (content.Annotations is null)
+ {
+ continue;
+ }
+
+ foreach (AIAnnotation annotation in content.Annotations)
+ {
+ // Container files from Code Interpreter have ContainerFileCitationMessageAnnotation as raw representation
+ if (annotation is CitationAnnotation citation
+ && citation.RawRepresentation is ContainerFileCitationMessageAnnotation containerCitation)
+ {
+ foundContainerFiles = true;
+
+ // Deduplicate by container+file ID in case the same file is cited multiple times
+ string key = $"{containerCitation.ContainerId}/{containerCitation.FileId}";
+ if (!downloadedFiles.Add(key))
+ {
+ continue;
+ }
+
+ Console.WriteLine($"\nDownloading container file: {containerCitation.Filename}");
+ Console.WriteLine($" Container ID: {containerCitation.ContainerId}");
+ Console.WriteLine($" File ID: {containerCitation.FileId}");
+
+ BinaryData fileData = await containerClient.DownloadContainerFileAsync(
+ containerCitation.ContainerId,
+ containerCitation.FileId);
+
+ // Sanitize filename to prevent path traversal
+ string safeFilename = Path.GetFileName(containerCitation.Filename);
+ string outputPath = Path.Combine(Directory.GetCurrentDirectory(), safeFilename);
+ await File.WriteAllBytesAsync(outputPath, fileData.ToArray());
+ Console.WriteLine($" Saved to: {outputPath}");
+ }
+ }
+}
+
+if (!foundContainerFiles)
+{
+ Console.WriteLine("\nNo container file citations found in the response.");
+ Console.WriteLine("The model may not have generated a downloadable file for this prompt.");
+}
diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step24_CodeInterpreterFileDownload/README.md b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step24_CodeInterpreterFileDownload/README.md
new file mode 100644
index 0000000000..4d50b98ca8
--- /dev/null
+++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step24_CodeInterpreterFileDownload/README.md
@@ -0,0 +1,56 @@
+# Code Interpreter File Download (Microsoft Foundry)
+
+This sample demonstrates how to download files generated by Code Interpreter when using Microsoft Foundry.
+
+## What this sample demonstrates
+
+- Creating an agent with Code Interpreter tool using `AIProjectClient.AsAIAgent()`
+- Generating files through Code Interpreter (e.g., CSV, Excel, images)
+- Extracting container file citations from agent response annotations
+- Downloading container files using the `ContainerClient` via `AIProjectClient.GetProjectOpenAIClient()`
+
+## Container files vs regular files
+
+When Code Interpreter generates a file, the file is stored inside a **container** with a `cntr_` prefixed ID. The file itself gets a `cfile_` prefixed ID.
+
+These container files **cannot** be downloaded using the standard Files API (`GetOpenAIFileClient`), which returns 404 for `cfile_` IDs. Instead, you must use the **Containers API** to download them.
+
+### Getting the ContainerClient with Foundry
+
+`AzureOpenAIClient.GetContainerClient()` is not supported and throws `InvalidOperationException`. Instead, use the project's OpenAI client which inherits directly from `OpenAI.OpenAIClient`:
+
+```csharp
+// ❌ AzureOpenAIClient does not support ContainerClient
+var azureClient = new AzureOpenAIClient(endpoint, credential);
+azureClient.GetContainerClient(); // Throws InvalidOperationException
+
+// ✅ Use AIProjectClient's project OpenAI client
+var containerClient = aiProjectClient.GetProjectOpenAIClient().GetContainerClient();
+await containerClient.DownloadContainerFileAsync("cntr_...", "cfile_...");
+```
+
+The container ID and file ID are available from the `ContainerFileCitationMessageAnnotation` annotation in the response, accessible via `CitationAnnotation.RawRepresentation`.
+
+## Prerequisites
+
+- .NET 10 SDK or later
+- Microsoft Foundry service endpoint and deployment configured
+- Azure CLI installed and authenticated (`az login`)
+
+Set the following environment variables:
+
+```powershell
+$env:AZURE_AI_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project"
+$env:AZURE_AI_MODEL_DEPLOYMENT_NAME="gpt-4o-mini" # Optional, defaults to gpt-4o-mini
+```
+
+## Run the sample
+
+```powershell
+dotnet run
+```
+
+## See also
+
+- [Code Interpreter File Download with OpenAI](../../../02-agents/AgentWithOpenAI/Agent_OpenAI_Step06_CodeInterpreterFileDownload/) — same scenario using Public OpenAI
+- [Code Interpreter](../Agent_Step14_CodeInterpreter/) — Code Interpreter without file download
diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/README.md b/dotnet/samples/02-agents/AgentsWithFoundry/README.md
index c65cb24acd..74160ec2ec 100644
--- a/dotnet/samples/02-agents/AgentsWithFoundry/README.md
+++ b/dotnet/samples/02-agents/AgentsWithFoundry/README.md
@@ -72,6 +72,7 @@ Some samples require extra tool-specific environment variables. See each sample
| [Web search](./Agent_Step21_WebSearch/) | Web search tool |
| [Memory search](./Agent_Step22_MemorySearch/) | Memory search tool |
| [Local MCP](./Agent_Step23_LocalMCP/) | Local MCP client with HTTP transport |
+| [Code interpreter file download](./Agent_Step24_CodeInterpreterFileDownload/) | Download container files generated by code interpreter |
## Running the samples
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Kit/ActionExecutorResult.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Kit/ActionExecutorResult.cs
index 99d2e29f50..4bf2a12500 100644
--- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Kit/ActionExecutorResult.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Kit/ActionExecutorResult.cs
@@ -1,5 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
+using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
+
namespace Microsoft.Agents.AI.Workflows.Declarative.Kit;
///
@@ -25,6 +27,11 @@ public sealed record class ActionExecutorResult
internal static ActionExecutorResult ThrowIfNot(object? message)
{
+ if (message is PortableValue portableValue && portableValue.IsType(out ActionExecutorResult? unwrapped))
+ {
+ return unwrapped;
+ }
+
if (message is not ActionExecutorResult executorMessage)
{
throw new DeclarativeActionException($"Unexpected message type: {message?.GetType().Name ?? "(null)"} (Expected: {nameof(ActionExecutorResult)})");
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/InvokeAzureAgentExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/InvokeAzureAgentExecutor.cs
index 24653af0f2..86efa6ec43 100644
--- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/InvokeAzureAgentExecutor.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/InvokeAzureAgentExecutor.cs
@@ -27,9 +27,11 @@ internal sealed class InvokeAzureAgentExecutor(InvokeAzureAgent model, ResponseA
public static string Resume(string id) => $"{id}_{nameof(Resume)}";
}
- public static bool RequiresInput(object? message) => message is ExternalInputRequest;
+ public static bool RequiresInput(object? message) =>
+ message is ExternalInputRequest || (message is PortableValue pv && pv.IsType(out ExternalInputRequest? _));
- public static bool RequiresNothing(object? message) => message is ActionExecutorResult;
+ public static bool RequiresNothing(object? message) =>
+ message is ActionExecutorResult || (message is PortableValue pv && pv.IsType(out ActionExecutorResult? _));
private AzureAgentUsage AgentUsage => Throw.IfNull(this.Model.Agent, $"{nameof(this.Model)}.{nameof(this.Model.Agent)}");
private AzureAgentInput? AgentInput => this.Model.Input;
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/InvokeMcpToolExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/InvokeMcpToolExecutor.cs
index b1d9a44269..7540556f64 100644
--- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/InvokeMcpToolExecutor.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/InvokeMcpToolExecutor.cs
@@ -46,12 +46,14 @@ internal sealed class InvokeMcpToolExecutor(
///
/// Determines if the message indicates external input is required.
///
- public static bool RequiresInput(object? message) => message is ExternalInputRequest;
+ public static bool RequiresInput(object? message) =>
+ message is ExternalInputRequest || (message is PortableValue pv && pv.IsType(out ExternalInputRequest? _));
///
/// Determines if the message indicates no external input is required.
///
- public static bool RequiresNothing(object? message) => message is ActionExecutorResult;
+ public static bool RequiresNothing(object? message) =>
+ message is ActionExecutorResult || (message is PortableValue pv && pv.IsType(out ActionExecutorResult? _));
///
protected override bool EmitResultEvent => false;
diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Kit/PortableValuePredicateTests.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Kit/PortableValuePredicateTests.cs
new file mode 100644
index 0000000000..4ed50afb5a
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Kit/PortableValuePredicateTests.cs
@@ -0,0 +1,191 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using FluentAssertions;
+using Microsoft.Agents.AI.Workflows.Declarative.Events;
+using Microsoft.Agents.AI.Workflows.Declarative.Kit;
+using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel;
+
+namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.Kit;
+
+///
+/// Tests that edge predicates correctly handle PortableValue-wrapped messages,
+/// which occur after checkpoint restore (JSON round-trip).
+///
+public sealed class PortableValuePredicateTests
+{
+ #region ActionExecutorResult.ThrowIfNot
+
+ [Fact]
+ public void ActionExecutorResult_ThrowIfNot_WithDirectActionExecutorResult_ReturnsResult()
+ {
+ // Arrange
+ ActionExecutorResult result = new("test-executor");
+
+ // Act
+ ActionExecutorResult actual = ActionExecutorResult.ThrowIfNot(result);
+
+ // Assert
+ actual.Should().BeSameAs(result);
+ }
+
+ [Fact]
+ public void ActionExecutorResult_ThrowIfNot_WithPortableValueWrappedActionExecutorResult_Unwraps()
+ {
+ // Arrange
+ ActionExecutorResult result = new("test-executor");
+ PortableValue wrapped = new(result);
+
+ // Act
+ ActionExecutorResult actual = ActionExecutorResult.ThrowIfNot(wrapped);
+
+ // Assert
+ actual.ExecutorId.Should().Be("test-executor");
+ }
+
+ [Fact]
+ public void ActionExecutorResult_ThrowIfNot_WithNonActionExecutorResult_Throws()
+ {
+ // Arrange
+ object message = "not an ActionExecutorResult";
+
+ // Act & Assert
+ Assert.Throws(() => ActionExecutorResult.ThrowIfNot(message));
+ }
+
+ [Fact]
+ public void ActionExecutorResult_ThrowIfNot_WithNull_Throws()
+ {
+ // Act & Assert
+ Assert.Throws(() => ActionExecutorResult.ThrowIfNot(null));
+ }
+
+ [Fact]
+ public void ActionExecutorResult_ThrowIfNot_WithPortableValueWrappedNonResult_Throws()
+ {
+ // Arrange
+ PortableValue wrapped = new("not an ActionExecutorResult");
+
+ // Act & Assert
+ Assert.Throws(() => ActionExecutorResult.ThrowIfNot(wrapped));
+ }
+
+ #endregion
+
+ #region InvokeAzureAgentExecutor Predicates
+
+ [Fact]
+ public void InvokeAzureAgentExecutor_RequiresInput_WithDirectExternalInputRequest_ReturnsTrue()
+ {
+ // Arrange
+ ExternalInputRequest request = new("test prompt");
+
+ // Act & Assert
+ InvokeAzureAgentExecutor.RequiresInput(request).Should().BeTrue();
+ }
+
+ [Fact]
+ public void InvokeAzureAgentExecutor_RequiresInput_WithPortableValueWrappedRequest_ReturnsTrue()
+ {
+ // Arrange
+ ExternalInputRequest request = new("test prompt");
+ PortableValue wrapped = new(request);
+
+ // Act & Assert
+ InvokeAzureAgentExecutor.RequiresInput(wrapped).Should().BeTrue();
+ }
+
+ [Fact]
+ public void InvokeAzureAgentExecutor_RequiresInput_WithActionExecutorResult_ReturnsFalse()
+ {
+ // Arrange
+ ActionExecutorResult result = new("test");
+
+ // Act & Assert
+ InvokeAzureAgentExecutor.RequiresInput(result).Should().BeFalse();
+ }
+
+ [Fact]
+ public void InvokeAzureAgentExecutor_RequiresNothing_WithDirectActionExecutorResult_ReturnsTrue()
+ {
+ // Arrange
+ ActionExecutorResult result = new("test");
+
+ // Act & Assert
+ InvokeAzureAgentExecutor.RequiresNothing(result).Should().BeTrue();
+ }
+
+ [Fact]
+ public void InvokeAzureAgentExecutor_RequiresNothing_WithPortableValueWrappedResult_ReturnsTrue()
+ {
+ // Arrange
+ ActionExecutorResult result = new("test");
+ PortableValue wrapped = new(result);
+
+ // Act & Assert
+ InvokeAzureAgentExecutor.RequiresNothing(wrapped).Should().BeTrue();
+ }
+
+ [Fact]
+ public void InvokeAzureAgentExecutor_RequiresNothing_WithExternalInputRequest_ReturnsFalse()
+ {
+ // Arrange
+ ExternalInputRequest request = new("test prompt");
+
+ // Act & Assert
+ InvokeAzureAgentExecutor.RequiresNothing(request).Should().BeFalse();
+ }
+
+ #endregion
+
+ #region InvokeMcpToolExecutor Predicates
+
+ [Fact]
+ public void InvokeMcpToolExecutor_RequiresInput_WithPortableValueWrappedRequest_ReturnsTrue()
+ {
+ // Arrange
+ ExternalInputRequest request = new("test prompt");
+ PortableValue wrapped = new(request);
+
+ // Act & Assert
+ InvokeMcpToolExecutor.RequiresInput(wrapped).Should().BeTrue();
+ }
+
+ [Fact]
+ public void InvokeMcpToolExecutor_RequiresNothing_WithPortableValueWrappedResult_ReturnsTrue()
+ {
+ // Arrange
+ ActionExecutorResult result = new("test");
+ PortableValue wrapped = new(result);
+
+ // Act & Assert
+ InvokeMcpToolExecutor.RequiresNothing(wrapped).Should().BeTrue();
+ }
+
+ #endregion
+
+ #region QuestionExecutor.IsComplete
+
+ [Fact]
+ public void QuestionExecutor_IsComplete_WithPortableValueWrappedResult_NullResult_ReturnsTrue()
+ {
+ // Arrange - result with null Result property means "complete"
+ ActionExecutorResult result = new("test", result: null);
+ PortableValue wrapped = new(result);
+
+ // Act & Assert
+ QuestionExecutor.IsComplete(wrapped).Should().BeTrue();
+ }
+
+ [Fact]
+ public void QuestionExecutor_IsComplete_WithPortableValueWrappedResult_NonNullResult_ReturnsFalse()
+ {
+ // Arrange - result with non-null Result property means "not complete"
+ ActionExecutorResult result = new("test", result: true);
+ PortableValue wrapped = new(result);
+
+ // Act & Assert
+ QuestionExecutor.IsComplete(wrapped).Should().BeFalse();
+ }
+
+ #endregion
+}