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..69a598d2d34 100644
--- a/dotnet/src/Microsoft.Agents.AI.Declarative/ChatClient/ChatClientPromptAgentFactory.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Declarative/ChatClient/ChatClientPromptAgentFactory.cs
@@ -20,36 +20,69 @@ 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, engine, configuration, loggerFactory, null)
+ {
+ // BINARY COMPAT CONSTRUCTOR
+ }
+ ///
+ /// Creates a new instance of the class.
+ ///
+ /// 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.
+ /// Optional explicitly allowed environment variables referenced by the agent definition.
+ public ChatClientPromptAgentFactory(
+ IChatClient chatClient,
+ IList? functions,
+ RecalcEngine? engine,
+ IConfiguration? configuration,
+ ILoggerFactory? loggerFactory,
+ IEnumerable? allowedConfigurationVariables)
+ : base(engine, configuration, allowedConfigurationVariables)
{
Throw.IfNull(chatClient);
-
this._chatClient = chatClient;
this._functions = functions;
this._loggerFactory = loggerFactory;
}
///
- public override Task TryCreateAsync(GptComponentMetadata promptAgent, CancellationToken cancellationToken = default)
+ public override async Task TryCreateAsync(GptComponentMetadata promptAgent, CancellationToken cancellationToken = default)
{
Throw.IfNull(promptAgent);
+ this.InitializeConfigurationVariables(promptAgent);
+
var options = new ChatClientAgentOptions()
{
Name = promptAgent.Name,
Description = promptAgent.Description,
- ChatOptions = promptAgent.GetChatOptions(this.Engine, this._functions),
+ ChatOptions = await promptAgent.GetChatOptionsAsync(this.Engine, this._functions, cancellationToken: cancellationToken).ConfigureAwait(false),
};
var agent = new ChatClientAgent(this._chatClient, options, this._loggerFactory);
Declarative.FeatureUsageMarker.MarkUsed();
- return Task.FromResult(agent);
+ return agent;
}
#region private
private readonly IChatClient _chatClient;
private readonly IList? _functions;
private readonly ILoggerFactory? _loggerFactory;
+
#endregion
}
diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/BoolExpressionExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/BoolExpressionExtensions.cs
index 9b12ea19fdd..a114f28ddcd 100644
--- a/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/BoolExpressionExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/BoolExpressionExtensions.cs
@@ -1,5 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
+using System.Threading;
+using System.Threading.Tasks;
using Microsoft.PowerFx;
using Microsoft.PowerFx.Types;
@@ -15,8 +17,9 @@ internal static class BoolExpressionExtensions
///
/// Expression to evaluate.
/// Recalc engine to use for evaluation.
+ /// Cancellation token to observe while evaluating the expression.
/// The evaluated boolean value, or null if the expression is null or cannot be evaluated.
- internal static bool? Eval(this BoolExpression? expression, RecalcEngine? engine)
+ internal static async Task EvalAsync(this BoolExpression? expression, RecalcEngine? engine, CancellationToken cancellationToken = default)
{
if (expression is null)
{
@@ -35,11 +38,11 @@ internal static class BoolExpressionExtensions
if (expression.IsExpression)
{
- return engine.Eval(expression.ExpressionText!).AsBoolean();
+ return (await engine.EvalAsync(expression.ExpressionText!, cancellationToken).ConfigureAwait(false)).AsBoolean();
}
else if (expression.IsVariableReference)
{
- var formulaValue = engine.Eval(expression.VariableReference!.VariableName);
+ var formulaValue = await engine.EvalAsync(expression.VariableReference!.VariableName, cancellationToken).ConfigureAwait(false);
if (formulaValue is BooleanValue booleanValue)
{
return booleanValue.Value;
diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/IntExpressionExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/IntExpressionExtensions.cs
index dbc6ff4dda4..3d24cb19fbe 100644
--- a/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/IntExpressionExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/IntExpressionExtensions.cs
@@ -1,6 +1,8 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Globalization;
+using System.Threading;
+using System.Threading.Tasks;
using Microsoft.PowerFx;
using Microsoft.PowerFx.Types;
@@ -16,8 +18,9 @@ internal static class IntExpressionExtensions
///
/// Expression to evaluate.
/// Recalc engine to use for evaluation.
+ /// Cancellation token to observe while evaluating the expression.
/// The evaluated integer value, or null if the expression is null or cannot be evaluated.
- internal static long? Eval(this IntExpression? expression, RecalcEngine? engine)
+ internal static async Task EvalAsync(this IntExpression? expression, RecalcEngine? engine, CancellationToken cancellationToken = default)
{
if (expression is null)
{
@@ -36,11 +39,11 @@ internal static class IntExpressionExtensions
if (expression.IsExpression)
{
- return (long)engine.Eval(expression.ExpressionText!).AsDouble();
+ return (long)(await engine.EvalAsync(expression.ExpressionText!, cancellationToken).ConfigureAwait(false)).AsDouble();
}
else if (expression.IsVariableReference)
{
- var formulaValue = engine.Eval(expression.VariableReference!.VariableName);
+ var formulaValue = await engine.EvalAsync(expression.VariableReference!.VariableName, cancellationToken).ConfigureAwait(false);
if (formulaValue is NumberValue numberValue)
{
return (long)numberValue.Value;
diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/NumberExpressionExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/NumberExpressionExtensions.cs
index b4f59a015a1..aa6710025b8 100644
--- a/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/NumberExpressionExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/NumberExpressionExtensions.cs
@@ -1,6 +1,8 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Globalization;
+using System.Threading;
+using System.Threading.Tasks;
using Microsoft.PowerFx;
using Microsoft.PowerFx.Types;
@@ -16,8 +18,9 @@ internal static class NumberExpressionExtensions
///
/// Expression to evaluate.
/// Recalc engine to use for evaluation.
+ /// Cancellation token to observe while evaluating the expression.
/// The evaluated number value, or null if the expression is null or cannot be evaluated.
- internal static double? Eval(this NumberExpression? expression, RecalcEngine? engine)
+ internal static async Task EvalAsync(this NumberExpression? expression, RecalcEngine? engine, CancellationToken cancellationToken = default)
{
if (expression is null)
{
@@ -36,11 +39,11 @@ internal static class NumberExpressionExtensions
if (expression.IsExpression)
{
- return engine.Eval(expression.ExpressionText!).AsDouble();
+ return (await engine.EvalAsync(expression.ExpressionText!, cancellationToken).ConfigureAwait(false)).AsDouble();
}
else if (expression.IsVariableReference)
{
- var formulaValue = engine.Eval(expression.VariableReference!.VariableName);
+ var formulaValue = await engine.EvalAsync(expression.VariableReference!.VariableName, cancellationToken).ConfigureAwait(false);
if (formulaValue is NumberValue numberValue)
{
return numberValue.Value;
diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/PromptAgentExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/PromptAgentExtensions.cs
index 0da3f18f85a..4a9153c94cc 100644
--- a/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/PromptAgentExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/PromptAgentExtensions.cs
@@ -1,7 +1,10 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
+using System.ComponentModel;
using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Microsoft.PowerFx;
using Microsoft.Shared.Diagnostics;
@@ -19,7 +22,20 @@ public static class PromptAgentExtensions
/// Instance of
/// Instance of
/// Instance of
+ [Obsolete("Use GetChatOptionsAsync instead. This method calls into async methods and might cause deadlocks")]
+ [EditorBrowsable(EditorBrowsableState.Never)]
public static ChatOptions? GetChatOptions(this GptComponentMetadata promptAgent, RecalcEngine? engine, IList? functions)
+#pragma warning disable VSTHRD002 // Avoid problematic synchronous waits
+ => promptAgent.GetChatOptionsAsync(engine, functions).GetAwaiter().GetResult();
+#pragma warning restore VSTHRD002 // Avoid problematic synchronous waits
+ ///
+ /// Retrieves the 'options' property from a as a instance.
+ ///
+ /// Instance of
+ /// Instance of
+ /// Instance of
+ /// Cancellation token to observe while retrieving chat options.
+ public static async Task GetChatOptionsAsync(this GptComponentMetadata promptAgent, RecalcEngine? engine, IList? functions, CancellationToken cancellationToken = default)
{
Throw.IfNull(promptAgent);
@@ -36,17 +52,17 @@ public static class PromptAgentExtensions
return new ChatOptions()
{
Instructions = promptAgent.Instructions?.ToTemplateString(),
- Temperature = (float?)modelOptions?.Temperature?.Eval(engine),
- MaxOutputTokens = (int?)modelOptions?.MaxOutputTokens?.Eval(engine),
- TopP = (float?)modelOptions?.TopP?.Eval(engine),
- TopK = (int?)modelOptions?.TopK?.Eval(engine),
- FrequencyPenalty = (float?)modelOptions?.FrequencyPenalty?.Eval(engine),
- PresencePenalty = (float?)modelOptions?.PresencePenalty?.Eval(engine),
- Seed = modelOptions?.Seed?.Eval(engine),
+ Temperature = modelOptions?.Temperature is { } temperature ? (float?)await temperature.EvalAsync(engine, cancellationToken: cancellationToken).ConfigureAwait(false) : default,
+ MaxOutputTokens = modelOptions?.MaxOutputTokens is { } maxOutputTokens ? (int?)await maxOutputTokens.EvalAsync(engine, cancellationToken: cancellationToken).ConfigureAwait(false) : default,
+ TopP = modelOptions?.TopP is { } topP ? (float?)await topP.EvalAsync(engine, cancellationToken: cancellationToken).ConfigureAwait(false) : default,
+ TopK = modelOptions?.TopK is { } topK ? (int?)await topK.EvalAsync(engine, cancellationToken: cancellationToken).ConfigureAwait(false) : default,
+ FrequencyPenalty = modelOptions?.FrequencyPenalty is { } frequencyPenalty ? (float?)await frequencyPenalty.EvalAsync(engine, cancellationToken: cancellationToken).ConfigureAwait(false) : default,
+ PresencePenalty = modelOptions?.PresencePenalty is { } presencePenalty ? (float?)await presencePenalty.EvalAsync(engine, cancellationToken: cancellationToken).ConfigureAwait(false) : default,
+ Seed = modelOptions?.Seed is { } seed ? (int?)await seed.EvalAsync(engine, cancellationToken: cancellationToken).ConfigureAwait(false) : default,
ResponseFormat = outputSchema?.AsChatResponseFormat(),
ModelId = promptAgent.Model?.ModelNameHint,
StopSequences = modelOptions?.StopSequences,
- AllowMultipleToolCalls = modelOptions?.AllowMultipleToolCalls?.Eval(engine),
+ AllowMultipleToolCalls = modelOptions?.AllowMultipleToolCalls is { } allowMultipleToolCalls ? await allowMultipleToolCalls.EvalAsync(engine, cancellationToken: cancellationToken).ConfigureAwait(false) : default,
ToolMode = modelOptions?.AsChatToolMode(),
Tools = tools,
AdditionalProperties = modelOptions?.GetAdditionalProperties(s_chatOptionProperties),
diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/StringExpressionExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/StringExpressionExtensions.cs
index 2a9b42e0873..7a01b66a14e 100644
--- a/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/StringExpressionExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/StringExpressionExtensions.cs
@@ -1,5 +1,9 @@
// Copyright (c) Microsoft. All rights reserved.
+using System;
+using System.ComponentModel;
+using System.Threading;
+using System.Threading.Tasks;
using Microsoft.PowerFx;
using Microsoft.PowerFx.Types;
@@ -16,7 +20,20 @@ public static class StringExpressionExtensions
/// Expression to evaluate.
/// Recalc engine to use for evaluation.
/// The evaluated string value, or null if the expression is null or cannot be evaluated.
+ [Obsolete("Use EvalAsync instead. This method calls into async methods and might cause deadlocks")]
+ [EditorBrowsable(EditorBrowsableState.Never)]
public static string? Eval(this StringExpression? expression, RecalcEngine? engine)
+#pragma warning disable VSTHRD002 // Avoid problematic synchronous waits
+ => EvalAsync(expression, engine).GetAwaiter().GetResult();
+#pragma warning restore VSTHRD002 // Avoid problematic synchronous waits
+ ///
+ /// Evaluates the given using the provided .
+ ///
+ /// Expression to evaluate.
+ /// Recalc engine to use for evaluation.
+ /// Cancellation token to use for the asynchronous operation.
+ /// The evaluated string value, or null if the expression is null or cannot be evaluated.
+ public static async Task EvalAsync(this StringExpression? expression, RecalcEngine? engine, CancellationToken cancellationToken = default)
{
if (expression is null)
{
@@ -35,11 +52,11 @@ public static class StringExpressionExtensions
if (expression.IsExpression)
{
- return engine.Eval(expression.ExpressionText!).ToString();
+ return (await engine.EvalAsync(expression.ExpressionText!, cancellationToken: cancellationToken).ConfigureAwait(false)).ToString();
}
else if (expression.IsVariableReference)
{
- var stringValue = engine.Eval(expression.VariableReference!.VariableName) as StringValue;
+ var stringValue = await engine.EvalAsync(expression.VariableReference!.VariableName, cancellationToken: cancellationToken).ConfigureAwait(false) as StringValue;
return stringValue?.Value;
}
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..f9ea1f2deda 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,85 @@ 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 .
- protected PromptAgentFactory(RecalcEngine? engine = null, IConfiguration? configuration = null)
+ /// 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, null, null, null)
{
- this.Engine = engine ?? new RecalcEngine();
+ // BINARY COMPAT CONSTRUCTOR
+ }
+
+ ///
+ /// 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(this._allowedConfigurationVariables.Contains))
+ {
+ 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 .
///
@@ -49,6 +107,7 @@ public async Task CreateAsync(GptComponentMetadata promptAgent, Cancell
{
Throw.IfNull(promptAgent);
+ this.InitializeConfigurationVariables(promptAgent);
var agent = await this.TryCreateAsync(promptAgent, cancellationToken).ConfigureAwait(false) ?? throw new NotSupportedException($"Agent type {promptAgent.Kind} is not supported.");
Declarative.FeatureUsageMarker.MarkUsed();
return agent;
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..b5b05b910d3 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.
///
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..3217ea774af 100644
--- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Kit/IWorkflowContextExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Kit/IWorkflowContextExtensions.cs
@@ -36,6 +36,27 @@ public static class IWorkflowContextExtensions
public static ValueTask FormatTemplateAsync(this IWorkflowContext context, string line, CancellationToken cancellationToken = default) =>
context.FormatTemplateAsync([line], cancellationToken);
+ ///
+ /// Formats a template line using the workflow's declarative state
+ /// and evaluating any embedded expressions (e.g., Power Fx) contained within the line.
+ ///
+ /// The workflow execution context used to restore persisted state prior to formatting.
+ /// The template line to format.
+ /// The formatted line and its sensitivity metadata.
+ public static ValueTask> FormatTemplateWithSensitivityAsync(this IWorkflowContext context, string line) =>
+ context.FormatTemplateWithSensitivityAsync(line, default);
+
+ ///
+ /// Formats a template line using the workflow's declarative state
+ /// and evaluating any embedded expressions (e.g., Power Fx) contained within the line.
+ ///
+ /// The workflow execution context used to restore persisted state prior to formatting.
+ /// The template line to format.
+ /// A token that propagates notification when operation should be canceled.
+ /// The formatted line and its sensitivity metadata.
+ public static ValueTask> FormatTemplateWithSensitivityAsync(this IWorkflowContext context, string line, CancellationToken cancellationToken) =>
+ context.FormatTemplateWithSensitivityAsync([line], cancellationToken);
+
///
/// Formats a template lines using the workflow's declarative state
/// and evaluating any embedded expressions (e.g., Power Fx) contained within each line.
@@ -52,16 +73,44 @@ public static ValueTask FormatTemplateAsync(this IWorkflowContext contex
/// var text = await context.FormatAsync("Hello @{User.Name}", "Count: @{Metrics.Count}");
///
public static async ValueTask FormatTemplateAsync(this IWorkflowContext context, IEnumerable lines, CancellationToken cancellationToken = default)
+ {
+ EvaluationResult result = await context.FormatTemplateWithSensitivityAsync(lines, cancellationToken).ConfigureAwait(false);
+ ThrowIfSensitive(result.Sensitivity);
+ return result.Value;
+ }
+
+ ///
+ /// Formats a template lines using the workflow's declarative state
+ /// and evaluating any embedded expressions (e.g., Power Fx) contained within each line.
+ ///
+ /// The workflow execution context used to restore persisted state prior to formatting.
+ /// The template lines to format.
+ /// The formatted lines and their sensitivity metadata.
+ public static ValueTask> FormatTemplateWithSensitivityAsync(this IWorkflowContext context, IEnumerable lines) =>
+ context.FormatTemplateWithSensitivityAsync(lines, default);
+
+ ///
+ /// Formats a template lines using the workflow's declarative state
+ /// and evaluating any embedded expressions (e.g., Power Fx) contained within each line.
+ ///
+ /// The workflow execution context used to restore persisted state prior to formatting.
+ /// The template lines to format.
+ /// A token that propagates notification when operation should be canceled.
+ /// The formatted lines and their sensitivity metadata.
+ public static async ValueTask> FormatTemplateWithSensitivityAsync(this IWorkflowContext context, IEnumerable lines, CancellationToken cancellationToken)
{
WorkflowFormulaState state = await context.GetStateAsync(cancellationToken).ConfigureAwait(false);
StringBuilder builder = new();
+ SensitivityLevel sensitivity = SensitivityLevel.None;
foreach (string line in lines)
{
- builder.AppendLine(state.Engine.Format(TemplateLine.Parse(line)));
+ EvaluationResult result = state.Evaluator.Format(TemplateLine.Parse(line));
+ sensitivity = MaxSensitivity(sensitivity, result.Sensitivity);
+ builder.AppendLine(result.Value);
}
- return builder.ToString();
+ return new(builder.ToString(), sensitivity);
}
///
@@ -82,12 +131,27 @@ public static async ValueTask FormatTemplateAsync(this IWorkflowContext
/// A token that propagates notification when operation should be canceled.
/// The evaluated expression value
public static async ValueTask EvaluateValueAsync(this IWorkflowContext context, string expression, CancellationToken cancellationToken = default)
+ {
+ EvaluationResult result = await context.EvaluateValueWithSensitivityAsync(expression, cancellationToken).ConfigureAwait(false);
+ ThrowIfSensitive(result.Sensitivity);
+ return result.Value;
+ }
+
+ ///
+ /// Evaluate an expression using the workflow's declarative state.
+ ///
+ /// The type of the evaluated value.
+ /// The workflow execution context used to restore persisted state prior to formatting.
+ /// The expression to evaluate.
+ /// A token that propagates notification when operation should be canceled.
+ /// The evaluated expression value and its sensitivity metadata.
+ public static async ValueTask> EvaluateValueWithSensitivityAsync(this IWorkflowContext context, string expression, CancellationToken cancellationToken = default)
{
WorkflowFormulaState state = await context.GetStateAsync(cancellationToken).ConfigureAwait(false);
EvaluationResult result = state.Evaluator.GetValue(ValueExpression.Expression(expression));
- return (TValue?)result.Value.ToObject();
+ return new((TValue?)result.Value.ToObject(), result.Sensitivity);
}
///
@@ -103,10 +167,74 @@ 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, plainScopeName, 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;
+ }
+
+ string plainScopeName = scopeName ?? WorkflowFormulaState.DefaultScopeName;
+ await context.QueueStateUpdateAsync(key, value.Value, plainScopeName, cancellationToken).ConfigureAwait(false);
+ 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 +255,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
/// The workflow execution context providing messaging and state services.
/// The set of variable names to initialize.
/// A representing the asynchronous execution operation.
protected async ValueTask InitializeEnvironmentAsync(IWorkflowContext context, params string[] variableNames)
{
- foreach (string variableName in variableNames)
+ foreach (string variableName in variableNames.Where(this._allowedEnvironmentVariables.Contains))
{
await context.QueueEnvironmentUpdateAsync(variableName, GetEnvironmentVariable(variableName)).ConfigureAwait(false);
}
string GetEnvironmentVariable(string name)
{
- if (this._configuration is not null)
- {
- return this._configuration[name] ?? string.Empty;
- }
-
- return Environment.GetEnvironmentVariable(name) ?? string.Empty;
+ string? configurationValue = this._configuration?[name];
+ return configurationValue ?? (this._allowProcessEnvironmentVariableFallback ? Environment.GetEnvironmentVariable(name) ?? string.Empty : string.Empty);
}
}
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/AddConversationMessageExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/AddConversationMessageExecutor.cs
index 21c14de546f..16740053de6 100644
--- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/AddConversationMessageExecutor.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/AddConversationMessageExecutor.cs
@@ -7,6 +7,7 @@
using Microsoft.Agents.AI.Workflows.Declarative.Interpreter;
using Microsoft.Agents.AI.Workflows.Declarative.PowerFx;
using Microsoft.Agents.ObjectModel;
+using Microsoft.Agents.ObjectModel.Abstractions;
using Microsoft.Extensions.AI;
using Microsoft.Shared.Diagnostics;
@@ -42,7 +43,13 @@ private IEnumerable GetContent()
{
foreach (AddConversationMessageContent content in this.Model.Content)
{
- AIContent? messageContent = content.Type.Value.ToContent(this.Engine.Format(content.Value), content.MediaType);
+ EvaluationResult contentResult = this.Evaluator.Format(content.Value);
+ if (contentResult.Sensitivity == SensitivityLevel.Sensitive)
+ {
+ throw new DeclarativeActionException($"Cannot send sensitive conversation message content: {this.Id}.");
+ }
+
+ AIContent? messageContent = content.Type.Value.ToContent(contentResult.Value, content.MediaType);
if (messageContent is not null)
{
yield return messageContent;
@@ -57,8 +64,12 @@ private IEnumerable GetContent()
return null;
}
- RecordDataValue? metadataValue = this.Evaluator.GetValue(this.Model.Metadata).Value;
+ EvaluationResult metadataResult = this.Evaluator.GetValue(this.Model.Metadata);
+ if (metadataResult.Sensitivity == SensitivityLevel.Sensitive)
+ {
+ throw new DeclarativeActionException($"Cannot send sensitive conversation message metadata: {this.Id}.");
+ }
- return metadataValue.ToMetadata();
+ return metadataResult.Value.ToMetadata();
}
}
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/CopyConversationMessagesExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/CopyConversationMessagesExecutor.cs
index 381abcb84dc..3b63ffeb26a 100644
--- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/CopyConversationMessagesExecutor.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/CopyConversationMessagesExecutor.cs
@@ -45,6 +45,11 @@ internal sealed class CopyConversationMessagesExecutor(CopyConversationMessages
Throw.IfNull(this.Model.Messages, $"{nameof(this.Model)}.{nameof(this.Model.Messages)}");
EvaluationResult expressionResult = this.Evaluator.GetValue(this.Model.Messages);
+ if (expressionResult.Sensitivity == SensitivityLevel.Sensitive)
+ {
+ throw new DeclarativeActionException($"Cannot send sensitive conversation messages: {this.Id}.");
+ }
+
DataValue messages = expressionResult.Value;
return messages.ToChatMessages();
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/EditTableExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/EditTableExecutor.cs
index 41fd8468e0e..e110ee4272a 100644
--- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/EditTableExecutor.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/EditTableExecutor.cs
@@ -25,6 +25,7 @@ internal sealed class EditTableExecutor(EditTable model, WorkflowFormulaState st
{
throw this.Exception($"Require '{variablePath}' to be a table, not: '{table.GetType().Name}'.");
}
+ SensitivityLevel tableSensitivity = this.GetSensitivity(variablePath);
TableChangeType changeType = this.Model.ChangeType.Value;
switch (this.Model.ChangeType.Value)
@@ -33,6 +34,7 @@ internal sealed class EditTableExecutor(EditTable model, WorkflowFormulaState st
ValueExpression addItemValue = Throw.IfNull(this.Model.Value, $"{nameof(this.Model)}.{nameof(this.Model.Value)}");
EvaluationResult addResult = this.Evaluator.GetValue(addItemValue);
FormulaValue addValue = addResult.Value.ToFormula();
+ SensitivityLevel addSensitivity = MaxSensitivity(tableSensitivity, addResult.Sensitivity);
RecordType recordType = tableValue.Type.ToRecord();
RecordValue newRecord;
TableValue resultTable;
@@ -47,35 +49,36 @@ internal sealed class EditTableExecutor(EditTable model, WorkflowFormulaState st
await tableValue.AppendAsync(newRecord, cancellationToken).ConfigureAwait(false);
resultTable = tableValue;
}
- await this.AssignAsync(variablePath, resultTable, context).ConfigureAwait(false);
- await this.AssignAsync(this.Model.ResultVariable?.Path, newRecord, context).ConfigureAwait(false);
+ await this.AssignAsync(variablePath, resultTable, context, addSensitivity).ConfigureAwait(false);
+ await this.AssignAsync(this.Model.ResultVariable?.Path, newRecord, context, addSensitivity).ConfigureAwait(false);
break;
case TableChangeType.Remove:
ValueExpression removeItemValue = Throw.IfNull(this.Model.Value, $"{nameof(this.Model)}.{nameof(this.Model.Value)}");
EvaluationResult removeResult = this.Evaluator.GetValue(removeItemValue);
+ SensitivityLevel removeSensitivity = MaxSensitivity(tableSensitivity, removeResult.Sensitivity);
if (removeResult.Value is TableDataValue removeItemTable)
{
await tableValue.RemoveAsync(removeItemTable?.Values.Select(row => row.ToRecordValue()), all: true, cancellationToken).ConfigureAwait(false);
- await this.AssignAsync(variablePath, tableValue, context).ConfigureAwait(false);
- await this.AssignAsync(this.Model.ResultVariable?.Path, RecordValue.Empty(), context).ConfigureAwait(false);
+ await this.AssignAsync(variablePath, tableValue, context, removeSensitivity).ConfigureAwait(false);
+ await this.AssignAsync(this.Model.ResultVariable?.Path, RecordValue.Empty(), context, removeSensitivity).ConfigureAwait(false);
}
break;
case TableChangeType.Clear:
await tableValue.ClearAsync(cancellationToken).ConfigureAwait(false);
- await this.AssignAsync(variablePath, tableValue, context).ConfigureAwait(false);
- await this.AssignAsync(this.Model.ResultVariable?.Path, FormulaValue.NewBlank(), context).ConfigureAwait(false);
+ await this.AssignAsync(variablePath, tableValue, context, tableSensitivity).ConfigureAwait(false);
+ await this.AssignAsync(this.Model.ResultVariable?.Path, FormulaValue.NewBlank(), context, tableSensitivity).ConfigureAwait(false);
break;
case TableChangeType.TakeFirst:
RecordValue? firstRow = tableValue.Rows.FirstOrDefault()?.Value;
if (firstRow is not null)
{
await tableValue.RemoveAsync([firstRow], all: true, cancellationToken).ConfigureAwait(false);
- await this.AssignAsync(variablePath, tableValue, context).ConfigureAwait(false);
- await this.AssignAsync(this.Model.ResultVariable?.Path, firstRow, context).ConfigureAwait(false);
+ await this.AssignAsync(variablePath, tableValue, context, tableSensitivity).ConfigureAwait(false);
+ await this.AssignAsync(this.Model.ResultVariable?.Path, firstRow, context, tableSensitivity).ConfigureAwait(false);
}
else
{
- await this.AssignAsync(this.Model.ResultVariable?.Path, FormulaValue.NewBlank(), context).ConfigureAwait(false);
+ await this.AssignAsync(this.Model.ResultVariable?.Path, FormulaValue.NewBlank(), context, tableSensitivity).ConfigureAwait(false);
}
break;
case TableChangeType.TakeLast:
@@ -83,12 +86,12 @@ internal sealed class EditTableExecutor(EditTable model, WorkflowFormulaState st
if (lastRow is not null)
{
await tableValue.RemoveAsync([lastRow], all: true, cancellationToken).ConfigureAwait(false);
- await this.AssignAsync(variablePath, tableValue, context).ConfigureAwait(false);
- await this.AssignAsync(this.Model.ResultVariable?.Path, lastRow, context).ConfigureAwait(false);
+ await this.AssignAsync(variablePath, tableValue, context, tableSensitivity).ConfigureAwait(false);
+ await this.AssignAsync(this.Model.ResultVariable?.Path, lastRow, context, tableSensitivity).ConfigureAwait(false);
}
else
{
- await this.AssignAsync(this.Model.ResultVariable?.Path, FormulaValue.NewBlank(), context).ConfigureAwait(false);
+ await this.AssignAsync(this.Model.ResultVariable?.Path, FormulaValue.NewBlank(), context, tableSensitivity).ConfigureAwait(false);
}
break;
}
@@ -120,4 +123,10 @@ IEnumerable GetValues()
}
}
}
+
+ private SensitivityLevel GetSensitivity(PropertyPath? path) =>
+ path?.VariableName is string variableName ? this.State.GetSensitivity(variableName, path.NamespaceAlias) : SensitivityLevel.None;
+
+ private static SensitivityLevel MaxSensitivity(SensitivityLevel left, SensitivityLevel right) =>
+ left == SensitivityLevel.Sensitive || right == SensitivityLevel.Sensitive ? SensitivityLevel.Sensitive : SensitivityLevel.None;
}
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/EditTableV2Executor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/EditTableV2Executor.cs
index 79b32428a87..7475916bf11 100644
--- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/EditTableV2Executor.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/EditTableV2Executor.cs
@@ -25,6 +25,7 @@ internal sealed class EditTableV2Executor(EditTableV2 model, WorkflowFormulaStat
{
throw this.Exception($"Require '{this.Model.ItemsVariable.Path}' to be a table, not: '{table.GetType().Name}'.");
}
+ SensitivityLevel tableSensitivity = this.GetSensitivity(this.Model.ItemsVariable);
EditTableOperation? changeType = this.Model.ChangeType;
if (changeType is AddItemOperation addItemOperation)
@@ -32,6 +33,7 @@ internal sealed class EditTableV2Executor(EditTableV2 model, WorkflowFormulaStat
ValueExpression addItemValue = Throw.IfNull(addItemOperation.Value, $"{nameof(this.Model)}.{nameof(this.Model.ChangeType)}");
EvaluationResult expressionResult = this.Evaluator.GetValue(addItemValue);
FormulaValue addValue = expressionResult.Value.ToFormula();
+ SensitivityLevel mutationSensitivity = MaxSensitivity(tableSensitivity, expressionResult.Sensitivity);
RecordType recordType = tableValue.Type.ToRecord();
TableValue resultTable;
if (!recordType.FieldNames.Any() && !tableValue.Rows.Any())
@@ -45,21 +47,22 @@ internal sealed class EditTableV2Executor(EditTableV2 model, WorkflowFormulaStat
await tableValue.AppendAsync(newRecord, cancellationToken).ConfigureAwait(false);
resultTable = tableValue;
}
- await this.AssignAsync(this.Model.ItemsVariable, resultTable, context).ConfigureAwait(false);
+ await this.AssignAsync(this.Model.ItemsVariable, resultTable, context, mutationSensitivity).ConfigureAwait(false);
}
else if (changeType is ClearItemsOperation)
{
await tableValue.ClearAsync(cancellationToken).ConfigureAwait(false);
- await this.AssignAsync(this.Model.ItemsVariable, tableValue, context).ConfigureAwait(false);
+ await this.AssignAsync(this.Model.ItemsVariable, tableValue, context, tableSensitivity).ConfigureAwait(false);
}
else if (changeType is RemoveItemOperation removeItemOperation)
{
ValueExpression removeItemValue = Throw.IfNull(removeItemOperation.Value, $"{nameof(this.Model)}.{nameof(this.Model.ChangeType)}");
EvaluationResult expressionResult = this.Evaluator.GetValue(removeItemValue);
+ SensitivityLevel mutationSensitivity = MaxSensitivity(tableSensitivity, expressionResult.Sensitivity);
if (expressionResult.Value.ToFormula() is TableValue removeItemTable)
{
await tableValue.RemoveAsync(removeItemTable.Rows.Select(row => row.Value), all: true, cancellationToken).ConfigureAwait(false);
- await this.AssignAsync(this.Model.ItemsVariable, tableValue, context).ConfigureAwait(false);
+ await this.AssignAsync(this.Model.ItemsVariable, tableValue, context, mutationSensitivity).ConfigureAwait(false);
}
}
else if (changeType is TakeLastItemOperation takeLastOperation)
@@ -68,12 +71,12 @@ internal sealed class EditTableV2Executor(EditTableV2 model, WorkflowFormulaStat
if (lastRow is not null)
{
await tableValue.RemoveAsync([lastRow], all: true, cancellationToken).ConfigureAwait(false);
- await this.AssignAsync(this.Model.ItemsVariable, tableValue, context).ConfigureAwait(false);
- await this.AssignAsync(takeLastOperation.ResultVariable?.Path, lastRow, context).ConfigureAwait(false);
+ await this.AssignAsync(this.Model.ItemsVariable, tableValue, context, tableSensitivity).ConfigureAwait(false);
+ await this.AssignAsync(takeLastOperation.ResultVariable?.Path, lastRow, context, tableSensitivity).ConfigureAwait(false);
}
else
{
- await this.AssignAsync(takeLastOperation.ResultVariable?.Path, FormulaValue.NewBlank(), context).ConfigureAwait(false);
+ await this.AssignAsync(takeLastOperation.ResultVariable?.Path, FormulaValue.NewBlank(), context, tableSensitivity).ConfigureAwait(false);
}
}
else if (changeType is TakeFirstItemOperation takeFirstOperation)
@@ -82,12 +85,12 @@ internal sealed class EditTableV2Executor(EditTableV2 model, WorkflowFormulaStat
if (firstRow is not null)
{
await tableValue.RemoveAsync([firstRow], all: true, cancellationToken).ConfigureAwait(false);
- await this.AssignAsync(this.Model.ItemsVariable, tableValue, context).ConfigureAwait(false);
- await this.AssignAsync(takeFirstOperation.ResultVariable?.Path, firstRow, context).ConfigureAwait(false);
+ await this.AssignAsync(this.Model.ItemsVariable, tableValue, context, tableSensitivity).ConfigureAwait(false);
+ await this.AssignAsync(takeFirstOperation.ResultVariable?.Path, firstRow, context, tableSensitivity).ConfigureAwait(false);
}
else
{
- await this.AssignAsync(takeFirstOperation.ResultVariable?.Path, FormulaValue.NewBlank(), context).ConfigureAwait(false);
+ await this.AssignAsync(takeFirstOperation.ResultVariable?.Path, FormulaValue.NewBlank(), context, tableSensitivity).ConfigureAwait(false);
}
}
@@ -118,4 +121,10 @@ IEnumerable GetValues()
}
}
}
+
+ private SensitivityLevel GetSensitivity(PropertyPath? path) =>
+ path?.VariableName is string variableName ? this.State.GetSensitivity(variableName, path.NamespaceAlias) : SensitivityLevel.None;
+
+ private static SensitivityLevel MaxSensitivity(SensitivityLevel left, SensitivityLevel right) =>
+ left == SensitivityLevel.Sensitive || right == SensitivityLevel.Sensitive ? SensitivityLevel.Sensitive : SensitivityLevel.None;
}
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/ForeachExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/ForeachExecutor.cs
index f154ad7f97b..d829f0ba8ae 100644
--- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/ForeachExecutor.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/ForeachExecutor.cs
@@ -26,9 +26,11 @@ public static class Steps
private const string IndexStateKey = nameof(_index);
private const string ValuesStateKey = nameof(_values);
private const string HasValueStateKey = nameof(HasValue);
+ private const string SensitivityStateKey = nameof(_sensitivity);
private int _index;
private FormulaValue[] _values;
+ private SensitivityLevel _sensitivity;
public ForeachExecutor(Foreach model, WorkflowFormulaState state)
: base(model, state)
@@ -55,6 +57,7 @@ public ForeachExecutor(Foreach model, WorkflowFormulaState state)
{
this._values = [expressionResult.Value.ToFormula()];
}
+ this._sensitivity = expressionResult.Sensitivity;
await this.ResetStateAsync(context, cancellationToken).ConfigureAwait(false);
@@ -67,7 +70,15 @@ public async ValueTask TakeNextAsync(IWorkflowContext context, object? _, Cancel
{
FormulaValue value = this._values[this._index];
- await context.QueueStateUpdateAsync(Throw.IfNull(this.Model.Value), value, cancellationToken).ConfigureAwait(false);
+ PropertyPath valuePath = Throw.IfNull(this.Model.Value);
+ if (context is DeclarativeWorkflowContext)
+ {
+ await this.AssignAsync(valuePath, value, context, this._sensitivity).ConfigureAwait(false);
+ }
+ else
+ {
+ await context.QueueStateUpdateAsync(valuePath, value, cancellationToken).ConfigureAwait(false);
+ }
if (this.Model.Index is not null)
{
@@ -122,6 +133,7 @@ protected override async ValueTask OnCheckpointingAsync(IWorkflowContext context
await context.QueueStateUpdateAsync(IndexStateKey, this._index, cancellationToken: cancellationToken).ConfigureAwait(false);
await context.QueueStateUpdateAsync(ValuesStateKey, portableValues, cancellationToken: cancellationToken).ConfigureAwait(false);
await context.QueueStateUpdateAsync(HasValueStateKey, this.HasValue, cancellationToken: cancellationToken).ConfigureAwait(false);
+ await context.QueueStateUpdateAsync(SensitivityStateKey, this._sensitivity, cancellationToken: cancellationToken).ConfigureAwait(false);
await base.OnCheckpointingAsync(context, cancellationToken).ConfigureAwait(false);
}
@@ -147,5 +159,6 @@ protected override async ValueTask OnCheckpointRestoredAsync(IWorkflowContext co
this._values = [.. savedValues.Select(value => value.ToFormula())];
this._index = await context.ReadStateAsync(IndexStateKey, cancellationToken: cancellationToken).ConfigureAwait(false);
this.HasValue = await context.ReadStateAsync(HasValueStateKey, cancellationToken: cancellationToken).ConfigureAwait(false);
+ this._sensitivity = await context.ReadStateAsync(SensitivityStateKey, cancellationToken: cancellationToken).ConfigureAwait(false);
}
}
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/InvokeAzureAgentExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/InvokeAzureAgentExecutor.cs
index 2b0378f9998..fee636b801b 100644
--- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/InvokeAzureAgentExecutor.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/InvokeAzureAgentExecutor.cs
@@ -154,6 +154,11 @@ private async ValueTask InvokeAgentAsync(IWorkflowContext context, IEnumerable expressionResult = this.Evaluator.GetValue(this.AgentInput.Messages);
+ if (expressionResult.Sensitivity == SensitivityLevel.Sensitive)
+ {
+ throw new DeclarativeActionException($"Cannot send sensitive agent input messages: {this.Id}.");
+ }
+
userInput = expressionResult.Value;
}
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/ParseValueExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/ParseValueExecutor.cs
index 57fe319aaff..16f061f37a6 100644
--- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/ParseValueExecutor.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/ParseValueExecutor.cs
@@ -30,7 +30,7 @@ internal sealed class ParseValueExecutor(ParseValue model, WorkflowFormulaState
object? parsedResult = expressionResult.Value.ToObject().ConvertType(targetType);
parsedValue = parsedResult.ToFormula();
- await this.AssignAsync(this.Model.Variable.Path, parsedValue, context).ConfigureAwait(false);
+ await this.AssignAsync(this.Model.Variable.Path, parsedValue, context, expressionResult.Sensitivity).ConfigureAwait(false);
return default;
}
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/QuestionExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/QuestionExecutor.cs
index 4ad88dd40cb..dacdb9151f6 100644
--- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/QuestionExecutor.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/QuestionExecutor.cs
@@ -10,6 +10,7 @@
using Microsoft.Agents.AI.Workflows.Declarative.Kit;
using Microsoft.Agents.AI.Workflows.Declarative.PowerFx;
using Microsoft.Agents.ObjectModel;
+using Microsoft.Agents.ObjectModel.Abstractions;
using Microsoft.Extensions.AI;
using Microsoft.PowerFx.Types;
using Microsoft.Shared.Diagnostics;
@@ -161,13 +162,13 @@ private async ValueTask PromptAsync(IWorkflowContext context, int actualCount, C
long repeatCount = this.Evaluator.GetValue(this.Model.RepeatCount).Value;
if (actualCount >= repeatCount)
{
- DataValue defaultValue = DataValue.Blank();
+ EvaluationResult defaultValue = new(DataValue.Blank(), SensitivityLevel.None);
if (this.Model.DefaultValue is not null)
{
ValueExpression defaultValueExpression = Throw.IfNull(this.Model.DefaultValue);
- defaultValue = this.Evaluator.GetValue(defaultValueExpression).Value;
+ defaultValue = this.Evaluator.GetValue(defaultValueExpression);
}
- await this.AssignAsync(Throw.IfNull(this.Model.Variable).Path, defaultValue.ToFormula(), context).ConfigureAwait(false);
+ await this.AssignAsync(Throw.IfNull(this.Model.Variable).Path, defaultValue.Value.ToFormula(), context, defaultValue.Sensitivity).ConfigureAwait(false);
string defaultValueResponse = this.FormatPrompt(this.Model.DefaultValueResponse);
await context.AddEventAsync(new MessageActivityEvent(defaultValueResponse.Trim()), cancellationToken).ConfigureAwait(false);
// Reset for any subsequent Question turn (e.g. via GotoAction re-entry) so the next attempt starts fresh.
@@ -187,6 +188,12 @@ private string FormatPrompt(ActivityTemplateBase? promptTemplate)
return string.Empty;
}
- return this.Engine.Format(messageActivity.Text).Trim();
+ EvaluationResult promptResult = this.Evaluator.Format(messageActivity.Text);
+ if (promptResult.Sensitivity == SensitivityLevel.Sensitive)
+ {
+ throw new DeclarativeActionException($"Cannot send sensitive question prompt: {this.Id}.");
+ }
+
+ return promptResult.Value.Trim();
}
}
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/SendActivityExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/SendActivityExecutor.cs
index 3b9794b197f..4763422b105 100644
--- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/SendActivityExecutor.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/SendActivityExecutor.cs
@@ -3,10 +3,10 @@
using System;
using System.Threading;
using System.Threading.Tasks;
-using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
using Microsoft.Agents.AI.Workflows.Declarative.Interpreter;
using Microsoft.Agents.AI.Workflows.Declarative.PowerFx;
using Microsoft.Agents.ObjectModel;
+using Microsoft.Agents.ObjectModel.Abstractions;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI.Workflows.Declarative.ObjectModel;
@@ -18,7 +18,13 @@ internal sealed class SendActivityExecutor(SendActivity model, WorkflowFormulaSt
{
if (this.Model.Activity is MessageActivityTemplate messageActivity)
{
- string activityText = this.Engine.Format(messageActivity.Text).Trim();
+ EvaluationResult activityResult = this.Evaluator.Format(messageActivity.Text);
+ if (activityResult.Sensitivity == SensitivityLevel.Sensitive)
+ {
+ throw new DeclarativeActionException($"Cannot send sensitive activity text: {this.Id}.");
+ }
+
+ string activityText = activityResult.Value.Trim();
await context.AddEventAsync(new MessageActivityEvent(activityText.Trim()), cancellationToken).ConfigureAwait(false);
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/SetMultipleVariablesExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/SetMultipleVariablesExecutor.cs
index e81126e9a5e..1015c0a0dd1 100644
--- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/SetMultipleVariablesExecutor.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/SetMultipleVariablesExecutor.cs
@@ -31,7 +31,7 @@ internal sealed class SetMultipleVariablesExecutor(SetMultipleVariables model, W
{
EvaluationResult expressionResult = this.Evaluator.GetValue(assignment.Value);
- await this.AssignAsync(assignment.Variable, expressionResult.Value.ToFormula(), context).ConfigureAwait(false);
+ await this.AssignAsync(assignment.Variable, expressionResult.Value.ToFormula(), context, expressionResult.Sensitivity).ConfigureAwait(false);
}
}
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/SetTextVariableExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/SetTextVariableExecutor.cs
index 37b8d43e8a5..8d7f2924c4e 100644
--- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/SetTextVariableExecutor.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/SetTextVariableExecutor.cs
@@ -2,10 +2,10 @@
using System.Threading;
using System.Threading.Tasks;
-using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
using Microsoft.Agents.AI.Workflows.Declarative.Interpreter;
using Microsoft.Agents.AI.Workflows.Declarative.PowerFx;
using Microsoft.Agents.ObjectModel;
+using Microsoft.Agents.ObjectModel.Abstractions;
using Microsoft.PowerFx.Types;
using Microsoft.Shared.Diagnostics;
@@ -19,9 +19,9 @@ internal sealed class SetTextVariableExecutor(SetTextVariable model, WorkflowFor
Throw.IfNull(this.Model.Variable);
Throw.IfNull(this.Model.Value);
- FormulaValue expressionResult = FormulaValue.New(this.Engine.Format(this.Model.Value));
+ EvaluationResult expressionResult = this.Evaluator.Format(this.Model.Value);
- await this.AssignAsync(this.Model.Variable.Path, expressionResult, context).ConfigureAwait(false);
+ await this.AssignAsync(this.Model.Variable.Path, FormulaValue.New(expressionResult.Value), context, expressionResult.Sensitivity).ConfigureAwait(false);
return default;
}
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/SetVariableExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/SetVariableExecutor.cs
index 6fd4002df5c..d75a47ca00a 100644
--- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/SetVariableExecutor.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/SetVariableExecutor.cs
@@ -21,7 +21,7 @@ internal sealed class SetVariableExecutor(SetVariable model, WorkflowFormulaStat
EvaluationResult expressionResult = this.Evaluator.GetValue(this.Model.Value);
- await this.AssignAsync(this.Model.Variable.Path, expressionResult.Value.ToFormula(), context).ConfigureAwait(false);
+ await this.AssignAsync(this.Model.Variable.Path, expressionResult.Value.ToFormula(), context, expressionResult.Sensitivity).ConfigureAwait(false);
return default;
}
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PowerFx/RecalcEngineFactory.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PowerFx/RecalcEngineFactory.cs
index 6c6fe5649fe..c6cc9cbea65 100644
--- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PowerFx/RecalcEngineFactory.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PowerFx/RecalcEngineFactory.cs
@@ -37,7 +37,6 @@ PowerFxConfig CreateConfig()
config.MaxCallDepth = maximumCallDepth.Value;
}
- config.EnableSetFunction();
config.AddFunction(new AgentMessage());
config.AddFunction(new UserMessage());
config.AddFunction(new MessageText.StringInput());
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PowerFx/WorkflowDiagnostics.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PowerFx/WorkflowDiagnostics.cs
index 95b8f9ab93d..c46016317f3 100644
--- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PowerFx/WorkflowDiagnostics.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PowerFx/WorkflowDiagnostics.cs
@@ -37,22 +37,43 @@ public static WorkflowTypeInfo Describe(this TElement workflowElement)
[.. semanticModel.GetVariables(workflowElement.SchemaName.Value).Where(x => !x.IsSystemVariable).Select(v => v.ToDiagnostic())]);
}
- public static void Initialize(this WorkflowFormulaState scopes, TElement workflowElement, IConfiguration? configuration) where TElement : BotElement, IDialogBase
+ public static void Initialize(
+ this WorkflowFormulaState scopes,
+ TElement workflowElement,
+ IConfiguration? configuration,
+ IEnumerable? allowedEnvironmentVariables,
+ bool allowProcessEnvironmentVariableFallback) where TElement : BotElement, IDialogBase
{
scopes.InitializeSystem();
SemanticModel semanticModel = workflowElement.GetSemanticModel(new PowerFxExpressionChecker(s_semanticFeatureConfig), s_semanticFeatureConfig);
- scopes.InitializeEnvironment(semanticModel, configuration);
+ scopes.InitializeEnvironment(semanticModel, configuration, allowedEnvironmentVariables, allowProcessEnvironmentVariableFallback);
scopes.InitializeDefaults(semanticModel, workflowElement.SchemaName.Value);
}
- private static void InitializeEnvironment(this WorkflowFormulaState scopes, SemanticModel semanticModel, IConfiguration? configuration)
+ private static void InitializeEnvironment(
+ this WorkflowFormulaState scopes,
+ SemanticModel semanticModel,
+ IConfiguration? configuration,
+ IEnumerable? allowedEnvironmentVariables,
+ bool allowProcessEnvironmentVariableFallback)
{
+ HashSet allowedVariables = new(allowedEnvironmentVariables ?? [], StringComparer.Ordinal);
foreach (string variableName in semanticModel.GetAllEnvironmentVariablesReferencedInTheBot())
{
- string? environmentValue = configuration is not null ? configuration[variableName] : Environment.GetEnvironmentVariable(variableName);
+ if (!allowedVariables.Contains(variableName))
+ {
+ continue;
+ }
+
+ string? environmentValue = configuration?[variableName];
+ if (environmentValue is null && allowProcessEnvironmentVariableFallback)
+ {
+ environmentValue = Environment.GetEnvironmentVariable(variableName);
+ }
+
FormulaValue variableValue = string.IsNullOrEmpty(environmentValue) ? FormulaType.String.NewBlank() : FormulaValue.New(environmentValue);
- scopes.Set(variableName, variableValue, VariableScopeNames.Environment);
+ scopes.Set(variableName, variableValue, VariableScopeNames.Environment, SensitivityLevel.Sensitive);
}
}
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PowerFx/WorkflowExpressionEngine.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PowerFx/WorkflowExpressionEngine.cs
index a22a857635a..eb0232d1b0f 100644
--- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PowerFx/WorkflowExpressionEngine.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PowerFx/WorkflowExpressionEngine.cs
@@ -3,11 +3,13 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
+using System.Linq;
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
using Microsoft.Agents.ObjectModel;
using Microsoft.Agents.ObjectModel.Abstractions;
using Microsoft.Agents.ObjectModel.Exceptions;
using Microsoft.PowerFx;
+using Microsoft.PowerFx.Syntax;
using Microsoft.PowerFx.Types;
using Microsoft.Shared.Diagnostics;
@@ -15,11 +17,13 @@ namespace Microsoft.Agents.AI.Workflows.Declarative.PowerFx;
internal sealed class WorkflowExpressionEngine
{
- private readonly RecalcEngine _engine;
+ private readonly WorkflowFormulaState _state;
+ private readonly ParserOptions? _parserOptions;
- public WorkflowExpressionEngine(RecalcEngine engine)
+ public WorkflowExpressionEngine(WorkflowFormulaState state)
{
- this._engine = engine;
+ this._state = state;
+ this._parserOptions = state.AllowsSideEffects ? new ParserOptions { AllowsSideEffects = true } : null;
}
public EvaluationResult GetValue(BoolExpression boolean) => this.Evaluate(boolean);
@@ -41,6 +45,55 @@ public WorkflowExpressionEngine(RecalcEngine engine)
public EvaluationResult GetValue(EnumExpression expression) where TValue : EnumWrapper =>
this.Evaluate(expression);
+ public EvaluationResult Format(IEnumerable template)
+ {
+ Throw.IfNull(template);
+
+ SensitivityLevel sensitivity = SensitivityLevel.None;
+ List segments = [];
+ foreach (EvaluationResult result in template.Select(this.Format))
+ {
+ sensitivity = MaxSensitivity(sensitivity, result.Sensitivity);
+ segments.Add(result.Value);
+ }
+
+ return new(string.Concat(segments), sensitivity);
+ }
+
+ public EvaluationResult Format(TemplateLine? line)
+ {
+ if (line is null)
+ {
+ return new(string.Empty, SensitivityLevel.None);
+ }
+
+ SensitivityLevel sensitivity = SensitivityLevel.None;
+ List segments = [];
+ foreach (EvaluationResult result in line.Segments.Select(this.Format))
+ {
+ sensitivity = MaxSensitivity(sensitivity, result.Sensitivity);
+ segments.Add(result.Value);
+ }
+
+ return new(string.Concat(segments), sensitivity);
+ }
+
+ private EvaluationResult Format(TemplateSegment segment)
+ {
+ if (segment is TextSegment textSegment)
+ {
+ return new(textSegment.Value ?? string.Empty, SensitivityLevel.None);
+ }
+
+ if (segment is ExpressionSegment { Expression: not null } expressionSegment)
+ {
+ EvaluationResult result = this.EvaluateScope(expressionSegment.Expression);
+ return new(result.Value.Format(), result.Sensitivity);
+ }
+
+ throw new DeclarativeModelException($"Unsupported segment type: {segment.GetType().Name}");
+ }
+
private EvaluationResult Evaluate(BoolExpression expression)
{
Throw.IfNull(expression);
@@ -274,13 +327,136 @@ private EvaluationResult EvaluateScope(ExpressionBase expression)
expression.VariableReference?.ToString() :
expression.ExpressionText;
- FormulaValue result = this._engine.Eval(expressionText);
+ FormulaValue result = this._state.Engine.Eval(expressionText, options: this._parserOptions);
if (result is ErrorValue errorValue)
{
throw new DeclarativeActionException(errorValue.Format());
}
- return new(result, SensitivityLevel.None);
+ return new(result, this.GetSensitivity(expression));
+ }
+
+ private SensitivityLevel GetSensitivity(ExpressionBase expression)
+ {
+ if (expression.VariableReference is { VariableName: string variableName })
+ {
+ return GetReferenceSensitivity(expression.VariableReference.NamespaceAlias, variableName);
+ }
+
+ string? expressionText = expression.ExpressionText;
+ if (string.IsNullOrWhiteSpace(expressionText))
+ {
+ return SensitivityLevel.None;
+ }
+
+ CheckResult checkResult = this._state.Engine.Check(expressionText, options: this._parserOptions);
+ checkResult.ThrowOnErrors();
+
+ SensitivityLevel sensitivity = SensitivityLevel.None;
+ foreach ((string? ScopeName, string VariableName) reference in GetVariableReferences(checkResult.Parse.Root))
+ {
+ sensitivity = MaxSensitivity(sensitivity, GetReferenceSensitivity(reference.ScopeName, reference.VariableName));
+ }
+
+ return sensitivity;
+
+ SensitivityLevel GetReferenceSensitivity(string? scopeName, string variableName) =>
+ scopeName is null && VariableScopeNames.IsValidName(variableName)
+ ? this._state.GetScopeSensitivity(variableName)
+ : this._state.GetSensitivity(variableName, scopeName);
+ }
+
+ private static IEnumerable<(string? ScopeName, string VariableName)> GetVariableReferences(TexlNode node)
+ {
+ switch (node)
+ {
+ case DottedNameNode dottedNameNode:
+ if (TryGetDottedReference(dottedNameNode, out (string? ScopeName, string VariableName) dottedReference))
+ {
+ yield return dottedReference;
+ }
+ else
+ {
+ foreach ((string? ScopeName, string VariableName) reference in GetVariableReferences(dottedNameNode.Left))
+ {
+ yield return reference;
+ }
+ }
+ yield break;
+
+ case FirstNameNode firstNameNode:
+ yield return (null, firstNameNode.Ident.Name.Value);
+ yield break;
+
+ case AsNode asNode:
+ foreach ((string? ScopeName, string VariableName) reference in GetVariableReferences(asNode.Left))
+ {
+ yield return reference;
+ }
+ yield break;
+
+ case BinaryOpNode binaryOpNode:
+ foreach ((string? ScopeName, string VariableName) reference in GetVariableReferences(binaryOpNode.Left))
+ {
+ yield return reference;
+ }
+ foreach ((string? ScopeName, string VariableName) reference in GetVariableReferences(binaryOpNode.Right))
+ {
+ yield return reference;
+ }
+ yield break;
+
+ case UnaryOpNode unaryOpNode:
+ foreach ((string? ScopeName, string VariableName) reference in GetVariableReferences(unaryOpNode.Child))
+ {
+ yield return reference;
+ }
+ yield break;
+
+ case CallNode callNode:
+ foreach ((string? ScopeName, string VariableName) reference in GetVariableReferences(callNode.Args))
+ {
+ yield return reference;
+ }
+ yield break;
+
+ case VariadicBase variadicBase:
+ foreach (TexlNode childNode in variadicBase.ChildNodes)
+ {
+ foreach ((string? ScopeName, string VariableName) reference in GetVariableReferences(childNode))
+ {
+ yield return reference;
+ }
+ }
+ yield break;
+ }
}
+
+ private static bool TryGetDottedReference(DottedNameNode dottedNameNode, out (string? ScopeName, string VariableName) reference)
+ {
+ List names = [];
+ TexlNode node = dottedNameNode;
+ while (node is DottedNameNode current)
+ {
+ names.Add(current.Right.Name.Value);
+ node = current.Left;
+ }
+
+ if (node is not FirstNameNode firstNameNode)
+ {
+ reference = default;
+ return false;
+ }
+
+ names.Add(firstNameNode.Ident.Name.Value);
+ names.Reverse();
+ reference = names.Count > 1 && VariableScopeNames.IsValidName(names[0])
+ ? (names[0], names[1])
+ : (null, names[0]);
+ return true;
+ }
+
+ private static SensitivityLevel MaxSensitivity(SensitivityLevel left, SensitivityLevel right) =>
+ left == SensitivityLevel.Sensitive || right == SensitivityLevel.Sensitive ? SensitivityLevel.Sensitive : SensitivityLevel.None;
}
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PowerFx/WorkflowFormulaState.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PowerFx/WorkflowFormulaState.cs
index aaff60b08b4..87f726a77e0 100644
--- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PowerFx/WorkflowFormulaState.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PowerFx/WorkflowFormulaState.cs
@@ -1,5 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.
+using System;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Diagnostics;
@@ -27,6 +28,8 @@ internal sealed class WorkflowFormulaState
VariableScopeNames.System,
];
+ private const string SensitivityScopePrefix = "__Microsoft_Agents_AI_Workflows_Declarative_Sensitivity:";
+
private readonly Dictionary _scopes;
private Dictionary _initialScopes;
@@ -37,13 +40,16 @@ internal sealed class WorkflowFormulaState
public WorkflowExpressionEngine Evaluator { get; }
- public WorkflowFormulaState(RecalcEngine engine)
+ public bool AllowsSideEffects { get; }
+
+ public WorkflowFormulaState(RecalcEngine engine, bool allowsSideEffects = false)
{
this._scopes = VariableScopeNames.AllScopes.ToDictionary(scopeName => GetScopeName(scopeName), _ => new WorkflowScope());
this._initialScopes = this.CreateScopeSnapshot();
this.Engine = engine;
- this.Evaluator = new WorkflowExpressionEngine(engine);
+ this.AllowsSideEffects = allowsSideEffects;
+ this.Evaluator = new WorkflowExpressionEngine(this);
this.Bind();
}
@@ -59,8 +65,38 @@ public FormulaValue Get(string variableName, string? scopeName = null)
return FormulaValue.NewBlank();
}
- public void Set(string variableName, FormulaValue value, string? scopeName = null) =>
- this.GetScope(scopeName ?? DefaultScopeName)[variableName] = value;
+ public void Set(string variableName, FormulaValue value, string? scopeName = null, SensitivityLevel sensitivity = SensitivityLevel.None)
+ {
+ WorkflowScope scope = this.GetScope(scopeName ?? DefaultScopeName);
+ scope[variableName] = value;
+ scope.Sensitivities[variableName] = sensitivity;
+ }
+
+ public SensitivityLevel GetSensitivity(string variableName, string? scopeName = null)
+ {
+ if (scopeName is not null && !VariableScopeNames.IsValidName(scopeName))
+ {
+ return SensitivityLevel.None;
+ }
+
+ WorkflowScope scope = this.GetScope(scopeName ?? DefaultScopeName);
+ return scope.Sensitivities.TryGetValue(variableName, out SensitivityLevel sensitivity) ? sensitivity : SensitivityLevel.None;
+ }
+
+ public SensitivityLevel GetScopeSensitivity(string scopeName)
+ {
+ if (!VariableScopeNames.IsValidName(scopeName))
+ {
+ return SensitivityLevel.None;
+ }
+
+ return this.GetScope(scopeName).Sensitivities.Values.Any(static sensitivity => sensitivity == SensitivityLevel.Sensitive)
+ ? SensitivityLevel.Sensitive
+ : SensitivityLevel.None;
+ }
+
+ public void SetSensitivity(string variableName, string? scopeName, SensitivityLevel sensitivity) =>
+ this.GetScope(scopeName ?? DefaultScopeName).Sensitivities[variableName] = sensitivity;
public bool SetInitialized() => Interlocked.CompareExchange(ref this._isInitialized, 1, 0) == 0;
@@ -96,13 +132,14 @@ async Task ReadScopeAsync(string scopeName)
foreach (string key in keys)
{
PortableValue? value = await context.ReadStateAsync(key, scopeName, cancellationToken).ConfigureAwait(false);
+ SensitivityLevel sensitivity = await context.ReadStateAsync(key, GetSensitivityScopeName(scopeName), cancellationToken).ConfigureAwait(false);
if (value is null)
{
- this.Set(key, FormulaValue.NewBlank(), scopeName);
+ this.Set(key, FormulaValue.NewBlank(), scopeName, sensitivity);
continue;
}
FormulaValue formulaValue = value.ToFormula();
- this.Set(key, formulaValue, scopeName);
+ this.Set(key, formulaValue, scopeName, sensitivity);
Debug.WriteLine($"RESTORED: {scopeName}.{key} => {formulaValue.Type}");
}
@@ -119,10 +156,16 @@ private void RestoreInitialState()
{
WorkflowScope scope = this._scopes[initialScopeEntry.Key];
scope.Clear();
+ scope.Sensitivities.Clear();
foreach (KeyValuePair initialValueEntry in initialScopeEntry.Value)
{
scope[initialValueEntry.Key] = initialValueEntry.Value;
}
+
+ foreach (KeyValuePair initialSensitivityEntry in initialScopeEntry.Value.Sensitivities)
+ {
+ scope.Sensitivities[initialSensitivityEntry.Key] = initialSensitivityEntry.Value;
+ }
}
}
@@ -157,6 +200,8 @@ void Bind(string scopeName, string? targetScope = null)
private WorkflowScope GetScope(string? scopeName) => this._scopes[GetScopeName(scopeName)];
+ public static string GetSensitivityScopeName(string scopeName) => $"{SensitivityScopePrefix}{GetScopeName(scopeName)}";
+
public static string GetScopeName(string? scopeName)
{
WorkflowDiagnostics.SetFoundryProduct();
@@ -185,6 +230,15 @@ public WorkflowScope()
public WorkflowScope(IDictionary values)
: base(values)
{
+ if (values is WorkflowScope scope)
+ {
+ foreach (KeyValuePair sensitivity in scope.Sensitivities)
+ {
+ this.Sensitivities[sensitivity.Key] = sensitivity.Value;
+ }
+ }
}
+
+ public Dictionary Sensitivities { get; } = new(StringComparer.OrdinalIgnoreCase);
}
}
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PublicAPI/net10.0/PublicAPI.Unshipped.txt b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PublicAPI/net10.0/PublicAPI.Unshipped.txt
index ab058de62d4..d7ac870b3a9 100644
--- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PublicAPI/net10.0/PublicAPI.Unshipped.txt
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PublicAPI/net10.0/PublicAPI.Unshipped.txt
@@ -1 +1,13 @@
#nullable enable
+Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.AllowProcessEnvironmentVariableFallback.get -> bool
+Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.AllowProcessEnvironmentVariableFallback.init -> void
+Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.AllowedEnvironmentVariables.get -> System.Collections.Generic.IEnumerable?
+Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.AllowedEnvironmentVariables.init -> void
+static Microsoft.Agents.AI.Workflows.Declarative.Kit.IWorkflowContextExtensions.QueueStateUpdateWithSensitivityAsync(this Microsoft.Agents.AI.Workflows.IWorkflowContext! context, string! key, Microsoft.Agents.ObjectModel.Abstractions.EvaluationResult! value, string? scopeName = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask
+static Microsoft.Agents.AI.Workflows.Declarative.Kit.IWorkflowContextExtensions.ReadStateWithSensitivityAsync(this Microsoft.Agents.AI.Workflows.IWorkflowContext! context, string! key, string? scopeName = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!>
+static Microsoft.Agents.AI.Workflows.Declarative.Kit.IWorkflowContextExtensions.ConvertValueWithSensitivityAsync(this Microsoft.Agents.AI.Workflows.IWorkflowContext! context, Microsoft.Agents.AI.Workflows.Declarative.Kit.VariableType! targetType, string! key, string? scopeName = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!>
+static Microsoft.Agents.AI.Workflows.Declarative.Kit.IWorkflowContextExtensions.EvaluateValueWithSensitivityAsync(this Microsoft.Agents.AI.Workflows.IWorkflowContext! context, string! expression, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!>
+static Microsoft.Agents.AI.Workflows.Declarative.Kit.IWorkflowContextExtensions.FormatTemplateWithSensitivityAsync(this Microsoft.Agents.AI.Workflows.IWorkflowContext! context, System.Collections.Generic.IEnumerable! lines) -> System.Threading.Tasks.ValueTask!>
+static Microsoft.Agents.AI.Workflows.Declarative.Kit.IWorkflowContextExtensions.FormatTemplateWithSensitivityAsync(this Microsoft.Agents.AI.Workflows.IWorkflowContext! context, System.Collections.Generic.IEnumerable! lines, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask!>
+static Microsoft.Agents.AI.Workflows.Declarative.Kit.IWorkflowContextExtensions.FormatTemplateWithSensitivityAsync(this Microsoft.Agents.AI.Workflows.IWorkflowContext! context, string! line) -> System.Threading.Tasks.ValueTask!>
+static Microsoft.Agents.AI.Workflows.Declarative.Kit.IWorkflowContextExtensions.FormatTemplateWithSensitivityAsync(this Microsoft.Agents.AI.Workflows.IWorkflowContext! context, string! line, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask!>
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PublicAPI/net472/PublicAPI.Unshipped.txt b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PublicAPI/net472/PublicAPI.Unshipped.txt
index ab058de62d4..d7ac870b3a9 100644
--- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PublicAPI/net472/PublicAPI.Unshipped.txt
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PublicAPI/net472/PublicAPI.Unshipped.txt
@@ -1 +1,13 @@
#nullable enable
+Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.AllowProcessEnvironmentVariableFallback.get -> bool
+Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.AllowProcessEnvironmentVariableFallback.init -> void
+Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.AllowedEnvironmentVariables.get -> System.Collections.Generic.IEnumerable?
+Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.AllowedEnvironmentVariables.init -> void
+static Microsoft.Agents.AI.Workflows.Declarative.Kit.IWorkflowContextExtensions.QueueStateUpdateWithSensitivityAsync(this Microsoft.Agents.AI.Workflows.IWorkflowContext! context, string! key, Microsoft.Agents.ObjectModel.Abstractions.EvaluationResult! value, string? scopeName = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask
+static Microsoft.Agents.AI.Workflows.Declarative.Kit.IWorkflowContextExtensions.ReadStateWithSensitivityAsync(this Microsoft.Agents.AI.Workflows.IWorkflowContext! context, string! key, string? scopeName = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!>
+static Microsoft.Agents.AI.Workflows.Declarative.Kit.IWorkflowContextExtensions.ConvertValueWithSensitivityAsync(this Microsoft.Agents.AI.Workflows.IWorkflowContext! context, Microsoft.Agents.AI.Workflows.Declarative.Kit.VariableType! targetType, string! key, string? scopeName = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!>
+static Microsoft.Agents.AI.Workflows.Declarative.Kit.IWorkflowContextExtensions.EvaluateValueWithSensitivityAsync(this Microsoft.Agents.AI.Workflows.IWorkflowContext! context, string! expression, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!>
+static Microsoft.Agents.AI.Workflows.Declarative.Kit.IWorkflowContextExtensions.FormatTemplateWithSensitivityAsync(this Microsoft.Agents.AI.Workflows.IWorkflowContext! context, System.Collections.Generic.IEnumerable! lines) -> System.Threading.Tasks.ValueTask!>
+static Microsoft.Agents.AI.Workflows.Declarative.Kit.IWorkflowContextExtensions.FormatTemplateWithSensitivityAsync(this Microsoft.Agents.AI.Workflows.IWorkflowContext! context, System.Collections.Generic.IEnumerable! lines, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask!>
+static Microsoft.Agents.AI.Workflows.Declarative.Kit.IWorkflowContextExtensions.FormatTemplateWithSensitivityAsync(this Microsoft.Agents.AI.Workflows.IWorkflowContext! context, string! line) -> System.Threading.Tasks.ValueTask!>
+static Microsoft.Agents.AI.Workflows.Declarative.Kit.IWorkflowContextExtensions.FormatTemplateWithSensitivityAsync(this Microsoft.Agents.AI.Workflows.IWorkflowContext! context, string! line, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask!>
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PublicAPI/net8.0/PublicAPI.Unshipped.txt b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PublicAPI/net8.0/PublicAPI.Unshipped.txt
index ab058de62d4..d7ac870b3a9 100644
--- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PublicAPI/net8.0/PublicAPI.Unshipped.txt
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PublicAPI/net8.0/PublicAPI.Unshipped.txt
@@ -1 +1,13 @@
#nullable enable
+Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.AllowProcessEnvironmentVariableFallback.get -> bool
+Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.AllowProcessEnvironmentVariableFallback.init -> void
+Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.AllowedEnvironmentVariables.get -> System.Collections.Generic.IEnumerable?
+Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.AllowedEnvironmentVariables.init -> void
+static Microsoft.Agents.AI.Workflows.Declarative.Kit.IWorkflowContextExtensions.QueueStateUpdateWithSensitivityAsync(this Microsoft.Agents.AI.Workflows.IWorkflowContext! context, string! key, Microsoft.Agents.ObjectModel.Abstractions.EvaluationResult! value, string? scopeName = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask
+static Microsoft.Agents.AI.Workflows.Declarative.Kit.IWorkflowContextExtensions.ReadStateWithSensitivityAsync(this Microsoft.Agents.AI.Workflows.IWorkflowContext! context, string! key, string? scopeName = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!>
+static Microsoft.Agents.AI.Workflows.Declarative.Kit.IWorkflowContextExtensions.ConvertValueWithSensitivityAsync(this Microsoft.Agents.AI.Workflows.IWorkflowContext! context, Microsoft.Agents.AI.Workflows.Declarative.Kit.VariableType! targetType, string! key, string? scopeName = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!>
+static Microsoft.Agents.AI.Workflows.Declarative.Kit.IWorkflowContextExtensions.EvaluateValueWithSensitivityAsync(this Microsoft.Agents.AI.Workflows.IWorkflowContext! context, string! expression, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!>
+static Microsoft.Agents.AI.Workflows.Declarative.Kit.IWorkflowContextExtensions.FormatTemplateWithSensitivityAsync(this Microsoft.Agents.AI.Workflows.IWorkflowContext! context, System.Collections.Generic.IEnumerable! lines) -> System.Threading.Tasks.ValueTask!>
+static Microsoft.Agents.AI.Workflows.Declarative.Kit.IWorkflowContextExtensions.FormatTemplateWithSensitivityAsync(this Microsoft.Agents.AI.Workflows.IWorkflowContext! context, System.Collections.Generic.IEnumerable! lines, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask!>
+static Microsoft.Agents.AI.Workflows.Declarative.Kit.IWorkflowContextExtensions.FormatTemplateWithSensitivityAsync(this Microsoft.Agents.AI.Workflows.IWorkflowContext! context, string! line) -> System.Threading.Tasks.ValueTask!>
+static Microsoft.Agents.AI.Workflows.Declarative.Kit.IWorkflowContextExtensions.FormatTemplateWithSensitivityAsync(this Microsoft.Agents.AI.Workflows.IWorkflowContext! context, string! line, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask!>
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PublicAPI/net9.0/PublicAPI.Unshipped.txt b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PublicAPI/net9.0/PublicAPI.Unshipped.txt
index ab058de62d4..d7ac870b3a9 100644
--- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PublicAPI/net9.0/PublicAPI.Unshipped.txt
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PublicAPI/net9.0/PublicAPI.Unshipped.txt
@@ -1 +1,13 @@
#nullable enable
+Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.AllowProcessEnvironmentVariableFallback.get -> bool
+Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.AllowProcessEnvironmentVariableFallback.init -> void
+Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.AllowedEnvironmentVariables.get -> System.Collections.Generic.IEnumerable?
+Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.AllowedEnvironmentVariables.init -> void
+static Microsoft.Agents.AI.Workflows.Declarative.Kit.IWorkflowContextExtensions.QueueStateUpdateWithSensitivityAsync(this Microsoft.Agents.AI.Workflows.IWorkflowContext! context, string! key, Microsoft.Agents.ObjectModel.Abstractions.EvaluationResult! value, string? scopeName = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask
+static Microsoft.Agents.AI.Workflows.Declarative.Kit.IWorkflowContextExtensions.ReadStateWithSensitivityAsync(this Microsoft.Agents.AI.Workflows.IWorkflowContext! context, string! key, string? scopeName = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!>
+static Microsoft.Agents.AI.Workflows.Declarative.Kit.IWorkflowContextExtensions.ConvertValueWithSensitivityAsync(this Microsoft.Agents.AI.Workflows.IWorkflowContext! context, Microsoft.Agents.AI.Workflows.Declarative.Kit.VariableType! targetType, string! key, string? scopeName = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!>
+static Microsoft.Agents.AI.Workflows.Declarative.Kit.IWorkflowContextExtensions.EvaluateValueWithSensitivityAsync(this Microsoft.Agents.AI.Workflows.IWorkflowContext! context, string! expression, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!>
+static Microsoft.Agents.AI.Workflows.Declarative.Kit.IWorkflowContextExtensions.FormatTemplateWithSensitivityAsync(this Microsoft.Agents.AI.Workflows.IWorkflowContext! context, System.Collections.Generic.IEnumerable! lines) -> System.Threading.Tasks.ValueTask!>
+static Microsoft.Agents.AI.Workflows.Declarative.Kit.IWorkflowContextExtensions.FormatTemplateWithSensitivityAsync(this Microsoft.Agents.AI.Workflows.IWorkflowContext! context, System.Collections.Generic.IEnumerable! lines, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask!>
+static Microsoft.Agents.AI.Workflows.Declarative.Kit.IWorkflowContextExtensions.FormatTemplateWithSensitivityAsync(this Microsoft.Agents.AI.Workflows.IWorkflowContext! context, string! line) -> System.Threading.Tasks.ValueTask!>
+static Microsoft.Agents.AI.Workflows.Declarative.Kit.IWorkflowContextExtensions.FormatTemplateWithSensitivityAsync(this Microsoft.Agents.AI.Workflows.IWorkflowContext! context, string! line, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask!>
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt
index ab058de62d4..d7ac870b3a9 100644
--- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt
@@ -1 +1,13 @@
#nullable enable
+Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.AllowProcessEnvironmentVariableFallback.get -> bool
+Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.AllowProcessEnvironmentVariableFallback.init -> void
+Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.AllowedEnvironmentVariables.get -> System.Collections.Generic.IEnumerable?
+Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.AllowedEnvironmentVariables.init -> void
+static Microsoft.Agents.AI.Workflows.Declarative.Kit.IWorkflowContextExtensions.QueueStateUpdateWithSensitivityAsync(this Microsoft.Agents.AI.Workflows.IWorkflowContext! context, string! key, Microsoft.Agents.ObjectModel.Abstractions.EvaluationResult! value, string? scopeName = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask
+static Microsoft.Agents.AI.Workflows.Declarative.Kit.IWorkflowContextExtensions.ReadStateWithSensitivityAsync(this Microsoft.Agents.AI.Workflows.IWorkflowContext! context, string! key, string? scopeName = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!>
+static Microsoft.Agents.AI.Workflows.Declarative.Kit.IWorkflowContextExtensions.ConvertValueWithSensitivityAsync(this Microsoft.Agents.AI.Workflows.IWorkflowContext! context, Microsoft.Agents.AI.Workflows.Declarative.Kit.VariableType! targetType, string! key, string? scopeName = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!>
+static Microsoft.Agents.AI.Workflows.Declarative.Kit.IWorkflowContextExtensions.EvaluateValueWithSensitivityAsync(this Microsoft.Agents.AI.Workflows.IWorkflowContext! context, string! expression, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!>
+static Microsoft.Agents.AI.Workflows.Declarative.Kit.IWorkflowContextExtensions.FormatTemplateWithSensitivityAsync(this Microsoft.Agents.AI.Workflows.IWorkflowContext! context, System.Collections.Generic.IEnumerable! lines) -> System.Threading.Tasks.ValueTask!>
+static Microsoft.Agents.AI.Workflows.Declarative.Kit.IWorkflowContextExtensions.FormatTemplateWithSensitivityAsync(this Microsoft.Agents.AI.Workflows.IWorkflowContext! context, System.Collections.Generic.IEnumerable! lines, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask!>
+static Microsoft.Agents.AI.Workflows.Declarative.Kit.IWorkflowContextExtensions.FormatTemplateWithSensitivityAsync(this Microsoft.Agents.AI.Workflows.IWorkflowContext! context, string! line) -> System.Threading.Tasks.ValueTask!>
+static Microsoft.Agents.AI.Workflows.Declarative.Kit.IWorkflowContextExtensions.FormatTemplateWithSensitivityAsync(this Microsoft.Agents.AI.Workflows.IWorkflowContext! context, string! line, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask!>
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/InProc/InProcessRunner.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/InProc/InProcessRunner.cs
index 47167cd9e53..69b6c1e9bcc 100644
--- a/dotnet/src/Microsoft.Agents.AI.Workflows/InProc/InProcessRunner.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows/InProc/InProcessRunner.cs
@@ -423,7 +423,7 @@ await Task.WhenAll(executorNotifyTask,
restoreCheckpointIndexTask.AsTask()).ConfigureAwait(false);
this._lastCheckpointInfo = checkpointInfo;
- this.StepTracer.Reload(this.StepTracer.StepNumber);
+ this.StepTracer.Reload(checkpoint.StepNumber);
async ValueTask UpdateCheckpointIndexAsync()
{
diff --git a/dotnet/src/Shared/Workflows/Execution/WorkflowFactory.cs b/dotnet/src/Shared/Workflows/Execution/WorkflowFactory.cs
index 68e4af28786..b2e0db233a6 100644
--- a/dotnet/src/Shared/Workflows/Execution/WorkflowFactory.cs
+++ b/dotnet/src/Shared/Workflows/Execution/WorkflowFactory.cs
@@ -16,6 +16,8 @@ internal sealed class WorkflowFactory(string workflowFile, Uri foundryEndpoint)
public IConfiguration? Configuration { get; init; }
+ public IEnumerable? AllowedEnvironmentVariables { get; init; }
+
// Assign to continue an existing conversation
public string? ConversationId { get; init; }
@@ -46,6 +48,7 @@ public Workflow CreateWorkflow()
new(agentProvider)
{
Configuration = this.Configuration,
+ AllowedEnvironmentVariables = this.AllowedEnvironmentVariables,
ConversationId = this.ConversationId,
LoggerFactory = this.LoggerFactory,
McpToolHandler = this.McpToolHandler,
diff --git a/dotnet/tests/Microsoft.Agents.AI.Declarative.UnitTests/AgentBotElementYamlTests.cs b/dotnet/tests/Microsoft.Agents.AI.Declarative.UnitTests/AgentBotElementYamlTests.cs
index 418a68e25e2..52cdbf30b30 100644
--- a/dotnet/tests/Microsoft.Agents.AI.Declarative.UnitTests/AgentBotElementYamlTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.Declarative.UnitTests/AgentBotElementYamlTests.cs
@@ -4,6 +4,8 @@
using System.IO;
using System.Linq;
using System.Text.Json.Serialization;
+using System.Threading;
+using System.Threading.Tasks;
using Microsoft.Agents.ObjectModel;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Configuration;
@@ -220,7 +222,7 @@ public void FromYaml_RemoteConnection()
}
[Fact]
- public void FromYaml_WithVariableReferences()
+ public async Task FromYaml_WithVariableReferences()
{
// Arrange
IConfiguration configuration = new ConfigurationBuilder()
@@ -234,7 +236,10 @@ public void FromYaml_WithVariableReferences()
.Build();
// Act
- var agent = AgentBotElementYaml.FromYaml(PromptAgents.AgentWithVariableReferences, configuration);
+ var agent = AgentBotElementYaml.FromYaml(
+ PromptAgents.AgentWithVariableReferences,
+ configuration,
+ ["OpenAIEndpoint", "OpenAIApiKey", "Temperature", "TopP"]);
// Assert
Assert.NotNull(agent);
@@ -242,16 +247,16 @@ public void FromYaml_WithVariableReferences()
CurrentModels model = (agent.Model as CurrentModels)!;
Assert.NotNull(model);
Assert.NotNull(model.Options);
- Assert.Equal(0.9, Eval(model.Options?.Temperature, configuration));
- Assert.Equal(0.8, Eval(model.Options?.TopP, configuration));
+ Assert.Equal(0.9, await EvalAsync(model.Options?.Temperature, configuration, TestContext.Current.CancellationToken));
+ Assert.Equal(0.8, await EvalAsync(model.Options?.TopP, configuration, TestContext.Current.CancellationToken));
Assert.NotNull(model.Connection);
Assert.IsType(model.Connection);
ApiKeyConnection connection = (model.Connection as ApiKeyConnection)!;
Assert.NotNull(connection);
Assert.NotNull(connection.Endpoint);
Assert.NotNull(connection.Key);
- Assert.Equal("endpoint", Eval(connection.Endpoint, configuration));
- Assert.Equal("apiKey", Eval(connection.Key, configuration));
+ Assert.Equal("endpoint", await EvalAsync(connection.Endpoint, configuration, TestContext.Current.CancellationToken));
+ Assert.Equal("apiKey", await EvalAsync(connection.Key, configuration, TestContext.Current.CancellationToken));
}
///
@@ -270,7 +275,7 @@ public sealed class PersonInfo
public string? Occupation { get; set; }
}
- private static string? Eval(StringExpression? expression, IConfiguration? configuration = null)
+ private static async Task EvalAsync(StringExpression? expression, IConfiguration? configuration = null, CancellationToken cancellationToken = default)
{
if (expression is null)
{
@@ -286,10 +291,10 @@ public sealed class PersonInfo
}
}
- return expression.Eval(engine);
+ return await expression.EvalAsync(engine, cancellationToken: cancellationToken).ConfigureAwait(false);
}
- private static double? Eval(NumberExpression? expression, IConfiguration? configuration = null)
+ private static async Task EvalAsync(NumberExpression? expression, IConfiguration? configuration = null, CancellationToken cancellationToken = default)
{
if (expression is null)
{
@@ -305,6 +310,6 @@ public sealed class PersonInfo
}
}
- return expression.Eval(engine);
+ return await expression.EvalAsync(engine, cancellationToken: cancellationToken).ConfigureAwait(false);
}
}
diff --git a/dotnet/tests/Microsoft.Agents.AI.Declarative.UnitTests/ChatClient/ChatClientAgentFactoryTests.cs b/dotnet/tests/Microsoft.Agents.AI.Declarative.UnitTests/ChatClient/ChatClientAgentFactoryTests.cs
index 85906620005..ae6a4109438 100644
--- a/dotnet/tests/Microsoft.Agents.AI.Declarative.UnitTests/ChatClient/ChatClientAgentFactoryTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.Declarative.UnitTests/ChatClient/ChatClientAgentFactoryTests.cs
@@ -1,7 +1,13 @@
// Copyright (c) Microsoft. All rights reserved.
+using System;
+using System.Collections.Generic;
+using System.Threading;
using System.Threading.Tasks;
+using Microsoft.Agents.ObjectModel;
using Microsoft.Extensions.AI;
+using Microsoft.Extensions.Configuration;
+using Microsoft.PowerFx.Types;
using Moq;
namespace Microsoft.Agents.AI.Declarative.UnitTests.ChatClient;
@@ -104,4 +110,185 @@ public async Task TryCreateAsync_Creates_ToolsAsync()
var tools = chatClientAgent?.ChatOptions?.Tools;
Assert.Equal(5, tools?.Count);
}
+
+ [Fact]
+ public async Task Constructor_WithNullFunctions_CreatesAgentAsync()
+ {
+ // Arrange
+ var promptAgent = PromptAgents.CreateTestPromptAgent();
+ ChatClientPromptAgentFactory factory = new(this._mockChatClient.Object, null);
+
+ // Act
+ AIAgent? agent = await factory.TryCreateAsync(promptAgent);
+
+ // Assert
+ Assert.NotNull(agent);
+ }
+
+ [Fact]
+ public async Task TryCreateAsync_WithOptions_LoadsAllowedConfigurationAsync()
+ {
+ // Arrange
+ IConfiguration configuration = new ConfigurationBuilder()
+ .AddInMemoryCollection(new Dictionary
+ {
+ ["Temperature"] = "0.9",
+ ["TopP"] = "0.8",
+ ["OpenAIEndpoint"] = "https://example.openai.azure.com/",
+ ["OpenAIApiKey"] = "test-key",
+ })
+ .Build();
+ GptComponentMetadata promptAgent = AgentBotElementYaml.FromYaml(PromptAgents.AgentWithVariableReferences);
+ ChatClientPromptAgentFactory factory = new(
+ this._mockChatClient.Object,
+ configuration: configuration,
+ allowedConfigurationVariables: ["Temperature", "TopP", "OpenAIEndpoint", "OpenAIApiKey"]);
+
+ // Act
+ AIAgent? agent = await factory.TryCreateAsync(promptAgent);
+
+ // Assert
+ ChatClientAgent chatClientAgent = Assert.IsType(agent);
+ Assert.Equal(0.9F, chatClientAgent.ChatOptions?.Temperature);
+ Assert.Equal(0.8F, chatClientAgent.ChatOptions?.TopP);
+ }
+
+ [Fact]
+ public async Task TryCreateAsync_WithLegacyConfiguration_ThrowsAsync()
+ {
+ // Arrange
+ IConfiguration configuration = new ConfigurationBuilder()
+ .AddInMemoryCollection(new Dictionary
+ {
+ ["Temperature"] = "0.9",
+ ["TopP"] = "0.8",
+ ["OpenAIEndpoint"] = "https://example.openai.azure.com/",
+ ["OpenAIApiKey"] = "test-key",
+ })
+ .Build();
+ GptComponentMetadata promptAgent = AgentBotElementYaml.FromYaml(PromptAgents.AgentWithVariableReferences);
+ ChatClientPromptAgentFactory factory = new(this._mockChatClient.Object, configuration: configuration);
+
+ // Act
+ var exception = await Assert.ThrowsAsync(async () => await factory.TryCreateAsync(promptAgent));
+
+ // Assert
+ Assert.Contains("Name isn't valid. 'Temperature' isn't recognized.", exception.Message);
+ }
+
+ [Fact]
+ public async Task ProtectedConstructor_WithLegacyConfiguration_ThrowsAsync()
+ {
+ // Arrange
+ IConfiguration configuration = new ConfigurationBuilder()
+ .AddInMemoryCollection(new Dictionary
+ {
+ ["Temperature"] = "0.9",
+ ["TopP"] = "0.8",
+ ["SOME_SECRET"] = "secret-value",
+ })
+ .Build();
+ GptComponentMetadata promptAgent = AgentBotElementYaml.FromYaml(PromptAgents.AgentWithVariableReferences);
+ LegacyInspectingPromptAgentFactory factory = new(configuration);
+
+ // Act
+ await factory.TryCreateAsync(promptAgent);
+ var exception = await Assert.ThrowsAsync(async () => await factory.EvaluateAsync("Temperature"));
+
+ // Assert
+ Assert.Contains("Name isn't valid. 'Temperature' isn't recognized.", exception.Message);
+ }
+
+ [Fact]
+ public async Task CreateAsync_WithLegacyConfiguration_ThrowsCreateAsync()
+ {
+ // Arrange
+ IConfiguration configuration = new ConfigurationBuilder()
+ .AddInMemoryCollection(new Dictionary
+ {
+ ["Temperature"] = "0.9",
+ })
+ .Build();
+ GptComponentMetadata promptAgent = AgentBotElementYaml.FromYaml(PromptAgents.AgentWithVariableReferences);
+ CreateAsyncInspectingPromptAgentFactory factory = new(configuration, this._mockChatClient.Object);
+
+ // Act
+ var exception = await Assert.ThrowsAsync(async () => await factory.CreateAsync(promptAgent));
+
+ // Assert
+ Assert.Contains("Name isn't valid. 'Temperature' isn't recognized.", exception.Message);
+ }
+
+ [Fact]
+ public async Task TryCreateAsync_OnlyLoadsAllowedReferencedConfigurationAsync()
+ {
+ // Arrange
+ IConfiguration configuration = new ConfigurationBuilder()
+ .AddInMemoryCollection(new Dictionary
+ {
+ ["Temperature"] = "0.9",
+ ["TopP"] = "0.8",
+ })
+ .Build();
+ GptComponentMetadata promptAgent = AgentBotElementYaml.FromYaml(PromptAgents.AgentWithVariableReferences);
+ InspectingPromptAgentFactory factory = new(configuration, ["Temperature"]);
+
+ // Act
+ await factory.TryCreateAsync(promptAgent);
+
+ // Assert
+ StringValue temperature = Assert.IsType(await factory.EvaluateAsync("Temperature"));
+ Assert.Equal("0.9", temperature.Value);
+ Assert.False(factory.CanEvaluate("TopP"));
+ }
+
+ private sealed class InspectingPromptAgentFactory(IConfiguration configuration, IEnumerable allowedConfigurationVariables)
+ : PromptAgentFactory(engine: null, configuration: configuration, allowedConfigurationVariables: allowedConfigurationVariables)
+ {
+ public Task EvaluateAsync(string expression, CancellationToken cancellationToken = default) => this.Engine.EvalAsync(expression, cancellationToken);
+
+ public bool CanEvaluate(string expression) => this.Engine.Check(expression).IsSuccess;
+
+ public override Task TryCreateAsync(GptComponentMetadata promptAgent, CancellationToken cancellationToken = default)
+ {
+ // Arrange
+ this.InitializeConfigurationVariables(promptAgent);
+
+ // Act & Assert
+ return Task.FromResult(null);
+ }
+ }
+
+ private sealed class CreateAsyncInspectingPromptAgentFactory(IConfiguration configuration, IChatClient chatClient)
+ : PromptAgentFactory(engine: null, configuration: configuration)
+ {
+ public string? TemperatureValue { get; private set; }
+
+ public override async Task TryCreateAsync(GptComponentMetadata promptAgent, CancellationToken cancellationToken = default)
+ {
+ // Arrange
+ StringValue temperature = Assert.IsType(await this.Engine.EvalAsync("Temperature", cancellationToken));
+
+ // Act
+ this.TemperatureValue = temperature.Value;
+
+ // Assert
+ return new ChatClientAgent(chatClient);
+ }
+ }
+
+ private sealed class LegacyInspectingPromptAgentFactory(IConfiguration configuration)
+ : PromptAgentFactory(engine: null, configuration: configuration)
+ {
+ public Task EvaluateAsync(string expression, CancellationToken cancellationToken = default) => this.Engine.EvalAsync(expression, cancellationToken);
+
+ public override Task TryCreateAsync(GptComponentMetadata promptAgent, CancellationToken cancellationToken = default)
+ {
+ // Arrange
+ this.InitializeConfigurationVariables(promptAgent);
+
+ // Act & Assert
+ return Task.FromResult(null);
+ }
+ }
}
diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/DeclarativeWorkflowTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/DeclarativeWorkflowTest.cs
index 7bb92f913c2..1f702d52858 100644
--- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/DeclarativeWorkflowTest.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/DeclarativeWorkflowTest.cs
@@ -4,6 +4,7 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.AI.Workflows.Declarative.Interpreter;
@@ -11,6 +12,8 @@
using Microsoft.Agents.AI.Workflows.Declarative.PowerFx;
using Microsoft.Agents.ObjectModel;
using Microsoft.Extensions.AI;
+using Microsoft.Extensions.Configuration;
+using Microsoft.PowerFx.Types;
using Moq;
using Xunit.Sdk;
@@ -125,6 +128,196 @@ public async Task HostedWorkflowAgentIsolatesDeclarativeStateForImplicitSessions
Assert.NotEqual(provider.MessageConversations[0], provider.MessageConversations[1]);
}
+ [Fact]
+ public async Task Build_OnlyInitializesAllowedReferencedEnvironmentVariablesAsync()
+ {
+ // Arrange
+ const string AllowedName = "AllowedConfig";
+ const string HiddenName = "HiddenConfig";
+ const string ProcessOnlyName = "ProcessOnlyConfig";
+ const string ProcessOnlyValue = "process-value";
+
+ string? originalProcessOnlyValue = Environment.GetEnvironmentVariable(ProcessOnlyName);
+ Environment.SetEnvironmentVariable(ProcessOnlyName, ProcessOnlyValue);
+
+ try
+ {
+ IConfiguration configuration = new ConfigurationBuilder()
+ .AddInMemoryCollection(new Dictionary
+ {
+ [AllowedName] = "allowed-value",
+ [HiddenName] = "hidden-value",
+ })
+ .Build();
+ using StringReader yamlReader = new(
+ """
+ kind: Workflow
+ trigger:
+
+ kind: OnConversationStart
+ id: env_boundary_workflow
+ actions:
+
+ - kind: ConditionGroup
+ id: environment_boundary_condition
+ conditions:
+ - id: environment_boundary_passed
+ condition: =Env.AllowedConfig = "allowed-value"
+ actions:
+ - kind: SendActivity
+ id: environment_boundary_passed_activity
+ activity: allowed-configuration-available
+ elseActions:
+ - kind: SendActivity
+ id: environment_boundary_failed_activity
+ activity: allowed-configuration-missing
+
+ - kind: SetVariable
+ id: referenced_hidden_configuration
+ disabled: true
+ variable: Local.Hidden
+ value: =Env.HiddenConfig
+
+ - kind: SetVariable
+ id: referenced_process_configuration
+ disabled: true
+ variable: Local.ProcessOnly
+ value: =Env.ProcessOnlyConfig
+ """);
+ Mock mockAgentProvider = CreateMockProvider("Test input message");
+ DeclarativeWorkflowOptions options =
+ new(mockAgentProvider.Object)
+ {
+ Configuration = configuration,
+ AllowedEnvironmentVariables = [AllowedName, ProcessOnlyName],
+ LoggerFactory = this.Output,
+ };
+ Workflow workflow = DeclarativeWorkflowBuilder.Build(yamlReader, options);
+ WorkflowFormulaState rootState = GetRootState(workflow);
+
+ // Act
+ await using StreamingRun run = await InProcessExecution.RunStreamingAsync(workflow, "Test input message");
+
+ await foreach (WorkflowEvent workflowEvent in run.WatchStreamAsync())
+ {
+ this.WorkflowEvents.Add(workflowEvent);
+ if (workflowEvent is WorkflowErrorEvent errorEvent)
+ {
+ throw errorEvent.Data as Exception ?? new XunitException("Unexpected failure...");
+ }
+ }
+
+ // Assert
+ StringValue allowedValue = Assert.IsType(rootState.Get(AllowedName, VariableScopeNames.Environment));
+ Assert.Equal("allowed-value", allowedValue.Value);
+ Assert.IsType(rootState.Get(HiddenName, VariableScopeNames.Environment));
+ Assert.IsType(rootState.Get(ProcessOnlyName, VariableScopeNames.Environment));
+ this.AssertMessage("allowed-configuration-available");
+ this.AssertNotMessage("allowed-configuration-missing");
+ }
+ finally
+ {
+ Environment.SetEnvironmentVariable(ProcessOnlyName, originalProcessOnlyValue);
+ }
+ }
+
+ [Fact]
+ public async Task Build_WithProcessEnvironmentFallback_LoadsAllowedMissingConfigurationFromProcessEnvironmentAsync()
+ {
+ // Arrange
+ const string ProcessOnlyName = "ProcessOnlyConfigForFallback";
+ const string ExplicitName = "ExplicitConfigWinsForFallback";
+ const string HiddenName = "HiddenConfigForFallback";
+ const string ProcessOnlyValue = "process-only-value";
+ const string ExplicitConfigurationValue = "configuration-value";
+ const string ExplicitProcessValue = "process-value";
+ const string HiddenValue = "hidden-value";
+
+ string? originalProcessOnlyValue = Environment.GetEnvironmentVariable(ProcessOnlyName);
+ string? originalExplicitValue = Environment.GetEnvironmentVariable(ExplicitName);
+ string? originalHiddenValue = Environment.GetEnvironmentVariable(HiddenName);
+ Environment.SetEnvironmentVariable(ProcessOnlyName, ProcessOnlyValue);
+ Environment.SetEnvironmentVariable(ExplicitName, ExplicitProcessValue);
+ Environment.SetEnvironmentVariable(HiddenName, HiddenValue);
+
+ try
+ {
+ IConfiguration configuration = new ConfigurationBuilder()
+ .AddInMemoryCollection(new Dictionary
+ {
+ [ExplicitName] = ExplicitConfigurationValue,
+ })
+ .Build();
+ using StringReader yamlReader = new(
+ """
+ kind: Workflow
+ trigger:
+
+ kind: OnConversationStart
+ id: env_fallback_workflow
+ actions:
+
+ - kind: ConditionGroup
+ id: environment_fallback_condition
+ conditions:
+ - id: environment_fallback_passed
+ condition: =Env.ProcessOnlyConfigForFallback = "process-only-value" && Env.ExplicitConfigWinsForFallback = "configuration-value"
+ actions:
+ - kind: SendActivity
+ id: environment_fallback_passed_activity
+ activity: process-environment-fallback-enabled
+ elseActions:
+ - kind: SendActivity
+ id: environment_fallback_failed_activity
+ activity: process-environment-fallback-failed
+
+ - kind: SetVariable
+ id: referenced_hidden_process_environment
+ disabled: true
+ variable: Local.Hidden
+ value: =Env.HiddenConfigForFallback
+ """);
+ Mock mockAgentProvider = CreateMockProvider("Test input message");
+ DeclarativeWorkflowOptions options =
+ new(mockAgentProvider.Object)
+ {
+ Configuration = configuration,
+ AllowedEnvironmentVariables = [ProcessOnlyName, ExplicitName],
+ AllowProcessEnvironmentVariableFallback = true,
+ LoggerFactory = this.Output,
+ };
+ Workflow workflow = DeclarativeWorkflowBuilder.Build(yamlReader, options);
+ WorkflowFormulaState rootState = GetRootState(workflow);
+
+ // Act
+ await using StreamingRun run = await InProcessExecution.RunStreamingAsync(workflow, "Test input message");
+
+ await foreach (WorkflowEvent workflowEvent in run.WatchStreamAsync())
+ {
+ this.WorkflowEvents.Add(workflowEvent);
+ if (workflowEvent is WorkflowErrorEvent errorEvent)
+ {
+ throw errorEvent.Data as Exception ?? new XunitException("Unexpected failure...");
+ }
+ }
+
+ // Assert
+ StringValue processOnlyValue = Assert.IsType(rootState.Get(ProcessOnlyName, VariableScopeNames.Environment));
+ Assert.Equal(ProcessOnlyValue, processOnlyValue.Value);
+ StringValue explicitValue = Assert.IsType(rootState.Get(ExplicitName, VariableScopeNames.Environment));
+ Assert.Equal(ExplicitConfigurationValue, explicitValue.Value);
+ Assert.IsType(rootState.Get(HiddenName, VariableScopeNames.Environment));
+ this.AssertMessage("process-environment-fallback-enabled");
+ this.AssertNotMessage("process-environment-fallback-failed");
+ }
+ finally
+ {
+ Environment.SetEnvironmentVariable(ProcessOnlyName, originalProcessOnlyValue);
+ Environment.SetEnvironmentVariable(ExplicitName, originalExplicitValue);
+ Environment.SetEnvironmentVariable(HiddenName, originalHiddenValue);
+ }
+ }
+
[Fact]
public async Task GotoActionAsync()
{
@@ -372,6 +565,17 @@ private void AssertExecuted(string executorId, bool isAction = true, bool isDisc
private void AssertMessage(string message) =>
Assert.Contains(this.WorkflowEvents.OfType(), e => string.Equals(e.Message.Trim(), message, StringComparison.Ordinal));
+ private void AssertNotMessage(string message) =>
+ Assert.DoesNotContain(this.WorkflowEvents.OfType(), e => string.Equals(e.Message.Trim(), message, StringComparison.Ordinal));
+
+ private static WorkflowFormulaState GetRootState(Workflow workflow)
+ {
+ ExecutorBinding rootBinding = workflow.ReflectExecutors()[workflow.StartExecutorId];
+ Executor rootExecutor = Assert.IsAssignableFrom(rootBinding.RawValue);
+ FieldInfo stateField = Assert.Single(rootExecutor.GetType().GetFields(BindingFlags.Instance | BindingFlags.NonPublic), field => field.FieldType == typeof(WorkflowFormulaState));
+ return Assert.IsType(stateField.GetValue(rootExecutor));
+ }
+
private Task RunWorkflowAsync(string workflowPath) =>
this.RunWorkflowAsync(workflowPath, "Test input message");
diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Kit/IWorkflowContextExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Kit/IWorkflowContextExtensionsTests.cs
new file mode 100644
index 00000000000..d26a1b6a84b
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Kit/IWorkflowContextExtensionsTests.cs
@@ -0,0 +1,216 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Collections;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
+using Microsoft.Agents.AI.Workflows.Declarative.Interpreter;
+using Microsoft.Agents.AI.Workflows.Declarative.Kit;
+using Microsoft.Agents.AI.Workflows.Declarative.PowerFx;
+using Microsoft.Agents.ObjectModel;
+using Microsoft.Agents.ObjectModel.Abstractions;
+using Microsoft.PowerFx.Types;
+using Moq;
+
+namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.Kit;
+
+public sealed class IWorkflowContextExtensionsTests
+{
+ [Fact]
+ public async Task FormatTemplateAsync_WithSensitiveValue_ThrowsAsync()
+ {
+ // Arrange
+ WorkflowFormulaState state = new(RecalcEngineFactory.Create());
+ state.Set("SOME_SECRET", FormulaValue.New("secret-value"), VariableScopeNames.Environment, SensitivityLevel.Sensitive);
+ state.Bind();
+ DeclarativeWorkflowContext context = new(new Mock().Object, state);
+
+ // Act
+ ValueTask FormatAsync() => context.FormatTemplateAsync("={Env.SOME_SECRET}");
+
+ // Assert
+ DeclarativeActionException exception = await Assert.ThrowsAsync(async () => await FormatAsync());
+ Assert.Contains("Cannot return sensitive workflow expression value", exception.Message);
+ }
+
+ [Fact]
+ public async Task FormatTemplateWithSensitivityAsync_WithSensitiveValue_ReturnsSensitivityAsync()
+ {
+ // Arrange
+ WorkflowFormulaState state = new(RecalcEngineFactory.Create());
+ state.Set("SOME_SECRET", FormulaValue.New("secret-value"), VariableScopeNames.Environment, SensitivityLevel.Sensitive);
+ state.Bind();
+ DeclarativeWorkflowContext context = new(new Mock().Object, state);
+
+ // Act
+ EvaluationResult result = await context.FormatTemplateWithSensitivityAsync("={Env.SOME_SECRET}");
+
+ // Assert
+ Assert.Equal("=secret-value" + System.Environment.NewLine, result.Value);
+ Assert.Equal(SensitivityLevel.Sensitive, result.Sensitivity);
+ }
+
+ [Fact]
+ public async Task EvaluateValueAsync_WithSensitiveValue_ThrowsAsync()
+ {
+ // Arrange
+ WorkflowFormulaState state = new(RecalcEngineFactory.Create());
+ state.Set(SystemScope.Names.LastMessageText, FormulaValue.New("secret-value"), VariableScopeNames.System, SensitivityLevel.Sensitive);
+ state.Bind();
+ DeclarativeWorkflowContext context = new(new Mock().Object, state);
+
+ // Act
+ ValueTask