diff --git a/dotnet/samples/03-workflows/Declarative/InvokeFoundryToolboxMcp/Program.cs b/dotnet/samples/03-workflows/Declarative/InvokeFoundryToolboxMcp/Program.cs
index 7c3ecbc7e96..d6d9396fd4c 100644
--- a/dotnet/samples/03-workflows/Declarative/InvokeFoundryToolboxMcp/Program.cs
+++ b/dotnet/samples/03-workflows/Declarative/InvokeFoundryToolboxMcp/Program.cs
@@ -100,6 +100,7 @@ public static async Task Main(string[] args)
WorkflowFactory workflowFactory = new("InvokeFoundryToolboxMcp.yaml", foundryEndpoint)
{
Configuration = workflowConfiguration,
+ AllowedEnvironmentVariables = [ToolboxMcpServerUrlSetting, DocsServerLabelSetting, WebSearchToolNameSetting],
McpToolHandler = mcpToolHandler
};
diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative/AgentBotElementYaml.cs b/dotnet/src/Microsoft.Agents.AI.Declarative/AgentBotElementYaml.cs
index 89eacdf8fa9..4e62030ccd8 100644
--- a/dotnet/src/Microsoft.Agents.AI.Declarative/AgentBotElementYaml.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Declarative/AgentBotElementYaml.cs
@@ -1,11 +1,14 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
+using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using Microsoft.Agents.ObjectModel;
using Microsoft.Agents.ObjectModel.Abstractions;
+using Microsoft.Agents.ObjectModel.Analysis;
+using Microsoft.Agents.ObjectModel.PowerFx;
using Microsoft.Agents.ObjectModel.Yaml;
using Microsoft.Extensions.Configuration;
using Microsoft.Shared.Diagnostics;
@@ -22,8 +25,9 @@ internal static class AgentBotElementYaml
///
/// YAML representation of the to use to create the prompt function.
/// Optional instance which provides environment variables to the template.
+ /// Configuration keys that may be exposed when the YAML references them through Env.
[RequiresDynamicCode("Calls YamlDotNet.Serialization.DeserializerBuilder.DeserializerBuilder()")]
- public static GptComponentMetadata FromYaml(string text, IConfiguration? configuration = null)
+ public static GptComponentMetadata FromYaml(string text, IConfiguration? configuration = null, IEnumerable? allowedConfigurationVariables = null)
{
Throw.IfNullOrEmpty(text);
@@ -35,7 +39,7 @@ public static GptComponentMetadata FromYaml(string text, IConfiguration? configu
throw new InvalidDataException($"Unsupported root element: {rootElement.GetType().Name}. Expected an {nameof(GptComponentMetadata)}.");
}
- var botDefinition = WrapPromptAgentWithBot(promptAgent, configuration);
+ var botDefinition = WrapPromptAgentWithBot(promptAgent, configuration, allowedConfigurationVariables);
return botDefinition.Descendants().OfType().First();
}
@@ -52,7 +56,7 @@ private sealed class AgentFeatureConfiguration : IFeatureConfiguration
public bool IsTenantFeatureEnabled(string featureName, bool defaultValue) => defaultValue;
}
- public static BotDefinition WrapPromptAgentWithBot(this GptComponentMetadata element, IConfiguration? configuration = null)
+ public static BotDefinition WrapPromptAgentWithBot(this GptComponentMetadata element, IConfiguration? configuration = null, IEnumerable? allowedConfigurationVariables = null)
{
var botBuilder =
new BotDefinition.Builder
@@ -67,19 +71,26 @@ public static BotDefinition WrapPromptAgentWithBot(this GptComponentMetadata ele
}
};
- if (configuration is not null)
+ if (configuration is not null && allowedConfigurationVariables is not null)
{
- foreach (var kvp in configuration.AsEnumerable().Where(kvp => kvp.Value is not null))
+ HashSet allowedVariables = new(allowedConfigurationVariables, StringComparer.OrdinalIgnoreCase);
+ foreach (string variableName in GetReferencedEnvironmentVariableNames(element).Where(allowedVariables.Contains))
{
+ string? configurationValue = configuration[variableName];
+ if (configurationValue is null)
+ {
+ continue;
+ }
+
botBuilder.EnvironmentVariables.Add(new EnvironmentVariableDefinition.Builder()
{
- SchemaName = kvp.Key,
+ SchemaName = variableName,
Id = Guid.NewGuid(),
- DisplayName = kvp.Key,
+ DisplayName = variableName,
ValueComponent = new EnvironmentVariableValue.Builder()
{
Id = Guid.NewGuid(),
- Value = kvp.Value!,
+ Value = configurationValue,
},
});
}
@@ -87,5 +98,13 @@ public static BotDefinition WrapPromptAgentWithBot(this GptComponentMetadata ele
return botBuilder.Build();
}
+
+ internal static ISet GetReferencedEnvironmentVariableNames(GptComponentMetadata element)
+ {
+ var botDefinition = WrapPromptAgentWithBot(element);
+ SemanticModel semanticModel = botDefinition.GetSemanticModel(new PowerFxExpressionChecker(new AgentFeatureConfiguration()), new AgentFeatureConfiguration());
+
+ return semanticModel.GetAllEnvironmentVariablesReferencedInTheBot();
+ }
#endregion
}
diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative/ChatClient/ChatClientPromptAgentFactory.cs b/dotnet/src/Microsoft.Agents.AI.Declarative/ChatClient/ChatClientPromptAgentFactory.cs
index 28f0c47fbb6..2be060b1318 100644
--- a/dotnet/src/Microsoft.Agents.AI.Declarative/ChatClient/ChatClientPromptAgentFactory.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Declarative/ChatClient/ChatClientPromptAgentFactory.cs
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
+using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.ObjectModel;
@@ -20,13 +21,57 @@ public sealed class ChatClientPromptAgentFactory : PromptAgentFactory
///
/// Creates a new instance of the class.
///
- public ChatClientPromptAgentFactory(IChatClient chatClient, IList? functions = null, RecalcEngine? engine = null, IConfiguration? configuration = null, ILoggerFactory? loggerFactory = null) : base(engine, configuration)
+ /// The chat client used by created agents.
+ /// Optional functions exposed as tools to created agents.
+ /// Optional Power Fx engine used to evaluate declarative expressions.
+ /// Optional configuration used to resolve explicitly allowed environment variables referenced by the agent definition.
+ /// Optional logger factory used by created agents.
+ public ChatClientPromptAgentFactory(
+ IChatClient chatClient,
+ IList? functions = null,
+ RecalcEngine? engine = null,
+ IConfiguration? configuration = null,
+ ILoggerFactory? loggerFactory = null)
+ : this(
+ chatClient,
+ functions,
+ new ChatClientPromptAgentFactoryOptions()
+ {
+ Engine = engine,
+ Configuration = configuration,
+ AllowedConfigurationVariables = configuration?.AsEnumerable().Select(static pair => pair.Key),
+ LoggerFactory = loggerFactory,
+ },
+ isValidated: true)
{
+ }
+
+ ///
+ /// Creates a new instance of the class.
+ ///
+ /// The chat client used by created agents.
+ /// Options used to configure the created agents and declarative expression evaluation.
+ /// Optional functions exposed as tools to created agents.
+ /// The configured instance.
+ public static ChatClientPromptAgentFactory Create(
+ IChatClient chatClient,
+ ChatClientPromptAgentFactoryOptions options,
+ IList? functions = null) =>
+ new(chatClient, functions, ValidateOptions(options), isValidated: true);
+
+ private ChatClientPromptAgentFactory(
+ IChatClient chatClient,
+ IList? functions,
+ ChatClientPromptAgentFactoryOptions options,
+ bool isValidated) :
+ base(options.Engine, options.Configuration, options.AllowedConfigurationVariables, options.MaximumExpressionLength, options.MaximumCallDepth)
+ {
+ _ = isValidated;
Throw.IfNull(chatClient);
this._chatClient = chatClient;
this._functions = functions;
- this._loggerFactory = loggerFactory;
+ this._loggerFactory = options.LoggerFactory;
}
///
@@ -34,6 +79,8 @@ public ChatClientPromptAgentFactory(IChatClient chatClient, IList? f
{
Throw.IfNull(promptAgent);
+ this.InitializeConfigurationVariables(promptAgent);
+
var options = new ChatClientAgentOptions()
{
Name = promptAgent.Name,
@@ -51,5 +98,44 @@ public ChatClientPromptAgentFactory(IChatClient chatClient, IList? f
private readonly IChatClient _chatClient;
private readonly IList? _functions;
private readonly ILoggerFactory? _loggerFactory;
+
+ private static ChatClientPromptAgentFactoryOptions ValidateOptions(ChatClientPromptAgentFactoryOptions? options) =>
+ Throw.IfNull(options);
#endregion
}
+
+///
+/// Options for configuring .
+///
+public sealed class ChatClientPromptAgentFactoryOptions
+{
+ ///
+ /// Gets or sets configuration keys that may be exposed to Power Fx when the agent definition references them through Env.
+ ///
+ public IEnumerable? AllowedConfigurationVariables { get; init; }
+
+ ///
+ /// Gets or sets an optional Power Fx engine used to evaluate declarative expressions.
+ ///
+ public RecalcEngine? Engine { get; init; }
+
+ ///
+ /// Gets or sets optional configuration used to resolve explicitly allowed environment variables referenced by the agent definition.
+ ///
+ public IConfiguration? Configuration { get; init; }
+
+ ///
+ /// Gets or sets an optional logger factory used by created agents.
+ ///
+ public ILoggerFactory? LoggerFactory { get; init; }
+
+ ///
+ /// Gets or sets an optional maximum length for Power Fx expressions evaluated by the factory-created engine.
+ ///
+ public int? MaximumExpressionLength { get; init; }
+
+ ///
+ /// Gets or sets an optional maximum nested call depth for Power Fx expressions evaluated by the factory-created engine.
+ ///
+ public int? MaximumCallDepth { get; init; }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/YamlAgentFactoryExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/YamlAgentFactoryExtensions.cs
index 1cc24055d90..b7a0e2da26a 100644
--- a/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/YamlAgentFactoryExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/YamlAgentFactoryExtensions.cs
@@ -24,7 +24,7 @@ public static Task CreateFromYamlAsync(this PromptAgentFactory agentFac
Throw.IfNull(agentFactory);
Throw.IfNullOrEmpty(agentYaml);
- var agentDefinition = AgentBotElementYaml.FromYaml(agentYaml);
+ var agentDefinition = agentFactory.FromYaml(agentYaml);
return agentFactory.CreateAsync(
agentDefinition,
diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative/PromptAgentFactory.cs b/dotnet/src/Microsoft.Agents.AI.Declarative/PromptAgentFactory.cs
index 22d55178ba0..d16424c4411 100644
--- a/dotnet/src/Microsoft.Agents.AI.Declarative/PromptAgentFactory.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Declarative/PromptAgentFactory.cs
@@ -1,6 +1,9 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.ObjectModel;
@@ -15,30 +18,83 @@ namespace Microsoft.Agents.AI;
///
public abstract class PromptAgentFactory
{
+ private const int DefaultMaximumExpressionLength = 10000;
+
+ private readonly IConfiguration? _configuration;
+ private readonly HashSet _allowedConfigurationVariables;
+
///
/// Initializes a new instance of the class.
///
/// Optional , if none is provided a default instance will be created.
- /// Optional configuration to be added as variables to the .
+ /// Optional configuration used to resolve explicitly allowed environment variables referenced by the agent definition.
protected PromptAgentFactory(RecalcEngine? engine = null, IConfiguration? configuration = null)
+ : this(engine, configuration, allowedConfigurationVariables: null)
{
- this.Engine = engine ?? new RecalcEngine();
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Optional , if none is provided a default instance will be created.
+ /// Optional configuration used to resolve explicitly allowed environment variables referenced by the agent definition.
+ /// Configuration keys that may be exposed to Power Fx when the agent definition references them through Env.
+ /// Optional maximum length for Power Fx expressions evaluated by the factory-created engine.
+ /// Optional maximum nested call depth for Power Fx expressions evaluated by the factory-created engine.
+ protected PromptAgentFactory(
+ RecalcEngine? engine,
+ IConfiguration? configuration,
+ IEnumerable? allowedConfigurationVariables,
+ int? maximumExpressionLength = null,
+ int? maximumCallDepth = null)
+ {
+ this.Engine = engine ?? new RecalcEngine(CreateConfig(maximumExpressionLength, maximumCallDepth));
+ this._configuration = configuration;
+ this._allowedConfigurationVariables = new(allowedConfigurationVariables ?? [], StringComparer.OrdinalIgnoreCase);
+ }
+
+ private static PowerFxConfig CreateConfig(int? maximumExpressionLength, int? maximumCallDepth)
+ {
+ PowerFxConfig config = new(Features.PowerFxV1)
+ {
+ MaximumExpressionLength = maximumExpressionLength ?? DefaultMaximumExpressionLength,
+ };
- if (configuration is not null)
+ if (maximumCallDepth is not null)
{
- foreach (var kvp in configuration.AsEnumerable())
- {
- this.Engine.UpdateVariable(kvp.Key, kvp.Value ?? string.Empty);
- }
+ config.MaxCallDepth = maximumCallDepth.Value;
}
+
+ return config;
}
///
/// Gets the Power Fx recalculation engine used to evaluate expressions in agent definitions.
- /// This engine is configured with variables from the provided during construction.
+ /// This engine is configured with only explicitly allowed variables from the provided during construction.
///
protected RecalcEngine Engine { get; }
+ ///
+ /// Adds allowed configuration values referenced through Env by the agent definition to the Power Fx engine.
+ ///
+ /// Definition of the agent to inspect.
+ protected void InitializeConfigurationVariables(GptComponentMetadata promptAgent)
+ {
+ if (this._configuration is null || this._allowedConfigurationVariables.Count == 0)
+ {
+ return;
+ }
+
+ foreach (string variableName in AgentBotElementYaml.GetReferencedEnvironmentVariableNames(promptAgent).Where(variableName => this._allowedConfigurationVariables.Contains(variableName)))
+ {
+ this.Engine.UpdateVariable(variableName, this._configuration[variableName] ?? string.Empty);
+ }
+ }
+
+ [RequiresDynamicCode("Calls YamlDotNet.Serialization.DeserializerBuilder.DeserializerBuilder()")]
+ internal GptComponentMetadata FromYaml(string text) =>
+ AgentBotElementYaml.FromYaml(text, this._configuration, this._allowedConfigurationVariables);
+
///
/// Create a from the specified .
///
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/DeclarativeWorkflowBuilder.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/DeclarativeWorkflowBuilder.cs
index 054aa38b237..e12370a53ca 100644
--- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/DeclarativeWorkflowBuilder.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/DeclarativeWorkflowBuilder.cs
@@ -73,7 +73,11 @@ public static Workflow Build(
string rootId = WorkflowActionVisitor.Steps.Root(workflowElement);
WorkflowFormulaState state = new(options.CreateRecalcEngine());
- state.Initialize(workflowElement.WrapWithBot(), options.Configuration);
+ state.Initialize(
+ workflowElement.WrapWithBot(),
+ options.Configuration,
+ options.AllowedEnvironmentVariables,
+ options.AllowProcessEnvironmentVariableFallback);
state.CaptureInitialState();
DeclarativeWorkflowExecutor rootExecutor =
new(rootId,
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/DeclarativeWorkflowOptions.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/DeclarativeWorkflowOptions.cs
index 90439402dbd..a41196de29c 100644
--- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/DeclarativeWorkflowOptions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/DeclarativeWorkflowOptions.cs
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
+using System.Collections.Generic;
using System.Diagnostics;
using Microsoft.Agents.AI.Workflows.Observability;
using Microsoft.Extensions.Configuration;
@@ -37,6 +38,16 @@ public sealed class DeclarativeWorkflowOptions(ResponseAgentProvider agentProvid
///
public IConfiguration? Configuration { get; init; }
+ ///
+ /// Gets the configuration or process environment variable names that may be exposed through the workflow Env scope.
+ ///
+ public IEnumerable? AllowedEnvironmentVariables { get; init; }
+
+ ///
+ /// Gets a value indicating whether the workflow may fall back to process environment variables for allowed Env names missing from .
+ ///
+ public bool AllowProcessEnvironmentVariableFallback { get; init; }
+
///
/// Optionally identifies a continued workflow conversation.
///
@@ -52,6 +63,11 @@ public sealed class DeclarativeWorkflowOptions(ResponseAgentProvider agentProvid
///
public int? MaximumExpressionLength { get; init; }
+ ///
+ /// Gets a value indicating whether the Power Fx Set function is enabled.
+ ///
+ public bool EnableSetFunction { get; init; }
+
///
/// Gets the used to create loggers for workflow components.
///
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Extensions/DeclarativeWorkflowOptionsExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Extensions/DeclarativeWorkflowOptionsExtensions.cs
index 1e1c52ab887..b4ce4587c2b 100644
--- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Extensions/DeclarativeWorkflowOptionsExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Extensions/DeclarativeWorkflowOptionsExtensions.cs
@@ -10,5 +10,5 @@ internal static class DeclarativeWorkflowOptionsExtensions
private const int DefaultMaximumExpressionLength = 10000;
public static RecalcEngine CreateRecalcEngine(this DeclarativeWorkflowOptions? context) =>
- RecalcEngineFactory.Create(context?.MaximumExpressionLength ?? DefaultMaximumExpressionLength, context?.MaximumCallDepth);
+ RecalcEngineFactory.Create(context?.MaximumExpressionLength ?? DefaultMaximumExpressionLength, context?.MaximumCallDepth, context?.EnableSetFunction ?? false);
}
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Extensions/IWorkflowContextExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Extensions/IWorkflowContextExtensions.cs
index 1b92235eee3..f7b2bfce695 100644
--- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Extensions/IWorkflowContextExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Extensions/IWorkflowContextExtensions.cs
@@ -38,17 +38,38 @@ public static ValueTask QueueStateResetAsync(this IWorkflowContext context, Prop
public static ValueTask QueueStateUpdateAsync(this IWorkflowContext context, PropertyPath variablePath, TValue? value, CancellationToken cancellationToken = default) =>
context.QueueStateUpdateAsync(Throw.IfNull(variablePath.VariableName), value, Throw.IfNull(variablePath.NamespaceAlias), cancellationToken);
+ public static ValueTask QueueStateUpdateAsync(
+ this IWorkflowContext context,
+ PropertyPath variablePath,
+ TValue? value,
+ SensitivityLevel sensitivity,
+ CancellationToken cancellationToken = default)
+ {
+ string variableName = Throw.IfNull(variablePath.VariableName);
+ string namespaceAlias = Throw.IfNull(variablePath.NamespaceAlias);
+
+ return context is DeclarativeWorkflowContext declarativeContext
+ ? declarativeContext.QueueStateUpdateAsync(variableName, value, namespaceAlias, sensitivity, cancellationToken)
+ : context.QueueStateUpdateAsync(variableName, value, namespaceAlias, cancellationToken);
+ }
+
public static async ValueTask QueueEnvironmentUpdateAsync(this IWorkflowContext context, string key, TValue? value, CancellationToken cancellationToken = default)
{
DeclarativeWorkflowContext declarativeContext = DeclarativeContext(context);
- await declarativeContext.UpdateStateAsync(key, value, VariableScopeNames.Environment, allowSystem: true, cancellationToken).ConfigureAwait(false);
+ await declarativeContext.UpdateStateAsync(
+ key,
+ value,
+ VariableScopeNames.Environment,
+ allowSystem: true,
+ sensitivity: SensitivityLevel.Sensitive,
+ cancellationToken: cancellationToken).ConfigureAwait(false);
declarativeContext.State.Bind();
}
public static async ValueTask QueueSystemUpdateAsync(this IWorkflowContext context, string key, TValue? value, CancellationToken cancellationToken = default)
{
DeclarativeWorkflowContext declarativeContext = DeclarativeContext(context);
- await declarativeContext.UpdateStateAsync(key, value, VariableScopeNames.System, allowSystem: true, cancellationToken).ConfigureAwait(false);
+ await declarativeContext.UpdateStateAsync(key, value, VariableScopeNames.System, allowSystem: true, cancellationToken: cancellationToken).ConfigureAwait(false);
declarativeContext.State.Bind();
}
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Interpreter/DeclarativeActionExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Interpreter/DeclarativeActionExecutor.cs
index 5d052c64d3d..597ea831296 100644
--- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Interpreter/DeclarativeActionExecutor.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Interpreter/DeclarativeActionExecutor.cs
@@ -24,8 +24,6 @@ internal abstract class DeclarativeActionExecutor(TAction model, Workfl
internal abstract class DeclarativeActionExecutor : Executor, IResettableExecutor, IModeledAction
{
- private readonly WorkflowFormulaState _state;
-
protected DeclarativeActionExecutor(DialogAction model, WorkflowFormulaState state)
: base(model.Id.Value)
{
@@ -34,7 +32,7 @@ protected DeclarativeActionExecutor(DialogAction model, WorkflowFormulaState sta
throw new DeclarativeModelException($"Missing required properties for element: {model.GetId()} ({model.GetType().Name}).");
}
- this._state = state;
+ this.State = state;
this.Model = model;
}
@@ -51,9 +49,11 @@ protected override ProtocolBuilder ConfigureProtocol(ProtocolBuilder protocolBui
public string ParentId { get => field ??= this.Model.GetParentId() ?? WorkflowActionVisitor.Steps.Root(); }
- public RecalcEngine Engine => this._state.Engine;
+ public RecalcEngine Engine => this.State.Engine;
+
+ public WorkflowExpressionEngine Evaluator => this.State.Evaluator;
- public WorkflowExpressionEngine Evaluator => this._state.Evaluator;
+ protected WorkflowFormulaState State { get; }
internal ILogger Logger { get; set; } = NullLogger.Instance;
@@ -64,7 +64,7 @@ protected override ProtocolBuilder ConfigureProtocol(ProtocolBuilder protocolBui
///
public virtual ValueTask ResetAsync()
{
- this._state.Reset();
+ this.State.Reset();
return default;
}
@@ -89,7 +89,7 @@ public override async ValueTask HandleAsync(ActionExecutorResult message, IWorkf
try
{
- object? result = await this.ExecuteAsync(new DeclarativeWorkflowContext(context, this._state), cancellationToken).ConfigureAwait(false);
+ object? result = await this.ExecuteAsync(new DeclarativeWorkflowContext(context, this.State), cancellationToken).ConfigureAwait(false);
Debug.WriteLine($"RESULT #{this.Id} - {result ?? "(null)"}");
if (this.EmitResultEvent)
@@ -123,19 +123,21 @@ public override async ValueTask HandleAsync(ActionExecutorResult message, IWorkf
/// This must be overridden to restore any state that was saved during checkpointing.
///
protected override ValueTask OnCheckpointRestoredAsync(IWorkflowContext context, CancellationToken cancellationToken = default) =>
- this._state.RestoreAsync(context, cancellationToken);
+ this.State.RestoreAsync(context, cancellationToken);
- protected async ValueTask AssignAsync(PropertyPath? targetPath, FormulaValue result, IWorkflowContext context)
+ protected async ValueTask AssignAsync(PropertyPath? targetPath, FormulaValue result, IWorkflowContext context, SensitivityLevel sensitivity = SensitivityLevel.None)
{
if (targetPath is null)
{
return;
}
- await context.QueueStateUpdateAsync(targetPath, result).ConfigureAwait(false);
+ await context.QueueStateUpdateAsync(targetPath, result, sensitivity).ConfigureAwait(false);
+ string variableName = targetPath.VariableName ?? throw new DeclarativeActionException($"Invalid variable reference: '{targetPath}'.");
+ this.State.SetSensitivity(variableName, targetPath.NamespaceAlias, sensitivity);
#if DEBUG
- string? resultValue = result.Format();
+ string? resultValue = sensitivity == SensitivityLevel.Sensitive ? "" : result.Format();
string valuePosition = (resultValue?.IndexOf('\n') ?? -1) >= 0 ? Environment.NewLine : " ";
Debug.WriteLine(
$"""
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Interpreter/DeclarativeWorkflowContext.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Interpreter/DeclarativeWorkflowContext.cs
index 6616aa5d00c..183b2e785e7 100644
--- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Interpreter/DeclarativeWorkflowContext.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Interpreter/DeclarativeWorkflowContext.cs
@@ -58,7 +58,7 @@ public async ValueTask QueueClearScopeAsync(string? scopeName = null, Cancellati
// Copy keys to array to avoid modifying collection during enumeration.
foreach (string key in this.State.Keys(scopeName).ToArray())
{
- await this.UpdateStateAsync(key, UnassignedValue.Instance, scopeName, allowSystem: false, cancellationToken).ConfigureAwait(false);
+ await this.UpdateStateAsync(key, UnassignedValue.Instance, scopeName, allowSystem: false, cancellationToken: cancellationToken).ConfigureAwait(false);
}
}
else
@@ -73,7 +73,18 @@ public async ValueTask QueueClearScopeAsync(string? scopeName = null, Cancellati
///
public async ValueTask QueueStateUpdateAsync(string key, T? value, string? scopeName = null, CancellationToken cancellationToken = default)
{
- await this.UpdateStateAsync(key, value, scopeName, allowSystem: false, cancellationToken).ConfigureAwait(false);
+ await this.UpdateStateAsync(key, value, scopeName, allowSystem: false, cancellationToken: cancellationToken).ConfigureAwait(false);
+ this.State.Bind();
+ }
+
+ internal async ValueTask QueueStateUpdateAsync(
+ string key,
+ T? value,
+ string? scopeName,
+ SensitivityLevel sensitivity,
+ CancellationToken cancellationToken = default)
+ {
+ await this.UpdateStateAsync(key, value, scopeName, allowSystem: false, sensitivity: sensitivity, cancellationToken: cancellationToken).ConfigureAwait(false);
this.State.Bind();
}
@@ -137,7 +148,13 @@ public ValueTask> ReadStateKeysAsync(string? scopeName = null, C
public ValueTask SendMessageAsync(object message, string? targetId = null, CancellationToken cancellationToken = default)
=> this.Source.SendMessageAsync(message, targetId, cancellationToken);
- public ValueTask UpdateStateAsync(string key, T? value, string? scopeName, bool allowSystem, CancellationToken cancellationToken = default)
+ public ValueTask UpdateStateAsync(
+ string key,
+ T? value,
+ string? scopeName,
+ bool allowSystem,
+ SensitivityLevel sensitivity = SensitivityLevel.None,
+ CancellationToken cancellationToken = default)
{
bool isManagedScope =
scopeName is not null && // null scope cannot be managed
@@ -165,47 +182,61 @@ scopeName is not null && // null scope cannot be managed
_ => QueueNativeStateAsync(value),
};
- ValueTask QueueEmptyStateAsync()
+ async ValueTask QueueEmptyStateAsync()
{
if (isManagedScope)
{
- this.State.Set(key, FormulaValue.NewBlank(), scopeName);
+ this.State.Set(key, FormulaValue.NewBlank(), scopeName, sensitivity);
}
- return this.Source.QueueStateUpdateAsync(key, UnassignedValue.Instance, scopeName, cancellationToken);
+ await this.Source.QueueStateUpdateAsync(key, UnassignedValue.Instance, scopeName, cancellationToken).ConfigureAwait(false);
+ await this.QueueSensitivityUpdateAsync(key, scopeName, sensitivity, cancellationToken).ConfigureAwait(false);
}
- ValueTask QueueFormulaStateAsync(FormulaValue formulaValue)
+ async ValueTask QueueFormulaStateAsync(FormulaValue formulaValue)
{
if (isManagedScope)
{
- this.State.Set(key, formulaValue, scopeName);
+ this.State.Set(key, formulaValue, scopeName, sensitivity);
}
- return this.Source.QueueStateUpdateAsync(key, formulaValue.AsPortable(), scopeName, cancellationToken);
+ await this.Source.QueueStateUpdateAsync(key, formulaValue.AsPortable(), scopeName, cancellationToken).ConfigureAwait(false);
+ await this.QueueSensitivityUpdateAsync(key, scopeName, sensitivity, cancellationToken).ConfigureAwait(false);
}
- ValueTask QueueDataValueStateAsync(DataValue dataValue)
+ async ValueTask QueueDataValueStateAsync(DataValue dataValue)
{
FormulaValue formulaValue = dataValue.ToFormula();
if (isManagedScope)
{
- this.State.Set(key, formulaValue, scopeName);
+ this.State.Set(key, formulaValue, scopeName, sensitivity);
}
- return this.Source.QueueStateUpdateAsync(key, formulaValue.AsPortable(), scopeName, cancellationToken);
+ await this.Source.QueueStateUpdateAsync(key, formulaValue.AsPortable(), scopeName, cancellationToken).ConfigureAwait(false);
+ await this.QueueSensitivityUpdateAsync(key, scopeName, sensitivity, cancellationToken).ConfigureAwait(false);
}
- ValueTask QueueNativeStateAsync(object rawValue)
+ async ValueTask QueueNativeStateAsync(object rawValue)
{
FormulaValue formulaValue = rawValue.ToFormula();
if (isManagedScope)
{
- this.State.Set(key, formulaValue, scopeName);
+ this.State.Set(key, formulaValue, scopeName, sensitivity);
}
- return this.Source.QueueStateUpdateAsync(key, formulaValue.AsPortable(), scopeName, cancellationToken);
+ await this.Source.QueueStateUpdateAsync(key, formulaValue.AsPortable(), scopeName, cancellationToken).ConfigureAwait(false);
+ await this.QueueSensitivityUpdateAsync(key, scopeName, sensitivity, cancellationToken).ConfigureAwait(false);
+ }
+ }
+
+ private ValueTask QueueSensitivityUpdateAsync(string key, string? scopeName, SensitivityLevel sensitivity, CancellationToken cancellationToken)
+ {
+ if (scopeName is null || (!ManagedScopes.Contains(scopeName) && scopeName != VariableScopeNames.Environment))
+ {
+ return default;
}
+
+ return this.Source.QueueStateUpdateAsync(key, sensitivity, WorkflowFormulaState.GetSensitivityScopeName(scopeName), cancellationToken);
}
}
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Kit/IWorkflowContextExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Kit/IWorkflowContextExtensions.cs
index 69cca12fafd..93beb091ab2 100644
--- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Kit/IWorkflowContextExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Kit/IWorkflowContextExtensions.cs
@@ -58,7 +58,9 @@ public static async ValueTask FormatTemplateAsync(this IWorkflowContext
StringBuilder builder = new();
foreach (string line in lines)
{
- builder.AppendLine(state.Engine.Format(TemplateLine.Parse(line)));
+ EvaluationResult result = state.Evaluator.Format(TemplateLine.Parse(line));
+ ThrowIfSensitive(result.Sensitivity);
+ builder.AppendLine(result.Value);
}
return builder.ToString();
@@ -86,6 +88,7 @@ public static async ValueTask FormatTemplateAsync(this IWorkflowContext
WorkflowFormulaState state = await context.GetStateAsync(cancellationToken).ConfigureAwait(false);
EvaluationResult result = state.Evaluator.GetValue(ValueExpression.Expression(expression));
+ ThrowIfSensitive(result.Sensitivity);
return (TValue?)result.Value.ToObject();
}
@@ -103,10 +106,75 @@ public static async ValueTask FormatTemplateAsync(this IWorkflowContext
WorkflowFormulaState state = await context.GetStateAsync(cancellationToken).ConfigureAwait(false);
EvaluationResult result = state.Evaluator.GetValue(ValueExpression.Expression(expression));
+ ThrowIfSensitive(result.Sensitivity);
return result.Value.AsList();
}
+ ///
+ /// Reads a state value together with its sensitivity metadata.
+ ///
+ /// The type of the state value.
+ /// The workflow execution context used to read state.
+ /// The key of the state value.
+ /// An optional name that specifies the scope to read. If null, the default scope is used.
+ /// A token that propagates notification when operation should be canceled.
+ /// The state value and its sensitivity metadata.
+ public static async ValueTask> ReadStateWithSensitivityAsync(
+ this IWorkflowContext context,
+ string key,
+ string? scopeName = null,
+ CancellationToken cancellationToken = default)
+ {
+ if (context is DeclarativeWorkflowContext declarativeContext)
+ {
+ string effectiveScopeName = scopeName ?? WorkflowFormulaState.DefaultScopeName;
+ TValue? declarativeValue = await context.ReadStateAsync(key, effectiveScopeName, cancellationToken).ConfigureAwait(false);
+ SensitivityLevel declarativeSensitivity = declarativeContext.State.GetSensitivity(key, effectiveScopeName);
+ return new(declarativeValue, declarativeSensitivity);
+ }
+
+ string plainScopeName = scopeName ?? WorkflowFormulaState.DefaultScopeName;
+ TValue? value = await context.ReadStateAsync(key, scopeName, cancellationToken).ConfigureAwait(false);
+ SensitivityLevel sensitivity = ShouldPersistSensitivity(plainScopeName)
+ ? await context.ReadStateAsync(key, WorkflowFormulaState.GetSensitivityScopeName(plainScopeName), cancellationToken).ConfigureAwait(false)
+ : SensitivityLevel.None;
+ return new(value, sensitivity);
+ }
+
+ ///
+ /// Queues a state update using sensitivity metadata carried with the value.
+ ///
+ /// The type of the state value.
+ /// The workflow execution context used to queue state updates.
+ /// The key of the state value.
+ /// The value and sensitivity metadata to store.
+ /// An optional name that specifies the scope to update. If null, the default scope is used.
+ /// A token that propagates notification when operation should be canceled.
+ /// A task representing the queued state update.
+ public static async ValueTask QueueStateUpdateWithSensitivityAsync(
+ this IWorkflowContext context,
+ string key,
+ EvaluationResult value,
+ string? scopeName = null,
+ CancellationToken cancellationToken = default)
+ {
+ if (context is DeclarativeWorkflowContext declarativeContext)
+ {
+ string effectiveScopeName = scopeName ?? WorkflowFormulaState.DefaultScopeName;
+ await declarativeContext.QueueStateUpdateAsync(key, value.Value, effectiveScopeName, value.Sensitivity, cancellationToken).ConfigureAwait(false);
+ return;
+ }
+
+ await context.QueueStateUpdateAsync(key, value.Value, scopeName, cancellationToken).ConfigureAwait(false);
+
+ string plainScopeName = scopeName ?? WorkflowFormulaState.DefaultScopeName;
+ if (ShouldPersistSensitivity(plainScopeName))
+ {
+ await context.QueueStateUpdateAsync(key, value.Sensitivity, WorkflowFormulaState.GetSensitivityScopeName(plainScopeName), cancellationToken).ConfigureAwait(false);
+ }
+ }
+
///
/// Convert the result of an expression to the specified target type.
///
@@ -127,7 +195,7 @@ public static async ValueTask FormatTemplateAsync(this IWorkflowContext
/// The workflow execution context used to restore persisted state prior to formatting.
/// Describes the target type for the value conversion.
/// The key of the state value.
- /// An optional name that specifies the scope to read.If null, the default scope is used.
+ /// An optional name that specifies the scope to read. If null, the default scope is used.
/// A token that propagates notification when operation should be canceled.
/// The converted value
public static async ValueTask