Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
8ed8310
Update package versions in `Directory.Build.props` and `Directory.Pac…
sfmskywalker Dec 10, 2025
c9253c9
Initial plan
Copilot Dec 11, 2025
5bbd847
Add Microsoft Agent Framework integration with code-first support
Copilot Dec 11, 2025
7e699df
Add comprehensive documentation and examples for Agent Framework feat…
Copilot Dec 11, 2025
163a17f
Add unit tests and fix null reference issues in ConfigurationKernelCo…
Copilot Dec 11, 2025
e03d1bf
Address code review feedback - improve error handling and remove unus…
Copilot Dec 11, 2025
1d616cb
Add migration guide for Agent Framework features
Copilot Dec 11, 2025
fb4f4eb
Refactor package versioning and temporarily comment out scoped activi…
sfmskywalker Dec 12, 2025
5409790
Add `Elsa.Agents.Tests` project and refactor `TestWorkflowDefinition`…
sfmskywalker Dec 12, 2025
b0705a7
Remove legacy KernelFactory and implement improved Agent Framework su…
sfmskywalker Dec 12, 2025
814cf6d
Remove SK-based agent workflows and introduce code-first agents
sfmskywalker Dec 12, 2025
d86dbd7
Refactor agent activities and improve dependency injection
sfmskywalker Dec 13, 2025
97a3299
Refactor agent workflow infrastructure to simplify providers.
sfmskywalker Dec 13, 2025
5089bc1
Refactor agents framework and replace deprecated agent implementation
sfmskywalker Dec 13, 2025
0269fae
Replace "plugins" with "skills" throughout agents framework
sfmskywalker Dec 13, 2025
b020eb8
Remove legacy API key and service management functionality.
sfmskywalker Dec 13, 2025
89f3e1b
Remove ApiKeyDefinition and ServiceDefinition tables.
sfmskywalker Dec 13, 2025
add31db
Add Azure OpenAI integration to Elsa Agents
sfmskywalker Dec 13, 2025
b4d7a24
Refactor agent activity providers and registry to improve naming cons…
sfmskywalker Dec 14, 2025
fe8d3a1
Improve naming consistency in documentation for agent activity provid…
sfmskywalker Dec 14, 2025
b8521cf
Refactor runAsynchronously logic and remove TaskActivityAttribute
sfmskywalker Dec 14, 2025
5db7bfb
Introduce `IAgentFactory` and `IAgentInvoker` interfaces to decouple …
sfmskywalker Dec 14, 2025
4725ce5
Refactor agents framework to unify configuration and improve invocati…
sfmskywalker Dec 14, 2025
f4c7864
Remove obsolete agent test suite and update solution references.
sfmskywalker Dec 14, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@
<PackageVersion Include="Microsoft.Identity.Client" Version="4.79.2"/>
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.1"/>
<PackageVersion Include="Microsoft.SemanticKernel" Version="1.68.0" />
<PackageVersion Include="Microsoft.SemanticKernel.Agents.Abstractions" Version="1.68.0" />
<PackageVersion Include="Microsoft.SemanticKernel.Agents.Core" Version="1.68.0" />
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.InMemory" Version="1.58.0-preview" />
<PackageVersion Include="Microsoft.SemanticKernel.PromptTemplates.Handlebars" Version="1.68.0" />
<PackageVersion Include="Microsoft.SemanticKernel.Plugins.Memory" Version="1.58.0-alpha" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
using System.ComponentModel;
using System.Dynamic;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Unicode;
using Elsa.Agents;
using Elsa.Expressions.Helpers;
using Elsa.Extensions;
using Elsa.Agents.Activities.ActivityProviders;
using Elsa.Workflows;
using Elsa.Workflows.Models;
using Elsa.Workflows.Serialization.Converters;

namespace Elsa.Agents.Activities;

/// <summary>
/// An activity that executes a multi-agent workflow. This is an internal activity used by <see cref="AgentActivityProvider"/>.
/// </summary>
[Browsable(false)]
public class AgentWorkflowActivity : CodeActivity
{
private static JsonSerializerOptions? _serializerOptions;

private static JsonSerializerOptions SerializerOptions =>
_serializerOptions ??= new JsonSerializerOptions
{
Encoder = JavaScriptEncoder.Create(UnicodeRanges.All),
PropertyNameCaseInsensitive = true
}.WithConverters(new ExpandoObjectConverterFactory());

[JsonIgnore] internal string WorkflowName { get; set; } = null!;

/// <inheritdoc />
protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
{
var activityDescriptor = context.ActivityDescriptor;
var inputDescriptors = activityDescriptor.Inputs;
var workflowInput = new Dictionary<string, object?>();

foreach (var inputDescriptor in inputDescriptors)
{
var input = (Input?)inputDescriptor.ValueGetter(this);
var inputValue = input != null ? context.Get(input.MemoryBlockReference()) : null;

if (inputValue is ExpandoObject expandoObject)
{
inputValue = expandoObject.ConvertTo<string>();
}

workflowInput[inputDescriptor.Name] = inputValue;
}

var kernelConfigProvider = context.GetRequiredService<IKernelConfigProvider>();
var workflowExecutor = context.GetRequiredService<AgentWorkflowExecutor>();

var kernelConfig = await kernelConfigProvider.GetKernelConfigAsync(context.CancellationToken);

if (!kernelConfig.AgentWorkflows.TryGetValue(WorkflowName, out var workflowConfig))
throw new InvalidOperationException($"Agent workflow '{WorkflowName}' not found");

var result = await workflowExecutor.ExecuteWorkflowAsync(WorkflowName, workflowConfig, workflowInput, context.CancellationToken);
var json = result.Output?.Trim();

if (string.IsNullOrWhiteSpace(json))
throw new InvalidOperationException("The workflow output is empty or null.");

var outputs = context.ActivityDescriptor.Outputs;
if (outputs.Count != 1)
throw new InvalidOperationException($"Expected exactly one output, but found {outputs.Count}");

var outputType = outputs.First().Type;

// If the target type is object, we want the JSON to be deserialized into an ExpandoObject for dynamic field access.
if (outputType == typeof(object))
outputType = typeof(ExpandoObject);

var converterOptions = new ObjectConverterOptions(SerializerOptions);
var outputValue = json.ConvertTo(outputType, converterOptions);
var outputDescriptor = outputs.First();
var output = outputDescriptor.ValueGetter(this) as Output;

if (output == null)
throw new InvalidOperationException("Output descriptor did not return a valid Output object");

context.Set(output, outputValue);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,71 +23,148 @@ IWellKnownTypeRegistry wellKnownTypeRegistry
public async ValueTask<IEnumerable<ActivityDescriptor>> GetDescriptorsAsync(CancellationToken cancellationToken = default)
{
var kernelConfig = await kernelConfigProvider.GetKernelConfigAsync(cancellationToken);
var agents = kernelConfig.Agents;
var activityDescriptors = new List<ActivityDescriptor>();

foreach (var kvp in agents)
// Add descriptors for individual agents
foreach (var kvp in kernelConfig.Agents)
{
var agentConfig = kvp.Value;
var activityDescriptor = await activityDescriber.DescribeActivityAsync(typeof(AgentActivity), cancellationToken);
var activityTypeName = $"Elsa.Agents.{agentConfig.Name.Pascalize()}";
activityDescriptor.Name = agentConfig.Name.Pascalize();
activityDescriptor.TypeName = activityTypeName;
activityDescriptor.Description = agentConfig.Description;
activityDescriptor.DisplayName = agentConfig.Name.Humanize().Transform(To.TitleCase);
activityDescriptor.IsBrowsable = true;
activityDescriptor.Category = "Agents";
activityDescriptor.Kind = ActivityKind.Task;
activityDescriptor.CustomProperties["RootType"] = nameof(AgentActivity);

activityDescriptor.Constructor = context =>
var descriptor = await CreateAgentActivityDescriptor(agentConfig, cancellationToken);
activityDescriptors.Add(descriptor);
}

// Add descriptors for agent workflows
foreach (var kvp in kernelConfig.AgentWorkflows)
{
var workflowConfig = kvp.Value;
var descriptor = await CreateAgentWorkflowActivityDescriptor(workflowConfig, cancellationToken);
activityDescriptors.Add(descriptor);
}

return activityDescriptors;
}

private async Task<ActivityDescriptor> CreateAgentActivityDescriptor(AgentConfig agentConfig, CancellationToken cancellationToken)
{
var activityDescriptor = await activityDescriber.DescribeActivityAsync(typeof(AgentActivity), cancellationToken);
var activityTypeName = $"Elsa.Agents.{agentConfig.Name.Pascalize()}";
activityDescriptor.Name = agentConfig.Name.Pascalize();
activityDescriptor.TypeName = activityTypeName;
activityDescriptor.Description = agentConfig.Description;
activityDescriptor.DisplayName = agentConfig.Name.Humanize().Transform(To.TitleCase);
activityDescriptor.IsBrowsable = true;
activityDescriptor.Category = "Agents";
activityDescriptor.Kind = ActivityKind.Task;
activityDescriptor.CustomProperties["RootType"] = nameof(AgentActivity);

activityDescriptor.Constructor = context =>
{
var activity = context.CreateActivity<AgentActivity>();
activity.Type = activityTypeName;
activity.AgentName = agentConfig.Name;
return activity;
};

activityDescriptor.Inputs.Clear();

foreach (var inputVariable in agentConfig.InputVariables)
{
var inputName = inputVariable.Name;
var inputType = inputVariable.Type == null! ? "object" : inputVariable.Type;
var nakedInputType = wellKnownTypeRegistry.GetTypeOrDefault(inputType);
var inputDescriptor = new InputDescriptor
{
var activity = context.CreateActivity<AgentActivity>();
activity.Type = activityTypeName;
activity.AgentName = agentConfig.Name;
return activity;
Name = inputVariable.Name,
DisplayName = inputVariable.Name.Humanize(),
Description = inputVariable.Description,
Type = nakedInputType,
ValueGetter = activity => activity.SyntheticProperties.GetValueOrDefault(inputName),
ValueSetter = (activity, value) => activity.SyntheticProperties[inputName] = value!,
IsSynthetic = true,
IsWrapped = true,
UIHint = ActivityDescriber.GetUIHint(nakedInputType)
};
activityDescriptor.Inputs.Add(inputDescriptor);
}

activityDescriptors.Add(activityDescriptor);
activityDescriptor.Inputs.Clear();
activityDescriptor.Outputs.Clear();
var outputVariable = agentConfig.OutputVariable;
var outputType = outputVariable.Type == null! ? "object" : outputVariable.Type;
var nakedOutputType = wellKnownTypeRegistry.GetTypeOrDefault(outputType);
var outputName = "Output";
var outputDescriptor = new OutputDescriptor
{
Name = outputName,
Description = agentConfig.OutputVariable.Description,
Type = nakedOutputType,
IsSynthetic = true,
ValueGetter = activity => activity.SyntheticProperties.GetValueOrDefault(outputName),
ValueSetter = (activity, value) => activity.SyntheticProperties[outputName] = value!,
};
activityDescriptor.Outputs.Add(outputDescriptor);

foreach (var inputVariable in agentConfig.InputVariables)
{
var inputName = inputVariable.Name;
var inputType = inputVariable.Type == null! ? "object" : inputVariable.Type;
var nakedInputType = wellKnownTypeRegistry.GetTypeOrDefault(inputType);
var inputDescriptor = new InputDescriptor
{
Name = inputVariable.Name,
DisplayName = inputVariable.Name.Humanize(),
Description = inputVariable.Description,
Type = nakedInputType,
ValueGetter = activity => activity.SyntheticProperties.GetValueOrDefault(inputName),
ValueSetter = (activity, value) => activity.SyntheticProperties[inputName] = value!,
IsSynthetic = true,
IsWrapped = true,
UIHint = ActivityDescriber.GetUIHint(nakedInputType)
};
activityDescriptor.Inputs.Add(inputDescriptor);
}

activityDescriptor.Outputs.Clear();
var outputVariable = agentConfig.OutputVariable;
var outputType = outputVariable.Type == null! ? "object" : outputVariable.Type;
var nakedOutputType = wellKnownTypeRegistry.GetTypeOrDefault(outputType);
var outputName = "Output";
var outputDescriptor = new OutputDescriptor
return activityDescriptor;
}

private async Task<ActivityDescriptor> CreateAgentWorkflowActivityDescriptor(AgentWorkflowConfig workflowConfig, CancellationToken cancellationToken)
{
var activityDescriptor = await activityDescriber.DescribeActivityAsync(typeof(AgentWorkflowActivity), cancellationToken);
var activityTypeName = $"Elsa.Agents.Workflows.{workflowConfig.Name.Pascalize()}";
activityDescriptor.Name = workflowConfig.Name.Pascalize();
activityDescriptor.TypeName = activityTypeName;
activityDescriptor.Description = workflowConfig.Description;
activityDescriptor.DisplayName = workflowConfig.Name.Humanize().Transform(To.TitleCase);
activityDescriptor.IsBrowsable = true;
activityDescriptor.Category = "Agent Workflows";
activityDescriptor.Kind = ActivityKind.Task;
activityDescriptor.CustomProperties["RootType"] = nameof(AgentWorkflowActivity);

activityDescriptor.Constructor = context =>
{
var activity = context.CreateActivity<AgentWorkflowActivity>();
activity.Type = activityTypeName;
activity.WorkflowName = workflowConfig.Name;
return activity;
};

activityDescriptor.Inputs.Clear();

foreach (var inputVariable in workflowConfig.InputVariables)
{
var inputName = inputVariable.Name;
var inputType = inputVariable.Type == null! ? "object" : inputVariable.Type;
Comment thread
sfmskywalker marked this conversation as resolved.
Outdated
var nakedInputType = wellKnownTypeRegistry.GetTypeOrDefault(inputType);
var inputDescriptor = new InputDescriptor
{
Name = outputName,
Description = agentConfig.OutputVariable.Description,
Type = nakedOutputType,
Name = inputVariable.Name,
DisplayName = inputVariable.Name.Humanize(),
Description = inputVariable.Description,
Type = nakedInputType,
ValueGetter = activity => activity.SyntheticProperties.GetValueOrDefault(inputName),
ValueSetter = (activity, value) => activity.SyntheticProperties[inputName] = value!,
IsSynthetic = true,
ValueGetter = activity => activity.SyntheticProperties.GetValueOrDefault(outputName),
ValueSetter = (activity, value) => activity.SyntheticProperties[outputName] = value!,
IsWrapped = true,
UIHint = ActivityDescriber.GetUIHint(nakedInputType)
};
activityDescriptor.Outputs.Add(outputDescriptor);
activityDescriptor.Inputs.Add(inputDescriptor);
}

return activityDescriptors;
activityDescriptor.Outputs.Clear();
var outputVariable = workflowConfig.OutputVariable;
var outputType = outputVariable.Type == null! ? "object" : outputVariable.Type;
Comment thread
sfmskywalker marked this conversation as resolved.
Outdated
var nakedOutputType = wellKnownTypeRegistry.GetTypeOrDefault(outputType);
var outputName = "Output";
var outputDescriptor = new OutputDescriptor
{
Name = outputName,
Description = workflowConfig.OutputVariable.Description,
Type = nakedOutputType,
IsSynthetic = true,
ValueGetter = activity => activity.SyntheticProperties.GetValueOrDefault(outputName),
ValueSetter = (activity, value) => activity.SyntheticProperties[outputName] = value!,
};
activityDescriptor.Outputs.Add(outputDescriptor);

return activityDescriptor;
}
}
22 changes: 22 additions & 0 deletions src/modules/agents/Elsa.Agents.Core/Contracts/IAgentDefinition.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
namespace Elsa.Agents;

/// <summary>
/// Represents a code-first agent definition that can be registered programmatically.
/// </summary>
public interface IAgentDefinition
{
/// <summary>
/// Gets the unique name of the agent.
/// </summary>
string Name { get; }

/// <summary>
/// Gets the description of the agent.
/// </summary>
string Description { get; }

/// <summary>
/// Gets the agent configuration.
/// </summary>
AgentConfig GetAgentConfig();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace Elsa.Agents;

/// <summary>
/// Provides access to registered agent definitions.
/// </summary>
public interface IAgentDefinitionProvider
{
/// <summary>
/// Gets all registered agent definitions.
/// </summary>
IEnumerable<IAgentDefinition> GetDefinitions();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
namespace Elsa.Agents;

/// <summary>
/// Represents a code-first multi-agent workflow (agent team/sequence/graph) that can be registered programmatically.
/// </summary>
public interface IAgentWorkflowDefinition
{
/// <summary>
/// Gets the unique name of the agent workflow.
/// </summary>
string Name { get; }

/// <summary>
/// Gets the description of the agent workflow.
/// </summary>
string Description { get; }

/// <summary>
/// Gets the agent workflow configuration.
/// </summary>
AgentWorkflowConfig GetWorkflowConfig();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace Elsa.Agents;

/// <summary>
/// Provides access to registered agent workflow definitions.
/// </summary>
public interface IAgentWorkflowDefinitionProvider
{
/// <summary>
/// Gets all registered agent workflow definitions.
/// </summary>
IEnumerable<IAgentWorkflowDefinition> GetDefinitions();
}
2 changes: 2 additions & 0 deletions src/modules/agents/Elsa.Agents.Core/Elsa.Agents.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
<PackageReference Include="Microsoft.Extensions.Logging" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" />
<PackageReference Include="Microsoft.SemanticKernel" />
<PackageReference Include="Microsoft.SemanticKernel.Agents.Abstractions" />
<PackageReference Include="Microsoft.SemanticKernel.Agents.Core" />
<PackageReference Include="Microsoft.SemanticKernel.PromptTemplates.Handlebars" />
<PackageReference Include="Microsoft.SemanticKernel.Plugins.Memory" />
<PackageReference Include="Microsoft.SemanticKernel.Connectors.InMemory" />
Expand Down
Loading
Loading