diff --git a/dotnet/.github/skills/verify-samples-tool/SKILL.md b/dotnet/.github/skills/verify-samples-tool/SKILL.md
new file mode 100644
index 0000000000..49878467d7
--- /dev/null
+++ b/dotnet/.github/skills/verify-samples-tool/SKILL.md
@@ -0,0 +1,213 @@
+---
+name: verify-samples-tool
+description: How to use the verify-samples tool to run, verify, and manage sample definitions in the Agent Framework repository. Use this when adding, updating, or running sample verification.
+---
+
+# verify-samples Tool
+
+The `verify-samples` project (`dotnet/eng/verify-samples/`) is an automated tool that runs sample projects and verifies their output using deterministic checks and AI-powered verification.
+
+## Running verify-samples
+
+```bash
+cd dotnet
+
+# Run all samples across all categories
+dotnet run --project eng/verify-samples -- --log results.log --csv results.csv
+
+# Run a specific category
+dotnet run --project eng/verify-samples -- --category 02-agents --log results.log
+
+# Run specific samples by name
+dotnet run --project eng/verify-samples -- Agent_Step02_StructuredOutput Agent_Step09_AsFunctionTool
+
+# Control parallelism (default 8)
+dotnet run --project eng/verify-samples -- --parallel 8 --log results.log
+
+# Combine options
+dotnet run --project eng/verify-samples -- --category 03-workflows --parallel 4 --log results.log --csv results.csv
+```
+
+### Required Environment Variables
+
+The tool itself needs:
+- `AZURE_OPENAI_ENDPOINT` — for the AI verification agent
+- `AZURE_OPENAI_DEPLOYMENT_NAME` (optional, defaults to `gpt-5-mini`)
+
+Individual samples require their own env vars (e.g., `AZURE_AI_PROJECT_ENDPOINT`). The tool automatically checks and skips samples with missing env vars.
+
+### Output Files
+
+- `--log results.log` — detailed per-sample log with stdout/stderr, AI reasoning, and a summary
+- `--csv results.csv` — tabular summary with Sample, ProjectPath, Status, FailedChecks, and Failures columns
+
+## Sample Categories
+
+Definitions are in the `dotnet/eng/verify-samples/` directory:
+
+| Category | Config File | Registered Key |
+|----------|-------------|----------------|
+| 01-get-started | `GetStartedSamples.cs` | `01-get-started` |
+| 02-agents | `AgentsSamples.cs` | `02-agents` |
+| 03-workflows | `WorkflowSamples.cs` | `03-workflows` |
+
+Categories are registered in `VerifyOptions.cs` in the `s_sampleSets` dictionary.
+
+## SampleDefinition Properties
+
+Each sample is defined as a `SampleDefinition` in the appropriate config file. Key properties:
+
+```csharp
+new SampleDefinition
+{
+ // Required: Display name for the sample
+ Name = "Agent_Step02_StructuredOutput",
+
+ // Required: Relative path from dotnet/ to the sample project directory
+ ProjectPath = "samples/02-agents/Agents/Agent_Step02_StructuredOutput",
+
+ // Environment variables the sample requires (throws if missing)
+ RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
+
+ // Environment variables with defaults that would prompt on console if unset
+ OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
+
+ // Skip this sample with a reason (for structural issues only)
+ SkipReason = null, // or "Requires external service X."
+
+ // Deterministic checks: substrings that must appear in stdout
+ MustContain = ["=== Section Header ==="],
+
+ // Substrings that must NOT appear in stdout
+ MustNotContain = [],
+
+ // If true, only MustContain checks are used (no AI verification)
+ IsDeterministic = false,
+
+ // AI verification: natural-language descriptions of expected output
+ // Each entry describes one aspect to verify independently
+ ExpectedOutputDescription =
+ [
+ "The output should show structured person information with Name, Age, and Occupation fields.",
+ "The output should not contain error messages or stack traces.",
+ ],
+
+ // Stdin inputs to feed to the sample (for interactive samples)
+ Inputs = ["Y", "Y", "Y"],
+
+ // Delay between stdin inputs in ms (default 2000, increase for LLM calls between inputs)
+ InputDelayMs = 3000,
+}
+```
+
+## How to Add a New Sample Definition
+
+1. **Check the sample's Program.cs** to understand:
+ - What environment variables it reads (look for `GetEnvironmentVariable`)
+ - Whether it needs stdin input (look for `Console.ReadLine`, `Application.GetInput`)
+ - Whether it has an external loop (look for `EXIT` patterns in YAML workflows)
+ - What output it produces (section headers, markers, expected behavior)
+ - Whether it exits on its own or runs as a server
+
+2. **Choose the right verification strategy:**
+ - **Deterministic** (`IsDeterministic = true`): Use `MustContain` for samples with fixed output strings. No AI verification.
+ - **AI-verified** (default): Use `ExpectedOutputDescription` with semantic descriptions. Write expectations that are flexible enough for non-deterministic LLM output.
+ - **Both**: Use `MustContain` for fixed markers AND `ExpectedOutputDescription` for LLM-generated content.
+
+3. **Set `SkipReason` only for structural issues:**
+ - Web servers that don't exit
+ - Multi-process client/server architectures
+ - Samples requiring external infrastructure (MCP servers you can't reach, Docker, etc.)
+ - Do NOT skip for missing env vars — the tool checks those dynamically.
+
+4. **For interactive samples, provide `Inputs`:**
+ - Samples using `Application.GetInput(args)` need one initial input
+ - Samples with `Console.ReadLine()` approval loops need `"Y"` inputs
+ - YAML workflows with `externalLoop` need `"EXIT"` as the last input
+ - Set `InputDelayMs` to 3000-8000ms for samples with LLM calls between inputs
+
+5. **Add the definition** to the appropriate config file (e.g., `AgentsSamples.cs`) in the `All` list.
+
+6. **Register new categories** (if needed) in `VerifyOptions.cs` `s_sampleSets` dictionary.
+
+### Writing Good ExpectedOutputDescription
+
+- Write descriptions that are **semantically flexible** — LLM output varies between runs
+- Each array entry should describe **one independent aspect** to verify
+- Always include `"The output should not contain error messages or stack traces."` as the last entry
+- Avoid exact wording expectations — use "should mention", "should contain information about", "should show"
+- Bad: `"The output should say 'The weather in Amsterdam is cloudy with a high of 15°C'"`
+- Good: `"The output should contain weather information about Amsterdam mentioning cloudy weather with a high of 15°C."`
+
+### Example: Simple LLM Sample
+
+```csharp
+new SampleDefinition
+{
+ Name = "Agent_With_AzureOpenAIChatCompletion",
+ ProjectPath = "samples/02-agents/AgentProviders/Agent_With_AzureOpenAIChatCompletion",
+ RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
+ ExpectedOutputDescription =
+ [
+ "The output should contain a joke about a pirate.",
+ "The output should not contain error messages or stack traces.",
+ ],
+},
+```
+
+### Example: Deterministic Sample
+
+```csharp
+new SampleDefinition
+{
+ Name = "Workflow_Declarative_GenerateCode",
+ ProjectPath = "samples/03-workflows/Declarative/GenerateCode",
+ IsDeterministic = true,
+ MustContain = ["WORKFLOW: Parsing", "WORKFLOW: Defined"],
+ ExpectedOutputDescription = ["The output should show a YAML workflow being parsed and C# code being generated from it."],
+},
+```
+
+### Example: Interactive Sample with Approval Loop
+
+```csharp
+new SampleDefinition
+{
+ Name = "FoundryAgent_Hosted_MCP",
+ ProjectPath = "samples/02-agents/ModelContextProtocol/FoundryAgent_Hosted_MCP",
+ RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
+ Inputs = ["Y", "Y", "Y", "Y", "Y"],
+ InputDelayMs = 5000,
+ ExpectedOutputDescription = ["The output should show an agent using the Microsoft Learn MCP tool with approval prompts."],
+},
+```
+
+### Example: Declarative Workflow with External Loop
+
+```csharp
+new SampleDefinition
+{
+ Name = "Workflow_Declarative_FunctionTools",
+ ProjectPath = "samples/03-workflows/Declarative/FunctionTools",
+ RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
+ Inputs = ["What are today's specials?", "EXIT"],
+ InputDelayMs = 8000,
+ ExpectedOutputDescription = ["The output should show a workflow calling function tools to answer a question about restaurant specials."],
+},
+```
+
+### Example: Skipped Sample
+
+```csharp
+new SampleDefinition
+{
+ Name = "Agent_MCP_Server",
+ ProjectPath = "samples/02-agents/ModelContextProtocol/Agent_MCP_Server",
+ RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
+ SkipReason = "Runs as an MCP stdio server that does not exit on its own.",
+},
+```
diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx
index 9f0356fb87..f16a580519 100644
--- a/dotnet/agent-framework-dotnet.slnx
+++ b/dotnet/agent-framework-dotnet.slnx
@@ -7,6 +7,7 @@
+
diff --git a/dotnet/eng/verify-samples/AgentsSamples.cs b/dotnet/eng/verify-samples/AgentsSamples.cs
new file mode 100644
index 0000000000..87707a384e
--- /dev/null
+++ b/dotnet/eng/verify-samples/AgentsSamples.cs
@@ -0,0 +1,1252 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+namespace VerifySamples;
+
+///
+/// Defines the expected behavior for each sample in 02-agents.
+///
+internal static class AgentsSamples
+{
+ public static IReadOnlyList All { get; } =
+ [
+ // ── AgentProviders ──────────────────────────────────────────────────
+
+ new SampleDefinition
+ {
+ Name = "Agent_With_CustomImplementation",
+ ProjectPath = "samples/02-agents/AgentProviders/Agent_With_CustomImplementation",
+ RequiredEnvironmentVariables = [],
+ ExpectedOutputDescription =
+ [
+ "The output should contain uppercased text, because the custom agent converts all text to uppercase.",
+ "There should be two outputs — one from a non-streaming call and one from a streaming call.",
+ "The output should not contain error messages or stack traces.",
+ ],
+ },
+
+ new SampleDefinition
+ {
+ Name = "Agent_With_AzureOpenAIChatCompletion",
+ ProjectPath = "samples/02-agents/AgentProviders/Agent_With_AzureOpenAIChatCompletion",
+ RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_OPENAI_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_AzureOpenAIResponses",
+ ProjectPath = "samples/02-agents/AgentProviders/Agent_With_AzureOpenAIResponses",
+ RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
+ ExpectedOutputDescription =
+ [
+ "The output should contain two separate joke responses about a pirate.",
+ "The output should not contain error messages or stack traces.",
+ ],
+ },
+
+ 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",
+ ProjectPath = "samples/02-agents/AgentProviders/Agent_With_AzureAIProject",
+ RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
+ MustContain = ["Latest agent version id:"],
+ ExpectedOutputDescription =
+ [
+ "The output should show a 'Latest agent version id:' line, then joke responses from the agent.",
+ "The output should not contain error messages or stack traces.",
+ ],
+ },
+
+ new SampleDefinition
+ {
+ Name = "Agent_With_AzureFoundryModel",
+ ProjectPath = "samples/02-agents/AgentProviders/Agent_With_AzureFoundryModel",
+ RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_OPENAI_API_KEY", "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.",
+ ],
+ },
+
+ // ── Agents ──────────────────────────────────────────────────────────
+
+ new SampleDefinition
+ {
+ Name = "Agent_Step01_UsingFunctionToolsWithApprovals",
+ ProjectPath = "samples/02-agents/Agents/Agent_Step01_UsingFunctionToolsWithApprovals",
+ RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
+ Inputs = ["Tell me a joke about a pirate", ""],
+ InputDelayMs = 5000,
+ ExpectedOutputDescription =
+ [
+ "The output should show the agent responding to user input. The response may be about any topic — jokes, weather, or tool call results are all acceptable.",
+ "The output should not contain unhandled exception stack traces.",
+ ],
+ },
+
+ new SampleDefinition
+ {
+ Name = "Agent_Step02_StructuredOutput",
+ ProjectPath = "samples/02-agents/Agents/Agent_Step02_StructuredOutput",
+ RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
+ MustContain =
+ [
+ "=== Structured Output with ResponseFormat ===",
+ "Assistant Output (JSON):",
+ "Assistant Output (Deserialized):",
+ "=== Structured Output with RunAsync ===",
+ "=== Structured Output with RunStreamingAsync ===",
+ "=== Structured Output with UseStructuredOutput Middleware ===",
+ "Name:",
+ ],
+ ExpectedOutputDescription =
+ [
+ "The output should have four clearly separated sections for different structured output approaches.",
+ "The first section should include raw JSON output and then deserialized fields including 'Name:' with a city name.",
+ "Each subsequent section should also show 'Name:' followed by a city name.",
+ "The output should not contain error messages or stack traces.",
+ ],
+ },
+
+ new SampleDefinition
+ {
+ Name = "Agent_Step03_PersistedConversations",
+ ProjectPath = "samples/02-agents/Agents/Agent_Step03_PersistedConversations",
+ RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
+ MustContain = ["--- Serialized session ---"],
+ ExpectedOutputDescription =
+ [
+ "The output should start with a joke about a pirate.",
+ "After the joke there should be a '--- Serialized session ---' separator followed by a JSON block representing the serialized session state.",
+ "After the JSON block there should be a second response that retells the same joke in a pirate voice with emojis, demonstrating that context was preserved across serialization.",
+ "The output should not contain error messages or stack traces.",
+ ],
+ },
+
+ new SampleDefinition
+ {
+ Name = "Agent_Step04_3rdPartyChatHistoryStorage",
+ ProjectPath = "samples/02-agents/Agents/Agent_Step04_3rdPartyChatHistoryStorage",
+ RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
+ MustContain = ["--- Serialized session ---"],
+ ExpectedOutputDescription =
+ [
+ "The output should contain a pirate joke response and a '--- Serialized session ---' separator with session JSON.",
+ "It should show that the session was stored in a vector store, with a 'Session is stored in vector store under key:' line.",
+ "The output should not contain error messages or stack traces.",
+ ],
+ },
+
+ new SampleDefinition
+ {
+ Name = "Agent_Step06_DependencyInjection",
+ ProjectPath = "samples/02-agents/Agents/Agent_Step06_DependencyInjection",
+ RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
+ Inputs = ["Tell me a joke about a pirate", ""],
+ InputDelayMs = 5000,
+ ExpectedOutputDescription =
+ [
+ "The output should contain a joke about a pirate in response to the user's request.",
+ "The output should not contain error messages or stack traces.",
+ ],
+ },
+
+ new SampleDefinition
+ {
+ Name = "Agent_Step08_UsingImages",
+ ProjectPath = "samples/02-agents/Agents/Agent_Step08_UsingImages",
+ RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
+ ExpectedOutputDescription =
+ [
+ "The output should describe an image of a nature boardwalk/walkway scene.",
+ "It should mention elements like a wooden boardwalk or path, greenery or vegetation, and an outdoor or natural setting.",
+ "The output should not contain error messages or stack traces.",
+ ],
+ },
+
+ new SampleDefinition
+ {
+ Name = "Agent_Step09_AsFunctionTool",
+ ProjectPath = "samples/02-agents/Agents/Agent_Step09_AsFunctionTool",
+ RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
+ ExpectedOutputDescription =
+ [
+ "The output should be a response about the weather in Amsterdam, written in French.",
+ "The response should reference the tool result: cloudy weather with a high of 15°C.",
+ "The output should not contain error messages or stack traces.",
+ ],
+ },
+
+ new SampleDefinition
+ {
+ Name = "Agent_Step10_BackgroundResponsesWithToolsAndPersistence",
+ ProjectPath = "samples/02-agents/Agents/Agent_Step10_BackgroundResponsesWithToolsAndPersistence",
+ RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
+ ExpectedOutputDescription =
+ [
+ "The output should contain a generated novel or story.",
+ "The output may include tool invocation messages like '[ResearchSpaceFacts]' or '[GenerateCharacterProfiles]'.",
+ "The output should not contain error messages or stack traces.",
+ ],
+ },
+
+ new SampleDefinition
+ {
+ Name = "Agent_Step11_Middleware",
+ ProjectPath = "samples/02-agents/Agents/Agent_Step11_Middleware",
+ RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
+ // Example 4 prompts for approval; provide "Y" for each possible tool call
+ Inputs = ["Y", "Y", "Y"],
+ InputDelayMs = 3000,
+ ExpectedOutputDescription =
+ [
+ "The output should contain multiple examples demonstrating different middleware patterns.",
+ "It should include sections with '===' headers for different middleware examples.",
+ "The output should not contain error messages or stack traces.",
+ ],
+ },
+
+ new SampleDefinition
+ {
+ Name = "Agent_Step12_Plugins",
+ ProjectPath = "samples/02-agents/Agents/Agent_Step12_Plugins",
+ RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
+ ExpectedOutputDescription =
+ [
+ "The output should contain information about both the current time and the weather in Seattle.",
+ "The weather information should reference the plugin result: cloudy with a high of 15°C.",
+ "The output should not contain error messages or stack traces.",
+ ],
+ },
+
+ new SampleDefinition
+ {
+ Name = "Agent_Step13_ChatReduction",
+ ProjectPath = "samples/02-agents/Agents/Agent_Step13_ChatReduction",
+ RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
+ MustContain = ["Chat history has", "messages."],
+ ExpectedOutputDescription =
+ [
+ "The output should contain joke responses about a pirate, a robot, and a lemur.",
+ "Between each response there should be a 'Chat history has N messages.' line showing the message count.",
+ "There should be a fourth response after the user asks about the first joke. Due to chat reduction, the agent may not remember the pirate joke — any response is acceptable (including repeating another joke or saying it doesn't remember).",
+ "The output should not contain error messages or stack traces.",
+ ],
+ },
+
+ new SampleDefinition
+ {
+ Name = "Agent_Step14_BackgroundResponses",
+ ProjectPath = "samples/02-agents/Agents/Agent_Step14_BackgroundResponses",
+ RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
+ ExpectedOutputDescription =
+ [
+ "The output should contain a generated story or novel text about otters in space.",
+ "The text may appear in two parts: first a polled-to-completion result, then a streamed continuation.",
+ "The output should not contain error messages or stack traces.",
+ ],
+ },
+
+ new SampleDefinition
+ {
+ Name = "Agent_Step16_Declarative",
+ ProjectPath = "samples/02-agents/Agents/Agent_Step16_Declarative",
+ RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
+ ExpectedOutputDescription =
+ [
+ "The output should contain a response in JSON format with 'language' and 'answer' fields, since the declarative agent is configured to respond in JSON.",
+ "The content should be a joke about a pirate in English.",
+ "There should be both a non-streaming and streaming response.",
+ "The output should not contain error messages or stack traces.",
+ ],
+ },
+
+ new SampleDefinition
+ {
+ Name = "Agent_Step17_AdditionalAIContext",
+ ProjectPath = "samples/02-agents/Agents/Agent_Step17_AdditionalAIContext",
+ RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
+ ExpectedOutputDescription =
+ [
+ "The output should show a personal assistant managing a todo list across multiple turns.",
+ "The assistant should acknowledge adding items like picking up milk, taking Sally to soccer practice, and making a dentist appointment for Jimmy.",
+ "There should be a JSON block showing the serialized session state.",
+ "The final response should reference the calendar appointments (doctor at 15:00, team meeting at 17:00, birthday party at 20:00).",
+ "The output should not contain error messages or stack traces.",
+ ],
+ },
+
+ new SampleDefinition
+ {
+ Name = "Agent_Step18_CompactionPipeline",
+ ProjectPath = "samples/02-agents/Agents/Agent_Step18_CompactionPipeline",
+ RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
+ MustContain = ["[User]", "[Agent]"],
+ ExpectedOutputDescription =
+ [
+ "The output should show a turn-by-turn conversation between [User] and [Agent] about shopping for electronics (laptops, keyboards, mice).",
+ "The output may include '[Messages: #N]' lines showing chat history compaction.",
+ "The agent should provide information about product prices from tool results.",
+ "The output should not contain error messages or stack traces.",
+ ],
+ },
+
+ new SampleDefinition
+ {
+ Name = "Agent_Step19_InFunctionLoopCheckpointing",
+ ProjectPath = "samples/02-agents/Agents/Agent_Step19_InFunctionLoopCheckpointing",
+ RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME", "AZURE_OPENAI_RESPONSES_STORE"],
+ MustContain = ["=== Non-Streaming Mode ===", "=== Streaming Mode ==="],
+ ExpectedOutputDescription =
+ [
+ "The output should show non-streaming and streaming modes demonstrating in-function-loop checkpointing with multi-turn conversations.",
+ "The output should not contain error messages or stack traces.",
+ ],
+ },
+
+ // ── AgentSkills ─────────────────────────────────────────────────────
+
+ new SampleDefinition
+ {
+ Name = "Agent_Step01_FileBasedSkills",
+ ProjectPath = "samples/02-agents/AgentSkills/Agent_Step01_FileBasedSkills",
+ RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
+ MustContain =
+ [
+ "Converting units with file-based skills",
+ "Agent:",
+ ],
+ ExpectedOutputDescription =
+ [
+ "The output should show the agent converting 26.2 miles to kilometers and 75 kilograms to pounds.",
+ "The response should contain approximate numeric values for both conversions.",
+ "The output should not contain error messages or stack traces.",
+ ],
+ },
+
+ // ── AgentWithMemory ─────────────────────────────────────────────────
+
+ new SampleDefinition
+ {
+ Name = "AgentWithMemory_Step01_ChatHistoryMemory",
+ ProjectPath = "samples/02-agents/AgentWithMemory/AgentWithMemory_Step01_ChatHistoryMemory",
+ RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME", "AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME"],
+ ExpectedOutputDescription =
+ [
+ "The output should contain two joke responses.",
+ "The first joke should be about a pirate (as explicitly requested).",
+ "The second joke should also be pirate-themed or similar to what the user likes, since the memory system should recall the user's preference for pirate jokes from the first session.",
+ "The output should not contain error messages or stack traces.",
+ ],
+ },
+
+ new SampleDefinition
+ {
+ Name = "AgentWithMemory_Step04_MemoryUsingFoundry",
+ ProjectPath = "samples/02-agents/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry",
+ RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_AI_MEMORY_STORE_ID", "AZURE_AI_MODEL_DEPLOYMENT_NAME", "AZURE_AI_EMBEDDING_DEPLOYMENT_NAME"],
+ MustContain =
+ [
+ ">> Setting up Foundry Memory Store",
+ ">> Serialize and deserialize the session to demonstrate persisted state",
+ ">> Start a new session that shares the same Foundry Memory scope",
+ ],
+ ExpectedOutputDescription =
+ [
+ "The output should show a Foundry Memory Store being set up and processing updates.",
+ "After serialization/deserialization, the agent should recall previously learned information.",
+ "In the new session section, the agent should know facts from the earlier session due to shared Foundry Memory.",
+ "The output should not contain error messages or stack traces.",
+ ],
+ },
+
+ new SampleDefinition
+ {
+ Name = "AgentWithMemory_Step05_BoundedChatHistory",
+ ProjectPath = "samples/02-agents/AgentWithMemory/AgentWithMemory_Step05_BoundedChatHistory",
+ RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME", "AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME"],
+ MustContain =
+ [
+ "--- Filling the session window",
+ "--- Next exchange will trigger overflow to vector store ---",
+ "--- Asking about overflowed information",
+ ],
+ ExpectedOutputDescription =
+ [
+ "The output should demonstrate bounded chat history with overflow to a vector store.",
+ "After the window fills up and overflows, the agent should still be able to recall older information (like a favorite color) from the vector store.",
+ "The output should not contain error messages or stack traces.",
+ ],
+ },
+
+ // ── AgentWithRAG ────────────────────────────────────────────────────
+
+ new SampleDefinition
+ {
+ Name = "AgentWithRAG_Step01_BasicTextRAG",
+ ProjectPath = "samples/02-agents/AgentWithRAG/AgentWithRAG_Step01_BasicTextRAG",
+ RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME", "AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME"],
+ MustContain = [">> Asking about returns", ">> Asking about shipping", ">> Asking about product care"],
+ ExpectedOutputDescription =
+ [
+ "The returns section should mention a 30-day return policy, unused condition, and original packaging.",
+ "The shipping section should mention 3-5 business days for standard shipping.",
+ "The product care section should mention tent fabric maintenance tips like using lukewarm water, non-detergent soap, and air drying.",
+ "The output should not contain error messages or stack traces.",
+ ],
+ },
+
+ new SampleDefinition
+ {
+ Name = "AgentWithRAG_Step03_CustomRAGDataSource",
+ ProjectPath = "samples/02-agents/AgentWithRAG/AgentWithRAG_Step03_CustomRAGDataSource",
+ RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
+ MustContain = [">> Asking about returns", ">> Asking about shipping", ">> Asking about product care"],
+ ExpectedOutputDescription =
+ [
+ "The returns section should mention a 30-day return policy.",
+ "The shipping section should mention 3-5 business days for standard shipping.",
+ "The product care section should mention tent fabric maintenance tips.",
+ "The output should not contain error messages or stack traces.",
+ ],
+ },
+
+ new SampleDefinition
+ {
+ Name = "AgentWithRAG_Step04_FoundryServiceRAG",
+ ProjectPath = "samples/02-agents/AgentWithRAG/AgentWithRAG_Step04_FoundryServiceRAG",
+ RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
+ MustContain = [">> Asking about returns", ">> Asking about shipping", ">> Asking about product care"],
+ ExpectedOutputDescription =
+ [
+ "The returns section should mention a 30-day return policy.",
+ "The shipping section should mention standard shipping timeframes.",
+ "The product care section should mention tent fabric maintenance tips.",
+ "The output should not contain error messages or stack traces.",
+ ],
+ },
+
+ // ── AgentsWithFoundry ────────────────────────────────────────────────
+
+ new SampleDefinition
+ {
+ Name = "FoundryAgent_Step00_FoundryAgentLifecycle",
+ ProjectPath = "samples/02-agents/AgentsWithFoundry/Agent_Step00_FoundryAgentLifecycle",
+ 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 = "FoundryAgent_Step01_Basics",
+ ProjectPath = "samples/02-agents/AgentsWithFoundry/Agent_Step01_Basics",
+ RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
+ ExpectedOutputDescription =
+ [
+ "The output should contain a joke response from the agent.",
+ "The output should not contain error messages or stack traces.",
+ ],
+ },
+
+ new SampleDefinition
+ {
+ Name = "FoundryAgent_Step02.1_MultiturnConversation",
+ ProjectPath = "samples/02-agents/AgentsWithFoundry/Agent_Step02.1_MultiturnConversation",
+ RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
+ ExpectedOutputDescription =
+ [
+ "The output should contain multiple joke responses showing a multi-turn conversation.",
+ "There should be both non-streaming and streaming responses, with the second turn in each building on the first (e.g., adding emojis or pirate voice).",
+ "The output should not contain error messages or stack traces.",
+ ],
+ },
+
+ new SampleDefinition
+ {
+ Name = "FoundryAgent_Step02.2_MultiturnWithServerConversations",
+ ProjectPath = "samples/02-agents/AgentsWithFoundry/Agent_Step02.2_MultiturnWithServerConversations",
+ RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
+ ExpectedOutputDescription =
+ [
+ "The output should demonstrate server-side conversation sessions with non-streaming and streaming turns.",
+ "The output should not contain error messages or stack traces.",
+ ],
+ },
+
+ new SampleDefinition
+ {
+ Name = "FoundryAgent_Step03_UsingFunctionTools",
+ ProjectPath = "samples/02-agents/AgentsWithFoundry/Agent_Step03_UsingFunctionTools",
+ RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
+ ExpectedOutputDescription =
+ [
+ "The output should contain weather information about Amsterdam from a function tool.",
+ "The response should mention cloudy weather with a high of 15°C (from the canned tool response).",
+ "There should be both a non-streaming and streaming response.",
+ "The output should not contain error messages or stack traces.",
+ ],
+ },
+
+ new SampleDefinition
+ {
+ Name = "FoundryAgent_Step04_UsingFunctionToolsWithApprovals",
+ ProjectPath = "samples/02-agents/AgentsWithFoundry/Agent_Step04_UsingFunctionToolsWithApprovals",
+ RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
+ Inputs = ["Y", "Y", "Y"],
+ InputDelayMs = 3000,
+ ExpectedOutputDescription =
+ [
+ "The output should contain a prompt asking the user to approve a tool call, followed by weather information about Amsterdam.",
+ "The response should mention cloudy weather with a high of 15°C.",
+ "The output should not contain error messages or stack traces.",
+ ],
+ },
+
+ new SampleDefinition
+ {
+ Name = "FoundryAgent_Step05_StructuredOutput",
+ ProjectPath = "samples/02-agents/AgentsWithFoundry/Agent_Step05_StructuredOutput",
+ RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
+ MustContain = ["Assistant Output:", "Name:"],
+ ExpectedOutputDescription =
+ [
+ "The output should contain structured person information with Name, Age, and Occupation fields.",
+ "There should be both a direct structured output and a streamed-then-deserialized output.",
+ "The output should not contain error messages or stack traces.",
+ ],
+ },
+
+ new SampleDefinition
+ {
+ Name = "FoundryAgent_Step06_PersistedConversations",
+ ProjectPath = "samples/02-agents/AgentsWithFoundry/Agent_Step06_PersistedConversations",
+ RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
+ ExpectedOutputDescription =
+ [
+ "The output should contain a pirate joke, then after session persistence, a second response retelling the joke in pirate voice with emojis.",
+ "The output should not contain error messages or stack traces.",
+ ],
+ },
+
+ new SampleDefinition
+ {
+ Name = "FoundryAgent_Step08_DependencyInjection",
+ ProjectPath = "samples/02-agents/AgentsWithFoundry/Agent_Step08_DependencyInjection",
+ RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
+ Inputs = ["Tell me a joke about a pirate", ""],
+ InputDelayMs = 5000,
+ ExpectedOutputDescription =
+ [
+ "The output should contain a joke about a pirate in response to the user's request.",
+ "The output should not contain error messages or stack traces.",
+ ],
+ },
+
+ new SampleDefinition
+ {
+ Name = "FoundryAgent_Step10_UsingImages",
+ ProjectPath = "samples/02-agents/AgentsWithFoundry/Agent_Step10_UsingImages",
+ RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
+ ExpectedOutputDescription =
+ [
+ "The output should describe an image of a nature walkway or boardwalk scene.",
+ "It should mention elements like a wooden path, greenery, and an outdoor setting.",
+ "The output should not contain error messages or stack traces.",
+ ],
+ },
+
+ new SampleDefinition
+ {
+ Name = "FoundryAgent_Step11_AsFunctionTool",
+ ProjectPath = "samples/02-agents/AgentsWithFoundry/Agent_Step11_AsFunctionTool",
+ RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
+ ExpectedOutputDescription =
+ [
+ "The output should be a response about the weather in Amsterdam, written in French.",
+ "The response should reference the tool result: cloudy weather with a high of 15°C.",
+ "The output should not contain error messages or stack traces.",
+ ],
+ },
+
+ new SampleDefinition
+ {
+ Name = "FoundryAgent_Step12_Middleware",
+ ProjectPath = "samples/02-agents/AgentsWithFoundry/Agent_Step12_Middleware",
+ RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
+ Inputs = ["Y", "Y", "Y"],
+ InputDelayMs = 3000,
+ ExpectedOutputDescription =
+ [
+ "The output should contain multiple middleware examples with '===' section headers.",
+ "The human-in-the-loop example should show tool approval prompts and agent responses.",
+ "The output should not contain error messages or stack traces.",
+ ],
+ },
+
+ new SampleDefinition
+ {
+ Name = "FoundryAgent_Step13_Plugins",
+ ProjectPath = "samples/02-agents/AgentsWithFoundry/Agent_Step13_Plugins",
+ RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
+ ExpectedOutputDescription =
+ [
+ "The output should contain information about both the current time and the weather in Seattle.",
+ "The weather information should reference the plugin result: cloudy with a high of 15°C.",
+ "The output should not contain error messages or stack traces.",
+ ],
+ },
+
+ new SampleDefinition
+ {
+ Name = "FoundryAgent_Step14_CodeInterpreter",
+ ProjectPath = "samples/02-agents/AgentsWithFoundry/Agent_Step14_CodeInterpreter",
+ RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
+ ExpectedOutputDescription =
+ [
+ "The output should show the code interpreter being used to solve sin(x) + x^2 = 42, including a 'Code Input:' section with Python code.",
+ "It should show a 'Code Input:' section with Python code for the math problem.",
+ "It may show a 'Code Tool Result:' section with computed answers, or annotations with file references.",
+ "The output should not contain error messages or stack traces.",
+ ],
+ },
+
+ new SampleDefinition
+ {
+ Name = "FoundryAgent_Step16_FileSearch",
+ ProjectPath = "samples/02-agents/AgentsWithFoundry/Agent_Step16_FileSearch",
+ RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
+ MustContain = ["--- Running File Search Agent ---"],
+ ExpectedOutputDescription =
+ [
+ "The output should show a file being uploaded and indexed in a vector store, then an agent answering a question based on the file content.",
+ "The output should not contain error messages or stack traces.",
+ ],
+ },
+
+ new SampleDefinition
+ {
+ Name = "FoundryAgent_Step17_OpenAPITools",
+ ProjectPath = "samples/02-agents/AgentsWithFoundry/Agent_Step17_OpenAPITools",
+ RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
+ ExpectedOutputDescription =
+ [
+ "The output should contain a list of countries or information about countries that use the EUR currency.",
+ "The output should not contain error messages or stack traces.",
+ ],
+ },
+
+ // ── Skipped samples ─────────────────────────────────────────────────
+
+ new SampleDefinition
+ {
+ Name = "AgentOpenTelemetry",
+ ProjectPath = "samples/02-agents/AgentOpenTelemetry",
+ RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
+ SkipReason = "Requires Aspire Dashboard / Docker for OpenTelemetry collection.",
+ },
+
+ new SampleDefinition
+ {
+ Name = "Agent_With_A2A",
+ ProjectPath = "samples/02-agents/AgentProviders/Agent_With_A2A",
+ RequiredEnvironmentVariables = ["A2A_AGENT_HOST"],
+ SkipReason = "Requires an external A2A agent host.",
+ },
+
+ new SampleDefinition
+ {
+ Name = "Agent_With_Anthropic",
+ ProjectPath = "samples/02-agents/AgentProviders/Agent_With_Anthropic",
+ RequiredEnvironmentVariables = ["ANTHROPIC_API_KEY"],
+ OptionalEnvironmentVariables = ["ANTHROPIC_CHAT_MODEL_NAME", "ANTHROPIC_RESOURCE"],
+ 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_GitHubCopilot",
+ ProjectPath = "samples/02-agents/AgentProviders/Agent_With_GitHubCopilot",
+ RequiredEnvironmentVariables = [],
+ // The sample prompts for shell command approval; provide "Y" for each possible permission request
+ Inputs = ["Y", "Y", "Y"],
+ InputDelayMs = 3000,
+ ExpectedOutputDescription =
+ [
+ "The output should contain a user prompt and a response listing files in the current directory.",
+ "The output should not contain error messages or stack traces.",
+ ],
+ },
+
+ new SampleDefinition
+ {
+ Name = "Agent_With_GoogleGemini",
+ ProjectPath = "samples/02-agents/AgentProviders/Agent_With_GoogleGemini",
+ RequiredEnvironmentVariables = ["GOOGLE_GENAI_API_KEY"],
+ OptionalEnvironmentVariables = ["GOOGLE_GENAI_MODEL"],
+ MustContain =
+ [
+ "Google GenAI client based agent response:",
+ "Community client based agent response:",
+ ],
+ ExpectedOutputDescription =
+ [
+ "The output should contain two labeled sections, each with a joke about a pirate from a different Gemini client.",
+ "The output should not contain error messages or stack traces.",
+ ],
+ },
+
+ new SampleDefinition
+ {
+ Name = "Agent_With_ONNX",
+ ProjectPath = "samples/02-agents/AgentProviders/Agent_With_ONNX",
+ RequiredEnvironmentVariables = ["ONNX_MODEL_PATH"],
+ SkipReason = "Requires local ONNX model.",
+ },
+
+ new SampleDefinition
+ {
+ Name = "Agent_With_Ollama",
+ ProjectPath = "samples/02-agents/AgentProviders/Agent_With_Ollama",
+ RequiredEnvironmentVariables = ["OLLAMA_ENDPOINT", "OLLAMA_MODEL_NAME"],
+ SkipReason = "Requires local Ollama server.",
+ },
+
+ new SampleDefinition
+ {
+ Name = "Agent_With_OpenAIAssistants",
+ ProjectPath = "samples/02-agents/AgentProviders/Agent_With_OpenAIAssistants",
+ RequiredEnvironmentVariables = ["OPENAI_API_KEY"],
+ OptionalEnvironmentVariables = ["OPENAI_CHAT_MODEL_NAME"],
+ ExpectedOutputDescription =
+ [
+ "The output should contain a joke about a pirate from the OpenAI Assistants API.",
+ "The output should not contain error messages or stack traces.",
+ ],
+ },
+
+ new SampleDefinition
+ {
+ Name = "Agent_With_OpenAIChatCompletion",
+ ProjectPath = "samples/02-agents/AgentProviders/Agent_With_OpenAIChatCompletion",
+ RequiredEnvironmentVariables = ["OPENAI_API_KEY"],
+ OptionalEnvironmentVariables = ["OPENAI_CHAT_MODEL_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_OpenAIResponses",
+ ProjectPath = "samples/02-agents/AgentProviders/Agent_With_OpenAIResponses",
+ RequiredEnvironmentVariables = ["OPENAI_API_KEY"],
+ OptionalEnvironmentVariables = ["OPENAI_CHAT_MODEL_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_Step05_Observability",
+ ProjectPath = "samples/02-agents/Agents/Agent_Step05_Observability",
+ RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME", "APPLICATIONINSIGHTS_CONNECTION_STRING"],
+ SkipReason = "Requires Application Insights / OpenTelemetry infrastructure.",
+ },
+
+ new SampleDefinition
+ {
+ Name = "Agent_Step07_AsMcpTool",
+ ProjectPath = "samples/02-agents/Agents/Agent_Step07_AsMcpTool",
+ RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
+ SkipReason = "Runs as an MCP stdio server that does not exit on its own.",
+ },
+
+ new SampleDefinition
+ {
+ Name = "Agent_Step15_DeepResearch",
+ ProjectPath = "samples/02-agents/Agents/Agent_Step15_DeepResearch",
+ RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT", "AZURE_AI_MODEL_DEPLOYMENT_NAME", "AZURE_AI_BING_CONNECTION_ID"],
+ OptionalEnvironmentVariables = ["AZURE_AI_REASONING_DEPLOYMENT_NAME"],
+ SkipReason = "Requires Azure AI Foundry project with Bing search connection.",
+ },
+
+ new SampleDefinition
+ {
+ Name = "Agent_Anthropic_Step01_Running",
+ ProjectPath = "samples/02-agents/AgentWithAnthropic/Agent_Anthropic_Step01_Running",
+ RequiredEnvironmentVariables = ["ANTHROPIC_API_KEY"],
+ OptionalEnvironmentVariables = ["ANTHROPIC_CHAT_MODEL_NAME"],
+ ExpectedOutputDescription =
+ [
+ "The output should contain a joke about a pirate.",
+ "There should be two responses — one from a non-streaming call and one from a streaming call.",
+ "The output should not contain error messages or stack traces.",
+ ],
+ },
+
+ new SampleDefinition
+ {
+ Name = "Agent_Anthropic_Step02_Reasoning",
+ ProjectPath = "samples/02-agents/AgentWithAnthropic/Agent_Anthropic_Step02_Reasoning",
+ RequiredEnvironmentVariables = ["ANTHROPIC_API_KEY"],
+ OptionalEnvironmentVariables = ["ANTHROPIC_CHAT_MODEL_NAME"],
+ MustContain =
+ [
+ "1. Non-streaming:",
+ "#### Start Thinking ####",
+ "#### End Thinking ####",
+ "#### Final Answer ####",
+ "Token usage:",
+ "2. Streaming",
+ ],
+ ExpectedOutputDescription =
+ [
+ "The non-streaming section should show the agent's reasoning about a math problem, followed by a final answer.",
+ "The streaming section should show reasoning and a response about the theory of relativity.",
+ "The output should not contain error messages or stack traces.",
+ ],
+ },
+
+ new SampleDefinition
+ {
+ Name = "Agent_Anthropic_Step03_UsingFunctionTools",
+ ProjectPath = "samples/02-agents/AgentWithAnthropic/Agent_Anthropic_Step03_UsingFunctionTools",
+ RequiredEnvironmentVariables = ["ANTHROPIC_API_KEY"],
+ OptionalEnvironmentVariables = ["ANTHROPIC_CHAT_MODEL_NAME"],
+ ExpectedOutputDescription =
+ [
+ "The output should contain information about the weather in Amsterdam.",
+ "There should be two responses — one from a non-streaming call and one from a streaming call.",
+ "The output should not contain error messages or stack traces.",
+ ],
+ },
+
+ new SampleDefinition
+ {
+ Name = "Agent_Anthropic_Step04_UsingSkills",
+ ProjectPath = "samples/02-agents/AgentWithAnthropic/Agent_Anthropic_Step04_UsingSkills",
+ RequiredEnvironmentVariables = ["ANTHROPIC_API_KEY"],
+ OptionalEnvironmentVariables = ["ANTHROPIC_CHAT_MODEL_NAME"],
+ MustContain =
+ [
+ "Creating a presentation about renewable energy...",
+ "#### Agent Response ####",
+ ],
+ ExpectedOutputDescription =
+ [
+ "The output should show the agent creating a presentation about renewable energy.",
+ "There should be an agent response section with content about renewable energy sources.",
+ "The output should not contain error messages or stack traces.",
+ ],
+ },
+
+ new SampleDefinition
+ {
+ Name = "AgentWithMemory_Step02_MemoryUsingMem0",
+ ProjectPath = "samples/02-agents/AgentWithMemory/AgentWithMemory_Step02_MemoryUsingMem0",
+ RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT", "AZURE_OPENAI_DEPLOYMENT_NAME", "MEM0_ENDPOINT", "MEM0_API_KEY"],
+ SkipReason = "Requires Mem0 service.",
+ },
+
+ new SampleDefinition
+ {
+ Name = "Agent_OpenAI_Step01_Running",
+ ProjectPath = "samples/02-agents/AgentWithOpenAI/Agent_OpenAI_Step01_Running",
+ RequiredEnvironmentVariables = ["OPENAI_API_KEY"],
+ OptionalEnvironmentVariables = ["OPENAI_CHAT_MODEL_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_OpenAI_Step02_Reasoning",
+ ProjectPath = "samples/02-agents/AgentWithOpenAI/Agent_OpenAI_Step02_Reasoning",
+ RequiredEnvironmentVariables = ["OPENAI_API_KEY"],
+ OptionalEnvironmentVariables = ["OPENAI_CHAT_MODEL_NAME"],
+ MustContain =
+ [
+ "1. Non-streaming:",
+ "Token usage:",
+ "2. Streaming",
+ ],
+ ExpectedOutputDescription =
+ [
+ "The non-streaming section should show the agent's reasoning about a math problem, followed by a final answer.",
+ "The streaming section should show reasoning and a response about the theory of relativity.",
+ "The output should not contain error messages or stack traces.",
+ ],
+ },
+
+ new SampleDefinition
+ {
+ Name = "Agent_OpenAI_Step03_CreateFromChatClient",
+ ProjectPath = "samples/02-agents/AgentWithOpenAI/Agent_OpenAI_Step03_CreateFromChatClient",
+ RequiredEnvironmentVariables = ["OPENAI_API_KEY"],
+ OptionalEnvironmentVariables = ["OPENAI_CHAT_MODEL_NAME"],
+ ExpectedOutputDescription =
+ [
+ "The output should contain a joke about a pirate.",
+ "There should be two responses — one from a non-streaming call and one from a streaming call.",
+ "The output should not contain error messages or stack traces.",
+ ],
+ },
+
+ new SampleDefinition
+ {
+ Name = "Agent_OpenAI_Step04_CreateFromOpenAIResponseClient",
+ ProjectPath = "samples/02-agents/AgentWithOpenAI/Agent_OpenAI_Step04_CreateFromOpenAIResponseClient",
+ RequiredEnvironmentVariables = ["OPENAI_API_KEY"],
+ OptionalEnvironmentVariables = ["OPENAI_CHAT_MODEL_NAME"],
+ ExpectedOutputDescription =
+ [
+ "The output should contain a joke about a pirate.",
+ "There should be two responses — one from a non-streaming call and one from a streaming call.",
+ "The output should not contain error messages or stack traces.",
+ ],
+ },
+
+ new SampleDefinition
+ {
+ Name = "Agent_OpenAI_Step05_Conversation",
+ ProjectPath = "samples/02-agents/AgentWithOpenAI/Agent_OpenAI_Step05_Conversation",
+ RequiredEnvironmentVariables = ["OPENAI_API_KEY"],
+ OptionalEnvironmentVariables = ["OPENAI_CHAT_MODEL_NAME"],
+ MustContain =
+ [
+ "=== Multi-turn Conversation Demo ===",
+ "Conversation created.",
+ "Conversation ID:",
+ ],
+ ExpectedOutputDescription =
+ [
+ "The output should show a multi-turn conversation about France: capital, landmarks, and height of the most famous one.",
+ "The output should show the conversation history retrieved from the server.",
+ "The output should not contain error messages or stack traces.",
+ ],
+ },
+
+ new SampleDefinition
+ {
+ Name = "AgentWithRAG_Step02_CustomVectorStoreRAG",
+ ProjectPath = "samples/02-agents/AgentWithRAG/AgentWithRAG_Step02_CustomVectorStoreRAG",
+ RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME", "AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME"],
+ SkipReason = "Requires external Qdrant vector store.",
+ },
+
+ new SampleDefinition
+ {
+ Name = "DeclarativeAgents_ChatClient",
+ ProjectPath = "samples/02-agents/DeclarativeAgents/ChatClient",
+ RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
+ SkipReason = "Requires command-line arguments (YAML file path) with no YAML files checked in.",
+ },
+
+ new SampleDefinition
+ {
+ Name = "DevUI_Step01_BasicUsage",
+ ProjectPath = "samples/02-agents/DevUI/DevUI_Step01_BasicUsage",
+ RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
+ SkipReason = "ASP.NET Core web server that does not exit on its own.",
+ },
+
+ new SampleDefinition
+ {
+ Name = "FoundryAgent_Step07_Observability",
+ ProjectPath = "samples/02-agents/AgentsWithFoundry/Agent_Step07_Observability",
+ RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_AI_MODEL_DEPLOYMENT_NAME", "APPLICATIONINSIGHTS_CONNECTION_STRING"],
+ SkipReason = "Requires Application Insights / OpenTelemetry infrastructure.",
+ },
+
+ new SampleDefinition
+ {
+ Name = "FoundryAgent_Step09_UsingMcpClientAsTools",
+ ProjectPath = "samples/02-agents/AgentsWithFoundry/Agent_Step09_UsingMcpClientAsTools",
+ RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
+ ExpectedOutputDescription =
+ [
+ "The output should show an agent using the Microsoft Learn MCP tool to search or retrieve documentation.",
+ "The output should not contain error messages or stack traces.",
+ ],
+ },
+
+ new SampleDefinition
+ {
+ Name = "FoundryAgent_Step15_ComputerUse",
+ ProjectPath = "samples/02-agents/AgentsWithFoundry/Agent_Step15_ComputerUse",
+ RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
+ ExpectedOutputDescription = ["The output should show a computer automation session processing simulated browser screenshots with iteration steps and a final response describing search results."],
+ },
+
+ new SampleDefinition
+ {
+ Name = "FoundryAgent_Step18_BingCustomSearch",
+ ProjectPath = "samples/02-agents/AgentsWithFoundry/Agent_Step18_BingCustomSearch",
+ RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT", "AZURE_AI_MODEL_DEPLOYMENT_NAME", "AZURE_AI_CUSTOM_SEARCH_CONNECTION_ID", "AZURE_AI_CUSTOM_SEARCH_INSTANCE_NAME"],
+ SkipReason = "Requires Bing Custom Search connection.",
+ },
+
+ new SampleDefinition
+ {
+ Name = "FoundryAgent_Step19_SharePoint",
+ ProjectPath = "samples/02-agents/AgentsWithFoundry/Agent_Step19_SharePoint",
+ RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT", "AZURE_AI_MODEL_DEPLOYMENT_NAME", "SHAREPOINT_PROJECT_CONNECTION_ID"],
+ SkipReason = "Requires SharePoint connection.",
+ },
+
+ new SampleDefinition
+ {
+ Name = "FoundryAgent_Step20_MicrosoftFabric",
+ ProjectPath = "samples/02-agents/AgentsWithFoundry/Agent_Step20_MicrosoftFabric",
+ RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT", "AZURE_AI_MODEL_DEPLOYMENT_NAME", "FABRIC_PROJECT_CONNECTION_ID"],
+ SkipReason = "Requires Microsoft Fabric connection.",
+ },
+
+ new SampleDefinition
+ {
+ Name = "FoundryAgent_Step21_WebSearch",
+ ProjectPath = "samples/02-agents/AgentsWithFoundry/Agent_Step21_WebSearch",
+ RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
+ ExpectedOutputDescription =
+ [
+ "The output should show an agent using web search to answer a question, with response text and citation annotations.",
+ "The output should not contain error messages or stack traces.",
+ ],
+ },
+
+ new SampleDefinition
+ {
+ Name = "FoundryAgent_Step22_MemorySearch",
+ ProjectPath = "samples/02-agents/AgentsWithFoundry/Agent_Step22_MemorySearch",
+ RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT", "AZURE_AI_MODEL_DEPLOYMENT_NAME", "AZURE_AI_EMBEDDING_DEPLOYMENT_NAME"],
+ OptionalEnvironmentVariables = ["AZURE_AI_MEMORY_STORE_ID"],
+ MustContain = ["Agent created with Memory Search tool. Starting conversation..."],
+ ExpectedOutputDescription =
+ [
+ "The output should show a memory store being created, memories stored from a prior conversation, and an agent querying those memories.",
+ "The output should not contain error messages or stack traces.",
+ ],
+ },
+
+ new SampleDefinition
+ {
+ Name = "FoundryAgent_Step23_LocalMCP",
+ ProjectPath = "samples/02-agents/AgentsWithFoundry/Agent_Step23_LocalMCP",
+ RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
+ ExpectedOutputDescription = ["The output should show an agent using the Microsoft Learn MCP server to search for documentation and provide a response."],
+ },
+
+ new SampleDefinition
+ {
+ Name = "FoundryAgent_Hosted_MCP",
+ ProjectPath = "samples/02-agents/ModelContextProtocol/FoundryAgent_Hosted_MCP",
+ RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
+ Inputs = ["Y", "Y", "Y", "Y", "Y"],
+ InputDelayMs = 5000,
+ ExpectedOutputDescription = ["The output should contain a summary or information about Azure AI documentation from Microsoft Learn."],
+ },
+
+ new SampleDefinition
+ {
+ Name = "ResponseAgent_Hosted_MCP",
+ ProjectPath = "samples/02-agents/ModelContextProtocol/ResponseAgent_Hosted_MCP",
+ RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
+ Inputs = ["Y", "Y", "Y", "Y", "Y"],
+ InputDelayMs = 5000,
+ ExpectedOutputDescription = ["The output should contain a summary or information about Azure AI documentation from Microsoft Learn."],
+ },
+
+ new SampleDefinition
+ {
+ Name = "Agent_MCP_Server",
+ ProjectPath = "samples/02-agents/ModelContextProtocol/Agent_MCP_Server",
+ RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
+ SkipReason = "Runs as an MCP stdio server that does not exit on its own.",
+ },
+
+ new SampleDefinition
+ {
+ Name = "Agent_MCP_Server_Auth",
+ ProjectPath = "samples/02-agents/ModelContextProtocol/Agent_MCP_Server_Auth",
+ RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
+ SkipReason = "Runs as an MCP stdio server that does not exit on its own.",
+ },
+
+ new SampleDefinition
+ {
+ Name = "AGUI_Step01_GettingStarted_Client",
+ ProjectPath = "samples/02-agents/AGUI/Step01_GettingStarted/Client",
+ RequiredEnvironmentVariables = [],
+ SkipReason = "Multi-process client/server architecture; requires AGUI server running.",
+ },
+
+ new SampleDefinition
+ {
+ Name = "AGUI_Step01_GettingStarted_Server",
+ ProjectPath = "samples/02-agents/AGUI/Step01_GettingStarted/Server",
+ RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
+ SkipReason = "ASP.NET Core web server that does not exit on its own.",
+ },
+
+ new SampleDefinition
+ {
+ Name = "AGUI_Step02_BackendTools_Client",
+ ProjectPath = "samples/02-agents/AGUI/Step02_BackendTools/Client",
+ RequiredEnvironmentVariables = [],
+ SkipReason = "Multi-process client/server architecture; requires AGUI server running.",
+ },
+
+ new SampleDefinition
+ {
+ Name = "AGUI_Step02_BackendTools_Server",
+ ProjectPath = "samples/02-agents/AGUI/Step02_BackendTools/Server",
+ RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
+ SkipReason = "ASP.NET Core web server that does not exit on its own.",
+ },
+
+ new SampleDefinition
+ {
+ Name = "AGUI_Step03_FrontendTools_Client",
+ ProjectPath = "samples/02-agents/AGUI/Step03_FrontendTools/Client",
+ RequiredEnvironmentVariables = [],
+ SkipReason = "Multi-process client/server architecture; requires AGUI server running.",
+ },
+
+ new SampleDefinition
+ {
+ Name = "AGUI_Step03_FrontendTools_Server",
+ ProjectPath = "samples/02-agents/AGUI/Step03_FrontendTools/Server",
+ RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
+ SkipReason = "ASP.NET Core web server that does not exit on its own.",
+ },
+
+ new SampleDefinition
+ {
+ Name = "AGUI_Step04_HumanInLoop_Client",
+ ProjectPath = "samples/02-agents/AGUI/Step04_HumanInLoop/Client",
+ RequiredEnvironmentVariables = [],
+ SkipReason = "Multi-process client/server architecture; requires AGUI server running.",
+ },
+
+ new SampleDefinition
+ {
+ Name = "AGUI_Step04_HumanInLoop_Server",
+ ProjectPath = "samples/02-agents/AGUI/Step04_HumanInLoop/Server",
+ RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
+ SkipReason = "ASP.NET Core web server that does not exit on its own.",
+ },
+
+ new SampleDefinition
+ {
+ Name = "AGUI_Step05_StateManagement_Client",
+ ProjectPath = "samples/02-agents/AGUI/Step05_StateManagement/Client",
+ RequiredEnvironmentVariables = [],
+ SkipReason = "Multi-process client/server architecture; requires AGUI server running.",
+ },
+
+ new SampleDefinition
+ {
+ Name = "AGUI_Step05_StateManagement_Server",
+ ProjectPath = "samples/02-agents/AGUI/Step05_StateManagement/Server",
+ RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
+ SkipReason = "ASP.NET Core web server that does not exit on its own.",
+ },
+ ];
+}
diff --git a/dotnet/eng/verify-samples/ConsoleReporter.cs b/dotnet/eng/verify-samples/ConsoleReporter.cs
new file mode 100644
index 0000000000..0b21138d79
--- /dev/null
+++ b/dotnet/eng/verify-samples/ConsoleReporter.cs
@@ -0,0 +1,95 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+namespace VerifySamples;
+
+///
+/// Thread-safe console output with sample-name prefixes and colored status.
+///
+internal sealed class ConsoleReporter
+{
+ private readonly object _lock = new();
+
+ ///
+ /// Writes a complete prefixed line atomically to the console.
+ ///
+ public void WriteLineWithPrefix(string sampleName, string message, ConsoleColor? color = null)
+ {
+ lock (this._lock)
+ {
+ Console.ForegroundColor = ConsoleColor.Cyan;
+ Console.Write($"[{sampleName}] ");
+ if (color.HasValue)
+ {
+ Console.ForegroundColor = color.Value;
+ }
+ else
+ {
+ Console.ResetColor();
+ }
+
+ Console.WriteLine(message);
+ Console.ResetColor();
+ }
+ }
+
+ ///
+ /// Prints the final summary table and elapsed time to the console.
+ ///
+ public void PrintSummary(
+ IReadOnlyList orderedResults,
+ IReadOnlyList<(string Name, string Reason)> skipped,
+ TimeSpan elapsed)
+ {
+ var passCount = orderedResults.Count(r => r.Passed);
+ var failCount = orderedResults.Count(r => !r.Passed);
+
+ Console.WriteLine();
+ Console.WriteLine(new string('─', 60));
+ Console.ForegroundColor = ConsoleColor.White;
+ Console.WriteLine("SUMMARY");
+ Console.ResetColor();
+
+ foreach (var result in orderedResults)
+ {
+ Console.ForegroundColor = result.Passed ? ConsoleColor.Green : ConsoleColor.Red;
+ Console.Write(result.Passed ? " ✓ " : " ✗ ");
+ Console.ResetColor();
+ Console.WriteLine($"{result.SampleName}: {result.Summary}");
+ }
+
+ foreach (var (name, reason) in skipped)
+ {
+ Console.ForegroundColor = ConsoleColor.Yellow;
+ Console.Write(" ○ ");
+ Console.ResetColor();
+ Console.WriteLine($"{name}: Skipped — {reason}");
+ }
+
+ Console.WriteLine();
+ Console.Write("Results: ");
+ Console.ForegroundColor = ConsoleColor.Green;
+ Console.Write($"{passCount} passed");
+ Console.ResetColor();
+
+ if (failCount > 0)
+ {
+ Console.Write(", ");
+ Console.ForegroundColor = ConsoleColor.Red;
+ Console.Write($"{failCount} failed");
+ Console.ResetColor();
+ }
+
+ if (skipped.Count > 0)
+ {
+ Console.Write(", ");
+ Console.ForegroundColor = ConsoleColor.Yellow;
+ Console.Write($"{skipped.Count} skipped");
+ Console.ResetColor();
+ }
+
+ Console.WriteLine();
+ Console.ForegroundColor = ConsoleColor.DarkGray;
+ Console.WriteLine($"Elapsed: {elapsed.Hours:D2}:{elapsed.Minutes:D2}:{elapsed.Seconds:D2}");
+ Console.ResetColor();
+ }
+}
diff --git a/dotnet/eng/verify-samples/CsvResultWriter.cs b/dotnet/eng/verify-samples/CsvResultWriter.cs
new file mode 100644
index 0000000000..9a1128dcba
--- /dev/null
+++ b/dotnet/eng/verify-samples/CsvResultWriter.cs
@@ -0,0 +1,56 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text;
+
+namespace VerifySamples;
+
+///
+/// Writes a CSV summary of sample verification results.
+///
+internal static class CsvResultWriter
+{
+ ///
+ /// Writes the results to a CSV file at the specified path.
+ ///
+ public static async Task WriteAsync(
+ string path,
+ IReadOnlyList orderedResults,
+ IReadOnlyList<(string Name, string Reason)> skipped,
+ IReadOnlyList samples)
+ {
+ var pathLookup = samples.ToDictionary(s => s.Name, s => s.ProjectPath);
+
+ var sb = new StringBuilder();
+ sb.AppendLine("Sample,ProjectPath,Status,FailedChecks,Failures");
+
+ foreach (var result in orderedResults)
+ {
+ var status = result.Passed ? "PASSED" : "FAILED";
+ var failedChecks = result.Failures.Count;
+ var failures = string.Join("; ", result.Failures);
+ pathLookup.TryGetValue(result.SampleName, out var projectPath);
+ sb.AppendLine($"{CsvEscape(result.SampleName)},{CsvEscape(projectPath ?? "")},{status},{failedChecks},{CsvEscape(failures)}");
+ }
+
+ foreach (var (name, reason) in skipped)
+ {
+ pathLookup.TryGetValue(name, out var projectPath);
+ sb.AppendLine($"{CsvEscape(name)},{CsvEscape(projectPath ?? "")},SKIPPED,0,{CsvEscape(reason)}");
+ }
+
+ await File.WriteAllTextAsync(path, sb.ToString());
+ }
+
+ ///
+ /// Escapes a value for CSV: wraps in quotes if it contains commas, quotes, or newlines.
+ ///
+ private static string CsvEscape(string value)
+ {
+ if (value.Contains('"') || value.Contains(',') || value.Contains('\n') || value.Contains('\r'))
+ {
+ return $"\"{value.Replace("\"", "\"\"")}\"";
+ }
+
+ return value;
+ }
+}
diff --git a/dotnet/eng/verify-samples/GetStartedSamples.cs b/dotnet/eng/verify-samples/GetStartedSamples.cs
new file mode 100644
index 0000000000..9298e39388
--- /dev/null
+++ b/dotnet/eng/verify-samples/GetStartedSamples.cs
@@ -0,0 +1,105 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+namespace VerifySamples;
+
+///
+/// Defines the expected behavior for each sample in 01-get-started.
+///
+internal static class GetStartedSamples
+{
+ public static IReadOnlyList All { get; } =
+ [
+ new SampleDefinition
+ {
+ Name = "05_first_workflow",
+ ProjectPath = "samples/01-get-started/05_first_workflow",
+ RequiredEnvironmentVariables = [],
+ IsDeterministic = true,
+ MustContain =
+ [
+ "UppercaseExecutor: HELLO, WORLD!",
+ "ReverseTextExecutor: !DLROW ,OLLEH",
+ ],
+ },
+
+ new SampleDefinition
+ {
+ Name = "01_hello_agent",
+ ProjectPath = "samples/01-get-started/01_hello_agent",
+ RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
+ ExpectedOutputDescription =
+ [
+ "The output should contain a joke about a pirate.",
+ "There should be two separate joke responses — one from a non-streaming call and one from a streaming call.",
+ "The output should not contain error messages or stack traces.",
+ ],
+ },
+
+ new SampleDefinition
+ {
+ Name = "02_add_tools",
+ ProjectPath = "samples/01-get-started/02_add_tools",
+ RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
+ MustContain = [],
+ ExpectedOutputDescription =
+ [
+ "The output should contain information about the weather in Amsterdam.",
+ "The response should mention that it is cloudy with a high of 15°C (or equivalent), since this comes from a tool that returns a canned response.",
+ "There should be two responses — one from a non-streaming call and one from a streaming call.",
+ "The output should not contain error messages or stack traces.",
+ ],
+ },
+
+ new SampleDefinition
+ {
+ Name = "03_multi_turn",
+ ProjectPath = "samples/01-get-started/03_multi_turn",
+ RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
+ ExpectedOutputDescription =
+ [
+ "The output should contain a joke about a pirate.",
+ "After the initial joke, there should be a modified version that includes emojis and is told in the voice of a pirate's parrot.",
+ "The pattern repeats: first a non-streaming pirate joke + parrot version, then a streaming pirate joke + parrot version.",
+ "The output should not contain error messages or stack traces.",
+ ],
+ },
+
+ new SampleDefinition
+ {
+ Name = "04_memory",
+ ProjectPath = "samples/01-get-started/04_memory",
+ RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
+ MustContain =
+ [
+ ">> Use session with blank memory",
+ ">> Use deserialized session with previously created memories",
+ ">> Read memories using memory component",
+ "MEMORY - User Name:",
+ "MEMORY - User Age:",
+ ">> Use new session with previously created memories",
+ ],
+ ExpectedOutputDescription =
+ [
+ "In the 'Use session with blank memory' section, the agent should respond to the user's messages. It may ask for the user's name or age if not yet known.",
+ "In the 'Use deserialized session with previously created memories' section, the agent should correctly recall that the user's name is Ruaidhrí and age is 20.",
+ "The 'MEMORY - User Name:' line should show 'Ruaidhrí' (or a close transliteration).",
+ "The 'MEMORY - User Age:' line should show '20'.",
+ "In the 'Use new session with previously created memories' section, the agent should know the user's name and age from the transferred memory.",
+ "The output should not contain error messages or stack traces.",
+ ],
+ },
+
+ new SampleDefinition
+ {
+ Name = "06_host_your_agent",
+ ProjectPath = "samples/01-get-started/06_host_your_agent",
+ RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
+ SkipReason = "Requires Azure Functions Core Tools runtime and starts a web server.",
+ },
+ ];
+}
diff --git a/dotnet/eng/verify-samples/LogFileWriter.cs b/dotnet/eng/verify-samples/LogFileWriter.cs
new file mode 100644
index 0000000000..a46096f3d6
--- /dev/null
+++ b/dotnet/eng/verify-samples/LogFileWriter.cs
@@ -0,0 +1,153 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text;
+
+namespace VerifySamples;
+
+///
+/// Incrementally writes a sequential (non-interleaved) log file, appending after each sample completes.
+/// Thread-safe: multiple parallel tasks may call write methods concurrently.
+///
+internal sealed class LogFileWriter : IDisposable
+{
+ private readonly string _path;
+ private readonly SemaphoreSlim _writeLock = new(1, 1);
+
+ public LogFileWriter(string path)
+ {
+ this._path = path;
+ }
+
+ ///
+ public void Dispose()
+ {
+ this._writeLock.Dispose();
+ }
+
+ ///
+ /// Writes the log file header. Call once at the start of the run.
+ ///
+ public async Task WriteHeaderAsync()
+ {
+ var sb = new StringBuilder();
+ sb.AppendLine($"Sample Verification Log — {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss} UTC");
+ sb.AppendLine(new string('═', 72));
+ sb.AppendLine();
+
+ await File.WriteAllTextAsync(this._path, sb.ToString());
+ }
+
+ ///
+ /// Appends a skipped-sample entry to the log file.
+ ///
+ public async Task WriteSkippedAsync(string name, string reason)
+ {
+ var sb = new StringBuilder();
+ sb.AppendLine($"── {name} ──");
+ sb.AppendLine($"Status: SKIPPED — {reason}");
+ sb.AppendLine();
+
+ await this.AppendAsync(sb.ToString());
+ }
+
+ ///
+ /// Appends a completed sample's full output section to the log file.
+ ///
+ public async Task WriteSampleResultAsync(VerificationResult result)
+ {
+ var sb = new StringBuilder();
+ sb.AppendLine(new string('─', 72));
+ sb.AppendLine($"── {result.SampleName} ──");
+ sb.AppendLine($"Status: {(result.Passed ? "PASSED" : "FAILED")}");
+ sb.AppendLine();
+
+ foreach (var line in result.LogLines)
+ {
+ sb.AppendLine(line);
+ }
+
+ sb.AppendLine();
+
+ if (!string.IsNullOrWhiteSpace(result.Stdout))
+ {
+ sb.AppendLine("--- stdout ---");
+ sb.AppendLine(result.Stdout.TrimEnd());
+ sb.AppendLine("--- end stdout ---");
+ sb.AppendLine();
+ }
+
+ if (!string.IsNullOrWhiteSpace(result.Stderr))
+ {
+ sb.AppendLine("--- stderr ---");
+ sb.AppendLine(result.Stderr.TrimEnd());
+ sb.AppendLine("--- end stderr ---");
+ sb.AppendLine();
+ }
+
+ if (result.Failures.Count > 0)
+ {
+ sb.AppendLine("Failures:");
+ foreach (var failure in result.Failures)
+ {
+ sb.AppendLine($" ✗ {failure}");
+ }
+
+ sb.AppendLine();
+ }
+
+ if (result.AIReasoning is not null)
+ {
+ sb.AppendLine("AI Reasoning:");
+ sb.AppendLine(result.AIReasoning);
+ sb.AppendLine();
+ }
+
+ await this.AppendAsync(sb.ToString());
+ }
+
+ ///
+ /// Appends the final summary section and elapsed time to the log file.
+ ///
+ public async Task WriteSummaryAsync(
+ IReadOnlyList orderedResults,
+ IReadOnlyList<(string Name, string Reason)> skipped,
+ TimeSpan elapsed)
+ {
+ var passCount = orderedResults.Count(r => r.Passed);
+ var failCount = orderedResults.Count(r => !r.Passed);
+
+ var sb = new StringBuilder();
+ sb.AppendLine(new string('═', 72));
+ sb.AppendLine("SUMMARY");
+ sb.AppendLine();
+
+ foreach (var result in orderedResults)
+ {
+ sb.AppendLine($" {(result.Passed ? "✓" : "✗")} {result.SampleName}: {result.Summary}");
+ }
+
+ foreach (var (name, reason) in skipped)
+ {
+ sb.AppendLine($" ○ {name}: Skipped — {reason}");
+ }
+
+ sb.AppendLine();
+ sb.AppendLine($"Results: {passCount} passed{(failCount > 0 ? $", {failCount} failed" : "")}{(skipped.Count > 0 ? $", {skipped.Count} skipped" : "")}");
+ sb.AppendLine($"Elapsed: {elapsed.Hours:D2}:{elapsed.Minutes:D2}:{elapsed.Seconds:D2}");
+
+ await this.AppendAsync(sb.ToString());
+ }
+
+ private async Task AppendAsync(string text)
+ {
+ await this._writeLock.WaitAsync();
+ try
+ {
+ await File.AppendAllTextAsync(this._path, text);
+ }
+ finally
+ {
+ this._writeLock.Release();
+ }
+ }
+}
diff --git a/dotnet/eng/verify-samples/Program.cs b/dotnet/eng/verify-samples/Program.cs
new file mode 100644
index 0000000000..e1a3bd3170
--- /dev/null
+++ b/dotnet/eng/verify-samples/Program.cs
@@ -0,0 +1,98 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+// This tool runs the 01-get-started, 02-agents, and 03-workflows samples and verifies their output.
+// Deterministic samples are verified with exact string matching.
+// Non-deterministic (LLM) samples are verified using an agent-framework agent.
+//
+// Usage:
+// dotnet run # Run all samples
+// dotnet run -- 01_hello_agent 05_first_workflow # Run specific samples by name
+// dotnet run -- --category 01-get-started # Run the 01-get-started category
+// dotnet run -- --category 02-agents # Run the 02-agents category
+// dotnet run -- --category 03-workflows # Run the 03-workflows category
+// dotnet run -- --parallel 16 # Run up to 16 samples concurrently
+// dotnet run -- --log results.log # Write sequential log to file
+// dotnet run -- --csv results.csv # Write CSV summary to file
+//
+// Required environment variables (for AI-powered samples):
+// AZURE_OPENAI_ENDPOINT
+// AZURE_OPENAI_DEPLOYMENT_NAME (optional, defaults to gpt-5-mini)
+
+using System.Diagnostics;
+using Azure.AI.OpenAI;
+using Azure.Identity;
+using VerifySamples;
+
+var options = VerifyOptions.Parse(args);
+if (options is null)
+{
+ return 1;
+}
+
+var stopwatch = Stopwatch.StartNew();
+
+// Resolve the dotnet/ root directory (verify-samples is at dotnet/eng/verify-samples/)
+var dotnetRoot = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", ".."));
+if (!File.Exists(Path.Combine(dotnetRoot, "agent-framework-dotnet.slnx")))
+{
+ dotnetRoot = Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), "..", ".."));
+}
+
+// Set up the AI verifier
+var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT");
+var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5-mini";
+
+OpenAI.Chat.ChatClient? chatClient = null;
+if (!string.IsNullOrEmpty(endpoint))
+{
+ chatClient = new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential())
+ .GetChatClient(deploymentName);
+}
+
+// Set up optional log file writer
+LogFileWriter? logWriter = null;
+if (options.LogFilePath is not null)
+{
+ logWriter = new LogFileWriter(options.LogFilePath);
+ await logWriter.WriteHeaderAsync();
+}
+
+try
+{
+ // Run all samples
+ var reporter = new ConsoleReporter();
+ var verifier = new SampleVerifier(chatClient);
+ var orchestrator = new VerificationOrchestrator(verifier, reporter, dotnetRoot, TimeSpan.FromMinutes(3), logWriter);
+
+ var run = await orchestrator.RunAllAsync(options.Samples, options.MaxParallelism);
+
+ stopwatch.Stop();
+
+ // Print summary
+ var orderedResults = run.SampleOrder
+ .Where(run.Results.ContainsKey)
+ .Select(name => run.Results[name])
+ .ToList();
+
+ reporter.PrintSummary(orderedResults, run.Skipped, stopwatch.Elapsed);
+
+ // Write log file summary
+ if (logWriter is not null)
+ {
+ await logWriter.WriteSummaryAsync(orderedResults, run.Skipped, stopwatch.Elapsed);
+ Console.WriteLine($"Log written to: {options.LogFilePath}");
+ }
+
+ // Write CSV summary
+ if (options.CsvFilePath is not null)
+ {
+ await CsvResultWriter.WriteAsync(options.CsvFilePath, orderedResults, run.Skipped, options.Samples);
+ Console.WriteLine($"CSV written to: {options.CsvFilePath}");
+ }
+
+ return orderedResults.Any(r => !r.Passed) ? 1 : 0;
+}
+finally
+{
+ logWriter?.Dispose();
+}
diff --git a/dotnet/eng/verify-samples/SampleDefinition.cs b/dotnet/eng/verify-samples/SampleDefinition.cs
new file mode 100644
index 0000000000..5f5f69a40e
--- /dev/null
+++ b/dotnet/eng/verify-samples/SampleDefinition.cs
@@ -0,0 +1,79 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+namespace VerifySamples;
+
+///
+/// Describes a sample to verify, including its expected output.
+///
+internal sealed class SampleDefinition
+{
+ ///
+ /// Display name for the sample (e.g., "01_hello_agent").
+ ///
+ public required string Name { get; init; }
+
+ ///
+ /// Relative path from the dotnet/ directory to the sample project directory.
+ ///
+ public required string ProjectPath { get; init; }
+
+ ///
+ /// Environment variables that the sample requires for a meaningful run.
+ /// The runner checks these before running and will skip the sample if any are unset,
+ /// recording a skip reason that indicates which required variables are missing.
+ ///
+ public string[] RequiredEnvironmentVariables { get; init; } = [];
+
+ ///
+ /// Environment variables that the sample can use but typically has fallbacks or defaults for.
+ /// If these are not set, the sample might prompt or behave interactively, which could cause
+ /// automated verification to hang. The runner checks these and skips the sample if they are unset
+ /// to avoid non-deterministic or blocking behavior in automated runs.
+ ///
+ public string[] OptionalEnvironmentVariables { get; init; } = [];
+
+ ///
+ /// If set, the sample is skipped with this reason.
+ /// Use only for structural reasons (e.g., web server, multi-process, needs external service).
+ /// Do NOT use for missing environment variables — those are checked dynamically.
+ ///
+ public string? SkipReason { get; init; }
+
+ ///
+ /// Substrings that must appear in stdout for the sample to pass.
+ /// Used for deterministic verification.
+ ///
+ public string[] MustContain { get; init; } = [];
+
+ ///
+ /// Substrings that must not appear in stdout for the sample to pass.
+ ///
+ public string[] MustNotContain { get; init; } = [];
+
+ ///
+ /// If true, entries cover the entire expected output —
+ /// no AI verification is needed.
+ ///
+ public bool IsDeterministic { get; init; }
+
+ ///
+ /// Natural-language description of what the sample output should look like.
+ /// Used by the AI verifier for non-deterministic samples.
+ /// Each entry describes one aspect of the expected output that should be verified.
+ ///
+ public string[] ExpectedOutputDescription { get; init; } = [];
+
+ ///
+ /// Sequence of stdin inputs to feed to the sample process.
+ /// Each entry is written as a line (followed by newline) to the process stdin.
+ /// A null entry inserts a delay without writing anything.
+ /// Inputs are sent with a short delay between each to allow the process to prompt.
+ ///
+ public string?[] Inputs { get; init; } = [];
+
+ ///
+ /// Delay in milliseconds between each input line. Default is 2000ms.
+ /// Increase for samples that need more time between prompts (e.g., LLM calls between inputs).
+ ///
+ public int InputDelayMs { get; init; } = 2000;
+}
diff --git a/dotnet/eng/verify-samples/SampleRunner.cs b/dotnet/eng/verify-samples/SampleRunner.cs
new file mode 100644
index 0000000000..0fabd82262
--- /dev/null
+++ b/dotnet/eng/verify-samples/SampleRunner.cs
@@ -0,0 +1,132 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Diagnostics;
+
+namespace VerifySamples;
+
+///
+/// Result of running a sample process.
+///
+internal sealed record SampleRunResult(
+ string Stdout,
+ string Stderr,
+ int ExitCode,
+ TimeSpan Elapsed);
+
+///
+/// Runs a sample project via dotnet run and captures its output.
+///
+internal static class SampleRunner
+{
+ ///
+ /// Runs dotnet run --framework net10.0 in the given project directory.
+ ///
+ public static Task RunAsync(
+ string projectPath,
+ TimeSpan timeout,
+ CancellationToken cancellationToken = default)
+ => RunAsync(projectPath, "run --framework net10.0", timeout, inputs: null, inputDelayMs: 0, cancellationToken: cancellationToken);
+
+ ///
+ /// Runs dotnet run --framework net10.0 with stdin inputs.
+ ///
+ public static Task RunAsync(
+ string projectPath,
+ TimeSpan timeout,
+ string?[]? inputs,
+ int inputDelayMs = 2000,
+ CancellationToken cancellationToken = default)
+ => RunAsync(projectPath, "run --framework net10.0", timeout, inputs, inputDelayMs, cancellationToken);
+
+ ///
+ /// Runs an arbitrary dotnet command in the given working directory.
+ ///
+ public static async Task RunAsync(
+ string workingDirectory,
+ string dotnetArgs,
+ TimeSpan timeout,
+ string?[]? inputs = null,
+ int inputDelayMs = 0,
+ CancellationToken cancellationToken = default)
+ {
+ var psi = new ProcessStartInfo
+ {
+ FileName = "dotnet",
+ Arguments = dotnetArgs,
+ WorkingDirectory = workingDirectory,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ RedirectStandardInput = inputs is { Length: > 0 },
+ UseShellExecute = false,
+ CreateNoWindow = true,
+ };
+
+ var sw = Stopwatch.StartNew();
+
+ using var process = new Process { StartInfo = psi };
+ process.Start();
+
+ var stdoutTask = process.StandardOutput.ReadToEndAsync(cancellationToken);
+ var stderrTask = process.StandardError.ReadToEndAsync(cancellationToken);
+
+ // Feed stdin inputs with delays if configured
+ if (inputs is { Length: > 0 })
+ {
+ _ = Task.Run(async () =>
+ {
+ try
+ {
+ foreach (var input in inputs)
+ {
+ await Task.Delay(inputDelayMs, cancellationToken);
+ if (input is not null)
+ {
+ await process.StandardInput.WriteLineAsync(input.AsMemory(), cancellationToken);
+ await process.StandardInput.FlushAsync(cancellationToken);
+ }
+ }
+
+ process.StandardInput.Close();
+ }
+ catch (Exception ex) when (ex is IOException or ObjectDisposedException or OperationCanceledException)
+ {
+ // Process may have exited before all inputs were sent
+ }
+ }, cancellationToken);
+ }
+
+ using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
+ cts.CancelAfter(timeout);
+
+ try
+ {
+ await process.WaitForExitAsync(cts.Token);
+ }
+ catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
+ {
+ // Timeout — kill the process
+ try
+ {
+ process.Kill(entireProcessTree: true);
+ }
+ catch
+ {
+ // Best effort
+ }
+
+ sw.Stop();
+ return new SampleRunResult(
+ Stdout: await stdoutTask,
+ Stderr: $"TIMEOUT: Sample did not complete within {timeout.TotalSeconds}s.\n{await stderrTask}",
+ ExitCode: -1,
+ Elapsed: sw.Elapsed);
+ }
+
+ sw.Stop();
+ return new SampleRunResult(
+ Stdout: await stdoutTask,
+ Stderr: await stderrTask,
+ ExitCode: process.ExitCode,
+ Elapsed: sw.Elapsed);
+ }
+}
diff --git a/dotnet/eng/verify-samples/SampleVerifier.cs b/dotnet/eng/verify-samples/SampleVerifier.cs
new file mode 100644
index 0000000000..9dc17b1769
--- /dev/null
+++ b/dotnet/eng/verify-samples/SampleVerifier.cs
@@ -0,0 +1,202 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json.Serialization;
+using Microsoft.Agents.AI;
+using Microsoft.Extensions.AI;
+using OpenAI.Chat;
+
+namespace VerifySamples;
+
+///
+/// Verifies sample output using deterministic checks and an AI agent
+/// for non-deterministic output validation.
+///
+internal sealed class SampleVerifier
+{
+ private readonly AIAgent? _verifierAgent;
+
+ ///
+ /// Creates a verifier. If is provided,
+ /// AI-based verification is available for non-deterministic samples.
+ ///
+ public SampleVerifier(ChatClient? chatClient = null)
+ {
+ if (chatClient is not null)
+ {
+ this._verifierAgent = chatClient.AsAIAgent(
+ instructions: """
+ You are a test output verifier. You will be given:
+ 1. The actual stdout output of a program
+ 2. A list of expectations about what the output should contain or demonstrate
+
+ Your job is to determine whether the actual output satisfies each expectation.
+ Be reasonable — the output comes from an LLM so exact wording won't match, but the
+ semantic intent should be clearly satisfied.
+ """,
+ name: "OutputVerifier");
+ }
+ }
+
+ ///
+ /// Verifies the output of a sample run against its definition.
+ ///
+ public async Task VerifyAsync(SampleDefinition sample, SampleRunResult run)
+ {
+ var failures = new List();
+
+ // 1. Exit code check
+ if (run.ExitCode != 0)
+ {
+ failures.Add($"Exit code was {run.ExitCode}, expected 0. Stderr: {Truncate(run.Stderr, 500)}");
+ }
+
+ // 2. Must-contain checks
+ foreach (var expected in sample.MustContain)
+ {
+ if (!run.Stdout.Contains(expected, StringComparison.Ordinal))
+ {
+ failures.Add($"Output missing expected substring: \"{expected}\"");
+ }
+ }
+
+ // 3. Must-not-contain checks
+ foreach (var unexpected in sample.MustNotContain)
+ {
+ if (run.Stdout.Contains(unexpected, StringComparison.Ordinal))
+ {
+ failures.Add($"Output contains unexpected substring: \"{unexpected}\"");
+ }
+ }
+
+ // 4. AI verification for non-deterministic samples
+ string? aiReasoning = null;
+ if (!sample.IsDeterministic && sample.ExpectedOutputDescription.Length > 0)
+ {
+ if (this._verifierAgent is null)
+ {
+ failures.Add("AI verification required but no AI agent configured (missing AZURE_OPENAI_ENDPOINT).");
+ }
+ else
+ {
+ var aiResult = await this.VerifyWithAIAsync(run.Stdout, sample.ExpectedOutputDescription);
+ aiReasoning = aiResult.Reasoning;
+
+ foreach (var unmet in aiResult.UnmetExpectations)
+ {
+ failures.Add($"AI expectation not met: {unmet}");
+ }
+ }
+ }
+
+ bool passed = failures.Count == 0;
+ return new VerificationResult
+ {
+ SampleName = sample.Name,
+ Passed = passed,
+ Summary = passed ? "All checks passed" : $"{failures.Count} check(s) failed",
+ Failures = failures,
+ AIReasoning = aiReasoning,
+ };
+ }
+
+ private async Task<(string Reasoning, List UnmetExpectations)> VerifyWithAIAsync(
+ string actualOutput,
+ string[] expectations)
+ {
+ var expectationList = string.Join("\n", expectations.Select((e, i) => $" {i + 1}. {e}"));
+ var prompt = $"""
+ Actual program output:
+ ---
+ {Truncate(actualOutput, 4000)}
+ ---
+
+ Expectations to verify:
+ {expectationList}
+
+ Does the output satisfy all expectations?
+ """;
+
+ try
+ {
+ var response = await this._verifierAgent!.RunAsync(prompt);
+ var result = response.Result;
+
+ if (result is null)
+ {
+ return ($"AI verification returned null result. Raw: {response.Text}", ["AI verification returned null result."]);
+ }
+
+ var reasoning = result.Reasoning ?? "(no reasoning provided)";
+
+ // Collect unmet expectations as individual failures
+ var unmet = new List();
+ if (result.ExpectationResults is { Count: > 0 })
+ {
+ foreach (var er in result.ExpectationResults.Where(er => !er.Met))
+ {
+ var detail = string.IsNullOrWhiteSpace(er.Detail) ? er.Expectation : $"{er.Expectation} — {er.Detail}";
+ unmet.Add(detail ?? "Unknown expectation");
+ }
+
+ // If the model flagged overall failure but all individual expectations were met,
+ // still treat as failure using the overall reasoning.
+ if (unmet.Count == 0 && !result.Pass)
+ {
+ unmet.Add(reasoning);
+ }
+ }
+ else if (!result.Pass)
+ {
+ // Fallback: no per-expectation detail but overall pass is false
+ unmet.Add(reasoning);
+ }
+
+ return (reasoning, unmet);
+ }
+ catch (Exception ex)
+ {
+ return ($"AI verification error: {ex.Message}", [$"AI verification error: {ex.Message}"]);
+ }
+ }
+
+ private static string Truncate(string text, int maxLength)
+ => text.Length <= maxLength ? text : text[..maxLength] + "... (truncated)";
+}
+
+///
+/// Structured response from the AI verification agent.
+///
+[System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1812:Avoid uninstantiated internal classes", Justification = "Instantiated by JSON deserialization via RunAsync.")]
+internal sealed class AIVerificationResponse
+{
+ /// Whether all expectations were met.
+ [JsonPropertyName("pass")]
+ public bool Pass { get; set; }
+
+ /// Brief explanation of the overall assessment.
+ [JsonPropertyName("reasoning")]
+ public string? Reasoning { get; set; }
+
+ /// Per-expectation results.
+ [JsonPropertyName("expectation_results")]
+ public List? ExpectationResults { get; set; }
+}
+
+///
+/// Result for an individual expectation check.
+///
+[System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1812:Avoid uninstantiated internal classes", Justification = "Instantiated by JSON deserialization via RunAsync.")]
+internal sealed class ExpectationResult
+{
+ /// The expectation text that was evaluated.
+ [JsonPropertyName("expectation")]
+ public string? Expectation { get; set; }
+
+ /// Whether this expectation was met.
+ [JsonPropertyName("met")]
+ public bool Met { get; set; }
+
+ /// Detail about how the expectation was or was not met.
+ [JsonPropertyName("detail")]
+ public string? Detail { get; set; }
+}
diff --git a/dotnet/eng/verify-samples/VerificationOrchestrator.cs b/dotnet/eng/verify-samples/VerificationOrchestrator.cs
new file mode 100644
index 0000000000..1ce805bc5a
--- /dev/null
+++ b/dotnet/eng/verify-samples/VerificationOrchestrator.cs
@@ -0,0 +1,197 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Collections.Concurrent;
+
+namespace VerifySamples;
+
+///
+/// Orchestrates sample verification: filters, runs in parallel, and collects results.
+///
+internal sealed class VerificationOrchestrator
+{
+ private readonly SampleVerifier _verifier;
+ private readonly ConsoleReporter _reporter;
+ private readonly LogFileWriter? _logWriter;
+ private readonly string _dotnetRoot;
+ private readonly TimeSpan _timeout;
+
+ public VerificationOrchestrator(
+ SampleVerifier verifier,
+ ConsoleReporter reporter,
+ string dotnetRoot,
+ TimeSpan timeout,
+ LogFileWriter? logWriter = null)
+ {
+ this._verifier = verifier;
+ this._reporter = reporter;
+ this._logWriter = logWriter;
+ this._dotnetRoot = dotnetRoot;
+ this._timeout = timeout;
+ }
+
+ ///
+ /// The result of running all samples through the orchestrator.
+ ///
+ internal sealed record RunAllResult(
+ ConcurrentDictionary Results,
+ List<(string Name, string Reason)> Skipped,
+ List SampleOrder);
+
+ ///
+ /// Filters samples, runs the runnable ones in parallel, and returns all results.
+ ///
+ public async Task RunAllAsync(
+ IReadOnlyList samples,
+ int maxParallelism)
+ {
+ var skipped = new List<(string Name, string Reason)>();
+ var runnableSamples = new List();
+ var sampleOrder = new List();
+
+ // Separate samples into skipped and runnable
+ foreach (var sample in samples)
+ {
+ sampleOrder.Add(sample.Name);
+
+ if (sample.SkipReason is not null)
+ {
+ skipped.Add((sample.Name, sample.SkipReason));
+ this._reporter.WriteLineWithPrefix(sample.Name, $"SKIPPED — {sample.SkipReason}", ConsoleColor.Yellow);
+
+ if (this._logWriter is not null)
+ {
+ await this._logWriter.WriteSkippedAsync(sample.Name, sample.SkipReason);
+ }
+
+ continue;
+ }
+
+ var missingRequired = sample.RequiredEnvironmentVariables
+ .Where(v => string.IsNullOrEmpty(Environment.GetEnvironmentVariable(v)))
+ .ToList();
+
+ var missingOptional = sample.OptionalEnvironmentVariables
+ .Where(v => string.IsNullOrEmpty(Environment.GetEnvironmentVariable(v)))
+ .ToList();
+
+ if (missingRequired.Count > 0 || missingOptional.Count > 0)
+ {
+ var reasons = new List();
+ if (missingRequired.Count > 0)
+ {
+ reasons.Add($"Missing required: {string.Join(", ", missingRequired)}");
+ }
+
+ if (missingOptional.Count > 0)
+ {
+ reasons.Add($"Missing optional (would cause console prompt hang): {string.Join(", ", missingOptional)}");
+ }
+
+ var skipReason = string.Join("; ", reasons);
+ skipped.Add((sample.Name, skipReason));
+ this._reporter.WriteLineWithPrefix(sample.Name, $"SKIPPED — {skipReason}", ConsoleColor.Yellow);
+
+ if (this._logWriter is not null)
+ {
+ await this._logWriter.WriteSkippedAsync(sample.Name, skipReason);
+ }
+
+ continue;
+ }
+
+ runnableSamples.Add(sample);
+ }
+
+ // Run samples in parallel
+ var results = new ConcurrentDictionary();
+ var semaphore = new SemaphoreSlim(maxParallelism);
+
+ this._reporter.WriteLineWithPrefix(
+ "runner", $"Running {runnableSamples.Count} samples (max {maxParallelism} parallel)...");
+
+ try
+ {
+ var tasks = runnableSamples.Select(sample => this.RunSingleAsync(sample, results, semaphore)).ToArray();
+ await Task.WhenAll(tasks);
+ }
+ finally
+ {
+ semaphore.Dispose();
+ }
+
+ return new RunAllResult(results, skipped, sampleOrder);
+ }
+
+ private async Task RunSingleAsync(
+ SampleDefinition sample,
+ ConcurrentDictionary results,
+ SemaphoreSlim semaphore)
+ {
+ await semaphore.WaitAsync();
+ try
+ {
+ var log = new List();
+ log.Add($"[{sample.Name}] Running...");
+ this._reporter.WriteLineWithPrefix(sample.Name, "Running...");
+
+ var projectPath = Path.Combine(this._dotnetRoot, sample.ProjectPath);
+ var run = sample.Inputs.Length > 0
+ ? await SampleRunner.RunAsync(projectPath, this._timeout, sample.Inputs, sample.InputDelayMs)
+ : await SampleRunner.RunAsync(projectPath, this._timeout);
+
+ log.Add($"[{sample.Name}] Completed ({run.Elapsed.TotalSeconds:F1}s, exit={run.ExitCode})");
+ this._reporter.WriteLineWithPrefix(
+ sample.Name, $"Completed ({run.Elapsed.TotalSeconds:F1}s, exit={run.ExitCode}). Verifying...");
+
+ var result = await this._verifier.VerifyAsync(sample, run);
+
+ if (result.Passed)
+ {
+ log.Add($"[{sample.Name}] PASSED");
+ this._reporter.WriteLineWithPrefix(sample.Name, "PASSED", ConsoleColor.Green);
+ }
+ else
+ {
+ log.Add($"[{sample.Name}] FAILED");
+ this._reporter.WriteLineWithPrefix(sample.Name, "FAILED", ConsoleColor.Red);
+ foreach (var failure in result.Failures)
+ {
+ log.Add($"[{sample.Name}] ✗ {failure}");
+ this._reporter.WriteLineWithPrefix(sample.Name, $" ✗ {failure}", ConsoleColor.Red);
+ }
+ }
+
+ if (result.AIReasoning is not null)
+ {
+ log.Add($"[{sample.Name}] AI: {result.AIReasoning}");
+ this._reporter.WriteLineWithPrefix(
+ sample.Name, $" AI: {Truncate(result.AIReasoning, 300)}", ConsoleColor.DarkGray);
+ }
+
+ var verificationResult = new VerificationResult
+ {
+ SampleName = result.SampleName,
+ Passed = result.Passed,
+ Summary = result.Summary,
+ Failures = result.Failures,
+ AIReasoning = result.AIReasoning,
+ Stdout = run.Stdout,
+ Stderr = run.Stderr,
+ LogLines = log,
+ };
+ results[sample.Name] = verificationResult;
+
+ if (this._logWriter is not null)
+ {
+ await this._logWriter.WriteSampleResultAsync(verificationResult);
+ }
+ }
+ finally
+ {
+ semaphore.Release();
+ }
+ }
+
+ private static string Truncate(string text, int maxLength)
+ => text.Length <= maxLength ? text : text[..maxLength] + "...";
+}
diff --git a/dotnet/eng/verify-samples/VerificationResult.cs b/dotnet/eng/verify-samples/VerificationResult.cs
new file mode 100644
index 0000000000..50a08f969e
--- /dev/null
+++ b/dotnet/eng/verify-samples/VerificationResult.cs
@@ -0,0 +1,31 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+namespace VerifySamples;
+
+///
+/// The result of verifying a single sample.
+///
+internal sealed class VerificationResult
+{
+ public required string SampleName { get; init; }
+ public required bool Passed { get; init; }
+ public required string Summary { get; init; }
+ public List Failures { get; init; } = [];
+ public string? AIReasoning { get; init; }
+
+ ///
+ /// The sample's stdout output, captured for log file output.
+ ///
+ public string? Stdout { get; init; }
+
+ ///
+ /// The sample's stderr output, captured for log file output.
+ ///
+ public string? Stderr { get; init; }
+
+ ///
+ /// Per-sample log lines, buffered during parallel execution
+ /// and written sequentially to the log file.
+ ///
+ public List LogLines { get; init; } = [];
+}
diff --git a/dotnet/eng/verify-samples/VerifyOptions.cs b/dotnet/eng/verify-samples/VerifyOptions.cs
new file mode 100644
index 0000000000..c4e3cd1f59
--- /dev/null
+++ b/dotnet/eng/verify-samples/VerifyOptions.cs
@@ -0,0 +1,124 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+namespace VerifySamples;
+
+///
+/// Parsed command-line options for the sample verification tool.
+///
+internal sealed class VerifyOptions
+{
+ ///
+ /// Maximum number of samples to run concurrently.
+ ///
+ public int MaxParallelism { get; init; } = 8;
+
+ ///
+ /// Path to write a CSV summary file, or null to skip.
+ ///
+ public string? CsvFilePath { get; init; }
+
+ ///
+ /// Path to write a sequential log file, or null to skip.
+ ///
+ public string? LogFilePath { get; init; }
+
+ ///
+ /// The filtered list of samples to process.
+ ///
+ public required IReadOnlyList Samples { get; init; }
+
+ ///
+ /// All known sample set registries, keyed by category name.
+ ///
+ private static readonly Dictionary> s_sampleSets =
+ new(StringComparer.OrdinalIgnoreCase)
+ {
+ ["01-get-started"] = GetStartedSamples.All,
+ ["02-agents"] = AgentsSamples.All,
+ ["03-workflows"] = WorkflowSamples.All,
+ };
+
+ ///
+ /// Parses command-line arguments and resolves the sample list.
+ /// Returns null and writes to stderr if the arguments are invalid.
+ ///
+ public static VerifyOptions? Parse(string[] args)
+ {
+ var argList = args.ToList();
+
+ var categoryFilter = ExtractArg(argList, "--category");
+ var logFilePath = ExtractArg(argList, "--log");
+ var csvFilePath = ExtractArg(argList, "--csv");
+
+ int maxParallelism = 8;
+ var parallelArg = ExtractArg(argList, "--parallel");
+ if (parallelArg is not null && int.TryParse(parallelArg, out var p) && p > 0)
+ {
+ maxParallelism = p;
+ }
+
+ HashSet? nameFilter = null;
+ if (argList.Count > 0)
+ {
+ nameFilter = argList.ToHashSet(StringComparer.OrdinalIgnoreCase);
+ }
+
+ // Build the sample list
+ IReadOnlyList samples;
+ if (categoryFilter is not null)
+ {
+ if (!s_sampleSets.TryGetValue(categoryFilter, out var categoryList))
+ {
+ Console.Error.WriteLine(
+ $"Unknown category '{categoryFilter}'. Available: {string.Join(", ", s_sampleSets.Keys)}");
+ return null;
+ }
+
+ samples = categoryList;
+ }
+ else
+ {
+ samples = s_sampleSets.Values.SelectMany(s => s).ToList();
+ }
+
+ if (nameFilter is not null)
+ {
+ samples = samples.Where(s => nameFilter.Contains(s.Name)).ToList();
+ }
+
+ if (samples.Count == 0)
+ {
+ var allNames = s_sampleSets.Values.SelectMany(s => s).Select(s => s.Name);
+ Console.Error.WriteLine($"No matching samples found. Available: {string.Join(", ", allNames)}");
+ return null;
+ }
+
+ return new VerifyOptions
+ {
+ MaxParallelism = maxParallelism,
+ LogFilePath = logFilePath,
+ CsvFilePath = csvFilePath,
+ Samples = samples,
+ };
+ }
+
+ private static string? ExtractArg(List list, string flag)
+ {
+ var idx = list.IndexOf(flag);
+ if (idx < 0)
+ {
+ return null;
+ }
+
+ if (idx + 1 >= list.Count)
+ {
+ Console.Error.WriteLine($"Missing value for {flag}.");
+ list.RemoveAt(idx);
+ return null;
+ }
+
+ var value = list[idx + 1];
+ list.RemoveRange(idx, 2);
+ return value;
+ }
+}
diff --git a/dotnet/eng/verify-samples/WorkflowSamples.cs b/dotnet/eng/verify-samples/WorkflowSamples.cs
new file mode 100644
index 0000000000..2842f4af89
--- /dev/null
+++ b/dotnet/eng/verify-samples/WorkflowSamples.cs
@@ -0,0 +1,525 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+namespace VerifySamples;
+
+///
+/// Defines the expected behavior for each sample in 03-workflows.
+///
+internal static class WorkflowSamples
+{
+ public static IReadOnlyList All { get; } =
+ [
+ // ───────────────────────────────────────────────────────────────────
+ // _StartHere
+ // ───────────────────────────────────────────────────────────────────
+
+ new SampleDefinition
+ {
+ Name = "Workflow_StartHere_01_Streaming",
+ ProjectPath = "samples/03-workflows/_StartHere/01_Streaming",
+ RequiredEnvironmentVariables = [],
+ IsDeterministic = true,
+ MustContain =
+ [
+ "UppercaseExecutor: HELLO, WORLD!",
+ "ReverseTextExecutor: !DLROW ,OLLEH",
+ ],
+ },
+
+ new SampleDefinition
+ {
+ Name = "Workflow_StartHere_02_AgentsInWorkflows",
+ ProjectPath = "samples/03-workflows/_StartHere/02_AgentsInWorkflows",
+ RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
+ ExpectedOutputDescription =
+ [
+ "The output should show agent responses from a translation workflow.",
+ "The output should not contain error messages or stack traces.",
+ ],
+ },
+
+ new SampleDefinition
+ {
+ Name = "Workflow_StartHere_03_AgentWorkflowPatterns",
+ ProjectPath = "samples/03-workflows/_StartHere/03_AgentWorkflowPatterns",
+ RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
+ Inputs = ["sequential"],
+ InputDelayMs = 3000,
+ ExpectedOutputDescription =
+ [
+ "The output should show a sequential workflow pattern with multiple agents executing tasks in order.",
+ "The output should not contain error messages or stack traces.",
+ ],
+ },
+
+ new SampleDefinition
+ {
+ Name = "Workflow_StartHere_04_MultiModelService",
+ ProjectPath = "samples/03-workflows/_StartHere/04_MultiModelService",
+ RequiredEnvironmentVariables = ["BEDROCK_ACCESS_KEY", "BEDROCK_SECRET_KEY", "ANTHROPIC_API_KEY", "OPENAI_API_KEY"],
+ SkipReason = "Requires multiple external provider API keys (Bedrock, Anthropic, OpenAI).",
+ },
+
+ new SampleDefinition
+ {
+ Name = "Workflow_StartHere_05_SubWorkflows",
+ ProjectPath = "samples/03-workflows/_StartHere/05_SubWorkflows",
+ RequiredEnvironmentVariables = [],
+ IsDeterministic = true,
+ MustContain =
+ [
+ "=== Sub-Workflow Demonstration ===",
+ "Final Output:",
+ "=== Main Workflow Completed ===",
+ "Sample Complete: Workflows can be composed hierarchically using sub-workflows",
+ ],
+ },
+
+ new SampleDefinition
+ {
+ Name = "Workflow_StartHere_06_MixedWorkflowAgentsAndExecutors",
+ ProjectPath = "samples/03-workflows/_StartHere/06_MixedWorkflowAgentsAndExecutors",
+ RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
+ Inputs = ["What is 2 plus 2?"],
+ InputDelayMs = 3000,
+ ExpectedOutputDescription =
+ [
+ "The output should show agents and executors working together to process a user question.",
+ "The output should not contain error messages or stack traces.",
+ ],
+ },
+
+ new SampleDefinition
+ {
+ Name = "Workflow_StartHere_07_WriterCriticWorkflow",
+ ProjectPath = "samples/03-workflows/_StartHere/07_WriterCriticWorkflow",
+ RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
+ MustContain = ["=== Writer-Critic Iteration Workflow ==="],
+ ExpectedOutputDescription =
+ [
+ "The output should show a writer-critic iteration workflow with writer and critic sections.",
+ "The critic should either approve or request revisions.",
+ "The output should not contain error messages or stack traces.",
+ ],
+ },
+
+ // ───────────────────────────────────────────────────────────────────
+ // Agents
+ // ───────────────────────────────────────────────────────────────────
+
+ new SampleDefinition
+ {
+ Name = "Workflow_Agents_CustomAgentExecutors",
+ ProjectPath = "samples/03-workflows/Agents/CustomAgentExecutors",
+ RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
+ ExpectedOutputDescription =
+ [
+ "The output should show custom workflow events including slogan generation and feedback.",
+ "The output should not contain error messages or stack traces.",
+ ],
+ },
+
+ new SampleDefinition
+ {
+ Name = "Workflow_Agents_FoundryAgent",
+ ProjectPath = "samples/03-workflows/Agents/FoundryAgent",
+ RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
+ SkipReason = "Requires Azure AI Foundry project endpoint.",
+ },
+
+ new SampleDefinition
+ {
+ Name = "Workflow_Agents_GroupChatToolApproval",
+ ProjectPath = "samples/03-workflows/Agents/GroupChatToolApproval",
+ RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
+ MustContain = ["Starting group chat workflow for software deployment..."],
+ ExpectedOutputDescription =
+ [
+ "The output should show a group chat workflow with QA and DevOps agents for software deployment.",
+ "There should be approval requests for tool calls.",
+ "The workflow should show interaction between QA and DevOps agents toward deployment.",
+ "The output should not contain error messages or stack traces.",
+ ],
+ },
+
+ new SampleDefinition
+ {
+ Name = "Workflow_Agents_WorkflowAsAnAgent",
+ ProjectPath = "samples/03-workflows/Agents/WorkflowAsAnAgent",
+ RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
+ Inputs = ["hello", "exit"],
+ InputDelayMs = 5000,
+ ExpectedOutputDescription =
+ [
+ "The output should show a conversational workflow responding to the user's hello message.",
+ "The output should not contain error messages or stack traces.",
+ ],
+ },
+
+ // ───────────────────────────────────────────────────────────────────
+ // Checkpoint
+ // ───────────────────────────────────────────────────────────────────
+
+ new SampleDefinition
+ {
+ Name = "Workflow_Checkpoint_CheckpointAndRehydrate",
+ ProjectPath = "samples/03-workflows/Checkpoint/CheckpointAndRehydrate",
+ RequiredEnvironmentVariables = [],
+ IsDeterministic = true,
+ MustContain =
+ [
+ "Workflow completed with result:",
+ "Number of checkpoints created:",
+ "Hydrating a new workflow instance from the 6th checkpoint.",
+ ],
+ },
+
+ new SampleDefinition
+ {
+ Name = "Workflow_Checkpoint_CheckpointAndResume",
+ ProjectPath = "samples/03-workflows/Checkpoint/CheckpointAndResume",
+ RequiredEnvironmentVariables = [],
+ IsDeterministic = true,
+ MustContain =
+ [
+ "Workflow completed with result:",
+ "Number of checkpoints created:",
+ "Restoring from the 6th checkpoint.",
+ ],
+ },
+
+ new SampleDefinition
+ {
+ Name = "Workflow_Checkpoint_CheckpointWithHumanInTheLoop",
+ ProjectPath = "samples/03-workflows/Checkpoint/CheckpointWithHumanInTheLoop",
+ RequiredEnvironmentVariables = [],
+ Inputs = ["50", "25", "40", "45", "42", "50", "25", "40", "45", "42"],
+ InputDelayMs = 1000,
+ MustContain = ["found in"],
+ ExpectedOutputDescription =
+ [
+ "The output should show a number guessing game with higher/lower hints that eventually reaches the correct number.",
+ "The output should demonstrate checkpoint save and restore behavior.",
+ ],
+ },
+
+ // ───────────────────────────────────────────────────────────────────
+ // Concurrent
+ // ───────────────────────────────────────────────────────────────────
+
+ new SampleDefinition
+ {
+ Name = "Workflow_Concurrent_Concurrent",
+ ProjectPath = "samples/03-workflows/Concurrent/Concurrent",
+ RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
+ ExpectedOutputDescription =
+ [
+ "The output should show results from concurrent agent processing.",
+ "The output should not contain error messages or stack traces.",
+ ],
+ },
+
+ new SampleDefinition
+ {
+ Name = "Workflow_Concurrent_MapReduce",
+ ProjectPath = "samples/03-workflows/Concurrent/MapReduce",
+ RequiredEnvironmentVariables = [],
+ MustContain =
+ [
+ "=== RUNNING WORKFLOW ===",
+ ],
+ },
+
+ // ───────────────────────────────────────────────────────────────────
+ // ConditionalEdges
+ // ───────────────────────────────────────────────────────────────────
+
+ new SampleDefinition
+ {
+ Name = "Workflow_ConditionalEdges_01_EdgeCondition",
+ ProjectPath = "samples/03-workflows/ConditionalEdges/01_EdgeCondition",
+ RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
+ ExpectedOutputDescription =
+ [
+ "The output should show an email being classified as spam or not spam and processed accordingly.",
+ "The output should not contain error messages or stack traces.",
+ ],
+ },
+
+ new SampleDefinition
+ {
+ Name = "Workflow_ConditionalEdges_02_SwitchCase",
+ ProjectPath = "samples/03-workflows/ConditionalEdges/02_SwitchCase",
+ RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
+ ExpectedOutputDescription =
+ [
+ "The output should show an ambiguous email being classified as spam, not spam, or uncertain.",
+ "The output should not contain error messages or stack traces.",
+ ],
+ },
+
+ new SampleDefinition
+ {
+ Name = "Workflow_ConditionalEdges_03_MultiSelection",
+ ProjectPath = "samples/03-workflows/ConditionalEdges/03_MultiSelection",
+ RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
+ ExpectedOutputDescription =
+ [
+ "The output should show an email being classified and potentially routed to multiple handlers.",
+ "The output should not contain error messages or stack traces.",
+ ],
+ },
+
+ // ───────────────────────────────────────────────────────────────────
+ // HumanInTheLoop
+ // ───────────────────────────────────────────────────────────────────
+
+ new SampleDefinition
+ {
+ Name = "Workflow_HumanInTheLoop_Basic",
+ ProjectPath = "samples/03-workflows/HumanInTheLoop/HumanInTheLoopBasic",
+ RequiredEnvironmentVariables = [],
+ Inputs = ["50", "25", "40", "45", "42"],
+ InputDelayMs = 1000,
+ MustContain = ["found in"],
+ ExpectedOutputDescription =
+ [
+ "The output should show a number guessing game with higher/lower hints that eventually reaches the correct number 42.",
+ ],
+ },
+
+ // ───────────────────────────────────────────────────────────────────
+ // Loop
+ // ───────────────────────────────────────────────────────────────────
+
+ new SampleDefinition
+ {
+ Name = "Workflow_Loop",
+ ProjectPath = "samples/03-workflows/Loop",
+ RequiredEnvironmentVariables = [],
+ MustContain = ["Result:"],
+ },
+
+ // ───────────────────────────────────────────────────────────────────
+ // SharedStates
+ // ───────────────────────────────────────────────────────────────────
+
+ new SampleDefinition
+ {
+ Name = "Workflow_SharedStates",
+ ProjectPath = "samples/03-workflows/SharedStates",
+ RequiredEnvironmentVariables = [],
+ IsDeterministic = true,
+ MustContain =
+ [
+ "Total Paragraphs:",
+ "Total Words:",
+ ],
+ },
+
+ // ───────────────────────────────────────────────────────────────────
+ // Visualization
+ // ───────────────────────────────────────────────────────────────────
+
+ new SampleDefinition
+ {
+ Name = "Workflow_Visualization",
+ ProjectPath = "samples/03-workflows/Visualization",
+ RequiredEnvironmentVariables = [],
+ IsDeterministic = true,
+ MustContain =
+ [
+ "Generating workflow visualization...",
+ "Mermaid string:",
+ "DiGraph string:",
+ ],
+ },
+
+ // ───────────────────────────────────────────────────────────────────
+ // Observability
+ // ───────────────────────────────────────────────────────────────────
+
+ new SampleDefinition
+ {
+ Name = "Workflow_Observability_ApplicationInsights",
+ ProjectPath = "samples/03-workflows/Observability/ApplicationInsights",
+ RequiredEnvironmentVariables = ["APPLICATIONINSIGHTS_CONNECTION_STRING"],
+ SkipReason = "Requires Application Insights connection string.",
+ },
+
+ new SampleDefinition
+ {
+ Name = "Workflow_Observability_AspireDashboard",
+ ProjectPath = "samples/03-workflows/Observability/AspireDashboard",
+ RequiredEnvironmentVariables = [],
+ SkipReason = "Requires Aspire Dashboard / OTLP endpoint.",
+ },
+
+ new SampleDefinition
+ {
+ Name = "Workflow_Observability_WorkflowAsAnAgent",
+ ProjectPath = "samples/03-workflows/Observability/WorkflowAsAnAgent",
+ RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
+ SkipReason = "Interactive console with ReadLine loop; requires OTLP endpoint.",
+ },
+
+ // ───────────────────────────────────────────────────────────────────
+ // Declarative
+ // ───────────────────────────────────────────────────────────────────
+
+ new SampleDefinition
+ {
+ Name = "Workflow_Declarative_ConfirmInput",
+ ProjectPath = "samples/03-workflows/Declarative/ConfirmInput",
+ RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
+ Inputs = ["hello", "hello"],
+ InputDelayMs = 8000,
+ ExpectedOutputDescription = ["The output should show a confirmation prompt and a user response."],
+ },
+
+ new SampleDefinition
+ {
+ Name = "Workflow_Declarative_CustomerSupport",
+ ProjectPath = "samples/03-workflows/Declarative/CustomerSupport",
+ RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
+ Inputs = ["My laptop won't start"],
+ InputDelayMs = 3000,
+ ExpectedOutputDescription = ["The output should show a customer support workflow processing a laptop issue, with agent responses providing troubleshooting or support."],
+ },
+
+ new SampleDefinition
+ {
+ Name = "Workflow_Declarative_DeepResearch",
+ ProjectPath = "samples/03-workflows/Declarative/DeepResearch",
+ RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
+ SkipReason = "Requires external weather API (wttr.in).",
+ },
+
+ new SampleDefinition
+ {
+ Name = "Workflow_Declarative_ExecuteCode",
+ ProjectPath = "samples/03-workflows/Declarative/ExecuteCode",
+ RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
+ Inputs = ["What is 12 * 34?"],
+ InputDelayMs = 5000,
+ ExpectedOutputDescription = ["The output should show a declarative workflow executing generated code, processing a math question and producing a result."],
+ },
+
+ new SampleDefinition
+ {
+ Name = "Workflow_Declarative_ExecuteWorkflow",
+ ProjectPath = "samples/03-workflows/Declarative/ExecuteWorkflow",
+ RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
+ SkipReason = "Requires a workflow file path as a CLI argument.",
+ },
+
+ new SampleDefinition
+ {
+ Name = "Workflow_Declarative_FunctionTools",
+ ProjectPath = "samples/03-workflows/Declarative/FunctionTools",
+ RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
+ Inputs = ["What are today's specials?", "EXIT"],
+ InputDelayMs = 8000,
+ ExpectedOutputDescription = ["The output should show a workflow calling function tools (e.g. a menu plugin) to answer a question about restaurant specials."],
+ },
+
+ new SampleDefinition
+ {
+ Name = "Workflow_Declarative_GenerateCode",
+ ProjectPath = "samples/03-workflows/Declarative/GenerateCode",
+ IsDeterministic = true,
+ MustContain = ["WORKFLOW: Parsing", "WORKFLOW: Defined"],
+ ExpectedOutputDescription = ["The output should show a YAML workflow being parsed and C# code being generated from it."],
+ },
+
+ new SampleDefinition
+ {
+ Name = "Workflow_Declarative_HostedWorkflow",
+ ProjectPath = "samples/03-workflows/Declarative/HostedWorkflow",
+ RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
+ SkipReason = "Hosts a persistent workflow server that does not exit.",
+ },
+
+ new SampleDefinition
+ {
+ Name = "Workflow_Declarative_InputArguments",
+ ProjectPath = "samples/03-workflows/Declarative/InputArguments",
+ RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
+ Inputs = ["I'd like to visit Seattle", "EXIT"],
+ InputDelayMs = 8000,
+ ExpectedOutputDescription = ["The output should show a workflow capturing location input and providing travel-related information about Seattle."],
+ },
+
+ new SampleDefinition
+ {
+ Name = "Workflow_Declarative_InvokeFunctionTool",
+ ProjectPath = "samples/03-workflows/Declarative/InvokeFunctionTool",
+ RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
+ Inputs = ["What's the soup of the day?", "EXIT"],
+ InputDelayMs = 8000,
+ ExpectedOutputDescription = ["The output should show a workflow invoking a function tool (e.g. a menu plugin) to answer a question about the soup of the day."],
+ },
+
+ new SampleDefinition
+ {
+ Name = "Workflow_Declarative_InvokeMcpTool",
+ ProjectPath = "samples/03-workflows/Declarative/InvokeMcpTool",
+ RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
+ Inputs = ["Search for .NET tutorials on Microsoft Learn"],
+ InputDelayMs = 3000,
+ ExpectedOutputDescription = ["The output should show a workflow using MCP tools to search Microsoft Learn documentation and provide a summary of results."],
+ },
+
+ new SampleDefinition
+ {
+ Name = "Workflow_Declarative_Marketing",
+ ProjectPath = "samples/03-workflows/Declarative/Marketing",
+ RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
+ Inputs = ["A smart water bottle that tracks hydration"],
+ InputDelayMs = 3000,
+ ExpectedOutputDescription = ["The output should show a marketing workflow generating content about a smart water bottle product."],
+ },
+
+ new SampleDefinition
+ {
+ Name = "Workflow_Declarative_StudentTeacher",
+ ProjectPath = "samples/03-workflows/Declarative/StudentTeacher",
+ RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
+ Inputs = ["What is 18 + 27?"],
+ InputDelayMs = 3000,
+ ExpectedOutputDescription = ["The output should show a student-teacher workflow where a student asks a math question and a teacher provides the answer."],
+ },
+
+ new SampleDefinition
+ {
+ Name = "Workflow_Declarative_ToolApproval",
+ ProjectPath = "samples/03-workflows/Declarative/ToolApproval",
+ RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
+ Inputs = ["Search for .NET tutorials", "EXIT"],
+ InputDelayMs = 8000,
+ ExpectedOutputDescription = ["The output should show a workflow using an MCP tool with approval to search Microsoft Learn, followed by an exit from the input loop."],
+ },
+ ];
+}
diff --git a/dotnet/eng/verify-samples/verify-samples.csproj b/dotnet/eng/verify-samples/verify-samples.csproj
new file mode 100644
index 0000000000..f7f86ba90d
--- /dev/null
+++ b/dotnet/eng/verify-samples/verify-samples.csproj
@@ -0,0 +1,24 @@
+
+
+
+ Exe
+ net10.0
+ enable
+ enable
+ false
+ false
+
+ $(NoWarn);CA2007
+
+
+
+
+
+
+
+
+
+
+
+
+