diff --git a/dotnet/nuget.config b/dotnet/nuget.config
index 76d943ce16..49f96479ca 100644
--- a/dotnet/nuget.config
+++ b/dotnet/nuget.config
@@ -3,10 +3,14 @@
+
+
+
+
\ No newline at end of file
diff --git a/dotnet/samples/AzureFunctions/09_Workflow/OrchFunction.cs b/dotnet/samples/AzureFunctions/09_Workflow/OrchFunction.cs
deleted file mode 100644
index c20fa550b4..0000000000
--- a/dotnet/samples/AzureFunctions/09_Workflow/OrchFunction.cs
+++ /dev/null
@@ -1,40 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using Microsoft.Azure.Functions.Worker;
-using Microsoft.Azure.Functions.Worker.Http;
-using Microsoft.DurableTask;
-using Microsoft.DurableTask.Client;
-using Microsoft.Extensions.Logging;
-
-namespace SingleAgent;
-
-public static class OrchFunction
-{
- [Function(nameof(OrchFunction))]
- public static async Task> RunOrchestratorAsync(
- [OrchestrationTrigger] TaskOrchestrationContext context)
- {
- ILogger logger = context.CreateReplaySafeLogger(nameof(OrchFunction));
- logger.LogInformation("Saying hello.");
- var outputs = new List();
-
- outputs.Add("Tokyo");
-
- return outputs;
- }
-
- [Function("OrchFunction_HttpStart")]
- public static async Task HttpStartAsync(
- [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequestData req,
- [DurableClient] DurableTaskClient client,
- FunctionContext executionContext)
- {
- // Function input comes from the request content.
- string instanceId = await client.ScheduleNewOrchestrationInstanceAsync(
- nameof(OrchFunction));
-
- // Returns an HTTP 202 response with an instance management payload.
- // See https://learn.microsoft.com/azure/azure-functions/durable/durable-functions-http-api#start-orchestration
- return await client.CreateCheckStatusResponseAsync(req, instanceId);
- }
-}
diff --git a/dotnet/samples/AzureFunctions/09_Workflow/Program.cs b/dotnet/samples/AzureFunctions/09_Workflow/Program.cs
index 87120d9d49..cab0b996bb 100644
--- a/dotnet/samples/AzureFunctions/09_Workflow/Program.cs
+++ b/dotnet/samples/AzureFunctions/09_Workflow/Program.cs
@@ -41,16 +41,16 @@ builder.AddEdge(agent2, reverse).WithOutputFrom(agent2);
var workflow = builder.WithName("MyTestWorkflow").Build();
-// Configure the function app to host the AI agent.
-// This will automatically generate HTTP API endpoints for the agent.
+// Configure the function app to host AI agents and workflows in a unified way.
+// This will automatically generate HTTP API endpoints for agents and workflows.
using IHost app = FunctionsApplication
.CreateBuilder(args)
.ConfigureFunctionsWebApplication()
- .ConfigureDurableAgents(options => options.AddAIAgent(agent, timeToLive: TimeSpan.FromHours(1)))
- .AddDurableWorkflows(options =>
+ //.ConfigureDurableAgents(op => op.AddAIAgent(agent, timeToLive: TimeSpan.FromHours(1)))
+ .ConfigureDurableOptions(options =>
{
- // Configure durable workflow options here if needed.
- options.AddWorkflow(workflow);
+ // Configure workflows - agents referenced in workflows are automatically registered!
+ options.Workflows.AddWorkflow(workflow);
})
.Build();
app.Run();
diff --git a/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAgentsOptions.cs b/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAgentsOptions.cs
index cefcad323a..f08d114121 100644
--- a/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAgentsOptions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAgentsOptions.cs
@@ -11,7 +11,10 @@ public sealed class DurableAgentsOptions
private readonly Dictionary> _agentFactories = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary _agentTimeToLive = new(StringComparer.OrdinalIgnoreCase);
- internal DurableAgentsOptions()
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public DurableAgentsOptions()
{
}
diff --git a/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableWorkflowOptions.cs b/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableWorkflowOptions.cs
index c3f7b6c75d..ab8bfcd4b1 100644
--- a/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableWorkflowOptions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableWorkflowOptions.cs
@@ -10,11 +10,25 @@ namespace Microsoft.Agents.AI.DurableTask;
public sealed class DurableWorkflowOptions
{
private readonly Dictionary _workflows = new(StringComparer.OrdinalIgnoreCase);
+ private readonly object? _parentOptions;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Optional parent options container for accessing related configuration.
+ public DurableWorkflowOptions(object? parentOptions = null)
+ {
+ this._parentOptions = parentOptions;
+ }
///
/// Adds a workflow to the collection for processing or execution.
///
/// The workflow instance to add. Cannot be null.
+ ///
+ /// When a workflow is added, any AI agent executors in the workflow will be automatically
+ /// registered with the DurableAgentsOptions if it was provided during construction.
+ ///
public void AddWorkflow(Workflow workflow)
{
ArgumentNullException.ThrowIfNull(workflow);
@@ -25,6 +39,13 @@ public sealed class DurableWorkflowOptions
}
this._workflows[workflow.Name] = workflow;
+
+ // Register any agentic executors with DurableAgentsOptions if available through parent
+ DurableAgentsOptions? agentOptions = this.TryGetAgentOptions();
+ if (agentOptions is not null)
+ {
+ RegisterAgenticExecutors(workflow, agentOptions);
+ }
}
///
@@ -34,4 +55,46 @@ public sealed class DurableWorkflowOptions
/// Changes to the underlying workflow collection are immediately visible through this property. Accessing a
/// workflow by name that does not exist will result in a KeyNotFoundException.
public IReadOnlyDictionary Workflows => this._workflows;
+
+ [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("ReflectionAnalysis", "IL2075",
+ Justification = "Reflection is used to access Agents property from parent options container for automatic agent registration.")]
+ private DurableAgentsOptions? TryGetAgentOptions()
+ {
+ // Try to extract DurableAgentsOptions from the parent container
+ // This uses reflection to access the Agents property if available
+ if (this._parentOptions is null)
+ {
+ return null;
+ }
+
+ // Check if parent has an Agents property (DurableOptions pattern)
+ System.Reflection.PropertyInfo? agentsProperty = this._parentOptions.GetType()
+ .GetProperty("Agents", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
+
+ if (agentsProperty?.PropertyType == typeof(DurableAgentsOptions))
+ {
+ return agentsProperty.GetValue(this._parentOptions) as DurableAgentsOptions;
+ }
+
+ // If parent is directly DurableAgentsOptions (for backward compatibility)
+ return this._parentOptions as DurableAgentsOptions;
+ }
+
+ private static void RegisterAgenticExecutors(Workflow workflow, DurableAgentsOptions agentOptions)
+ {
+ // Use the public EnumerateAgentExecutors method to get all AIAgent instances
+ foreach (AIAgent agent in workflow.EnumerateAgentExecutors())
+ {
+ try
+ {
+ // Register the agent with DurableAgentsOptions
+ agentOptions.AddAIAgent(agent);
+ }
+ catch (ArgumentException)
+ {
+ // Agent with this name is already registered, skip it
+ // This is expected behavior when multiple workflows use the same agent
+ }
+ }
+ }
}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctionExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctionExecutor.cs
index 47c2599499..6733615875 100644
--- a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctionExecutor.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctionExecutor.cs
@@ -5,7 +5,6 @@ using Microsoft.Azure.Functions.Worker.Context.Features;
using Microsoft.Azure.Functions.Worker.Extensions.Mcp;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.Azure.Functions.Worker.Invocation;
-using Microsoft.DurableTask;
using Microsoft.DurableTask.Client;
namespace Microsoft.Agents.AI.Hosting.AzureFunctions;
@@ -26,17 +25,9 @@ internal sealed class BuiltInFunctionExecutor : IFunctionExecutor
IFunctionInputBindingFeature? functionInputBindingFeature = context.Features.Get() ??
throw new InvalidOperationException("Function input binding feature is not available on the current context.");
- if (context.FunctionDefinition.EntryPoint == BuiltInFunctions.RunWorkflowOrechstrtationFunctionEntryPoint)
+ if (context.FunctionDefinition.EntryPoint == BuiltInFunctions.InvokeWorkflowActivityFunctionEntryPoint)
{
- var triggerBinding = context.FunctionDefinition.InputBindings.Values.FirstOrDefault(b => b.Type == "orchestrationTrigger");
- var taskOrechstrationContextBinding = context.BindInputAsync(triggerBinding!);
-
- if (taskOrechstrationContextBinding.IsCompletedSuccessfully)
- {
- var t = taskOrechstrationContextBinding.Result.Value;
- context.GetInvocationResult().Value = await BuiltInFunctions.RunWorkflowOrchestratorAsync("todo", context);
- }
-
+ context.GetInvocationResult().Value = await BuiltInFunctions.InvokeWorkflowActivityAsync("aa", context);
return;
}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctions.cs
index 275be12f86..e146425adf 100644
--- a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctions.cs
@@ -36,25 +36,17 @@ internal static class BuiltInFunctions
[ActivityTrigger] string input,
FunctionContext functionContext)
{
- return Task.FromResult($"Hello from activity with input: {input}");
+ string activityFunctionName = functionContext.FunctionDefinition.Name;
+
+ DurableWorkflowRunner runner = functionContext.InstanceServices.GetRequiredService();
+ return runner.ExecuteActivityAsync(activityFunctionName, input, functionContext);
}
- //[Function("my-Orchestration")]
- //public static async Task> RunOrchestrator1Async(
- //[OrchestrationTrigger] TaskOrchestrationContext context)
- //{
- // ILogger logger = context.CreateReplaySafeLogger(nameof(Function));
- // logger.LogInformation("Saying hello.");
-
- // // returns ["Hello Tokyo!", "Hello Seattle!", "Hello London!"]
- // return new List();
- //}
-
public static async Task> RunWorkflowOrchestratorAsync(string taskOrchestrationContext, FunctionContext functionsContext)
{
var logger = functionsContext.GetLogger("BuiltInFunctions");
var outputs = new List();
- const string WorkflowName = "MyTestWorkflow"; // to do: get from TaskOrchestrtionContext
+ const string WorkflowName = "MyTestWorkflow"; // to do: get from TaskOrchestrationContext
if (logger.IsEnabled(LogLevel.Information))
{
logger.LogInformation("Orchestrator {WorkflowName} is executing. Input: {Input}", WorkflowName, taskOrchestrationContext);
@@ -95,10 +87,10 @@ internal static class BuiltInFunctions
FunctionContext context)
{
// to do: Retrieve the workflow and execute it.
- var workflowName = context.FunctionDefinition.Name.Replace("http", "dafx");
-
+ var workflowName = context.FunctionDefinition.Name.Replace("http-", "");
+ var inputMessage = await req.ReadAsStringAsync();
//string instanceId = await client.ScheduleNewOrchestrationInstanceAsync("dafx-MyTestWorkflow");
- string instanceId = await client.ScheduleNewOrchestrationInstanceAsync("dafx-MyTestWorkflow"); //OrchFunction"); // dafx-MyTestWorkflow");
+ string instanceId = await client.ScheduleNewOrchestrationInstanceAsync("WorkflowRunnerOrchestration", new DuableWorkflowRunRequest { WorkflowName = workflowName, Input = inputMessage! }); //OrchFunction"); // dafx-MyTestWorkflow");
HttpResponseData response = req.CreateResponse(HttpStatusCode.OK);
await response.WriteStringAsync($"InvokeWorkflowOrechstrtationAsync is invoked for {workflowName}.{instanceId}");
@@ -240,6 +232,26 @@ internal static class BuiltInFunctions
return agentResponse.Text;
}
+#pragma warning disable DURTASK001 // Durable analyzer complained
+ public static Task> WorkflowRunnerOrchestrationAsync(TaskOrchestrationContext context, DuableWorkflowRunRequest input)
+ {
+ ArgumentNullException.ThrowIfNull(context);
+
+ ILogger logger = context.CreateReplaySafeLogger("BuiltInFunctions");
+ logger.LogInformation("WorkflowRunnerOrchestrationAsync function called.");
+
+ FunctionContext? functionContext = context.GetFunctionContext();
+ if (functionContext == null)
+ {
+ throw new InvalidOperationException("FunctionContext is not available in the orchestration context.");
+ }
+
+ var workFlowName = input.WorkflowName;
+
+ return Task.FromResult(new List() { workFlowName });
+ }
+#pragma warning restore DURTASK001
+
///
/// Creates an error response with the specified status code and error message.
///
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DuableWorkflowRunRequest.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DuableWorkflowRunRequest.cs
new file mode 100644
index 0000000000..325fed1af5
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DuableWorkflowRunRequest.cs
@@ -0,0 +1,23 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json.Serialization;
+
+namespace Microsoft.Agents.AI.Hosting.AzureFunctions;
+
+///
+/// Represents a request to run a workflow in the Duable system.
+///
+public sealed class DuableWorkflowRunRequest
+{
+ ///
+ /// Gets or sets the name of the workflow.
+ ///
+ [JsonPropertyName("workflowName")]
+ public string WorkflowName { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the input string to be processed or analyzed.
+ ///
+ [JsonPropertyName("input")]
+ public string Input { get; set; } = string.Empty;
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableOptions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableOptions.cs
new file mode 100644
index 0000000000..71f6b741ff
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableOptions.cs
@@ -0,0 +1,29 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.Agents.AI.DurableTask;
+
+namespace Microsoft.Agents.AI.Hosting.AzureFunctions;
+
+///
+/// Provides configuration options for durable features in Azure Functions.
+///
+public sealed class DurableOptions
+{
+ ///
+ /// Gets the configuration options for durable agents.
+ ///
+ public DurableAgentsOptions Agents { get; } = new();
+
+ ///
+ /// Gets the configuration options for durable workflows.
+ ///
+ public DurableWorkflowOptions Workflows { get; }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ internal DurableOptions()
+ {
+ this.Workflows = new DurableWorkflowOptions(this);
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableOptionsExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableOptionsExtensions.cs
new file mode 100644
index 0000000000..4a4b9b157c
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableOptionsExtensions.cs
@@ -0,0 +1,97 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.Agents.AI.DurableTask;
+using Microsoft.Azure.Functions.Worker;
+using Microsoft.Azure.Functions.Worker.Builder;
+using Microsoft.Azure.Functions.Worker.Core.FunctionMetadata;
+using Microsoft.DurableTask;
+using Microsoft.DurableTask.Worker;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.Hosting;
+
+namespace Microsoft.Agents.AI.Hosting.AzureFunctions;
+
+///
+/// Extension methods for configuring durable options (agents and workflows).
+///
+public static class DurableOptionsExtensions
+{
+ ///
+ /// Configures durable agents and workflows in a unified way.
+ ///
+ /// The Functions application builder.
+ /// A delegate to configure the durable options.
+ /// The Functions application builder for method chaining.
+ ///
+ /// This method provides a unified configuration point for both durable agents and workflows.
+ /// Agents configured here will be automatically registered when referenced in workflows.
+ ///
+ public static FunctionsApplicationBuilder ConfigureDurableOptions(
+ this FunctionsApplicationBuilder builder,
+ Action configure)
+ {
+ ArgumentNullException.ThrowIfNull(configure);
+
+ DurableOptions options = new();
+ configure(options);
+
+ // Register the unified DurableOptions for components that need both agents and workflows
+ builder.Services.AddSingleton(options);
+
+ // Register AgentsOption for backward compatibility. Can be removed if needed, after syncing with team.
+ builder.Services.AddSingleton(options.Agents);
+
+ // Configure agents using the existing infrastructure
+ builder.Services.ConfigureDurableAgents(agentOpts =>
+ {
+ // Copy agent registrations from the unified options to the service-level options
+ foreach (KeyValuePair> agentFactory in options.Agents.GetAgentFactories())
+ {
+ agentOpts.AddAIAgentFactory(
+ agentFactory.Key,
+ agentFactory.Value,
+ options.Agents.GetTimeToLive(agentFactory.Key));
+ }
+
+ // Copy TTL settings
+ agentOpts.DefaultTimeToLive = options.Agents.DefaultTimeToLive;
+ agentOpts.MinimumTimeToLiveSignalDelay = options.Agents.MinimumTimeToLiveSignalDelay;
+ });
+
+ builder.Services.TryAddSingleton(_ =>
+ new DefaultFunctionsAgentOptionsProvider(DurableAgentsOptionsExtensions.GetAgentOptionsSnapshot()));
+
+ builder.Services.AddSingleton();
+
+ // Handling of built-in function execution for Agent HTTP, MCP tool, or Entity invocations.
+ builder.UseWhen(static context =>
+ string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.RunAgentHttpFunctionEntryPoint, StringComparison.Ordinal) ||
+ string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.RunAgentMcpToolFunctionEntryPoint, StringComparison.Ordinal) ||
+ string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.RunWorkflowOrechstrtationFunctionEntryPoint, StringComparison.Ordinal) ||
+ string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.RunWorkflowOrechstrtationHttpFunctionEntryPoint, StringComparison.Ordinal) ||
+ string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.InvokeWorkflowActivityFunctionEntryPoint, StringComparison.Ordinal) ||
+ string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.RunAgentEntityFunctionEntryPoint, StringComparison.Ordinal));
+ builder.Services.AddSingleton();
+
+ builder.Services.AddSingleton();
+
+ builder.ConfigureDurableWorker().AddTasks(t => t.AddOrchestratorFunc>(
+ "WorkflowRunnerOrchestration",
+ async (tc, inputBindingData) =>
+ {
+ FunctionContext? functionContext = tc.GetFunctionContext();
+ if (functionContext == null)
+ {
+ throw new InvalidOperationException("FunctionContext is not available in the orchestration context.");
+ }
+
+ DurableWorkflowRunner runner = functionContext.InstanceServices.GetRequiredService();
+ return await runner.RunWorkflowOrchestrationAsync(tc, inputBindingData).ConfigureAwait(false);
+ }));
+
+ builder.Services.AddSingleton();
+
+ return builder;
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableWorkflowFunctionMetadataTransformer.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableWorkflowFunctionMetadataTransformer.cs
index 3b4e541eec..5d02405ea5 100644
--- a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableWorkflowFunctionMetadataTransformer.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableWorkflowFunctionMetadataTransformer.cs
@@ -12,10 +12,11 @@ internal sealed class DurableWorkflowFunctionMetadataTransformer : IFunctionMeta
private readonly ILogger _logger;
private readonly DurableWorkflowOptions _options;
- public DurableWorkflowFunctionMetadataTransformer(ILogger logger, DurableWorkflowOptions durableWorkflowOptions)
+ public DurableWorkflowFunctionMetadataTransformer(ILogger logger, DurableOptions durableOptions)
{
this._logger = logger ?? throw new ArgumentNullException(nameof(logger));
- this._options = durableWorkflowOptions ?? throw new ArgumentNullException(nameof(durableWorkflowOptions));
+ ArgumentNullException.ThrowIfNull(durableOptions);
+ this._options = durableOptions.Workflows;
}
public string Name => nameof(DurableWorkflowFunctionMetadataTransformer);
@@ -28,7 +29,7 @@ internal sealed class DurableWorkflowFunctionMetadataTransformer : IFunctionMeta
{
this._logger.LogAddingWorkflowFunction(workflow.Key);
- original.Add(CreateOrchestrationTrigger(workflow.Key));
+ //original.Add(CreateOrchestrationTrigger(workflow.Key));
// We also want to create an HTTP trigger for this orchestration so users can start it via HTTP.
this._logger.LogAddingHttpTrigger(workflow.Key);
@@ -57,7 +58,8 @@ internal sealed class DurableWorkflowFunctionMetadataTransformer : IFunctionMeta
{
if (executorInfos.TryGetValue(executorId, out ExecutorInfo? executorInfo))
{
- string functionName = $"{AgentSessionId.ToEntityName(workflow.Key)}_{executorId}";
+ // string functionName = $"{AgentSessionId.ToEntityName(workflow.Key)}-{executorId}"; //.Split("_")[0]
+ string functionName = $"{AgentSessionId.ToEntityName(workflow.Key)}-{executorId.Split("_")[0]}"; //.Split("_")[0]
// Check if the executor type is an agent-related type
if (IsAgentExecutorType(executorInfo.ExecutorType))
@@ -106,22 +108,22 @@ internal sealed class DurableWorkflowFunctionMetadataTransformer : IFunctionMeta
};
}
- private static DefaultFunctionMetadata CreateOrchestrationTrigger(string name)
- {
- return new DefaultFunctionMetadata()
- {
- Name = AgentSessionId.ToEntityName(name),
- Language = "dotnet-isolated",
- RawBindings =
- [
- // """{"name":"context","type":"orchestrationTrigger","direction":"In"}""",
- """{"name":"taskOrchestrationContext","type":"orchestrationTrigger","direction":"In"}""",
+ //private static DefaultFunctionMetadata CreateOrchestrationTrigger(string name)
+ //{
+ // return new DefaultFunctionMetadata()
+ // {
+ // Name = AgentSessionId.ToEntityName(name),
+ // Language = "dotnet-isolated",
+ // RawBindings =
+ // [
+ // // """{"name":"context","type":"orchestrationTrigger","direction":"In"}""",
+ // """{"name":"taskOrchestrationContext","type":"orchestrationTrigger","direction":"In"}""",
- ],
- EntryPoint = BuiltInFunctions.RunWorkflowOrechstrtationFunctionEntryPoint,
- ScriptFile = BuiltInFunctions.ScriptFile,
- };
- }
+ // ],
+ // EntryPoint = BuiltInFunctions.RunWorkflowOrechstrtationFunctionEntryPoint,
+ // ScriptFile = BuiltInFunctions.ScriptFile,
+ // };
+ //}
private static DefaultFunctionMetadata CreateActivityTrigger(string functionName)
{
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableWorkflowRunner.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableWorkflowRunner.cs
index 5a8c80c33d..43e4483ef7 100644
--- a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableWorkflowRunner.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableWorkflowRunner.cs
@@ -2,6 +2,8 @@
using Microsoft.Agents.AI.DurableTask;
using Microsoft.Agents.AI.Workflows;
+using Microsoft.Agents.AI.Workflows.Checkpointing;
+using Microsoft.Azure.Functions.Worker;
using Microsoft.DurableTask;
using Microsoft.Extensions.Logging;
@@ -11,40 +13,149 @@ internal sealed class DurableWorkflowRunner
{
private readonly DurableWorkflowOptions _options;
private readonly ILogger _logger;
- public DurableWorkflowRunner(ILogger logger, DurableWorkflowOptions durableWorkflowOptions)
+
+ public DurableWorkflowRunner(ILogger logger, DurableOptions durableOptions)
{
this._logger = logger;
- this._options = durableWorkflowOptions;
+ ArgumentNullException.ThrowIfNull(durableOptions);
+ this._options = durableOptions.Workflows;
}
- internal async Task RunAsync(
- TaskOrchestrationContext taskOrchestrationContext,
- string workflowName,
- object? initialInput = null,
- CancellationToken cancellationToken = default)
+ internal async Task> RunWorkflowOrchestrationAsync(TaskOrchestrationContext context, DuableWorkflowRunRequest input)
{
+ ArgumentNullException.ThrowIfNull(context);
+ ArgumentNullException.ThrowIfNull(input);
+
+ string workflowName = input.WorkflowName;
+
this._logger.LogAttemptingToRunWorkflow(workflowName);
- if (this._options.Workflows.TryGetValue(workflowName, out Workflow? wf))
- {
- this._logger.LogRunningWorkflow(wf.Name);
-
- await this.RunExecutorsInWorkFlowAsync(taskOrchestrationContext, wf, initialInput, cancellationToken).ConfigureAwait(false);
- }
- else
+ if (!this._options.Workflows.TryGetValue(workflowName, out Workflow? wf))
{
throw new InvalidOperationException($"Workflow '{workflowName}' not found.");
}
+
+ this._logger.LogRunningWorkflow(wf.Name);
+
+ var result = await this.RunExecutorsInWorkFlowAsync(context, wf, input.Input).ConfigureAwait(false);
+
+ return [result];
}
- private Task RunExecutorsInWorkFlowAsync(
+ private async Task RunExecutorsInWorkFlowAsync(
TaskOrchestrationContext taskOrchestrationContext,
- Workflow wf,
- object? initialInput,
- CancellationToken cancellationToken)
+ Workflow workflow,
+ string initialInput)
{
- // Extract edeges and executors from the workflow and execute them in order/based on the pattern as Durable entities.
+ List executorResult = [];
- return Task.CompletedTask;
+ if (this._logger.IsEnabled(LogLevel.Information))
+ {
+ foreach (WorkflowExecutorInfo executorInfo in WorkflowHelper.GetExecutorsFromWorkflowInOrder(workflow))
+ {
+ string triggerName = this.BuildTriggerName(workflow.Name!, executorInfo.ExecutorId);
+ this._logger.LogInformation(
+ " Scheduling executor '{ExecutorId}' (IsAgentic: {IsAgentic}) with trigger name '{TriggerName}'",
+ executorInfo.ExecutorId,
+ executorInfo.IsAgenticExecutor,
+ triggerName);
+
+ string input = executorResult.Count == 0 ? initialInput : executorResult.Last()!;
+ if (!executorInfo.IsAgenticExecutor)
+ {
+ var result = await taskOrchestrationContext.CallActivityAsync(triggerName, input);
+ executorResult.Add(result);
+ }
+ else
+ {
+ string AgentName = this.GetAgentNameFromExecutorId(workflow.Name!, executorInfo.ExecutorId);
+ this._logger.LogInformation(
+ " Invoking agentic executor '{ExecutorId}'",
+ AgentName);
+ DurableAIAgent agent = taskOrchestrationContext.GetAgent(AgentName);
+ if (agent != null)
+ {
+ AgentThread destinationThread = agent.GetNewThread();
+ var agentResponse = await agent.RunAsync(input, destinationThread);
+ executorResult.Add(agentResponse.Text);
+ this._logger.LogInformation(
+ "Agentic executor '{ExecutorId}' completed with response: {AgentResponse}",
+ AgentName,
+ agentResponse);
+ }
+ }
+ }
+
+ // return final result
+ return executorResult.Last();
+ }
+
+ return string.Empty;
+ }
+
+ private string GetAgentNameFromExecutorId(string workflowName, string executorId)
+ {
+ // Example: "InspirationBot_edaac621050849efb1a62805fa03d3f8"
+ var parts = executorId.Split('_');
+ return parts.Length > 0 ? parts[0] : executorId;
+ }
+
+ private string BuildTriggerName(string workflowName, string executorName)
+ {
+ return $"dafx-{workflowName}-{executorName}";
+ }
+
+ internal async Task ExecuteActivityAsync(string activityFunctionName, string input, FunctionContext functionContext)
+ {
+ ArgumentNullException.ThrowIfNull(activityFunctionName);
+ ArgumentNullException.ThrowIfNull(input);
+ ArgumentNullException.ThrowIfNull(functionContext);
+
+ // Parse the activity function name to extract workflow name and executor name
+ // Format: "dafx-{workflowName}-{executorName}"
+ if (!activityFunctionName.StartsWith("dafx-", StringComparison.Ordinal))
+ {
+ throw new InvalidOperationException($"Activity function name '{activityFunctionName}' does not start with 'dafx-' prefix.");
+ }
+
+ string nameWithoutPrefix = activityFunctionName["dafx-".Length..];
+ string[] parts = nameWithoutPrefix.Split('-', 2);
+
+ if (parts.Length != 2)
+ {
+ throw new InvalidOperationException($"Activity function name '{activityFunctionName}' is not in the expected format 'dafx-{{workflowName}}-{{executorName}}'.");
+ }
+
+ string workflowName = parts[0];
+ string executorName = parts[1];
+
+ this._logger.LogAttemptingToExecuteActivity(workflowName, executorName);
+
+ // Get the workflow
+ if (!this._options.Workflows.TryGetValue(workflowName, out Workflow? workflow))
+ {
+ throw new InvalidOperationException($"Workflow '{workflowName}' not found.");
+ }
+
+ // Get the executor info
+ Dictionary executorInfos = workflow.ReflectExecutors();
+
+ // Find the executor by matching the executor name (which may have a GUID suffix)
+ KeyValuePair executorPair = executorInfos.FirstOrDefault(e =>
+ e.Key.StartsWith(executorName + "_", StringComparison.Ordinal) ||
+ string.Equals(e.Key, executorName, StringComparison.Ordinal));
+
+ if (executorPair.Key == null)
+ {
+ throw new InvalidOperationException($"Executor '{executorName}' not found in workflow '{workflowName}'.");
+ }
+
+ this._logger.LogExecutingActivity(executorPair.Key, executorPair.Value.ExecutorType.TypeName);
+
+ const string result = "Many types are internal.";
+
+ this._logger.LogActivityExecuted(executorPair.Key, result);
+
+ return result;
}
}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableWorkflowRunnerLogs.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableWorkflowRunnerLogs.cs
index fbec81f77d..037d76f170 100644
--- a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableWorkflowRunnerLogs.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableWorkflowRunnerLogs.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Microsoft. All rights reserved.
+// Copyright (c) Microsoft. All rights reserved.
using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.Logging;
@@ -20,4 +20,19 @@ internal static partial class DurableWorkflowRunnerLogs
Level = LogLevel.Information,
Message = "Running workflow: {WorkflowName}")]
public static partial void LogRunningWorkflow(this ILogger logger, string? workflowName);
+
+ [LoggerMessage(
+ Level = LogLevel.Information,
+ Message = "Attempting to execute activity in workflow '{WorkflowName}' for executor '{ExecutorName}'")]
+ public static partial void LogAttemptingToExecuteActivity(this ILogger logger, string workflowName, string executorName);
+
+ [LoggerMessage(
+ Level = LogLevel.Information,
+ Message = "Executing activity for executor '{ExecutorId}' of type '{ExecutorType}'")]
+ public static partial void LogExecutingActivity(this ILogger logger, string executorId, string executorType);
+
+ [LoggerMessage(
+ Level = LogLevel.Information,
+ Message = "Activity executed for executor '{ExecutorId}' with result: {Result}")]
+ public static partial void LogActivityExecuted(this ILogger logger, string executorId, string result);
}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/FunctionsApplicationBuilderExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/FunctionsApplicationBuilderExtensions.cs
index fbae490782..a6458873df 100644
--- a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/FunctionsApplicationBuilderExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/FunctionsApplicationBuilderExtensions.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Microsoft. All rights reserved.
+// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI.DurableTask;
using Microsoft.Azure.Functions.Worker.Builder;
@@ -14,23 +14,6 @@ namespace Microsoft.Agents.AI.Hosting.AzureFunctions;
///
public static class FunctionsApplicationBuilderExtensions
{
- ///
- /// Adds support for durable workflows to the specified Functions application builder.
- ///
- /// The Functions application builder to configure with durable workflow capabilities.
- ///
- /// The same instance of to allow for method chaining.
- public static FunctionsApplicationBuilder AddDurableWorkflows(this FunctionsApplicationBuilder builder, Action configure)
- {
- var options = new DurableWorkflowOptions();
- configure(options);
- builder.Services.AddSingleton(options);
- builder.Services.AddSingleton();
- builder.Services.AddSingleton();
-
- return builder;
- }
-
///
/// Configures the application to use durable agents with a builder pattern.
///
@@ -57,6 +40,7 @@ public static class FunctionsApplicationBuilderExtensions
string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.RunAgentMcpToolFunctionEntryPoint, StringComparison.Ordinal) ||
string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.RunWorkflowOrechstrtationFunctionEntryPoint, StringComparison.Ordinal) ||
string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.RunWorkflowOrechstrtationHttpFunctionEntryPoint, StringComparison.Ordinal) ||
+ string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.InvokeWorkflowActivityFunctionEntryPoint, StringComparison.Ordinal) ||
string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.RunAgentEntityFunctionEntryPoint, StringComparison.Ordinal));
builder.Services.AddSingleton();
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/Microsoft.Agents.AI.Hosting.AzureFunctions.csproj b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/Microsoft.Agents.AI.Hosting.AzureFunctions.csproj
index ce67c9621e..ad044b6a71 100644
--- a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/Microsoft.Agents.AI.Hosting.AzureFunctions.csproj
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/Microsoft.Agents.AI.Hosting.AzureFunctions.csproj
@@ -4,7 +4,7 @@
$(TargetFrameworksCore)
enable
- $(NoWarn);CA2007
+ $(NoWarn);CA2007;AD0001
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/WorkflowHelper.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/WorkflowHelper.cs
new file mode 100644
index 0000000000..0fa3e946af
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/WorkflowHelper.cs
@@ -0,0 +1,139 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.Agents.AI.Workflows;
+using Microsoft.Agents.AI.Workflows.Checkpointing;
+
+namespace Microsoft.Agents.AI.Hosting.AzureFunctions;
+
+///
+/// Represents an executor in the workflow with its metadata.
+///
+/// The unique identifier of the executor.
+/// Indicates whether this executor is an agentic executor.
+internal sealed record WorkflowExecutorInfo(string ExecutorId, bool IsAgenticExecutor);
+
+internal static class WorkflowHelper
+{
+ ///
+ /// Accepts a workflow instance and returns a list of executors with metadata in the order they should be executed.
+ ///
+ /// The workflow instance to analyze.
+ /// A list of executor information in topological order (execution order).
+ public static List GetExecutorsFromWorkflowInOrder(Workflow workflow)
+ {
+ ArgumentNullException.ThrowIfNull(workflow);
+
+ Dictionary executors = workflow.ReflectExecutors();
+ Dictionary> edges = workflow.ReflectEdges();
+
+ // Build adjacency list and in-degree map
+ Dictionary> adjacencyList = new();
+ Dictionary inDegree = new();
+
+ // Initialize all executors with in-degree 0
+ foreach (string executorId in executors.Keys)
+ {
+ adjacencyList[executorId] = new List();
+ inDegree[executorId] = 0;
+ }
+
+ // Build the graph from edges
+ foreach (KeyValuePair> edgeGroup in edges)
+ {
+ string sourceId = edgeGroup.Key;
+
+ foreach (EdgeInfo edge in edgeGroup.Value)
+ {
+ // For each sink (target) in this edge
+ foreach (string sinkId in edge.Connection.SinkIds)
+ {
+ // Add edge from source to sink
+ adjacencyList[sourceId].Add(sinkId);
+
+ // Increment in-degree of the sink
+ if (inDegree.TryGetValue(sinkId, out int currentDegree))
+ {
+ inDegree[sinkId] = currentDegree + 1;
+ }
+ }
+ }
+ }
+
+ // Perform topological sort using Kahn's algorithm
+ List orderedExecutorIds = new();
+ Queue queue = new();
+
+ // Start with the workflow's starting executor
+ queue.Enqueue(workflow.StartExecutorId);
+
+ // Also add any other executors with in-degree 0 (shouldn't be any if workflow is well-formed)
+ foreach (KeyValuePair kvp in inDegree)
+ {
+ if (kvp.Value == 0 && kvp.Key != workflow.StartExecutorId)
+ {
+ queue.Enqueue(kvp.Key);
+ }
+ }
+
+ while (queue.Count > 0)
+ {
+ string current = queue.Dequeue();
+ orderedExecutorIds.Add(current);
+
+ // For each neighbor of the current executor
+ foreach (string neighbor in adjacencyList[current])
+ {
+ inDegree[neighbor]--;
+
+ // If in-degree becomes 0, add to queue
+ if (inDegree[neighbor] == 0)
+ {
+ queue.Enqueue(neighbor);
+ }
+ }
+ }
+
+ // If result doesn't contain all executors, there might be a cycle or disconnected components
+ if (orderedExecutorIds.Count != executors.Count)
+ {
+ // Add any remaining executors that weren't reached
+ foreach (string executorId in executors.Keys)
+ {
+ if (!orderedExecutorIds.Contains(executorId))
+ {
+ orderedExecutorIds.Add(executorId);
+ }
+ }
+ }
+
+ // Convert to WorkflowExecutorInfo with agentic executor detection
+ List result = new();
+ foreach (string executorId in orderedExecutorIds)
+ {
+ if (executors.TryGetValue(executorId, out ExecutorInfo? executorInfo))
+ {
+ bool isAgentic = IsAgentExecutorType(executorInfo.ExecutorType);
+ result.Add(new WorkflowExecutorInfo(executorId, isAgentic));
+ }
+ }
+
+ return result;
+ }
+
+ ///
+ /// Determines whether the specified executor type is an agentic executor.
+ ///
+ /// The executor type to check.
+ /// true if the executor is an agentic executor; otherwise, false.
+ private static bool IsAgentExecutorType(TypeId executorType)
+ {
+ // hack for now. In the future, the MAF type could expose something which can help with this.
+ // Check if the type name or assembly indicates it's an agent executor
+ // This includes AgentRunStreamingExecutor, AgentExecutor, ChatClientAgent wrappers, etc.
+ string typeName = executorType.TypeName;
+ string assemblyName = executorType.AssemblyName;
+
+ return typeName.Contains("AIAgentHostExecutor", StringComparison.OrdinalIgnoreCase) &&
+ assemblyName.Contains("Microsoft.Agents.AI", StringComparison.OrdinalIgnoreCase);
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/Workflow.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/Workflow.cs
index e41f2a642a..47009f8d9d 100644
--- a/dotnet/src/Microsoft.Agents.AI.Workflows/Workflow.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows/Workflow.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Microsoft. All rights reserved.
+// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
@@ -204,4 +204,24 @@ public class Workflow
.ConfigureAwait(false);
return startExecutor.DescribeProtocol();
}
+
+ ///
+ /// Enumerates all AIAgent instances that are directly bound in this workflow.
+ ///
+ ///
+ /// This method only returns agents that are directly bound as executor values.
+ /// It does not include agents created by factory functions, as those require
+ /// an IServiceProvider to instantiate.
+ ///
+ /// An enumerable collection of AIAgent instances found in the workflow.
+ public IEnumerable EnumerateAgentExecutors()
+ {
+ foreach (ExecutorBinding binding in this.ExecutorBindings.Values)
+ {
+ if (binding.RawValue is AIAgent agent)
+ {
+ yield return agent;
+ }
+ }
+ }
}