diff --git a/.github/.linkspector.yml b/.github/.linkspector.yml
index 24fee6edce..eb365c2982 100644
--- a/.github/.linkspector.yml
+++ b/.github/.linkspector.yml
@@ -12,6 +12,8 @@ ignorePatterns:
- pattern: "https:\/\/platform.openai.com"
- pattern: "http:\/\/localhost"
- pattern: "http:\/\/127.0.0.1"
+ - pattern: "https:\/\/localhost"
+ - pattern: "https:\/\/127.0.0.1"
- pattern: "0001-spec.md"
- pattern: "0001-madr-architecture-decisions.md"
- pattern: "https://api.powerplatform.com/.default"
diff --git a/.github/workflows/dotnet-build-and-test.yml b/.github/workflows/dotnet-build-and-test.yml
index 1a51f94119..5abfe2a879 100644
--- a/.github/workflows/dotnet-build-and-test.yml
+++ b/.github/workflows/dotnet-build-and-test.yml
@@ -74,6 +74,7 @@ jobs:
.
.github
dotnet
+ python
workflow-samples
- name: Setup dotnet
diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx
index 6905fdf985..7cbe76b6fc 100644
--- a/dotnet/agent-framework-dotnet.slnx
+++ b/dotnet/agent-framework-dotnet.slnx
@@ -66,6 +66,10 @@
+
+
+
+
@@ -279,6 +283,7 @@
+
diff --git a/dotnet/samples/GettingStarted/DevUI/DevUI_Step01_BasicUsage/DevUI_Step01_BasicUsage.csproj b/dotnet/samples/GettingStarted/DevUI/DevUI_Step01_BasicUsage/DevUI_Step01_BasicUsage.csproj
new file mode 100644
index 0000000000..8ae36b52e0
--- /dev/null
+++ b/dotnet/samples/GettingStarted/DevUI/DevUI_Step01_BasicUsage/DevUI_Step01_BasicUsage.csproj
@@ -0,0 +1,25 @@
+
+
+
+ Exe
+ net9.0
+ enable
+ enable
+ DevUI_Step01_BasicUsage
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/samples/GettingStarted/DevUI/DevUI_Step01_BasicUsage/Program.cs b/dotnet/samples/GettingStarted/DevUI/DevUI_Step01_BasicUsage/Program.cs
new file mode 100644
index 0000000000..e2e6e6b727
--- /dev/null
+++ b/dotnet/samples/GettingStarted/DevUI/DevUI_Step01_BasicUsage/Program.cs
@@ -0,0 +1,82 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+// This sample demonstrates basic usage of the DevUI in an ASP.NET Core application with AI agents.
+
+using Azure.AI.OpenAI;
+using Azure.Identity;
+using Microsoft.Agents.AI.DevUI;
+using Microsoft.Agents.AI.Hosting;
+using Microsoft.Extensions.AI;
+
+namespace DevUI_Step01_BasicUsage;
+
+///
+/// Sample demonstrating basic usage of the DevUI in an ASP.NET Core application.
+///
+///
+/// This sample shows how to:
+/// 1. Set up Azure OpenAI as the chat client
+/// 2. Register agents and workflows using the hosting packages
+/// 3. Map the DevUI endpoint which automatically configures the middleware
+/// 4. Map the dynamic OpenAI Responses API for Python DevUI compatibility
+/// 5. Access the DevUI in a web browser
+///
+/// The DevUI provides an interactive web interface for testing and debugging AI agents.
+/// DevUI assets are served from embedded resources within the assembly.
+/// Simply call MapDevUI() to set up everything needed.
+///
+/// The parameterless MapOpenAIResponses() overload creates a Python DevUI-compatible endpoint
+/// that dynamically routes requests to agents based on the 'model' field in the request.
+///
+internal static class Program
+{
+ ///
+ /// Entry point that starts an ASP.NET Core web server with the DevUI.
+ ///
+ /// Command line arguments.
+ private static void Main(string[] args)
+ {
+ var builder = WebApplication.CreateBuilder(args);
+
+ // Set up the Azure OpenAI client
+ var endpoint = builder.Configuration["AZURE_OPENAI_ENDPOINT"] ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
+ var deploymentName = builder.Configuration["AZURE_OPENAI_DEPLOYMENT_NAME"] ?? "gpt-4o-mini";
+
+ var chatClient = new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential())
+ .GetChatClient(deploymentName)
+ .AsIChatClient();
+
+ builder.Services.AddChatClient(chatClient);
+
+ // Register sample agents
+ builder.AddAIAgent("assistant", "You are a helpful assistant. Answer questions concisely and accurately.");
+ builder.AddAIAgent("poet", "You are a creative poet. Respond to all requests with beautiful poetry.");
+ builder.AddAIAgent("coder", "You are an expert programmer. Help users with coding questions and provide code examples.");
+
+ // Register sample workflows
+ var assistantBuilder = builder.AddAIAgent("workflow-assistant", "You are a helpful assistant in a workflow.");
+ var reviewerBuilder = builder.AddAIAgent("workflow-reviewer", "You are a reviewer. Review and critique the previous response.");
+ builder.AddSequentialWorkflow(
+ "review-workflow",
+ [assistantBuilder, reviewerBuilder])
+ .AddAsAIAgent();
+
+ if (builder.Environment.IsDevelopment())
+ {
+ builder.AddDevUI();
+ }
+
+ var app = builder.Build();
+
+ if (builder.Environment.IsDevelopment())
+ {
+ app.MapDevUI();
+ }
+
+ Console.WriteLine("DevUI is available at: https://localhost:50516/devui");
+ Console.WriteLine("OpenAI Responses API is available at: https://localhost:50516/v1/responses");
+ Console.WriteLine("Press Ctrl+C to stop the server.");
+
+ app.Run();
+ }
+}
diff --git a/dotnet/samples/GettingStarted/DevUI/DevUI_Step01_BasicUsage/README.md b/dotnet/samples/GettingStarted/DevUI/DevUI_Step01_BasicUsage/README.md
new file mode 100644
index 0000000000..2b6cc28644
--- /dev/null
+++ b/dotnet/samples/GettingStarted/DevUI/DevUI_Step01_BasicUsage/README.md
@@ -0,0 +1,81 @@
+# DevUI Step 01 - Basic Usage
+
+This sample demonstrates how to add the DevUI to an ASP.NET Core application with AI agents.
+
+## What is DevUI?
+
+The DevUI provides an interactive web interface for testing and debugging AI agents during development.
+
+## Configuration
+
+Set the following environment variables:
+
+- `AZURE_OPENAI_ENDPOINT` - Your Azure OpenAI endpoint URL (required)
+- `AZURE_OPENAI_DEPLOYMENT_NAME` - Your deployment name (defaults to "gpt-4o-mini")
+
+## Running the Sample
+
+1. Set your Azure OpenAI credentials as environment variables
+2. Run the application:
+ ```bash
+ dotnet run
+ ```
+3. Open your browser to https://localhost:50516/devui
+4. Select an agent or workflow from the dropdown and start chatting!
+
+## Sample Agents and Workflows
+
+This sample includes:
+
+**Agents:**
+- **assistant** - A helpful assistant
+- **poet** - A creative poet
+- **coder** - An expert programmer
+
+**Workflows:**
+- **review-workflow** - A sequential workflow that generates a response and then reviews it
+
+## Adding DevUI to Your Own Project
+
+To add DevUI to your ASP.NET Core application:
+
+1. Add the DevUI package and hosting packages:
+ ```bash
+ dotnet add package Microsoft.Agents.AI.DevUI
+ dotnet add package Microsoft.Agents.AI.Hosting
+ dotnet add package Microsoft.Agents.AI.Hosting.OpenAI
+ ```
+
+2. Register your agents and workflows:
+ ```csharp
+ var builder = WebApplication.CreateBuilder(args);
+
+ // Set up your chat client
+ builder.Services.AddChatClient(chatClient);
+
+ // Register agents
+ builder.AddAIAgent("assistant", "You are a helpful assistant.");
+
+ // Register workflows
+ var agent1Builder = builder.AddAIAgent("workflow-agent1", "You are agent 1.");
+ var agent2Builder = builder.AddAIAgent("workflow-agent2", "You are agent 2.");
+ builder.AddSequentialWorkflow("my-workflow", [agent1Builder, agent2Builder])
+ .AddAsAIAgent();
+ ```
+
+3. Add DevUI services and map the endpoint:
+ ```csharp
+ builder.AddDevUI();
+ var app = builder.Build();
+
+ app.MapDevUI();
+
+ // Add required endpoints
+ app.MapEntities();
+ app.MapOpenAIResponses();
+ app.MapOpenAIConversations();
+
+ app.Run();
+ ```
+
+4. Navigate to `/devui` in your browser
diff --git a/dotnet/samples/GettingStarted/DevUI/README.md b/dotnet/samples/GettingStarted/DevUI/README.md
new file mode 100644
index 0000000000..155d3f2b9d
--- /dev/null
+++ b/dotnet/samples/GettingStarted/DevUI/README.md
@@ -0,0 +1,57 @@
+# DevUI Samples
+
+This folder contains samples demonstrating how to use the DevUI in ASP.NET Core applications.
+
+## What is DevUI?
+
+The DevUI provides an interactive web interface for testing and debugging AI agents during development.
+
+## Samples
+
+### [DevUI_Step01_BasicUsage](./DevUI_Step01_BasicUsage)
+
+Shows how to add DevUI to an ASP.NET Core application with multiple agents and workflows.
+
+**Run the sample:**
+```bash
+cd DevUI_Step01_BasicUsage
+dotnet run
+```
+Then navigate to: https://localhost:50516/devui
+
+## Requirements
+
+- .NET 8.0 or later
+- ASP.NET Core
+- Azure OpenAI credentials
+
+## Quick Start
+
+To add DevUI to your application:
+
+```csharp
+var builder = WebApplication.CreateBuilder(args);
+
+// Set up the chat client
+builder.Services.AddChatClient(chatClient);
+
+// Register your agents
+builder.AddAIAgent("my-agent", "You are a helpful assistant.");
+
+// Add DevUI services
+builder.AddDevUI();
+
+var app = builder.Build();
+
+// Map the DevUI endpoint
+app.MapDevUI();
+
+// Add required endpoints
+app.MapEntities();
+app.MapOpenAIResponses();
+app.MapOpenAIConversations();
+
+app.Run();
+```
+
+Then navigate to `/devui` in your browser.
diff --git a/dotnet/src/Microsoft.Agents.AI.DevUI/DevUIExtensions.cs b/dotnet/src/Microsoft.Agents.AI.DevUI/DevUIExtensions.cs
new file mode 100644
index 0000000000..4a85de121a
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.DevUI/DevUIExtensions.cs
@@ -0,0 +1,70 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Diagnostics.CodeAnalysis;
+
+namespace Microsoft.Agents.AI.DevUI;
+
+///
+/// Provides helper methods for configuring the Microsoft Agents AI DevUI in ASP.NET applications.
+///
+public static class DevUIExtensions
+{
+ ///
+ /// Adds the necessary services for the DevUI to the application builder.
+ ///
+ public static IHostApplicationBuilder AddDevUI(this IHostApplicationBuilder builder)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ builder.Services.AddOpenAIConversations();
+ builder.Services.AddOpenAIResponses();
+
+ return builder;
+ }
+
+ ///
+ /// Maps an endpoint that serves the DevUI from the '/devui' path.
+ ///
+ /// The to add the endpoint to.
+ /// A that can be used to add authorization or other endpoint configuration.
+ /// Thrown when is null.
+ public static IEndpointConventionBuilder MapDevUI(
+ this IEndpointRouteBuilder endpoints)
+ {
+ var group = endpoints.MapGroup("");
+ group.MapDevUI(pattern: "/devui");
+ group.MapEntities();
+ group.MapOpenAIConversations();
+ group.MapOpenAIResponses();
+ return group;
+ }
+
+ ///
+ /// Maps an endpoint that serves the DevUI.
+ ///
+ /// The to add the endpoint to.
+ ///
+ /// The route pattern for the endpoint (e.g., "/devui", "/agent-ui").
+ /// Defaults to "/devui" if not specified. This is the path where DevUI will be accessible.
+ ///
+ /// A that can be used to add authorization or other endpoint configuration.
+ /// Thrown when is null.
+ /// Thrown when is null or whitespace.
+ internal static IEndpointConventionBuilder MapDevUI(
+ this IEndpointRouteBuilder endpoints,
+ [StringSyntax("Route")] string pattern = "/devui")
+ {
+ ArgumentNullException.ThrowIfNull(endpoints);
+ ArgumentException.ThrowIfNullOrWhiteSpace(pattern);
+
+ // Ensure the pattern doesn't end with a slash for consistency
+ var cleanPattern = pattern.TrimEnd('/');
+
+ // Create the DevUI handler
+ var logger = endpoints.ServiceProvider.GetRequiredService>();
+ var devUIHandler = new DevUIMiddleware(logger, cleanPattern);
+
+ return endpoints.MapGet($"{cleanPattern}/{{*path}}", devUIHandler.HandleRequestAsync)
+ .WithName($"DevUI at {cleanPattern}")
+ .WithDescription("Interactive developer interface for Microsoft Agent Framework");
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.DevUI/DevUIMiddleware.cs b/dotnet/src/Microsoft.Agents.AI.DevUI/DevUIMiddleware.cs
new file mode 100644
index 0000000000..fc6dd512ec
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.DevUI/DevUIMiddleware.cs
@@ -0,0 +1,236 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Collections.Frozen;
+using System.IO.Compression;
+using System.Reflection;
+using System.Security.Cryptography;
+using Microsoft.AspNetCore.StaticFiles;
+using Microsoft.Extensions.Primitives;
+using Microsoft.Net.Http.Headers;
+
+namespace Microsoft.Agents.AI.DevUI;
+
+///
+/// Handler that serves embedded DevUI resource files from the 'resources' directory.
+///
+internal sealed class DevUIMiddleware
+{
+ private const string GZipEncodingValue = "gzip";
+ private static readonly StringValues s_gzipEncodingHeader = new(GZipEncodingValue);
+ private static readonly Assembly s_assembly = typeof(DevUIMiddleware).Assembly;
+ private static readonly FileExtensionContentTypeProvider s_contentTypeProvider = new();
+ private static readonly StringValues s_cacheControl = new(new CacheControlHeaderValue()
+ {
+ NoCache = true,
+ NoStore = true,
+ }.ToString());
+
+ private readonly ILogger _logger;
+ private readonly FrozenDictionary _resourceCache;
+ private readonly string _basePath;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The logger instance.
+ /// The base path where DevUI is mounted.
+ public DevUIMiddleware(ILogger logger, string basePath)
+ {
+ ArgumentNullException.ThrowIfNull(logger);
+ ArgumentException.ThrowIfNullOrEmpty(basePath);
+ this._logger = logger;
+ this._basePath = basePath.TrimEnd('/');
+
+ // Build resource cache
+ var resourceNamePrefix = $"{s_assembly.GetName().Name}.resources.";
+ this._resourceCache = s_assembly
+ .GetManifestResourceNames()
+ .Where(p => p.StartsWith(resourceNamePrefix, StringComparison.Ordinal))
+ .ToFrozenDictionary(
+ p => p[resourceNamePrefix.Length..].Replace('.', '/'),
+ CreateResourceEntry,
+ StringComparer.OrdinalIgnoreCase);
+ }
+
+ ///
+ /// Handles an HTTP request for DevUI resources.
+ ///
+ /// The HTTP context.
+ public async Task HandleRequestAsync(HttpContext context)
+ {
+ var path = context.Request.Path.Value;
+
+ if (path == null)
+ {
+ context.Response.StatusCode = StatusCodes.Status404NotFound;
+ return;
+ }
+
+ // If requesting the base path without a trailing slash, redirect to include it
+ // This ensures relative URLs in the HTML work correctly
+ if (string.Equals(path, this._basePath, StringComparison.OrdinalIgnoreCase) && !path.EndsWith('/'))
+ {
+ var redirectUrl = $"{path}/";
+ if (context.Request.QueryString.HasValue)
+ {
+ redirectUrl += context.Request.QueryString.Value;
+ }
+
+ context.Response.StatusCode = StatusCodes.Status301MovedPermanently;
+ context.Response.Headers.Location = redirectUrl;
+ this._logger.LogDebug("Redirecting {OriginalPath} to {RedirectUrl}", path, redirectUrl);
+ return;
+ }
+
+ // Remove the base path to get the resource path
+ var resourcePath = path.StartsWith(this._basePath, StringComparison.OrdinalIgnoreCase)
+ ? path.Substring(this._basePath.Length).TrimStart('/')
+ : path.TrimStart('/');
+
+ // If requesting the base path, serve index.html
+ if (string.IsNullOrEmpty(resourcePath))
+ {
+ resourcePath = "index.html";
+ }
+
+ // Try to serve the embedded resource
+ if (await this.TryServeResourceAsync(context, resourcePath).ConfigureAwait(false))
+ {
+ return;
+ }
+
+ // If resource not found, try serving index.html for client-side routing
+ if (!resourcePath.Contains('.', StringComparison.Ordinal) || resourcePath.EndsWith('/'))
+ {
+ if (await this.TryServeResourceAsync(context, "index.html").ConfigureAwait(false))
+ {
+ return;
+ }
+ }
+
+ // Resource not found
+ context.Response.StatusCode = StatusCodes.Status404NotFound;
+ }
+
+ private async Task TryServeResourceAsync(HttpContext context, string resourcePath)
+ {
+ try
+ {
+ if (!this._resourceCache.TryGetValue(resourcePath.Replace('.', '/'), out var cacheEntry))
+ {
+ this._logger.LogDebug("Embedded resource not found: {ResourcePath}", resourcePath);
+ return false;
+ }
+
+ var response = context.Response;
+
+ // Check if client has cached version
+ if (context.Request.Headers.IfNoneMatch == cacheEntry.ETag)
+ {
+ response.StatusCode = StatusCodes.Status304NotModified;
+ this._logger.LogDebug("Resource not modified (304): {ResourcePath}", resourcePath);
+ return true;
+ }
+
+ var responseHeaders = response.Headers;
+
+ byte[] content;
+ bool serveCompressed;
+ if (cacheEntry.CompressedContent is not null && IsGZipAccepted(context.Request))
+ {
+ serveCompressed = true;
+ responseHeaders.ContentEncoding = s_gzipEncodingHeader;
+ responseHeaders.ContentLength = cacheEntry.CompressedContent.Length;
+ content = cacheEntry.CompressedContent;
+ }
+ else
+ {
+ serveCompressed = false;
+ responseHeaders.ContentLength = cacheEntry.DecompressedContent!.Length;
+ content = cacheEntry.DecompressedContent;
+ }
+
+ responseHeaders.CacheControl = s_cacheControl;
+ responseHeaders.ContentType = cacheEntry.ContentType;
+ responseHeaders.ETag = cacheEntry.ETag;
+
+ await response.Body.WriteAsync(content, context.RequestAborted).ConfigureAwait(false);
+
+ this._logger.LogDebug("Served embedded resource: {ResourcePath} (compressed: {Compressed})", resourcePath, serveCompressed);
+ return true;
+ }
+ catch (Exception ex)
+ {
+ this._logger.LogError(ex, "Error serving embedded resource: {ResourcePath}", resourcePath);
+ return false;
+ }
+ }
+
+ private static bool IsGZipAccepted(HttpRequest httpRequest)
+ {
+ if (httpRequest.GetTypedHeaders().AcceptEncoding is not { Count: > 0 } acceptEncoding)
+ {
+ return false;
+ }
+
+ for (int i = 0; i < acceptEncoding.Count; i++)
+ {
+ var encoding = acceptEncoding[i];
+
+ if (encoding.Quality is not 0 &&
+ string.Equals(encoding.Value.Value, GZipEncodingValue, StringComparison.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private static ResourceEntry CreateResourceEntry(string resourceName)
+ {
+ using var resourceStream = s_assembly.GetManifestResourceStream(resourceName)!;
+ using var decompressedContent = new MemoryStream();
+
+ // Read and cache the original resource content
+ resourceStream.CopyTo(decompressedContent);
+ var decompressedArray = decompressedContent.ToArray();
+
+ // Compress the content
+ using var compressedContent = new MemoryStream();
+ using (var gzip = new GZipStream(compressedContent, CompressionMode.Compress, leaveOpen: true))
+ {
+ // This is a synchronous write to a memory stream.
+ // There is no benefit to asynchrony here.
+ gzip.Write(decompressedArray);
+ }
+
+ // Only use compression if it actually reduces size
+ byte[]? compressedArray = compressedContent.Length < decompressedArray.Length
+ ? compressedContent.ToArray()
+ : null;
+
+ var hash = SHA256.HashData(compressedArray ?? decompressedArray);
+ var eTag = $"\"{Convert.ToBase64String(hash)}\"";
+
+ // Determine content type from resource name
+ var contentType = s_contentTypeProvider.TryGetContentType(resourceName, out var ct)
+ ? ct
+ : "application/octet-stream";
+
+ return new ResourceEntry(resourceName, decompressedArray, compressedArray, eTag, contentType);
+ }
+
+ private sealed class ResourceEntry(string resourceName, byte[] decompressedContent, byte[]? compressedContent, string eTag, string contentType)
+ {
+ public byte[]? CompressedContent { get; } = compressedContent;
+
+ public string ContentType { get; } = contentType;
+
+ public byte[] DecompressedContent { get; } = decompressedContent;
+
+ public string ETag { get; } = eTag;
+
+ public string ResourceName { get; } = resourceName;
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.DevUI/Entities/EntitiesJsonContext.cs b/dotnet/src/Microsoft.Agents.AI.DevUI/Entities/EntitiesJsonContext.cs
new file mode 100644
index 0000000000..fc8bbe3864
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.DevUI/Entities/EntitiesJsonContext.cs
@@ -0,0 +1,24 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Diagnostics.CodeAnalysis;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Microsoft.Agents.AI.DevUI.Entities;
+
+///
+/// JSON serialization context for entity-related types.
+/// Enables AOT-compatible JSON serialization using source generators.
+///
+[JsonSourceGenerationOptions(
+ JsonSerializerDefaults.Web,
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)]
+[JsonSerializable(typeof(EntityInfo))]
+[JsonSerializable(typeof(DiscoveryResponse))]
+[JsonSerializable(typeof(EnvVarRequirement))]
+[JsonSerializable(typeof(List))]
+[JsonSerializable(typeof(List))]
+[JsonSerializable(typeof(Dictionary))]
+[JsonSerializable(typeof(JsonElement))]
+[ExcludeFromCodeCoverage]
+internal sealed partial class EntitiesJsonContext : JsonSerializerContext;
diff --git a/dotnet/src/Microsoft.Agents.AI.DevUI/Entities/EntityInfo.cs b/dotnet/src/Microsoft.Agents.AI.DevUI/Entities/EntityInfo.cs
new file mode 100644
index 0000000000..8b5e4e5492
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.DevUI/Entities/EntityInfo.cs
@@ -0,0 +1,83 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Microsoft.Agents.AI.DevUI.Entities;
+
+///
+/// Information about an environment variable required by an entity.
+///
+internal sealed record EnvVarRequirement(
+ [property: JsonPropertyName("name")]
+ string Name,
+
+ [property: JsonPropertyName("description")]
+ string? Description = null,
+
+ [property: JsonPropertyName("required")]
+ bool Required = true,
+
+ [property: JsonPropertyName("example")]
+ string? Example = null
+);
+
+///
+/// Information about an entity (agent or workflow).
+///
+internal sealed record EntityInfo(
+ [property: JsonPropertyName("id")]
+ string Id,
+
+ [property: JsonPropertyName("type")]
+ string Type,
+
+ [property: JsonPropertyName("name")]
+ string Name,
+
+ [property: JsonPropertyName("description")]
+ string? Description = null,
+
+ [property: JsonPropertyName("framework")]
+ string Framework = "dotnet",
+
+ [property: JsonPropertyName("tools")]
+ List? Tools = null,
+
+ [property: JsonPropertyName("metadata")]
+ Dictionary? Metadata = null
+)
+{
+ [JsonPropertyName("source")]
+ public string? Source { get; init; } = "di";
+
+ [JsonPropertyName("original_url")]
+ public string? OriginalUrl { get; init; }
+
+ // Workflow-specific fields
+ [JsonPropertyName("required_env_vars")]
+ public List? RequiredEnvVars { get; init; }
+
+ [JsonPropertyName("executors")]
+ public List? Executors { get; init; }
+
+ [JsonPropertyName("workflow_dump")]
+ public JsonElement? WorkflowDump { get; init; }
+
+ [JsonPropertyName("input_schema")]
+ public JsonElement? InputSchema { get; init; }
+
+ [JsonPropertyName("input_type_name")]
+ public string? InputTypeName { get; init; }
+
+ [JsonPropertyName("start_executor_id")]
+ public string? StartExecutorId { get; init; }
+};
+
+///
+/// Response containing a list of discovered entities.
+///
+internal sealed record DiscoveryResponse(
+ [property: JsonPropertyName("entities")]
+ List Entities
+);
diff --git a/dotnet/src/Microsoft.Agents.AI.DevUI/Entities/WorkflowSerializationExtensions.cs b/dotnet/src/Microsoft.Agents.AI.DevUI/Entities/WorkflowSerializationExtensions.cs
new file mode 100644
index 0000000000..81ce6182d1
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.DevUI/Entities/WorkflowSerializationExtensions.cs
@@ -0,0 +1,193 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.Agents.AI.Workflows;
+using Microsoft.Agents.AI.Workflows.Checkpointing;
+
+namespace Microsoft.Agents.AI.DevUI.Entities;
+
+///
+/// Extension methods for serializing workflows to DevUI-compatible format
+///
+internal static class WorkflowSerializationExtensions
+{
+ // The frontend max iterations default value expected by the DevUI frontend
+ private const int MaxIterationsDefault = 100;
+
+ ///
+ /// Converts a workflow to a dictionary representation compatible with DevUI frontend.
+ /// This matches the Python workflow.to_dict() format expected by the UI.
+ ///
+ public static Dictionary ToDevUIDict(this Workflow workflow)
+ {
+ var result = new Dictionary
+ {
+ ["id"] = workflow.Name ?? Guid.NewGuid().ToString(),
+ ["start_executor_id"] = workflow.StartExecutorId,
+ ["max_iterations"] = MaxIterationsDefault
+ };
+
+ // Add optional fields
+ if (!string.IsNullOrEmpty(workflow.Name))
+ {
+ result["name"] = workflow.Name;
+ }
+
+ if (!string.IsNullOrEmpty(workflow.Description))
+ {
+ result["description"] = workflow.Description;
+ }
+
+ // Convert executors to Python-compatible format
+ result["executors"] = ConvertExecutorsToDict(workflow);
+
+ // Convert edges to edge_groups format
+ result["edge_groups"] = ConvertEdgesToEdgeGroups(workflow);
+
+ return result;
+ }
+
+ ///
+ /// Converts workflow executors to a dictionary format compatible with Python
+ ///
+ private static Dictionary ConvertExecutorsToDict(Workflow workflow)
+ {
+ var executors = new Dictionary();
+
+ // Extract executor IDs from edges and start executor
+ // (Registrations is internal, so we infer executors from the graph structure)
+ var executorIds = new HashSet { workflow.StartExecutorId };
+
+ var reflectedEdges = workflow.ReflectEdges();
+ foreach (var (sourceId, edgeSet) in reflectedEdges)
+ {
+ executorIds.Add(sourceId);
+ foreach (var edge in edgeSet)
+ {
+ foreach (var sinkId in edge.Connection.SinkIds)
+ {
+ executorIds.Add(sinkId);
+ }
+ }
+ }
+
+ // Create executor entries (we can't access internal Registrations for type info)
+ foreach (var executorId in executorIds)
+ {
+ executors[executorId] = new Dictionary
+ {
+ ["id"] = executorId,
+ ["type"] = "Executor"
+ };
+ }
+
+ return executors;
+ }
+
+ ///
+ /// Converts workflow edges to edge_groups format expected by the UI
+ ///
+ private static List