diff --git a/.github/workflows/dotnet-integration-tests.yml b/.github/workflows/dotnet-integration-tests.yml
index 5b08752abb..4a10694bda 100644
--- a/.github/workflows/dotnet-integration-tests.yml
+++ b/.github/workflows/dotnet-integration-tests.yml
@@ -88,6 +88,7 @@ jobs:
env:
COSMOSDB_ENDPOINT: https://localhost:8081
COSMOSDB_KEY: C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==
+ COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
OpenAI__ApiKey: ${{ secrets.OPENAI__APIKEY }}
OpenAI__ChatModelId: ${{ vars.OPENAI__CHATMODELID }}
OpenAI__ChatReasoningModelId: ${{ vars.OPENAI__CHATREASONINGMODELID }}
diff --git a/.github/workflows/merge-gatekeeper.yml b/.github/workflows/merge-gatekeeper.yml
index 52adbcb8e4..c33d3e016a 100644
--- a/.github/workflows/merge-gatekeeper.yml
+++ b/.github/workflows/merge-gatekeeper.yml
@@ -27,7 +27,7 @@ jobs:
# "Cleanup artifacts", "Agent", "Prepare", and "Upload results" are check runs
# created by an org-level GitHub App (MSDO), not by any workflow in this repo.
# They are outside our control and their transient failures should not block merges.
- IGNORED_NAMES: "CodeQL,CodeQL analysis (csharp),Cleanup artifacts,Agent,Prepare,Upload results"
+ IGNORED_NAMES: "CodeQL,CodeQL analysis (csharp),Cleanup artifacts,Agent,Prepare,Upload results,review"
with:
script: |
const timeoutSeconds = Number(process.env.TIMEOUT_SECONDS);
diff --git a/.gitignore b/.gitignore
index 9cb714813a..258a8c0704 100644
--- a/.gitignore
+++ b/.gitignore
@@ -206,6 +206,7 @@ temp*/
.temp/
# AI
+**/.checkpoints/
.claude/
.omc/
.omx/
@@ -213,6 +214,7 @@ WARP.md
**/memory-bank/
**/projectBrief.md
**/tmpclaude*
+.kiro/
# Dependency-bound validation reports
python/scripts/dependency-*-results.json
python/scripts/dependencies/dependency-*-results.json
diff --git a/docs/assets/readme-banner.png b/docs/assets/readme-banner.png
index defc110e49..0b79ce6cc9 100644
Binary files a/docs/assets/readme-banner.png and b/docs/assets/readme-banner.png differ
diff --git a/docs/decisions/0007-agent-filtering-middleware.md b/docs/decisions/0007-agent-filtering-middleware.md
index dbdd6d37d1..4fd3b57727 100644
--- a/docs/decisions/0007-agent-filtering-middleware.md
+++ b/docs/decisions/0007-agent-filtering-middleware.md
@@ -1125,7 +1125,7 @@ Naming (Python): N/A (Composable Components)
Supports: N
Observation: No explicit middleware/filters; modularity allows composable units but no dedicated interception hooks or callbacks for custom reading/modification mid-execution.
-For more details, see the official documentation: [Atomic Agents Docs](https://brainblend-ai.github.io/atomic-agents/). No specific code examples available for interception.
+No specific code examples available for interception.
#### Smolagents (Hugging Face)
diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props
index e4e4b83e71..76a4444bc1 100644
--- a/dotnet/Directory.Packages.props
+++ b/dotnet/Directory.Packages.props
@@ -41,19 +41,19 @@
-
+
-
+
-
-
+
+
@@ -72,12 +72,12 @@
-
-
-
-
-
-
+
+
+
+
+
+
@@ -86,12 +86,12 @@
-
+
-
+
@@ -138,10 +138,15 @@
+
+
+
+
+
diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx
index e395627bc9..3160eb6de7 100644
--- a/dotnet/agent-framework-dotnet.slnx
+++ b/dotnet/agent-framework-dotnet.slnx
@@ -24,7 +24,6 @@
-
@@ -129,6 +128,7 @@
+
@@ -194,6 +194,8 @@
+
+
@@ -344,6 +346,9 @@
+
+
+
@@ -622,6 +627,7 @@
+
@@ -675,5 +681,6 @@
+
diff --git a/dotnet/eng/verify-samples/AgentsSamples.cs b/dotnet/eng/verify-samples/AgentsSamples.cs
index 589b3a6fb8..66d68248f6 100644
--- a/dotnet/eng/verify-samples/AgentsSamples.cs
+++ b/dotnet/eng/verify-samples/AgentsSamples.cs
@@ -50,19 +50,6 @@ internal static class AgentsSamples
],
},
- new SampleDefinition
- {
- Name = "Agent_With_AzureAIAgentsPersistent",
- ProjectPath = "samples/02-agents/AgentProviders/Agent_With_AzureAIAgentsPersistent",
- RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
- OptionalEnvironmentVariables = ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
- ExpectedOutputDescription =
- [
- "The output should contain a joke about a pirate.",
- "The output should not contain error messages or stack traces.",
- ],
- },
-
new SampleDefinition
{
Name = "Agent_With_AzureAIProject",
diff --git a/dotnet/nuget/nuget-package.props b/dotnet/nuget/nuget-package.props
index 28bc03d112..c5c14856da 100644
--- a/dotnet/nuget/nuget-package.props
+++ b/dotnet/nuget/nuget-package.props
@@ -1,14 +1,14 @@
- 1.9.0
+ 1.10.0
1
- 260603
+ 260610
$(VersionPrefix)-rc$(RCNumber)
$(VersionPrefix)-$(VersionSuffix).$(DateSuffix).1
$(VersionPrefix)-preview.$(DateSuffix).1
$(VersionPrefix)
- 1.9.0
+ 1.10.0
Debug;Release;Publish
true
diff --git a/dotnet/samples/02-agents/AgentProviders/Agent_With_AzureAIAgentsPersistent/Program.cs b/dotnet/samples/02-agents/AgentProviders/Agent_With_AzureAIAgentsPersistent/Program.cs
deleted file mode 100644
index af41e69c77..0000000000
--- a/dotnet/samples/02-agents/AgentProviders/Agent_With_AzureAIAgentsPersistent/Program.cs
+++ /dev/null
@@ -1,44 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-#pragma warning disable CS0618 // Type or member is obsolete - sample uses deprecated PersistentAgentsClientExtensions
-
-// This sample shows how to create and use a simple AI agent with Microsoft Foundry Agents as the backend.
-
-using Azure.AI.Agents.Persistent;
-using Azure.Identity;
-using Microsoft.Agents.AI;
-
-var endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.");
-var deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-5.4-mini";
-
-const string JokerName = "Joker";
-const string JokerInstructions = "You are good at telling jokes.";
-
-// Get a client to create/retrieve server side agents with.
-// 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.
-var persistentAgentsClient = new PersistentAgentsClient(endpoint, new DefaultAzureCredential());
-
-// You can create a server side persistent agent with the Azure.AI.Agents.Persistent SDK.
-var agentMetadata = await persistentAgentsClient.Administration.CreateAgentAsync(
- model: deploymentName,
- name: JokerName,
- instructions: JokerInstructions);
-
-// You can retrieve an already created server side persistent agent as an AIAgent.
-AIAgent agent1 = await persistentAgentsClient.GetAIAgentAsync(agentMetadata.Value.Id);
-
-// You can also create a server side persistent agent and return it as an AIAgent directly.
-AIAgent agent2 = await persistentAgentsClient.CreateAIAgentAsync(
- model: deploymentName,
- name: JokerName,
- instructions: JokerInstructions);
-
-// You can then invoke the agent like any other AIAgent.
-AgentSession session = await agent1.CreateSessionAsync();
-Console.WriteLine(await agent1.RunAsync("Tell me a joke about a pirate.", session));
-
-// Cleanup for sample purposes.
-await persistentAgentsClient.Administration.DeleteAgentAsync(agent1.Id);
-await persistentAgentsClient.Administration.DeleteAgentAsync(agent2.Id);
diff --git a/dotnet/samples/02-agents/AgentProviders/Agent_With_AzureAIAgentsPersistent/README.md b/dotnet/samples/02-agents/AgentProviders/Agent_With_AzureAIAgentsPersistent/README.md
deleted file mode 100644
index dbe7c2c12f..0000000000
--- a/dotnet/samples/02-agents/AgentProviders/Agent_With_AzureAIAgentsPersistent/README.md
+++ /dev/null
@@ -1,26 +0,0 @@
-# Classic Foundry Agents
-
-This sample demonstrates how to create an agent using the classic Foundry Agents experience.
-
-# Classic vs New Foundry Agents
-
-Below is a comparison between the classic and new Foundry Agents approaches:
-
-[Migration Guide](https://learn.microsoft.com/en-us/azure/ai-foundry/agents/how-to/migrate?view=foundry)
-
-# Prerequisites
-
-Before you begin, ensure you have the following prerequisites:
-
-- .NET 10 SDK or later
-- Microsoft Foundry service endpoint and deployment configured
-- Azure CLI installed and authenticated (for Azure credential authentication)
-
-**Note**: This demo uses Azure CLI credentials for authentication. Make sure you're logged in with `az login` and have access to the Microsoft Foundry resource. For more information, see the [Azure CLI documentation](https://learn.microsoft.com/cli/azure/authenticate-azure-cli-interactively).
-
-Set the following environment variables:
-
-```powershell
-$env:AZURE_AI_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project" # Replace with your Microsoft Foundry resource endpoint
-$env:AZURE_AI_MODEL_DEPLOYMENT_NAME="gpt-5.4-mini" # Optional, defaults to gpt-5.4-mini
-```
diff --git a/dotnet/samples/02-agents/AgentProviders/Agent_With_AzureAIProject/Program.cs b/dotnet/samples/02-agents/AgentProviders/Agent_With_AzureAIProject/Program.cs
index 233705d4af..d523e3c8bd 100644
--- a/dotnet/samples/02-agents/AgentProviders/Agent_With_AzureAIProject/Program.cs
+++ b/dotnet/samples/02-agents/AgentProviders/Agent_With_AzureAIProject/Program.cs
@@ -8,8 +8,8 @@ using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Foundry;
-var endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.");
-var deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-5.4-mini";
+var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
+var deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
const string JokerName = "JokerAgent";
diff --git a/dotnet/samples/02-agents/AgentProviders/Agent_With_AzureAIProject/README.md b/dotnet/samples/02-agents/AgentProviders/Agent_With_AzureAIProject/README.md
index 0e225751fb..4a1412838d 100644
--- a/dotnet/samples/02-agents/AgentProviders/Agent_With_AzureAIProject/README.md
+++ b/dotnet/samples/02-agents/AgentProviders/Agent_With_AzureAIProject/README.md
@@ -1,4 +1,4 @@
-# New Foundry Agents
+# New Foundry Agents
This sample demonstrates how to create an agent using the new Foundry Agents experience.
@@ -21,6 +21,6 @@ Before you begin, ensure you have the following prerequisites:
Set the following environment variables:
```powershell
-$env:AZURE_AI_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project" # Replace with your Microsoft Foundry resource endpoint
-$env:AZURE_AI_MODEL_DEPLOYMENT_NAME="gpt-5.4-mini" # Optional, defaults to gpt-5.4-mini
+$env:FOUNDRY_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project" # Replace with your Microsoft Foundry resource endpoint
+$env:FOUNDRY_MODEL="gpt-5.4-mini" # Optional, defaults to gpt-5.4-mini
```
diff --git a/dotnet/samples/02-agents/AgentProviders/Agent_With_AzureFoundryModel/Program.cs b/dotnet/samples/02-agents/AgentProviders/Agent_With_AzureFoundryModel/Program.cs
index 556b52bf17..bc55293037 100644
--- a/dotnet/samples/02-agents/AgentProviders/Agent_With_AzureFoundryModel/Program.cs
+++ b/dotnet/samples/02-agents/AgentProviders/Agent_With_AzureFoundryModel/Program.cs
@@ -13,7 +13,7 @@ using OpenAI.Chat;
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
var apiKey = Environment.GetEnvironmentVariable("AZURE_OPENAI_API_KEY");
-var model = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "Phi-4-mini-instruct";
+var model = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "Phi-4-mini-instruct";
// Since we are using the OpenAI Client SDK, we need to override the default endpoint to point to Microsoft Foundry.
var clientOptions = new OpenAIClientOptions() { Endpoint = new Uri(endpoint) };
diff --git a/dotnet/samples/02-agents/AgentProviders/Agent_With_AzureFoundryModel/README.md b/dotnet/samples/02-agents/AgentProviders/Agent_With_AzureFoundryModel/README.md
index 9bc4d60881..9e518e9456 100644
--- a/dotnet/samples/02-agents/AgentProviders/Agent_With_AzureFoundryModel/README.md
+++ b/dotnet/samples/02-agents/AgentProviders/Agent_With_AzureFoundryModel/README.md
@@ -1,4 +1,4 @@
-## Overview
+## Overview
This sample shows how to use the OpenAI SDK to create and use a simple AI agent with any model hosted in Microsoft Foundry.
@@ -13,7 +13,7 @@ Before you begin, ensure you have the following prerequisites:
- .NET 10 SDK or later
- Microsoft Foundry resource
- A model deployment in your Microsoft Foundry resource. This example defaults to using the `Phi-4-mini-instruct` model,
-so if you want to use a different model, ensure that you set your `AZURE_AI_MODEL_DEPLOYMENT_NAME` environment
+so if you want to use a different model, ensure that you set your `FOUNDRY_MODEL` environment
variable to the name of your deployed model.
- An API key or role based authentication to access the Microsoft Foundry resource
@@ -30,5 +30,5 @@ $env:AZURE_OPENAI_ENDPOINT="https://ai-foundry-.services.ai.azur
$env:AZURE_OPENAI_API_KEY="************"
# Optional, defaults to Phi-4-mini-instruct
-$env:AZURE_AI_MODEL_DEPLOYMENT_NAME="Phi-4-mini-instruct"
+$env:FOUNDRY_MODEL="Phi-4-mini-instruct"
```
diff --git a/dotnet/samples/02-agents/AgentProviders/README.md b/dotnet/samples/02-agents/AgentProviders/README.md
index 5584fdc810..7c0e003713 100644
--- a/dotnet/samples/02-agents/AgentProviders/README.md
+++ b/dotnet/samples/02-agents/AgentProviders/README.md
@@ -16,7 +16,6 @@ See the README.md for each sample for the prerequisites for that sample.
|---|---|
|[Creating an AIAgent with A2A](./Agent_With_A2A/)|This sample demonstrates how to create AIAgent for an existing A2A agent.|
|[Creating an AIAgent with Anthropic](./Agent_With_Anthropic/)|This sample demonstrates how to create an AIAgent using Anthropic Claude models as the underlying inference service|
-|[Creating an AIAgent with Foundry Agents using Azure.AI.Agents.Persistent](./Agent_With_AzureAIAgentsPersistent/)|This sample demonstrates how to create a Foundry Persistent agent and expose it as an AIAgent using the Azure.AI.Agents.Persistent SDK|
|[Creating an AIAgent with Foundry Agents using Azure.AI.Project](./Agent_With_AzureAIProject/)|This sample demonstrates how to create an Foundry Project agent and expose it as an AIAgent using the Azure.AI.Project SDK|
|[Creating an AIAgent with Foundry Model](./Agent_With_AzureFoundryModel/)|This sample demonstrates how to use any model deployed to Microsoft Foundry to create an AIAgent|
|[Creating an AIAgent with Azure OpenAI ChatCompletion](./Agent_With_AzureOpenAIChatCompletion/)|This sample demonstrates how to create an AIAgent using Azure OpenAI ChatCompletion as the underlying inference service|
diff --git a/dotnet/samples/02-agents/AgentSkills/Agent_Step01_FileBasedSkills/Program.cs b/dotnet/samples/02-agents/AgentSkills/Agent_Step01_FileBasedSkills/Program.cs
index c9dc86e3b4..83c6823089 100644
--- a/dotnet/samples/02-agents/AgentSkills/Agent_Step01_FileBasedSkills/Program.cs
+++ b/dotnet/samples/02-agents/AgentSkills/Agent_Step01_FileBasedSkills/Program.cs
@@ -26,6 +26,9 @@ var skillsProvider = new AgentSkillsProvider(
SubprocessScriptRunner.RunAsync);
// --- Agent Setup ---
+// 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 AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential())
.GetResponsesClient()
.AsAIAgent(new ChatClientAgentOptions
diff --git a/dotnet/samples/02-agents/AgentSkills/Agent_Step02_CodeDefinedSkills/Program.cs b/dotnet/samples/02-agents/AgentSkills/Agent_Step02_CodeDefinedSkills/Program.cs
index 8c1cfa33bb..059ec7e4a4 100644
--- a/dotnet/samples/02-agents/AgentSkills/Agent_Step02_CodeDefinedSkills/Program.cs
+++ b/dotnet/samples/02-agents/AgentSkills/Agent_Step02_CodeDefinedSkills/Program.cs
@@ -67,6 +67,9 @@ var unitConverterSkill = new AgentInlineSkill(
var skillsProvider = new AgentSkillsProvider(unitConverterSkill);
// --- Agent Setup ---
+// 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 AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential())
.GetResponsesClient()
.AsAIAgent(new ChatClientAgentOptions
diff --git a/dotnet/samples/02-agents/AgentSkills/Agent_Step03_ClassBasedSkills/Program.cs b/dotnet/samples/02-agents/AgentSkills/Agent_Step03_ClassBasedSkills/Program.cs
index aa70c4461e..d255f1be6e 100644
--- a/dotnet/samples/02-agents/AgentSkills/Agent_Step03_ClassBasedSkills/Program.cs
+++ b/dotnet/samples/02-agents/AgentSkills/Agent_Step03_ClassBasedSkills/Program.cs
@@ -22,6 +22,9 @@ var unitConverter = new UnitConverterSkill();
var skillsProvider = new AgentSkillsProvider(unitConverter);
// --- Agent Setup ---
+// 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 AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential())
.GetResponsesClient()
.AsAIAgent(new ChatClientAgentOptions
diff --git a/dotnet/samples/02-agents/AgentSkills/Agent_Step04_MixedSkills/Program.cs b/dotnet/samples/02-agents/AgentSkills/Agent_Step04_MixedSkills/Program.cs
index 28d5cb9ee9..a67658b444 100644
--- a/dotnet/samples/02-agents/AgentSkills/Agent_Step04_MixedSkills/Program.cs
+++ b/dotnet/samples/02-agents/AgentSkills/Agent_Step04_MixedSkills/Program.cs
@@ -64,6 +64,9 @@ var skillsProvider = new AgentSkillsProviderBuilder()
.Build();
// --- Agent Setup ---
+// 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 AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential())
.GetResponsesClient()
.AsAIAgent(new ChatClientAgentOptions
diff --git a/dotnet/samples/02-agents/AgentSkills/Agent_Step05_SkillsWithDI/Program.cs b/dotnet/samples/02-agents/AgentSkills/Agent_Step05_SkillsWithDI/Program.cs
index 251503a918..7c5e594fd2 100644
--- a/dotnet/samples/02-agents/AgentSkills/Agent_Step05_SkillsWithDI/Program.cs
+++ b/dotnet/samples/02-agents/AgentSkills/Agent_Step05_SkillsWithDI/Program.cs
@@ -80,6 +80,9 @@ var weightSkill = new WeightConverterSkill();
var skillsProvider = new AgentSkillsProvider(distanceSkill, weightSkill);
// --- Agent Setup ---
+// 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 AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential())
.GetResponsesClient()
.AsAIAgent(
diff --git a/dotnet/samples/02-agents/AgentWithCodeAct/AgentWithCodeAct_Step01_Interpreter/Program.cs b/dotnet/samples/02-agents/AgentWithCodeAct/AgentWithCodeAct_Step01_Interpreter/Program.cs
index ed3b1315cf..7737eed3c2 100644
--- a/dotnet/samples/02-agents/AgentWithCodeAct/AgentWithCodeAct_Step01_Interpreter/Program.cs
+++ b/dotnet/samples/02-agents/AgentWithCodeAct/AgentWithCodeAct_Step01_Interpreter/Program.cs
@@ -16,6 +16,9 @@ var guestPath = Environment.GetEnvironmentVariable("HYPERLIGHT_PYTHON_GUEST_PATH
using var codeAct = new HyperlightCodeActProvider(HyperlightCodeActProviderOptions.CreateForWasm(guestPath));
+// 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 AzureOpenAIClient(
new Uri(endpoint),
new DefaultAzureCredential())
diff --git a/dotnet/samples/02-agents/AgentWithCodeAct/AgentWithCodeAct_Step02_ToolEnabled/Program.cs b/dotnet/samples/02-agents/AgentWithCodeAct/AgentWithCodeAct_Step02_ToolEnabled/Program.cs
index 3ae1faccf2..563fee388b 100644
--- a/dotnet/samples/02-agents/AgentWithCodeAct/AgentWithCodeAct_Step02_ToolEnabled/Program.cs
+++ b/dotnet/samples/02-agents/AgentWithCodeAct/AgentWithCodeAct_Step02_ToolEnabled/Program.cs
@@ -39,6 +39,9 @@ options.Tools = [fetchDocs, queryData, sendEmail];
using var codeAct = new HyperlightCodeActProvider(options);
+// 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 AzureOpenAIClient(
new Uri(endpoint),
new DefaultAzureCredential())
diff --git a/dotnet/samples/02-agents/AgentWithCodeAct/AgentWithCodeAct_Step03_ManualWiring/Program.cs b/dotnet/samples/02-agents/AgentWithCodeAct/AgentWithCodeAct_Step03_ManualWiring/Program.cs
index fae83b14fd..cae0613429 100644
--- a/dotnet/samples/02-agents/AgentWithCodeAct/AgentWithCodeAct_Step03_ManualWiring/Program.cs
+++ b/dotnet/samples/02-agents/AgentWithCodeAct/AgentWithCodeAct_Step03_ManualWiring/Program.cs
@@ -31,6 +31,9 @@ var instructions =
+ "and calling `execute_code` instead of computing values yourself.\n\n"
+ executeCode.BuildInstructions(toolsVisibleToModel: false);
+// 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 AzureOpenAIClient(
new Uri(endpoint),
new DefaultAzureCredential())
diff --git a/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step03_MemoryUsingValkey/AgentWithMemory_Step03_MemoryUsingValkey.csproj b/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step03_MemoryUsingValkey/AgentWithMemory_Step03_MemoryUsingValkey.csproj
new file mode 100644
index 0000000000..1217591bc1
--- /dev/null
+++ b/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step03_MemoryUsingValkey/AgentWithMemory_Step03_MemoryUsingValkey.csproj
@@ -0,0 +1,22 @@
+
+
+
+ Exe
+ net10.0
+
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step03_MemoryUsingValkey/Program.cs b/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step03_MemoryUsingValkey/Program.cs
new file mode 100644
index 0000000000..6faa02a0f3
--- /dev/null
+++ b/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step03_MemoryUsingValkey/Program.cs
@@ -0,0 +1,55 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+// This sample demonstrates using Valkey for persistent chat history with the Agent Framework.
+// ValkeyChatHistoryProvider persists conversation history across sessions using Valkey lists.
+//
+// Prerequisites:
+// - A running Valkey server (any version):
+// docker run -d --name valkey -p 6379:6379 valkey/valkey:latest
+// - Azure OpenAI endpoint and deployment configured via environment variables
+
+using Azure.AI.OpenAI;
+using Azure.Identity;
+using Microsoft.Agents.AI;
+using Microsoft.Agents.AI.Valkey;
+using Microsoft.Extensions.AI;
+using OpenAI.Chat;
+using Valkey.Glide;
+
+var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
+var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5.4-mini";
+var valkeyConnection = Environment.GetEnvironmentVariable("VALKEY_CONNECTION") ?? "localhost:6379";
+
+var connection = await ConnectionMultiplexer.ConnectAsync(valkeyConnection);
+
+Console.WriteLine("=== ValkeyChatHistoryProvider — Persistent Chat History ===\n");
+
+var historyProvider = new ValkeyChatHistoryProvider(
+ connection,
+ _ => new ValkeyChatHistoryProvider.State($"sample-{Guid.NewGuid():N}"),
+ new ValkeyChatHistoryProviderOptions
+ {
+ KeyPrefix = "sample_chat",
+ MaxMessages = 20
+ });
+
+AIAgent historyAgent = new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential())
+ .GetChatClient(deploymentName)
+ .AsAIAgent(new ChatClientAgentOptions()
+ {
+ ChatOptions = new() { Instructions = "You are a helpful assistant that remembers our conversation." },
+ ChatHistoryProvider = historyProvider
+ });
+
+AgentSession session1 = await historyAgent.CreateSessionAsync();
+Console.WriteLine(await historyAgent.RunAsync("Hello! My name is Alex and I'm a software engineer.", session1));
+Console.WriteLine(await historyAgent.RunAsync("I'm working on a project using Valkey for caching.", session1));
+Console.WriteLine(await historyAgent.RunAsync("What do you remember about me?", session1));
+
+var messageCount = await historyProvider.GetMessageCountAsync(session1);
+Console.WriteLine($"\n Stored {messageCount} messages in Valkey.\n");
+
+// Clean up
+connection.Dispose();
+
+Console.WriteLine("Done!");
diff --git a/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step03_MemoryUsingValkey/README.md b/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step03_MemoryUsingValkey/README.md
new file mode 100644
index 0000000000..08f65ecffa
--- /dev/null
+++ b/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step03_MemoryUsingValkey/README.md
@@ -0,0 +1,30 @@
+# Agent with Memory Using Valkey
+
+This sample demonstrates using Valkey for persistent chat history with the Agent Framework.
+
+## Components
+
+- **ValkeyChatHistoryProvider** — Persists conversation history across sessions using Valkey lists. Works with any Valkey or Redis OSS server (no search module required).
+
+## Prerequisites
+
+- Azure OpenAI endpoint and deployment
+- A running Valkey server (any version):
+
+```bash
+docker run -d --name valkey -p 6379:6379 valkey/valkey:latest
+```
+
+## Environment Variables
+
+| Variable | Description | Default |
+|---|---|---|
+| `AZURE_OPENAI_ENDPOINT` | Azure OpenAI endpoint URL | (required) |
+| `AZURE_OPENAI_DEPLOYMENT_NAME` | Model deployment name | `gpt-5.4-mini` |
+| `VALKEY_CONNECTION` | Valkey connection string | `localhost:6379` |
+
+## Running
+
+```bash
+dotnet run
+```
diff --git a/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step03_MemoryUsingValkey_Bedrock/AgentWithMemory_Step03_MemoryUsingValkey_Bedrock.csproj b/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step03_MemoryUsingValkey_Bedrock/AgentWithMemory_Step03_MemoryUsingValkey_Bedrock.csproj
new file mode 100644
index 0000000000..274ac15c97
--- /dev/null
+++ b/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step03_MemoryUsingValkey_Bedrock/AgentWithMemory_Step03_MemoryUsingValkey_Bedrock.csproj
@@ -0,0 +1,20 @@
+
+
+
+ Exe
+ net10.0
+
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step03_MemoryUsingValkey_Bedrock/Program.cs b/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step03_MemoryUsingValkey_Bedrock/Program.cs
new file mode 100644
index 0000000000..6f3027681f
--- /dev/null
+++ b/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step03_MemoryUsingValkey_Bedrock/Program.cs
@@ -0,0 +1,57 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+// This sample demonstrates using Valkey for persistent chat history with the Agent Framework,
+// powered by Amazon Bedrock.
+//
+// Prerequisites:
+// - A running Valkey server (any version):
+// docker run -d --name valkey -p 6379:6379 valkey/valkey:latest
+// - AWS credentials configured (environment variables, AWS profile, or IAM role)
+// - Access to an Amazon Bedrock model (e.g., Anthropic Claude)
+
+using Amazon;
+using Amazon.BedrockRuntime;
+using Microsoft.Agents.AI;
+using Microsoft.Agents.AI.Valkey;
+using Microsoft.Extensions.AI;
+using Valkey.Glide;
+
+var awsRegion = Environment.GetEnvironmentVariable("AWS_REGION") ?? "us-east-1";
+var modelId = Environment.GetEnvironmentVariable("BEDROCK_MODEL_ID") ?? "anthropic.claude-3-5-sonnet-20241022-v2:0";
+var valkeyConnection = Environment.GetEnvironmentVariable("VALKEY_CONNECTION") ?? "localhost:6379";
+
+// Create the Bedrock runtime client.
+var bedrockRuntime = new AmazonBedrockRuntimeClient(RegionEndpoint.GetBySystemName(awsRegion));
+IChatClient chatClient = bedrockRuntime.AsIChatClient(modelId);
+
+var connection = await ConnectionMultiplexer.ConnectAsync(valkeyConnection);
+
+Console.WriteLine("=== ValkeyChatHistoryProvider — Persistent Chat History (Bedrock) ===\n");
+
+var historyProvider = new ValkeyChatHistoryProvider(
+ connection,
+ _ => new ValkeyChatHistoryProvider.State($"bedrock-sample-{Guid.NewGuid():N}"),
+ new ValkeyChatHistoryProviderOptions
+ {
+ KeyPrefix = "bedrock_chat",
+ MaxMessages = 20
+ });
+
+AIAgent historyAgent = chatClient.AsAIAgent(new ChatClientAgentOptions()
+{
+ ChatOptions = new() { Instructions = "You are a helpful assistant that remembers our conversation." },
+ ChatHistoryProvider = historyProvider
+});
+
+AgentSession session1 = await historyAgent.CreateSessionAsync();
+Console.WriteLine(await historyAgent.RunAsync("Hello! My name is Alex and I'm a software engineer.", session1));
+Console.WriteLine(await historyAgent.RunAsync("I'm working on a project using Valkey for caching.", session1));
+Console.WriteLine(await historyAgent.RunAsync("What do you remember about me?", session1));
+
+var messageCount = await historyProvider.GetMessageCountAsync(session1);
+Console.WriteLine($"\n Stored {messageCount} messages in Valkey.\n");
+
+// Clean up
+connection.Dispose();
+
+Console.WriteLine("Done!");
diff --git a/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step03_MemoryUsingValkey_Bedrock/README.md b/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step03_MemoryUsingValkey_Bedrock/README.md
new file mode 100644
index 0000000000..06d4012bd9
--- /dev/null
+++ b/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step03_MemoryUsingValkey_Bedrock/README.md
@@ -0,0 +1,41 @@
+# Agent with Memory Using Valkey + Amazon Bedrock
+
+This sample demonstrates using Valkey for persistent chat history with the Agent Framework, powered by Amazon Bedrock via the `AWSSDK.Extensions.Bedrock.MEAI` adapter.
+
+## Components
+
+- **ValkeyChatHistoryProvider** — Persists conversation history across sessions using Valkey lists. Works with any Valkey or Redis OSS server (no search module required).
+- **Amazon Bedrock** — Provides the LLM via `AWSSDK.Extensions.Bedrock.MEAI`, which implements `IChatClient` from `Microsoft.Extensions.AI`.
+
+## Prerequisites
+
+- AWS credentials configured (environment variables, AWS CLI profile, or IAM role)
+- Access to an Amazon Bedrock model (e.g., Anthropic Claude 3.5 Sonnet)
+- A running Valkey server (any version):
+
+```bash
+docker run -d --name valkey -p 6379:6379 valkey/valkey:latest
+```
+
+## Environment Variables
+
+| Variable | Description | Default |
+|---|---|---|
+| `AWS_REGION` | AWS region for Bedrock | `us-east-1` |
+| `BEDROCK_MODEL_ID` | Bedrock model identifier | `anthropic.claude-3-5-sonnet-20241022-v2:0` |
+| `VALKEY_CONNECTION` | Valkey connection string | `localhost:6379` |
+| `AWS_ACCESS_KEY_ID` | AWS access key (if not using profile/role) | — |
+| `AWS_SECRET_ACCESS_KEY` | AWS secret key (if not using profile/role) | — |
+
+## Running
+
+```bash
+# Using default AWS credential chain (profile, env vars, or IAM role)
+dotnet run
+
+# Or with explicit credentials
+export AWS_ACCESS_KEY_ID="your-access-key"
+export AWS_SECRET_ACCESS_KEY="your-secret-key"
+export AWS_REGION="us-east-1"
+dotnet run
+```
diff --git a/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry/Program.cs b/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry/Program.cs
index 402ae47a2d..6129a097c6 100644
--- a/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry/Program.cs
+++ b/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry/Program.cs
@@ -13,9 +13,9 @@ using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Foundry;
-string foundryEndpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.");
+string foundryEndpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
string memoryStoreName = Environment.GetEnvironmentVariable("AZURE_AI_MEMORY_STORE_ID") ?? "memory-store-sample";
-string deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-5.4-mini";
+string deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
string embeddingModelName = Environment.GetEnvironmentVariable("AZURE_AI_EMBEDDING_DEPLOYMENT_NAME") ?? "text-embedding-ada-002";
// Create an AIProjectClient for Foundry with Azure Identity authentication.
diff --git a/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry/README.md b/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry/README.md
index e863b2eada..f0ed4e43cd 100644
--- a/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry/README.md
+++ b/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry/README.md
@@ -1,4 +1,4 @@
-# Agent with Memory Using Microsoft Foundry
+# Agent with Memory Using Microsoft Foundry
This sample demonstrates how to create and run an agent that uses Microsoft Foundry's managed memory service to extract and retrieve individual memories across sessions.
@@ -22,11 +22,11 @@ This sample demonstrates how to create and run an agent that uses Microsoft Foun
```bash
# Microsoft Foundry project endpoint and memory store name
-export AZURE_AI_PROJECT_ENDPOINT="https://your-account.services.ai.azure.com/api/projects/your-project"
+export FOUNDRY_PROJECT_ENDPOINT="https://your-account.services.ai.azure.com/api/projects/your-project"
export AZURE_AI_MEMORY_STORE_ID="my_memory_store"
# Model deployment names (models deployed in your Foundry project)
-export AZURE_AI_MODEL_DEPLOYMENT_NAME="gpt-5.4-mini"
+export FOUNDRY_MODEL="gpt-5.4-mini"
export AZURE_AI_EMBEDDING_DEPLOYMENT_NAME="text-embedding-ada-002"
```
diff --git a/dotnet/samples/02-agents/AgentWithRAG/AgentWithRAG_Step04_FoundryServiceRAG/Program.cs b/dotnet/samples/02-agents/AgentWithRAG/AgentWithRAG_Step04_FoundryServiceRAG/Program.cs
index 8fc21174a1..0d3cb06fd4 100644
--- a/dotnet/samples/02-agents/AgentWithRAG/AgentWithRAG_Step04_FoundryServiceRAG/Program.cs
+++ b/dotnet/samples/02-agents/AgentWithRAG/AgentWithRAG_Step04_FoundryServiceRAG/Program.cs
@@ -13,8 +13,8 @@ using OpenAI.Files;
using OpenAI.Responses;
using OpenAI.VectorStores;
-var endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.");
-var deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-5.4-mini";
+var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
+var deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
// Create an AI Project client and get an OpenAI client that works with the foundry service.
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
diff --git a/dotnet/samples/02-agents/AgentWithRAG/AgentWithRAG_Step05_Neo4jGraphRAG/AgentWithRAG_Step05_Neo4jGraphRAG.csproj b/dotnet/samples/02-agents/AgentWithRAG/AgentWithRAG_Step05_Neo4jGraphRAG/AgentWithRAG_Step05_Neo4jGraphRAG.csproj
index a25c626323..5476ec3221 100644
--- a/dotnet/samples/02-agents/AgentWithRAG/AgentWithRAG_Step05_Neo4jGraphRAG/AgentWithRAG_Step05_Neo4jGraphRAG.csproj
+++ b/dotnet/samples/02-agents/AgentWithRAG/AgentWithRAG_Step05_Neo4jGraphRAG/AgentWithRAG_Step05_Neo4jGraphRAG.csproj
@@ -23,7 +23,7 @@
-
+
diff --git a/dotnet/samples/02-agents/Agents/Agent_Step07_AsMcpTool/Program.cs b/dotnet/samples/02-agents/Agents/Agent_Step07_AsMcpTool/Program.cs
index 82b9e16fdd..37c0d68d6a 100644
--- a/dotnet/samples/02-agents/Agents/Agent_Step07_AsMcpTool/Program.cs
+++ b/dotnet/samples/02-agents/Agents/Agent_Step07_AsMcpTool/Program.cs
@@ -10,8 +10,8 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using ModelContextProtocol.Server;
-var endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.");
-var deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-5.4-mini";
+var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
+var deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-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
diff --git a/dotnet/samples/02-agents/Agents/Agent_Step07_AsMcpTool/README.md b/dotnet/samples/02-agents/Agents/Agent_Step07_AsMcpTool/README.md
index 14b0835151..18dfabd9c7 100644
--- a/dotnet/samples/02-agents/Agents/Agent_Step07_AsMcpTool/README.md
+++ b/dotnet/samples/02-agents/Agents/Agent_Step07_AsMcpTool/README.md
@@ -1,4 +1,4 @@
-This sample demonstrates how to expose an existing AI agent as an MCP tool.
+This sample demonstrates how to expose an existing AI agent as an MCP tool.
## Run the sample
@@ -21,9 +21,9 @@ To use the [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector)
```
1. Open a web browser and navigate to the URL displayed in the terminal. If not opened automatically, this will open the MCP Inspector interface.
1. In the MCP Inspector interface, add the following environment variables to allow your MCP server to access Microsoft Foundry Project to create and run the agent:
- - AZURE_AI_PROJECT_ENDPOINT = https://your-resource.openai.azure.com/ # Replace with your Microsoft Foundry Project endpoint
- - AZURE_AI_MODEL_DEPLOYMENT_NAME = gpt-5.4-mini # Replace with your model deployment name
+ - FOUNDRY_PROJECT_ENDPOINT = https://your-resource.openai.azure.com/ # Replace with your Microsoft Foundry Project endpoint
+ - FOUNDRY_MODEL = gpt-5.4-mini # Replace with your model deployment name
1. Find and click the `Connect` button in the MCP Inspector interface to connect to the MCP server.
1. As soon as the connection is established, open the `Tools` tab in the MCP Inspector interface and select the `Joker` tool from the list.
1. Specify your prompt as a value for the `query` argument, for example: `Tell me a joke about a pirate` and click the `Run Tool` button to run the tool.
-1. The agent will process the request and return a response in accordance with the provided instructions that instruct it to always start each joke with 'Aye aye, captain!'.
\ No newline at end of file
+1. The agent will process the request and return a response in accordance with the provided instructions that instruct it to always start each joke with 'Aye aye, captain!'.
diff --git a/dotnet/samples/02-agents/Agents/Agent_Step15_DeepResearch/Program.cs b/dotnet/samples/02-agents/Agents/Agent_Step15_DeepResearch/Program.cs
index 92222f64e7..e4ed51ddac 100644
--- a/dotnet/samples/02-agents/Agents/Agent_Step15_DeepResearch/Program.cs
+++ b/dotnet/samples/02-agents/Agents/Agent_Step15_DeepResearch/Program.cs
@@ -8,9 +8,9 @@ using Azure.AI.Agents.Persistent;
using Azure.Identity;
using Microsoft.Agents.AI;
-var endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.");
+var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
var deepResearchDeploymentName = Environment.GetEnvironmentVariable("AZURE_AI_REASONING_DEPLOYMENT_NAME") ?? "o3-deep-research";
-var modelDeploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-5.4-mini";
+var modelDeploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
var bingConnectionId = Environment.GetEnvironmentVariable("AZURE_AI_BING_CONNECTION_ID") ?? throw new InvalidOperationException("AZURE_AI_BING_CONNECTION_ID is not set.");
// Configure extended network timeout for long-running Deep Research tasks.
diff --git a/dotnet/samples/02-agents/Agents/Agent_Step15_DeepResearch/README.md b/dotnet/samples/02-agents/Agents/Agent_Step15_DeepResearch/README.md
index ee3c0935a2..ca2ffac65b 100644
--- a/dotnet/samples/02-agents/Agents/Agent_Step15_DeepResearch/README.md
+++ b/dotnet/samples/02-agents/Agents/Agent_Step15_DeepResearch/README.md
@@ -1,4 +1,4 @@
-# What this sample demonstrates
+# What this sample demonstrates
This sample demonstrates how to create an Azure AI Agent with the Deep Research Tool, which leverages the o3-deep-research reasoning model to perform comprehensive research on complex topics.
@@ -37,7 +37,7 @@ Set the following environment variables:
```powershell
# Replace with your Microsoft Foundry project endpoint
-$env:AZURE_AI_PROJECT_ENDPOINT="https://your-project.services.ai.azure.com/"
+$env:FOUNDRY_PROJECT_ENDPOINT="https://your-project.services.ai.azure.com/"
# Replace with your Bing Grounding connection ID (full ARM resource URI)
$env:AZURE_AI_BING_CONNECTION_ID="/subscriptions//resourceGroups//providers/Microsoft.CognitiveServices/accounts//projects//connections/"
@@ -46,4 +46,4 @@ $env:AZURE_AI_BING_CONNECTION_ID="/subscriptions//resourceGroups//pr
$env:AZURE_AI_REASONING_DEPLOYMENT_NAME="o3-deep-research"
# Optional, defaults to gpt-5.4-mini
-$env:AZURE_AI_MODEL_DEPLOYMENT_NAME="gpt-5.4-mini"
+$env:FOUNDRY_MODEL="gpt-5.4-mini"
diff --git a/dotnet/samples/02-agents/Agents/Agent_Step21_ShellWithEnvironment/Program.cs b/dotnet/samples/02-agents/Agents/Agent_Step21_ShellWithEnvironment/Program.cs
index 447dfe92ee..9731f464db 100644
--- a/dotnet/samples/02-agents/Agents/Agent_Step21_ShellWithEnvironment/Program.cs
+++ b/dotnet/samples/02-agents/Agents/Agent_Step21_ShellWithEnvironment/Program.cs
@@ -40,6 +40,9 @@ using OpenAI.Chat;
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5.4-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.
var chatClient = new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential())
.GetChatClient(deploymentName);
diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step00_FoundryAgentLifecycle/Program.cs b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step00_FoundryAgentLifecycle/Program.cs
index 0803418ca4..7328366433 100644
--- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step00_FoundryAgentLifecycle/Program.cs
+++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step00_FoundryAgentLifecycle/Program.cs
@@ -9,13 +9,16 @@ using Azure.AI.Projects.Agents;
using Azure.Identity;
using Microsoft.Agents.AI.Foundry;
-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-5.4-mini";
+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-mini";
const string JokerName = "JokerAgent";
// Create the AIProjectClient to manage server-side agents.
-AIProjectClient aiProjectClient = new(new Uri(endpoint), new AzureCliCredential());
+// 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 a server-side agent version using the native SDK.
ProjectsAgentVersion agentVersion = await aiProjectClient.AgentAdministrationClient.CreateAgentVersionAsync(
diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step00_FoundryAgentLifecycle/README.md b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step00_FoundryAgentLifecycle/README.md
index 8179c3b299..50b00dc4b7 100644
--- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step00_FoundryAgentLifecycle/README.md
+++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step00_FoundryAgentLifecycle/README.md
@@ -1,4 +1,4 @@
-# Agent Step 00 - FoundryAgent Lifecycle
+# Agent Step 00 - FoundryAgent Lifecycle
This sample demonstrates the full lifecycle of a `FoundryAgent` backed by a server-side versioned agent in Microsoft Foundry: create → run → delete.
@@ -6,14 +6,14 @@ This sample demonstrates the full lifecycle of a `FoundryAgent` backed by a serv
- A Microsoft Foundry project endpoint
- A model deployment name (defaults to `gpt-5.4-mini`)
-- Azure CLI installed and authenticated
+- An authenticated Azure identity (for example, sign in with `az login`)
## Environment Variables
| Variable | Description | Required |
| --- | --- | --- |
-| `AZURE_AI_PROJECT_ENDPOINT` | Microsoft Foundry project endpoint | Yes |
-| `AZURE_AI_MODEL_DEPLOYMENT_NAME` | Model deployment name | No (defaults to `gpt-5.4-mini`) |
+| `FOUNDRY_PROJECT_ENDPOINT` | Microsoft Foundry project endpoint | Yes |
+| `FOUNDRY_MODEL` | Model deployment name | No (defaults to `gpt-5.4-mini`) |
## Running the sample
diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step01_Basics/Program.cs b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step01_Basics/Program.cs
index 403bae05c2..200a9bf530 100644
--- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step01_Basics/Program.cs
+++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step01_Basics/Program.cs
@@ -6,8 +6,8 @@ using Azure.AI.Projects;
using Azure.Identity;
using Microsoft.Agents.AI;
-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-5.4-mini";
+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-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
diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step01_Basics/README.md b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step01_Basics/README.md
index 612bd21891..09bd0f7c02 100644
--- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step01_Basics/README.md
+++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step01_Basics/README.md
@@ -14,15 +14,15 @@ Before you begin, ensure you have the following prerequisites:
- .NET 10 SDK or later
- Microsoft Foundry service endpoint and deployment configured
-- Azure CLI installed and authenticated (for Azure credential authentication)
+- An authenticated Azure identity (for example, sign in with `az login`)
-**Note**: This demo uses Azure CLI credentials for authentication. Make sure you're logged in with `az login` and have access to the Microsoft Foundry resource. For more information, see the [Azure CLI documentation](https://learn.microsoft.com/cli/azure/authenticate-azure-cli-interactively).
+**Note**: This sample uses `DefaultAzureCredential`. `az login` is the easiest local development path, but Visual Studio, VS Code, and managed identity credentials also work when available.
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-5.4-mini"
+$env:FOUNDRY_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project"
+$env:FOUNDRY_MODEL="gpt-5.4-mini"
```
## Run the sample
diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step02.1_MultiturnConversation/Program.cs b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step02.1_MultiturnConversation/Program.cs
index e00982199f..5bdda025c7 100644
--- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step02.1_MultiturnConversation/Program.cs
+++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step02.1_MultiturnConversation/Program.cs
@@ -7,8 +7,8 @@ using Azure.AI.Projects;
using Azure.Identity;
using Microsoft.Agents.AI;
-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-5.4-mini";
+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-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
diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step02.1_MultiturnConversation/README.md b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step02.1_MultiturnConversation/README.md
index f34c486b53..7394ff432a 100644
--- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step02.1_MultiturnConversation/README.md
+++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step02.1_MultiturnConversation/README.md
@@ -15,15 +15,15 @@ Before you begin, ensure you have the following prerequisites:
- .NET 10 SDK or later
- Microsoft Foundry service endpoint and deployment configured
-- Azure CLI installed and authenticated (for Azure credential authentication)
+- An authenticated Azure identity (for example, sign in with `az login`)
-**Note**: This demo uses Azure CLI credentials for authentication. Make sure you're logged in with `az login` and have access to the Microsoft Foundry resource. For more information, see the [Azure CLI documentation](https://learn.microsoft.com/cli/azure/authenticate-azure-cli-interactively).
+**Note**: This sample uses `DefaultAzureCredential`. `az login` is the easiest local development path, but Visual Studio, VS Code, and managed identity credentials also work when available.
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-5.4-mini"
+$env:FOUNDRY_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project"
+$env:FOUNDRY_MODEL="gpt-5.4-mini"
```
## Run the sample
diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step02.2_MultiturnWithServerConversations/Program.cs b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step02.2_MultiturnWithServerConversations/Program.cs
index 317474aa6e..c41d9713f3 100644
--- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step02.2_MultiturnWithServerConversations/Program.cs
+++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step02.2_MultiturnWithServerConversations/Program.cs
@@ -9,8 +9,8 @@ using Azure.AI.Projects;
using Azure.Identity;
using Microsoft.Agents.AI;
-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-5.4-mini";
+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-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
diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step02.2_MultiturnWithServerConversations/README.md b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step02.2_MultiturnWithServerConversations/README.md
index ee91d935ef..4cc1f635ec 100644
--- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step02.2_MultiturnWithServerConversations/README.md
+++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step02.2_MultiturnWithServerConversations/README.md
@@ -15,15 +15,15 @@ Before you begin, ensure you have the following prerequisites:
- .NET 10 SDK or later
- Microsoft Foundry service endpoint and deployment configured
-- Azure CLI installed and authenticated (for Azure credential authentication)
+- An authenticated Azure identity (for example, sign in with `az login`)
-**Note**: This demo uses Azure CLI credentials for authentication. Make sure you're logged in with `az login` and have access to the Microsoft Foundry resource. For more information, see the [Azure CLI documentation](https://learn.microsoft.com/cli/azure/authenticate-azure-cli-interactively).
+**Note**: This sample uses `DefaultAzureCredential`. `az login` is the easiest local development path, but Visual Studio, VS Code, and managed identity credentials also work when available.
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-5.4-mini"
+$env:FOUNDRY_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project"
+$env:FOUNDRY_MODEL="gpt-5.4-mini"
```
## Run the sample
diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step03_UsingFunctionTools/Program.cs b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step03_UsingFunctionTools/Program.cs
index e1b4548a04..94492f6cbc 100644
--- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step03_UsingFunctionTools/Program.cs
+++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step03_UsingFunctionTools/Program.cs
@@ -15,8 +15,8 @@ static string GetWeather([Description("The location to get the weather for.")] s
// Define the function tool.
AITool tool = AIFunctionFactory.Create(GetWeather);
-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-5.4-mini";
+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-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
diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step03_UsingFunctionTools/README.md b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step03_UsingFunctionTools/README.md
index dfad8d0b5c..4692b4838f 100644
--- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step03_UsingFunctionTools/README.md
+++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step03_UsingFunctionTools/README.md
@@ -16,15 +16,15 @@ Before you begin, ensure you have the following prerequisites:
- .NET 10 SDK or later
- Microsoft Foundry service endpoint and deployment configured
-- Azure CLI installed and authenticated (for Azure credential authentication)
+- An authenticated Azure identity (for example, sign in with `az login`)
-**Note**: This demo uses Azure CLI credentials for authentication. Make sure you're logged in with `az login` and have access to the Microsoft Foundry resource. For more information, see the [Azure CLI documentation](https://learn.microsoft.com/cli/azure/authenticate-azure-cli-interactively).
+**Note**: This sample uses `DefaultAzureCredential`. `az login` is the easiest local development path, but Visual Studio, VS Code, and managed identity credentials also work when available.
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-5.4-mini"
+$env:FOUNDRY_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project"
+$env:FOUNDRY_MODEL="gpt-5.4-mini"
```
## Run the sample
diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step04_UsingFunctionToolsWithApprovals/Program.cs b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step04_UsingFunctionToolsWithApprovals/Program.cs
index 3943f32295..4e470845b8 100644
--- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step04_UsingFunctionToolsWithApprovals/Program.cs
+++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step04_UsingFunctionToolsWithApprovals/Program.cs
@@ -12,8 +12,8 @@ using Microsoft.Extensions.AI;
static string GetWeather([Description("The location to get the weather for.")] string location)
=> $"The weather in {location} is cloudy with a high of 15°C.";
-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-5.4-mini";
+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-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
diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step04_UsingFunctionToolsWithApprovals/README.md b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step04_UsingFunctionToolsWithApprovals/README.md
index a832d308e9..27db13b748 100644
--- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step04_UsingFunctionToolsWithApprovals/README.md
+++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step04_UsingFunctionToolsWithApprovals/README.md
@@ -13,13 +13,13 @@ This sample demonstrates how to use function tools that require human-in-the-loo
- .NET 10 SDK or later
- Microsoft Foundry service endpoint and deployment configured
-- Azure CLI installed and authenticated (`az login`)
+- An authenticated Azure identity (for example, sign in with `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-5.4-mini"
+$env:FOUNDRY_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project"
+$env:FOUNDRY_MODEL="gpt-5.4-mini"
```
## Run the sample
diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step05_StructuredOutput/Program.cs b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step05_StructuredOutput/Program.cs
index 07636f12dd..e887b00a6d 100644
--- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step05_StructuredOutput/Program.cs
+++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step05_StructuredOutput/Program.cs
@@ -12,8 +12,8 @@ using SampleApp;
#pragma warning disable CA5399
-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-5.4-mini";
+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-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
diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step05_StructuredOutput/README.md b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step05_StructuredOutput/README.md
index f2770d6055..48e5697f60 100644
--- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step05_StructuredOutput/README.md
+++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step05_StructuredOutput/README.md
@@ -12,13 +12,13 @@ This sample demonstrates how to configure an agent to produce structured output
- .NET 10 SDK or later
- Microsoft Foundry service endpoint and deployment configured
-- Azure CLI installed and authenticated (`az login`)
+- An authenticated Azure identity (for example, sign in with `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-5.4-mini"
+$env:FOUNDRY_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project"
+$env:FOUNDRY_MODEL="gpt-5.4-mini"
```
## Run the sample
diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step06_PersistedConversations/Program.cs b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step06_PersistedConversations/Program.cs
index 18ce97ef88..d99007c972 100644
--- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step06_PersistedConversations/Program.cs
+++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step06_PersistedConversations/Program.cs
@@ -7,8 +7,8 @@ using Azure.AI.Projects;
using Azure.Identity;
using Microsoft.Agents.AI;
-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-5.4-mini";
+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-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
diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step06_PersistedConversations/README.md b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step06_PersistedConversations/README.md
index 42074f2972..3b572503e8 100644
--- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step06_PersistedConversations/README.md
+++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step06_PersistedConversations/README.md
@@ -13,13 +13,13 @@ This sample demonstrates how to persist and resume agent conversations using ses
- .NET 10 SDK or later
- Microsoft Foundry service endpoint and deployment configured
-- Azure CLI installed and authenticated (`az login`)
+- An authenticated Azure identity (for example, sign in with `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-5.4-mini"
+$env:FOUNDRY_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project"
+$env:FOUNDRY_MODEL="gpt-5.4-mini"
```
## Run the sample
diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step07_Observability/Program.cs b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step07_Observability/Program.cs
index e4b451fea3..c1066fb0a0 100644
--- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step07_Observability/Program.cs
+++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step07_Observability/Program.cs
@@ -10,8 +10,8 @@ using OpenTelemetry;
using OpenTelemetry.Trace;
string? applicationInsightsConnectionString = Environment.GetEnvironmentVariable("APPLICATIONINSIGHTS_CONNECTION_STRING");
-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-5.4-mini";
+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-mini";
// Create TracerProvider with console exporter.
string sourceName = Guid.NewGuid().ToString("N");
diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step07_Observability/README.md b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step07_Observability/README.md
index 70e10d805b..760f2d27dc 100644
--- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step07_Observability/README.md
+++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step07_Observability/README.md
@@ -13,13 +13,13 @@ This sample demonstrates how to add OpenTelemetry observability to an agent usin
- .NET 10 SDK or later
- Microsoft Foundry service endpoint and deployment configured
-- Azure CLI installed and authenticated (`az login`)
+- An authenticated Azure identity (for example, sign in with `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-5.4-mini"
+$env:FOUNDRY_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project"
+$env:FOUNDRY_MODEL="gpt-5.4-mini"
$env:APPLICATIONINSIGHTS_CONNECTION_STRING="..." # Optional
```
diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step08_DependencyInjection/Program.cs b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step08_DependencyInjection/Program.cs
index 019323e56f..7ce6d1eab6 100644
--- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step08_DependencyInjection/Program.cs
+++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step08_DependencyInjection/Program.cs
@@ -9,8 +9,8 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using SampleApp;
-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-5.4-mini";
+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-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
diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step08_DependencyInjection/README.md b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step08_DependencyInjection/README.md
index 52bb3f591e..cb20f43bbf 100644
--- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step08_DependencyInjection/README.md
+++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step08_DependencyInjection/README.md
@@ -13,13 +13,13 @@ This sample demonstrates how to register a `ChatClientAgent` in a dependency inj
- .NET 10 SDK or later
- Microsoft Foundry service endpoint and deployment configured
-- Azure CLI installed and authenticated (`az login`)
+- An authenticated Azure identity (for example, sign in with `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-5.4-mini"
+$env:FOUNDRY_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project"
+$env:FOUNDRY_MODEL="gpt-5.4-mini"
```
## Run the sample
diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step09_UsingMcpClientAsTools/Program.cs b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step09_UsingMcpClientAsTools/Program.cs
index b07917ee01..62682d5cc0 100644
--- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step09_UsingMcpClientAsTools/Program.cs
+++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step09_UsingMcpClientAsTools/Program.cs
@@ -9,8 +9,8 @@ using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using ModelContextProtocol.Client;
-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-5.4-mini";
+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-mini";
// Connect to the Microsoft Learn MCP server via HTTP (Streamable HTTP transport).
Console.WriteLine("Connecting to MCP server at https://learn.microsoft.com/api/mcp ...");
diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step09_UsingMcpClientAsTools/README.md b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step09_UsingMcpClientAsTools/README.md
index ae7fffcb2a..378d72c408 100644
--- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step09_UsingMcpClientAsTools/README.md
+++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step09_UsingMcpClientAsTools/README.md
@@ -12,14 +12,14 @@ This sample shows how to use MCP (Model Context Protocol) client tools with a `C
- .NET 10 SDK or later
- Microsoft Foundry service endpoint and deployment configured
-- Azure CLI installed and authenticated (`az login`)
+- An authenticated Azure identity (for example, sign in with `az login`)
- Node.js installed (for npx/MCP server)
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-5.4-mini"
+$env:FOUNDRY_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project"
+$env:FOUNDRY_MODEL="gpt-5.4-mini"
```
## Run the sample
diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step10_UsingImages/Program.cs b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step10_UsingImages/Program.cs
index 076237c072..8a2126dbdb 100644
--- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step10_UsingImages/Program.cs
+++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step10_UsingImages/Program.cs
@@ -7,8 +7,8 @@ using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
-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-5.4-mini";
+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-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
diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step10_UsingImages/README.md b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step10_UsingImages/README.md
index 12d5fb6284..8bd095e99a 100644
--- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step10_UsingImages/README.md
+++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step10_UsingImages/README.md
@@ -13,13 +13,13 @@ This sample demonstrates how to use image multi-modality with an agent.
- .NET 10 SDK or later
- Microsoft Foundry service endpoint and a vision-capable model deployment (e.g., `gpt-5.4-mini`)
-- Azure CLI installed and authenticated (`az login`)
+- An authenticated Azure identity (for example, sign in with `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-5.4-mini"
+$env:FOUNDRY_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project"
+$env:FOUNDRY_MODEL="gpt-5.4-mini"
```
## Run the sample
diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step11_AsFunctionTool/Program.cs b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step11_AsFunctionTool/Program.cs
index 06d2a4dc18..892c27c427 100644
--- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step11_AsFunctionTool/Program.cs
+++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step11_AsFunctionTool/Program.cs
@@ -12,8 +12,8 @@ using Microsoft.Extensions.AI;
static string GetWeather([Description("The location to get the weather for.")] string location)
=> $"The weather in {location} is cloudy with a high of 15°C.";
-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-5.4-mini";
+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-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
diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step11_AsFunctionTool/README.md b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step11_AsFunctionTool/README.md
index 4fe155d76d..793235b5ac 100644
--- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step11_AsFunctionTool/README.md
+++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step11_AsFunctionTool/README.md
@@ -13,13 +13,13 @@ This sample demonstrates how to use one agent as a function tool for another age
- .NET 10 SDK or later
- Microsoft Foundry service endpoint and deployment configured
-- Azure CLI installed and authenticated (`az login`)
+- An authenticated Azure identity (for example, sign in with `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-5.4-mini"
+$env:FOUNDRY_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project"
+$env:FOUNDRY_MODEL="gpt-5.4-mini"
```
## Run the sample
diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step12_Middleware/Program.cs b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step12_Middleware/Program.cs
index 27240b8372..eaa7cbd07e 100644
--- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step12_Middleware/Program.cs
+++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step12_Middleware/Program.cs
@@ -20,8 +20,8 @@ static string GetWeather([Description("The location to get the weather for.")] s
static string GetDateTime()
=> DateTimeOffset.Now.ToString();
-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-5.4-mini";
+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-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
diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step12_Middleware/README.md b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step12_Middleware/README.md
index 45543329bc..58d59086a7 100644
--- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step12_Middleware/README.md
+++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step12_Middleware/README.md
@@ -14,13 +14,13 @@ This sample demonstrates multiple middleware layers working together: PII filter
- .NET 10 SDK or later
- Microsoft Foundry service endpoint and deployment configured
-- Azure CLI installed and authenticated (`az login`)
+- An authenticated Azure identity (for example, sign in with `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-5.4-mini"
+$env:FOUNDRY_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project"
+$env:FOUNDRY_MODEL="gpt-5.4-mini"
```
## Run the sample
diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step13_Plugins/Program.cs b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step13_Plugins/Program.cs
index 966a7bba12..5d67220fae 100644
--- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step13_Plugins/Program.cs
+++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step13_Plugins/Program.cs
@@ -16,8 +16,8 @@ using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
using SampleApp;
-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-5.4-mini";
+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-mini";
const string AssistantInstructions = "You are a helpful assistant that helps people find information.";
const string AssistantName = "PluginAssistant";
diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step13_Plugins/README.md b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step13_Plugins/README.md
index e10025b7f3..1c8d078204 100644
--- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step13_Plugins/README.md
+++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step13_Plugins/README.md
@@ -13,13 +13,13 @@ This sample shows how to use plugins with a `ChatClientAgent` using the Response
- .NET 10 SDK or later
- Microsoft Foundry service endpoint and deployment configured
-- Azure CLI installed and authenticated (`az login`)
+- An authenticated Azure identity (for example, sign in with `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-5.4-mini"
+$env:FOUNDRY_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project"
+$env:FOUNDRY_MODEL="gpt-5.4-mini"
```
## Run the sample
diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step14_CodeInterpreter/Program.cs b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step14_CodeInterpreter/Program.cs
index c4661ae49e..9dec5a9eca 100644
--- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step14_CodeInterpreter/Program.cs
+++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step14_CodeInterpreter/Program.cs
@@ -12,8 +12,8 @@ using OpenAI.Assistants;
const string AgentInstructions = "You are a personal math tutor. When asked a math question, write and run code using the python tool to answer the question.";
const string AgentName = "CoderAgent-RAPI";
-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-5.4-mini";
+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-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
diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step14_CodeInterpreter/README.md b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step14_CodeInterpreter/README.md
index db63f82e9c..e339acb934 100644
--- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step14_CodeInterpreter/README.md
+++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step14_CodeInterpreter/README.md
@@ -12,13 +12,13 @@ This sample shows how to use the Code Interpreter tool with a `ChatClientAgent`
- .NET 10 SDK or later
- Microsoft Foundry service endpoint and deployment configured
-- Azure CLI installed and authenticated (`az login`)
+- An authenticated Azure identity (for example, sign in with `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-5.4-mini"
+$env:FOUNDRY_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project"
+$env:FOUNDRY_MODEL="gpt-5.4-mini"
```
## Run the sample
diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step15_ComputerUse/Program.cs b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step15_ComputerUse/Program.cs
index 00e4e02843..865dfac806 100644
--- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step15_ComputerUse/Program.cs
+++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step15_ComputerUse/Program.cs
@@ -10,9 +10,12 @@ using Microsoft.Agents.AI.Foundry;
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 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") ?? "computer-use-preview";
+// 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());
using IHostedFileClient fileClient = projectClient.GetProjectOpenAIClient().AsIHostedFileClient();
diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step15_ComputerUse/README.md b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step15_ComputerUse/README.md
index eee05e2a69..85ceb4c415 100644
--- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step15_ComputerUse/README.md
+++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step15_ComputerUse/README.md
@@ -1,4 +1,4 @@
-# Computer Use with the Responses API
+# Computer Use with the Responses API
This sample shows how to use the Computer Use tool with `AIProjectClient.AsAIAgent(...)`.
@@ -39,12 +39,12 @@ The model receives a screenshot as input, analyzes it, and responds with a compu
- .NET 10 SDK or later
- Microsoft Foundry service endpoint and deployment configured
-- Azure CLI installed and authenticated (`az login`)
+- An authenticated Azure identity (for example, sign in with `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:FOUNDRY_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project"
$env:AZURE_AI_COMPUTER_USE_DEPLOYMENT_NAME="computer-use-preview"
```
diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step16_FileSearch/Program.cs b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step16_FileSearch/Program.cs
index 1a2d870342..adb3376993 100644
--- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step16_FileSearch/Program.cs
+++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step16_FileSearch/Program.cs
@@ -9,8 +9,8 @@ using Microsoft.Extensions.AI;
using OpenAI.Assistants;
using OpenAI.Files;
-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-5.4-mini";
+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-mini";
const string AgentInstructions = "You are a helpful assistant that can search through uploaded files to answer questions.";
diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step16_FileSearch/README.md b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step16_FileSearch/README.md
index 45818ca354..6009492113 100644
--- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step16_FileSearch/README.md
+++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step16_FileSearch/README.md
@@ -13,13 +13,13 @@ This sample shows how to use the File Search tool with a `ChatClientAgent` using
- .NET 10 SDK or later
- Microsoft Foundry service endpoint and deployment configured
-- Azure CLI installed and authenticated (`az login`)
+- An authenticated Azure identity (for example, sign in with `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-5.4-mini"
+$env:FOUNDRY_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project"
+$env:FOUNDRY_MODEL="gpt-5.4-mini"
```
## Run the sample
diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step17_OpenAPITools/Program.cs b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step17_OpenAPITools/Program.cs
index d47f4b0078..6c6ba4b111 100644
--- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step17_OpenAPITools/Program.cs
+++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step17_OpenAPITools/Program.cs
@@ -9,8 +9,8 @@ using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Foundry;
using Microsoft.Extensions.AI;
-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-5.4-mini";
+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-mini";
const string AgentInstructions = "You are a helpful assistant that can use the countries API to retrieve information about countries by their currency code.";
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step17_OpenAPITools/README.md b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step17_OpenAPITools/README.md
index 05227fdd98..0bc08c21f2 100644
--- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step17_OpenAPITools/README.md
+++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step17_OpenAPITools/README.md
@@ -13,13 +13,13 @@ This sample shows how to use OpenAPI tools with a `ChatClientAgent` using the Re
- .NET 10 SDK or later
- Microsoft Foundry service endpoint and deployment configured
-- Azure CLI installed and authenticated (`az login`)
+- An authenticated Azure identity (for example, sign in with `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-5.4-mini"
+$env:FOUNDRY_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project"
+$env:FOUNDRY_MODEL="gpt-5.4-mini"
```
## Run the sample
diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step18_BingCustomSearch/Program.cs b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step18_BingCustomSearch/Program.cs
index 11048c8154..e3ad81c95c 100644
--- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step18_BingCustomSearch/Program.cs
+++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step18_BingCustomSearch/Program.cs
@@ -21,8 +21,8 @@ BingCustomSearchToolOptions bingCustomSearchToolParameters = new([
new BingCustomSearchConfiguration(connectionId, instanceName)
]);
-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-5.4-mini";
+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-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
diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step18_BingCustomSearch/README.md b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step18_BingCustomSearch/README.md
index 18bffeacd6..8174ca32f8 100644
--- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step18_BingCustomSearch/README.md
+++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step18_BingCustomSearch/README.md
@@ -12,14 +12,14 @@ This sample shows how to use the Bing Custom Search tool with a `ChatClientAgent
- .NET 10 SDK or later
- Microsoft Foundry service endpoint and deployment configured
-- Azure CLI installed and authenticated (`az login`)
+- An authenticated Azure identity (for example, sign in with `az login`)
- Bing Custom Search resource configured with a connection ID
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-5.4-mini"
+$env:FOUNDRY_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project"
+$env:FOUNDRY_MODEL="gpt-5.4-mini"
$env:AZURE_AI_CUSTOM_SEARCH_CONNECTION_ID="your-connection-id" # The full ARM resource URI, e.g., "/subscriptions/.../connections/your-bing-connection"
$env:AZURE_AI_CUSTOM_SEARCH_INSTANCE_NAME="your-instance-name" # The Bing Custom Search configuration name (from Azure portal)
```
diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step19_SharePoint/Program.cs b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step19_SharePoint/Program.cs
index 186acb9da5..9abb8d5e78 100644
--- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step19_SharePoint/Program.cs
+++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step19_SharePoint/Program.cs
@@ -19,8 +19,8 @@ const string AgentInstructions = """
var sharepointOptions = new SharePointGroundingToolOptions();
sharepointOptions.ProjectConnections.Add(new ToolProjectConnection(sharepointConnectionId));
-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-5.4-mini";
+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-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
diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step19_SharePoint/README.md b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step19_SharePoint/README.md
index 1049eb4ebc..12d58cf6c1 100644
--- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step19_SharePoint/README.md
+++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step19_SharePoint/README.md
@@ -12,14 +12,14 @@ This sample shows how to use the SharePoint Grounding tool with a `ChatClientAge
- .NET 10 SDK or later
- Microsoft Foundry service endpoint and deployment configured
-- Azure CLI installed and authenticated (`az login`)
+- An authenticated Azure identity (for example, sign in with `az login`)
- SharePoint connection configured in your Microsoft Foundry project
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-5.4-mini"
+$env:FOUNDRY_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project"
+$env:FOUNDRY_MODEL="gpt-5.4-mini"
$env:SHAREPOINT_PROJECT_CONNECTION_ID="your-sharepoint-connection-id" # The full ARM resource URI, e.g., "/subscriptions/.../connections/SharepointTestTool"
```
diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step20_MicrosoftFabric/Program.cs b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step20_MicrosoftFabric/Program.cs
index ccc3c0dcf0..1d1e27fa5b 100644
--- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step20_MicrosoftFabric/Program.cs
+++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step20_MicrosoftFabric/Program.cs
@@ -16,8 +16,8 @@ const string AgentInstructions = "You are a helpful assistant with access to Mic
var fabricToolOptions = new FabricDataAgentToolOptions();
fabricToolOptions.ProjectConnections.Add(new ToolProjectConnection(fabricConnectionId));
-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-5.4-mini";
+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-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
diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step20_MicrosoftFabric/README.md b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step20_MicrosoftFabric/README.md
index 03536262d2..0d848ca1e5 100644
--- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step20_MicrosoftFabric/README.md
+++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step20_MicrosoftFabric/README.md
@@ -12,14 +12,14 @@ This sample shows how to use the Microsoft Fabric tool with a `ChatClientAgent`
- .NET 10 SDK or later
- Microsoft Foundry service endpoint and deployment configured
-- Azure CLI installed and authenticated (`az login`)
+- An authenticated Azure identity (for example, sign in with `az login`)
- Microsoft Fabric connection configured in your Microsoft Foundry project
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-5.4-mini"
+$env:FOUNDRY_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project"
+$env:FOUNDRY_MODEL="gpt-5.4-mini"
$env:FABRIC_PROJECT_CONNECTION_ID="your-fabric-connection-id" # The full ARM resource URI, e.g., "/subscriptions/.../connections/FabricTestTool"
```
diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step21_WebSearch/Program.cs b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step21_WebSearch/Program.cs
index da1652536b..509d2dfdea 100644
--- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step21_WebSearch/Program.cs
+++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step21_WebSearch/Program.cs
@@ -11,8 +11,8 @@ using OpenAI.Responses;
const string AgentInstructions = "You are a helpful assistant that can search the web to find current information and answer questions accurately.";
const string AgentName = "WebSearchAgent-RAPI";
-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-5.4-mini";
+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-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
diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step21_WebSearch/README.md b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step21_WebSearch/README.md
index 81d37e6ff5..3b4ea1b429 100644
--- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step21_WebSearch/README.md
+++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step21_WebSearch/README.md
@@ -12,13 +12,13 @@ This sample shows how to use the Web Search tool with a `ChatClientAgent` using
- .NET 10 SDK or later
- Microsoft Foundry service endpoint and deployment configured
-- Azure CLI installed and authenticated (`az login`)
+- An authenticated Azure identity (for example, sign in with `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-5.4-mini"
+$env:FOUNDRY_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project"
+$env:FOUNDRY_MODEL="gpt-5.4-mini"
```
## Run the sample
diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step22_MemorySearch/Program.cs b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step22_MemorySearch/Program.cs
index 5ba9ccedb1..908d03f7bd 100644
--- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step22_MemorySearch/Program.cs
+++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step22_MemorySearch/Program.cs
@@ -14,8 +14,8 @@ using Microsoft.Agents.AI.Foundry;
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-5.4-mini";
+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-mini";
string embeddingModelName = Environment.GetEnvironmentVariable("AZURE_AI_EMBEDDING_DEPLOYMENT_NAME") ?? "text-embedding-ada-002";
string memoryStoreName = Environment.GetEnvironmentVariable("AZURE_AI_MEMORY_STORE_ID") ?? $"foundry-memory-sample-{Guid.NewGuid():N}";
diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step22_MemorySearch/README.md b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step22_MemorySearch/README.md
index 63af9cd9c8..b6293eb674 100644
--- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step22_MemorySearch/README.md
+++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step22_MemorySearch/README.md
@@ -13,14 +13,14 @@ This sample demonstrates how to use the Memory Search tool with a `ChatClientAge
- .NET 10 SDK or later
- Microsoft Foundry service endpoint and deployment configured
-- Azure CLI installed and authenticated (`az login`)
+- An authenticated Azure identity (for example, sign in with `az login`)
- A memory store created beforehand via Azure Portal or Python SDK
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-5.4-mini"
+$env:FOUNDRY_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project"
+$env:FOUNDRY_MODEL="gpt-5.4-mini"
$env:AZURE_AI_MEMORY_STORE_ID="your-memory-store-name"
```
diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step23_LocalMCP/Program.cs b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step23_LocalMCP/Program.cs
index 772d1a17f9..89d5b0029b 100644
--- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step23_LocalMCP/Program.cs
+++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step23_LocalMCP/Program.cs
@@ -30,8 +30,8 @@ Console.WriteLine($"MCP tools available: {string.Join(", ", mcpTools.Select(t =>
// Wrap each MCP tool with a DelegatingAIFunction to log local invocations.
List wrappedTools = mcpTools.Select(tool => (AITool)new LoggingMcpTool(tool)).ToList();
-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-5.4-mini";
+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-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
diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step23_LocalMCP/README.md b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step23_LocalMCP/README.md
index c3464efe5d..3c37a9e50a 100644
--- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step23_LocalMCP/README.md
+++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step23_LocalMCP/README.md
@@ -13,13 +13,13 @@ This sample demonstrates how to use a local MCP (Model Context Protocol) client
- .NET 10 SDK or later
- Microsoft Foundry service endpoint and deployment configured
-- Azure CLI installed and authenticated (`az login`)
+- An authenticated Azure identity (for example, sign in with `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-5.4-mini"
+$env:FOUNDRY_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project"
+$env:FOUNDRY_MODEL="gpt-5.4-mini"
```
## Run the sample
diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step24_CodeInterpreterFileDownload/Program.cs b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step24_CodeInterpreterFileDownload/Program.cs
index 79fac0d5d4..3d33c91c9d 100644
--- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step24_CodeInterpreterFileDownload/Program.cs
+++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step24_CodeInterpreterFileDownload/Program.cs
@@ -12,8 +12,8 @@ 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";
+string endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
+string deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "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
diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step24_CodeInterpreterFileDownload/README.md b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step24_CodeInterpreterFileDownload/README.md
index 4d50b98ca8..a74123d711 100644
--- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step24_CodeInterpreterFileDownload/README.md
+++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step24_CodeInterpreterFileDownload/README.md
@@ -35,13 +35,13 @@ The container ID and file ID are available from the `ContainerFileCitationMessag
- .NET 10 SDK or later
- Microsoft Foundry service endpoint and deployment configured
-- Azure CLI installed and authenticated (`az login`)
+- An authenticated Azure identity (for example, sign in with `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
+$env:FOUNDRY_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project"
+$env:FOUNDRY_MODEL="gpt-4o-mini" # Optional, defaults to gpt-4o-mini
```
## Run the sample
diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step25_FoundryToolboxMcp/Program.cs b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step25_FoundryToolboxMcp/Program.cs
index 01a1f36d1b..e611c34502 100644
--- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step25_FoundryToolboxMcp/Program.cs
+++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step25_FoundryToolboxMcp/Program.cs
@@ -24,9 +24,9 @@ using OpenAI.Responses;
const string ToolboxName = "research_toolbox";
const string Query = "What tools do you have access to?";
-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-5.4-mini";
+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-mini";
TokenCredential credential = new DefaultAzureCredential();
diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step25_FoundryToolboxMcp/README.md b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step25_FoundryToolboxMcp/README.md
index 8a9d22e28a..3120647778 100644
--- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step25_FoundryToolboxMcp/README.md
+++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step25_FoundryToolboxMcp/README.md
@@ -12,18 +12,18 @@ This sample shows how to use a Foundry Toolbox by pointing an `McpClient` at the
## Prerequisites
- A Microsoft Foundry project with a toolbox configured (or let the sample create one for you)
-- Azure CLI installed and authenticated (`az login`)
+- An authenticated Azure identity (for example, sign in with `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-5.4-mini"
+$env:FOUNDRY_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project"
+$env:FOUNDRY_MODEL="gpt-5.4-mini"
```
The sample creates a toolbox named `research_toolbox` in your Foundry project on
startup, then connects to its MCP endpoint at
-`{AZURE_AI_PROJECT_ENDPOINT}/toolboxes/research_toolbox/mcp?api-version=v{version}`.
+`{FOUNDRY_PROJECT_ENDPOINT}/toolboxes/research_toolbox/mcp?api-version=v{version}`.
## Run the sample
diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step26_FoundryToolboxMcpSkills/Program.cs b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step26_FoundryToolboxMcpSkills/Program.cs
index 6efb8ce40e..d8fff025c5 100644
--- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step26_FoundryToolboxMcpSkills/Program.cs
+++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step26_FoundryToolboxMcpSkills/Program.cs
@@ -14,9 +14,9 @@ using Microsoft.Agents.AI;
using ModelContextProtocol.Client;
// --- Configuration ---
-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-5.4-mini";
+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-mini";
string toolboxMcpServerUrl = Environment.GetEnvironmentVariable("FOUNDRY_TOOLBOX_MCP_SERVER_URL")
?? throw new InvalidOperationException("FOUNDRY_TOOLBOX_MCP_SERVER_URL is not set.");
diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step26_FoundryToolboxMcpSkills/README.md b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step26_FoundryToolboxMcpSkills/README.md
index 2e8efef8cc..fc7ff655cc 100644
--- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step26_FoundryToolboxMcpSkills/README.md
+++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step26_FoundryToolboxMcpSkills/README.md
@@ -15,13 +15,13 @@ and inject them as `AIContextProviders` so the agent can discover and use them a
- A Microsoft Foundry project with a toolbox already configured
- The toolbox MCP endpoint must expose `skill://index.json` with `skill-md` entries (SEP-2640). If the resource is absent, the sample runs but the skills provider will be empty.
-- Azure CLI installed and authenticated (`az login`)
+- An authenticated Azure identity (for example, sign in with `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-5.4-mini"
+$env:FOUNDRY_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project"
+$env:FOUNDRY_MODEL="gpt-5.4-mini"
$env:FOUNDRY_TOOLBOX_MCP_SERVER_URL="https://your-foundry-service.services.ai.azure.com/api/projects/your-project/toolboxes/your-toolbox/mcp?api-version=v1"
```
diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/README.md b/dotnet/samples/02-agents/AgentsWithFoundry/README.md
index dda1e476f0..e90f92c2a2 100644
--- a/dotnet/samples/02-agents/AgentsWithFoundry/README.md
+++ b/dotnet/samples/02-agents/AgentsWithFoundry/README.md
@@ -4,12 +4,12 @@ These samples demonstrate how to use Microsoft Foundry with Agent Framework.
## Quick start
-The simplest way to create a Foundry agent is using the `FoundryAgent` type directly:
+You can create a Foundry agent directly with the `FoundryAgent` type:
```csharp
FoundryAgent agent = new(
new Uri(endpoint),
- new AzureCliCredential(),
+ new DefaultAzureCredential(),
model: "gpt-5.4-mini",
instructions: "You are good at telling jokes.",
name: "JokerAgent");
@@ -32,13 +32,13 @@ FoundryAgent agent = aiProjectClient.AsAIAgent(
- .NET 10 SDK or later
- Foundry project endpoint
-- Azure CLI installed and authenticated
+- An authenticated Azure identity (for example, sign in with `az login`)
Set:
```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-5.4-mini"
+$env:FOUNDRY_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project"
+$env:FOUNDRY_MODEL="gpt-5.4-mini"
```
Some samples require extra tool-specific environment variables. See each sample for details.
@@ -78,7 +78,11 @@ Some samples require extra tool-specific environment variables. See each sample
## Running the samples
+Use the basics sample for a quick smoke test:
+
```powershell
cd dotnet/samples/02-agents/AgentsWithFoundry
-dotnet run --project .\FoundryAgent_Step01
-```
\ No newline at end of file
+dotnet run --project .\Agent_Step01_Basics
+```
+
+If you want to exercise the full create-run-delete lifecycle, run `Agent_Step00_FoundryAgentLifecycle`.
diff --git a/dotnet/samples/02-agents/Evaluation/Evaluation_CustomEvals/Program.cs b/dotnet/samples/02-agents/Evaluation/Evaluation_CustomEvals/Program.cs
index a5fa9cc945..0c263d1620 100644
--- a/dotnet/samples/02-agents/Evaluation/Evaluation_CustomEvals/Program.cs
+++ b/dotnet/samples/02-agents/Evaluation/Evaluation_CustomEvals/Program.cs
@@ -8,8 +8,8 @@ using Azure.AI.Projects;
using Azure.Identity;
using Microsoft.Agents.AI;
-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";
+string endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
+string deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "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
diff --git a/dotnet/samples/02-agents/Evaluation/Evaluation_CustomEvals/README.md b/dotnet/samples/02-agents/Evaluation/Evaluation_CustomEvals/README.md
index da4c9c652f..691537efeb 100644
--- a/dotnet/samples/02-agents/Evaluation/Evaluation_CustomEvals/README.md
+++ b/dotnet/samples/02-agents/Evaluation/Evaluation_CustomEvals/README.md
@@ -1,4 +1,4 @@
-# Evaluation - Custom Evals
+# Evaluation - Custom Evals
This sample demonstrates writing custom domain-specific evaluation functions using `FunctionEvaluator.Create`. Custom evaluators run locally with no cloud evaluator service needed — useful for enforcing business rules, format requirements, or safety guardrails.
@@ -13,13 +13,13 @@ This sample demonstrates writing custom domain-specific evaluation functions usi
## Prerequisites
- .NET 10 SDK or later
-- Azure CLI installed and authenticated (`az login`)
+- Azure authentication available to `DefaultAzureCredential` (for local development, run `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"
+$env:FOUNDRY_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project"
+$env:FOUNDRY_MODEL="gpt-4o-mini"
```
## Run the sample
diff --git a/dotnet/samples/02-agents/Evaluation/Evaluation_ExpectedOutputs/Program.cs b/dotnet/samples/02-agents/Evaluation/Evaluation_ExpectedOutputs/Program.cs
index 96f41bd835..8a9de240be 100644
--- a/dotnet/samples/02-agents/Evaluation/Evaluation_ExpectedOutputs/Program.cs
+++ b/dotnet/samples/02-agents/Evaluation/Evaluation_ExpectedOutputs/Program.cs
@@ -6,10 +6,13 @@ using Azure.AI.Projects;
using Azure.Identity;
using Microsoft.Agents.AI;
-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";
+string endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
+string deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-4o-mini";
// Create a math tutor agent.
+// 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 AIProjectClient(new Uri(endpoint), new DefaultAzureCredential())
.AsAIAgent(
model: deploymentName,
diff --git a/dotnet/samples/02-agents/Evaluation/Evaluation_ExpectedOutputs/README.md b/dotnet/samples/02-agents/Evaluation/Evaluation_ExpectedOutputs/README.md
index 34f16865d2..1e82cda2e5 100644
--- a/dotnet/samples/02-agents/Evaluation/Evaluation_ExpectedOutputs/README.md
+++ b/dotnet/samples/02-agents/Evaluation/Evaluation_ExpectedOutputs/README.md
@@ -1,4 +1,4 @@
-# Evaluation - Expected Outputs
+# Evaluation - Expected Outputs
This sample demonstrates evaluating agent responses against expected outputs using built-in checks.
@@ -16,8 +16,8 @@ This sample demonstrates evaluating agent responses against expected outputs usi
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"
+$env:FOUNDRY_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project"
+$env:FOUNDRY_MODEL="gpt-4o-mini"
```
## Run the sample
diff --git a/dotnet/samples/02-agents/Evaluation/Evaluation_SimpleEval/Program.cs b/dotnet/samples/02-agents/Evaluation/Evaluation_SimpleEval/Program.cs
index f43a1253e7..8a11f5c016 100644
--- a/dotnet/samples/02-agents/Evaluation/Evaluation_SimpleEval/Program.cs
+++ b/dotnet/samples/02-agents/Evaluation/Evaluation_SimpleEval/Program.cs
@@ -10,8 +10,8 @@ using Microsoft.Agents.AI;
using Microsoft.Extensions.AI.Evaluation;
using FoundryEvals = Microsoft.Agents.AI.Foundry.FoundryEvals;
-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";
+string endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
+string deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "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
diff --git a/dotnet/samples/02-agents/Evaluation/Evaluation_SimpleEval/README.md b/dotnet/samples/02-agents/Evaluation/Evaluation_SimpleEval/README.md
index 35bb11c3bd..d130661fae 100644
--- a/dotnet/samples/02-agents/Evaluation/Evaluation_SimpleEval/README.md
+++ b/dotnet/samples/02-agents/Evaluation/Evaluation_SimpleEval/README.md
@@ -1,4 +1,4 @@
-# Evaluation - Simple Eval
+# Evaluation - Simple Eval
The simplest agent evaluation: create a Foundry agent, run it against test questions, and use Foundry quality evaluators (Relevance, Coherence) to score the responses.
@@ -11,14 +11,14 @@ The simplest agent evaluation: create a Foundry agent, run it against test quest
## Prerequisites
- .NET 10 SDK or later
-- Azure CLI installed and authenticated (`az login`)
+- Azure authentication available to `DefaultAzureCredential` (for local development, run `az login`)
- A deployed model in your Azure AI Foundry project
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"
+$env:FOUNDRY_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project"
+$env:FOUNDRY_MODEL="gpt-4o-mini"
```
## Run the sample
diff --git a/dotnet/samples/02-agents/Harness/Harness_Step01_Research/Program.cs b/dotnet/samples/02-agents/Harness/Harness_Step01_Research/Program.cs
index a1d8aca1b8..904a4ab7e4 100644
--- a/dotnet/samples/02-agents/Harness/Harness_Step01_Research/Program.cs
+++ b/dotnet/samples/02-agents/Harness/Harness_Step01_Research/Program.cs
@@ -25,8 +25,8 @@ using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using SampleApp;
-var endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.");
-var deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-5.4";
+var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
+var deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4";
const int MaxContextWindowTokens = 1_050_000;
const int MaxOutputTokens = 128_000;
diff --git a/dotnet/samples/02-agents/Harness/Harness_Step01_Research/README.md b/dotnet/samples/02-agents/Harness/Harness_Step01_Research/README.md
index 7adb0a311f..9511178cec 100644
--- a/dotnet/samples/02-agents/Harness/Harness_Step01_Research/README.md
+++ b/dotnet/samples/02-agents/Harness/Harness_Step01_Research/README.md
@@ -1,4 +1,4 @@
-# What this sample demonstrates
+# What this sample demonstrates
This sample demonstrates how to use a `HarnessAgent` with the Harness `AIContextProviders` (`TodoProvider` and `AgentModeProvider`) for interactive research tasks with web search capabilities powered by Azure AI Foundry. The `HarnessAgent` pre-configures function invocation, per-service-call chat history persistence, and context-window compaction.
@@ -30,7 +30,7 @@ Set the following environment variables:
export AZURE_FOUNDRY_OPENAI_ENDPOINT="https://your-project.services.ai.azure.com/openai/v1/"
# Optional: Model deployment name (defaults to gpt-5.4)
-export AZURE_AI_MODEL_DEPLOYMENT_NAME="gpt-5.4"
+export FOUNDRY_MODEL="gpt-5.4"
```
## Running the Sample
diff --git a/dotnet/samples/02-agents/Harness/Harness_Step02_Research_WithBackgroundAgents/Program.cs b/dotnet/samples/02-agents/Harness/Harness_Step02_Research_WithBackgroundAgents/Program.cs
index 654c169850..6c372f8ed1 100644
--- a/dotnet/samples/02-agents/Harness/Harness_Step02_Research_WithBackgroundAgents/Program.cs
+++ b/dotnet/samples/02-agents/Harness/Harness_Step02_Research_WithBackgroundAgents/Program.cs
@@ -20,8 +20,8 @@ using Harness.Shared.Console.OpenAI;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
-var endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.");
-var deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-5.4";
+var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
+var deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4";
const int MaxContextWindowTokens = 1_050_000;
const int MaxOutputTokens = 128_000;
@@ -30,6 +30,9 @@ const string TracingSourceName = "Harness.SubAgents";
// Set up OpenTelemetry tracing that writes spans to a text file.
using var tracerProvider = HarnessTracing.CreateFileTracerProvider(TracingSourceName);
+// 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.
// Create the AIProjectClient for communicating with the Foundry responses service.
var projectClient = new AIProjectClient(
new Uri(endpoint),
diff --git a/dotnet/samples/02-agents/Harness/Harness_Step02_Research_WithBackgroundAgents/README.md b/dotnet/samples/02-agents/Harness/Harness_Step02_Research_WithBackgroundAgents/README.md
index c04f68d13c..4b61a77b76 100644
--- a/dotnet/samples/02-agents/Harness/Harness_Step02_Research_WithBackgroundAgents/README.md
+++ b/dotnet/samples/02-agents/Harness/Harness_Step02_Research_WithBackgroundAgents/README.md
@@ -1,4 +1,4 @@
-# Harness Step 02 — BackgroundAgents (Stock Price Research)
+# Harness Step 02 — BackgroundAgents (Stock Price Research)
This sample demonstrates how to use the **BackgroundAgentsProvider** to delegate work from a parent agent to background agents. Both agents use `HarnessAgent` for pre-configured function invocation, per-service-call persistence, and context-window compaction.
@@ -35,7 +35,7 @@ A parent agent receives a list of stock tickers and uses a web-search background
- An Azure AI Foundry endpoint with an OpenAI model deployment
- Set the following environment variables:
- `AZURE_FOUNDRY_OPENAI_ENDPOINT` — Your Foundry OpenAI endpoint URL
- - `AZURE_AI_MODEL_DEPLOYMENT_NAME` — Model deployment name (defaults to `gpt-5.4`)
+ - `FOUNDRY_MODEL` — Model deployment name (defaults to `gpt-5.4`)
## Running the Sample
diff --git a/dotnet/samples/02-agents/Harness/Harness_Step03_DataProcessing/Program.cs b/dotnet/samples/02-agents/Harness/Harness_Step03_DataProcessing/Program.cs
index 6b88d708f2..e2f9b46307 100644
--- a/dotnet/samples/02-agents/Harness/Harness_Step03_DataProcessing/Program.cs
+++ b/dotnet/samples/02-agents/Harness/Harness_Step03_DataProcessing/Program.cs
@@ -22,8 +22,8 @@ using Harness.Shared.Console;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
-var endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.");
-var deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-5.4";
+var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
+var deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4";
const int MaxContextWindowTokens = 1_050_000;
const int MaxOutputTokens = 128_000;
@@ -57,6 +57,9 @@ var instructions =
- Always explain what you learned and what you are going to do next between tool calls, so the user can follow along with your thought process.
""";
+// 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.
// Create the agent using AsHarnessAgent. The FileAccessStore is explicitly set to the
// sample's working/ folder (copied to the output directory) so it works regardless of cwd.
// Unused features are disabled.
diff --git a/dotnet/samples/02-agents/Harness/Harness_Step03_DataProcessing/README.md b/dotnet/samples/02-agents/Harness/Harness_Step03_DataProcessing/README.md
index a9d6cba384..d06348fa72 100644
--- a/dotnet/samples/02-agents/Harness/Harness_Step03_DataProcessing/README.md
+++ b/dotnet/samples/02-agents/Harness/Harness_Step03_DataProcessing/README.md
@@ -1,4 +1,4 @@
-# What this sample demonstrates
+# What this sample demonstrates
This sample demonstrates how to use a `HarnessAgent` with the default `FileAccessProvider` to give an agent access to a folder of data files for reading, analyzing, and writing results. The `HarnessAgent` pre-configures function invocation, per-service-call chat history persistence, in-loop compaction, tool approval, and OpenTelemetry — so the sample only needs to supply the chat client, token limits, custom instructions, and opt out of unused features.
@@ -27,7 +27,7 @@ Set the following environment variables:
export AZURE_FOUNDRY_OPENAI_ENDPOINT="https://your-project.services.ai.azure.com/openai/v1/"
# Optional: Model deployment name (defaults to gpt-5.4)
-export AZURE_AI_MODEL_DEPLOYMENT_NAME="gpt-5.4"
+export FOUNDRY_MODEL="gpt-5.4"
```
## Running the Sample
diff --git a/dotnet/samples/02-agents/Harness/Harness_Step04_CodeExecution/Program.cs b/dotnet/samples/02-agents/Harness/Harness_Step04_CodeExecution/Program.cs
index 908e43abd7..46f4c32c81 100644
--- a/dotnet/samples/02-agents/Harness/Harness_Step04_CodeExecution/Program.cs
+++ b/dotnet/samples/02-agents/Harness/Harness_Step04_CodeExecution/Program.cs
@@ -27,8 +27,8 @@ using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Hyperlight;
using Microsoft.Extensions.AI;
-var endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.");
-var deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-5.4";
+var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
+var deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4";
const int MaxContextWindowTokens = 1_050_000;
const int MaxOutputTokens = 128_000;
@@ -78,6 +78,9 @@ var instructions =
- If applicable, save final results to file memory.
""";
+// 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.
// Create the agent with ALL HarnessAgent features enabled plus Hyperlight CodeAct.
// No Disable* flags are set — TodoProvider, AgentModeProvider, FileMemory, FileAccess,
// ToolApproval, WebSearch, and AgentSkillsProvider are all active.
diff --git a/dotnet/samples/02-agents/Harness/Harness_Step04_CodeExecution/README.md b/dotnet/samples/02-agents/Harness/Harness_Step04_CodeExecution/README.md
index 0d1b109bee..daebc0a19b 100644
--- a/dotnet/samples/02-agents/Harness/Harness_Step04_CodeExecution/README.md
+++ b/dotnet/samples/02-agents/Harness/Harness_Step04_CodeExecution/README.md
@@ -1,4 +1,4 @@
-# Harness Step 04 — Code Execution (Hyperlight + Skills)
+# Harness Step 04 — Code Execution (Hyperlight + Skills)
This sample demonstrates a HarnessAgent with **all features enabled**, plus:
@@ -17,8 +17,8 @@ The agent can plan tasks, manage modes, store memories, read/write files, search
| Variable | Description |
|----------|-------------|
-| `AZURE_AI_PROJECT_ENDPOINT` | Your Azure AI Foundry project endpoint |
-| `AZURE_AI_MODEL_DEPLOYMENT_NAME` | Model deployment name (default: `gpt-5.4`) |
+| `FOUNDRY_PROJECT_ENDPOINT` | Your Azure AI Foundry project endpoint |
+| `FOUNDRY_MODEL` | Model deployment name (default: `gpt-5.4`) |
## Running
diff --git a/dotnet/samples/02-agents/AgentProviders/Agent_With_AzureAIAgentsPersistent/Agent_With_AzureAIAgentsPersistent.csproj b/dotnet/samples/02-agents/Harness/Harness_Step05_Loop/Harness_Step05_Loop.csproj
similarity index 65%
rename from dotnet/samples/02-agents/AgentProviders/Agent_With_AzureAIAgentsPersistent/Agent_With_AzureAIAgentsPersistent.csproj
rename to dotnet/samples/02-agents/Harness/Harness_Step05_Loop/Harness_Step05_Loop.csproj
index d40e93232b..f5d6f368b6 100644
--- a/dotnet/samples/02-agents/AgentProviders/Agent_With_AzureAIAgentsPersistent/Agent_With_AzureAIAgentsPersistent.csproj
+++ b/dotnet/samples/02-agents/Harness/Harness_Step05_Loop/Harness_Step05_Loop.csproj
@@ -9,12 +9,13 @@
-
+
-
+
+
diff --git a/dotnet/samples/02-agents/Harness/Harness_Step05_Loop/Program.cs b/dotnet/samples/02-agents/Harness/Harness_Step05_Loop/Program.cs
new file mode 100644
index 0000000000..1e19dabc22
--- /dev/null
+++ b/dotnet/samples/02-agents/Harness/Harness_Step05_Loop/Program.cs
@@ -0,0 +1,272 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+// This sample demonstrates how to wrap a HarnessAgent with the LoopAgent decorator to re-invoke
+// the agent until a configured LoopEvaluator decides to stop. It covers the common looping patterns
+// through one decorator, each driven by a different evaluator:
+//
+// 1. Completion-marker (Ralph-style) loop — keep refining until the agent emits a completion
+// marker, restarting each pass from a fresh context (CompletionMarkerLoopEvaluator +
+// FreshContextPerIteration).
+// 2. Delegate predicate (todos remaining) — loop while the built-in TodoProvider still has open
+// items (DelegateLoopEvaluator).
+// 3. AI judge — a second chat client decides whether the original request was answered, and the
+// loop continues while the answer is "no" (AIJudgeLoopEvaluator).
+// 4. Approval heuristics + loop — combine the LoopAgent with the ToolApprovalAgent auto-approval
+// heuristics so a looped agent auto-approves tool calls instead of stalling on approval.
+//
+// The demos run sequentially and print each loop's final response.
+
+#pragma warning disable OPENAI001 // Suppress experimental API warnings for Responses API usage.
+#pragma warning disable MAAI001 // Suppress experimental API warnings for Agents AI experiments.
+
+using System.ClientModel.Primitives;
+using System.ComponentModel;
+using Azure.AI.Projects;
+using Azure.Identity;
+using Microsoft.Agents.AI;
+using Microsoft.Extensions.AI;
+
+var endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.");
+var deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-5.4";
+
+// The HarnessAgent pre-configures function invocation, per-service-call chat history persistence, and
+// context-window compaction. These bounds size the in-loop compaction window.
+const int MaxContextWindowTokens = 1_050_000;
+const int MaxOutputTokens = 32_000;
+
+// Build a single Foundry-backed IChatClient factory shared by every demo. Each call returns a fresh
+// IChatClient over the same Responses endpoint.
+var projectClient = new AIProjectClient(
+ new Uri(endpoint),
+ // 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.
+ new DefaultAzureCredential(),
+ new AIProjectClientOptions { RetryPolicy = new ClientRetryPolicy(3) });
+
+IChatClient CreateChatClient() =>
+ projectClient.GetProjectOpenAIClient().GetResponsesClient().AsIChatClient(deploymentName);
+
+await RalphLoopAsync();
+await TodoLoopAsync();
+await JudgeLoopAsync();
+await ApprovalLoopAsync();
+
+// Pattern 1: a "Ralph"-style loop that refines until the agent signals completion.
+async Task RalphLoopAsync()
+{
+ Console.WriteLine("\n=== 1. Completion-marker (Ralph-style) loop — refine until COMPLETE (max 5) ===");
+
+ // Build a lean HarnessAgent: no todo or mode providers for this iterative-refinement task.
+ AIAgent harnessAgent = CreateLeanHarnessAgent(
+ name: "ralph",
+ instructions:
+ """
+ You are iteratively refining a product name for a note-taking app. Each turn, build on the
+ feedback so far: propose an improved candidate with a short reason. When you are confident the
+ name is final, end your message with the exact marker COMPLETE.
+ """);
+
+ // CompletionMarkerLoopEvaluator stops once the marker appears in the response; until then it
+ // re-invokes the agent. FreshContextPerIteration restarts each pass from the original task plus the
+ // aggregated feedback log on a brand-new session. Because each pass starts fresh, the agent has no
+ // memory of its prior suggestion — so the feedback template includes the {last_response} placeholder
+ // to echo the previous candidate back to it.
+ AIAgent loopAgent = new LoopAgent(
+ harnessAgent,
+ new CompletionMarkerLoopEvaluator("COMPLETE", options: new()
+ {
+ FeedbackMessageTemplate =
+ "Your previous suggestion was:\n" + CompletionMarkerLoopEvaluator.LastResponsePlaceholder +
+ "\n\nContinue to refine the name and remember to reply with " +
+ CompletionMarkerLoopEvaluator.CompletionMarkerPlaceholder + " when happy.",
+ }),
+ new LoopAgentOptions { MaxIterations = 5, FreshContextPerIteration = true });
+
+ AgentResponse response = await StreamLoopAsync(loopAgent, "Suggest a name for a note-taking app.");
+ Console.WriteLine($"\nFinal response:\n{response.Text}");
+}
+
+// Pattern 2: loop while the built-in TodoProvider still has open items.
+async Task TodoLoopAsync()
+{
+ Console.WriteLine("\n=== 2. Delegate predicate — loop while todos remain (max 6) ===");
+
+ // Keep the built-in TodoProvider enabled (only the mode provider is disabled) so the agent has
+ // todo tools to plan and track work.
+ AIAgent harnessAgent = CreateLeanHarnessAgent(
+ name: "planner",
+ instructions:
+ """
+ You are a planning assistant. First break the task into todo items using your todo tools.
+ Then, on each turn, make progress and mark completed items as done. When all items are
+ complete, summarize the result.
+ """,
+ disableTodoProvider: false);
+
+ // The predicate re-invokes the agent while any todo item is still open. The evaluator fetches the
+ // built-in TodoProvider from context.Agent (via GetService, which forwards through the harness
+ // decorators to the underlying ChatClientAgent's context providers), keeping the delegate
+ // self-contained, then queries it against the loop's current session. When items remain, it returns
+ // feedback telling the agent to finish them. MaxIterations guarantees the loop stops even if the
+ // agent stalls.
+ AIAgent loopAgent = new LoopAgent(
+ harnessAgent,
+ new DelegateLoopEvaluator(async (context, cancellationToken) =>
+ {
+ var todoProvider = context.Agent.GetService()
+ ?? throw new InvalidOperationException("The agent did not expose a TodoProvider.");
+ var remaining = await todoProvider.GetRemainingTodosAsync(context.Session).ConfigureAwait(false);
+ return remaining.Count > 0
+ ? LoopEvaluation.Continue($"Not all todos are complete yet ({remaining.Count} remaining). Please complete the remaining todo items.")
+ : LoopEvaluation.Stop();
+ }),
+ new LoopAgentOptions { MaxIterations = 6 });
+
+ // The LoopAgent creates a single session up front and reuses it across iterations (non-fresh
+ // mode), so the todo state persists; the predicate reads it via context.Session.
+ AgentResponse response = await StreamLoopAsync(
+ loopAgent,
+ "Plan and outline a 3-section blog post about Rayleigh scattering.");
+ Console.WriteLine($"\nFinal response:\n{response.Text}");
+}
+
+// Pattern 3: a second chat client judges whether the original request was answered.
+async Task JudgeLoopAsync()
+{
+ Console.WriteLine("\n=== 3. AI judge — loop until the request is answered (max 4) ===");
+
+ AIAgent harnessAgent = CreateLeanHarnessAgent(
+ name: "answerer",
+ instructions: "You are a helpful assistant. Answer the user's question thoroughly.");
+
+ // The judge uses its own IChatClient. AIJudgeLoopEvaluator asks it (via a JudgeVerdict structured
+ // output) whether the original request has been fully addressed and continues while the answer is
+ // "no", injecting the judge's gap analysis as the next iteration's input. Judge loops use a small
+ // MaxIterations cap because each pass costs an extra model call.
+ AIAgent loopAgent = new LoopAgent(
+ harnessAgent,
+ new AIJudgeLoopEvaluator(CreateChatClient()),
+ new LoopAgentOptions { MaxIterations = 4 });
+
+ AgentResponse response = await StreamLoopAsync(
+ loopAgent,
+ "Explain why the sky is blue, then also explain why sunsets are red.");
+ Console.WriteLine($"\nFinal response:\n{response.Text}");
+}
+
+// Pattern 4: combine the loop with the ToolApprovalAgent auto-approval heuristics.
+async Task ApprovalLoopAsync()
+{
+ Console.WriteLine("\n=== 4. Approval heuristics + loop — auto-approve tool calls in the loop (max 2) ===");
+
+ var deployTool = new ApprovalRequiredAIFunction(
+ AIFunctionFactory.Create(DeploymentTools.DeployService));
+
+ // Configure the HarnessAgent's built-in ToolApprovalAgent with an auto-approval rule. The rule
+ // approves the deploy_service call without prompting, so the inner agent resolves the approval
+ // internally and never surfaces a pending approval to the LoopAgent — letting the loop proceed.
+ AIAgent harnessAgent = CreateLeanHarnessAgent(
+ name: "operator",
+ instructions: "You are a deployment operator. Use the DeployService tool to fulfil requests.",
+ tools: [deployTool],
+ toolApprovalAgentOptions: new ToolApprovalAgentOptions
+ {
+ AutoApprovalRules =
+ [
+ functionCall =>
+ {
+ Console.WriteLine($" Auto-approving: {functionCall.Name}");
+ return ValueTask.FromResult(true);
+ },
+ ],
+ });
+
+ // Drive a short loop that continues until the response confirms the deployment.
+ AIAgent loopAgent = new LoopAgent(
+ harnessAgent,
+ new DelegateLoopEvaluator((context, _) =>
+ new ValueTask(
+ context.LastResponse.Text.Contains("deployed", StringComparison.OrdinalIgnoreCase)
+ ? LoopEvaluation.Stop()
+ : LoopEvaluation.Continue())),
+ new LoopAgentOptions { MaxIterations = 2 });
+
+ // The LoopAgent reuses a single session across iterations, so the approval response flows back in.
+ AgentResponse response = await StreamLoopAsync(loopAgent, "Deploy the billing service.");
+ Console.WriteLine($"\nFinal response:\n{response.Text}");
+}
+
+// Streams a loop run to the console, printing updates live and marking each new inner run (detected
+// via a change in ResponseId) with an "--- run N ---" header so you can see when the LoopAgent
+// re-invokes the inner agent. Each message is prefixed with "User:" or "Agent:" based on its role, so
+// the loop's on-behalf-of feedback (User) is visually distinct from the agent's responses (Agent).
+// Returns the aggregated final response.
+static async Task StreamLoopAsync(AIAgent loopAgent, string input, AgentSession? session = null)
+{
+ string? currentResponseId = null;
+ ChatRole? currentRole = null;
+ var runCount = 0;
+ var updates = new List();
+
+ await foreach (var update in loopAgent.RunStreamingAsync(input, session))
+ {
+ // A new ResponseId signals the start of another inner run (loop iteration).
+ if (update.ResponseId is { } responseId && responseId != currentResponseId)
+ {
+ currentResponseId = responseId;
+ currentRole = null;
+ Console.WriteLine($"\n--- run {++runCount} ---");
+ }
+
+ // Print a role-based prefix whenever the speaker changes — for example the loop's on-behalf-of
+ // user feedback versus the agent's response.
+ if (update.Role is { } role && role != currentRole)
+ {
+ currentRole = role;
+ var prefix = role == ChatRole.User ? "User" : role == ChatRole.Assistant ? "Agent" : role.Value;
+ Console.Write($"\n{prefix}: ");
+ }
+
+ Console.Write(update.Text);
+ updates.Add(update);
+ }
+
+ Console.WriteLine();
+ return updates.ToAgentResponse();
+}
+
+// Creates a HarnessAgent with the agent-mode provider always disabled (and the todo provider disabled
+// by default), plus all other heavyweight providers turned off so each loop demo stays focused.
+AIAgent CreateLeanHarnessAgent(
+ string name,
+ string instructions,
+ bool disableTodoProvider = true,
+ IList? tools = null,
+ ToolApprovalAgentOptions? toolApprovalAgentOptions = null) =>
+ CreateChatClient().AsHarnessAgent(new HarnessAgentOptions
+ {
+ Name = name,
+ MaxContextWindowTokens = MaxContextWindowTokens,
+ MaxOutputTokens = MaxOutputTokens,
+ DisableAgentModeProvider = true,
+ DisableTodoProvider = disableTodoProvider,
+ DisableFileMemory = true,
+ DisableFileAccess = true,
+ DisableWebSearch = true,
+ ToolApprovalAgentOptions = toolApprovalAgentOptions,
+ ChatOptions = new ChatOptions
+ {
+ Instructions = instructions,
+ Tools = tools,
+ MaxOutputTokens = MaxOutputTokens,
+ },
+ });
+
+/// Tool used by the approval-handling demo.
+internal static class DeploymentTools
+{
+ [Description("Deploy a service to production (requires approval).")]
+ public static string DeployService([Description("The name of the service to deploy.")] string service) =>
+ $"Deployed {service} to production.";
+}
diff --git a/dotnet/samples/02-agents/Harness/Harness_Step05_Loop/README.md b/dotnet/samples/02-agents/Harness/Harness_Step05_Loop/README.md
new file mode 100644
index 0000000000..4a19f66e57
--- /dev/null
+++ b/dotnet/samples/02-agents/Harness/Harness_Step05_Loop/README.md
@@ -0,0 +1,59 @@
+# What this sample demonstrates
+
+This sample demonstrates how to wrap a `HarnessAgent` with the **`LoopAgent`** decorator to re-invoke the agent until a configured **`LoopEvaluator`** decides to stop. A single decorator covers the common looping patterns — you just plug in a different evaluator (and optionally switch on fresh-context mode).
+
+The `HarnessAgent` pre-configures function invocation, per-service-call chat history persistence, and in-loop compaction, so each demo only supplies the chat client, token limits, and instructions, then wraps the result with a `LoopAgent`.
+
+## Looping patterns showcased
+
+The program runs four demos sequentially, each driven by a different evaluator:
+
+| # | Pattern | Evaluator | Notes |
+| --- | --- | --- | --- |
+| 1 | Completion-marker ("Ralph"-style) loop | `CompletionMarkerLoopEvaluator` | Re-invokes until the agent emits `COMPLETE`. Uses `FreshContextPerIteration = true` to restart each pass from the original task plus the aggregated feedback log on a new session, and includes the `{last_response}` placeholder in the feedback template so the agent sees its previous suggestion even though each pass starts fresh. |
+| 2 | Delegate predicate (todos remaining) | `DelegateLoopEvaluator` | Loops while the built-in `TodoProvider` still has open items. The provider is fetched from the agent via `GetService()` and queried against the loop's current session. |
+| 3 | AI judge | `AIJudgeLoopEvaluator` | A second `IChatClient` judges whether the original request was fully answered and continues while the answer is "no", injecting its gap analysis as the next input. |
+| 4 | Approval heuristics + loop | `DelegateLoopEvaluator` + `ToolApprovalAgent` | Combines the `ToolApprovalAgent` auto-approval heuristics (`AutoApprovalRules`) with the loop, so a looped agent auto-approves tool calls instead of stalling on a pending approval. |
+
+`MaxIterations` caps every loop so it always terminates even if the evaluator never stops.
+
+### Evaluator mapping (Python → .NET)
+
+The Python sample in [microsoft/agent-framework#6174](https://github.com/microsoft/agent-framework/pull/6174) exposes several distinct loop classes. In .NET these collapse into one `LoopAgent` that consumes evaluators:
+
+| Python | .NET |
+| --- | --- |
+| Ralph loop (completion marker) | `LoopAgent` + `CompletionMarkerLoopEvaluator` |
+| Ralph loop (fresh context each pass) | `LoopAgent` + `CompletionMarkerLoopEvaluator` + `FreshContextPerIteration = true` |
+| Callable / predicate loop | `LoopAgent` + `DelegateLoopEvaluator` |
+| AI judge loop | `LoopAgent` + `AIJudgeLoopEvaluator` |
+
+## Prerequisites
+
+Before running this sample, ensure you have:
+
+1. An Azure AI Foundry project with a deployed model (e.g., `gpt-5.4`)
+2. Azure CLI installed and authenticated (`az login`)
+
+## Environment Variables
+
+Set the following environment variables:
+
+```bash
+# Required: Your Azure AI Foundry project endpoint
+export AZURE_AI_PROJECT_ENDPOINT="https://your-project.services.ai.azure.com/api/projects/your-project"
+
+# Optional: Model deployment name (defaults to gpt-5.4)
+export AZURE_AI_MODEL_DEPLOYMENT_NAME="gpt-5.4"
+```
+
+## Running the Sample
+
+```bash
+cd dotnet
+dotnet run --project samples/02-agents/Harness/Harness_Step05_Loop
+```
+
+## What to Expect
+
+The program runs the four demos in order. Each loop is executed with `RunStreamingAsync`, so output is printed live and every re-invocation of the inner agent is marked with a `--- run N ---` header (detected via a change in the streamed `ResponseId`) — this lets you see exactly when the `LoopAgent` loops. Each streamed message is prefixed with `User:` or `Agent:` based on its role, so the loop's on-behalf-of feedback messages (surfaced as `User` turns) are visually distinct from the agent's responses (`Agent`). Each demo finishes by printing its aggregated final response. Demo 4 also prints an `Auto-approving: ...` line each time the `ToolApprovalAgent` heuristic approves the `DeployService` tool call, showing how approval-aware agents integrate with the loop.
diff --git a/dotnet/samples/02-agents/Harness/README.md b/dotnet/samples/02-agents/Harness/README.md
index 16fad9ac62..61981827c4 100644
--- a/dotnet/samples/02-agents/Harness/README.md
+++ b/dotnet/samples/02-agents/Harness/README.md
@@ -9,3 +9,4 @@ Samples demonstrating the [Harness AIContextProviders](../../../src/Microsoft.Ag
| [Harness_Step01_Research](./Harness_Step01_Research/README.md) | Using a ChatClientAgent with TodoProvider and AgentModeProvider for research, showcasing planning mode and todo management |
| [Harness_Step02_Research_WithBackgroundAgents](./Harness_Step02_Research_WithBackgroundAgents/README.md) | Using BackgroundAgentsProvider to delegate stock price lookups to a web-search background agent concurrently |
| [Harness_Step03_DataProcessing](./Harness_Step03_DataProcessing/README.md) | Using FileAccessProvider to give an agent access to CSV data files for reading, analysis, and output generation |
+| [Harness_Step05_Loop](./Harness_Step05_Loop/README.md) | Wrapping a HarnessAgent with the LoopAgent decorator to re-invoke it until a configured LoopEvaluator (completion marker, predicate, AI judge, or approval-aware loop) decides to stop |
diff --git a/dotnet/samples/02-agents/ModelContextProtocol/FoundryAgent_Hosted_MCP/Program.cs b/dotnet/samples/02-agents/ModelContextProtocol/FoundryAgent_Hosted_MCP/Program.cs
index ce27d036f9..7494cc9c0c 100644
--- a/dotnet/samples/02-agents/ModelContextProtocol/FoundryAgent_Hosted_MCP/Program.cs
+++ b/dotnet/samples/02-agents/ModelContextProtocol/FoundryAgent_Hosted_MCP/Program.cs
@@ -11,8 +11,8 @@ using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using OpenAI.Responses;
-var endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.");
-var model = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-5.4-mini";
+var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
+var model = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
// Get a client to create/retrieve server side agents with.
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
diff --git a/dotnet/samples/02-agents/ModelContextProtocol/FoundryAgent_Hosted_MCP/README.md b/dotnet/samples/02-agents/ModelContextProtocol/FoundryAgent_Hosted_MCP/README.md
index c1a62a9080..1ef0ca0f17 100644
--- a/dotnet/samples/02-agents/ModelContextProtocol/FoundryAgent_Hosted_MCP/README.md
+++ b/dotnet/samples/02-agents/ModelContextProtocol/FoundryAgent_Hosted_MCP/README.md
@@ -1,4 +1,4 @@
-# Prerequisites
+# Prerequisites
Before you begin, ensure you have the following prerequisites:
@@ -11,6 +11,6 @@ Before you begin, ensure you have the following prerequisites:
Set the following environment variables:
```powershell
-$env:AZURE_AI_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project" # Replace with your Microsoft Foundry resource endpoint
-$env:AZURE_AI_MODEL_DEPLOYMENT_NAME="gpt-5.4-mini" # Optional, defaults to gpt-5.4-mini
+$env:FOUNDRY_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project" # Replace with your Microsoft Foundry resource endpoint
+$env:FOUNDRY_MODEL="gpt-5.4-mini" # Optional, defaults to gpt-5.4-mini
```
diff --git a/dotnet/samples/03-workflows/Agents/FoundryAgent/Program.cs b/dotnet/samples/03-workflows/Agents/FoundryAgent/Program.cs
index 91d52398f9..5c2f02f733 100644
--- a/dotnet/samples/03-workflows/Agents/FoundryAgent/Program.cs
+++ b/dotnet/samples/03-workflows/Agents/FoundryAgent/Program.cs
@@ -23,10 +23,13 @@ public static class Program
private static async Task Main()
{
// Set up the Azure AI Project client
- var endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT")
- ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.");
- var deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-5.4-mini";
- var aiProjectClient = new AIProjectClient(new Uri(endpoint), new AzureCliCredential());
+ var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT")
+ ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
+ var deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-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.
+ var aiProjectClient = new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential());
// Create agents
AIAgent frenchAgent = await CreateTranslationAgentAsync("French", aiProjectClient, deploymentName);
diff --git a/dotnet/samples/03-workflows/Concurrent/Concurrent/Program.cs b/dotnet/samples/03-workflows/Concurrent/Concurrent/Program.cs
index 38e89653d6..43a04f5da1 100644
--- a/dotnet/samples/03-workflows/Concurrent/Concurrent/Program.cs
+++ b/dotnet/samples/03-workflows/Concurrent/Concurrent/Program.cs
@@ -33,10 +33,13 @@ public static class Program
private static async Task Main()
{
// Set up the Azure AI Project client
- var endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT")
- ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.");
- var deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-5.4-mini";
- var chatClient = new AIProjectClient(new Uri(endpoint), new AzureCliCredential())
+ var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT")
+ ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
+ var deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-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.
+ var chatClient = new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential())
.ProjectOpenAIClient.GetChatClient(deploymentName).AsIChatClient();
// Create the executors
diff --git a/dotnet/samples/03-workflows/Declarative/ExecuteCode/Program.cs b/dotnet/samples/03-workflows/Declarative/ExecuteCode/Program.cs
index 67d467266b..0566a5ff55 100644
--- a/dotnet/samples/03-workflows/Declarative/ExecuteCode/Program.cs
+++ b/dotnet/samples/03-workflows/Declarative/ExecuteCode/Program.cs
@@ -17,7 +17,7 @@ namespace Demo.DeclarativeCode;
///
///
/// Configuration
-/// Define AZURE_AI_PROJECT_ENDPOINT as a user-secret or environment variable that
+/// Define FOUNDRY_PROJECT_ENDPOINT as a user-secret or environment variable that
/// points to your Foundry project endpoint.
///
internal sealed class Program
diff --git a/dotnet/samples/03-workflows/Declarative/ExecuteWorkflow/Program.cs b/dotnet/samples/03-workflows/Declarative/ExecuteWorkflow/Program.cs
index f7e4dea673..041b6b10bf 100644
--- a/dotnet/samples/03-workflows/Declarative/ExecuteWorkflow/Program.cs
+++ b/dotnet/samples/03-workflows/Declarative/ExecuteWorkflow/Program.cs
@@ -19,7 +19,7 @@ namespace Demo.DeclarativeWorkflow;
///
///
/// Configuration
-/// Define AZURE_AI_PROJECT_ENDPOINT as a user-secret or environment variable that
+/// Define FOUNDRY_PROJECT_ENDPOINT as a user-secret or environment variable that
/// points to your Foundry project endpoint.
/// Usage
/// Provide the path to the workflow definition file as the first argument.
diff --git a/dotnet/samples/03-workflows/Declarative/HostedWorkflow/Program.cs b/dotnet/samples/03-workflows/Declarative/HostedWorkflow/Program.cs
index a871d233ca..29fc74a62d 100644
--- a/dotnet/samples/03-workflows/Declarative/HostedWorkflow/Program.cs
+++ b/dotnet/samples/03-workflows/Declarative/HostedWorkflow/Program.cs
@@ -21,7 +21,7 @@ namespace Demo.DeclarativeWorkflow;
///
///
/// Configuration
-/// Define AZURE_AI_PROJECT_ENDPOINT as a user-secret or environment variable that
+/// Define FOUNDRY_PROJECT_ENDPOINT as a user-secret or environment variable that
/// points to your Foundry project endpoint.
/// Usage
/// Provide the path to the workflow definition file as the first argument.
diff --git a/dotnet/samples/03-workflows/Declarative/README.md b/dotnet/samples/03-workflows/Declarative/README.md
index 6bd2c85824..1fe87a6a78 100644
--- a/dotnet/samples/03-workflows/Declarative/README.md
+++ b/dotnet/samples/03-workflows/Declarative/README.md
@@ -18,8 +18,8 @@ The configuraton required by the samples is:
|Setting Name| Description|
|:--|:--|
-|AZURE_AI_PROJECT_ENDPOINT| The endpoint URL of your Microsoft Foundry Project.|
-|AZURE_AI_MODEL_DEPLOYMENT_NAME| The name of the model deployment to use
+|FOUNDRY_PROJECT_ENDPOINT| The endpoint URL of your Microsoft Foundry Project.|
+|FOUNDRY_MODEL| The name of the model deployment to use
|AZURE_AI_BING_CONNECTION_ID| The name of the Bing Grounding connection configured in your Microsoft Foundry Project.|
To set your secrets with .NET Secret Manager:
@@ -45,13 +45,13 @@ To set your secrets with .NET Secret Manager:
4. Define setting that identifies your Microsoft Foundry Project (endpoint):
```
- dotnet user-secrets set "AZURE_AI_PROJECT_ENDPOINT" "https://..."
+ dotnet user-secrets set "FOUNDRY_PROJECT_ENDPOINT" "https://..."
```
5. Define setting that identifies your Microsoft Foundry Model Deployment (endpoint):
```
- dotnet user-secrets set "AZURE_AI_MODEL_DEPLOYMENT_NAME" "gpt-5"
+ dotnet user-secrets set "FOUNDRY_MODEL" "gpt-5"
```
6. Define setting that identifies your Bing Grounding connection:
@@ -63,8 +63,8 @@ To set your secrets with .NET Secret Manager:
You may alternatively set your secrets as an environment variable (PowerShell):
```pwsh
-$env:AZURE_AI_PROJECT_ENDPOINT="https://..."
-$env:AZURE_AI_MODEL_DEPLOYMENT_NAME="gpt-5"
+$env:FOUNDRY_PROJECT_ENDPOINT="https://..."
+$env:FOUNDRY_MODEL="gpt-5"
$env:AZURE_AI_BING_CONNECTION_ID="mybinggrounding"
```
@@ -96,4 +96,4 @@ To run the sampes from the command line:
dotnet run "An eco-friendly stainless steel water bottle that keeps drinks cold for 24 hours."
dotnet run c:/myworkflows/Marketing.yaml
```
- > The sample will allow for interactive input in the absence of an input argument.
\ No newline at end of file
+ > The sample will allow for interactive input in the absence of an input argument.
diff --git a/dotnet/samples/03-workflows/Evaluation/Evaluation_WorkflowEval/Program.cs b/dotnet/samples/03-workflows/Evaluation/Evaluation_WorkflowEval/Program.cs
index ce37dd89f6..6755074d7f 100644
--- a/dotnet/samples/03-workflows/Evaluation/Evaluation_WorkflowEval/Program.cs
+++ b/dotnet/samples/03-workflows/Evaluation/Evaluation_WorkflowEval/Program.cs
@@ -8,10 +8,13 @@ using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Extensions.AI;
-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";
+string endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT")
+ ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
+string deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "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 two agents: a planner and an executor.
diff --git a/dotnet/samples/03-workflows/Evaluation/Evaluation_WorkflowEval/README.md b/dotnet/samples/03-workflows/Evaluation/Evaluation_WorkflowEval/README.md
index 7a550f8833..64dfd72e7b 100644
--- a/dotnet/samples/03-workflows/Evaluation/Evaluation_WorkflowEval/README.md
+++ b/dotnet/samples/03-workflows/Evaluation/Evaluation_WorkflowEval/README.md
@@ -1,4 +1,4 @@
-# Evaluation - Workflow Eval
+# Evaluation - Workflow Eval
This sample demonstrates evaluating a multi-agent workflow with per-agent breakdown.
@@ -13,13 +13,13 @@ This sample demonstrates evaluating a multi-agent workflow with per-agent breakd
## Prerequisites
- .NET 10 SDK or later
-- Azure CLI installed and authenticated (`az login`)
+- Azure authentication available to `DefaultAzureCredential` (for local development, run `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"
+$env:FOUNDRY_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project"
+$env:FOUNDRY_MODEL="gpt-4o-mini"
```
## Run the sample
diff --git a/dotnet/samples/03-workflows/Evaluation/Evaluation_WorkflowExpectedOutputs/Program.cs b/dotnet/samples/03-workflows/Evaluation/Evaluation_WorkflowExpectedOutputs/Program.cs
index 30fa79faa8..b008f5043f 100644
--- a/dotnet/samples/03-workflows/Evaluation/Evaluation_WorkflowExpectedOutputs/Program.cs
+++ b/dotnet/samples/03-workflows/Evaluation/Evaluation_WorkflowExpectedOutputs/Program.cs
@@ -10,9 +10,9 @@ using Microsoft.Agents.AI.Workflows;
using Microsoft.Extensions.AI;
using FoundryEvals = Microsoft.Agents.AI.Foundry.FoundryEvals;
-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";
+string endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT")
+ ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
+string deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "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
diff --git a/dotnet/samples/03-workflows/Evaluation/Evaluation_WorkflowExpectedOutputs/README.md b/dotnet/samples/03-workflows/Evaluation/Evaluation_WorkflowExpectedOutputs/README.md
index 9390e91e4c..0462f618dc 100644
--- a/dotnet/samples/03-workflows/Evaluation/Evaluation_WorkflowExpectedOutputs/README.md
+++ b/dotnet/samples/03-workflows/Evaluation/Evaluation_WorkflowExpectedOutputs/README.md
@@ -1,4 +1,4 @@
-# Evaluation - Workflow Expected Outputs
+# Evaluation - Workflow Expected Outputs
This sample demonstrates evaluating a multi-agent workflow's final answer
against a golden expected output using Foundry's reference-based **Similarity**
@@ -20,13 +20,13 @@ Evals API.
## Prerequisites
- .NET 10 SDK or later
-- Azure CLI installed and authenticated (`az login`)
+- Azure authentication available to `DefaultAzureCredential` (for local development, run `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"
+$env:FOUNDRY_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project"
+$env:FOUNDRY_MODEL="gpt-4o-mini"
```
## Run the sample
diff --git a/dotnet/samples/03-workflows/Orchestration/Handoff/Program.cs b/dotnet/samples/03-workflows/Orchestration/Handoff/Program.cs
index 69cf8c168b..c5fefe191f 100644
--- a/dotnet/samples/03-workflows/Orchestration/Handoff/Program.cs
+++ b/dotnet/samples/03-workflows/Orchestration/Handoff/Program.cs
@@ -6,9 +6,9 @@ using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Extensions.AI;
-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-5.4-mini";
+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-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
diff --git a/dotnet/samples/03-workflows/Orchestration/Magentic/Program.cs b/dotnet/samples/03-workflows/Orchestration/Magentic/Program.cs
index 4cb148b2cd..0382745d7d 100644
--- a/dotnet/samples/03-workflows/Orchestration/Magentic/Program.cs
+++ b/dotnet/samples/03-workflows/Orchestration/Magentic/Program.cs
@@ -33,9 +33,9 @@ public static class Program
private static async Task Main()
{
- 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-5.4-mini";
+ 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-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
diff --git a/dotnet/samples/03-workflows/Orchestration/Magentic/README.md b/dotnet/samples/03-workflows/Orchestration/Magentic/README.md
index e8314759f1..739229df26 100644
--- a/dotnet/samples/03-workflows/Orchestration/Magentic/README.md
+++ b/dotnet/samples/03-workflows/Orchestration/Magentic/README.md
@@ -15,8 +15,8 @@ This sample showcases the Magentic Orchestration Pattern in .NET, setting up a t
## Prerequisites
-- `AZURE_AI_PROJECT_ENDPOINT` set to your Azure AI Foundry project endpoint
-- `AZURE_AI_MODEL_DEPLOYMENT_NAME` set to your model deployment name (defaults to `gpt-5.4-mini`)
+- `FOUNDRY_PROJECT_ENDPOINT` set to your Azure AI Foundry project endpoint
+- `FOUNDRY_MODEL` set to your model deployment name (defaults to `gpt-5.4-mini`)
- `az login` completed before running the sample
## Running the Sample
diff --git a/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/05_WorkflowAndAgents/Program.cs b/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/05_WorkflowAndAgents/Program.cs
index 51b9fb4d7f..b9108b4b29 100644
--- a/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/05_WorkflowAndAgents/Program.cs
+++ b/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/05_WorkflowAndAgents/Program.cs
@@ -27,6 +27,9 @@ string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYM
string? azureOpenAiKey = Environment.GetEnvironmentVariable("AZURE_OPENAI_API_KEY");
AzureOpenAIClient client = !string.IsNullOrEmpty(azureOpenAiKey)
? new AzureOpenAIClient(new Uri(endpoint), new AzureKeyCredential(azureOpenAiKey))
+ // 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.
: new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential());
ChatClient chatClient = client.GetChatClient(deploymentName);
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/README.md
index 5fcfddab22..404f88d074 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/README.md
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/README.md
@@ -71,6 +71,34 @@ curl -X POST http://localhost:8088/invocations \
-d "Hello from Docker!"
```
+## Deploying to Foundry (azd spec)
+
+This sample includes an `azd` manifest (`agent.manifest.yaml`) and hosted agent spec (`agent.yaml`) for deployment to Foundry.
+
+Initialize an `azd` project from this sample's manifest:
+
+```bash
+mkdir hosted-invocations-echo-agent && cd hosted-invocations-echo-agent
+azd ai agent init -m https://github.com/microsoft/agent-framework/blob/main/dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/agent.manifest.yaml
+```
+
+Then deploy:
+
+```bash
+azd deploy
+```
+
+If you need to override defaults, set deployment-time environment variables in the `azd` environment before deploying:
+
+```bash
+azd env set AGENT_NAME hosted-invocations-echo-agent
+azd env set AZURE_AI_MODEL_DEPLOYMENT_NAME gpt-4o
+```
+
+For end-to-end hosted agent deployment guidance, see the [official deployment guide](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/deploy-hosted-agent).
+
+---
+
## NuGet package users
If you are consuming the Agent Framework as a NuGet package (not building from source), use the standard `Dockerfile` instead of `Dockerfile.contributor`. See the commented section in `Hosted-Invocations-EchoAgent.csproj` for the `PackageReference` alternative.
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/.env.example b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/.env.example
index 79fac42841..c40c94eb4a 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/.env.example
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/.env.example
@@ -1,7 +1,7 @@
-AZURE_AI_PROJECT_ENDPOINT=
+FOUNDRY_PROJECT_ENDPOINT=
ASPNETCORE_URLS=http://+:8088
ASPNETCORE_ENVIRONMENT=Development
-AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o
+FOUNDRY_MODEL=gpt-4o
AGENT_NAME=hosted-agent-skills
SKILL_NAMES=support-style,escalation-policy
# Set to true to provision sample skills to Foundry on startup (first-run convenience).
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/Program.cs
index 90dc325120..b7a3925841 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/Program.cs
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/Program.cs
@@ -34,9 +34,9 @@ using Microsoft.Extensions.AI;
// Load .env file if present (for local development)
Env.TraversePath().Load();
-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";
+string endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT")
+ ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
+string deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-4o";
string skillNames = Environment.GetEnvironmentVariable("SKILL_NAMES")
?? throw new InvalidOperationException("SKILL_NAMES is not set. Provide a comma-separated list of skill names (e.g., support-style,escalation-policy).");
@@ -56,6 +56,9 @@ foreach (string name in requestedSkills)
}
}
+// 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.
// Use a chained credential: try a temporary dev token first (for local Docker debugging),
// then fall back to DefaultAzureCredential (for local dev via dotnet run / managed identity in production).
TokenCredential credential = new ChainedTokenCredential(
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/README.md
index 28917a19e0..747e1dec4f 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/README.md
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/README.md
@@ -1,4 +1,4 @@
-# What this sample demonstrates
+# What this sample demonstrates
An [Agent Framework](https://github.com/microsoft/agent-framework) agent that loads its behavioral guidelines from [**Foundry Skills**](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/tools/skills) at startup, hosted using the **Responses protocol**. Skills are authored once as `SKILL.md` files, uploaded to your Foundry project through the Skills REST API, and downloaded by the agent on boot so updates ship without code changes.
@@ -54,8 +54,8 @@ Your identity (or the Managed Identity running the container in production) need
Set the required environment variables and run the sample with `dotnet run`:
```bash
-export AZURE_AI_PROJECT_ENDPOINT="https://.services.ai.azure.com/api/projects/"
-export AZURE_AI_MODEL_DEPLOYMENT_NAME="gpt-4o"
+export FOUNDRY_PROJECT_ENDPOINT="https://.services.ai.azure.com/api/projects/"
+export FOUNDRY_MODEL="gpt-4o"
export SKILL_NAMES="support-style,escalation-policy"
export PROVISION_SAMPLE_SKILLS="true" # First run only — provisions skills to Foundry
```
@@ -107,3 +107,29 @@ azd env set SKILL_NAMES "support-style,escalation-policy"
The deployed agent's Managed Identity needs **Azure AI User** on the Foundry project to download skills at startup.
> The `skills/` source folder is **not** deployed to Foundry — only the downloaded skills are used at runtime. The provisioning step must have been run against the same Foundry project before the agent can download the skills.
+
+### Deploying to Foundry (azd spec)
+
+This sample includes an `azd` manifest (`agent.manifest.yaml`) and hosted agent spec (`agent.yaml`) for deployment to Foundry.
+
+Initialize an `azd` project from this sample's manifest:
+
+```bash
+mkdir hosted-agent-skills && cd hosted-agent-skills
+azd ai agent init -m https://github.com/microsoft/agent-framework/blob/main/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/agent.manifest.yaml
+```
+
+Then deploy:
+
+```bash
+azd deploy
+```
+
+If you need to override defaults, set deployment-time environment variables in the `azd` environment before deploying:
+
+```bash
+azd env set AGENT_NAME hosted-agent-skills
+azd env set AZURE_AI_MODEL_DEPLOYMENT_NAME gpt-4o
+```
+
+For end-to-end hosted agent deployment guidance, see the [official deployment guide](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/deploy-hosted-agent).
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/agent.manifest.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/agent.manifest.yaml
index 6be5e63017..83f442a637 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/agent.manifest.yaml
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/agent.manifest.yaml
@@ -26,8 +26,8 @@ template:
cpu: "0.25"
memory: 0.5Gi
environment_variables:
- - name: AZURE_AI_MODEL_DEPLOYMENT_NAME
- value: "{{AZURE_AI_MODEL_DEPLOYMENT_NAME}}"
+ - name: FOUNDRY_MODEL
+ value: "{{FOUNDRY_MODEL}}"
- name: SKILL_NAMES
value: "{{SKILL_NAMES}}"
parameters:
@@ -38,4 +38,4 @@ parameters:
resources:
- kind: model
id: gpt-4.1-mini
- name: AZURE_AI_MODEL_DEPLOYMENT_NAME
+ name: FOUNDRY_MODEL
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/agent.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/agent.yaml
index 363107d0ea..a63017f828 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/agent.yaml
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/agent.yaml
@@ -8,7 +8,7 @@ resources:
cpu: "0.25"
memory: 0.5Gi
environment_variables:
- - name: AZURE_AI_MODEL_DEPLOYMENT_NAME
- value: ${AZURE_AI_MODEL_DEPLOYMENT_NAME}
+ - name: FOUNDRY_MODEL
+ value: ${FOUNDRY_MODEL}
- name: SKILL_NAMES
value: ${SKILL_NAMES}
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/scripts/smoke.ps1 b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/scripts/smoke.ps1
index 09094706a1..e13c94cfd6 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/scripts/smoke.ps1
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/scripts/smoke.ps1
@@ -11,7 +11,7 @@
Prerequisites:
- Docker
- az login (token is fetched from the host)
- - .env populated with AZURE_AI_PROJECT_ENDPOINT and model deployment
+ - .env populated with FOUNDRY_PROJECT_ENDPOINT and model deployment
- Skills provisioned to Foundry (set PROVISION_SAMPLE_SKILLS=true on first run)
.NOTES
This script is for local Docker debugging only. The Foundry platform supplies the
@@ -30,7 +30,7 @@ $ErrorActionPreference = 'Stop'
Set-Location -Path $PSScriptRoot/..
if (-not (Test-Path .env)) {
- throw '.env not found. Copy .env.example to .env and fill in AZURE_AI_PROJECT_ENDPOINT.'
+ throw '.env not found. Copy .env.example to .env and fill in FOUNDRY_PROJECT_ENDPOINT.'
}
Write-Host '==> Publishing sample for linux-musl-x64 ...'
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AzureSearchRag/.env.example b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AzureSearchRag/.env.example
index 3b63f9d218..fe9adaaad8 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AzureSearchRag/.env.example
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AzureSearchRag/.env.example
@@ -1,5 +1,5 @@
-AZURE_AI_PROJECT_ENDPOINT=
-AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o
+FOUNDRY_PROJECT_ENDPOINT=
+FOUNDRY_MODEL=gpt-4o
AZURE_SEARCH_ENDPOINT=
AZURE_SEARCH_INDEX_NAME=contoso-outdoors
AZURE_BEARER_TOKEN_FOUNDRY=DefaultAzureCredential
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AzureSearchRag/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AzureSearchRag/Program.cs
index 4b97324134..51b827467b 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AzureSearchRag/Program.cs
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AzureSearchRag/Program.cs
@@ -23,15 +23,18 @@ using OpenAI.Chat;
// Load .env file if present (for local development)
Env.TraversePath().Load();
-string projectEndpoint = 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";
+string projectEndpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT")
+ ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
+string deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-4o";
string searchEndpoint = Environment.GetEnvironmentVariable("AZURE_SEARCH_ENDPOINT")
?? throw new InvalidOperationException("AZURE_SEARCH_ENDPOINT is not set.");
string searchIndexName = Environment.GetEnvironmentVariable("AZURE_SEARCH_INDEX_NAME")
?? throw new InvalidOperationException("AZURE_SEARCH_INDEX_NAME is not set.");
+// 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.
// Use a chained credential. Try a temporary dev token first (for local Docker debugging),
// then fall back to DefaultAzureCredential (for local dev via dotnet run / managed identity in
// production). The dev credential is scope aware so a single instance serves both Foundry and
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AzureSearchRag/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AzureSearchRag/README.md
index ede4db1010..638ca559df 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AzureSearchRag/README.md
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AzureSearchRag/README.md
@@ -1,4 +1,4 @@
-# Hosted-AzureSearchRag
+# Hosted-AzureSearchRag
A hosted agent with **Retrieval Augmented Generation (RAG)** capabilities backed by **Azure AI Search**. The agent grounds its answers in product documentation by running a keyword search against an Azure AI Search index before each model invocation, then citing the source in its response.
@@ -77,8 +77,8 @@ cp .env.example .env
Edit `.env`:
```env
-AZURE_AI_PROJECT_ENDPOINT=https://.services.ai.azure.com/api/projects/
-AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o
+FOUNDRY_PROJECT_ENDPOINT=https://.services.ai.azure.com/api/projects/
+FOUNDRY_MODEL=gpt-4o
AZURE_SEARCH_ENDPOINT=https://.search.windows.net
AZURE_SEARCH_INDEX_NAME=contoso-outdoors
AZURE_BEARER_TOKEN_FOUNDRY=DefaultAzureCredential
@@ -174,6 +174,34 @@ The model receives the top three search results as additional context and cites
Replace the seed documents (or point the sample at an existing index with your own content) to ground the agent in your own knowledge base.
+## Deploying to Foundry (azd spec)
+
+This sample includes an `azd` manifest (`agent.manifest.yaml`) and hosted agent spec (`agent.yaml`) for deployment to Foundry.
+
+Initialize an `azd` project from this sample's manifest:
+
+```bash
+mkdir hosted-azure-search-rag && cd hosted-azure-search-rag
+azd ai agent init -m https://github.com/microsoft/agent-framework/blob/main/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AzureSearchRag/agent.manifest.yaml
+```
+
+Then deploy:
+
+```bash
+azd deploy
+```
+
+If you need to override defaults, set deployment-time environment variables in the `azd` environment before deploying:
+
+```bash
+azd env set AGENT_NAME hosted-azure-search-rag
+azd env set AZURE_AI_MODEL_DEPLOYMENT_NAME gpt-4o
+```
+
+For end-to-end hosted agent deployment guidance, see the [official deployment guide](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/deploy-hosted-agent).
+
+---
+
## NuGet package users
If you are consuming the Agent Framework as a NuGet package (not building from source), use the standard `Dockerfile` instead of `Dockerfile.contributor`. See the commented section in `HostedAzureSearchRag.csproj` for the `PackageReference` alternative.
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/.env.example b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/.env.example
index 984e8625cf..99a2f75c03 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/.env.example
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/.env.example
@@ -1,6 +1,6 @@
-AZURE_AI_PROJECT_ENDPOINT=
+FOUNDRY_PROJECT_ENDPOINT=
ASPNETCORE_URLS=http://+:8088
ASPNETCORE_ENVIRONMENT=Development
-AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o
+FOUNDRY_MODEL=gpt-4o
AGENT_NAME=hosted-chat-client-agent
AZURE_BEARER_TOKEN=DefaultAzureCredential
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/Program.cs
index b4b08ba5a8..b25889b93a 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/Program.cs
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/Program.cs
@@ -11,14 +11,17 @@ using Microsoft.Agents.AI.Foundry.Hosting;
// Load .env file if present (for local development)
Env.TraversePath().Load();
-var projectEndpoint = new Uri(Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT")
- ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set."));
+var projectEndpoint = new Uri(Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT")
+ ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set."));
var agentName = Environment.GetEnvironmentVariable("AGENT_NAME")
?? throw new InvalidOperationException("AGENT_NAME is not set.");
-var deployment = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-4o";
+var deployment = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-4o";
+// 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.
// Use a chained credential: try a temporary dev token first (for local Docker debugging),
// then fall back to DefaultAzureCredential (for local dev via dotnet run / managed identity running in foundry).
TokenCredential credential = new ChainedTokenCredential(
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/README.md
index ace8892572..89c9bb8592 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/README.md
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/README.md
@@ -1,4 +1,4 @@
-# Hosted-ChatClientAgent
+# Hosted-ChatClientAgent
A simple general-purpose AI assistant hosted as a Foundry Hosted Agent using the Agent Framework instance hosting pattern. The agent is created inline via `AIProjectClient.AsAIAgent(model, instructions)` and served using the Responses protocol.
@@ -19,10 +19,10 @@ cp .env.example .env
Edit `.env` and set your Azure AI Foundry project endpoint:
```env
-AZURE_AI_PROJECT_ENDPOINT=https://.services.ai.azure.com/api/projects/
+FOUNDRY_PROJECT_ENDPOINT=https://.services.ai.azure.com/api/projects/
ASPNETCORE_URLS=http://+:8088
ASPNETCORE_ENVIRONMENT=Development
-AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o
+FOUNDRY_MODEL=gpt-4o
```
> **Note:** `.env` is gitignored. The `.env.example` template is checked in as a reference.
@@ -104,6 +104,32 @@ curl -X POST http://localhost:8088/responses \
-d '{"input": "Hello!", "model": "hosted-chat-client-agent"}'
```
+## Deploying to Foundry (azd spec)
+
+This sample includes an `azd` manifest (`agent.manifest.yaml`) and hosted agent spec (`agent.yaml`) for deployment to Foundry.
+
+Initialize an `azd` project from this sample's manifest:
+
+```bash
+mkdir hosted-chat-client-agent && cd hosted-chat-client-agent
+azd ai agent init -m https://github.com/microsoft/agent-framework/blob/main/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/agent.manifest.yaml
+```
+
+Then deploy:
+
+```bash
+azd deploy
+```
+
+If you need to override defaults, set deployment-time environment variables in the `azd` environment before deploying:
+
+```bash
+azd env set AGENT_NAME hosted-chat-client-agent
+azd env set AZURE_AI_MODEL_DEPLOYMENT_NAME gpt-4o
+```
+
+For end-to-end hosted agent deployment guidance, see the [official deployment guide](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/deploy-hosted-agent).
+
## NuGet package users
If you are consuming the Agent Framework as a NuGet package (not building from source), use the standard `Dockerfile` instead of `Dockerfile.contributor` — it performs a full `dotnet restore` and `dotnet publish` inside the container. See the commented section in `HostedChatClientAgent.csproj` for the `PackageReference` alternative.
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/.env.example b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/.env.example
index b8fe9e8e7a..04335e65b8 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/.env.example
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/.env.example
@@ -1,5 +1,5 @@
-AZURE_AI_PROJECT_ENDPOINT=
+FOUNDRY_PROJECT_ENDPOINT=
ASPNETCORE_URLS=http://+:8088
ASPNETCORE_ENVIRONMENT=Development
-AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o
+FOUNDRY_MODEL=gpt-4o
AZURE_BEARER_TOKEN=DefaultAzureCredential
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/Program.cs
index 3f79a0eb7d..4472370e9a 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/Program.cs
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/Program.cs
@@ -20,8 +20,8 @@
// indirect prompt injection in an uploaded file.
//
// Required environment variables:
-// AZURE_AI_PROJECT_ENDPOINT - Azure AI Foundry project endpoint
-// AZURE_AI_MODEL_DEPLOYMENT_NAME - Model deployment name (default: gpt-4o)
+// FOUNDRY_PROJECT_ENDPOINT - Azure AI Foundry project endpoint
+// FOUNDRY_MODEL - Model deployment name (default: gpt-4o)
//
// Optional:
// AGENT_NAME - Agent name (default: hosted-files)
@@ -46,10 +46,13 @@ Env.TraversePath().Load();
// Bypass SampleEnvironment alias (which prompts on missing env vars) for optional values.
string? GetOptionalEnv(string key) => System.Environment.GetEnvironmentVariable(key);
-string endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT")
- ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.");
-string deploymentName = GetOptionalEnv("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-4o";
+string endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT")
+ ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
+string deploymentName = GetOptionalEnv("FOUNDRY_MODEL") ?? "gpt-4o";
+// 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.
// Use a chained credential: try a temporary dev token first (for local Docker debugging),
// then fall back to DefaultAzureCredential (for local dev via dotnet run / managed identity in production).
TokenCredential credential = new ChainedTokenCredential(
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/README.md
index 729aca5c5f..d9c77c073d 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/README.md
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/README.md
@@ -1,4 +1,4 @@
-# Hosted-Files
+# Hosted-Files
A hosted agent that demonstrates **two distinct file knowledge sources** through scoped, security-hardened tools:
@@ -57,10 +57,10 @@ cp .env.example .env
Edit `.env`:
```env
-AZURE_AI_PROJECT_ENDPOINT=https://.services.ai.azure.com/api/projects/
+FOUNDRY_PROJECT_ENDPOINT=https://.services.ai.azure.com/api/projects/
ASPNETCORE_URLS=http://+:8088
ASPNETCORE_ENVIRONMENT=Development
-AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o
+FOUNDRY_MODEL=gpt-4o
```
> `.env` is gitignored. The `.env.example` template is checked in as a reference.
@@ -112,6 +112,34 @@ docker run --rm -p 8088:8088 \
The bundled `resources/` folder is part of the published output and ships inside the image.
+## Deploying to Foundry (azd spec)
+
+This sample includes an `azd` manifest (`agent.manifest.yaml`) and hosted agent spec (`agent.yaml`) for deployment to Foundry.
+
+Initialize an `azd` project from this sample's manifest:
+
+```bash
+mkdir hosted-files && cd hosted-files
+azd ai agent init -m https://github.com/microsoft/agent-framework/blob/main/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/agent.manifest.yaml
+```
+
+Then deploy:
+
+```bash
+azd deploy
+```
+
+If you need to override defaults, set deployment-time environment variables in the `azd` environment before deploying:
+
+```bash
+azd env set AGENT_NAME hosted-files
+azd env set AZURE_AI_MODEL_DEPLOYMENT_NAME gpt-4o
+```
+
+For end-to-end hosted agent deployment guidance, see the [official deployment guide](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/deploy-hosted-agent).
+
+---
+
## NuGet package users
If consuming the Agent Framework as a NuGet package, use the standard `Dockerfile` instead of `Dockerfile.contributor` and switch the `ProjectReference` entries in `HostedFiles.csproj` to `PackageReference` (commented section in the csproj).
@@ -125,4 +153,4 @@ Drop additional text files into [`resources/`](./resources/). The csproj `/resources` (`/app/resources/` in container) |
-| `HOME` | The per-session sandbox volume root the session-files tools read from. Set by the Foundry platform; can be overridden for local testing. | `/home/session` |
\ No newline at end of file
+| `HOME` | The per-session sandbox volume root the session-files tools read from. Set by the Foundry platform; can be overridden for local testing. | `/home/session` |
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/.env.example b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/.env.example
index c72380d125..aaeb71a9e4 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/.env.example
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/.env.example
@@ -1,4 +1,4 @@
-AZURE_AI_PROJECT_ENDPOINT=
+FOUNDRY_PROJECT_ENDPOINT=
ASPNETCORE_URLS=http://+:8088
ASPNETCORE_ENVIRONMENT=Development
AGENT_NAME=
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/Program.cs
index f83a67f66d..1c0f1768ee 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/Program.cs
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/Program.cs
@@ -12,11 +12,14 @@ using Microsoft.Agents.AI.Foundry.Hosting;
// Load .env file if present (for local development)
Env.TraversePath().Load();
-var projectEndpoint = new Uri(Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT")
- ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set."));
+var projectEndpoint = new Uri(Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT")
+ ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set."));
var agentName = Environment.GetEnvironmentVariable("AGENT_NAME")
?? throw new InvalidOperationException("AGENT_NAME is not set.");
+// 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.
// Use a chained credential: try a temporary dev token first (for local Docker debugging),
// then fall back to DefaultAzureCredential (for local dev via dotnet run / managed identity running in foundry).
TokenCredential credential = new ChainedTokenCredential(
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/README.md
index 8265a80632..a4894e0d24 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/README.md
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/README.md
@@ -1,4 +1,4 @@
-# Hosted-FoundryAgent
+# Hosted-FoundryAgent
A hosted agent that delegates to a **Foundry-managed agent definition**. Instead of defining the model, instructions, and tools inline in code, this sample retrieves an existing agent registered in the Foundry platform via `AIProjectClient.AsAIAgent(agentRecord)` and hosts it using the Responses protocol.
@@ -21,7 +21,7 @@ cp .env.example .env
Edit `.env` and set your Azure AI Foundry project endpoint:
```env
-AZURE_AI_PROJECT_ENDPOINT=https://.services.ai.azure.com/api/projects/
+FOUNDRY_PROJECT_ENDPOINT=https://.services.ai.azure.com/api/projects/
ASPNETCORE_URLS=http://+:8088
ASPNETCORE_ENVIRONMENT=Development
```
@@ -107,6 +107,32 @@ curl -X POST http://localhost:8088/responses \
-d '{"input": "Hello!", "model": ""}'
```
+## Deploying to Foundry (azd spec)
+
+This sample includes an `azd` manifest (`agent.manifest.yaml`) and hosted agent spec (`agent.yaml`) for deployment to Foundry.
+
+Initialize an `azd` project from this sample's manifest:
+
+```bash
+mkdir hosted-foundry-agent && cd hosted-foundry-agent
+azd ai agent init -m https://github.com/microsoft/agent-framework/blob/main/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/agent.manifest.yaml
+```
+
+Then deploy:
+
+```bash
+azd deploy
+```
+
+If you need to override defaults, set deployment-time environment variables in the `azd` environment before deploying:
+
+```bash
+azd env set AGENT_NAME hosted-foundry-agent
+azd env set AZURE_AI_MODEL_DEPLOYMENT_NAME gpt-4o
+```
+
+For end-to-end hosted agent deployment guidance, see the [official deployment guide](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/deploy-hosted-agent).
+
## NuGet package users
If you are consuming the Agent Framework as a NuGet package (not building from source), use the standard `Dockerfile` instead of `Dockerfile.contributor` — it performs a full `dotnet restore` and `dotnet publish` inside the container. See the commented section in `HostedFoundryAgent.csproj` for the `PackageReference` alternative.
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/.env.example b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/.env.example
index b8fe9e8e7a..04335e65b8 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/.env.example
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/.env.example
@@ -1,5 +1,5 @@
-AZURE_AI_PROJECT_ENDPOINT=
+FOUNDRY_PROJECT_ENDPOINT=
ASPNETCORE_URLS=http://+:8088
ASPNETCORE_ENVIRONMENT=Development
-AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o
+FOUNDRY_MODEL=gpt-4o
AZURE_BEARER_TOKEN=DefaultAzureCredential
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/Program.cs
index 8a665d38a3..19922f19e5 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/Program.cs
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/Program.cs
@@ -19,10 +19,13 @@ using Microsoft.Extensions.AI;
// Load .env file if present (for local development)
Env.TraversePath().Load();
-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";
+string endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT")
+ ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
+string deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-4o";
+// 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.
// Use a chained credential: try a temporary dev token first (for local Docker debugging),
// then fall back to DefaultAzureCredential (for local dev via dotnet run / managed identity in production).
TokenCredential credential = new ChainedTokenCredential(
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/README.md
index 8016ff7ae9..246c2a495a 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/README.md
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/README.md
@@ -1,4 +1,4 @@
-# Hosted-LocalTools
+# Hosted-LocalTools
A hosted agent with **local C# function tools** for hotel search. Demonstrates how to define and wire local tools that the LLM can invoke — a key advantage of code-based hosted agents over prompt agents.
@@ -21,10 +21,10 @@ cp .env.example .env
Edit `.env` and set your Azure AI Foundry project endpoint:
```env
-AZURE_AI_PROJECT_ENDPOINT=https://.services.ai.azure.com/api/projects/
+FOUNDRY_PROJECT_ENDPOINT=https://.services.ai.azure.com/api/projects/
ASPNETCORE_URLS=http://+:8088
ASPNETCORE_ENVIRONMENT=Development
-AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o
+FOUNDRY_MODEL=gpt-4o
```
> **Note:** `.env` is gitignored. The `.env.example` template is checked in as a reference.
@@ -108,6 +108,34 @@ The agent has a single tool `GetAvailableHotels` defined as a C# method with `[D
The tool searches a mock database of 6 Seattle hotels and returns formatted results with name, location, rating, and pricing.
+## Deploying to Foundry (azd spec)
+
+This sample includes an `azd` manifest (`agent.manifest.yaml`) and hosted agent spec (`agent.yaml`) for deployment to Foundry.
+
+Initialize an `azd` project from this sample's manifest:
+
+```bash
+mkdir hosted-local-tools && cd hosted-local-tools
+azd ai agent init -m https://github.com/microsoft/agent-framework/blob/main/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/agent.manifest.yaml
+```
+
+Then deploy:
+
+```bash
+azd deploy
+```
+
+If you need to override defaults, set deployment-time environment variables in the `azd` environment before deploying:
+
+```bash
+azd env set AGENT_NAME hosted-local-tools
+azd env set AZURE_AI_MODEL_DEPLOYMENT_NAME gpt-4o
+```
+
+For end-to-end hosted agent deployment guidance, see the [official deployment guide](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/deploy-hosted-agent).
+
+---
+
## NuGet package users
If you are consuming the Agent Framework as a NuGet package (not building from source), use the standard `Dockerfile` instead of `Dockerfile.contributor`. See the commented section in `HostedLocalTools.csproj` for the `PackageReference` alternative.
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/.env.example b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/.env.example
index b8fe9e8e7a..04335e65b8 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/.env.example
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/.env.example
@@ -1,5 +1,5 @@
-AZURE_AI_PROJECT_ENDPOINT=
+FOUNDRY_PROJECT_ENDPOINT=
ASPNETCORE_URLS=http://+:8088
ASPNETCORE_ENVIRONMENT=Development
-AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o
+FOUNDRY_MODEL=gpt-4o
AZURE_BEARER_TOKEN=DefaultAzureCredential
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/Program.cs
index 1eed2126f7..ae11482133 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/Program.cs
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/Program.cs
@@ -28,10 +28,13 @@ using ModelContextProtocol.Client;
// Load .env file if present (for local development)
Env.TraversePath().Load();
-var projectEndpoint = new Uri(Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT")
- ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set."));
-var deployment = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-4o";
+var projectEndpoint = new Uri(Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT")
+ ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set."));
+var deployment = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-4o";
+// 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.
// Use a chained credential: try a temporary dev token first (for local Docker debugging),
// then fall back to DefaultAzureCredential (for local dev via dotnet run / managed identity in production).
TokenCredential credential = new ChainedTokenCredential(
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/README.md
index 3773d9760d..d635de65bd 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/README.md
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/README.md
@@ -1,4 +1,4 @@
-# Hosted-McpTools
+# Hosted-McpTools
A hosted agent demonstrating **two layers of MCP (Model Context Protocol) tool integration**:
@@ -33,8 +33,8 @@ cp .env.example .env
Edit `.env`:
```env
-AZURE_AI_PROJECT_ENDPOINT=https://.services.ai.azure.com/api/projects/
-AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o
+FOUNDRY_PROJECT_ENDPOINT=https://.services.ai.azure.com/api/projects/
+FOUNDRY_MODEL=gpt-4o
```
## Running directly (contributors)
@@ -78,6 +78,40 @@ docker run --rm -p 8088:8088 \
hosted-mcp-tools
```
+## Deploying to Foundry (azd spec)
+
+This sample includes an `azd` manifest (`agent.manifest.yaml`) and hosted agent spec (`agent.yaml`) for deployment to Foundry.
+
+Initialize an `azd` project from this sample's manifest:
+
+```bash
+mkdir mcp-tools && cd mcp-tools
+azd ai agent init -m https://github.com/microsoft/agent-framework/blob/main/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/agent.manifest.yaml
+```
+
+Then deploy:
+
+```bash
+azd deploy
+```
+
+If you need to override defaults, set deployment-time environment variables in the `azd` environment before deploying:
+
+```bash
+azd env set AGENT_NAME mcp-tools
+azd env set AZURE_AI_MODEL_DEPLOYMENT_NAME gpt-4o
+```
+
+For end-to-end hosted agent deployment guidance, see the [official deployment guide](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/deploy-hosted-agent).
+
+---
+
## NuGet package users
Use the standard `Dockerfile` instead of `Dockerfile.contributor`. See the commented section in `HostedMcpTools.csproj` for the `PackageReference` alternative.
+
+## Related samples
+
+- [`Hosted-Toolbox/`](../Hosted-Toolbox/) — connects to a single Foundry Toolbox via the AF Foundry hosting bridge (`AddFoundryToolboxes` + `FoundryAITool.CreateHostedMcpToolbox`).
+- [`Hosted-Toolbox-AuthPaths/`](../Hosted-Toolbox-AuthPaths/) — same hosting bones as `Hosted-Toolbox/`, but the toolbox bundles three MCP tools each authenticated differently (key, Entra agent identity, inline `Authorization`), driven by the shared `Using-Samples/SimpleAgent/` REPL.
+
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-MemoryAgent/.env.example b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-MemoryAgent/.env.example
index 29d86f5ef1..9eb77d2a5b 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-MemoryAgent/.env.example
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-MemoryAgent/.env.example
@@ -1,7 +1,7 @@
-AZURE_AI_PROJECT_ENDPOINT=
+FOUNDRY_PROJECT_ENDPOINT=
ASPNETCORE_URLS=http://+:8088
ASPNETCORE_ENVIRONMENT=Development
-AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o
+FOUNDRY_MODEL=gpt-4o
AZURE_AI_EMBEDDING_DEPLOYMENT_NAME=text-embedding-ada-002
AZURE_AI_MEMORY_STORE_ID=hosted-memory-sample
AGENT_NAME=hosted-memory-agent
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-MemoryAgent/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-MemoryAgent/Program.cs
index 22bfd316b3..1221dcbc9d 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-MemoryAgent/Program.cs
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-MemoryAgent/Program.cs
@@ -26,14 +26,17 @@ using Microsoft.Extensions.AI;
// Load .env file if present (for local development).
Env.TraversePath().Load();
-var projectEndpoint = new Uri(Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT")
- ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set."));
+var projectEndpoint = new Uri(Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT")
+ ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set."));
var agentName = Environment.GetEnvironmentVariable("AGENT_NAME")
?? throw new InvalidOperationException("AGENT_NAME is not set.");
-var deployment = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-4o";
+var deployment = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-4o";
var embeddingDeployment = Environment.GetEnvironmentVariable("AZURE_AI_EMBEDDING_DEPLOYMENT_NAME") ?? "text-embedding-ada-002";
var memoryStoreName = Environment.GetEnvironmentVariable("AZURE_AI_MEMORY_STORE_ID") ?? "hosted-memory-sample";
+// 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.
// Use a chained credential: try a temporary dev token first (for local Docker debugging),
// then fall back to DefaultAzureCredential (for local dev via dotnet run / managed identity in foundry).
TokenCredential credential = new ChainedTokenCredential(
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-MemoryAgent/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-MemoryAgent/README.md
index d9b3a11825..69c8e34ec5 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-MemoryAgent/README.md
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-MemoryAgent/README.md
@@ -1,4 +1,4 @@
-# Hosted-MemoryAgent
+# Hosted-MemoryAgent
A hosted Foundry agent that uses **FoundryMemoryProvider** to remember user-private details across
requests and across sessions, scoped per end user via the Foundry platform's isolation keys. The
@@ -31,8 +31,8 @@ cp .env.example .env
Required:
```env
-AZURE_AI_PROJECT_ENDPOINT=https://.services.ai.azure.com/api/projects/
-AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o
+FOUNDRY_PROJECT_ENDPOINT=https://.services.ai.azure.com/api/projects/
+FOUNDRY_MODEL=gpt-4o
AZURE_AI_EMBEDDING_DEPLOYMENT_NAME=text-embedding-ada-002
AZURE_AI_MEMORY_STORE_ID=hosted-memory-sample
AGENT_NAME=hosted-memory-agent
@@ -139,6 +139,34 @@ The script publishes the project, builds the image, runs the container with two
`HOSTED_USER_ISOLATION_KEY` values, drives a multi-turn conversation per user, asserts that each
user only sees their own memories, and exits non-zero on failure.
+## Deploying to Foundry (azd spec)
+
+This sample includes an `azd` manifest (`agent.manifest.yaml`) and hosted agent spec (`agent.yaml`) for deployment to Foundry.
+
+Initialize an `azd` project from this sample's manifest:
+
+```bash
+mkdir hosted-memory-agent && cd hosted-memory-agent
+azd ai agent init -m https://github.com/microsoft/agent-framework/blob/main/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-MemoryAgent/agent.manifest.yaml
+```
+
+Then deploy:
+
+```bash
+azd deploy
+```
+
+If you need to override defaults, set deployment-time environment variables in the `azd` environment before deploying:
+
+```bash
+azd env set AGENT_NAME hosted-memory-agent
+azd env set AZURE_AI_MODEL_DEPLOYMENT_NAME gpt-4o
+```
+
+For end-to-end hosted agent deployment guidance, see the [official deployment guide](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/deploy-hosted-agent).
+
+---
+
## NuGet package users
If you are consuming the Agent Framework as a NuGet package (not building from source), use the
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-MemoryAgent/scripts/smoke.ps1 b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-MemoryAgent/scripts/smoke.ps1
index 4f85fb3873..fd35751839 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-MemoryAgent/scripts/smoke.ps1
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-MemoryAgent/scripts/smoke.ps1
@@ -11,7 +11,7 @@
Prerequisites:
- Docker
- az login (token is fetched from the host)
- - .env populated with AZURE_AI_PROJECT_ENDPOINT and model deployments
+ - .env populated with FOUNDRY_PROJECT_ENDPOINT and model deployments
.NOTES
This script is for local Docker debugging only. The Foundry platform supplies the isolation
keys for every inbound request in production and the dev fallback used here must not be
@@ -29,7 +29,7 @@ $ErrorActionPreference = 'Stop'
Set-Location -Path $PSScriptRoot/..
if (-not (Test-Path .env)) {
- throw '.env not found. Copy .env.example to .env and fill in AZURE_AI_PROJECT_ENDPOINT.'
+ throw '.env not found. Copy .env.example to .env and fill in FOUNDRY_PROJECT_ENDPOINT.'
}
Write-Host '==> Publishing sample for linux-musl-x64 ...'
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Observability/.env.example b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Observability/.env.example
index 4a6101948c..46900211af 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Observability/.env.example
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Observability/.env.example
@@ -1,7 +1,7 @@
-AZURE_AI_PROJECT_ENDPOINT=
+FOUNDRY_PROJECT_ENDPOINT=
ASPNETCORE_URLS=http://+:8088
ASPNETCORE_ENVIRONMENT=Development
-AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o
+FOUNDRY_MODEL=gpt-4o
AZURE_BEARER_TOKEN=DefaultAzureCredential
# Capture prompt / completion / tool argument content on GenAI spans.
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Observability/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Observability/Program.cs
index fa57fc03a2..135fe92aac 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Observability/Program.cs
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Observability/Program.cs
@@ -18,10 +18,13 @@ using Microsoft.Extensions.AI;
// Load .env file if present (for local development)
Env.TraversePath().Load();
-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";
+string endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT")
+ ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
+string deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-4o";
+// 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.
// Use a chained credential: try a temporary dev token first (for local Docker debugging),
// then fall back to DefaultAzureCredential (for local dev via dotnet run / managed identity in production).
TokenCredential credential = new ChainedTokenCredential(
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Observability/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Observability/README.md
index 889eacca82..20e6a7f2b1 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Observability/README.md
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Observability/README.md
@@ -1,4 +1,4 @@
-# Hosted-Observability
+# Hosted-Observability
A hosted [Agent Framework](https://github.com/microsoft/agent-framework) agent that demonstrates how the Foundry hosting pipeline emits OpenTelemetry traces, metrics and logs with no extra wiring.
@@ -39,10 +39,10 @@ cp .env.example .env
Edit `.env` and set your Azure AI Foundry project endpoint:
```env
-AZURE_AI_PROJECT_ENDPOINT=https://.services.ai.azure.com/api/projects/
+FOUNDRY_PROJECT_ENDPOINT=https://.services.ai.azure.com/api/projects/
ASPNETCORE_URLS=http://+:8088
ASPNETCORE_ENVIRONMENT=Development
-AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o
+FOUNDRY_MODEL=gpt-4o
OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true
```
@@ -104,6 +104,34 @@ docker run --rm -p 8088:8088 \
Once deployed, telemetry flows to the Application Insights instance attached to your Foundry project. In the Foundry UI, the **Traces** tab next to **Playground** lists conversations and lets you drill into the span tree for any request.
+## Deploying to Foundry (azd spec)
+
+This sample includes an `azd` manifest (`agent.manifest.yaml`) and hosted agent spec (`agent.yaml`) for deployment to Foundry.
+
+Initialize an `azd` project from this sample's manifest:
+
+```bash
+mkdir hosted-observability && cd hosted-observability
+azd ai agent init -m https://github.com/microsoft/agent-framework/blob/main/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Observability/agent.manifest.yaml
+```
+
+Then deploy:
+
+```bash
+azd deploy
+```
+
+If you need to override defaults, set deployment-time environment variables in the `azd` environment before deploying:
+
+```bash
+azd env set AGENT_NAME hosted-observability
+azd env set AZURE_AI_MODEL_DEPLOYMENT_NAME gpt-4o
+```
+
+For end-to-end hosted agent deployment guidance, see the [official deployment guide](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/deploy-hosted-agent).
+
+---
+
## NuGet package users
If consuming the Agent Framework as a NuGet package, use the standard `Dockerfile` instead of `Dockerfile.contributor`. See the commented section in `HostedObservability.csproj` for the `PackageReference` alternative.
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/.env.example b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/.env.example
index b8fe9e8e7a..04335e65b8 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/.env.example
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/.env.example
@@ -1,5 +1,5 @@
-AZURE_AI_PROJECT_ENDPOINT=
+FOUNDRY_PROJECT_ENDPOINT=
ASPNETCORE_URLS=http://+:8088
ASPNETCORE_ENVIRONMENT=Development
-AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o
+FOUNDRY_MODEL=gpt-4o
AZURE_BEARER_TOKEN=DefaultAzureCredential
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/Program.cs
index a374f81fd7..0194971576 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/Program.cs
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/Program.cs
@@ -17,10 +17,13 @@ using OpenAI.Chat;
// Load .env file if present (for local development)
Env.TraversePath().Load();
-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";
+string endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT")
+ ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
+string deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-4o";
+// 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.
// Use a chained credential: try a temporary dev token first (for local Docker debugging),
// then fall back to DefaultAzureCredential (for local dev via dotnet run / managed identity in production).
TokenCredential credential = new ChainedTokenCredential(
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/README.md
index 5e4e5140c0..2e7511fb6b 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/README.md
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/README.md
@@ -1,4 +1,4 @@
-# Hosted-TextRag
+# Hosted-TextRag
A hosted agent with **Retrieval Augmented Generation (RAG)** capabilities using `TextSearchProvider`. The agent grounds its answers in product documentation by running a search before each model invocation, then citing the source in its response.
@@ -21,10 +21,10 @@ cp .env.example .env
Edit `.env` and set your Azure AI Foundry project endpoint:
```env
-AZURE_AI_PROJECT_ENDPOINT=https://.services.ai.azure.com/api/projects/
+FOUNDRY_PROJECT_ENDPOINT=https://.services.ai.azure.com/api/projects/
ASPNETCORE_URLS=http://+:8088
ASPNETCORE_ENVIRONMENT=Development
-AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o
+FOUNDRY_MODEL=gpt-4o
AZURE_BEARER_TOKEN=
```
@@ -111,6 +111,34 @@ The `TextSearchProvider` runs a mock search **before each model invocation**:
The model receives the search results as additional context and cites the source in its response. In production, replace `MockSearchAsync` with a call to Azure AI Search or your preferred search provider.
+## Deploying to Foundry (azd spec)
+
+This sample includes an `azd` manifest (`agent.manifest.yaml`) and hosted agent spec (`agent.yaml`) for deployment to Foundry.
+
+Initialize an `azd` project from this sample's manifest:
+
+```bash
+mkdir hosted-text-rag && cd hosted-text-rag
+azd ai agent init -m https://github.com/microsoft/agent-framework/blob/main/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/agent.manifest.yaml
+```
+
+Then deploy:
+
+```bash
+azd deploy
+```
+
+If you need to override defaults, set deployment-time environment variables in the `azd` environment before deploying:
+
+```bash
+azd env set AGENT_NAME hosted-text-rag
+azd env set AZURE_AI_MODEL_DEPLOYMENT_NAME gpt-4o
+```
+
+For end-to-end hosted agent deployment guidance, see the [official deployment guide](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/deploy-hosted-agent).
+
+---
+
## NuGet package users
If you are consuming the Agent Framework as a NuGet package (not building from source), use the standard `Dockerfile` instead of `Dockerfile.contributor`. See the commented section in `HostedTextRag.csproj` for the `PackageReference` alternative.
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox-AuthPaths/.env.example b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox-AuthPaths/.env.example
new file mode 100644
index 0000000000..e4302a5a9a
--- /dev/null
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox-AuthPaths/.env.example
@@ -0,0 +1,16 @@
+# Azure AI Foundry project endpoint (auto-injected in hosted containers).
+AZURE_AI_PROJECT_ENDPOINT=https://.services.ai.azure.com/api/projects/
+
+# Model deployment name. Must exist in the Foundry project above.
+AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o
+
+# Name of the Foundry Toolbox you provisioned in the portal (see README.md).
+TOOLBOX_NAME=auth-paths-toolbox
+
+# Agent name advertised over the wire. Must be unique if running side-by-side with
+# other Hosted-* samples (e.g. Hosted-Toolbox), otherwise the REPL client cannot
+# disambiguate which agent to chat with.
+AGENT_NAME=hosted-toolbox-auth-paths-agent
+
+# Application Insights connection string (auto-injected in hosted containers; optional locally).
+# APPLICATIONINSIGHTS_CONNECTION_STRING=InstrumentationKey=...
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox-AuthPaths/Dockerfile b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox-AuthPaths/Dockerfile
new file mode 100644
index 0000000000..b803098b6a
--- /dev/null
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox-AuthPaths/Dockerfile
@@ -0,0 +1,17 @@
+# Use the official .NET 10.0 ASP.NET runtime as a parent image
+FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
+WORKDIR /app
+
+FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
+WORKDIR /src
+COPY . .
+RUN dotnet restore
+RUN dotnet publish -c Release -o /app/publish
+
+# Final stage
+FROM base AS final
+WORKDIR /app
+COPY --from=build /app/publish .
+EXPOSE 8088
+ENV ASPNETCORE_URLS=http://+:8088
+ENTRYPOINT ["dotnet", "HostedToolboxAuthPaths.dll"]
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox-AuthPaths/Dockerfile.contributor b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox-AuthPaths/Dockerfile.contributor
new file mode 100644
index 0000000000..bbeb4a098b
--- /dev/null
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox-AuthPaths/Dockerfile.contributor
@@ -0,0 +1,21 @@
+# Dockerfile for contributors building from the agent-framework repository source.
+#
+# This project uses ProjectReference to the local source, which means a standard
+# multi-stage Docker build cannot resolve dependencies outside this folder.
+# Pre-publish the app targeting the container runtime and copy the output:
+#
+# dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out
+# docker build -f Dockerfile.contributor -t hosted-toolbox-auth-paths .
+# docker run --rm -p 8088:8088 \
+# -e AGENT_NAME=hosted-toolbox-auth-paths-agent \
+# -e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN \
+# --env-file .env hosted-toolbox-auth-paths
+#
+# For end-users consuming the NuGet package (not ProjectReference), use the standard
+# Dockerfile which performs a full dotnet restore + publish inside the container.
+FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS final
+WORKDIR /app
+COPY out/ .
+EXPOSE 8088
+ENV ASPNETCORE_URLS=http://+:8088
+ENTRYPOINT ["dotnet", "HostedToolboxAuthPaths.dll"]
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox-AuthPaths/Hosted-Toolbox-AuthPaths.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox-AuthPaths/Hosted-Toolbox-AuthPaths.csproj
new file mode 100644
index 0000000000..da484bb9e1
--- /dev/null
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox-AuthPaths/Hosted-Toolbox-AuthPaths.csproj
@@ -0,0 +1,33 @@
+
+
+
+ net10.0
+ enable
+ enable
+ false
+ HostedToolboxAuthPaths
+ HostedToolboxAuthPaths
+ $(NoWarn);OPENAI001
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox-AuthPaths/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox-AuthPaths/Program.cs
new file mode 100644
index 0000000000..7bf9baeaf5
--- /dev/null
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox-AuthPaths/Program.cs
@@ -0,0 +1,145 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+// Foundry Toolbox Auth Paths Agent — A hosted agent backed by a single Foundry Toolbox
+// that bundles MCP tools using THREE different authentication paths.
+//
+// This sample demonstrates the same hosting bones as Hosted-Toolbox/, but the toolbox
+// (provisioned by the user out-of-band) contains three MCP tool entries each authenticated
+// differently. The agent code itself is agnostic to authentication — the educational
+// surface lives in the toolbox configuration in the Foundry portal and in this sample's
+// README.md.
+//
+// Required environment variables:
+// AZURE_AI_PROJECT_ENDPOINT (local-dev) OR FOUNDRY_PROJECT_ENDPOINT (hosted runtime)
+// - Azure AI Foundry project endpoint. The Foundry hosted
+// runtime auto-injects FOUNDRY_PROJECT_ENDPOINT; locally
+// set AZURE_AI_PROJECT_ENDPOINT (the AF-repo convention).
+// TOOLBOX_NAME - Name of the Foundry Toolbox to load
+// (default: auth-paths-toolbox)
+//
+// Optional:
+// AZURE_AI_MODEL_DEPLOYMENT_NAME - Model deployment name (default: gpt-4o)
+// AGENT_NAME - Defaults to "hosted-toolbox-auth-paths-agent".
+//
+// The Foundry.Hosting package builds the toolbox proxy URL from FOUNDRY_PROJECT_ENDPOINT
+// per tools-integration-spec.md §2–§3, so the sample does not need to plumb any
+// toolbox-specific URL env var.
+//
+// NOTE: All FOUNDRY_* and AGENT_* env-var prefixes (other than the platform-injected ones
+// listed above) are reserved by the Foundry container platform and rejected by the
+// agent-create API. Use TOOLBOX_NAME, not FOUNDRY_TOOLBOX_NAME, for sample-owned config.
+
+#pragma warning disable OPENAI001 // FoundryAITool.CreateHostedMcpToolbox is experimental
+
+using Azure.AI.Projects;
+using Azure.Core;
+using Azure.Identity;
+using DotNetEnv;
+using Hosted_Shared_Contributor_Setup;
+using Microsoft.Agents.AI;
+using Microsoft.Agents.AI.Foundry.Hosting;
+
+// Load .env file if present (for local development)
+Env.TraversePath().Load();
+
+// Project endpoint resolution order:
+// 1. FOUNDRY_PROJECT_ENDPOINT — auto-injected by the Foundry hosted runtime.
+// 2. AZURE_AI_PROJECT_ENDPOINT — the convention developers set locally for `dotnet run`.
+// When deployed, only (1) is available; the AF-repo sample convention to set (2) at
+// deploy time fails silently because the platform reserves all FOUNDRY_* env-var names
+// and rejects them at agent-create time. Read both, prefer the platform-injected one.
+string endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT")
+ ?? Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT")
+ ?? throw new InvalidOperationException(
+ "Neither FOUNDRY_PROJECT_ENDPOINT (platform-injected in hosted runtime) " +
+ "nor AZURE_AI_PROJECT_ENDPOINT (local-dev convention) is set.");
+string deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-4o";
+string toolboxName = Environment.GetEnvironmentVariable("TOOLBOX_NAME") ?? "auth-paths-toolbox";
+string agentName = Environment.GetEnvironmentVariable("AGENT_NAME") ?? "hosted-toolbox-auth-paths-agent";
+
+TokenCredential credential = new ChainedTokenCredential(
+ new DevTemporaryTokenCredential(),
+ new DefaultAzureCredential());
+
+// Notes on toolbox wiring — there are two ways to attach a Foundry Toolbox to an agent:
+// - Server-side "baked-in" (what this sample uses): calling AddFoundryToolboxes(name)
+// below registers the toolbox with the Foundry.Hosting layer, which resolves that
+// toolbox's MCP tools once at startup and automatically makes them available to the
+// agent on every request. The agent code does nothing per request.
+// - Per-request / caller-driven (NOT used here): a client can attach a toolbox for a
+// single call by placing a FoundryAITool.CreateHostedMcpToolbox(name) marker in the
+// request body's tool list.
+// Because this sample bakes the toolbox in on the server, it uses AddFoundryToolboxes and
+// does NOT put the CreateHostedMcpToolbox marker in the agent's `tools:` array.
+AIAgent agent = new AIProjectClient(new Uri(endpoint), credential)
+ .AsAIAgent(
+ model: deploymentName,
+ instructions: """
+ You are a helpful assistant with access to several tools, each provided by a different
+ upstream service authenticated through a distinct mechanism (API key, agent managed
+ identity, and a literal token
+ shipped with the tool definition). Pick the tool that best fits the user's question
+ and explain which upstream service answered when you respond.
+ """,
+ name: agentName,
+ description: "Hosted agent demonstrating three MCP-tool authentication paths via a Foundry Toolbox.");
+
+// Tier 3 spine (WebApplication.CreateBuilder + AddFoundryResponses + MapFoundryResponses):
+// the Foundry.Hosting package auto-maps the spec-required GET /readiness probe inside
+// MapFoundryResponses (idempotent — skipped when AgentHost or the developer already
+// mapped it), so the sample stays free of platform plumbing.
+var builder = WebApplication.CreateBuilder(args);
+
+builder.Services.AddFoundryResponses(agent);
+// Pre-register the toolbox name so FoundryToolboxService resolves the foundry-toolbox://
+// marker at request time. With FOUNDRY_PROJECT_ENDPOINT injected by the platform, startup
+// MCP tools/list against the toolbox proxy is typically <100ms in-region.
+builder.Services.AddFoundryToolboxes(toolboxName);
+
+var app = builder.Build();
+app.MapFoundryResponses();
+
+// Contributor-only: in Development, also map the per-agent OpenAI route shape that live Foundry
+// uses so a local REPL client can target this server via AIProjectClient.AsAIAgent(Uri agentEndpoint).
+// Do not use this in production. Hosted Foundry agents only support the agent-endpoint path.
+app.MapDevTemporaryLocalAgentEndpoint();
+
+app.Run();
+
+// ── DevTemporaryTokenCredential ───────────────────────────────────────────────
+
+///
+/// A for local Docker debugging only.
+/// Reads a pre-fetched bearer token from the AZURE_BEARER_TOKEN environment variable
+/// once at startup. This should NOT be used in production.
+///
+/// Generate a token on your host and pass it to the container:
+/// export AZURE_BEARER_TOKEN=$(az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv)
+/// docker run -e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN ...
+///
+internal sealed class DevTemporaryTokenCredential : TokenCredential
+{
+ private const string EnvironmentVariable = "AZURE_BEARER_TOKEN";
+ private readonly string? _token;
+
+ public DevTemporaryTokenCredential()
+ {
+ this._token = Environment.GetEnvironmentVariable(EnvironmentVariable);
+ }
+
+ public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken)
+ => this.GetAccessToken();
+
+ public override ValueTask GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken)
+ => new(this.GetAccessToken());
+
+ private AccessToken GetAccessToken()
+ {
+ if (string.IsNullOrEmpty(this._token) || this._token == "DefaultAzureCredential")
+ {
+ throw new CredentialUnavailableException($"{EnvironmentVariable} environment variable is not set.");
+ }
+
+ return new AccessToken(this._token, DateTimeOffset.MaxValue);
+ }
+}
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox-AuthPaths/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox-AuthPaths/README.md
new file mode 100644
index 0000000000..e1cf78fc24
--- /dev/null
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox-AuthPaths/README.md
@@ -0,0 +1,197 @@
+# Hosted Toolbox — Authentication Paths
+
+A hosted Foundry agent backed by a single Foundry Toolbox that bundles MCP tools using **three different authentication paths**. The educational surface lives in the toolbox configuration (which you provision in the Foundry portal) and in this README — the agent code itself is identical to the existing [`Hosted-Toolbox/`](../Hosted-Toolbox/) sample.
+
+Drive the agent interactively across the auth paths with the shared [`Using-Samples/SimpleAgent/`](../Using-Samples/SimpleAgent/) REPL client, pointed at this agent.
+
+## What this sample teaches
+
+| Aspect | This sample | Existing siblings |
+|---|---|---|
+| Toolbox marker pattern | `FoundryAITool.CreateHostedMcpToolbox(name)` + `AddFoundryToolboxes(name)` | Same as [`Hosted-Toolbox/`](../Hosted-Toolbox/) |
+| Tools per toolbox | **Three MCP tools, each with a different auth method** | `Hosted-Toolbox/`: typically one demo tool |
+| Consumption | Server-side (Foundry resolves the marker) | Same |
+| Client | Shared [`Using-Samples/SimpleAgent/`](../Using-Samples/SimpleAgent/) REPL, pointed at this agent | `Hosted-Toolbox/`: any client |
+
+Related samples:
+- [`Hosted-Toolbox/`](../Hosted-Toolbox/) — simpler single-tool toolbox.
+- [`Hosted-McpTools/`](../Hosted-McpTools/) — contrasts client-side `McpClient` vs server-side `HostedMcpServerTool` for non-toolbox MCP servers.
+
+## Authentication-path matrix
+
+The sample's purpose is to enumerate every authentication path a Foundry toolbox can drive, so each path appears alongside the others. Pick the ones your scenario needs — each connection in a toolbox is independent.
+
+| # | Auth method | MCP target | Connection `authType` | What flows where | When to pick this |
+|---|---|---|---|---|---|
+| 1 | **Key-based via project connection** | GitHub MCP at `https://api.githubcopilot.com/mcp` | `CustomKeys` | A PAT stored as `Authorization: Bearer ` lives in the Foundry connection. The toolbox proxy reads it server-side and injects on every MCP call. | The upstream service only accepts API keys or PATs. |
+| 2 | **Microsoft Entra — agent identity** | Any Azure Cognitive Services MCP endpoint your project can reach (e.g., Language service MCP) | `AgenticIdentityToken` | Foundry mints an Entra token for the agent's own identity (`instance_identity` in the new agent object model), scoped to the connection's `audience`, and forwards it to the MCP server. The agent identity must hold the required role (typically `Cognitive Services User`) on the target resource. | Per-agent least-privilege access to Entra-protected services. Recommended default for new agents. |
+| 3 | **Inline `Authorization` (anti-pattern)** | `https://gitmcp.io/Azure/azure-rest-api-specs` | none | A literal bearer string lives on the toolbox tool entry's `authorization` field. **Do not do this in production** — there's no rotation, no secret store, no per-user identity. Shown for completeness. | Local-dev or public MCP servers that accept any (or no) bearer. |
+
+## Prerequisites
+
+### 0. (Path #2 only) Identify an Entra-authenticated MCP target
+
+Path #2 requires an MCP server that accepts Microsoft Entra tokens. Any **Azure Cognitive Services** resource that exposes an MCP endpoint works — they all accept Entra ID tokens and gate access via standard RBAC.
+
+The reference walkthrough below uses an **Azure Language service** MCP endpoint:
+
+```
+https://.cognitiveservices.azure.com/language/mcp?api-version=2025-11-15-preview
+```
+
+Substitute any other Cognitive Services MCP endpoint you have. If your project has none, omit tool #2 from your toolbox — the remaining two paths still work.
+
+#### RBAC for path #2
+
+Grant the **`Cognitive Services User`** role on the target resource to the agent's instance identity. Find it on the agent ARM resource (Azure portal → your agent → JSON view) at `instance_identity.principal_id`. This is the principal the Foundry proxy uses when minting tokens for `AgenticIdentityToken` connections.
+
+```powershell
+$lang = "/subscriptions//resourceGroups//providers/Microsoft.CognitiveServices/accounts/"
+
+az role assignment create `
+ --assignee-object-id `
+ --assignee-principal-type ServicePrincipal `
+ --role "Cognitive Services User" `
+ --scope $lang
+```
+
+Repeat for any additional Cognitive Services resources the agent identity needs to call.
+
+> The RBAC grant requires `Microsoft.Authorization/roleAssignments/write` on the target scope. In many enterprise subscriptions this needs a PIM JIT activation.
+
+### 1. Foundry project + Azure AI User role
+
+- An active Microsoft Foundry project ([create one](https://learn.microsoft.com/en-us/azure/foundry/how-to/create-projects)).
+- The **Azure AI User** role on the project assigned to:
+ - The developer (you) creating the toolbox.
+ - The agent identity for tool invocation.
+
+### 2. Create the project connections
+
+The Entra-based connection (path #2) is not available in the Foundry portal connection wizard today. Create it via ARM REST:
+
+```powershell
+$armToken = az account get-access-token --query accessToken -o tsv
+$h = @{ Authorization = "Bearer $armToken"; "Content-Type" = "application/json" }
+$proj = "/subscriptions//resourceGroups//providers/Microsoft.CognitiveServices/accounts//projects/"
+$lang = "https://.cognitiveservices.azure.com/language/mcp?api-version=2025-11-15-preview"
+
+# Path 2 — agent identity
+$body2 = @{ properties = @{
+ category = "RemoteTool"; target = $lang
+ authType = "AgenticIdentityToken"; audience = "https://cognitiveservices.azure.com"
+ isSharedToAll = $false
+}} | ConvertTo-Json -Depth 5
+az rest --method PUT --headers "Content-Type=application/json" `
+ --url "https://management.azure.com$proj/connections/lang-mcp-agent-id?api-version=2025-04-01-preview" `
+ --body $body2
+```
+
+Connection summary:
+
+| Connection name (used by the toolbox) | `category` | `authType` | `audience` |
+|---|---|---|---|
+| `github-mcp-key` | `CustomKeys` | `CustomKeys` | n/a (key value carries `Authorization: Bearer `) |
+| `lang-mcp-agent-id` | `RemoteTool` | `AgenticIdentityToken` | `https://cognitiveservices.azure.com` |
+
+Path #3 (`gitmcp.io`) needs no connection — the auth lives on the toolbox tool entry itself.
+
+The `audience` value is the token resource identifier of the target service — for any Cognitive Services resource it is `https://cognitiveservices.azure.com`. For other Azure services consult [Agent identity — runtime token exchange](https://learn.microsoft.com/azure/foundry/agents/concepts/agent-identity#runtime-token-exchange).
+
+### 3. Create the toolbox
+
+In the Foundry portal → Tools → Add Toolbox. Name it `auth-paths-toolbox` (or whatever you prefer; export the name as `TOOLBOX_NAME`). Add three MCP tool entries:
+
+| Tool `server_label` | `server_url` | Auth |
+|---|---|---|
+| `github_pat` | `https://api.githubcopilot.com/mcp` | `project_connection_id: github-mcp-key` |
+| `lang_agent` | Your Language service MCP URL | `project_connection_id: lang-mcp-agent-id` |
+| `gitmcp_inline` | `https://gitmcp.io/Azure/azure-rest-api-specs` | `authorization: "Bearer demo-only-not-real"` (no `project_connection_id`) |
+
+Each entry should also carry:
+
+- `require_approval: never` (this sample is focused on auth, not approval flows; see [`ToolCallingApprovalHostedAgentFixture.cs`](../../../../../tests/Foundry.Hosting.IntegrationTests/Fixtures/ToolCallingApprovalHostedAgentFixture.cs) for that concern).
+- A tight `allowed_tools` list. GitHub MCP exposes ~50 tools; restrict to what you actually want the model to invoke. For example: `github_pat` → `["search_issues", "list_pull_requests"]`. **Every name in `allowed_tools` must match a real tool on the upstream server** — an unknown name (e.g., `get_issue`, which GitHub MCP does not expose) makes the whole source fail enumeration. See the partial-failure note below.
+
+### Sidebar — what the toolbox-creation code looks like
+
+This sample assumes the toolbox already exists; it does not provision one programmatically. For an end-to-end code example of toolbox creation from a publisher script (suitable for a CI/CD pipeline), see [`02-agents/AgentsWithFoundry/Agent_Step25_FoundryToolboxMcp/Program.cs`](../../../../02-agents/AgentsWithFoundry/Agent_Step25_FoundryToolboxMcp/Program.cs) — its `CreateSampleToolboxAsync` helper uses `AgentAdministrationClient.GetAgentToolboxes().CreateToolboxVersionAsync(...)` and is the canonical pattern.
+
+## Run the agent
+
+Set environment variables (or copy `.env.example` to `.env` and fill it in):
+
+```powershell
+$env:AZURE_AI_PROJECT_ENDPOINT = "https://.services.ai.azure.com/api/projects/"
+$env:AZURE_AI_MODEL_DEPLOYMENT_NAME = "gpt-4o"
+$env:TOOLBOX_NAME = "auth-paths-toolbox"
+```
+
+Locally, the `Foundry.Hosting` package reads `AZURE_AI_PROJECT_ENDPOINT` as a fallback when `FOUNDRY_PROJECT_ENDPOINT` is absent. In the hosted Foundry runtime, the platform auto-injects `FOUNDRY_PROJECT_ENDPOINT` and the package builds the toolbox proxy URL as `{FOUNDRY_PROJECT_ENDPOINT}/toolboxes/{TOOLBOX_NAME}/mcp?api-version=v1` per [`tools-integration-spec.md`](https://github.com/microsoft/AgentSchema/blob/main/specs/agents/hosted_agents/container-spec/docs/tools-integration-spec.md) §2–§3.
+
+Then sign in (`az login`) and start the server:
+
+```powershell
+dotnet run --tl:off
+```
+
+The server logs at `http://localhost:8088/`. In Development it also maps the per-agent OpenAI route shape (`MapDevTemporaryLocalAgentEndpoint()`), so the shared `SimpleAgent` REPL client can reach it through `AsAIAgent(agentEndpoint)` — the only supported way to consume a hosted Foundry agent. In a separate terminal:
+
+**Against the local dev server** (point the client at localhost; the `{project}` segment is a wildcard the server ignores):
+
+```powershell
+cd ../Using-Samples/SimpleAgent
+$env:AZURE_AI_PROJECT_ENDPOINT = "http://localhost:8088/api/projects/local"
+$env:AZURE_AI_AGENT_NAME = "hosted-toolbox-auth-paths-agent"
+dotnet run --tl:off
+```
+
+**Against a deployed agent** (point the client at the real project endpoint and the deployed agent name):
+
+```powershell
+cd ../Using-Samples/SimpleAgent
+$env:AZURE_AI_PROJECT_ENDPOINT = "https://.services.ai.azure.com/api/projects/"
+$env:AZURE_AI_AGENT_NAME = "hosted-toolbox-auth-paths-agent"
+dotnet run --tl:off
+```
+
+Either way the client derives the per-agent endpoint URL (`{AZURE_AI_PROJECT_ENDPOINT}/agents/{AZURE_AI_AGENT_NAME}/endpoint/protocols/openai`) and consumes the agent via `AsAIAgent(agentEndpoint)`. Run `az login` first so the client can mint a bearer token.
+
+> **Parallel-run warning**: `Hosted-Toolbox/` and other `Hosted-*` samples default to the same port (8088) and the same agent name slot. Always set a unique `AGENT_NAME` (this sample defaults to `hosted-toolbox-auth-paths-agent`) and stop other hosted samples before starting this one.
+
+## Sample prompts
+
+One per auth path so each tool gets exercised at least once:
+
+```
+List the latest 3 issues in microsoft/agent-framework. # path #1 — GitHub MCP (key)
+Detect the language of "Bonjour le monde". # path #2 — Language MCP (agent identity)
+What's the latest API version for Microsoft.CognitiveServices? # path #3 — gitmcp.io (inline Authorization)
+```
+
+## Troubleshooting / partial-failure semantics
+
+`AddFoundryToolboxes` resolves the toolbox at startup by listing its tools via MCP `tools/list`. This enumeration is **all-or-nothing**: if *any* single tool source fails to enumerate, the Foundry toolbox proxy returns a top-level JSON-RPC error (`-32007`) instead of a partial list, the hosting package marks the toolbox startup as failed, `/readiness` returns 503, and *every* invoke against the agent returns **HTTP 424** — even for the auth paths that are configured correctly. So one misconfigured connection or one bad `allowed_tools` entry bricks the whole agent at startup, not just at tool-call time. Get each source enumerating cleanly before deploying. Symptoms per auth path:
+
+| Symptom | Likely cause |
+|---|---|
+| **All invokes return HTTP 424 ("Failed Dependency")** | One or more tool sources failed `tools/list` at startup (see all-or-nothing note above). Common causes: an `allowed_tools` name that does not exist on the upstream server, or an Entra connection whose token is rejected. Reproduce by calling the toolbox `tools/list` directly with your own token — a `-32007` top-level error names the failing source. |
+| **HTTP 401 "audience is incorrect"** | The connection's `audience` field is missing or does not match the OAuth resource identifier the target service accepts. For Cognitive Services targets, set `audience: "https://cognitiveservices.azure.com"`. |
+| **HTTP 401 / 403 "principal does not have access"** | Path #1: PAT expired or scope insufficient. Path #2: the agent's instance identity is missing the required role on the target resource. |
+| **Container reports zero tools but startup succeeded** | `FoundryToolboxService.StartAsync` caches the `tools/list` result at startup. If a connection or RBAC grant changed after the container started, force a fresh container (re-deploy the agent version) — the cache won't pick up the change until then. |
+| **HTTP 404 from a tool call** | Toolbox name mismatch (`TOOLBOX_NAME` vs the name in the portal), or the toolbox was deleted. |
+| **Server logs a warning "Neither FOUNDRY_PROJECT_ENDPOINT nor AZURE_AI_PROJECT_ENDPOINT is set; toolbox support is disabled"** | Local dev without the env var set. The agent will load with zero tools and respond as if it has none. Set `AZURE_AI_PROJECT_ENDPOINT` (local-dev fallback) or `FOUNDRY_PROJECT_ENDPOINT` to your project endpoint. |
+| **Tools appear but model never invokes them** | `instructions:` in `Program.cs` may not surface what each tool is for. Tighten the `allowed_tools` lists and rephrase prompts to mention the upstream service by name. |
+
+## Region and model compatibility
+
+Foundry Toolboxes have region constraints; some tool types are limited to specific models. This sample defaults to `gpt-4o`, which works in all supported regions. For the full matrix, see the [Foundry tools compatibility matrix](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/tools/toolbox#region-and-model-compatibility).
+
+## Anti-pattern note for path #3
+
+Inline `authorization` on a toolbox tool entry stores credentials **inside the toolbox definition**. There is no rotation, no per-user scoping, no secret-store integration. Use it only for:
+
+- Public MCP servers that ignore the bearer (the `gitmcp.io` case demonstrated here).
+- Local development against a test MCP server with a throwaway token.
+
+For everything else use `project_connection_id` and let the platform inject credentials.
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox-AuthPaths/agent.manifest.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox-AuthPaths/agent.manifest.yaml
new file mode 100644
index 0000000000..77336ea78f
--- /dev/null
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox-AuthPaths/agent.manifest.yaml
@@ -0,0 +1,48 @@
+# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/AgentManifest.yaml
+name: hosted-toolbox-auth-paths
+displayName: "Hosted Toolbox - Authentication Paths"
+
+description: >
+ A hosted agent demonstrating three MCP-tool authentication paths in a single
+ Foundry Toolbox: API key via project connection, Microsoft Entra agent
+ identity, and inline Authorization
+ (anti-pattern). The toolbox itself is
+ provisioned out of band; see this sample's README for the portal walkthrough.
+
+metadata:
+ tags:
+ - AI Agent Hosting
+ - Azure AI AgentServer
+ - Responses Protocol
+ - Agent Framework
+ - Foundry Toolbox
+ - Authentication
+ - MCP
+
+template:
+ name: hosted-toolbox-auth-paths
+ kind: hosted
+ protocols:
+ - protocol: responses
+ version: 1.0.0
+ resources:
+ cpu: "0.25"
+ memory: 0.5Gi
+ environment_variables:
+ - name: AZURE_AI_MODEL_DEPLOYMENT_NAME
+ value: "{{AZURE_AI_MODEL_DEPLOYMENT_NAME}}"
+ - name: TOOLBOX_NAME
+ value: "{{TOOLBOX_NAME}}"
+parameters:
+ properties:
+ - name: TOOLBOX_NAME
+ type: string
+ default: "auth-paths-toolbox"
+ description: "Name of the Foundry Toolbox to load at runtime."
+resources:
+ - kind: model
+ id: gpt-4o
+ name: AZURE_AI_MODEL_DEPLOYMENT_NAME
+ - kind: toolbox
+ name: "{{TOOLBOX_NAME}}"
+ tools: []
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox-AuthPaths/agent.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox-AuthPaths/agent.yaml
new file mode 100644
index 0000000000..87cca57bb7
--- /dev/null
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox-AuthPaths/agent.yaml
@@ -0,0 +1,9 @@
+# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml
+kind: hosted
+name: hosted-toolbox-auth-paths
+protocols:
+ - protocol: responses
+ version: 1.0.0
+resources:
+ cpu: "0.25"
+ memory: 0.5Gi
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox/Program.cs
index 3f6c0a70a7..3adae07004 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox/Program.cs
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox/Program.cs
@@ -1,21 +1,29 @@
// Copyright (c) Microsoft. All rights reserved.
-// Foundry Toolbox Agent - A hosted agent that uses Foundry Toolset MCP tools.
+// Foundry Toolbox Agent - A hosted agent that uses Foundry Toolbox MCP tools.
//
-// Demonstrates how to register one or more Foundry toolsets so the agent can
+// Demonstrates how to register one or more Foundry toolboxes so the agent can
// call tools provided by the Foundry platform's managed MCP proxy.
//
// Required environment variables:
-// AZURE_AI_PROJECT_ENDPOINT - Azure AI Foundry project endpoint
-// AZURE_AI_MODEL_DEPLOYMENT_NAME - Model deployment name (default: gpt-4o)
-// FOUNDRY_AGENT_TOOLSET_ENDPOINT - Foundry Toolsets proxy base URL
-// (injected automatically by Foundry platform at runtime)
+// FOUNDRY_PROJECT_ENDPOINT (hosted runtime) OR AZURE_AI_PROJECT_ENDPOINT (local-dev)
+// - Azure AI Foundry project endpoint. The Foundry hosted
+// runtime auto-injects FOUNDRY_PROJECT_ENDPOINT; locally
+// set AZURE_AI_PROJECT_ENDPOINT.
+// FOUNDRY_MODEL - Model deployment name (default: gpt-4o)
//
// Optional:
-// FOUNDRY_TOOLBOX_NAME - Name of the toolset to load (default: my-toolset)
-// FOUNDRY_AGENT_NAME - Client name reported to MCP server
-// FOUNDRY_AGENT_VERSION - Client version reported to MCP server
-// FOUNDRY_AGENT_TOOLSET_FEATURES - Feature flags sent to Foundry proxy via header
+// FOUNDRY_TOOLBOX_NAME - Name of the toolbox to load (default: my-toolset)
+// FOUNDRY_AGENT_TOOLSET_ENDPOINT - Foundry Toolsets proxy base URL
+// (injected automatically by Foundry platform at runtime)
+// FOUNDRY_AGENT_NAME - Client name reported to MCP server (auto-injected in hosted runtime)
+// FOUNDRY_AGENT_VERSION - Client version reported to MCP server (auto-injected in hosted runtime)
+// FOUNDRY_AGENT_TOOLSET_FEATURES - Additional Foundry-Features header flags (the mandatory
+// Toolboxes=V1Preview flag is always sent; this env var
+// appends additional flags if present).
+//
+// The Foundry.Hosting package builds the toolbox proxy URL from FOUNDRY_PROJECT_ENDPOINT
+// per tools-integration-spec.md §2–§3.
using Azure.AI.Projects;
using Azure.Core;
@@ -28,11 +36,19 @@ using Microsoft.Agents.AI.Foundry.Hosting;
// Load .env file if present (for local development)
Env.TraversePath().Load();
-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";
-string toolboxName = Environment.GetEnvironmentVariable("FOUNDRY_TOOLBOX_NAME") ?? "my-toolset";
+string endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT")
+ ?? Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT")
+ ?? throw new InvalidOperationException(
+ "Neither FOUNDRY_PROJECT_ENDPOINT (platform-injected in hosted runtime) " +
+ "nor AZURE_AI_PROJECT_ENDPOINT (local-dev convention) is set.");
+string deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL")
+ ?? Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-4o";
+string toolboxName = Environment.GetEnvironmentVariable("FOUNDRY_TOOLBOX_NAME")
+ ?? Environment.GetEnvironmentVariable("TOOLBOX_NAME") ?? "my-toolset";
+// 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.
// Use a chained credential: try a temporary dev token first (for local Docker debugging),
// then fall back to DefaultAzureCredential (for local dev via dotnet run / managed identity in production).
TokenCredential credential = new ChainedTokenCredential(
@@ -45,12 +61,12 @@ AIAgent agent = new AIProjectClient(new Uri(endpoint), credential)
.AsAIAgent(
model: deploymentName,
instructions: """
- You are a helpful assistant with access to tools provided by the Foundry Toolset.
+ You are a helpful assistant with access to tools provided by the Foundry Toolbox.
Use the available tools to answer user questions.
If a tool is not available for a request, let the user know clearly.
""",
name: Environment.GetEnvironmentVariable("AGENT_NAME") ?? "hosted-toolbox-agent",
- description: "Hosted agent backed by Foundry Toolset MCP tools");
+ description: "Hosted agent backed by Foundry Toolbox MCP tools");
// ── Build the host ────────────────────────────────────────────────────────────
@@ -61,8 +77,8 @@ builder.Services.AddFoundryResponses(agent);
builder.Services.AddDevTemporaryLocalContributorSetup(); // Local Docker debugging only - must not be used in production.
// Register Foundry Toolbox: connects to the MCP proxy at startup and makes tools available.
-// The toolset name must match a toolset registered in your Foundry project.
-// When FOUNDRY_AGENT_TOOLSET_ENDPOINT is absent (e.g., in local development without Foundry
+// The toolbox name must match a toolbox registered in your Foundry project.
+// When FOUNDRY_PROJECT_ENDPOINT is absent (e.g., in local development without Foundry
// infrastructure), startup succeeds without error and no toolbox tools are loaded.
builder.Services.AddFoundryToolboxes(toolboxName);
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox/README.md
new file mode 100644
index 0000000000..37d4dd7d28
--- /dev/null
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox/README.md
@@ -0,0 +1,27 @@
+# Hosted-Toolbox
+
+A hosted Foundry agent that loads tools from a Foundry Toolbox via the AF Foundry hosting bridge.
+
+The agent declares one `FoundryAITool.CreateHostedMcpToolbox(name)` marker; `AddFoundryToolboxes(name)` registers a `FoundryToolboxService` that resolves the marker into the individual MCP tools the toolbox bundles, connecting to the Foundry Toolboxes MCP proxy at startup and discovering tools via `tools/list`.
+
+## Prerequisites
+
+- A Microsoft Foundry project with a Toolbox configured.
+- Azure CLI logged in (`az login`).
+- Set environment variables:
+ - `AZURE_AI_PROJECT_ENDPOINT` (local-dev) or `FOUNDRY_PROJECT_ENDPOINT` (auto-injected in hosted containers)
+ - `AZURE_AI_MODEL_DEPLOYMENT_NAME` (default `gpt-4o`)
+ - `TOOLBOX_NAME` (default `my-toolbox`)
+
+The `Foundry.Hosting` package builds the toolbox proxy URL from `FOUNDRY_PROJECT_ENDPOINT` as `{FOUNDRY_PROJECT_ENDPOINT}/toolboxes/{TOOLBOX_NAME}/mcp?api-version=v1` per [`tools-integration-spec.md`](https://github.com/microsoft/AgentSchema/blob/main/specs/agents/hosted_agents/container-spec/docs/tools-integration-spec.md) §2–§3.
+
+## Run
+
+```powershell
+dotnet run --tl:off
+```
+
+## Related samples
+
+- [`Hosted-Toolbox-AuthPaths/`](../Hosted-Toolbox-AuthPaths/) — extends this pattern with a three-tool toolbox demonstrating different MCP-tool authentication paths (key, Entra agent identity, inline `Authorization`), driven by the shared `Using-Samples/SimpleAgent/` REPL.
+- [`Hosted-McpTools/`](../Hosted-McpTools/) — contrasts client-side `McpClient` vs server-side `HostedMcpServerTool` for non-toolbox MCP servers.
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/.env.example b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/.env.example
index 5c312b3f8e..0f603ea20c 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/.env.example
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/.env.example
@@ -1,6 +1,6 @@
-AZURE_AI_PROJECT_ENDPOINT=
+FOUNDRY_PROJECT_ENDPOINT=
ASPNETCORE_URLS=http://+:8088
ASPNETCORE_ENVIRONMENT=Development
-AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-5
+FOUNDRY_MODEL=gpt-5
FOUNDRY_TOOLBOX_NAME=
AZURE_BEARER_TOKEN=DefaultAzureCredential
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/Program.cs
index f8ca3f4991..99dbdc6573 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/Program.cs
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/Program.cs
@@ -7,9 +7,9 @@
// AgentSkillsProviderBuilder.UseMcpSkills().
//
// Required environment variables:
-// AZURE_AI_PROJECT_ENDPOINT - Azure AI Foundry project endpoint
+// FOUNDRY_PROJECT_ENDPOINT - Azure AI Foundry project endpoint
// FOUNDRY_TOOLBOX_NAME - Name of the Foundry Toolbox to connect to
-// AZURE_AI_MODEL_DEPLOYMENT_NAME - Model deployment name (default: gpt-5)
+// FOUNDRY_MODEL - Model deployment name (default: gpt-5)
using System.Net.Http.Headers;
using Azure.AI.Projects;
@@ -24,15 +24,18 @@ using ModelContextProtocol.Client;
// Load .env file if present (for local development)
Env.TraversePath().Load();
-var projectEndpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT")
- ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.");
-var deployment = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-5";
+var projectEndpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT")
+ ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
+var deployment = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5";
var toolboxName = Environment.GetEnvironmentVariable("FOUNDRY_TOOLBOX_NAME")
?? throw new InvalidOperationException("FOUNDRY_TOOLBOX_NAME is not set.");
// Build the Toolbox MCP URL from the project endpoint and toolbox name.
var toolboxMcpServerUrl = $"{projectEndpoint.TrimEnd('/')}/toolboxes/{toolboxName}/mcp?api-version=v1";
+// 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.
// Use a chained credential: try a temporary dev token first (for local Docker debugging),
// then fall back to DefaultAzureCredential (for local dev via dotnet run / managed identity in production).
TokenCredential credential = new ChainedTokenCredential(
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/README.md
index 0c6b5ba60e..7b09109520 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/README.md
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/README.md
@@ -1,4 +1,4 @@
-# Hosted-ToolboxMcpSkills
+# Hosted-ToolboxMcpSkills
A hosted agent that discovers **MCP-based skills from a Foundry Toolbox** and makes them available to the agent using `AgentSkillsProviderBuilder.UseMcpSkills(mcpClient)`.
@@ -28,10 +28,10 @@ cp .env.example .env
Edit `.env` and set your Azure AI Foundry project endpoint and toolbox name:
```env
-AZURE_AI_PROJECT_ENDPOINT=https://.services.ai.azure.com/api/projects/
+FOUNDRY_PROJECT_ENDPOINT=https://.services.ai.azure.com/api/projects/
ASPNETCORE_URLS=http://+:8088
ASPNETCORE_ENVIRONMENT=Development
-AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-5
+FOUNDRY_MODEL=gpt-5
FOUNDRY_TOOLBOX_NAME=my-toolbox
```
@@ -98,6 +98,34 @@ Using the Azure Developer CLI:
azd ai agent invoke --local "What skills do you have available?"
```
+## Deploying to Foundry (azd spec)
+
+This sample includes an `azd` manifest (`agent.manifest.yaml`) and hosted agent spec (`agent.yaml`) for deployment to Foundry.
+
+Initialize an `azd` project from this sample's manifest:
+
+```bash
+mkdir hosted-toolbox-mcp-skills && cd hosted-toolbox-mcp-skills
+azd ai agent init -m https://github.com/microsoft/agent-framework/blob/main/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/agent.manifest.yaml
+```
+
+Then deploy:
+
+```bash
+azd deploy
+```
+
+If you need to override defaults, set deployment-time environment variables in the `azd` environment before deploying:
+
+```bash
+azd env set AGENT_NAME hosted-toolbox-mcp-skills
+azd env set AZURE_AI_MODEL_DEPLOYMENT_NAME gpt-5
+```
+
+For end-to-end hosted agent deployment guidance, see the [official deployment guide](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/deploy-hosted-agent).
+
+---
+
## NuGet package users
If you are consuming the Agent Framework as a NuGet package (not building from source), use the standard `Dockerfile` instead of `Dockerfile.contributor`. See the commented section in `HostedToolboxMcpSkills.csproj` for the `PackageReference` alternative.
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/agent.manifest.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/agent.manifest.yaml
index 2887336252..ad85fb1bca 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/agent.manifest.yaml
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/agent.manifest.yaml
@@ -28,8 +28,8 @@ template:
cpu: "0.25"
memory: 0.5Gi
environment_variables:
- - name: AZURE_AI_MODEL_DEPLOYMENT_NAME
- value: "{{AZURE_AI_MODEL_DEPLOYMENT_NAME}}"
+ - name: FOUNDRY_MODEL
+ value: "{{FOUNDRY_MODEL}}"
- name: FOUNDRY_TOOLBOX_NAME
value: "{{FOUNDRY_TOOLBOX_NAME}}"
parameters:
@@ -40,4 +40,4 @@ parameters:
resources:
- kind: model
id: gpt-5
- name: AZURE_AI_MODEL_DEPLOYMENT_NAME
+ name: FOUNDRY_MODEL
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/agent.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/agent.yaml
index 5f53abb2e2..140a5fc4c6 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/agent.yaml
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/agent.yaml
@@ -8,7 +8,7 @@ resources:
cpu: "0.25"
memory: 0.5Gi
environment_variables:
- - name: AZURE_AI_MODEL_DEPLOYMENT_NAME
- value: ${AZURE_AI_MODEL_DEPLOYMENT_NAME}
+ - name: FOUNDRY_MODEL
+ value: ${FOUNDRY_MODEL}
- name: FOUNDRY_TOOLBOX_NAME
value: ${FOUNDRY_TOOLBOX_NAME}
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/Program.cs
index 30d9d43616..e89ad78210 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/Program.cs
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/Program.cs
@@ -38,6 +38,9 @@ var builder = WebApplication.CreateBuilder(args);
var endpoint = new Uri(Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."));
var deployment = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT") ?? "gpt-4o";
+// 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.
var azureClient = new AzureOpenAIClient(endpoint, new ChainedTokenCredential(
new DevTemporaryTokenCredential(),
new DefaultAzureCredential()));
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/README.md
index 643af74551..341564ad73 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/README.md
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/README.md
@@ -121,6 +121,34 @@ User message
The triage agent receives every message and hands off to the appropriate specialist. Specialists route back to the triage agent after responding, allowing for multi-turn conversations.
+## Deploying to Foundry (azd spec)
+
+This sample includes an `azd` manifest (`agent.manifest.yaml`) and hosted agent spec (`agent.yaml`) for deployment to Foundry.
+
+Initialize an `azd` project from this sample's manifest:
+
+```bash
+mkdir triage-workflow && cd triage-workflow
+azd ai agent init -m https://github.com/microsoft/agent-framework/blob/main/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/agent.manifest.yaml
+```
+
+Then deploy:
+
+```bash
+azd deploy
+```
+
+If you need to override defaults, set deployment-time environment variables in the `azd` environment before deploying:
+
+```bash
+azd env set AGENT_NAME triage-workflow
+azd env set AZURE_AI_MODEL_DEPLOYMENT_NAME gpt-4o
+```
+
+For end-to-end hosted agent deployment guidance, see the [official deployment guide](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/deploy-hosted-agent).
+
+---
+
## NuGet package users
Use the standard `Dockerfile` instead of `Dockerfile.contributor`. See the commented section in `HostedWorkflowHandoff.csproj` for the `PackageReference` alternative.
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/.env.example b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/.env.example
index b8fe9e8e7a..04335e65b8 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/.env.example
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/.env.example
@@ -1,5 +1,5 @@
-AZURE_AI_PROJECT_ENDPOINT=
+FOUNDRY_PROJECT_ENDPOINT=
ASPNETCORE_URLS=http://+:8088
ASPNETCORE_ENVIRONMENT=Development
-AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o
+FOUNDRY_MODEL=gpt-4o
AZURE_BEARER_TOKEN=DefaultAzureCredential
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/Program.cs
index d0fb8ee129..b4d54bb977 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/Program.cs
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/Program.cs
@@ -18,10 +18,13 @@ using Microsoft.Extensions.AI;
// Load .env file if present (for local development)
Env.TraversePath().Load();
-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";
+string endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT")
+ ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
+string deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-4o";
+// 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.
// Use a chained credential: try a temporary dev token first (for local Docker debugging),
// then fall back to DefaultAzureCredential (for local dev via dotnet run / managed identity in production).
TokenCredential credential = new ChainedTokenCredential(
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/README.md
index d91d27445d..cdbe36911d 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/README.md
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/README.md
@@ -1,11 +1,11 @@
-# Hosted-Workflow-Simple
+# Hosted-Workflow-Simple
A hosted agent that demonstrates **multi-agent workflow orchestration**. Three translation agents are composed into a sequential pipeline: English → French → Spanish → English, showing how agents can be chained as workflow executors using `WorkflowBuilder`.
## Prerequisites
- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0)
-- An Azure AI Foundry project with a deployed model (e.g., `gpt-4o`)
+- An Azure AI Foundry project with a deployed model (e.g., `hosted-workflow-simple`)
- Azure CLI logged in (`az login`)
## Configuration
@@ -19,10 +19,10 @@ cp .env.example .env
Edit `.env` and set your Azure AI Foundry project endpoint:
```env
-AZURE_AI_PROJECT_ENDPOINT=https://.services.ai.azure.com/api/projects/
+FOUNDRY_PROJECT_ENDPOINT=https://.services.ai.azure.com/api/projects/
ASPNETCORE_URLS=http://+:8088
ASPNETCORE_ENVIRONMENT=Development
-AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o
+FOUNDRY_MODEL=hosted-workflow-simple
```
> **Note:** `.env` is gitignored. The `.env.example` template is checked in as a reference.
@@ -104,6 +104,34 @@ Input text
Each agent in the chain receives the output of the previous agent. The final result demonstrates how meaning is preserved (or subtly shifted) through multiple translation hops.
+## Deploying to Foundry (azd spec)
+
+This sample includes an `azd` manifest (`agent.manifest.yaml`) and hosted agent spec (`agent.yaml`) for deployment to Foundry.
+
+Initialize an `azd` project from this sample's manifest:
+
+```bash
+mkdir hosted-workflows && cd hosted-workflows
+azd ai agent init -m https://github.com/microsoft/agent-framework/blob/main/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/agent.manifest.yaml
+```
+
+Then deploy:
+
+```bash
+azd deploy
+```
+
+If you need to override defaults, set deployment-time environment variables in the `azd` environment before deploying:
+
+```bash
+azd env set AGENT_NAME hosted-workflow-simple
+azd env set FOUNDRY_MODEL hosted-workflow-simple
+```
+
+For end-to-end hosted agent deployment guidance, see the [official deployment guide](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/deploy-hosted-agent).
+
+---
+
## NuGet package users
Use the standard `Dockerfile` instead of `Dockerfile.contributor`. See the commented section in `HostedWorkflowSimple.csproj` for the `PackageReference` alternative.
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SessionFilesClient/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SessionFilesClient/Program.cs
index 0c2ba2d038..716a0b37c4 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SessionFilesClient/Program.cs
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SessionFilesClient/Program.cs
@@ -10,10 +10,10 @@ using Microsoft.Agents.AI.Foundry;
// Load .env file if present (for local development)
Env.TraversePath().Load();
-// AZURE_AI_PROJECT_ENDPOINT is the Foundry project endpoint. Shape:
+// FOUNDRY_PROJECT_ENDPOINT is the Foundry project endpoint. Shape:
// https:///api/projects/
-Uri projectEndpoint = new(Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT")
- ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set."));
+Uri projectEndpoint = new(Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT")
+ ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set."));
// AZURE_AI_AGENT_NAME is the registered server-side agent name.
string agentName = Environment.GetEnvironmentVariable("AZURE_AI_AGENT_NAME")
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SessionFilesClient/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SessionFilesClient/README.md
index 356416596a..043cc8fa7d 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SessionFilesClient/README.md
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SessionFilesClient/README.md
@@ -1,4 +1,4 @@
-# SessionFilesClient
+# SessionFilesClient
A thin chat REPL that connects to a deployed [`Hosted-Files`](../../Hosted-Files/) agent via `FoundryAgent` and lets you ask questions whose answers come from the files bundled with that agent. Same shape as [`SimpleAgent`](../SimpleAgent/) — point it at an `AGENT_ENDPOINT`, build a `FoundryAgent`, run.
@@ -13,17 +13,17 @@ The agent's container-side `ListFiles` and `ReadFile` tools surface the bundled
## Configuration
```env
-AZURE_AI_PROJECT_ENDPOINT=https:///api/projects/
+FOUNDRY_PROJECT_ENDPOINT=https:///api/projects/
AZURE_AI_AGENT_NAME=hosted-files
```
-Both are required. `AZURE_AI_PROJECT_ENDPOINT` is the Foundry project endpoint URL and `AZURE_AI_AGENT_NAME` is the registered server-side agent name. The sample builds the per-agent OpenAI endpoint URL from these.
+Both are required. `FOUNDRY_PROJECT_ENDPOINT` is the Foundry project endpoint URL and `AZURE_AI_AGENT_NAME` is the registered server-side agent name. The sample builds the per-agent OpenAI endpoint URL from these.
## Run
```bash
cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SessionFilesClient
-$env:AZURE_AI_PROJECT_ENDPOINT = "http://localhost:8088/api/projects/local"
+$env:FOUNDRY_PROJECT_ENDPOINT = "http://localhost:8088/api/projects/local"
$env:AZURE_AI_AGENT_NAME = "hosted-files"
dotnet run
```
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent/Program.cs
index eedf136abb..b74e85e19d 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent/Program.cs
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent/Program.cs
@@ -10,10 +10,10 @@ using Microsoft.Agents.AI.Foundry;
// Load .env file if present (for local development)
Env.TraversePath().Load();
-// AZURE_AI_PROJECT_ENDPOINT is the Foundry project endpoint. Shape:
+// FOUNDRY_PROJECT_ENDPOINT is the Foundry project endpoint. Shape:
// https:///api/projects/
-Uri projectEndpoint = new(Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT")
- ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set."));
+Uri projectEndpoint = new(Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT")
+ ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set."));
// AZURE_AI_AGENT_NAME is the registered server-side agent name.
string agentName = Environment.GetEnvironmentVariable("AZURE_AI_AGENT_NAME")
diff --git a/dotnet/samples/05-end-to-end/A2AClientServer/A2AServer/Program.cs b/dotnet/samples/05-end-to-end/A2AClientServer/A2AServer/Program.cs
index c694351599..26e2460dd1 100644
--- a/dotnet/samples/05-end-to-end/A2AClientServer/A2AServer/Program.cs
+++ b/dotnet/samples/05-end-to-end/A2AClientServer/A2AServer/Program.cs
@@ -33,7 +33,7 @@ IConfigurationRoot configuration = new ConfigurationBuilder()
string? apiKey = configuration["OPENAI_API_KEY"];
string model = configuration["OPENAI_CHAT_MODEL_NAME"] ?? "gpt-5.4-mini";
-string? endpoint = configuration["AZURE_AI_PROJECT_ENDPOINT"];
+string? endpoint = configuration["FOUNDRY_PROJECT_ENDPOINT"];
string[] agentUrls = (builder.Configuration["urls"] ?? "http://localhost:5000").Split(';');
var invoiceQueryPlugin = new InvoiceQuery();
diff --git a/dotnet/samples/05-end-to-end/A2AClientServer/README.md b/dotnet/samples/05-end-to-end/A2AClientServer/README.md
index 0efb17e748..d0771ff4e1 100644
--- a/dotnet/samples/05-end-to-end/A2AClientServer/README.md
+++ b/dotnet/samples/05-end-to-end/A2AClientServer/README.md
@@ -1,4 +1,4 @@
-# A2A Client and Server samples
+# A2A Client and Server samples
> **Warning**
> The [A2A protocol](https://google.github.io/A2A/) is still under development and changing fast.
@@ -84,7 +84,7 @@ You must create the agents in a Microsoft Foundry project and then provide the p
```
```powershell
-$env:AZURE_AI_PROJECT_ENDPOINT="https://ai-foundry-your-project.services.ai.azure.com/api/projects/ai-proj-ga-your-project" # Replace with your Foundry Project endpoint
+$env:FOUNDRY_PROJECT_ENDPOINT="https://ai-foundry-your-project.services.ai.azure.com/api/projects/ai-proj-ga-your-project" # Replace with your Foundry Project endpoint
```
Use the following commands to run each A2A server
diff --git a/dotnet/samples/05-end-to-end/DevUIAspireIntegration/EditorAgent/Program.cs b/dotnet/samples/05-end-to-end/DevUIAspireIntegration/EditorAgent/Program.cs
index d50213a9f7..ecef13f275 100644
--- a/dotnet/samples/05-end-to-end/DevUIAspireIntegration/EditorAgent/Program.cs
+++ b/dotnet/samples/05-end-to-end/DevUIAspireIntegration/EditorAgent/Program.cs
@@ -13,6 +13,9 @@ builder.AddServiceDefaults();
builder.AddAzureChatCompletionsClient(connectionName: "foundry",
configureSettings: settings =>
{
+ // 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.
settings.TokenCredential = new DefaultAzureCredential();
settings.EnableSensitiveTelemetryData = builder.Environment.IsDevelopment();
})
diff --git a/dotnet/samples/05-end-to-end/DevUIAspireIntegration/WriterAgent/Program.cs b/dotnet/samples/05-end-to-end/DevUIAspireIntegration/WriterAgent/Program.cs
index 72f3215453..f8061d73da 100644
--- a/dotnet/samples/05-end-to-end/DevUIAspireIntegration/WriterAgent/Program.cs
+++ b/dotnet/samples/05-end-to-end/DevUIAspireIntegration/WriterAgent/Program.cs
@@ -10,6 +10,9 @@ builder.AddServiceDefaults();
builder.AddAzureChatCompletionsClient(connectionName: "foundry",
configureSettings: settings =>
{
+ // 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.
settings.TokenCredential = new DefaultAzureCredential();
settings.EnableSensitiveTelemetryData = builder.Environment.IsDevelopment();
})
diff --git a/dotnet/samples/05-end-to-end/Evaluation/Evaluation_ConversationSplits/Program.cs b/dotnet/samples/05-end-to-end/Evaluation/Evaluation_ConversationSplits/Program.cs
index a4cd3c5257..76f930bc42 100644
--- a/dotnet/samples/05-end-to-end/Evaluation/Evaluation_ConversationSplits/Program.cs
+++ b/dotnet/samples/05-end-to-end/Evaluation/Evaluation_ConversationSplits/Program.cs
@@ -9,8 +9,8 @@ using Microsoft.Extensions.AI;
using Microsoft.Extensions.AI.Evaluation;
using FoundryEvals = Microsoft.Agents.AI.Foundry.FoundryEvals;
-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";
+string endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
+string deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "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
diff --git a/dotnet/samples/05-end-to-end/Evaluation/Evaluation_ConversationSplits/README.md b/dotnet/samples/05-end-to-end/Evaluation/Evaluation_ConversationSplits/README.md
index b2c220a9ba..829ff982ca 100644
--- a/dotnet/samples/05-end-to-end/Evaluation/Evaluation_ConversationSplits/README.md
+++ b/dotnet/samples/05-end-to-end/Evaluation/Evaluation_ConversationSplits/README.md
@@ -1,4 +1,4 @@
-# Evaluation - Conversation Splits
+# Evaluation - Conversation Splits
This sample demonstrates multi-turn conversation evaluation with different split strategies.
@@ -14,13 +14,13 @@ This sample demonstrates multi-turn conversation evaluation with different split
## Prerequisites
- .NET 10 SDK or later
-- Azure CLI installed and authenticated (`az login`)
+- Azure authentication available to `DefaultAzureCredential` (for local development, run `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"
+$env:FOUNDRY_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project"
+$env:FOUNDRY_MODEL="gpt-4o-mini"
```
## Run the sample
@@ -28,4 +28,4 @@ $env:AZURE_AI_MODEL_DEPLOYMENT_NAME="gpt-4o-mini"
```powershell
cd dotnet/samples/05-end-to-end/Evaluation
dotnet run --project .\Evaluation_ConversationSplits
-```
\ No newline at end of file
+```
diff --git a/dotnet/samples/05-end-to-end/Evaluation/Evaluation_FoundryQuality/Program.cs b/dotnet/samples/05-end-to-end/Evaluation/Evaluation_FoundryQuality/Program.cs
index 8d1a150f47..2aaec605f1 100644
--- a/dotnet/samples/05-end-to-end/Evaluation/Evaluation_FoundryQuality/Program.cs
+++ b/dotnet/samples/05-end-to-end/Evaluation/Evaluation_FoundryQuality/Program.cs
@@ -9,8 +9,8 @@ using Microsoft.Agents.AI;
using Microsoft.Extensions.AI.Evaluation;
using FoundryEvals = Microsoft.Agents.AI.Foundry.FoundryEvals;
-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";
+string endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
+string deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "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
diff --git a/dotnet/samples/05-end-to-end/Evaluation/Evaluation_FoundryQuality/README.md b/dotnet/samples/05-end-to-end/Evaluation/Evaluation_FoundryQuality/README.md
index 53b67cec0c..0f86359943 100644
--- a/dotnet/samples/05-end-to-end/Evaluation/Evaluation_FoundryQuality/README.md
+++ b/dotnet/samples/05-end-to-end/Evaluation/Evaluation_FoundryQuality/README.md
@@ -1,4 +1,4 @@
-# Evaluation - Foundry Quality
+# Evaluation - Foundry Quality
This sample demonstrates agent evaluation using MEAI quality evaluators (Relevance, Coherence) via `FoundryEvals`.
@@ -13,13 +13,13 @@ This sample demonstrates agent evaluation using MEAI quality evaluators (Relevan
## Prerequisites
- .NET 10 SDK or later
-- Azure CLI installed and authenticated (`az login`)
+- Azure authentication available to `DefaultAzureCredential` (for local development, run `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"
+$env:FOUNDRY_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project"
+$env:FOUNDRY_MODEL="gpt-4o-mini"
```
## Run the sample
diff --git a/dotnet/samples/05-end-to-end/Evaluation/Evaluation_MixedProviders/Program.cs b/dotnet/samples/05-end-to-end/Evaluation/Evaluation_MixedProviders/Program.cs
index 6c1c163317..7c15887421 100644
--- a/dotnet/samples/05-end-to-end/Evaluation/Evaluation_MixedProviders/Program.cs
+++ b/dotnet/samples/05-end-to-end/Evaluation/Evaluation_MixedProviders/Program.cs
@@ -8,8 +8,8 @@ using Microsoft.Agents.AI;
using Microsoft.Extensions.AI.Evaluation;
using FoundryEvals = Microsoft.Agents.AI.Foundry.FoundryEvals;
-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";
+string endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
+string deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "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
diff --git a/dotnet/samples/05-end-to-end/Evaluation/Evaluation_MixedProviders/README.md b/dotnet/samples/05-end-to-end/Evaluation/Evaluation_MixedProviders/README.md
index 1346635868..712cf0d783 100644
--- a/dotnet/samples/05-end-to-end/Evaluation/Evaluation_MixedProviders/README.md
+++ b/dotnet/samples/05-end-to-end/Evaluation/Evaluation_MixedProviders/README.md
@@ -1,4 +1,4 @@
-# Evaluation - Mixed Providers
+# Evaluation - Mixed Providers
This sample demonstrates mixing local and cloud evaluators in a single evaluation run.
@@ -14,13 +14,13 @@ This sample demonstrates mixing local and cloud evaluators in a single evaluatio
## Prerequisites
- .NET 10 SDK or later
-- Azure CLI installed and authenticated (`az login`)
+- Azure authentication available to `DefaultAzureCredential` (for local development, run `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"
+$env:FOUNDRY_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project"
+$env:FOUNDRY_MODEL="gpt-4o-mini"
```
## Run the sample
@@ -28,4 +28,4 @@ $env:AZURE_AI_MODEL_DEPLOYMENT_NAME="gpt-4o-mini"
```powershell
cd dotnet/samples/05-end-to-end/Evaluation
dotnet run --project .\Evaluation_MixedProviders
-```
\ No newline at end of file
+```
diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/FoundryToolboxBearerTokenHandler.cs b/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/FoundryToolboxBearerTokenHandler.cs
index d345297276..23e2858322 100644
--- a/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/FoundryToolboxBearerTokenHandler.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/FoundryToolboxBearerTokenHandler.cs
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
+using System.Diagnostics;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
@@ -13,24 +14,32 @@ namespace Microsoft.Agents.AI.Foundry.Hosting;
///
/// An that:
///
-/// - Acquires a fresh Azure bearer token (scope: https://cognitiveservices.azure.com/.default) per request.
-/// - Injects the Foundry-Features header from FOUNDRY_AGENT_TOOLSET_FEATURES when non-empty.
+/// - Acquires a fresh Azure bearer token (scope: https://ai.azure.com/.default) per request, per tools-integration-spec.md §4.
+/// - Always injects the mandatory Foundry-Features: Toolboxes=V1Preview header per spec §2, merging any additional flags from FOUNDRY_AGENT_TOOLSET_FEATURES.
+/// - Propagates W3C trace context (traceparent, tracestate, baggage) from per spec §6.3.
/// - Retries on HTTP 429, 500, 502, and 503 with exponential back-off (max 3 attempts, per spec §7).
///
///
internal sealed class FoundryToolboxBearerTokenHandler : DelegatingHandler
{
private const int MaxRetries = 3;
+
+ // Per tools-integration-spec.md §4, the container authenticates to the Foundry Toolbox
+ // proxy with a bearer token whose audience is https://ai.azure.com.
private static readonly TokenRequestContext s_tokenContext =
- new(["https://cognitiveservices.azure.com/.default"]);
+ new(["https://ai.azure.com/.default"]);
+
+ // Per tools-integration-spec.md §2, every proxy request MUST include the
+ // Foundry-Features: Toolboxes=V1Preview opt-in header while the service is in preview.
+ private const string MandatoryFeatureFlag = "Toolboxes=V1Preview";
private readonly TokenCredential _credential;
- private readonly string? _featuresHeaderValue;
+ private readonly string? _additionalFeaturesHeaderValue;
- internal FoundryToolboxBearerTokenHandler(TokenCredential credential, string? featuresHeaderValue)
+ internal FoundryToolboxBearerTokenHandler(TokenCredential credential, string? additionalFeaturesHeaderValue)
{
this._credential = credential;
- this._featuresHeaderValue = featuresHeaderValue;
+ this._additionalFeaturesHeaderValue = additionalFeaturesHeaderValue;
}
protected override async Task SendAsync(
@@ -43,10 +52,9 @@ internal sealed class FoundryToolboxBearerTokenHandler : DelegatingHandler
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token.Token);
- if (!string.IsNullOrEmpty(this._featuresHeaderValue))
- {
- request.Headers.TryAddWithoutValidation("Foundry-Features", this._featuresHeaderValue);
- }
+ request.Headers.TryAddWithoutValidation("Foundry-Features", BuildFeaturesHeaderValue(this._additionalFeaturesHeaderValue));
+
+ PropagateTraceContext(request);
// MaxRetries is the total number of attempts (not additional retries after the first).
for (int attempt = 0; attempt < MaxRetries; attempt++)
@@ -82,6 +90,75 @@ internal sealed class FoundryToolboxBearerTokenHandler : DelegatingHandler
throw new InvalidOperationException("Retry loop completed without returning a response.");
}
+ // Returns "Toolboxes=V1Preview" when no override is set, or
+ // "Toolboxes=V1Preview," when an override is set and doesn't already include it.
+ internal static string BuildFeaturesHeaderValue(string? additional)
+ {
+ if (string.IsNullOrWhiteSpace(additional))
+ {
+ return MandatoryFeatureFlag;
+ }
+
+ // Avoid duplicating the mandatory flag if the override happens to already include it
+ // (case-insensitive, ignore surrounding whitespace).
+ foreach (var part in additional!.Split(','))
+ {
+ if (string.Equals(part.Trim(), MandatoryFeatureFlag, StringComparison.OrdinalIgnoreCase))
+ {
+ return additional;
+ }
+ }
+
+ return $"{MandatoryFeatureFlag},{additional}";
+ }
+
+ // Per tools-integration-spec.md §6.3, propagate W3C trace context onto outbound requests.
+ // Skip headers already set on the message (callers / inner handlers may override).
+ private static void PropagateTraceContext(HttpRequestMessage request)
+ {
+ var activity = Activity.Current;
+ if (activity is null)
+ {
+ return;
+ }
+
+ if (!request.Headers.Contains("traceparent"))
+ {
+ var traceparent = activity.Id;
+ if (!string.IsNullOrEmpty(traceparent))
+ {
+ request.Headers.TryAddWithoutValidation("traceparent", traceparent);
+ }
+ }
+
+ var traceState = activity.TraceStateString;
+ if (!string.IsNullOrEmpty(traceState) && !request.Headers.Contains("tracestate"))
+ {
+ request.Headers.TryAddWithoutValidation("tracestate", traceState);
+ }
+
+ // Baggage is a comma-separated list of key=value pairs per the W3C Baggage spec.
+ if (!request.Headers.Contains("baggage"))
+ {
+ string? baggageHeader = null;
+ foreach (var pair in activity.Baggage)
+ {
+ if (pair.Value is null)
+ {
+ continue;
+ }
+
+ var entry = $"{Uri.EscapeDataString(pair.Key)}={Uri.EscapeDataString(pair.Value)}";
+ baggageHeader = baggageHeader is null ? entry : $"{baggageHeader},{entry}";
+ }
+
+ if (baggageHeader is not null)
+ {
+ request.Headers.TryAddWithoutValidation("baggage", baggageHeader);
+ }
+ }
+ }
+
private static async Task CloneRequestAsync(
HttpRequestMessage original,
CancellationToken cancellationToken)
diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/FoundryToolboxHealthCheck.cs b/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/FoundryToolboxHealthCheck.cs
new file mode 100644
index 0000000000..e4e297d63b
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/FoundryToolboxHealthCheck.cs
@@ -0,0 +1,66 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Diagnostics.HealthChecks;
+using Microsoft.Shared.DiagnosticIds;
+
+namespace Microsoft.Agents.AI.Foundry.Hosting;
+
+///
+/// Adapts to the AspNetCore
+/// HealthChecks pipeline so the GET /readiness probe (mapped by
+/// ) reflects whether
+/// pre-registered toolbox connections are usable. Registered automatically by
+///
+/// and its overloads.
+///
+[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
+internal sealed class FoundryToolboxHealthCheck : IHealthCheck
+{
+ private readonly FoundryToolboxService _toolboxService;
+
+ public FoundryToolboxHealthCheck(FoundryToolboxService toolboxService)
+ {
+ ArgumentNullException.ThrowIfNull(toolboxService);
+ this._toolboxService = toolboxService;
+ }
+
+ public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
+ {
+ switch (this._toolboxService.StartupStatus)
+ {
+ case FoundryToolboxStartupStatus.Healthy:
+ return Task.FromResult(HealthCheckResult.Healthy(
+ description: $"Foundry toolbox: {this._toolboxService.Tools.Count} tool(s) available."));
+
+ case FoundryToolboxStartupStatus.NoEndpoint:
+ return Task.FromResult(HealthCheckResult.Healthy(
+ description: "Foundry toolbox: neither FOUNDRY_PROJECT_ENDPOINT nor AZURE_AI_PROJECT_ENDPOINT is set; toolbox support disabled (local dev)."));
+
+ case FoundryToolboxStartupStatus.Pending:
+ return Task.FromResult(new HealthCheckResult(
+ status: context.Registration.FailureStatus,
+ description: "Foundry toolbox: startup has not completed yet."));
+
+ case FoundryToolboxStartupStatus.Unhealthy:
+ var data = new Dictionary(StringComparer.Ordinal)
+ {
+ ["failedToolboxes"] = this._toolboxService.FailedToolboxNames,
+ };
+ return Task.FromResult(new HealthCheckResult(
+ status: context.Registration.FailureStatus,
+ description: $"Foundry toolbox: {this._toolboxService.FailedToolboxNames.Count} pre-registered toolbox(es) failed to open at startup.",
+ data: data));
+
+ default:
+ return Task.FromResult(new HealthCheckResult(
+ status: context.Registration.FailureStatus,
+ description: $"Foundry toolbox: unknown startup status '{this._toolboxService.StartupStatus}'."));
+ }
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/FoundryToolboxOptions.cs b/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/FoundryToolboxOptions.cs
index 78430f40bf..de3ccbf2ad 100644
--- a/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/FoundryToolboxOptions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/FoundryToolboxOptions.cs
@@ -16,14 +16,15 @@ public sealed class FoundryToolboxOptions
/// Gets the list of toolbox names to connect to at startup.
/// Each name corresponds to a toolbox registered in the Foundry project.
/// The platform proxy URL is constructed as:
- /// {FOUNDRY_AGENT_TOOLSET_ENDPOINT}/{toolboxName}/mcp?api-version={ApiVersion}
+ /// {FOUNDRY_PROJECT_ENDPOINT}/toolboxes/{toolboxName}/mcp?api-version={ApiVersion}
+ /// per tools-integration-spec.md §2–§3.
///
public IList ToolboxNames { get; } = [];
///
- /// Gets or sets the Toolsets API version to use when constructing proxy URLs.
+ /// Gets or sets the Toolboxes API version to use when constructing proxy URLs.
///
- public string ApiVersion { get; set; } = "2025-05-01-preview";
+ public string ApiVersion { get; set; } = "v1";
///
/// Gets or sets a value indicating whether per-request toolbox markers (referenced via
@@ -36,7 +37,9 @@ public sealed class FoundryToolboxOptions
public bool StrictMode { get; set; } = true;
///
- /// For testing only: overrides FOUNDRY_AGENT_TOOLSET_ENDPOINT.
+ /// For testing only: overrides the toolbox proxy base URL (skipping the
+ /// FOUNDRY_PROJECT_ENDPOINT-derived default). When set, the proxy URL
+ /// becomes {EndpointOverride}/toolboxes/{toolboxName}/mcp?api-version={ApiVersion}.
/// Not part of the public API.
///
internal string? EndpointOverride { get; set; }
diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/FoundryToolboxService.cs b/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/FoundryToolboxService.cs
index 7a8bc71e02..44b3d123b8 100644
--- a/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/FoundryToolboxService.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/FoundryToolboxService.cs
@@ -24,7 +24,13 @@ namespace Microsoft.Agents.AI.Foundry.Hosting;
///
///
///
-/// When FOUNDRY_AGENT_TOOLSET_ENDPOINT is absent the service starts without error and
+/// The toolbox proxy base URL is derived from the platform-injected
+/// FOUNDRY_PROJECT_ENDPOINT environment variable per tools-integration-spec.md
+/// §2–§3. The per-toolbox proxy URL is constructed as
+/// {FOUNDRY_PROJECT_ENDPOINT}/toolboxes/{toolboxName}/mcp?api-version={ApiVersion}.
+///
+///
+/// When FOUNDRY_PROJECT_ENDPOINT is absent the service starts without error and
/// no tools are registered, keeping the container healthy per spec §2.
///
///
@@ -56,6 +62,24 @@ public sealed class FoundryToolboxService : IHostedService, IAsyncDisposable
///
public IReadOnlyList Tools { get; private set; } = [];
+ ///
+ /// Gets the startup status of the service. Reflects the outcome of pre-registered
+ /// toolbox connections opened in ; lazy-opens triggered by
+ /// per-request markers do not change this value.
+ ///
+ ///
+ /// Consumed by to gate the
+ /// GET /readiness probe so the Foundry hosted runtime does not start routing
+ /// traffic to a container whose pre-registered toolbox failed to open at startup.
+ ///
+ public FoundryToolboxStartupStatus StartupStatus { get; private set; } = FoundryToolboxStartupStatus.Pending;
+
+ ///
+ /// Gets the names of pre-registered toolboxes that failed to open during
+ /// . Empty when startup was successful or has not run yet.
+ ///
+ public IReadOnlyList FailedToolboxNames { get; private set; } = [];
+
///
/// Initializes a new instance of .
///
@@ -75,16 +99,24 @@ public sealed class FoundryToolboxService : IHostedService, IAsyncDisposable
///
public async Task StartAsync(CancellationToken cancellationToken)
{
- this._resolvedEndpoint = this._options.EndpointOverride
- ?? Environment.GetEnvironmentVariable("FOUNDRY_AGENT_TOOLSET_ENDPOINT");
+ // Per tools-integration-spec.md §2-§3, the container derives the toolbox proxy base
+ // URL from the platform-injected FOUNDRY_PROJECT_ENDPOINT. The EndpointOverride
+ // option exists for tests; AZURE_AI_PROJECT_ENDPOINT is honored as a local-dev
+ // fallback to mirror the convention used by AF-repo samples.
+ var projectEndpoint = this._options.EndpointOverride
+ ?? Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT")
+ ?? Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT");
- if (string.IsNullOrEmpty(this._resolvedEndpoint))
+ if (string.IsNullOrEmpty(projectEndpoint))
{
- this._logger.LogInformation("FOUNDRY_AGENT_TOOLSET_ENDPOINT is not set; toolbox support is disabled.");
+ this._logger.LogWarning(
+ "Neither FOUNDRY_PROJECT_ENDPOINT nor AZURE_AI_PROJECT_ENDPOINT is set; toolbox support is disabled.");
this.Tools = [];
+ this.StartupStatus = FoundryToolboxStartupStatus.NoEndpoint;
return;
}
+ this._resolvedEndpoint = projectEndpoint.TrimEnd('/');
this._featuresHeader = Environment.GetEnvironmentVariable("FOUNDRY_AGENT_TOOLSET_FEATURES");
this._agentName = Environment.GetEnvironmentVariable("FOUNDRY_AGENT_NAME") ?? "hosted-agent";
this._agentVersion = Environment.GetEnvironmentVariable("FOUNDRY_AGENT_VERSION") ?? "1.0.0";
@@ -93,10 +125,12 @@ public sealed class FoundryToolboxService : IHostedService, IAsyncDisposable
{
this._logger.LogInformation("No pre-registered toolbox names configured.");
this.Tools = [];
+ this.StartupStatus = FoundryToolboxStartupStatus.Healthy;
return;
}
var allTools = new List();
+ var failed = new List();
var seen = new HashSet(StringComparer.OrdinalIgnoreCase);
foreach (var toolboxName in this._options.ToolboxNames)
@@ -121,10 +155,16 @@ public sealed class FoundryToolboxService : IHostedService, IAsyncDisposable
"Failed to connect to toolbox '{ToolboxName}'. Tools from this toolbox will not be available.",
toolboxName);
}
+
+ failed.Add(toolboxName);
}
}
this.Tools = allTools;
+ this.FailedToolboxNames = failed;
+ this.StartupStatus = failed.Count == 0
+ ? FoundryToolboxStartupStatus.Healthy
+ : FoundryToolboxStartupStatus.Unhealthy;
}
///
@@ -165,7 +205,7 @@ public sealed class FoundryToolboxService : IHostedService, IAsyncDisposable
if (string.IsNullOrEmpty(this._resolvedEndpoint))
{
throw new InvalidOperationException(
- $"Cannot resolve toolbox '{toolboxName}': FOUNDRY_AGENT_TOOLSET_ENDPOINT is not set.");
+ $"Cannot resolve toolbox '{toolboxName}': FOUNDRY_PROJECT_ENDPOINT is not set.");
}
await this._lazyOpenLock.WaitAsync(cancellationToken).ConfigureAwait(false);
@@ -192,7 +232,7 @@ public sealed class FoundryToolboxService : IHostedService, IAsyncDisposable
string? version,
CancellationToken cancellationToken)
{
- var proxyUrl = $"{this._resolvedEndpoint!.TrimEnd('/')}/{toolboxName}/mcp?api-version={this._options.ApiVersion}";
+ var proxyUrl = $"{this._resolvedEndpoint!}/toolboxes/{toolboxName}/mcp?api-version={this._options.ApiVersion}";
if (this._logger.IsEnabled(LogLevel.Information))
{
diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/FoundryToolboxStartupStatus.cs b/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/FoundryToolboxStartupStatus.cs
new file mode 100644
index 0000000000..821287192d
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/FoundryToolboxStartupStatus.cs
@@ -0,0 +1,46 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Diagnostics.CodeAnalysis;
+using Microsoft.Shared.DiagnosticIds;
+
+namespace Microsoft.Agents.AI.Foundry.Hosting;
+
+///
+/// Outcome of startup. Drives the
+/// foundry-toolbox health-check that gates the GET /readiness probe so the
+/// Foundry hosted runtime does not start routing traffic before pre-registered toolbox
+/// connections are confirmed open (per container-image-spec.md §3.1).
+///
+[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
+public enum FoundryToolboxStartupStatus
+{
+ ///
+ /// has not run yet. The health-check
+ /// reports Unhealthy in this state so the platform waits for startup to
+ /// complete before the first invocation.
+ ///
+ Pending = 0,
+
+ ///
+ /// Startup completed and either every pre-registered toolbox opened successfully or
+ /// no pre-registered toolboxes were configured. The health-check reports
+ /// Healthy.
+ ///
+ Healthy = 1,
+
+ ///
+ /// One or more pre-registered toolboxes failed to open during startup (including the
+ /// partial case where some opened and some did not). The health-check reports
+ /// Unhealthy and exposes the failed names in the HealthCheckResult.Data
+ /// dictionary so operators can diagnose the failure without parsing log output.
+ ///
+ Unhealthy = 2,
+
+ ///
+ /// Neither the FOUNDRY_PROJECT_ENDPOINT nor the AZURE_AI_PROJECT_ENDPOINT
+ /// environment variable is set. This is normal for local dotnet run flows and the
+ /// health-check reports Healthy so the container is still routable; toolbox tools
+ /// will simply not be available.
+ ///
+ NoEndpoint = 3,
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/Microsoft.Agents.AI.Foundry.Hosting.csproj b/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/Microsoft.Agents.AI.Foundry.Hosting.csproj
index 71d9af8f71..ec348b2167 100644
--- a/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/Microsoft.Agents.AI.Foundry.Hosting.csproj
+++ b/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/Microsoft.Agents.AI.Foundry.Hosting.csproj
@@ -13,7 +13,7 @@
true
true
true
- $(NoWarn);OPENAI001;MEAI001;NU1903
+ $(NoWarn);OPENAI001;MEAI001;MAAI001;NU1903
false
diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/ServiceCollectionExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/ServiceCollectionExtensions.cs
index a0f53b342e..bd61b9f4eb 100644
--- a/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/ServiceCollectionExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/ServiceCollectionExtensions.cs
@@ -7,10 +7,12 @@ using System.Runtime.CompilerServices;
using Azure.AI.AgentServer.Responses;
using Azure.Core;
using Azure.Identity;
+using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Shared.DiagnosticIds;
namespace Microsoft.Agents.AI.Foundry.Hosting;
@@ -49,6 +51,7 @@ public static class FoundryHostingExtensions
{
ArgumentNullException.ThrowIfNull(services);
services.AddResponsesServer();
+ services.AddHealthChecks();
services.TryAddSingleton(_ => FileSystemAgentSessionStore.CreateDefault());
services.TryAddSingleton();
return services;
@@ -84,6 +87,7 @@ public static class FoundryHostingExtensions
ArgumentNullException.ThrowIfNull(agent);
services.AddResponsesServer();
+ services.AddHealthChecks();
agentSessionStore ??= FileSystemAgentSessionStore.CreateDefault();
if (!string.IsNullOrWhiteSpace(agent.Name))
@@ -109,10 +113,10 @@ public static class FoundryHostingExtensions
///
/// Each string in is a toolbox name registered in the Foundry
/// project. The proxy URL per toolbox is constructed as:
- /// {FOUNDRY_AGENT_TOOLSET_ENDPOINT}/{toolboxName}/mcp?api-version=2025-05-01-preview
+ /// {FOUNDRY_PROJECT_ENDPOINT}/toolboxes/{toolboxName}/mcp?api-version=v1
///
///
- /// When FOUNDRY_AGENT_TOOLSET_ENDPOINT is absent, startup succeeds without error and
+ /// When FOUNDRY_PROJECT_ENDPOINT is absent, startup succeeds without error and
/// no tools are loaded (the container remains healthy per spec §2).
///
///
@@ -167,12 +171,61 @@ public static class FoundryHostingExtensions
// multiple times will not invoke StartAsync twice on the same singleton.
services.AddHostedService(sp => sp.GetRequiredService());
+ // Register the toolbox health check on the same /readiness pipeline that
+ // MapFoundryResponses maps. This gates the Foundry hosted runtime's readiness
+ // probe (per container-image-spec.md §3.1) on the outcome of the pre-registered
+ // toolbox connections opened in FoundryToolboxService.StartAsync.
+ // AddCheck(name, ...) does NOT dedupe by name, so guard against duplicate
+ // registration when AddFoundryToolboxes is called multiple times.
+ const string HealthCheckName = "foundry-toolbox";
+ services.AddHealthChecks();
+ services.Configure(opts =>
+ {
+ foreach (var existing in opts.Registrations)
+ {
+ if (string.Equals(existing.Name, HealthCheckName, StringComparison.Ordinal))
+ {
+ return;
+ }
+ }
+
+ opts.Registrations.Add(new HealthCheckRegistration(
+ name: HealthCheckName,
+ factory: sp => ActivatorUtilities.CreateInstance(sp),
+ failureStatus: HealthStatus.Unhealthy,
+ tags: ["foundry", "toolbox", "readiness"]));
+ });
+
return services;
}
///
/// Maps the Responses API routes for the agent-framework handler to the endpoint routing pipeline.
///
+ ///
+ ///
+ /// Also maps the Foundry-required GET /readiness health probe to
+ ///
+ /// when no /readiness route is already registered. This makes the package
+ /// spec-compliant in the Foundry hosted runtime (which probes /readiness
+ /// before accepting any invocation per container-image-spec.md §2; without
+ /// it every request fails with HTTP 424 session_not_ready) regardless of the
+ /// host spine the developer chose:
+ ///
+ ///
+ /// - Tier 1/2 (AgentHost.CreateBuilder) — the Core SDK
+ /// already maps /readiness. The duplicate-route guard below skips
+ /// re-mapping it.
+ /// - Tier 3 (WebApplication.CreateBuilder +
+ /// AddFoundryResponses + MapFoundryResponses) — the Core SDK
+ /// does NOT map it. This call covers the gap automatically.
+ ///
+ ///
+ /// Developers can still opt out by registering their own /readiness route
+ /// before calling MapFoundryResponses; the existing route is detected and
+ /// preserved.
+ ///
+ ///
/// The endpoint route builder.
/// Optional route prefix (e.g., "/openai/v1"). Default: empty (routes at /responses).
/// The endpoint route builder for chaining.
@@ -180,9 +233,37 @@ public static class FoundryHostingExtensions
{
ArgumentNullException.ThrowIfNull(endpoints);
endpoints.MapResponsesServer(prefix);
+ MapReadinessIfMissing(endpoints);
return endpoints;
}
+ ///
+ /// Maps GET /readiness to the AspNetCore HealthChecks pipeline only when no
+ /// route already serves that path. The duplicate guard scans
+ /// entries by route pattern, which catches both the
+ /// SDK-mapped MapHealthChecks("/readiness") path used by
+ /// AgentHostBuilder and any user-registered app.MapGet("/readiness", ...)
+ /// route. Idempotent across multiple MapFoundryResponses invocations.
+ ///
+ private static void MapReadinessIfMissing(IEndpointRouteBuilder endpoints)
+ {
+ const string ReadinessPath = "/readiness";
+
+ foreach (var dataSource in endpoints.DataSources)
+ {
+ foreach (var endpoint in dataSource.Endpoints)
+ {
+ if (endpoint is RouteEndpoint route &&
+ string.Equals(route.RoutePattern.RawText, ReadinessPath, StringComparison.OrdinalIgnoreCase))
+ {
+ return;
+ }
+ }
+ }
+
+ endpoints.MapHealthChecks(ReadinessPath);
+ }
+
///
/// The ActivitySource name for the Responses hosting pipeline.
///
diff --git a/dotnet/src/Microsoft.Agents.AI.GitHub.Copilot/GitHubCopilotAgent.cs b/dotnet/src/Microsoft.Agents.AI.GitHub.Copilot/GitHubCopilotAgent.cs
index 053a08f725..a85c126fd6 100644
--- a/dotnet/src/Microsoft.Agents.AI.GitHub.Copilot/GitHubCopilotAgent.cs
+++ b/dotnet/src/Microsoft.Agents.AI.GitHub.Copilot/GitHubCopilotAgent.cs
@@ -145,11 +145,12 @@ public sealed class GitHubCopilotAgent : AIAgent, IAsyncDisposable
// Ensure the client is started
await this.EnsureClientStartedAsync(cancellationToken).ConfigureAwait(false);
- // Create or resume a session with streaming enabled
+ // Create or resume a session with streaming enabled by default
SessionConfig sessionConfig = this._sessionConfig != null
? CopySessionConfig(this._sessionConfig)
: new SessionConfig { Streaming = true };
+ bool isStreaming = sessionConfig.Streaming ?? true;
CopilotSession copilotSession;
if (typedSession.SessionId is not null)
{
@@ -178,7 +179,7 @@ public sealed class GitHubCopilotAgent : AIAgent, IAsyncDisposable
break;
case AssistantMessageEvent assistantMessage:
- channel.Writer.TryWrite(this.ConvertToAgentResponseUpdate(assistantMessage));
+ channel.Writer.TryWrite(this.ConvertToAgentResponseUpdate(assistantMessage, isStreaming));
break;
case AssistantUsageEvent usageEvent:
@@ -271,19 +272,20 @@ public sealed class GitHubCopilotAgent : AIAgent, IAsyncDisposable
}
///
- /// Copies all supported properties from a source into a new instance
- /// with set to true.
+ /// Copies all supported properties from a source into a new instance,
+ /// preserving from the source (defaulting to true if unset).
///
internal static SessionConfig CopySessionConfig(SessionConfig source)
{
SessionConfig copy = source.Clone();
- copy.Streaming = true;
+ copy.Streaming = source.Streaming ?? true;
return copy;
}
///
/// Copies all supported properties from a source into a new
- /// with set to true.
+ /// , preserving
+ /// from the source (defaulting to true if unset).
///
internal static ResumeSessionConfig CopyResumeSessionConfig(SessionConfig? source)
{
@@ -306,7 +308,7 @@ public sealed class GitHubCopilotAgent : AIAgent, IAsyncDisposable
SkillDirectories = source?.SkillDirectories,
DisabledSkills = source?.DisabledSkills,
InfiniteSessions = source?.InfiniteSessions,
- Streaming = true
+ Streaming = source?.Streaming ?? true
};
}
@@ -325,12 +327,18 @@ public sealed class GitHubCopilotAgent : AIAgent, IAsyncDisposable
};
}
- internal AgentResponseUpdate ConvertToAgentResponseUpdate(AssistantMessageEvent assistantMessage)
+ ///
+ /// Converts an to an .
+ /// When streaming is enabled, text was already delivered via delta events, so only raw metadata is emitted.
+ /// When streaming is disabled, the full message text is emitted as .
+ ///
+ internal AgentResponseUpdate ConvertToAgentResponseUpdate(AssistantMessageEvent assistantMessage, bool isStreaming)
{
- AIContent content = new()
- {
- RawRepresentation = assistantMessage
- };
+ // When streaming, text was already delivered via AssistantMessageDeltaEvent.
+ // When not streaming, this is the only opportunity to emit the response text.
+ AIContent content = isStreaming
+ ? new AIContent { RawRepresentation = assistantMessage }
+ : new TextContent(assistantMessage.Data?.Content ?? string.Empty) { RawRepresentation = assistantMessage };
return new AgentResponseUpdate(ChatRole.Assistant, [content])
{
diff --git a/dotnet/src/Microsoft.Agents.AI.GitHub.Copilot/Microsoft.Agents.AI.GitHub.Copilot.csproj b/dotnet/src/Microsoft.Agents.AI.GitHub.Copilot/Microsoft.Agents.AI.GitHub.Copilot.csproj
index 8b868b5f77..dad0b31f59 100644
--- a/dotnet/src/Microsoft.Agents.AI.GitHub.Copilot/Microsoft.Agents.AI.GitHub.Copilot.csproj
+++ b/dotnet/src/Microsoft.Agents.AI.GitHub.Copilot/Microsoft.Agents.AI.GitHub.Copilot.csproj
@@ -1,7 +1,7 @@
- preview
+ true
$(TargetFrameworksCore)
$(NoWarn);GHCP001
@@ -32,4 +32,52 @@
Provides Microsoft Agent Framework support for GitHub Copilot SDK.
+
+
+
+
+
+
+
+
+ <_CopilotSdkPackageVersion Include="@(PackageVersion)" Condition="'%(Identity)' == 'GitHub.Copilot.SDK'" />
+
+
+ <_CopilotSdkResolvedVersion>@(_CopilotSdkPackageVersion->'%(Version)')
+
+
+
+ <_BuildTransitivePropsContent>
+
+
+ <_MicrosoftAgentsAICopilotSdkPackagedVersion>$(_CopilotSdkResolvedVersion)
+
+]]>
+
+
+
+
+
diff --git a/dotnet/src/Microsoft.Agents.AI.GitHub.Copilot/buildTransitive/.gitignore b/dotnet/src/Microsoft.Agents.AI.GitHub.Copilot/buildTransitive/.gitignore
new file mode 100644
index 0000000000..87bc8ad53e
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.GitHub.Copilot/buildTransitive/.gitignore
@@ -0,0 +1,3 @@
+# Auto-generated at pack time by _GenerateBuildTransitiveProps in the csproj.
+Microsoft.Agents.AI.GitHub.Copilot.props
+
diff --git a/dotnet/src/Microsoft.Agents.AI.GitHub.Copilot/buildTransitive/Microsoft.Agents.AI.GitHub.Copilot.targets b/dotnet/src/Microsoft.Agents.AI.GitHub.Copilot/buildTransitive/Microsoft.Agents.AI.GitHub.Copilot.targets
new file mode 100644
index 0000000000..04a777550f
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.GitHub.Copilot/buildTransitive/Microsoft.Agents.AI.GitHub.Copilot.targets
@@ -0,0 +1,34 @@
+
+
+
+ <_MicrosoftAgentsAICopilotSdkVersion Condition="'$(_MicrosoftAgentsAICopilotSdkVersion)' == ''">$(_MicrosoftAgentsAICopilotSdkPackagedVersion)
+ <_MicrosoftAgentsAICopilotSdkTargetsPath Condition="'$(_MicrosoftAgentsAICopilotSdkVersion)' != ''">$([System.IO.Path]::Combine('$(NuGetPackageRoot)', 'github.copilot.sdk', '$(_MicrosoftAgentsAICopilotSdkVersion)', 'build', 'GitHub.Copilot.SDK.targets'))
+
+
+
+
diff --git a/dotnet/src/Microsoft.Agents.AI.Purview/BackgroundJobRunner.cs b/dotnet/src/Microsoft.Agents.AI.Purview/BackgroundJobRunner.cs
index 85a4fa54c3..03f73d8007 100644
--- a/dotnet/src/Microsoft.Agents.AI.Purview/BackgroundJobRunner.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Purview/BackgroundJobRunner.cs
@@ -1,10 +1,14 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
+using System.Collections.Generic;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
+using Microsoft.Agents.AI.Purview.Models.Common;
using Microsoft.Agents.AI.Purview.Models.Jobs;
+using Microsoft.Agents.AI.Purview.Models.Requests;
+using Microsoft.Agents.AI.Purview.Models.Responses;
using Microsoft.Extensions.Logging;
namespace Microsoft.Agents.AI.Purview;
@@ -16,6 +20,7 @@ internal sealed class BackgroundJobRunner : IBackgroundJobRunner
{
private readonly IChannelHandler _channelHandler;
private readonly IPurviewClient _purviewClient;
+ private readonly ICacheProvider _cacheProvider;
private readonly ILogger _logger;
///
@@ -23,12 +28,14 @@ internal sealed class BackgroundJobRunner : IBackgroundJobRunner
///
/// The channel handler used to manage job channels.
/// The Purview client used to send requests to Purview.
+ /// The cache provider used to store protection scopes results.
/// The logger used to log information about background jobs.
/// The settings used to configure Purview client behavior.
- public BackgroundJobRunner(IChannelHandler channelHandler, IPurviewClient purviewClient, ILogger logger, PurviewSettings purviewSettings)
+ public BackgroundJobRunner(IChannelHandler channelHandler, IPurviewClient purviewClient, ICacheProvider cacheProvider, ILogger logger, PurviewSettings purviewSettings)
{
this._channelHandler = channelHandler;
this._purviewClient = purviewClient;
+ this._cacheProvider = cacheProvider;
this._logger = logger;
for (int i = 0; i < purviewSettings.MaxConcurrentJobConsumers; i++)
@@ -67,6 +74,28 @@ internal sealed class BackgroundJobRunner : IBackgroundJobRunner
break;
case ContentActivityJob contentActivityJob:
_ = await this._purviewClient.SendContentActivitiesAsync(contentActivityJob.Request, CancellationToken.None).ConfigureAwait(false);
+ break;
+ case ScopeRetrievalJob scopeRetrievalJob:
+ try
+ {
+ ProtectionScopesResponse response = await this._purviewClient.GetProtectionScopesAsync(scopeRetrievalJob.Request, CancellationToken.None).ConfigureAwait(false);
+ await this._cacheProvider.SetAsync(scopeRetrievalJob.CacheKey, response, CancellationToken.None).ConfigureAwait(false);
+ (bool shouldProcess, List _, ExecutionMode _) = ScopedContentProcessor.CheckApplicableScopes(scopeRetrievalJob.ProcessContentRequest, response);
+ if (!shouldProcess)
+ {
+ ProcessContentRequest pcRequest = scopeRetrievalJob.ProcessContentRequest;
+ ContentActivitiesRequest caRequest = new(pcRequest.UserId, pcRequest.TenantId, pcRequest.ContentToProcess, pcRequest.CorrelationId);
+ this._channelHandler.QueueJob(new ContentActivityJob(caRequest));
+ }
+ }
+ catch (PurviewPaymentRequiredException ex)
+ {
+ await this._cacheProvider.SetAsync(
+ new PaymentRequiredCacheKey(scopeRetrievalJob.Request.TenantId),
+ new PaymentRequiredCacheEntry(ex.Message),
+ CancellationToken.None).ConfigureAwait(false);
+ }
+
break;
}
}
diff --git a/dotnet/src/Microsoft.Agents.AI.Purview/Models/Common/PaymentRequiredCacheEntry.cs b/dotnet/src/Microsoft.Agents.AI.Purview/Models/Common/PaymentRequiredCacheEntry.cs
new file mode 100644
index 0000000000..6bd9d40853
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Purview/Models/Common/PaymentRequiredCacheEntry.cs
@@ -0,0 +1,23 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+namespace Microsoft.Agents.AI.Purview.Models.Common;
+
+///
+/// Cached tenant-level payment required state.
+///
+internal sealed class PaymentRequiredCacheEntry
+{
+ ///
+ /// Creates a new instance of .
+ ///
+ /// The payment required error message.
+ public PaymentRequiredCacheEntry(string? message)
+ {
+ this.Message = message;
+ }
+
+ ///
+ /// The payment required error message.
+ ///
+ public string? Message { get; set; }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Purview/Models/Common/PaymentRequiredCacheKey.cs b/dotnet/src/Microsoft.Agents.AI.Purview/Models/Common/PaymentRequiredCacheKey.cs
new file mode 100644
index 0000000000..3c9ad4f813
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Purview/Models/Common/PaymentRequiredCacheKey.cs
@@ -0,0 +1,23 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+namespace Microsoft.Agents.AI.Purview.Models.Common;
+
+///
+/// A cache key for tenant-level payment required state.
+///
+internal sealed class PaymentRequiredCacheKey
+{
+ ///
+ /// Creates a new instance of .
+ ///
+ /// The id of the tenant.
+ public PaymentRequiredCacheKey(string tenantId)
+ {
+ this.TenantId = tenantId;
+ }
+
+ ///
+ /// The id of the tenant.
+ ///
+ public string TenantId { get; set; }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Purview/Models/Jobs/ScopeRetrievalJob.cs b/dotnet/src/Microsoft.Agents.AI.Purview/Models/Jobs/ScopeRetrievalJob.cs
new file mode 100644
index 0000000000..c23553f185
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Purview/Models/Jobs/ScopeRetrievalJob.cs
@@ -0,0 +1,44 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.Agents.AI.Purview.Models.Common;
+using Microsoft.Agents.AI.Purview.Models.Requests;
+
+namespace Microsoft.Agents.AI.Purview.Models.Jobs;
+
+///
+/// Class representing a job that refreshes the protection scopes cache in the background.
+///
+///
+/// Used by the parallel protection scopes retrieval path to warm the cache without blocking the
+/// foreground ProcessContent call.
+///
+internal sealed class ScopeRetrievalJob : BackgroundJobBase
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The protection scopes request to send to Purview.
+ /// The cache key used to store the response.
+ /// The original process content request that triggered scope retrieval.
+ public ScopeRetrievalJob(ProtectionScopesRequest request, ProtectionScopesCacheKey cacheKey, ProcessContentRequest processContentRequest)
+ {
+ this.Request = request;
+ this.CacheKey = cacheKey;
+ this.ProcessContentRequest = processContentRequest;
+ }
+
+ ///
+ /// Gets the protection scopes request.
+ ///
+ public ProtectionScopesRequest Request { get; }
+
+ ///
+ /// Gets the cache key used to store the response.
+ ///
+ public ProtectionScopesCacheKey CacheKey { get; }
+
+ ///
+ /// Gets the original process content request that triggered scope retrieval.
+ ///
+ public ProcessContentRequest ProcessContentRequest { get; }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Purview/Models/Requests/ProcessContentRequest.cs b/dotnet/src/Microsoft.Agents.AI.Purview/Models/Requests/ProcessContentRequest.cs
index f8e9602cef..d41a3a2090 100644
--- a/dotnet/src/Microsoft.Agents.AI.Purview/Models/Requests/ProcessContentRequest.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Purview/Models/Requests/ProcessContentRequest.cs
@@ -53,4 +53,10 @@ internal sealed class ProcessContentRequest
///
[JsonIgnore]
internal string? ScopeIdentifier { get; set; }
+
+ ///
+ /// Indicates whether the ProcessContent request should ask the service for inline evaluation.
+ ///
+ [JsonIgnore]
+ internal bool ProcessInline { get; set; }
}
diff --git a/dotnet/src/Microsoft.Agents.AI.Purview/PurviewClient.cs b/dotnet/src/Microsoft.Agents.AI.Purview/PurviewClient.cs
index 28013f524e..43b564b58f 100644
--- a/dotnet/src/Microsoft.Agents.AI.Purview/PurviewClient.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Purview/PurviewClient.cs
@@ -130,6 +130,11 @@ internal sealed class PurviewClient : IPurviewClient
message.Headers.Add("If-None-Match", request.ScopeIdentifier);
}
+ if (request.ProcessInline)
+ {
+ message.Headers.Add("Prefer", "evaluateInline");
+ }
+
string content = JsonSerializer.Serialize(request, PurviewSerializationUtils.SerializationSettings.GetTypeInfo(typeof(ProcessContentRequest)));
message.Content = new StringContent(content, Encoding.UTF8, "application/json");
diff --git a/dotnet/src/Microsoft.Agents.AI.Purview/README.md b/dotnet/src/Microsoft.Agents.AI.Purview/README.md
index 1a9fc70725..bcd1a26192 100644
--- a/dotnet/src/Microsoft.Agents.AI.Purview/README.md
+++ b/dotnet/src/Microsoft.Agents.AI.Purview/README.md
@@ -218,8 +218,8 @@ The policy logic is identical; the only difference is the hook point in the pipe
The user id from the prompt message(s) is reused for the response evaluation so both evaluations map consistently to the same user.
-There are several optimizations to speed up Purview calls. Protection scope lookups (the first step in evaluation) are cached to minimize network calls.
-If the policies allow content to be processed offline, the middleware will add the process content request to a channel and run it in a background worker. Similarly, the middleware will run a background request if no scopes apply and the interaction only has to be logged in Audit.
+There are several optimizations to speed up Purview calls. Protection scope lookups (the first step in evaluation) are cached to minimize network calls. When a lookup is not cached, the middleware will refresh it in a background worker so the foreground ProcessContent request does not have to wait.
+If the policies allow content to be processed offline, the middleware will add the process content request to a channel and run it in a background worker. Similarly, the middleware will run a background request if no scopes apply and the interaction only has to be logged in Audit. Payment Required responses from background scope lookups are cached at the tenant level so subsequent requests for the tenant short-circuit.
## Exceptions
| Exception | Scenario |
diff --git a/dotnet/src/Microsoft.Agents.AI.Purview/ScopedContentProcessor.cs b/dotnet/src/Microsoft.Agents.AI.Purview/ScopedContentProcessor.cs
index 3fb7aa6c4d..3e280014a0 100644
--- a/dotnet/src/Microsoft.Agents.AI.Purview/ScopedContentProcessor.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Purview/ScopedContentProcessor.cs
@@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
+using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.AI.Purview.Models.Common;
@@ -193,43 +194,60 @@ internal sealed class ScopedContentProcessor : IScopedContentProcessor
{
ProtectionScopesRequest psRequest = CreateProtectionScopesRequest(pcRequest, pcRequest.UserId, pcRequest.TenantId, pcRequest.CorrelationId);
+ PaymentRequiredCacheEntry? cachedPaymentRequired = await this._cacheProvider.GetAsync(
+ new PaymentRequiredCacheKey(pcRequest.TenantId),
+ cancellationToken).ConfigureAwait(false);
+
+ if (cachedPaymentRequired != null)
+ {
+ throw new PurviewPaymentRequiredException(cachedPaymentRequired.Message ?? "Payment required");
+ }
+
ProtectionScopesCacheKey cacheKey = new(psRequest);
ProtectionScopesResponse? cacheResponse = await this._cacheProvider.GetAsync(cacheKey, cancellationToken).ConfigureAwait(false);
- ProtectionScopesResponse psResponse;
-
if (cacheResponse != null)
{
- psResponse = cacheResponse;
- }
- else
- {
- psResponse = await this._purviewClient.GetProtectionScopesAsync(psRequest, cancellationToken).ConfigureAwait(false);
- await this._cacheProvider.SetAsync(cacheKey, psResponse, cancellationToken).ConfigureAwait(false);
+ return await this.ProcessWithCachedScopesAsync(pcRequest, cacheResponse, cacheKey, cancellationToken).ConfigureAwait(false);
}
+ try
+ {
+ this._channelHandler.QueueJob(new ScopeRetrievalJob(psRequest, cacheKey, pcRequest));
+ }
+ catch (PurviewJobException)
+ {
+ // QueueJob already logs failures. Scope warmup is best effort; don't block ProcessContent.
+ }
+
+ return await this.CallProcessContentAsync(pcRequest, cacheKey, dlpActions: null, cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ /// Apply locally-cached protection scopes to the request and dispatch ProcessContent appropriately.
+ ///
+ private async Task ProcessWithCachedScopesAsync(
+ ProcessContentRequest pcRequest,
+ ProtectionScopesResponse psResponse,
+ ProtectionScopesCacheKey cacheKey,
+ CancellationToken cancellationToken)
+ {
pcRequest.ScopeIdentifier = psResponse.ScopeIdentifier;
(bool shouldProcess, List dlpActions, ExecutionMode executionMode) = CheckApplicableScopes(pcRequest, psResponse);
if (shouldProcess)
{
+ pcRequest.ProcessInline = executionMode == ExecutionMode.EvaluateInline;
+
if (executionMode == ExecutionMode.EvaluateOffline)
{
this._channelHandler.QueueJob(new ProcessContentJob(pcRequest));
return new ProcessContentResponse();
}
- ProcessContentResponse pcResponse = await this._purviewClient.ProcessContentAsync(pcRequest, cancellationToken).ConfigureAwait(false);
-
- if (pcResponse.ProtectionScopeState == ProtectionScopeState.Modified)
- {
- await this._cacheProvider.RemoveAsync(cacheKey, cancellationToken).ConfigureAwait(false);
- }
-
- pcResponse = CombinePolicyActions(pcResponse, dlpActions);
- return pcResponse;
+ return await this.CallProcessContentAsync(pcRequest, cacheKey, dlpActions, cancellationToken).ConfigureAwait(false);
}
ContentActivitiesRequest caRequest = new(pcRequest.UserId, pcRequest.TenantId, pcRequest.ContentToProcess, pcRequest.CorrelationId);
@@ -238,6 +256,30 @@ internal sealed class ScopedContentProcessor : IScopedContentProcessor
return new ProcessContentResponse();
}
+ ///
+ /// Call ProcessContent and invalidate the protection scopes cache when the response indicates the cached scopes are stale.
+ ///
+ private async Task CallProcessContentAsync(
+ ProcessContentRequest pcRequest,
+ ProtectionScopesCacheKey cacheKey,
+ List? dlpActions,
+ CancellationToken cancellationToken)
+ {
+ ProcessContentResponse pcResponse = await this._purviewClient.ProcessContentAsync(pcRequest, cancellationToken).ConfigureAwait(false);
+
+ if (pcRequest.ScopeIdentifier != null && pcResponse.ProtectionScopeState == ProtectionScopeState.Modified)
+ {
+ await this._cacheProvider.RemoveAsync(cacheKey, cancellationToken).ConfigureAwait(false);
+ }
+
+ if (dlpActions?.Count > 0)
+ {
+ pcResponse = CombinePolicyActions(pcResponse, dlpActions);
+ }
+
+ return pcResponse;
+ }
+
///
/// Dedupe policy actions received from the service.
///
@@ -248,9 +290,21 @@ internal sealed class ScopedContentProcessor : IScopedContentProcessor
{
if (actionInfos?.Count > 0)
{
- pcResponse.PolicyActions = pcResponse.PolicyActions is null ?
- actionInfos :
- [.. pcResponse.PolicyActions, .. actionInfos];
+ List combinedActions = [];
+ HashSet<(DlpAction Action, RestrictionAction? RestrictionAction)> seenActions = [];
+ IEnumerable allActions = pcResponse.PolicyActions is null
+ ? actionInfos
+ : pcResponse.PolicyActions.Concat(actionInfos);
+
+ foreach (DlpActionInfo actionInfo in allActions)
+ {
+ if (seenActions.Add((actionInfo.Action, actionInfo.RestrictionAction)))
+ {
+ combinedActions.Add(actionInfo);
+ }
+ }
+
+ pcResponse.PolicyActions = combinedActions;
}
return pcResponse;
@@ -262,7 +316,7 @@ internal sealed class ScopedContentProcessor : IScopedContentProcessor
/// The process content request.
/// The protection scopes response that was returned for the process content request.
/// A bool indicating if the content needs to be processed. A list of applicable actions from the scopes response, and the execution mode for the process content request.
- private static (bool shouldProcess, List dlpActions, ExecutionMode executionMode) CheckApplicableScopes(ProcessContentRequest pcRequest, ProtectionScopesResponse psResponse)
+ internal static (bool shouldProcess, List dlpActions, ExecutionMode executionMode) CheckApplicableScopes(ProcessContentRequest pcRequest, ProtectionScopesResponse psResponse)
{
ProtectionScopeActivities requestActivity = TranslateActivity(pcRequest.ContentToProcess.ActivityMetadata.Activity);
@@ -284,7 +338,11 @@ internal sealed class ScopedContentProcessor : IScopedContentProcessor
foreach (var location in scope.Locations ?? Array.Empty())
{
- locationMatch = location.DataType.EndsWith(locationType, StringComparison.OrdinalIgnoreCase) && location.Value.Equals(locationValue, StringComparison.OrdinalIgnoreCase);
+ if (location.DataType.EndsWith(locationType, StringComparison.OrdinalIgnoreCase) && location.Value.Equals(locationValue, StringComparison.OrdinalIgnoreCase))
+ {
+ locationMatch = true;
+ break;
+ }
}
if (activityMatch && locationMatch)
diff --git a/dotnet/src/Microsoft.Agents.AI.Purview/Serialization/PurviewSerializationUtils.cs b/dotnet/src/Microsoft.Agents.AI.Purview/Serialization/PurviewSerializationUtils.cs
index 320fbcd3b6..0be4c59267 100644
--- a/dotnet/src/Microsoft.Agents.AI.Purview/Serialization/PurviewSerializationUtils.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Purview/Serialization/PurviewSerializationUtils.cs
@@ -18,6 +18,8 @@ namespace Microsoft.Agents.AI.Purview.Serialization;
[JsonSerializable(typeof(ContentActivitiesRequest))]
[JsonSerializable(typeof(ContentActivitiesResponse))]
[JsonSerializable(typeof(ProtectionScopesCacheKey))]
+[JsonSerializable(typeof(PaymentRequiredCacheKey))]
+[JsonSerializable(typeof(PaymentRequiredCacheEntry))]
internal sealed partial class SourceGenerationContext : JsonSerializerContext;
///
diff --git a/dotnet/src/Microsoft.Agents.AI.Valkey/Microsoft.Agents.AI.Valkey.csproj b/dotnet/src/Microsoft.Agents.AI.Valkey/Microsoft.Agents.AI.Valkey.csproj
new file mode 100644
index 0000000000..e819c3f51c
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Valkey/Microsoft.Agents.AI.Valkey.csproj
@@ -0,0 +1,40 @@
+
+
+
+ $(TargetFrameworksCore)
+ Microsoft.Agents.AI.Valkey
+ alpha
+ $(NoWarn);CA1873
+
+
+
+ true
+ true
+
+
+
+
+
+ false
+
+
+
+
+ Microsoft Agent Framework - Valkey integration
+ Provides Valkey integration for Microsoft Agent Framework, including chat history persistence and context provider with full-text search.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/src/Microsoft.Agents.AI.Valkey/ValkeyChatHistoryProvider.cs b/dotnet/src/Microsoft.Agents.AI.Valkey/ValkeyChatHistoryProvider.cs
new file mode 100644
index 0000000000..088d66ed47
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Valkey/ValkeyChatHistoryProvider.cs
@@ -0,0 +1,225 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.AI;
+using Microsoft.Extensions.Logging;
+using Microsoft.Shared.Diagnostics;
+using Valkey.Glide;
+
+namespace Microsoft.Agents.AI.Valkey;
+
+///
+/// Provides a Valkey-backed implementation of for persistent chat history storage.
+///
+///
+///
+/// Uses basic Valkey list operations via Valkey.Glide.
+/// No search module is required — this provider works with any Valkey server.
+///
+///
+/// Data retention: Stored messages have no TTL and persist indefinitely.
+/// Use to limit per-conversation storage, and
+/// for explicit cleanup. Callers are responsible for implementing data retention policies.
+///
+///
+/// Security considerations:
+///
+/// - PII and sensitive data: Chat history stored in Valkey may contain PII and sensitive
+/// conversation content. Ensure the Valkey server is configured with appropriate access controls and encryption in transit
+/// (TLS). The property can limit stored messages per conversation.
+/// - Compromised store risks: Agent Framework does not validate or filter messages loaded
+/// from the store — they are accepted as-is. If the Valkey store is compromised, adversarial content could be injected
+/// into the conversation context.
+///
+///
+///
+public sealed class ValkeyChatHistoryProvider : ChatHistoryProvider
+{
+ private readonly ProviderSessionState _sessionState;
+ private IReadOnlyList? _stateKeys;
+ private readonly IConnectionMultiplexer _connection;
+ private readonly string _keyPrefix;
+ private readonly int? _maxMessages;
+ private readonly int? _maxMessagesToRetrieve;
+ private readonly JsonSerializerOptions _jsonSerializerOptions;
+ private readonly ILogger? _logger;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// An existing instance.
+ /// A delegate that initializes the provider state on the first invocation.
+ /// Optional configuration options.
+ /// Optional logger factory.
+ public ValkeyChatHistoryProvider(
+ IConnectionMultiplexer connection,
+ Func stateInitializer,
+ ValkeyChatHistoryProviderOptions? options = null,
+ ILoggerFactory? loggerFactory = null)
+ : base(options?.ProvideOutputMessageFilter, options?.StoreInputRequestMessageFilter, options?.StoreInputResponseMessageFilter)
+ {
+ this._sessionState = new ProviderSessionState(
+ Throw.IfNull(stateInitializer),
+ options?.StateKey ?? this.GetType().Name,
+ options?.JsonSerializerOptions);
+ this._connection = Throw.IfNull(connection);
+ this._keyPrefix = options?.KeyPrefix ?? "chat_history";
+ this._maxMessages = options?.MaxMessages;
+ this._maxMessagesToRetrieve = options?.MaxMessagesToRetrieve;
+ this._jsonSerializerOptions = options?.JsonSerializerOptions ?? AgentAbstractionsJsonUtilities.DefaultOptions;
+ this._logger = loggerFactory?.CreateLogger();
+ }
+
+ ///
+ public override IReadOnlyList StateKeys => this._stateKeys ??= [this._sessionState.StateKey];
+
+ ///
+ protected override async ValueTask> ProvideChatHistoryAsync(InvokingContext context, CancellationToken cancellationToken = default)
+ {
+ Throw.IfNull(context);
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var state = this._sessionState.GetOrInitializeState(context.Session);
+ var db = this._connection.GetDatabase();
+ var key = this.BuildKey(state);
+
+ // Fetch only the tail when MaxMessagesToRetrieve is set [Low: avoid fetching all then trimming]
+ ValkeyValue[] values;
+ if (this._maxMessagesToRetrieve.HasValue)
+ {
+ values = await db.ListRangeAsync(key, -this._maxMessagesToRetrieve.Value, -1).ConfigureAwait(false);
+ }
+ else
+ {
+ values = await db.ListRangeAsync(key).ConfigureAwait(false);
+ }
+
+ var messages = new List(values.Length);
+
+ foreach (var value in values)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ if (value.IsNullOrEmpty)
+ {
+ continue;
+ }
+
+ try
+ {
+ var message = JsonSerializer.Deserialize(value.ToString(), this._jsonSerializerOptions.GetTypeInfo(typeof(ChatMessage))) as ChatMessage;
+ if (message is not null)
+ {
+ messages.Add(message);
+ }
+ }
+ catch (JsonException ex)
+ {
+ // Skip malformed entries rather than crashing the session [VERIFY-002]
+ this._logger?.LogWarning(ex, "ValkeyChatHistoryProvider: Skipping malformed message in conversation '{ConversationId}'.", state.ConversationId);
+ }
+ }
+
+ this._logger?.LogInformation(
+ "ValkeyChatHistoryProvider: Retrieved {Count} messages for conversation.",
+ messages.Count);
+
+ return messages;
+ }
+
+ ///
+ protected override async ValueTask StoreChatHistoryAsync(InvokedContext context, CancellationToken cancellationToken = default)
+ {
+ Throw.IfNull(context);
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var state = this._sessionState.GetOrInitializeState(context.Session);
+ var messageList = context.RequestMessages.Concat(context.ResponseMessages ?? []).ToList();
+ if (messageList.Count == 0)
+ {
+ return;
+ }
+
+ var db = this._connection.GetDatabase();
+ var key = this.BuildKey(state);
+
+ // Batch push — single round-trip [Medium-8]
+ var serialized = new ValkeyValue[messageList.Count];
+ for (int i = 0; i < messageList.Count; i++)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ serialized[i] = JsonSerializer.Serialize(messageList[i], this._jsonSerializerOptions.GetTypeInfo(typeof(ChatMessage)));
+ }
+
+ await db.ListRightPushAsync(key, serialized).ConfigureAwait(false);
+
+ // Trim to max messages if configured
+ if (this._maxMessages.HasValue)
+ {
+ await db.ListTrimAsync(key, -this._maxMessages.Value, -1).ConfigureAwait(false);
+ }
+
+ this._logger?.LogInformation(
+ "ValkeyChatHistoryProvider: Stored {Count} messages for conversation.",
+ messageList.Count);
+ }
+
+ ///
+ /// Clears all messages for the specified session's conversation.
+ ///
+ /// The session containing the conversation state.
+ /// Cancellation token.
+ /// A task representing the asynchronous operation.
+ public async Task ClearMessagesAsync(AgentSession? session, CancellationToken cancellationToken = default)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ var state = this._sessionState.GetOrInitializeState(session);
+ var db = this._connection.GetDatabase();
+ var key = this.BuildKey(state);
+ await db.KeyDeleteAsync(key).ConfigureAwait(false);
+ }
+
+ ///
+ /// Gets the count of stored messages for the specified session's conversation.
+ ///
+ /// The session containing the conversation state.
+ /// Cancellation token.
+ /// The number of stored messages.
+ public async Task GetMessageCountAsync(AgentSession? session, CancellationToken cancellationToken = default)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ var state = this._sessionState.GetOrInitializeState(session);
+ var db = this._connection.GetDatabase();
+ var key = this.BuildKey(state);
+ return await db.ListLengthAsync(key).ConfigureAwait(false);
+ }
+
+ private string BuildKey(State state) => $"{this._keyPrefix}:{state.ConversationId}";
+
+ ///
+ /// Represents the per-session state of a .
+ ///
+ public sealed class State
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The unique identifier for this conversation thread.
+ [JsonConstructor]
+ public State(string conversationId)
+ {
+ this.ConversationId = Throw.IfNullOrWhitespace(conversationId);
+ }
+
+ ///
+ /// Gets the conversation ID associated with this state.
+ ///
+ public string ConversationId { get; }
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Valkey/ValkeyChatHistoryProviderOptions.cs b/dotnet/src/Microsoft.Agents.AI.Valkey/ValkeyChatHistoryProviderOptions.cs
new file mode 100644
index 0000000000..eabe4680cf
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Valkey/ValkeyChatHistoryProviderOptions.cs
@@ -0,0 +1,56 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.Text.Json;
+using Microsoft.Extensions.AI;
+
+namespace Microsoft.Agents.AI.Valkey;
+
+///
+/// Options for configuring .
+///
+public sealed class ValkeyChatHistoryProviderOptions
+{
+ ///
+ /// Gets or sets the prefix for Valkey keys. Defaults to "chat_history".
+ ///
+ public string KeyPrefix { get; set; } = "chat_history";
+
+ ///
+ /// Gets or sets the maximum number of messages to retain per conversation.
+ /// When exceeded, oldest messages are automatically trimmed. Null means unlimited.
+ ///
+ public int? MaxMessages { get; set; }
+
+ ///
+ /// Gets or sets the maximum number of messages to retrieve from the provider.
+ /// Null means no limit.
+ ///
+ public int? MaxMessagesToRetrieve { get; set; }
+
+ ///
+ /// Gets or sets an optional key for storing state in the session's StateBag.
+ ///
+ public string? StateKey { get; set; }
+
+ ///
+ /// Gets or sets optional JSON serializer options for serializing the state of this provider.
+ ///
+ public JsonSerializerOptions? JsonSerializerOptions { get; set; }
+
+ ///
+ /// Gets or sets an optional filter for messages when retrieving from history.
+ ///
+ public Func, IEnumerable>? ProvideOutputMessageFilter { get; set; }
+
+ ///
+ /// Gets or sets an optional filter for request messages before storing.
+ ///
+ public Func, IEnumerable>? StoreInputRequestMessageFilter { get; set; }
+
+ ///
+ /// Gets or sets an optional filter for response messages before storing.
+ ///
+ public Func, IEnumerable>? StoreInputResponseMessageFilter { get; set; }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative.Mcp/DefaultMcpToolHandler.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative.Mcp/DefaultMcpToolHandler.cs
index c133b38bbd..8b0a410d8b 100644
--- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative.Mcp/DefaultMcpToolHandler.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative.Mcp/DefaultMcpToolHandler.cs
@@ -2,10 +2,10 @@
using System;
using System.Collections.Generic;
-using System.Globalization;
using System.IO;
using System.Linq;
using System.Net.Http;
+using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Threading;
@@ -39,7 +39,7 @@ public sealed class DefaultMcpToolHandler : IMcpToolHandler, IAsyncDisposable
private static readonly JsonWriterOptions s_toolListJsonWriterOptions = new() { Indented = true };
private readonly Func>? _httpClientProvider;
- private readonly Dictionary _clients = [];
+ private readonly Dictionary<(string Url, string Label, string Connection, string HeadersHash), McpClient> _clients = [];
private readonly Dictionary _ownedHttpClients = [];
private readonly SemaphoreSlim _clientLock = new(1, 1);
@@ -66,16 +66,15 @@ public sealed class DefaultMcpToolHandler : IMcpToolHandler, IAsyncDisposable
string? connectionName,
CancellationToken cancellationToken = default)
{
- // TODO: Handle connectionName and server label appropriately when Hosted scenario supports them. For now, ignore
if (IsListToolsToolName(toolName))
{
ThrowIfListToolsArgumentsSpecified(arguments);
- McpClient listToolsClient = await this.GetOrCreateClientAsync(serverUrl, serverLabel, headers, cancellationToken).ConfigureAwait(false);
+ McpClient listToolsClient = await this.GetOrCreateClientAsync(serverUrl, serverLabel, headers, connectionName, cancellationToken).ConfigureAwait(false);
IList tools = await listToolsClient.ListToolsAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
return CreateListToolsResultContent(tools.Select(tool => tool.ProtocolTool));
}
- McpClient client = await this.GetOrCreateClientAsync(serverUrl, serverLabel, headers, cancellationToken).ConfigureAwait(false);
+ McpClient client = await this.GetOrCreateClientAsync(serverUrl, serverLabel, headers, connectionName, cancellationToken).ConfigureAwait(false);
McpServerToolResultContent resultContent = new(Guid.NewGuid().ToString());
@@ -145,10 +144,11 @@ public sealed class DefaultMcpToolHandler : IMcpToolHandler, IAsyncDisposable
string serverUrl,
string? serverLabel,
IDictionary? headers,
+ string? connectionName,
CancellationToken cancellationToken)
{
- string normalizedUrl = serverUrl.Trim().ToUpperInvariant();
- string clientCacheKey = $"{normalizedUrl}|{ComputeHeadersHash(headers)}";
+ string trimmedUrl = serverUrl.Trim();
+ var clientCacheKey = BuildCacheKey(trimmedUrl, serverLabel, connectionName, headers);
await this._clientLock.WaitAsync(cancellationToken).ConfigureAwait(false);
try
@@ -158,7 +158,7 @@ public sealed class DefaultMcpToolHandler : IMcpToolHandler, IAsyncDisposable
return existingClient;
}
- McpClient newClient = await this.CreateClientAsync(serverUrl, serverLabel, headers, normalizedUrl, cancellationToken).ConfigureAwait(false);
+ McpClient newClient = await this.CreateClientAsync(trimmedUrl, serverLabel, headers, trimmedUrl, cancellationToken).ConfigureAwait(false);
this._clients[clientCacheKey] = newClient;
return newClient;
}
@@ -168,6 +168,19 @@ public sealed class DefaultMcpToolHandler : IMcpToolHandler, IAsyncDisposable
}
}
+ ///
+ /// Builds the per-client cache key as a 4-tuple of
+ /// (trimmed serverUrl, serverLabel, connectionName, headers hash). All four components
+ /// participate so that callers using different labels/connections/headers receive
+ /// distinct instances even when targeting the same URL.
+ ///
+ internal static (string Url, string Label, string Connection, string HeadersHash) BuildCacheKey(
+ string trimmedUrl,
+ string? serverLabel,
+ string? connectionName,
+ IDictionary? headers) =>
+ (trimmedUrl, serverLabel ?? string.Empty, connectionName ?? string.Empty, ComputeHeadersHash(headers));
+
private async Task CreateClientAsync(
string serverUrl,
string? serverLabel,
@@ -185,7 +198,12 @@ public sealed class DefaultMcpToolHandler : IMcpToolHandler, IAsyncDisposable
if (httpClient is null && !this._ownedHttpClients.TryGetValue(httpClientCacheKey, out httpClient))
{
- httpClient = new HttpClient();
+ // Disable cookies so handler-level state (cookie jar) cannot cross the cache-key
+ // isolation boundary established by GetOrCreateClientAsync. The actual MCP auth
+ // travels via AdditionalHeaders (set per-transport below), not session cookies.
+ // CheckCertificateRevocationList satisfies CA5399 since we're explicitly constructing the handler.
+ HttpClientHandler handler = new() { UseCookies = false, CheckCertificateRevocationList = true };
+ httpClient = new HttpClient(handler);
this._ownedHttpClients[httpClientCacheKey] = httpClient;
}
@@ -202,26 +220,50 @@ public sealed class DefaultMcpToolHandler : IMcpToolHandler, IAsyncDisposable
return await McpClient.CreateAsync(transport, cancellationToken: cancellationToken).ConfigureAwait(false);
}
- private static string ComputeHeadersHash(IDictionary? headers)
+ ///
+ /// Computes a deterministic, order-independent hash of the header set.
+ /// Header names are lower-cased for case-insensitive matching (RFC 7230 §3.2).
+ /// Header values remain case-sensitive (RFC 7235 — credentials are case-sensitive).
+ ///
+#pragma warning disable CA1308 // RFC 7230 §3.2 requires lower-cased header names for case-insensitive comparison; CA1308's uppercase preference does not apply here
+ internal static string ComputeHeadersHash(IDictionary? headers)
{
if (headers is null || headers.Count == 0)
{
return string.Empty;
}
- // Build a deterministic, sorted representation of the headers
- // Within a single process lifetime, the hashcodes are consistent.
- // This will ensure that the same set of headers always produces the same hash, regardless of order.
- SortedDictionary sorted = new(headers.ToDictionary(h => h.Key.ToUpperInvariant(), h => h.Value.ToUpperInvariant()));
- int hashCode = 17;
- foreach (KeyValuePair kvp in sorted)
+ // Sort by lower-cased key for deterministic ordering, preserving value case.
+ SortedDictionary sorted = new(StringComparer.Ordinal);
+ foreach (KeyValuePair header in headers)
{
- hashCode = (hashCode * 31) + StringComparer.OrdinalIgnoreCase.GetHashCode(kvp.Key);
- hashCode = (hashCode * 31) + StringComparer.OrdinalIgnoreCase.GetHashCode(kvp.Value);
+ sorted[header.Key.ToLowerInvariant()] = header.Value;
}
- return hashCode.ToString(CultureInfo.InvariantCulture);
+ StringBuilder payload = new();
+ foreach (KeyValuePair kvp in sorted)
+ {
+ payload.Append(kvp.Key).Append(':').Append(kvp.Value).Append('\n');
+ }
+
+ byte[] inputBytes = Encoding.UTF8.GetBytes(payload.ToString());
+#if NET5_0_OR_GREATER
+ byte[] hashBytes = SHA256.HashData(inputBytes);
+#else
+ using SHA256 sha256 = SHA256.Create();
+ byte[] hashBytes = sha256.ComputeHash(inputBytes);
+#endif
+
+ // Convert to hex string (compatible with net472/netstandard2.0)
+ StringBuilder hex = new(hashBytes.Length * 2);
+ foreach (byte b in hashBytes)
+ {
+ hex.Append(b.ToString("X2", System.Globalization.CultureInfo.InvariantCulture));
+ }
+
+ return hex.ToString();
}
+#pragma warning restore CA1308
private static void ThrowIfListToolsArgumentsSpecified(IDictionary? arguments)
{
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/InvokeFunctionToolExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/InvokeFunctionToolExecutor.cs
index 6ca429c648..08d57a6b6e 100644
--- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/InvokeFunctionToolExecutor.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/InvokeFunctionToolExecutor.cs
@@ -13,6 +13,7 @@ using Microsoft.Agents.AI.Workflows.Declarative.Kit;
using Microsoft.Agents.AI.Workflows.Declarative.PowerFx;
using Microsoft.Agents.ObjectModel;
using Microsoft.Extensions.AI;
+using Microsoft.Extensions.Logging;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Agents.AI.Workflows.Declarative.ObjectModel;
@@ -27,6 +28,13 @@ internal sealed class InvokeFunctionToolExecutor(
WorkflowFormulaState state) :
DeclarativeActionExecutor(model, state)
{
+ private const string ApprovalSnapshotStateKey = nameof(_approvalSnapshot);
+
+ ///
+ /// Snapshot of evaluated parameters at approval-request time.
+ ///
+ private ApprovalSnapshot? _approvalSnapshot;
+
///
/// Step identifiers for the function tool invocation workflow.
///
@@ -69,6 +77,10 @@ internal sealed class InvokeFunctionToolExecutor(
// If approval is required, add user input request content
if (requireApproval)
{
+ // Snapshot the evaluated parameters.
+ // If state mutates during the approval window, the approved values are used on resume.
+ this._approvalSnapshot = new ApprovalSnapshot(functionName, arguments);
+
requestMessage.Contents.Add(new ToolApprovalRequestContent(this.Id, functionCall));
}
@@ -155,6 +167,31 @@ internal sealed class InvokeFunctionToolExecutor(
// Completes the action after processing the function result.
await context.RaiseCompletionEventAsync(this.Model, cancellationToken).ConfigureAwait(false);
+
+ // Clear the approval snapshot after the action completes so a subsequent
+ // execution of the same executor instance doesn't reuse stale data.
+ this._approvalSnapshot = null;
+ await context.QueueStateUpdateAsync(ApprovalSnapshotStateKey, null, null, cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ ///
+ /// Persists the approval snapshot to workflow state so it survives checkpoint/restore cycles.
+ ///
+ protected override async ValueTask OnCheckpointingAsync(IWorkflowContext context, CancellationToken cancellationToken = default)
+ {
+ await context.QueueStateUpdateAsync(ApprovalSnapshotStateKey, this._approvalSnapshot, null, cancellationToken).ConfigureAwait(false);
+ await base.OnCheckpointingAsync(context, cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ ///
+ /// Restores the approval snapshot from workflow state after a checkpoint restore.
+ ///
+ protected override async ValueTask OnCheckpointRestoredAsync(IWorkflowContext context, CancellationToken cancellationToken = default)
+ {
+ await base.OnCheckpointRestoredAsync(context, cancellationToken).ConfigureAwait(false);
+ this._approvalSnapshot = await context.ReadStateAsync(ApprovalSnapshotStateKey, null, cancellationToken).ConfigureAwait(false);
}
///
@@ -262,7 +299,24 @@ internal sealed class InvokeFunctionToolExecutor(
private async ValueTask InvokeRegisteredFunctionAsync(CancellationToken cancellationToken)
{
- string functionName = this.GetFunctionName();
+ string functionName;
+ Dictionary? arguments;
+
+ if (this._approvalSnapshot is { } snapshot)
+ {
+ // Use the snapshot captured at approval-request time so we invoke exactly what
+ // the user approved, even if Power Fx state has mutated during the approval window.
+ functionName = snapshot.FunctionName;
+ arguments = snapshot.Arguments;
+ }
+ else
+ {
+ // Fallback for checkpoints created before approval snapshots were introduced.
+ this.Logger.LogWarning("Approval snapshot missing for '{ActionId}'; falling back to expression re-evaluation.", this.Id);
+ functionName = this.GetFunctionName();
+ arguments = this.GetArguments();
+ }
+
AIFunction? function = agentProvider.Functions?.FirstOrDefault(
f => string.Equals(f.Name, functionName, StringComparison.Ordinal));
@@ -275,8 +329,7 @@ internal sealed class InvokeFunctionToolExecutor(
};
}
- Dictionary? arguments = this.GetArguments();
- AIFunctionArguments? functionArguments = arguments is null ? null : new AIFunctionArguments(arguments);
+ AIFunctionArguments? functionArguments = arguments is null ? null : new AIFunctionArguments(arguments.NormalizePortableValues());
object? result;
try
@@ -341,4 +394,13 @@ internal sealed class InvokeFunctionToolExecutor(
return result;
}
+
+ ///
+ /// Stores the evaluated parameters at approval-request time so that
+ /// uses the values the user reviewed,
+ /// even if mutates during the approval window.
+ ///
+ internal sealed record ApprovalSnapshot(
+ string FunctionName,
+ Dictionary? Arguments);
}
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/Magentic/MagenticConstants.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/Magentic/MagenticConstants.cs
index 2ff41cc43d..b71a50390c 100644
--- a/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/Magentic/MagenticConstants.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/Magentic/MagenticConstants.cs
@@ -5,4 +5,5 @@ namespace Microsoft.Agents.AI.Workflows.Specialized.Magentic;
internal static class MagenticConstants
{
public const string MagenticTaskContextKey = nameof(MagenticTaskContextKey);
+ public const string CurrentSpeakerStateKey = nameof(CurrentSpeakerStateKey);
}
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/Magentic/MagenticOrchestrator.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/Magentic/MagenticOrchestrator.cs
index 31e80d8725..5b685dc835 100644
--- a/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/Magentic/MagenticOrchestrator.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/Magentic/MagenticOrchestrator.cs
@@ -90,6 +90,7 @@ internal class MagenticOrchestrator(AIAgent managerAgent, List team, Ta
private MagenticTaskContext? _taskContext;
private PortBinding? _planReviewPort;
+ private string? _currentSpeakerExecutorId;
protected override ProtocolBuilder ConfigureProtocol(ProtocolBuilder protocolBuilder)
{
@@ -196,15 +197,46 @@ internal class MagenticOrchestrator(AIAgent managerAgent, List team, Ta
else
{
// Subsequent turns: agent returned control, go directly to coordination (progress ledger only, no replan).
- // Capture the participant's reply into the manager-visible chat history so the progress ledger can see it.
if (messages is { Count: > 0 })
{
+ // Capture the participant's reply into the manager-visible chat history so the progress ledger can see it.
this._taskContext.ChatHistory.AddRange(messages);
+
+ // Share the reply with the other participants except the replier
+ await this.BroadcastReplyToOtherParticipantsAsync(messages, context, cancellationToken).ConfigureAwait(false);
}
await this.RunCoordinationRoundAsync(this._taskContext, context, cancellationToken).ConfigureAwait(false);
}
}
+ ///
+ /// Forwards a participant's reply to every other participant so they share the running conversation.
+ /// The messages are buffered (no is sent) - they only become context for the participant's next turn.
+ ///
+ private ValueTask BroadcastReplyToOtherParticipantsAsync(
+ List messages, IWorkflowContext context, CancellationToken cancellationToken)
+ {
+ // Without a known current speaker we cannot exclude the reply's author, so skip the broadcast
+ // rather than risk echoing the reply back to its own author. This covers the window after a
+ // checkpoint restore but before any delegation has set the current speaker.
+ if (string.IsNullOrEmpty(this._currentSpeakerExecutorId))
+ {
+ return default;
+ }
+
+ List? sendTasks = null;
+ foreach (AIAgent agent in team)
+ {
+ string executorId = AIAgentHostExecutor.IdFor(agent);
+ if (string.Equals(executorId, this._currentSpeakerExecutorId, StringComparison.Ordinal))
+ {
+ continue;
+ }
+ (sendTasks ??= []).Add(context.SendMessageAsync(messages, executorId, cancellationToken).AsTask());
+ }
+ return sendTasks is null ? default : new ValueTask(Task.WhenAll(sendTasks));
+ }
+
private ChatMessage? _fullTaskLedgerMessage;
private ValueTask DelegateToTeamAsync(MagenticTaskContext taskContext, IWorkflowContext context, CancellationToken cancellationToken)
{
@@ -287,15 +319,18 @@ internal class MagenticOrchestrator(AIAgent managerAgent, List team, Ta
return;
}
+ string nextExecutorId = AIAgentHostExecutor.IdFor(nextAgent);
+
if (!string.IsNullOrWhiteSpace(taskContext.ProgressLedger.InstructionOrQuestion))
{
ChatMessage instruction = new(ChatRole.Assistant, taskContext.ProgressLedger.InstructionOrQuestion);
taskContext.ChatHistory.Add(instruction);
- await context.SendMessageAsync(instruction, cancellationToken).ConfigureAwait(false);
+ // Target the instruction at the chosen speaker only.
+ await context.SendMessageAsync(instruction, nextExecutorId, cancellationToken).ConfigureAwait(false);
}
- string nextExecutorId = AIAgentHostExecutor.IdFor(nextAgent);
+ this._currentSpeakerExecutorId = nextExecutorId;
await context.SendMessageAsync(new TurnToken(taskContext.EmitUpdateEvents), nextExecutorId, cancellationToken).ConfigureAwait(false);
}
@@ -303,6 +338,7 @@ internal class MagenticOrchestrator(AIAgent managerAgent, List team, Ta
{
bool wasStalled = taskContext.IsStalled;
taskContext.Reset();
+ this._currentSpeakerExecutorId = null;
await context.SendMessageAsync(new ResetChatSignal(), cancellationToken: cancellationToken).ConfigureAwait(false);
await this.UpdatePlanAndDelegateAsync(taskContext, context, cancellationToken, replanAfterStall: wasStalled).ConfigureAwait(false);
@@ -313,9 +349,9 @@ internal class MagenticOrchestrator(AIAgent managerAgent, List team, Ta
List messages = [await this._manager.PrepareFinalAnswerAsync(taskContext, context, cancellationToken).ConfigureAwait(false)];
await context.YieldOutputAsync(messages, cancellationToken).ConfigureAwait(false);
taskContext.IsTerminated = true;
+ this._currentSpeakerExecutorId = null;
}
- private const string CurrentTurnEmitUpdateEventsKey = nameof(CurrentTurnEmitUpdateEventsKey);
protected internal override async ValueTask OnCheckpointingAsync(IWorkflowContext context, CancellationToken cancellationToken = default)
{
Task contextStateTask = this._taskContext == null
@@ -325,14 +361,21 @@ internal class MagenticOrchestrator(AIAgent managerAgent, List team, Ta
cancellationToken: cancellationToken)
.AsTask();
+ Task currentSpeakerTask = context.QueueStateUpdateAsync(MagenticConstants.CurrentSpeakerStateKey,
+ this._currentSpeakerExecutorId,
+ cancellationToken: cancellationToken)
+ .AsTask();
+
await Task.WhenAll(base.OnCheckpointingAsync(context, cancellationToken).AsTask(),
- contextStateTask).ConfigureAwait(false);
+ contextStateTask,
+ currentSpeakerTask).ConfigureAwait(false);
}
protected internal override async ValueTask OnCheckpointRestoredAsync(IWorkflowContext context, CancellationToken cancellationToken = default)
{
- await Task.WhenAll(base.OnCheckpointRestoredAsync(context, cancellationToken).AsTask(), LoadContextStateAsync())
- .ConfigureAwait(false);
+ await Task.WhenAll(base.OnCheckpointRestoredAsync(context, cancellationToken).AsTask(),
+ LoadContextStateAsync(),
+ LoadCurrentSpeakerAsync()).ConfigureAwait(false);
async Task LoadContextStateAsync()
{
@@ -344,5 +387,11 @@ internal class MagenticOrchestrator(AIAgent managerAgent, List team, Ta
this._taskContext = new MagenticTaskContext(state, team, limits, []);
}
}
+
+ async Task LoadCurrentSpeakerAsync()
+ {
+ this._currentSpeakerExecutorId = await context.ReadStateAsync(MagenticConstants.CurrentSpeakerStateKey, cancellationToken: cancellationToken)
+ .ConfigureAwait(false);
+ }
}
}
diff --git a/dotnet/src/Microsoft.Agents.AI/Harness/Loop/AIJudgeLoopEvaluator.cs b/dotnet/src/Microsoft.Agents.AI/Harness/Loop/AIJudgeLoopEvaluator.cs
new file mode 100644
index 0000000000..b482d6c93e
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI/Harness/Loop/AIJudgeLoopEvaluator.cs
@@ -0,0 +1,201 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.AI;
+using Microsoft.Shared.DiagnosticIds;
+using Microsoft.Shared.Diagnostics;
+
+namespace Microsoft.Agents.AI;
+
+///
+/// A that uses a separate judge chat client to decide whether the user's original request
+/// has been fully addressed, continuing the loop (with the judge's gap analysis as feedback) while the answer is "no".
+///
+///
+///
+/// After each iteration the judge is queried directly (without any agent tools, session, or middleware) with the
+/// original request and the agent's latest response, and asked for a structured . If the
+/// judge client does not honor structured output, the verdict falls back to parsing the raw text for the
+/// non-overlapping / markers (with
+/// winning, so the loop keeps running, when the verdict is ambiguous or absent).
+///
+///
+/// When the request is not yet answered, the evaluator returns feedback built from
+/// with the judge's gap analysis substituted for
+/// . How that feedback is delivered to the agent (and whether the session is
+/// reset) is decided by the that consumes this evaluator.
+///
+///
+/// The judge instructions act as a template: any occurrence of is replaced with the
+/// rendered (or removed when no criteria are supplied), letting
+/// callers add bespoke standards the response must satisfy.
+///
+///
+/// LLM-judged loops are costly and probabilistic, so consider setting a stricter
+/// on the owning .
+///
+///
+[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
+public sealed class AIJudgeLoopEvaluator : LoopEvaluator
+{
+ /// The default system instructions used to prompt the judge.
+ ///
+ /// Acts as a template: the trailing is replaced with the rendered
+ /// (or removed when none are supplied).
+ ///
+ public const string DefaultInstructions =
+ "You are an evaluator. You are given a user's original request and an agent's latest response. " +
+ "Decide whether the agent has fully addressed the original request. " +
+ "Set 'answered' to true if the request has been fully addressed, or false if more work is still required. " +
+ "When 'answered' is false, use 'gapAnalysis' to explain what is still missing or what work remains. " +
+ "If you cannot return structured output, reply with " + DoneVerdictMarker + " when the request has been fully " +
+ "addressed, or " + MoreVerdictMarker + " when more work is still required." +
+ CriteriaPlaceholder;
+
+ ///
+ /// The verdict marker the judge is asked to emit (for clients that do not honor structured output) when the
+ /// original request has been fully addressed.
+ ///
+ ///
+ /// and are deliberately non-overlapping (neither is
+ /// a substring of the other), so the text fallback cannot misclassify one verdict as the other. When the marker is
+ /// ambiguous or absent, wins so the loop keeps running rather than stopping on an
+ /// incomplete answer.
+ ///
+ public const string DoneVerdictMarker = "VERDICT: DONE";
+
+ ///
+ /// The verdict marker the judge is asked to emit (for clients that do not honor structured output) when more work
+ /// is still required. Takes precedence over when both (or neither) are present.
+ ///
+ public const string MoreVerdictMarker = "VERDICT: MORE";
+
+ ///
+ /// The placeholder token within (or a custom
+ /// ) that is replaced with the rendered
+ /// . When no criteria are supplied, the placeholder is removed.
+ ///
+ public const string CriteriaPlaceholder = "{criteria}";
+
+ ///
+ /// The placeholder token within (or a custom
+ /// ) that is replaced with the judge's gap analysis.
+ ///
+ public const string GapAnalysisPlaceholder = "{gap_analysis}";
+
+ /// The default template used to build the feedback produced when the request is not yet answered.
+ public const string DefaultFeedbackMessageTemplate =
+ "Your previous response did not fully address the original request. " +
+ "The following is still missing or incomplete: " + GapAnalysisPlaceholder + " " +
+ "Please continue and fully address the original request.";
+
+ /// The value substituted for the gap analysis when the judge did not provide one.
+ private const string UnknownGapAnalysis = "";
+
+ private readonly IChatClient _judgeClient;
+ private readonly string _instructions;
+ private readonly string _feedbackMessageTemplate;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The chat client used to judge whether the original request was answered.
+ /// Optional configuration for the judge. When , defaults are used.
+ /// is .
+ public AIJudgeLoopEvaluator(IChatClient judgeClient, AIJudgeLoopEvaluatorOptions? options = null)
+ {
+ this._judgeClient = Throw.IfNull(judgeClient);
+ this._instructions = (options?.Instructions ?? DefaultInstructions)
+ .Replace(CriteriaPlaceholder, RenderCriteria(options?.Criteria));
+ this._feedbackMessageTemplate = options?.FeedbackMessageTemplate ?? DefaultFeedbackMessageTemplate;
+ }
+
+ ///
+ public override async ValueTask EvaluateAsync(LoopContext context, CancellationToken cancellationToken = default)
+ {
+ _ = Throw.IfNull(context);
+
+ // Build the judge's user message from AIContent so non-text request content (images, data, etc.) is
+ // preserved rather than flattened to text. The original request's contents are framed between header
+ // text segments, followed by the agent's latest response text.
+ var userContents = new List
+ {
+ new TextContent("# Has the original request been fully addressed?\n\n## Original request:\n"),
+ };
+ foreach (ChatMessage message in context.InitialMessages)
+ {
+ userContents.AddRange(message.Contents);
+ }
+
+ userContents.Add(new TextContent($"\n\n## Agent's latest response:\n{context.LastResponse.Text}"));
+
+ List judgeMessages =
+ [
+ new ChatMessage(ChatRole.System, this._instructions),
+ new ChatMessage(ChatRole.User, userContents),
+ ];
+
+ bool answered;
+ string gapAnalysis = UnknownGapAnalysis;
+ ChatResponse response = await this._judgeClient
+ .GetResponseAsync(judgeMessages, LoopJsonContext.Default.Options, cancellationToken: cancellationToken)
+ .ConfigureAwait(false);
+
+ if (response.TryGetResult(out JudgeVerdict? verdict) && verdict is not null)
+ {
+ answered = verdict.Answered;
+ if (!string.IsNullOrWhiteSpace(verdict.GapAnalysis))
+ {
+ gapAnalysis = verdict.GapAnalysis;
+ }
+ }
+ else
+ {
+ // Fallback for clients that do not honor structured output: look for the explicit, non-overlapping verdict
+ // markers. MoreVerdictMarker wins so an ambiguous or marker-less reply keeps looping rather than stopping
+ // on an incomplete answer.
+ string text = response.Text.ToUpperInvariant();
+ answered = !text.Contains(MoreVerdictMarker) && text.Contains(DoneVerdictMarker);
+ }
+
+ // The request is answered: stop looping.
+ if (answered)
+ {
+ return LoopEvaluation.Stop();
+ }
+
+ // Not yet answered: continue, providing feedback describing what is still missing.
+ string feedback = this._feedbackMessageTemplate.Replace(GapAnalysisPlaceholder, gapAnalysis);
+ return LoopEvaluation.Continue(feedback);
+ }
+
+ ///
+ /// Renders the supplied into a bullet block appended at ,
+ /// or an empty string when no non-blank criteria are supplied.
+ ///
+ private static string RenderCriteria(IEnumerable? criteria)
+ {
+ if (criteria is null)
+ {
+ return string.Empty;
+ }
+
+ var builder = new StringBuilder();
+ foreach (string criterion in criteria)
+ {
+ if (!string.IsNullOrWhiteSpace(criterion))
+ {
+ builder.Append("\n- ").Append(criterion);
+ }
+ }
+
+ return builder.Length == 0
+ ? string.Empty
+ : "\n\nThe response must satisfy all of the following criteria:" + builder;
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI/Harness/Loop/AIJudgeLoopEvaluatorOptions.cs b/dotnet/src/Microsoft.Agents.AI/Harness/Loop/AIJudgeLoopEvaluatorOptions.cs
new file mode 100644
index 0000000000..73285a924c
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI/Harness/Loop/AIJudgeLoopEvaluatorOptions.cs
@@ -0,0 +1,48 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using Microsoft.Shared.DiagnosticIds;
+
+namespace Microsoft.Agents.AI;
+
+///
+/// Provides configuration options for .
+///
+[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
+public sealed class AIJudgeLoopEvaluatorOptions
+{
+ ///
+ /// Gets or sets the system instructions used to prompt the judge, or to use
+ /// .
+ ///
+ ///
+ /// Any occurrence of in the instructions is replaced with
+ /// the rendered (or removed when no criteria are supplied). Instructions that omit the
+ /// placeholder do not receive the criteria.
+ ///
+ public string? Instructions { get; set; }
+
+ ///
+ /// Gets or sets an optional list of additional criteria the agent's response must satisfy, evaluated by the judge
+ /// alongside the original request.
+ ///
+ ///
+ /// When supplied, the criteria are rendered into the judge instructions wherever
+ /// appears (including in
+ /// ). When or empty, the placeholder is
+ /// removed and no criteria are added.
+ ///
+ public IEnumerable? Criteria { get; set; }
+
+ ///
+ /// Gets or sets the template used to build the feedback produced when the judge decides the original request was
+ /// not fully addressed, or to use
+ /// .
+ ///
+ ///
+ /// Any occurrence of in the template is replaced with the
+ /// judge's gap analysis (or a placeholder when none is available).
+ ///
+ public string? FeedbackMessageTemplate { get; set; }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI/Harness/Loop/CompletionMarkerLoopEvaluator.cs b/dotnet/src/Microsoft.Agents.AI/Harness/Loop/CompletionMarkerLoopEvaluator.cs
new file mode 100644
index 0000000000..cd2d7c8aa6
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI/Harness/Loop/CompletionMarkerLoopEvaluator.cs
@@ -0,0 +1,78 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Diagnostics.CodeAnalysis;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Shared.DiagnosticIds;
+using Microsoft.Shared.Diagnostics;
+
+namespace Microsoft.Agents.AI;
+
+///
+/// A that stops the loop once a configured marker string appears in the agent's latest
+/// response, and otherwise continues with feedback asking the agent to keep working and to emit the marker when done.
+///
+///
+/// The feedback produced while the marker is absent is built from a template (see
+/// ) with the configured marker substituted
+/// for , and the agent's latest response text substituted for
+/// . How that feedback is delivered to the agent (and whether the session
+/// is reset) is decided by the that consumes this evaluator.
+///
+[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
+public sealed class CompletionMarkerLoopEvaluator : LoopEvaluator
+{
+ ///
+ /// The placeholder token within (or a custom
+ /// ) that is replaced with the
+ /// configured completion marker.
+ ///
+ public const string CompletionMarkerPlaceholder = "{completion_marker}";
+
+ ///
+ /// The placeholder token within a custom
+ /// that is replaced with the text of the agent's latest response. This is substituted on each evaluation, so it lets
+ /// the feedback echo back what the agent previously produced — useful when the consuming
+ /// uses , where the agent would
+ /// otherwise have no record of its prior output.
+ ///
+ public const string LastResponsePlaceholder = "{last_response}";
+
+ /// The default template used to build the feedback produced while the completion marker is absent.
+ public const string DefaultFeedbackMessageTemplate =
+ "Continue working on the request. When you have fully completed the task, end your response with the marker '" +
+ CompletionMarkerPlaceholder + "' to indicate completion.";
+
+ private readonly string _completionMarker;
+ private readonly string _feedbackMessageTemplate;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The marker string that stops the loop once it appears in the agent's latest response text.
+ /// Optional configuration for the feedback message. When , defaults are used.
+ /// is , empty, or whitespace.
+ public CompletionMarkerLoopEvaluator(string completionMarker, CompletionMarkerLoopEvaluatorOptions? options = null)
+ {
+ this._completionMarker = Throw.IfNullOrWhitespace(completionMarker);
+
+ // The completion marker is fixed, so substitute it once here. The optional {last_response} placeholder depends
+ // on the per-iteration response text, so it is substituted later in EvaluateAsync.
+ this._feedbackMessageTemplate = (options?.FeedbackMessageTemplate ?? DefaultFeedbackMessageTemplate)
+ .Replace(CompletionMarkerPlaceholder, this._completionMarker);
+ }
+
+ ///
+ public override ValueTask EvaluateAsync(LoopContext context, CancellationToken cancellationToken = default)
+ {
+ _ = Throw.IfNull(context);
+
+ if (context.LastResponse.Text.Contains(this._completionMarker))
+ {
+ return new ValueTask(LoopEvaluation.Stop());
+ }
+
+ string feedback = this._feedbackMessageTemplate.Replace(LastResponsePlaceholder, context.LastResponse.Text);
+ return new ValueTask(LoopEvaluation.Continue(feedback));
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI/Harness/Loop/CompletionMarkerLoopEvaluatorOptions.cs b/dotnet/src/Microsoft.Agents.AI/Harness/Loop/CompletionMarkerLoopEvaluatorOptions.cs
new file mode 100644
index 0000000000..de3c394c48
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI/Harness/Loop/CompletionMarkerLoopEvaluatorOptions.cs
@@ -0,0 +1,26 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Diagnostics.CodeAnalysis;
+using Microsoft.Shared.DiagnosticIds;
+
+namespace Microsoft.Agents.AI;
+
+///
+/// Provides configuration options for .
+///
+[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
+public sealed class CompletionMarkerLoopEvaluatorOptions
+{
+ ///
+ /// Gets or sets the template used to build the feedback produced when the completion marker has not yet appeared,
+ /// or to use .
+ ///
+ ///
+ /// Any occurrence of in the template is
+ /// replaced with the configured completion marker. Any occurrence of
+ /// is replaced, on each evaluation, with the
+ /// text of the agent's latest response — useful for echoing the agent's prior output back to it when the consuming
+ /// is used with a fresh context per iteration.
+ ///
+ public string? FeedbackMessageTemplate { get; set; }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI/Harness/Loop/DelegateLoopEvaluator.cs b/dotnet/src/Microsoft.Agents.AI/Harness/Loop/DelegateLoopEvaluator.cs
new file mode 100644
index 0000000000..9c41b1a11c
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI/Harness/Loop/DelegateLoopEvaluator.cs
@@ -0,0 +1,40 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Shared.DiagnosticIds;
+using Microsoft.Shared.Diagnostics;
+
+namespace Microsoft.Agents.AI;
+
+///
+/// A that delegates the re-invocation decision and feedback to a user-supplied callback.
+///
+///
+/// This is the most flexible evaluator: the supplied delegate receives the full and returns
+/// a , so it can decide both whether to continue and what feedback (if any) to provide.
+///
+[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
+public sealed class DelegateLoopEvaluator : LoopEvaluator
+{
+ private readonly Func> _evaluate;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// A callback that decides whether to re-invoke the agent and what feedback to provide.
+ /// is .
+ public DelegateLoopEvaluator(Func> evaluate)
+ {
+ this._evaluate = Throw.IfNull(evaluate);
+ }
+
+ ///
+ public override ValueTask EvaluateAsync(LoopContext context, CancellationToken cancellationToken = default)
+ {
+ _ = Throw.IfNull(context);
+ return this._evaluate(context, cancellationToken);
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI/Harness/Loop/JudgeVerdict.cs b/dotnet/src/Microsoft.Agents.AI/Harness/Loop/JudgeVerdict.cs
new file mode 100644
index 0000000000..19d802e2fc
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI/Harness/Loop/JudgeVerdict.cs
@@ -0,0 +1,26 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
+using Microsoft.Shared.DiagnosticIds;
+
+namespace Microsoft.Agents.AI;
+
+///
+/// Represents the structured verdict returned by the judge chat client used by .
+///
+[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
+internal sealed class JudgeVerdict
+{
+ ///
+ /// Gets or sets a value indicating whether the agent has fully addressed the user's original request.
+ ///
+ [Description("True if the agent has fully addressed the original request, otherwise false.")]
+ public bool Answered { get; set; }
+
+ ///
+ /// Gets or sets an explanation of what is still missing when the request has not been fully addressed.
+ ///
+ [Description("When 'answered' is false, explain what is still missing or what work remains to fully address the original request.")]
+ public string GapAnalysis { get; set; } = string.Empty;
+}
diff --git a/dotnet/src/Microsoft.Agents.AI/Harness/Loop/LoopAgent.cs b/dotnet/src/Microsoft.Agents.AI/Harness/Loop/LoopAgent.cs
new file mode 100644
index 0000000000..c92de6a331
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI/Harness/Loop/LoopAgent.cs
@@ -0,0 +1,548 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Text;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.AI;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Shared.DiagnosticIds;
+using Microsoft.Shared.Diagnostics;
+
+namespace Microsoft.Agents.AI;
+
+///
+/// A that re-invokes the wrapped agent in a loop until the configured
+/// set decides to stop.
+///
+///
+///
+/// After each run of the wrapped agent, the configured evaluators are asked whether to re-invoke the agent and what
+/// feedback to carry forward. This enables patterns such as iterative refinement, working through a task list, or
+/// judging whether the original request was answered. Out-of-the-box evaluators include
+/// , , and
+/// .
+///
+///
+/// When multiple evaluators are supplied they are evaluated in order after each iteration. The first evaluator that
+/// asks to re-invoke wins: its feedback drives the next iteration and the remaining evaluators are not evaluated. The
+/// loop stops only when every evaluator asks to stop. Consequently, evaluator order is priority order and
+/// means "this evaluator does not request continuation" rather than a veto that
+/// terminates the loop; place stop-only guards accordingly.
+///
+///
+/// The caller's initial messages are sent to the wrapped agent exactly once. By default (when
+/// is ) the loop reuses a single session
+/// and sends only the winning evaluator's feedback as the next input, letting the agent continue from session history.
+/// When is , each re-invocation restarts
+/// from the original input messages plus an aggregated feedback log, and the session is reset for each iteration: a
+/// loop-owned session is created anew, while a caller-supplied session is restored from a snapshot taken at the start
+/// of the run (so the wrapped agent must support session serialization). An evaluator may instead supply the exact next
+/// messages via , bypassing this construction.
+///
+///
+/// The loop is bounded by a global safety cap () regardless of the
+/// evaluators. If an iteration produces a pending tool-approval request, the loop stops and returns that response to
+/// the caller rather than attempting to resolve the approval automatically.
+///
+///
+/// A non-streaming run returns, by default, a single that aggregates the full transcript
+/// in order: the on-behalf-of messages the loop injected for each re-invocation followed by that iteration's response
+/// messages. The caller's original input messages are not echoed. Set
+/// to instead return only the final iteration's
+/// response. A streaming run always yields every iteration's updates, emitting the injected on-behalf-of messages as
+/// updates before each re-invocation. The injected messages can be attributed with
+/// , or omitted from the surfaced output entirely with
+/// .
+///
+///
+[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
+public sealed class LoopAgent : DelegatingAIAgent
+{
+ /// The default value used for when none is specified.
+ public const int DefaultMaxIterations = 10;
+
+ private readonly IReadOnlyList _evaluators;
+ private readonly int _maxIterations;
+ private readonly bool _freshContextPerIteration;
+ private readonly string? _onBehalfOfAuthorName;
+ private readonly bool _excludeOnBehalfOfMessages;
+ private readonly bool _nonStreamingReturnsLastResponseOnly;
+ private readonly System.Func? _sessionCreatedCallback;
+ private readonly ILogger _logger;
+
+ ///
+ /// Initializes a new instance of the class with a single evaluator.
+ ///
+ /// The underlying agent to invoke in a loop.
+ /// The that decides whether to re-invoke the agent.
+ /// Optional configuration for the loop. When , defaults are used.
+ /// Optional factory used to create the loop's logger.
+ /// or is .
+ /// is less than 1.
+ public LoopAgent(AIAgent innerAgent, LoopEvaluator evaluator, LoopAgentOptions? options = null, ILoggerFactory? loggerFactory = null)
+ : this(innerAgent, [Throw.IfNull(evaluator)], options, loggerFactory)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class with one or more evaluators.
+ ///
+ /// The underlying agent to invoke in a loop.
+ ///
+ /// The ordered set of that decide whether to re-invoke the agent. They are evaluated in
+ /// order after each iteration and the first that asks to re-invoke wins.
+ ///
+ /// Optional configuration for the loop. When , defaults are used.
+ /// Optional factory used to create the loop's logger.
+ /// or is , or contains a element.
+ /// is empty.
+ /// is less than 1.
+ public LoopAgent(AIAgent innerAgent, IEnumerable evaluators, LoopAgentOptions? options = null, ILoggerFactory? loggerFactory = null)
+ : base(innerAgent)
+ {
+ _ = Throw.IfNull(evaluators);
+ LoopEvaluator[] evaluatorArray = evaluators.ToArray();
+ if (evaluatorArray.Length == 0)
+ {
+ throw new System.ArgumentException("At least one evaluator must be supplied.", nameof(evaluators));
+ }
+
+ foreach (LoopEvaluator item in evaluatorArray)
+ {
+ _ = Throw.IfNull(item, nameof(evaluators));
+ }
+
+ this._evaluators = evaluatorArray;
+
+ this._maxIterations = Throw.IfLessThan(options?.MaxIterations ?? DefaultMaxIterations, 1);
+ this._freshContextPerIteration = options?.FreshContextPerIteration ?? false;
+ this._onBehalfOfAuthorName = options?.OnBehalfOfAuthorName;
+ this._excludeOnBehalfOfMessages = options?.ExcludeOnBehalfOfMessages ?? false;
+ this._nonStreamingReturnsLastResponseOnly = options?.NonStreamingReturnsLastResponseOnly ?? false;
+ this._sessionCreatedCallback = options?.SessionCreatedCallback;
+ this._logger = (loggerFactory ?? NullLoggerFactory.Instance).CreateLogger();
+ }
+
+ ///
+ protected override async Task RunCoreAsync(
+ IEnumerable messages,
+ AgentSession? session = null,
+ AgentRunOptions? options = null,
+ CancellationToken cancellationToken = default)
+ {
+ _ = Throw.IfNull(messages);
+
+ // Capture the caller's initial messages (sent once) and ensure the loop always runs against a session.
+ IReadOnlyList initialMessages = messages as IReadOnlyList ?? messages.ToList();
+ bool sessionProvidedByCaller = session is not null;
+ if (session is null)
+ {
+ session = await this.InnerAgent.CreateSessionAsync(cancellationToken).ConfigureAwait(false);
+ await this.NotifyNewSessionAsync(session, cancellationToken).ConfigureAwait(false);
+ }
+
+ // When a fresh context is requested over a caller-supplied session, snapshot the pristine session up front so
+ // each re-invocation can restart from a fresh clone (see CreateFreshIterationSessionAsync). Taken before the
+ // first iteration mutates the session.
+ JsonElement? initialSessionSnapshot = this._freshContextPerIteration && sessionProvidedByCaller
+ ? await this.InnerAgent.SerializeSessionAsync(session, cancellationToken: cancellationToken).ConfigureAwait(false)
+ : null;
+
+ LoopContext? context = null;
+ List feedbackLog = [];
+ IEnumerable currentMessages = initialMessages;
+ int iteration = 0;
+
+ // Aggregates the full transcript across iterations: each iteration's surfaced on-behalf-of input messages
+ // followed by that iteration's response messages. Unused when only the final response is returned.
+ List transcript = [];
+
+ // The loop-synthesized on-behalf-of messages that drive the current iteration (none for the first iteration).
+ IReadOnlyList currentSurfaced = [];
+
+ while (true)
+ {
+ // Run the wrapped agent using the context's session once it exists (it may have been replaced for a fresh
+ // context), otherwise the resolved session for the first run.
+ AgentSession activeSession = context?.Session ?? session;
+ AgentResponse response = await this.InnerAgent.RunAsync(currentMessages, activeSession, options, cancellationToken).ConfigureAwait(false);
+ iteration++;
+
+ // Record this iteration's on-behalf-of input (before the response it elicited) and the response itself.
+ transcript.AddRange(currentSurfaced);
+ transcript.AddRange(response.Messages);
+
+ // Create the context after the first run (so LastResponse is never null) and reuse it thereafter.
+ // Expose the feedback log as a read-only wrapper so evaluators cannot downcast and mutate it; the
+ // wrapper still reflects entries appended by the loop.
+ context ??= new LoopContext(this.InnerAgent, session, initialMessages, response, options) { Feedback = feedbackLog.AsReadOnly() };
+
+ context.Iteration = iteration;
+ context.LastResponse = response;
+
+ // Stop and surface the response when the agent is waiting for a tool approval.
+ if (HasPendingApprovalRequests(response))
+ {
+ return this.BuildResult(response, transcript);
+ }
+
+ // Enforce the global safety cap regardless of what the evaluators want.
+ if (iteration >= this._maxIterations)
+ {
+ this.LogMaxIterationsReached(iteration);
+ return this.BuildResult(response, transcript);
+ }
+
+ // Ask the evaluators whether to continue; stop when none of them request a re-invocation.
+ LoopNextStep step = await this.EvaluateAndBuildNextAsync(context, feedbackLog, initialSessionSnapshot, cancellationToken).ConfigureAwait(false);
+ if (!step.ShouldContinue)
+ {
+ return this.BuildResult(response, transcript);
+ }
+
+ currentMessages = step.Messages;
+ currentSurfaced = step.SurfacedMessages;
+ }
+ }
+
+ ///
+ protected override async IAsyncEnumerable RunCoreStreamingAsync(
+ IEnumerable messages,
+ AgentSession? session = null,
+ AgentRunOptions? options = null,
+ [EnumeratorCancellation] CancellationToken cancellationToken = default)
+ {
+ _ = Throw.IfNull(messages);
+
+ // Capture the caller's initial messages (sent once) and ensure the loop always runs against a session.
+ IReadOnlyList initialMessages = messages as IReadOnlyList ?? messages.ToList();
+ bool sessionProvidedByCaller = session is not null;
+ if (session is null)
+ {
+ session = await this.InnerAgent.CreateSessionAsync(cancellationToken).ConfigureAwait(false);
+ await this.NotifyNewSessionAsync(session, cancellationToken).ConfigureAwait(false);
+ }
+
+ // When a fresh context is requested over a caller-supplied session, snapshot the pristine session up front so
+ // each re-invocation can restart from a fresh clone (see CreateFreshIterationSessionAsync). Taken before the
+ // first iteration mutates the session.
+ JsonElement? initialSessionSnapshot = this._freshContextPerIteration && sessionProvidedByCaller
+ ? await this.InnerAgent.SerializeSessionAsync(session, cancellationToken: cancellationToken).ConfigureAwait(false)
+ : null;
+
+ LoopContext? context = null;
+ List feedbackLog = [];
+ IEnumerable currentMessages = initialMessages;
+ int iteration = 0;
+
+ // The loop-synthesized on-behalf-of messages that drive the current iteration (none for the first iteration).
+ IReadOnlyList currentSurfaced = [];
+
+ while (true)
+ {
+ // Stream this iteration's updates to the caller while collecting them so the iteration's full
+ // response can be aggregated for evaluation (true per-iteration streaming). Uses the context's
+ // session once it exists (it may have been replaced for a fresh context), otherwise the resolved session.
+ AgentSession activeSession = context?.Session ?? session;
+ List updates = [];
+
+ // The on-behalf-of messages that drive this iteration are surfaced before the response they elicit (none
+ // for the first iteration). They are flushed lazily on the first inner update so they can be stamped with
+ // that update's ResponseId/AgentId, keeping them grouped with the iteration for downstream mergers.
+ bool surfacedPending = currentSurfaced.Count > 0;
+ await foreach (var update in this.InnerAgent.RunStreamingAsync(currentMessages, activeSession, options, cancellationToken).ConfigureAwait(false))
+ {
+ if (surfacedPending)
+ {
+ foreach (ChatMessage surfaced in currentSurfaced)
+ {
+ yield return CreateOnBehalfOfUpdate(surfaced, update.ResponseId);
+ }
+
+ surfacedPending = false;
+ }
+
+ updates.Add(update);
+ yield return update;
+ }
+
+ // The inner agent produced no updates this iteration; surface the on-behalf-of messages anyway. Since there
+ // is no iteration response to inherit from, generate a ResponseId so they still group together downstream.
+ if (surfacedPending)
+ {
+ string fallbackResponseId = System.Guid.NewGuid().ToString("N");
+ foreach (ChatMessage surfaced in currentSurfaced)
+ {
+ yield return CreateOnBehalfOfUpdate(surfaced, fallbackResponseId);
+ }
+ }
+
+ // Aggregate this iteration's updates and record the result on the context.
+ iteration++;
+ AgentResponse response = updates.ToAgentResponse();
+
+ // Create the context after the first run (so LastResponse is never null) and reuse it thereafter.
+ // Expose the feedback log as a read-only wrapper so evaluators cannot downcast and mutate it; the
+ // wrapper still reflects entries appended by the loop.
+ context ??= new LoopContext(this.InnerAgent, session, initialMessages, response, options) { Feedback = feedbackLog.AsReadOnly() };
+
+ context.Iteration = iteration;
+ context.LastResponse = response;
+
+ // Stop when the agent is waiting for a tool approval.
+ if (HasPendingApprovalRequests(response))
+ {
+ yield break;
+ }
+
+ // Enforce the global safety cap regardless of what the evaluators want.
+ if (iteration >= this._maxIterations)
+ {
+ this.LogMaxIterationsReached(iteration);
+ yield break;
+ }
+
+ // Ask the evaluators whether to continue; stop when none of them request a re-invocation.
+ LoopNextStep step = await this.EvaluateAndBuildNextAsync(context, feedbackLog, initialSessionSnapshot, cancellationToken).ConfigureAwait(false);
+ if (!step.ShouldContinue)
+ {
+ yield break;
+ }
+
+ currentMessages = step.Messages;
+ currentSurfaced = step.SurfacedMessages;
+ }
+ }
+
+ ///
+ /// Evaluates the evaluators in order and, for the first one that requests a re-invocation, builds the next input
+ /// according to the loop's feedback and fresh-context policy.
+ ///
+ private async ValueTask EvaluateAndBuildNextAsync(LoopContext context, List feedbackLog, JsonElement? initialSessionSnapshot, CancellationToken cancellationToken)
+ {
+ // Evaluate in order; the first evaluator that requests a re-invocation wins.
+ LoopEvaluation? winner = null;
+ foreach (LoopEvaluator evaluator in this._evaluators)
+ {
+ LoopEvaluation evaluation = await evaluator.EvaluateAsync(context, cancellationToken).ConfigureAwait(false);
+ if (evaluation.ShouldReinvoke)
+ {
+ winner = evaluation;
+ break;
+ }
+ }
+
+ // Every evaluator asked to stop.
+ if (winner is null)
+ {
+ return LoopNextStep.Stop();
+ }
+
+ // Start the next iteration from a fresh session when a fresh context is requested, so no prior conversation
+ // history leaks across iterations. This applies regardless of how the next input is built (feedback or explicit
+ // ContinueWithMessages): a caller-supplied session is cloned from the pristine start-of-run snapshot; a
+ // loop-owned session is created anew.
+ if (this._freshContextPerIteration)
+ {
+ context.Session = await this.CreateFreshIterationSessionAsync(context, initialSessionSnapshot, cancellationToken).ConfigureAwait(false);
+ }
+
+ // Record one feedback entry for this re-invoked iteration (null when none, including ContinueWithMessages
+ // iterations which carry no feedback string) so the log stays aligned: one entry per re-invoked iteration, with
+ // the last element always corresponding to the latest re-invoked iteration. Continue() normalizes whitespace to null.
+ feedbackLog.Add(winner.Feedback);
+
+ // An evaluator supplied explicit messages: send them verbatim, bypassing feedback/message construction (the
+ // session is still reset above when a fresh context is requested). These are surfaced to the caller as-is (the
+ // evaluator owns them, including any author name).
+ if (winner.Messages is not null)
+ {
+ return LoopNextStep.Continue(winner.Messages, this.Surfaced(winner.Messages));
+ }
+
+ (List messages, List surfaced) = this.BuildNextMessages(context, feedbackLog);
+ return LoopNextStep.Continue(messages, this.Surfaced(surfaced));
+ }
+
+ ///
+ /// Returns the messages to surface to the caller, honoring .
+ ///
+ private IReadOnlyList Surfaced(IReadOnlyList surfaced)
+ => this._excludeOnBehalfOfMessages ? [] : surfaced;
+
+ ///
+ /// Creates a streaming update for a surfaced on-behalf-of message, inheriting the driven iteration's
+ /// so downstream mergers group it with that iteration, and ensuring a unique
+ /// non-null . The is left
+ /// unset because the message is synthesized by the loop, not produced by the wrapped agent.
+ ///
+ private static AgentResponseUpdate CreateOnBehalfOfUpdate(ChatMessage message, string? responseId)
+ => new(message.Role, message.Contents)
+ {
+ AuthorName = message.AuthorName,
+ MessageId = message.MessageId is { Length: > 0 } messageId ? messageId : System.Guid.NewGuid().ToString("N"),
+ ResponseId = responseId,
+ };
+
+ ///
+ /// Builds the messages sent to the wrapped agent for the next iteration along with the subset that should be
+ /// surfaced to the caller (the loop-synthesized on-behalf-of feedback). Replayed caller input is excluded from the
+ /// surfaced subset.
+ ///
+ private (List Messages, List Surfaced) BuildNextMessages(LoopContext context, List feedback)
+ {
+ var messages = new List();
+ var surfaced = new List();
+
+ if (this._freshContextPerIteration)
+ {
+ // Fresh context: re-send the original task plus an aggregated log of all feedback recorded so far. Only the
+ // synthesized feedback message is surfaced; the replayed caller input messages are not.
+ messages.AddRange(context.InitialMessages);
+
+ ChatMessage? feedbackMessage = this.BuildAggregatedFeedbackMessage(feedback);
+ if (feedbackMessage is not null)
+ {
+ messages.Add(feedbackMessage);
+ surfaced.Add(feedbackMessage);
+ }
+ }
+ else
+ {
+ // Reused session: send only the latest feedback verbatim (the session already retains earlier turns). When
+ // the latest iteration produced no feedback, send no messages and let the agent continue from history.
+ string? latest = feedback.Count > 0 ? feedback[feedback.Count - 1] : null;
+ if (!string.IsNullOrWhiteSpace(latest))
+ {
+ var feedbackMessage = new ChatMessage(ChatRole.User, latest) { AuthorName = this._onBehalfOfAuthorName, MessageId = System.Guid.NewGuid().ToString("N") };
+ messages.Add(feedbackMessage);
+ surfaced.Add(feedbackMessage);
+ }
+ }
+
+ return (messages, surfaced);
+ }
+
+ private ChatMessage? BuildAggregatedFeedbackMessage(IReadOnlyList feedback)
+ {
+ var body = new StringBuilder("## Feedback\n");
+ bool any = false;
+ foreach (string? entry in feedback)
+ {
+ if (!string.IsNullOrWhiteSpace(entry))
+ {
+ body.Append("\n- ").Append(entry);
+ any = true;
+ }
+ }
+
+ return any ? new ChatMessage(ChatRole.User, body.ToString()) { AuthorName = this._onBehalfOfAuthorName, MessageId = System.Guid.NewGuid().ToString("N") } : null;
+ }
+
+ ///
+ /// Produces the non-streaming run result: either the final iteration's response (when configured) or an
+ /// aggregated response carrying the full transcript with the final response's metadata.
+ ///
+ private AgentResponse BuildResult(AgentResponse lastResponse, List transcript)
+ {
+ if (this._nonStreamingReturnsLastResponseOnly)
+ {
+ return lastResponse;
+ }
+
+ return new AgentResponse(transcript)
+ {
+ AgentId = lastResponse.AgentId,
+ ResponseId = lastResponse.ResponseId,
+ CreatedAt = lastResponse.CreatedAt,
+ FinishReason = lastResponse.FinishReason,
+ Usage = lastResponse.Usage,
+ AdditionalProperties = lastResponse.AdditionalProperties,
+ ContinuationToken = lastResponse.ContinuationToken,
+ };
+ }
+
+ private static bool HasPendingApprovalRequests(AgentResponse response)
+ {
+ foreach (ChatMessage message in response.Messages)
+ {
+ foreach (AIContent content in message.Contents)
+ {
+ if (content is ToolApprovalRequestContent)
+ {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ private void LogMaxIterationsReached(int iteration)
+ {
+ if (this._logger.IsEnabled(LogLevel.Information))
+ {
+ this._logger.LogInformation("LoopAgent reached the maximum of {MaxIterations} iterations and stopped.", iteration);
+ }
+ }
+
+ ///
+ /// Creates the session used for the next iteration when a fresh context is requested. A caller-supplied session is
+ /// restored from the pristine start-of-run snapshot by deserializing a fresh clone; a loop-owned session (no
+ /// snapshot) is created anew. The configured session-created callback is notified of the new session.
+ ///
+ private async ValueTask CreateFreshIterationSessionAsync(LoopContext context, JsonElement? initialSessionSnapshot, CancellationToken cancellationToken)
+ {
+ AgentSession session = initialSessionSnapshot is { } snapshot
+ ? await this.InnerAgent.DeserializeSessionAsync(snapshot, cancellationToken: cancellationToken).ConfigureAwait(false)
+ : await context.Agent.CreateSessionAsync(cancellationToken).ConfigureAwait(false);
+
+ await this.NotifyNewSessionAsync(session, cancellationToken).ConfigureAwait(false);
+ return session;
+ }
+
+ ///
+ /// Invokes the configured (if any) with a session the loop
+ /// has just created, so the caller can observe the latest session.
+ ///
+ private async ValueTask NotifyNewSessionAsync(AgentSession session, CancellationToken cancellationToken)
+ {
+ if (this._sessionCreatedCallback is not null)
+ {
+ await this._sessionCreatedCallback(session, cancellationToken).ConfigureAwait(false);
+ }
+ }
+
+ /// Represents the loop's decision for the next iteration: stop, or continue with a set of messages.
+ private readonly struct LoopNextStep
+ {
+ private LoopNextStep(bool shouldContinue, IReadOnlyList messages, IReadOnlyList surfacedMessages)
+ {
+ this.ShouldContinue = shouldContinue;
+ this.Messages = messages;
+ this.SurfacedMessages = surfacedMessages;
+ }
+
+ public bool ShouldContinue { get; }
+
+ /// Gets the full set of messages sent to the wrapped agent for the next iteration.
+ public IReadOnlyList Messages { get; }
+
+ ///
+ /// Gets the subset of the loop synthesized on the caller's behalf (feedback or
+ /// evaluator-supplied messages) that should be surfaced to the caller. Replayed caller input is excluded.
+ ///
+ public IReadOnlyList SurfacedMessages { get; }
+
+ public static LoopNextStep Stop() => new(shouldContinue: false, [], []);
+
+ public static LoopNextStep Continue(IReadOnlyList messages, IReadOnlyList surfacedMessages)
+ => new(shouldContinue: true, messages, surfacedMessages);
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI/Harness/Loop/LoopAgentOptions.cs b/dotnet/src/Microsoft.Agents.AI/Harness/Loop/LoopAgentOptions.cs
new file mode 100644
index 0000000000..ec009b4594
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI/Harness/Loop/LoopAgentOptions.cs
@@ -0,0 +1,117 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Shared.DiagnosticIds;
+
+namespace Microsoft.Agents.AI;
+
+///
+/// Provides configuration options for .
+///
+[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
+public sealed class LoopAgentOptions
+{
+ ///
+ /// Gets or sets the global safety cap on the number of times the wrapped agent is invoked in a single loop run,
+ /// or to use .
+ ///
+ ///
+ /// This is an absolute upper bound that applies regardless of the configured set. An
+ /// evaluator may stop the loop earlier, but no evaluator can cause the loop to exceed this cap, so raise this value
+ /// if you intend to allow longer loops.
+ ///
+ public int? MaxIterations { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether each re-invocation restarts from a clean context: the original input
+ /// messages plus an aggregated feedback log, rather than the latest feedback appended to the prior conversation.
+ /// Defaults to .
+ ///
+ ///
+ ///
+ /// This rebuilds the input messages each iteration and resets the session before each re-invocation so no
+ /// prior conversation history leaks across iterations. When the loop owns the session it creates a new one each
+ /// iteration. When the caller supplies a session, serializes it once at the start of the run
+ /// and restores a fresh clone (by deserializing that snapshot) before each re-invocation; this requires the wrapped
+ /// agent to support session serialization. The first iteration still runs against the caller's supplied session.
+ ///
+ ///
+ /// Note that cloning will only result in a fresh context, if the chat history storage mechanism supports cloning.
+ /// For example the default in-memory storage supports cloning, since the messages are serialized as part of the snapshot.
+ ///
+ ///
+ /// However, if the Conversations service is used, which stores messages in a single threaded list of messages,
+ /// then the cloned session will still contain the full message history, since the snapshot only captures an id reference
+ /// to the conversation and not the individual messages.
+ ///
+ ///
+ /// On the other hand, if responses are used with response ids, cloning will work well, since response ids are
+ /// forkable. Each new response has its own id, and is based on the id of the previous response.
+ ///
+ ///
+ /// On iterations where an evaluator returns explicit messages via
+ /// , the session is still reset (a fresh or cloned session is
+ /// used); only the rebuild of the input messages from the feedback log is skipped, because the evaluator's explicit
+ /// messages are sent verbatim.
+ ///
+ ///
+ public bool FreshContextPerIteration { get; set; }
+
+ ///
+ /// Gets or sets the author name stamped on the loop-synthesized "on-behalf-of" messages that the loop injects
+ /// into the wrapped agent for re-invocations, or to leave them unattributed. Defaults to
+ /// .
+ ///
+ ///
+ /// When the loop re-invokes the wrapped agent it sends feedback messages on the caller's behalf. Setting this name
+ /// marks those autonomous messages (for example with a value such as "loop") so that callers and the wrapped
+ /// agent can distinguish them from the caller's own turns. It is applied only to messages the loop synthesizes
+ /// itself; messages supplied explicitly by an evaluator via are
+ /// left untouched, and the caller's original input messages are never modified.
+ ///
+ public string? OnBehalfOfAuthorName { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether the on-behalf-of messages the loop injects for re-invocations are
+ /// omitted from the output surfaced back to the caller. Defaults to .
+ ///
+ ///
+ /// When (the default) a streaming run emits the injected feedback / evaluator-supplied
+ /// messages as updates before each re-invocation, and a non-streaming run includes them in the aggregated
+ /// transcript, so callers can see the loop acting autonomously on their behalf. Set this to
+ /// to omit those messages from the returned output and surface only the wrapped agent's responses; the messages are
+ /// still sent to the wrapped agent. This setting has no effect when
+ /// causes a non-streaming run to return only the final response.
+ ///
+ public bool ExcludeOnBehalfOfMessages { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether a non-streaming run returns only the final iteration's response instead
+ /// of the aggregated transcript of every iteration. Defaults to .
+ ///
+ ///
+ /// By default a non-streaming run returns a single that
+ /// aggregates, in order, the on-behalf-of messages the loop injected and the responses produced by every
+ /// iteration — mirroring the full sequence of updates yielded by a streaming run. Set this to
+ /// to instead return only the last iteration's . This setting affects non-streaming runs
+ /// only; streaming runs always yield every iteration's updates.
+ ///
+ public bool NonStreamingReturnsLastResponseOnly { get; set; }
+
+ ///
+ /// Gets or sets an optional callback invoked whenever creates a new session, so the caller
+ /// can capture the latest session (for example to continue the conversation after the loop completes). Defaults to
+ /// .
+ ///
+ ///
+ /// The callback is invoked with each session the loop itself creates: the initial loop-owned session (when the
+ /// caller does not supply one) and, when is enabled, every session created
+ /// for a re-invocation — whether a brand-new loop-owned session or a fresh clone deserialized from the caller's
+ /// original session. It is not invoked for a caller-supplied session, since the caller already holds that one. When
+ /// it fires multiple times, the most recent invocation carries the session the loop is currently using.
+ ///
+ public Func? SessionCreatedCallback { get; set; }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI/Harness/Loop/LoopContext.cs b/dotnet/src/Microsoft.Agents.AI/Harness/Loop/LoopContext.cs
new file mode 100644
index 0000000000..d0bdf03e7b
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI/Harness/Loop/LoopContext.cs
@@ -0,0 +1,97 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using Microsoft.Extensions.AI;
+using Microsoft.Shared.DiagnosticIds;
+using Microsoft.Shared.Diagnostics;
+
+namespace Microsoft.Agents.AI;
+
+///
+/// Provides the per-run state that a uses to decide whether a
+/// should re-invoke the wrapped agent and what feedback to provide.
+///
+///
+/// A single instance is created for each run and is
+/// reused across iterations, with and updated before
+/// each call to . Because evaluator instances are expected to be
+/// stateless and may be shared across concurrent runs, any per-run mutable state must be stored on this
+/// context — for example via — rather than in fields on the evaluator itself.
+///
+[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
+public sealed class LoopContext
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The wrapped that is being looped.
+ /// The used for the loop.
+ /// The messages passed in for the first iteration of the loop.
+ /// The produced by the iteration that just completed.
+ /// The that were passed to the loop run, if any.
+ ///
+ /// , , , or
+ /// is .
+ ///
+ public LoopContext(
+ AIAgent agent,
+ AgentSession session,
+ IReadOnlyList initialMessages,
+ AgentResponse lastResponse,
+ AgentRunOptions? runOptions = null)
+ {
+ this.Agent = Throw.IfNull(agent);
+ this.Session = Throw.IfNull(session);
+ this.InitialMessages = Throw.IfNull(initialMessages);
+ this.LastResponse = Throw.IfNull(lastResponse);
+ this.RunOptions = runOptions;
+ }
+
+ /// Gets the wrapped that is being looped.
+ public AIAgent Agent { get; }
+
+ /// Gets the used for the loop.
+ ///
+ /// When the caller does not provide a session, creates one up front. By default the same
+ /// session is reused across every iteration so that conversation continuity is preserved and the original request
+ /// is not replayed. When is enabled,
+ /// resets the session before each re-invocation: a loop-owned session is created anew, while a caller-supplied
+ /// session is restored from a snapshot taken at the start of the run by deserializing a fresh clone.
+ ///
+ public AgentSession Session { get; internal set; }
+
+ /// Gets the messages that were passed in for the first iteration of the loop.
+ public IReadOnlyList InitialMessages { get; }
+
+ /// Gets the that were passed to the loop run, if any.
+ public AgentRunOptions? RunOptions { get; }
+
+ /// Gets the number of completed agent runs so far (1-based after the first run).
+ public int Iteration { get; internal set; }
+
+ /// Gets the produced by the iteration that just completed.
+ public AgentResponse LastResponse { get; internal set; }
+
+ ///
+ /// Gets the feedback accumulated across iterations so far, one entry per re-invoked iteration in order.
+ ///
+ ///
+ /// Each entry is the feedback supplied by the evaluator that requested the corresponding re-invocation, or
+ /// when that iteration produced no feedback string (for example a plain
+ /// with no text, or a
+ /// that supplied explicit messages instead). The log records one entry per re-invoked iteration regardless of mode,
+ /// so the last entry always corresponds to the most recent re-invoked iteration. This log is owned and populated by
+ /// ; evaluators may read it to reason over prior feedback.
+ ///
+ public IReadOnlyList Feedback { get; internal set; } = [];
+
+ ///
+ /// Gets a mutable bag of per-run state shared across iterations and available to every evaluator.
+ ///
+ ///
+ /// This dictionary is owned by the loop run (not by any evaluator instance) so that evaluators can remain
+ /// stateless. Evaluators can stash arbitrary per-run state here keyed by a collision-resistant key.
+ ///
+ public AdditionalPropertiesDictionary AdditionalProperties { get; } = new();
+}
diff --git a/dotnet/src/Microsoft.Agents.AI/Harness/Loop/LoopEvaluation.cs b/dotnet/src/Microsoft.Agents.AI/Harness/Loop/LoopEvaluation.cs
new file mode 100644
index 0000000000..2d8de152e8
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI/Harness/Loop/LoopEvaluation.cs
@@ -0,0 +1,86 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using Microsoft.Extensions.AI;
+using Microsoft.Shared.DiagnosticIds;
+using Microsoft.Shared.Diagnostics;
+
+namespace Microsoft.Agents.AI;
+
+///
+/// Represents the result produced by a after an agent iteration: whether the
+/// should re-invoke the wrapped agent and, optionally, the feedback or explicit messages that
+/// should inform the next iteration.
+///
+///
+/// An evaluator is concerned only with the judgment (continue or stop) and what to carry forward. In the common case
+/// it returns a feedback string and lets the decide how that feedback is turned into the next
+/// input (and whether the session is reset). For full control, supplies the exact
+/// messages to send next, bypassing the loop's feedback and message construction.
+///
+[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
+public sealed class LoopEvaluation
+{
+ private static readonly LoopEvaluation s_stop = new(shouldReinvoke: false, feedback: null, messages: null);
+
+ private LoopEvaluation(bool shouldReinvoke, string? feedback, IReadOnlyList? messages)
+ {
+ this.ShouldReinvoke = shouldReinvoke;
+ this.Feedback = feedback;
+ this.Messages = messages;
+ }
+
+ /// Gets a value indicating whether the loop should run the wrapped agent again.
+ public bool ShouldReinvoke { get; }
+
+ ///
+ /// Gets the feedback describing what is missing or what the agent should do next, or when
+ /// no feedback was produced.
+ ///
+ /// This value is only meaningful when is .
+ public string? Feedback { get; }
+
+ ///
+ /// Gets the explicit messages to send on the next iteration, or when the loop should build
+ /// the next input from feedback instead.
+ ///
+ ///
+ /// When non-, the sends these messages verbatim and does not apply
+ /// its feedback or message construction. The session is still reset when
+ /// is enabled. Only meaningful when
+ /// is .
+ ///
+ internal IReadOnlyList? Messages { get; }
+
+ /// Creates an evaluation that stops the loop and returns the latest response to the caller.
+ /// An evaluation with set to .
+ public static LoopEvaluation Stop() => s_stop;
+
+ /// Creates an evaluation that re-invokes the wrapped agent, optionally carrying feedback forward.
+ ///
+ /// Optional feedback to inform the next iteration. , empty, or whitespace is treated as no
+ /// feedback.
+ ///
+ /// An evaluation with set to .
+ public static LoopEvaluation Continue(string? feedback = null) => new(shouldReinvoke: true, string.IsNullOrWhiteSpace(feedback) ? null : feedback, messages: null);
+
+ ///
+ /// Creates an evaluation that re-invokes the wrapped agent with the specified messages, bypassing the loop's
+ /// feedback and message construction.
+ ///
+ /// The messages to send to the wrapped agent on the next iteration.
+ /// An evaluation with set to .
+ /// is .
+ ///
+ /// Use this for full control over the next input (for example to send non-user roles, multiple messages, or
+ /// non-text content). The supplied messages are sent verbatim and the loop does not accumulate or inject feedback
+ /// for this iteration.
+ ///
+ public static LoopEvaluation ContinueWithMessages(IEnumerable messages)
+ {
+ _ = Throw.IfNull(messages);
+ return new LoopEvaluation(shouldReinvoke: true, feedback: null, messages: messages as IReadOnlyList ?? messages.ToList());
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI/Harness/Loop/LoopEvaluator.cs b/dotnet/src/Microsoft.Agents.AI/Harness/Loop/LoopEvaluator.cs
new file mode 100644
index 0000000000..328c99e80c
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI/Harness/Loop/LoopEvaluator.cs
@@ -0,0 +1,41 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Diagnostics.CodeAnalysis;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Shared.DiagnosticIds;
+
+namespace Microsoft.Agents.AI;
+
+///
+/// Provides the abstract base class for the component that decides, after each agent iteration, whether a
+/// should re-invoke the wrapped agent and what feedback to provide.
+///
+///
+///
+/// A is pure judgment: it inspects the and returns a
+/// describing whether to continue and any feedback for the next iteration. It does not
+/// manage the session or construct the next input messages — that is the responsibility of the
+/// that consumes it.
+///
+///
+/// Out-of-the-box implementations include , ,
+/// and . Implementations should be stateless and safe to share across
+/// concurrent loop runs; any per-run state must be stored on the supplied .
+///
+///
+[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
+public abstract class LoopEvaluator
+{
+ ///
+ /// Evaluates the loop state after an iteration and decides whether to re-invoke the wrapped agent and what
+ /// feedback to provide.
+ ///
+ /// The per-run describing the current loop state.
+ /// The to monitor for cancellation requests.
+ ///
+ /// A value task whose result is a indicating whether to continue and, if so, the
+ /// feedback to carry forward to the next iteration.
+ ///
+ public abstract ValueTask EvaluateAsync(LoopContext context, CancellationToken cancellationToken = default);
+}
diff --git a/dotnet/src/Microsoft.Agents.AI/Harness/Loop/LoopJsonContext.cs b/dotnet/src/Microsoft.Agents.AI/Harness/Loop/LoopJsonContext.cs
new file mode 100644
index 0000000000..8d69383e3f
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI/Harness/Loop/LoopJsonContext.cs
@@ -0,0 +1,16 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Diagnostics.CodeAnalysis;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Microsoft.Agents.AI;
+
+///
+/// Source-generated for loop types that require JSON serialization, such as the
+/// structured used by .
+///
+[JsonSourceGenerationOptions(JsonSerializerDefaults.Web)]
+[JsonSerializable(typeof(JudgeVerdict))]
+[ExcludeFromCodeCoverage]
+internal sealed partial class LoopJsonContext : JsonSerializerContext;
diff --git a/dotnet/tests/Foundry.Hosting.IntegrationTests.TestContainer/Program.cs b/dotnet/tests/Foundry.Hosting.IntegrationTests.TestContainer/Program.cs
index babfdfcddf..29d57d369a 100644
--- a/dotnet/tests/Foundry.Hosting.IntegrationTests.TestContainer/Program.cs
+++ b/dotnet/tests/Foundry.Hosting.IntegrationTests.TestContainer/Program.cs
@@ -54,7 +54,6 @@ builder.Services.AddFoundryResponses(agent);
var app = builder.Build();
app.MapFoundryResponses();
-app.MapGet("/readiness", () => Results.Ok());
app.Run();
static AIAgent CreateHappyPathAgent(AIProjectClient client, string deployment) =>
diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/FoundryProjectEndpointEnvFixture.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/FoundryProjectEndpointEnvFixture.cs
new file mode 100644
index 0000000000..667bc46b59
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/FoundryProjectEndpointEnvFixture.cs
@@ -0,0 +1,14 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+namespace Microsoft.Agents.AI.Foundry.Hosting.UnitTests;
+
+///
+/// xUnit collection that serializes tests mutating the FOUNDRY_PROJECT_ENDPOINT
+/// process environment variable. Without this, parallel test execution causes flaky
+/// races between tests that set / unset the variable.
+///
+[CollectionDefinition(Name, DisableParallelization = true)]
+public sealed class FoundryProjectEndpointEnvFixture
+{
+ public const string Name = "FoundryProjectEndpointEnv";
+}
diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/FoundryToolboxBearerTokenHandlerTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/FoundryToolboxBearerTokenHandlerTests.cs
index e619445481..eafa3c497a 100644
--- a/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/FoundryToolboxBearerTokenHandlerTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/FoundryToolboxBearerTokenHandlerTests.cs
@@ -1,6 +1,9 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading;
@@ -51,28 +54,144 @@ public class FoundryToolboxBearerTokenHandlerTests
}
[Fact]
- public async Task SendAsync_InjectsFoundryFeaturesHeaderAsync()
+ public async Task SendAsync_UsesAiAzureComScopeAsync()
+ {
+ // Arrange
+ var capturedContexts = new List();
+ var credential = new Mock();
+ credential
+ .Setup(c => c.GetTokenAsync(It.IsAny(), It.IsAny()))
+ .Callback((ctx, _) => capturedContexts.Add(ctx))
+ .ReturnsAsync(new AccessToken(FakeToken, DateTimeOffset.MaxValue));
+ var (handler, _) = CreateHandlerPair(credential);
+ using var invoker = new HttpMessageInvoker(handler);
+
+ // Act
+ using var request = new HttpRequestMessage(HttpMethod.Get, "https://example.com/api");
+ await invoker.SendAsync(request, CancellationToken.None);
+
+ // Assert: spec §4 mandates the https://ai.azure.com audience.
+ Assert.Single(capturedContexts);
+ Assert.Contains("https://ai.azure.com/.default", capturedContexts[0].Scopes);
+ }
+
+ [Fact]
+ public async Task SendAsync_AlwaysInjectsMandatoryFoundryFeaturesHeaderAsync()
+ {
+ // Arrange
+ var (handler, _) = CreateHandlerPair(featuresHeader: null);
+ using var invoker = new HttpMessageInvoker(handler);
+
+ // Act
+ using var request = new HttpRequestMessage(HttpMethod.Get, "https://example.com/api");
+ using var response = await invoker.SendAsync(request, CancellationToken.None);
+
+ // Assert: spec §2 requires Foundry-Features: Toolboxes=V1Preview on every request.
+ Assert.True(request.Headers.TryGetValues("Foundry-Features", out var values));
+ Assert.Equal("Toolboxes=V1Preview", values.Single());
+ }
+
+ [Fact]
+ public async Task SendAsync_MergesMandatoryAndOverrideFeaturesAsync()
{
var (handler, _) = CreateHandlerPair(featuresHeader: "feature1,feature2");
using var invoker = new HttpMessageInvoker(handler);
using var request = new HttpRequestMessage(HttpMethod.Get, "https://example.com/api");
- using var response = await invoker.SendAsync(request, CancellationToken.None);
+ await invoker.SendAsync(request, CancellationToken.None);
Assert.True(request.Headers.TryGetValues("Foundry-Features", out var values));
- Assert.Contains("feature1,feature2", values);
+ var header = values.Single();
+ Assert.Contains("Toolboxes=V1Preview", header, StringComparison.Ordinal);
+ Assert.Contains("feature1", header, StringComparison.Ordinal);
+ Assert.Contains("feature2", header, StringComparison.Ordinal);
}
[Fact]
- public async Task SendAsync_OmitsFeaturesHeaderWhenNullAsync()
+ public async Task SendAsync_DoesNotDuplicateMandatoryFlagAsync()
{
- var (handler, _) = CreateHandlerPair(featuresHeader: null);
+ // Override already contains the mandatory flag — must not be duplicated in the merged value.
+ var (handler, _) = CreateHandlerPair(featuresHeader: "Toolboxes=V1Preview");
using var invoker = new HttpMessageInvoker(handler);
using var request = new HttpRequestMessage(HttpMethod.Get, "https://example.com/api");
- using var response = await invoker.SendAsync(request, CancellationToken.None);
+ await invoker.SendAsync(request, CancellationToken.None);
- Assert.False(request.Headers.Contains("Foundry-Features"));
+ Assert.True(request.Headers.TryGetValues("Foundry-Features", out var values));
+ var header = values.Single();
+ var count = 0;
+ var idx = 0;
+ while ((idx = header.IndexOf("Toolboxes=V1Preview", idx, StringComparison.OrdinalIgnoreCase)) >= 0)
+ {
+ count++;
+ idx += "Toolboxes=V1Preview".Length;
+ }
+ Assert.Equal(1, count);
+ }
+
+ [Fact]
+ public async Task SendAsync_PropagatesTraceContextFromActivityAsync()
+ {
+ // Arrange: activate an Activity so Activity.Current is populated.
+ using var listener = new ActivityListener
+ {
+ ShouldListenTo = _ => true,
+ Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllData,
+ };
+ ActivitySource.AddActivityListener(listener);
+ using var source = new ActivitySource("test-source");
+ using var activity = source.StartActivity("test-op")!;
+ Assert.NotNull(activity);
+ activity.TraceStateString = "vendor=value";
+ activity.AddBaggage("user", "alice");
+
+ var (handler, _) = CreateHandlerPair();
+ using var invoker = new HttpMessageInvoker(handler);
+
+ // Act
+ using var request = new HttpRequestMessage(HttpMethod.Get, "https://example.com/api");
+ await invoker.SendAsync(request, CancellationToken.None);
+
+ // Assert: spec §6.3 requires traceparent/tracestate/baggage propagation.
+ Assert.True(request.Headers.TryGetValues("traceparent", out var tpValues));
+ Assert.Contains(activity.TraceId.ToString(), tpValues.Single(), StringComparison.Ordinal);
+
+ Assert.True(request.Headers.TryGetValues("tracestate", out var tsValues));
+ Assert.Equal("vendor=value", tsValues.Single());
+
+ Assert.True(request.Headers.TryGetValues("baggage", out var bgValues));
+ Assert.Contains("user=alice", bgValues.Single(), StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public async Task SendAsync_DoesNotOverrideExistingTraceparentAsync()
+ {
+ // Caller pre-set traceparent on the message; must not be duplicated or replaced.
+ using var listener = new ActivityListener
+ {
+ ShouldListenTo = _ => true,
+ Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllData,
+ };
+ ActivitySource.AddActivityListener(listener);
+ using var source = new ActivitySource("test-source");
+ using var activity = source.StartActivity("test-op")!;
+ Assert.NotNull(activity);
+
+ var (handler, _) = CreateHandlerPair();
+ using var invoker = new HttpMessageInvoker(handler);
+
+ const string PresetTraceparent = "00-00000000000000000000000000000001-0000000000000001-01";
+ using var request = new HttpRequestMessage(HttpMethod.Get, "https://example.com/api");
+ request.Headers.TryAddWithoutValidation("traceparent", PresetTraceparent);
+
+ // Act
+ await invoker.SendAsync(request, CancellationToken.None);
+
+ // Assert
+ Assert.True(request.Headers.TryGetValues("traceparent", out var values));
+ var list = values.ToList();
+ Assert.Single(list);
+ Assert.Equal(PresetTraceparent, list[0]);
}
[Theory]
diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/FoundryToolboxHealthCheckTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/FoundryToolboxHealthCheckTests.cs
new file mode 100644
index 0000000000..7ea5d14e1c
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/FoundryToolboxHealthCheckTests.cs
@@ -0,0 +1,135 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Azure.Core;
+using Microsoft.Extensions.Diagnostics.HealthChecks;
+using Microsoft.Extensions.Options;
+using Moq;
+
+namespace Microsoft.Agents.AI.Foundry.Hosting.UnitTests;
+
+[Collection(FoundryProjectEndpointEnvFixture.Name)]
+public class FoundryToolboxHealthCheckTests
+{
+ [Fact]
+ public async Task CheckHealthAsync_PendingStatus_ReturnsConfiguredFailureAsync()
+ {
+ // Arrange: a fresh FoundryToolboxService whose StartAsync has never run reports
+ // Pending. The health check must surface that as the registration's failure
+ // status so the platform waits before sending traffic.
+ var service = CreateServiceWithoutStarting();
+ var check = new FoundryToolboxHealthCheck(service);
+ var context = NewContext(failureStatus: HealthStatus.Unhealthy);
+
+ // Act
+ var result = await check.CheckHealthAsync(context);
+
+ // Assert
+ Assert.Equal(HealthStatus.Unhealthy, result.Status);
+ Assert.Contains("startup has not completed", result.Description, StringComparison.OrdinalIgnoreCase);
+ }
+
+ [Fact]
+ public async Task CheckHealthAsync_NoEndpointStatus_ReturnsHealthyAsync()
+ {
+ // Arrange: no FOUNDRY_PROJECT_ENDPOINT / AZURE_AI_PROJECT_ENDPOINT is normal local-dev.
+ // The container must still pass readiness because the rest of the agent is functional.
+ var savedFoundry = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT");
+ var savedAzure = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT");
+ Environment.SetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT", null);
+ Environment.SetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT", null);
+ try
+ {
+ var service = CreateServiceWithoutStarting(toolbox: "any");
+ await service.StartAsync(CancellationToken.None);
+
+ var check = new FoundryToolboxHealthCheck(service);
+ var context = NewContext(failureStatus: HealthStatus.Unhealthy);
+
+ // Act
+ var result = await check.CheckHealthAsync(context);
+
+ // Assert
+ Assert.Equal(HealthStatus.Healthy, result.Status);
+ Assert.Equal(FoundryToolboxStartupStatus.NoEndpoint, service.StartupStatus);
+ }
+ finally
+ {
+ Environment.SetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT", savedFoundry);
+ Environment.SetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT", savedAzure);
+ }
+ }
+
+ [Fact]
+ public async Task CheckHealthAsync_UnhealthyStatus_ReturnsConfiguredFailureWithFailedNamesAsync()
+ {
+ // Arrange: pre-registered toolbox at an unreachable endpoint forces StartAsync to
+ // record the failure. The health-check must reflect Unhealthy and expose the
+ // failed toolbox names in the result data so operators can diagnose without log
+ // diving.
+ var options = new FoundryToolboxOptions
+ {
+ EndpointOverride = "http://127.0.0.1:1/unreachable",
+ };
+ options.ToolboxNames.Add("broken-toolbox");
+ var service = new FoundryToolboxService(Options.Create(options), Mock.Of());
+ await service.StartAsync(CancellationToken.None);
+
+ var check = new FoundryToolboxHealthCheck(service);
+ var context = NewContext(failureStatus: HealthStatus.Unhealthy);
+
+ // Act
+ var result = await check.CheckHealthAsync(context);
+
+ // Assert
+ Assert.Equal(HealthStatus.Unhealthy, result.Status);
+ Assert.True(result.Data.ContainsKey("failedToolboxes"));
+ var failed = Assert.IsAssignableFrom>(result.Data["failedToolboxes"]);
+ Assert.Equal("broken-toolbox", Assert.Single(failed));
+ }
+
+ [Fact]
+ public async Task CheckHealthAsync_HealthyStatus_ReturnsHealthyAsync()
+ {
+ // Arrange: an endpoint set but no pre-registered toolboxes is the legitimate
+ // lazy-only setup. StartAsync reports Healthy and the check must agree.
+ var options = new FoundryToolboxOptions
+ {
+ EndpointOverride = "http://127.0.0.1:1/unused",
+ };
+ var service = new FoundryToolboxService(Options.Create(options), Mock.Of());
+ await service.StartAsync(CancellationToken.None);
+
+ var check = new FoundryToolboxHealthCheck(service);
+ var context = NewContext(failureStatus: HealthStatus.Unhealthy);
+
+ // Act
+ var result = await check.CheckHealthAsync(context);
+
+ // Assert
+ Assert.Equal(HealthStatus.Healthy, result.Status);
+ }
+
+ private static FoundryToolboxService CreateServiceWithoutStarting(string? toolbox = null)
+ {
+ var options = new FoundryToolboxOptions();
+ if (toolbox is not null)
+ {
+ options.ToolboxNames.Add(toolbox);
+ }
+ return new FoundryToolboxService(Options.Create(options), Mock.Of());
+ }
+
+ private static HealthCheckContext NewContext(HealthStatus failureStatus) =>
+ new()
+ {
+ Registration = new HealthCheckRegistration(
+ name: "foundry-toolbox",
+ instance: Mock.Of(),
+ failureStatus: failureStatus,
+ tags: null),
+ };
+}
diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/FoundryToolboxServiceTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/FoundryToolboxServiceTests.cs
index cdcdf5ee8e..d8a56adcd6 100644
--- a/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/FoundryToolboxServiceTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/FoundryToolboxServiceTests.cs
@@ -9,6 +9,7 @@ using Moq;
namespace Microsoft.Agents.AI.Foundry.Hosting.UnitTests;
+[Collection(FoundryProjectEndpointEnvFixture.Name)]
public class FoundryToolboxServiceTests
{
[Fact]
@@ -39,15 +40,17 @@ public class FoundryToolboxServiceTests
var ex = await Assert.ThrowsAsync(
async () => await service.GetToolboxToolsAsync("missing", version: null, CancellationToken.None));
- Assert.Contains("FOUNDRY_AGENT_TOOLSET_ENDPOINT", ex.Message, StringComparison.Ordinal);
+ Assert.Contains("FOUNDRY_PROJECT_ENDPOINT", ex.Message, StringComparison.Ordinal);
}
[Fact]
public async Task StartAsync_WithoutEndpoint_LeavesToolsEmptyAsync()
{
- // Ensure env var is not set (tests may run in any CI environment)
- var saved = Environment.GetEnvironmentVariable("FOUNDRY_AGENT_TOOLSET_ENDPOINT");
- Environment.SetEnvironmentVariable("FOUNDRY_AGENT_TOOLSET_ENDPOINT", null);
+ // Ensure neither env var is set (tests may run in any CI environment)
+ var savedFoundry = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT");
+ var savedAzure = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT");
+ Environment.SetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT", null);
+ Environment.SetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT", null);
try
{
var options = new FoundryToolboxOptions();
@@ -59,10 +62,157 @@ public class FoundryToolboxServiceTests
await service.StartAsync(CancellationToken.None);
Assert.Empty(service.Tools);
+ Assert.Equal(FoundryToolboxStartupStatus.NoEndpoint, service.StartupStatus);
+ Assert.Empty(service.FailedToolboxNames);
}
finally
{
- Environment.SetEnvironmentVariable("FOUNDRY_AGENT_TOOLSET_ENDPOINT", saved);
+ Environment.SetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT", savedFoundry);
+ Environment.SetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT", savedAzure);
}
}
+
+ [Fact]
+ public async Task StartAsync_AttemptsOpenForPreRegisteredToolboxFromProjectEndpointAsync()
+ {
+ // Arrange: point the service at an unreachable host and confirm StartAsync
+ // attempts to open the pre-registered toolbox (verified via FailedToolboxNames
+ // recording the attempted name and StartupStatus reflecting the failure).
+ var saved = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT");
+ Environment.SetEnvironmentVariable(
+ "FOUNDRY_PROJECT_ENDPOINT",
+ "https://example.invalid/api/projects/proj");
+ try
+ {
+ var options = new FoundryToolboxOptions { ApiVersion = "v1" };
+ options.ToolboxNames.Add("my-toolbox");
+ var service = new FoundryToolboxService(
+ Options.Create(options),
+ Mock.Of());
+
+ // Act: StartAsync attempts to connect to the invalid endpoint and fails.
+ // The failure path records FailedToolboxNames; the value confirms the resolver ran.
+ await service.StartAsync(CancellationToken.None);
+
+ // Assert: open failed, status reflects that (resolver was reached), and
+ // the failed name matches — i.e. we attempted the right toolbox.
+ Assert.Equal(FoundryToolboxStartupStatus.Unhealthy, service.StartupStatus);
+ Assert.Single(service.FailedToolboxNames);
+ Assert.Equal("my-toolbox", service.FailedToolboxNames[0]);
+ }
+ finally
+ {
+ Environment.SetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT", saved);
+ }
+ }
+
+ [Fact]
+ public async Task StartAsync_TrailingSlashOnProjectEndpoint_AttemptsOpenAsync()
+ {
+ var saved = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT");
+ Environment.SetEnvironmentVariable(
+ "FOUNDRY_PROJECT_ENDPOINT",
+ "https://example.invalid/api/projects/proj/");
+ try
+ {
+ var options = new FoundryToolboxOptions();
+ options.ToolboxNames.Add("tb");
+ var service = new FoundryToolboxService(
+ Options.Create(options),
+ Mock.Of());
+
+ await service.StartAsync(CancellationToken.None);
+
+ // Arrange/Act: when trailing-slash normalization works the open still fails
+ // (host is unreachable), but FailedToolboxNames records the attempted name —
+ // proof that the resolver did not throw on the slash and the URL was built.
+ Assert.Equal(FoundryToolboxStartupStatus.Unhealthy, service.StartupStatus);
+ Assert.Single(service.FailedToolboxNames);
+ Assert.Equal("tb", service.FailedToolboxNames[0]);
+ }
+ finally
+ {
+ Environment.SetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT", saved);
+ }
+ }
+
+ [Fact]
+ public async Task StartAsync_EndpointOverrideWinsOverEnvAsync()
+ {
+ var saved = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT");
+ Environment.SetEnvironmentVariable(
+ "FOUNDRY_PROJECT_ENDPOINT",
+ "https://from-env.invalid/api/projects/proj");
+ try
+ {
+ // EndpointOverride should take precedence over the env var.
+ var options = new FoundryToolboxOptions
+ {
+ EndpointOverride = "http://127.0.0.1:1/from-override",
+ };
+ options.ToolboxNames.Add("tb");
+
+ var service = new FoundryToolboxService(
+ Options.Create(options),
+ Mock.Of());
+
+ await service.StartAsync(CancellationToken.None);
+
+ // Override URL is unreachable; we expect Unhealthy (proving Start did try to open
+ // a toolbox, i.e. did not fall into the NoEndpoint branch).
+ Assert.Equal(FoundryToolboxStartupStatus.Unhealthy, service.StartupStatus);
+ Assert.Single(service.FailedToolboxNames);
+ }
+ finally
+ {
+ Environment.SetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT", saved);
+ }
+ }
+
+ [Fact]
+ public async Task StartAsync_WithEndpointButFailingToolbox_RecordsFailureAndStaysReachableAsync()
+ {
+ // Arrange: a syntactically valid but unreachable endpoint forces OpenToolboxAsync
+ // to throw inside the catch-and-log path. The service must still complete StartAsync
+ // (so the host doesn't crash) and surface the failure via StartupStatus.
+ var options = new FoundryToolboxOptions
+ {
+ EndpointOverride = "http://127.0.0.1:1/unreachable",
+ };
+ options.ToolboxNames.Add("broken-toolbox");
+
+ var service = new FoundryToolboxService(
+ Options.Create(options),
+ Mock.Of