From e803079ebe35969035f8bac0f6b10a6001a93d62 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Wed, 9 Sep 2026 12:43:00 -0400 Subject: [PATCH 01/46] fix: use allow list for configuration keys Signed-off-by: Vincent Biret --- .../InvokeFoundryToolboxMcp/Program.cs | 1 + .../AgentBotElementYaml.cs | 35 +++++-- .../ChatClientPromptAgentFactory.cs | 38 +++++++- .../Extensions/YamlAgentFactoryExtensions.cs | 2 +- .../PromptAgentFactory.cs | 75 +++++++++++++-- .../DeclarativeWorkflowBuilder.cs | 6 +- .../DeclarativeWorkflowOptions.cs | 16 ++++ .../DeclarativeWorkflowOptionsExtensions.cs | 2 +- .../Extensions/IWorkflowContextExtensions.cs | 1 + .../Interpreter/DeclarativeActionExecutor.cs | 4 +- .../Kit/RootExecutor.cs | 14 ++- .../ObjectModel/QuestionExecutor.cs | 9 +- .../ObjectModel/SendActivityExecutor.cs | 9 +- .../SetMultipleVariablesExecutor.cs | 2 +- .../ObjectModel/SetTextVariableExecutor.cs | 5 +- .../ObjectModel/SetVariableExecutor.cs | 2 +- .../PowerFx/RecalcEngineFactory.cs | 8 +- .../PowerFx/WorkflowDiagnostics.cs | 31 ++++++- .../PowerFx/WorkflowExpressionEngine.cs | 91 ++++++++++++++++++- .../PowerFx/WorkflowFormulaState.cs | 29 +++++- .../PublicAPI/net10.0/PublicAPI.Unshipped.txt | 6 ++ .../PublicAPI/net472/PublicAPI.Unshipped.txt | 6 ++ .../PublicAPI/net8.0/PublicAPI.Unshipped.txt | 6 ++ .../PublicAPI/net9.0/PublicAPI.Unshipped.txt | 6 ++ .../netstandard2.0/PublicAPI.Unshipped.txt | 6 ++ .../Workflows/Execution/WorkflowFactory.cs | 3 + .../AgentBotElementYamlTests.cs | 5 +- .../ChatClient/ChatClientAgentFactoryTests.cs | 45 +++++++++ .../ObjectModel/SendActivityExecutorTest.cs | 20 ++++ .../PowerFx/RecalcEngineFactoryTests.cs | 6 +- .../PowerFx/WorkflowExpressionEngineTests.cs | 14 +++ 31 files changed, 448 insertions(+), 55 deletions(-) diff --git a/dotnet/samples/03-workflows/Declarative/InvokeFoundryToolboxMcp/Program.cs b/dotnet/samples/03-workflows/Declarative/InvokeFoundryToolboxMcp/Program.cs index 812d13cf7a2..0d35ad0056f 100644 --- a/dotnet/samples/03-workflows/Declarative/InvokeFoundryToolboxMcp/Program.cs +++ b/dotnet/samples/03-workflows/Declarative/InvokeFoundryToolboxMcp/Program.cs @@ -103,6 +103,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..907779341d5 100644 --- a/dotnet/src/Microsoft.Agents.AI.Declarative/ChatClient/ChatClientPromptAgentFactory.cs +++ b/dotnet/src/Microsoft.Agents.AI.Declarative/ChatClient/ChatClientPromptAgentFactory.cs @@ -20,7 +20,41 @@ 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, allowedConfigurationVariables: null, functions, engine, configuration, loggerFactory) + { + } + + /// + /// Creates a new instance of the class. + /// + /// The chat client used by created agents. + /// Configuration keys that may be exposed to Power Fx when the agent definition references them through Env. + /// 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 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. + public ChatClientPromptAgentFactory( + IChatClient chatClient, + IEnumerable? allowedConfigurationVariables, + IList? functions = null, + RecalcEngine? engine = null, + IConfiguration? configuration = null, + ILoggerFactory? loggerFactory = null, + int? maximumExpressionLength = null, + int? maximumCallDepth = null) : base(engine, configuration, allowedConfigurationVariables, maximumExpressionLength, maximumCallDepth) { Throw.IfNull(chatClient); @@ -34,6 +68,8 @@ public ChatClientPromptAgentFactory(IChatClient chatClient, IList? f { Throw.IfNull(promptAgent); + this.InitializeConfigurationVariables(promptAgent); + var options = new ChatClientAgentOptions() { Name = promptAgent.Name, 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..6e5c0a82edc 100644 --- a/dotnet/src/Microsoft.Agents.AI.Declarative/PromptAgentFactory.cs +++ b/dotnet/src/Microsoft.Agents.AI.Declarative/PromptAgentFactory.cs @@ -1,11 +1,14 @@ // Copyright (c) Microsoft. All rights reserved. using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Threading; using System.Threading.Tasks; using Microsoft.Agents.ObjectModel; using Microsoft.Extensions.Configuration; using Microsoft.PowerFx; +using Microsoft.PowerFx.Types; using Microsoft.Shared.Diagnostics; namespace Microsoft.Agents.AI; @@ -15,30 +18,86 @@ namespace Microsoft.Agents.AI; /// public abstract class PromptAgentFactory { + private const int DefaultMaximumExpressionLength = 10000; + + private readonly IConfiguration? _configuration; + private readonly HashSet _allowedConfigurationVariables; + /// /// Initializes a new instance of the class. /// /// Optional , if none is provided a default instance will be created. - /// Optional configuration to be added as variables to the . + /// Optional configuration used to resolve explicitly allowed environment variables referenced by the agent definition. protected PromptAgentFactory(RecalcEngine? engine = null, IConfiguration? configuration = null) + : this(engine, configuration, allowedConfigurationVariables: null) { - this.Engine = engine ?? new RecalcEngine(); + } - if (configuration is not null) + /// + /// 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) { - foreach (var kvp in configuration.AsEnumerable()) - { - this.Engine.UpdateVariable(kvp.Key, kvp.Value ?? string.Empty); - } + MaximumExpressionLength = maximumExpressionLength ?? DefaultMaximumExpressionLength, + }; + + if (maximumCallDepth is not null) + { + 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)) + { + if (this._allowedConfigurationVariables.Contains(variableName)) + { + this.Engine.UpdateVariable(variableName, this._configuration[variableName] ?? string.Empty); + } + } + } + + [RequiresDynamicCode("Calls YamlDotNet.Serialization.DeserializerBuilder.DeserializerBuilder()")] + internal GptComponentMetadata FromYaml(string text) => + AgentBotElementYaml.FromYaml(text, this._configuration, this._allowedConfigurationVariables); + /// /// Create a from the specified . /// diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/DeclarativeWorkflowBuilder.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/DeclarativeWorkflowBuilder.cs index aeb3d2b6e95..c12f62d3c6b 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); DeclarativeWorkflowExecutor rootExecutor = new(rootId, options, diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/DeclarativeWorkflowOptions.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/DeclarativeWorkflowOptions.cs index 90439402dbd..a41196de29c 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/DeclarativeWorkflowOptions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/DeclarativeWorkflowOptions.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using System; +using System.Collections.Generic; using System.Diagnostics; using Microsoft.Agents.AI.Workflows.Observability; using Microsoft.Extensions.Configuration; @@ -37,6 +38,16 @@ public sealed class DeclarativeWorkflowOptions(ResponseAgentProvider agentProvid /// public IConfiguration? Configuration { get; init; } + /// + /// Gets the configuration or process environment variable names that may be exposed through the workflow Env scope. + /// + public IEnumerable? AllowedEnvironmentVariables { get; init; } + + /// + /// Gets a value indicating whether the workflow may fall back to process environment variables for allowed Env names missing from . + /// + public bool AllowProcessEnvironmentVariableFallback { get; init; } + /// /// Optionally identifies a continued workflow conversation. /// @@ -52,6 +63,11 @@ public sealed class DeclarativeWorkflowOptions(ResponseAgentProvider agentProvid /// public int? MaximumExpressionLength { get; init; } + /// + /// Gets a value indicating whether the Power Fx Set function is enabled. + /// + public bool EnableSetFunction { get; init; } + /// /// Gets the used to create loggers for workflow components. /// diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Extensions/DeclarativeWorkflowOptionsExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Extensions/DeclarativeWorkflowOptionsExtensions.cs index 1e1c52ab887..b4ce4587c2b 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Extensions/DeclarativeWorkflowOptionsExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Extensions/DeclarativeWorkflowOptionsExtensions.cs @@ -10,5 +10,5 @@ internal static class DeclarativeWorkflowOptionsExtensions private const int DefaultMaximumExpressionLength = 10000; public static RecalcEngine CreateRecalcEngine(this DeclarativeWorkflowOptions? context) => - RecalcEngineFactory.Create(context?.MaximumExpressionLength ?? DefaultMaximumExpressionLength, context?.MaximumCallDepth); + RecalcEngineFactory.Create(context?.MaximumExpressionLength ?? DefaultMaximumExpressionLength, context?.MaximumCallDepth, context?.EnableSetFunction ?? false); } diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Extensions/IWorkflowContextExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Extensions/IWorkflowContextExtensions.cs index 1b92235eee3..b2bb5cac79d 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Extensions/IWorkflowContextExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Extensions/IWorkflowContextExtensions.cs @@ -42,6 +42,7 @@ public static async ValueTask QueueEnvironmentUpdateAsync(this IWorkflow { DeclarativeWorkflowContext declarativeContext = DeclarativeContext(context); await declarativeContext.UpdateStateAsync(key, value, VariableScopeNames.Environment, allowSystem: true, cancellationToken).ConfigureAwait(false); + declarativeContext.State.SetSensitivity(key, VariableScopeNames.Environment, SensitivityLevel.Sensitive); 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 776caebda60..72bdc4d753e 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Interpreter/DeclarativeActionExecutor.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Interpreter/DeclarativeActionExecutor.cs @@ -124,7 +124,7 @@ public override async ValueTask HandleAsync(ActionExecutorResult message, IWorkf protected override ValueTask OnCheckpointRestoredAsync(IWorkflowContext context, CancellationToken cancellationToken = default) => 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) { @@ -132,6 +132,8 @@ protected async ValueTask AssignAsync(PropertyPath? targetPath, FormulaValue res } await context.QueueStateUpdateAsync(targetPath, result).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(); diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Kit/RootExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Kit/RootExecutor.cs index 80f6e69b60d..62a1c732d24 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Kit/RootExecutor.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Kit/RootExecutor.cs @@ -21,6 +21,7 @@ public abstract class RootExecutor : Executor, IResettableExecut private readonly ResponseAgentProvider _agentProvider; private readonly WorkflowFormulaState _state; private readonly Func? _inputTransform; + private readonly bool _allowProcessEnvironmentVariableFallback; private string? _conversationId; @@ -42,6 +43,7 @@ protected RootExecutor(string id, DeclarativeWorkflowOptions options, Func - /// Initializes the specified variables from if available; - /// otherwise falls back to the process environment variables. + /// Initializes the specified variables from if available. + /// Process environment variables are used only when enabled by . /// /// The workflow execution context providing messaging and state services. /// The set of variable names to initialize. @@ -104,12 +106,8 @@ protected async ValueTask InitializeEnvironmentAsync(IWorkflowContext context, p 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/QuestionExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/QuestionExecutor.cs index 4ad88dd40cb..b21803706b7 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; @@ -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..72589d6467e 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/SendActivityExecutor.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/SendActivityExecutor.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; namespace Microsoft.Agents.AI.Workflows.Declarative.ObjectModel; @@ -18,7 +19,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..00e88ac5e69 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/SetTextVariableExecutor.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/SetTextVariableExecutor.cs @@ -6,6 +6,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.PowerFx.Types; using Microsoft.Shared.Diagnostics; @@ -19,9 +20,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..7eb2b583a89 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PowerFx/RecalcEngineFactory.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PowerFx/RecalcEngineFactory.cs @@ -11,7 +11,8 @@ internal static class RecalcEngineFactory { public static RecalcEngine Create( int? maximumExpressionLength = null, - int? maximumCallDepth = null) + int? maximumCallDepth = null, + bool enableSetFunction = false) { RecalcEngine engine = new(CreateConfig()); @@ -37,7 +38,10 @@ PowerFxConfig CreateConfig() config.MaxCallDepth = maximumCallDepth.Value; } - config.EnableSetFunction(); + if (enableSetFunction) + { + 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..39dfc7efa54 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.OrdinalIgnoreCase); 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..82b1a21e0a0 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PowerFx/WorkflowExpressionEngine.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PowerFx/WorkflowExpressionEngine.cs @@ -10,16 +10,19 @@ using Microsoft.PowerFx; using Microsoft.PowerFx.Types; using Microsoft.Shared.Diagnostics; +using System.Text.RegularExpressions; namespace Microsoft.Agents.AI.Workflows.Declarative.PowerFx; internal sealed class WorkflowExpressionEngine { - private readonly RecalcEngine _engine; + private static readonly Regex s_scopedVariableReference = new(@"\b(?:(?[A-Za-z][A-Za-z0-9_]*)\.)?(?[A-Za-z_][A-Za-z0-9_]*)\b", RegexOptions.Compiled); - public WorkflowExpressionEngine(RecalcEngine engine) + private readonly WorkflowFormulaState _state; + + public WorkflowExpressionEngine(WorkflowFormulaState state) { - this._engine = engine; + this._state = state; } public EvaluationResult GetValue(BoolExpression boolean) => this.Evaluate(boolean); @@ -41,6 +44,57 @@ 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 (TemplateLine line in template) + { + EvaluationResult result = this.Format(line); + 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 (TemplateSegment segment in line.Segments) + { + EvaluationResult result = this.Format(segment); + 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 +328,40 @@ private EvaluationResult EvaluateScope(ExpressionBase expression) expression.VariableReference?.ToString() : expression.ExpressionText; - FormulaValue result = this._engine.Eval(expressionText); + FormulaValue result = this._state.Engine.Eval(expressionText); if (result is ErrorValue errorValue) { throw new DeclarativeActionException(errorValue.Format()); } - return new(result, SensitivityLevel.None); + return new(result, GetSensitivity(expression)); } + + private SensitivityLevel GetSensitivity(ExpressionBase expression) + { + if (expression.VariableReference is { VariableName: string variableName }) + { + return this._state.GetSensitivity(variableName, expression.VariableReference.NamespaceAlias); + } + + string? expressionText = expression.ExpressionText; + if (string.IsNullOrWhiteSpace(expressionText)) + { + return SensitivityLevel.None; + } + + SensitivityLevel sensitivity = SensitivityLevel.None; + foreach (Match match in s_scopedVariableReference.Matches(expressionText)) + { + string? scopeName = match.Groups["scope"].Success ? match.Groups["scope"].Value : null; + string referencedName = match.Groups["name"].Value; + sensitivity = MaxSensitivity(sensitivity, this._state.GetSensitivity(referencedName, scopeName)); + } + + return sensitivity; + } + + 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 c739cb3bf95..41d48610db8 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PowerFx/WorkflowFormulaState.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PowerFx/WorkflowFormulaState.cs @@ -40,7 +40,7 @@ public WorkflowFormulaState(RecalcEngine engine) this._scopes = VariableScopeNames.AllScopes.ToDictionary(scopeName => GetScopeName(scopeName), _ => new WorkflowScope()); this.Engine = engine; - this.Evaluator = new WorkflowExpressionEngine(engine); + this.Evaluator = new WorkflowExpressionEngine(this); this.Bind(); } @@ -56,8 +56,26 @@ 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 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; @@ -143,5 +161,8 @@ public static string GetScopeName(string? scopeName) /// /// The set of variables for a specific action scope. /// - private sealed class WorkflowScope : Dictionary; + private sealed class WorkflowScope : Dictionary + { + public Dictionary Sensitivities { get; } = []; + } } 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..ff5197a66ba 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,7 @@ #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 +Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.EnableSetFunction.get -> bool +Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.EnableSetFunction.init -> void 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..ff5197a66ba 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,7 @@ #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 +Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.EnableSetFunction.get -> bool +Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.EnableSetFunction.init -> void 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..ff5197a66ba 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,7 @@ #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 +Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.EnableSetFunction.get -> bool +Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.EnableSetFunction.init -> void 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..ff5197a66ba 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,7 @@ #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 +Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.EnableSetFunction.get -> bool +Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.EnableSetFunction.init -> void 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..ff5197a66ba 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,7 @@ #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 +Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.EnableSetFunction.get -> bool +Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.EnableSetFunction.init -> void 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..a0cfdc94acd 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Declarative.UnitTests/AgentBotElementYamlTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Declarative.UnitTests/AgentBotElementYamlTests.cs @@ -234,7 +234,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); 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..dfeecf68a4b 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,12 @@ // Copyright (c) Microsoft. All rights reserved. using System.Threading.Tasks; +using System.Collections.Generic; +using System.Threading; +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 +109,44 @@ public async Task TryCreateAsync_Creates_ToolsAsync() var tools = chatClientAgent?.ChatOptions?.Tools; Assert.Equal(5, tools?.Count); } + + [Fact] + public async Task TryCreateAsync_OnlyLoadsAllowedReferencedConfigurationAsync() + { + // Arrange + IConfiguration configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + ["Temperature"] = "0.9", + ["SOME_SECRET"] = "secret-value", + }) + .Build(); + GptComponentMetadata promptAgent = AgentBotElementYaml.FromYaml(PromptAgents.AgentWithVariableReferences); + InspectingPromptAgentFactory factory = new(configuration, ["Temperature"]); + + // Act + await factory.TryCreateAsync(promptAgent); + + // Assert + StringValue temperature = Assert.IsType(factory.Evaluate("Temperature")); + Assert.Equal("0.9", temperature.Value); + Assert.False(factory.CanEvaluate("SOME_SECRET")); + } + + private sealed class InspectingPromptAgentFactory(IConfiguration configuration, IEnumerable allowedConfigurationVariables) + : PromptAgentFactory(engine: null, configuration: configuration, allowedConfigurationVariables: allowedConfigurationVariables) + { + public FormulaValue Evaluate(string expression) => this.Engine.Eval(expression); + + 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); + } + } } diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/SendActivityExecutorTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/SendActivityExecutorTest.cs index bfddd1b8f0d..4a12f4528f9 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/SendActivityExecutorTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/SendActivityExecutorTest.cs @@ -5,6 +5,7 @@ using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel; using Microsoft.Agents.ObjectModel; using Microsoft.Extensions.AI; +using Microsoft.PowerFx.Types; namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.ObjectModel; @@ -43,6 +44,25 @@ public async Task CaptureActivityAsync() Assert.Equal(message.MessageId, updateEvent.Update.MessageId); } + [Fact] + public async Task CaptureActivity_WithSensitiveEnvironmentValue_ThrowsAsync() + { + // Arrange + this.State.Set("SOME_SECRET", FormulaValue.New("secret-value"), VariableScopeNames.Environment, SensitivityLevel.Sensitive); + SendActivity model = + this.CreateModel( + this.FormatDisplayName(nameof(CaptureActivity_WithSensitiveEnvironmentValue_ThrowsAsync)), + "={Env.SOME_SECRET}"); + + // Act + SendActivityExecutor action = new(model, this.State); + Task ExecuteAsync() => this.ExecuteAsync(action); + + // Assert + DeclarativeActionException exception = await Assert.ThrowsAsync(ExecuteAsync); + Assert.Contains("Cannot send sensitive activity text", exception.Message); + } + private SendActivity CreateModel(string displayName, string activityMessage, string? summary = null) { MessageActivityTemplate.Builder activityBuilder = diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/PowerFx/RecalcEngineFactoryTests.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/PowerFx/RecalcEngineFactoryTests.cs index d158ca552b2..07923d57cca 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/PowerFx/RecalcEngineFactoryTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/PowerFx/RecalcEngineFactoryTests.cs @@ -32,16 +32,16 @@ public void NewInstanceEachTime() } [Fact] - public void HasSetFunctionEnabled() + public void SetFunctionDisabledByDefault() { // Arrange RecalcEngine engine = RecalcEngineFactory.Create(); // Act - CheckResult result = engine.Check("1+1"); + CheckResult result = engine.Check("Set(MyVariable, 1)"); // Assert - Assert.True(result.IsSuccess); + Assert.False(result.IsSuccess); } [Fact] diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/PowerFx/WorkflowExpressionEngineTests.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/PowerFx/WorkflowExpressionEngineTests.cs index ebaaf5d0466..ed3a4e9f165 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/PowerFx/WorkflowExpressionEngineTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/PowerFx/WorkflowExpressionEngineTests.cs @@ -123,6 +123,20 @@ public void StringExpressionGetValueForVariable() expectedValue: "Hello World"); } + [Fact] + public void StringExpressionGetValueForEnvironmentVariableIsSensitive() + { + // Arrange + this.State.Set("SOME_SECRET", FormulaValue.New("secret-value"), VariableScopeNames.Environment, SensitivityLevel.Sensitive); + this.State.Bind(); + + // Act & Assert + this.EvaluateExpression( + StringExpression.Variable(PropertyPath.Create("Env.SOME_SECRET")), + expectedValue: "secret-value", + expectedSensitivity: SensitivityLevel.Sensitive); + } + [Fact] public void StringExpressionGetValueForFormula() => // Arrange, Act & Assert From d2842d459fc54543e00c9a8225b8f83931b2b150 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Wed, 9 Sep 2026 13:25:39 -0400 Subject: [PATCH 02/46] Apply batched suggestions from code review Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> --- .../Microsoft.Agents.AI.Declarative/PromptAgentFactory.cs | 8 +++----- .../PowerFx/WorkflowExpressionEngine.cs | 4 ++-- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative/PromptAgentFactory.cs b/dotnet/src/Microsoft.Agents.AI.Declarative/PromptAgentFactory.cs index 6e5c0a82edc..afee5a16519 100644 --- a/dotnet/src/Microsoft.Agents.AI.Declarative/PromptAgentFactory.cs +++ b/dotnet/src/Microsoft.Agents.AI.Declarative/PromptAgentFactory.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.Agents.ObjectModel; @@ -85,12 +86,9 @@ protected void InitializeConfigurationVariables(GptComponentMetadata promptAgent return; } - foreach (string variableName in AgentBotElementYaml.GetReferencedEnvironmentVariableNames(promptAgent)) + foreach (string variableName in AgentBotElementYaml.GetReferencedEnvironmentVariableNames(promptAgent).Where(variableName => this._allowedConfigurationVariables.Contains(variableName))) { - if (this._allowedConfigurationVariables.Contains(variableName)) - { - this.Engine.UpdateVariable(variableName, this._configuration[variableName] ?? string.Empty); - } + this.Engine.UpdateVariable(variableName, this._configuration[variableName] ?? string.Empty); } } 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 82b1a21e0a0..34d2dfdfd0d 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PowerFx/WorkflowExpressionEngine.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PowerFx/WorkflowExpressionEngine.cs @@ -3,6 +3,7 @@ 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; @@ -50,9 +51,8 @@ public EvaluationResult Format(IEnumerable template) SensitivityLevel sensitivity = SensitivityLevel.None; List segments = []; - foreach (TemplateLine line in template) + foreach (EvaluationResult result in template.Select(this.Format)) { - EvaluationResult result = this.Format(line); sensitivity = MaxSensitivity(sensitivity, result.Sensitivity); segments.Add(result.Value); } From 9508b4b836b1a0b1ccad588ed3180849ffd8d826 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Wed, 9 Sep 2026 13:38:17 -0400 Subject: [PATCH 03/46] chore: formatting --- .../src/Microsoft.Agents.AI.Declarative/PromptAgentFactory.cs | 1 - .../ObjectModel/SendActivityExecutor.cs | 1 - .../ObjectModel/SetTextVariableExecutor.cs | 1 - .../PowerFx/WorkflowExpressionEngine.cs | 4 ++-- 4 files changed, 2 insertions(+), 5 deletions(-) diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative/PromptAgentFactory.cs b/dotnet/src/Microsoft.Agents.AI.Declarative/PromptAgentFactory.cs index afee5a16519..d16424c4411 100644 --- a/dotnet/src/Microsoft.Agents.AI.Declarative/PromptAgentFactory.cs +++ b/dotnet/src/Microsoft.Agents.AI.Declarative/PromptAgentFactory.cs @@ -9,7 +9,6 @@ using Microsoft.Agents.ObjectModel; using Microsoft.Extensions.Configuration; using Microsoft.PowerFx; -using Microsoft.PowerFx.Types; using Microsoft.Shared.Diagnostics; namespace Microsoft.Agents.AI; 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 72589d6467e..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,7 +3,6 @@ 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; 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 00e88ac5e69..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,7 +2,6 @@ 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; 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 34d2dfdfd0d..845b385944f 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PowerFx/WorkflowExpressionEngine.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PowerFx/WorkflowExpressionEngine.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; +using System.Text.RegularExpressions; using Microsoft.Agents.AI.Workflows.Declarative.Extensions; using Microsoft.Agents.ObjectModel; using Microsoft.Agents.ObjectModel.Abstractions; @@ -11,7 +12,6 @@ using Microsoft.PowerFx; using Microsoft.PowerFx.Types; using Microsoft.Shared.Diagnostics; -using System.Text.RegularExpressions; namespace Microsoft.Agents.AI.Workflows.Declarative.PowerFx; @@ -335,7 +335,7 @@ private EvaluationResult EvaluateScope(ExpressionBase expression) throw new DeclarativeActionException(errorValue.Format()); } - return new(result, GetSensitivity(expression)); + return new(result, this.GetSensitivity(expression)); } private SensitivityLevel GetSensitivity(ExpressionBase expression) From 2e5089ef033af8eb225ec9d58995e07c60a3dbd6 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Wed, 9 Sep 2026 13:39:54 -0400 Subject: [PATCH 04/46] Potential fix for pull request finding 'Missed opportunity to use Select' Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> --- .../PowerFx/WorkflowExpressionEngine.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 845b385944f..a8fa746177c 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PowerFx/WorkflowExpressionEngine.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PowerFx/WorkflowExpressionEngine.cs @@ -69,9 +69,8 @@ public EvaluationResult Format(TemplateLine? line) SensitivityLevel sensitivity = SensitivityLevel.None; List segments = []; - foreach (TemplateSegment segment in line.Segments) + foreach (EvaluationResult result in line.Segments.Select(this.Format)) { - EvaluationResult result = this.Format(segment); sensitivity = MaxSensitivity(sensitivity, result.Sensitivity); segments.Add(result.Value); } From 181ce0d76ba82f96501045e7edb4215681505ad9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 9 Sep 2026 17:39:59 +0000 Subject: [PATCH 05/46] Address declarative workflow review feedback Co-authored-by: baywet <7905502+baywet@users.noreply.github.com> --- .../ChatClientPromptAgentFactory.cs | 66 ++++++++++++++----- .../Extensions/IWorkflowContextExtensions.cs | 25 ++++++- .../Interpreter/DeclarativeActionExecutor.cs | 4 +- .../Interpreter/DeclarativeWorkflowContext.cs | 50 +++++++++----- .../Kit/IWorkflowContextExtensions.cs | 14 +++- .../Kit/RootExecutor.cs | 8 ++- .../PowerFx/WorkflowExpressionEngine.cs | 37 +++++++++-- .../PowerFx/WorkflowFormulaState.cs | 9 ++- .../ChatClient/ChatClientAgentFactoryTests.cs | 46 +++++++++++++ .../Kit/IWorkflowContextExtensionsTests.cs | 33 ++++++++++ .../Kit/RootExecutorTests.cs | 58 ++++++++++++++++ .../PowerFx/WorkflowExpressionEngineTests.cs | 27 ++++++++ .../PowerFx/WorkflowFormulaStateTests.cs | 24 +++++++ 13 files changed, 357 insertions(+), 44 deletions(-) create mode 100644 dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Kit/IWorkflowContextExtensionsTests.cs create mode 100644 dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Kit/RootExecutorTests.cs diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative/ChatClient/ChatClientPromptAgentFactory.cs b/dotnet/src/Microsoft.Agents.AI.Declarative/ChatClient/ChatClientPromptAgentFactory.cs index 907779341d5..e061f1f9276 100644 --- a/dotnet/src/Microsoft.Agents.AI.Declarative/ChatClient/ChatClientPromptAgentFactory.cs +++ b/dotnet/src/Microsoft.Agents.AI.Declarative/ChatClient/ChatClientPromptAgentFactory.cs @@ -31,7 +31,15 @@ public ChatClientPromptAgentFactory( RecalcEngine? engine = null, IConfiguration? configuration = null, ILoggerFactory? loggerFactory = null) - : this(chatClient, allowedConfigurationVariables: null, functions, engine, configuration, loggerFactory) + : this( + chatClient, + functions, + new ChatClientPromptAgentFactoryOptions() + { + Engine = engine, + Configuration = configuration, + LoggerFactory = loggerFactory, + }) { } @@ -39,28 +47,20 @@ public ChatClientPromptAgentFactory( /// Creates a new instance of the class. /// /// The chat client used by created agents. - /// Configuration keys that may be exposed to Power Fx when the agent definition references them through Env. /// 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 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. + /// Options used to configure the created agents and declarative expression evaluation. public ChatClientPromptAgentFactory( IChatClient chatClient, - IEnumerable? allowedConfigurationVariables, - IList? functions = null, - RecalcEngine? engine = null, - IConfiguration? configuration = null, - ILoggerFactory? loggerFactory = null, - int? maximumExpressionLength = null, - int? maximumCallDepth = null) : base(engine, configuration, allowedConfigurationVariables, maximumExpressionLength, maximumCallDepth) + IList? functions, + ChatClientPromptAgentFactoryOptions options) : + base(options?.Engine, options?.Configuration, options?.AllowedConfigurationVariables, options?.MaximumExpressionLength, options?.MaximumCallDepth) { Throw.IfNull(chatClient); + Throw.IfNull(options); this._chatClient = chatClient; this._functions = functions; - this._loggerFactory = loggerFactory; + this._loggerFactory = options.LoggerFactory; } /// @@ -89,3 +89,39 @@ public ChatClientPromptAgentFactory( private readonly ILoggerFactory? _loggerFactory; #endregion } + +/// +/// Options for configuring . +/// +public sealed class ChatClientPromptAgentFactoryOptions +{ + /// + /// Gets or sets configuration keys that may be exposed to Power Fx when the agent definition references them through Env. + /// + public IEnumerable? AllowedConfigurationVariables { get; init; } + + /// + /// Gets or sets an optional Power Fx engine used to evaluate declarative expressions. + /// + public RecalcEngine? Engine { get; init; } + + /// + /// Gets or sets optional configuration used to resolve explicitly allowed environment variables referenced by the agent definition. + /// + public IConfiguration? Configuration { get; init; } + + /// + /// Gets or sets an optional logger factory used by created agents. + /// + public ILoggerFactory? LoggerFactory { get; init; } + + /// + /// Gets or sets an optional maximum length for Power Fx expressions evaluated by the factory-created engine. + /// + public int? MaximumExpressionLength { get; init; } + + /// + /// Gets or sets an optional maximum nested call depth for Power Fx expressions evaluated by the factory-created engine. + /// + public int? MaximumCallDepth { get; init; } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Extensions/IWorkflowContextExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Extensions/IWorkflowContextExtensions.cs index b2bb5cac79d..57da281e2c2 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Extensions/IWorkflowContextExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Extensions/IWorkflowContextExtensions.cs @@ -38,18 +38,37 @@ 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) => + DeclarativeContext(context).UpdateStateAsync( + Throw.IfNull(variablePath.VariableName), + value, + Throw.IfNull(variablePath.NamespaceAlias), + allowSystem: false, + sensitivity: sensitivity, + cancellationToken: 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); - declarativeContext.State.SetSensitivity(key, VariableScopeNames.Environment, SensitivityLevel.Sensitive); + 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 72bdc4d753e..93c29f80722 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Interpreter/DeclarativeActionExecutor.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Interpreter/DeclarativeActionExecutor.cs @@ -131,12 +131,12 @@ protected async ValueTask AssignAsync(PropertyPath? targetPath, FormulaValue res 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..404d013debc 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,7 @@ 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(); } @@ -137,7 +137,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 +171,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)) + { + 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..0656c4ded6e 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Kit/IWorkflowContextExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Kit/IWorkflowContextExtensions.cs @@ -58,7 +58,9 @@ public static async ValueTask FormatTemplateAsync(this IWorkflowContext StringBuilder builder = new(); foreach (string line in lines) { - builder.AppendLine(state.Engine.Format(TemplateLine.Parse(line))); + EvaluationResult result = state.Evaluator.Format(TemplateLine.Parse(line)); + ThrowIfSensitive(result.Sensitivity); + builder.AppendLine(result.Value); } return builder.ToString(); @@ -86,6 +88,7 @@ public static async ValueTask FormatTemplateAsync(this IWorkflowContext WorkflowFormulaState state = await context.GetStateAsync(cancellationToken).ConfigureAwait(false); EvaluationResult result = state.Evaluator.GetValue(ValueExpression.Expression(expression)); + ThrowIfSensitive(result.Sensitivity); return (TValue?)result.Value.ToObject(); } @@ -103,6 +106,7 @@ public static async ValueTask FormatTemplateAsync(this IWorkflowContext WorkflowFormulaState state = await context.GetStateAsync(cancellationToken).ConfigureAwait(false); EvaluationResult result = state.Evaluator.GetValue(ValueExpression.Expression(expression)); + ThrowIfSensitive(result.Sensitivity); return result.Value.AsList(); } @@ -164,4 +168,12 @@ private static async Task GetStateAsync(this IWorkflowCont return state; } + + private static void ThrowIfSensitive(SensitivityLevel sensitivity) + { + if (sensitivity == SensitivityLevel.Sensitive) + { + throw new DeclarativeActionException("Cannot return sensitive workflow expression value."); + } + } } diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Kit/RootExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Kit/RootExecutor.cs index 62a1c732d24..4d018b72888 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Kit/RootExecutor.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Kit/RootExecutor.cs @@ -1,6 +1,9 @@ // Copyright (c) Microsoft. All rights reserved. using System; +using System.Collections.Frozen; +using System.Collections.Generic; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.Agents.AI.Workflows.Declarative.Extensions; @@ -22,6 +25,7 @@ public abstract class RootExecutor : Executor, IResettableExecut private readonly WorkflowFormulaState _state; private readonly Func? _inputTransform; private readonly bool _allowProcessEnvironmentVariableFallback; + private readonly FrozenSet _allowedEnvironmentVariables; private string? _conversationId; @@ -44,6 +48,7 @@ protected RootExecutor(string id, DeclarativeWorkflowOptions options, Func /// Initializes the specified variables from if available. + /// Only names included in are initialized. /// Process environment variables are used only when enabled by . /// /// The workflow execution context providing messaging and state services. @@ -99,7 +105,7 @@ public override async ValueTask HandleAsync(TInput message, IWorkflowContext con /// 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); } 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 a8fa746177c..38b34afd000 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PowerFx/WorkflowExpressionEngine.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PowerFx/WorkflowExpressionEngine.cs @@ -10,6 +10,7 @@ using Microsoft.Agents.ObjectModel.Abstractions; using Microsoft.Agents.ObjectModel.Exceptions; using Microsoft.PowerFx; +using Microsoft.PowerFx.Core.Texl.Intellisense; using Microsoft.PowerFx.Types; using Microsoft.Shared.Diagnostics; @@ -17,7 +18,22 @@ namespace Microsoft.Agents.AI.Workflows.Declarative.PowerFx; internal sealed class WorkflowExpressionEngine { - private static readonly Regex s_scopedVariableReference = new(@"\b(?:(?[A-Za-z][A-Za-z0-9_]*)\.)?(?[A-Za-z_][A-Za-z0-9_]*)\b", RegexOptions.Compiled); + private static readonly TokenType[] s_nonReferenceTokenTypes = + [ + TokenType.BoolLit, + TokenType.Comment, + TokenType.DecLit, + TokenType.Delimiter, + TokenType.Function, + TokenType.NumLit, + TokenType.StrLit, + TokenType.UnaryOp, + TokenType.BinaryOp, + TokenType.VariadicOp, + TokenType.Punctuator, + TokenType.Self, + TokenType.Parent, + ]; private readonly WorkflowFormulaState _state; @@ -350,17 +366,28 @@ private SensitivityLevel GetSensitivity(ExpressionBase expression) return SensitivityLevel.None; } + TokenTextSpan[] tokens = this._state.Engine.Check(expressionText).GetTextTokens(s_nonReferenceTokenTypes).ToArray(); + SensitivityLevel sensitivity = SensitivityLevel.None; - foreach (Match match in s_scopedVariableReference.Matches(expressionText)) + for (int index = 0; index < tokens.Length; index++) { - string? scopeName = match.Groups["scope"].Success ? match.Groups["scope"].Value : null; - string referencedName = match.Groups["name"].Value; - sensitivity = MaxSensitivity(sensitivity, this._state.GetSensitivity(referencedName, scopeName)); + TokenTextSpan token = tokens[index]; + if (index + 1 < tokens.Length && IsDotSeparated(token, tokens[index + 1])) + { + TokenTextSpan nameToken = tokens[index + 1]; + sensitivity = MaxSensitivity(sensitivity, this._state.GetSensitivity(nameToken.TokenName, token.TokenName)); + index++; + continue; + } + + sensitivity = MaxSensitivity(sensitivity, this._state.GetSensitivity(token.TokenName)); } return sensitivity; } + private static bool IsDotSeparated(TokenTextSpan left, TokenTextSpan right) => left.EndIndex < right.StartIndex && right.StartIndex - left.EndIndex <= 1; + 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 41d48610db8..b75c592f952 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PowerFx/WorkflowFormulaState.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PowerFx/WorkflowFormulaState.cs @@ -27,6 +27,8 @@ internal sealed class WorkflowFormulaState VariableScopeNames.System, ]; + private const string SensitivityScopePrefix = "__Microsoft_Agents_AI_Workflows_Declarative_Sensitivity:"; + private readonly Dictionary _scopes; private int _isInitialized; @@ -97,13 +99,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}"); } @@ -142,6 +145,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(); 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 dfeecf68a4b..151add3eadd 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Declarative.UnitTests/ChatClient/ChatClientAgentFactoryTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Declarative.UnitTests/ChatClient/ChatClientAgentFactoryTests.cs @@ -110,6 +110,52 @@ public async Task TryCreateAsync_Creates_ToolsAsync() 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, + functions: null, + options: new ChatClientPromptAgentFactoryOptions() + { + 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_OnlyLoadsAllowedReferencedConfigurationAsync() { 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..454510266d9 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Kit/IWorkflowContextExtensionsTests.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Agents.AI.Workflows; +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.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); + } +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Kit/RootExecutorTests.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Kit/RootExecutorTests.cs new file mode 100644 index 00000000000..49faffd29b7 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Kit/RootExecutorTests.cs @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Agents.AI.Workflows; +using Microsoft.Agents.AI.Workflows.Declarative.Interpreter; +using Microsoft.Agents.AI.Workflows.Declarative.Kit; +using Microsoft.Agents.ObjectModel; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.Configuration; +using Moq; + +namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.Kit; + +public sealed class RootExecutorTests +{ + [Fact] + public async Task InitializeEnvironmentAsync_OnlyQueuesAllowedVariablesAsync() + { + // Arrange + IConfiguration configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + ["ALLOWED"] = "allowed-value", + ["HIDDEN"] = "hidden-value", + }) + .Build(); + DeclarativeWorkflowOptions options = + new(new MockAgentProvider().Object) + { + Configuration = configuration, + AllowedEnvironmentVariables = ["ALLOWED"], + }; + TestRootExecutor executor = new(options); + Mock sourceContext = new(MockBehavior.Strict); + sourceContext.Setup(c => c.QueueStateUpdateAsync("ALLOWED", It.IsAny(), VariableScopeNames.Environment, It.IsAny())) + .Returns(default(ValueTask)); + + DeclarativeWorkflowContext context = new(sourceContext.Object, executor.Session.State); + + // Act + await executor.InitializeAsync(context, "ALLOWED", "HIDDEN"); + + // Assert + sourceContext.Verify(c => c.QueueStateUpdateAsync("ALLOWED", It.IsAny(), VariableScopeNames.Environment, It.IsAny()), Times.Once); + sourceContext.Verify(c => c.QueueStateUpdateAsync("HIDDEN", It.IsAny(), VariableScopeNames.Environment, It.IsAny()), Times.Never); + } + + private sealed class TestRootExecutor(DeclarativeWorkflowOptions options) : RootExecutor("test_root", options, inputTransform: null) + { + public ValueTask InitializeAsync(IWorkflowContext context, params string[] variableNames) => + this.InitializeEnvironmentAsync(context, variableNames); + + protected override ValueTask ExecuteAsync(string message, IWorkflowContext context, CancellationToken cancellationToken = default) => + default; + } +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/PowerFx/WorkflowExpressionEngineTests.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/PowerFx/WorkflowExpressionEngineTests.cs index ed3a4e9f165..6d64bdfc6b6 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/PowerFx/WorkflowExpressionEngineTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/PowerFx/WorkflowExpressionEngineTests.cs @@ -137,6 +137,33 @@ public void StringExpressionGetValueForEnvironmentVariableIsSensitive() expectedSensitivity: SensitivityLevel.Sensitive); } + [Fact] + public void StringExpressionGetValueForQuotedEnvironmentVariableIsSensitive() + { + // Arrange + this.State.Set("API-KEY", FormulaValue.New("secret-value"), VariableScopeNames.Environment, SensitivityLevel.Sensitive); + this.State.Bind(); + + // Act & Assert + this.EvaluateExpression( + StringExpression.Expression("Env.'API-KEY'"), + expectedValue: "secret-value", + expectedSensitivity: SensitivityLevel.Sensitive); + } + + [Fact] + public void StringExpressionGetValueForEnvironmentVariableTextLiteralIsNotSensitive() + { + // Arrange + this.State.Set("SOME_SECRET", FormulaValue.New("secret-value"), VariableScopeNames.Environment, SensitivityLevel.Sensitive); + this.State.Bind(); + + // Act & Assert + this.EvaluateExpression( + StringExpression.Expression(@"Concatenate(""Env.SOME_SECRET"", "" literal"")"), + expectedValue: "Env.SOME_SECRET literal"); + } + [Fact] public void StringExpressionGetValueForFormula() => // Arrange, Act & Assert diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/PowerFx/WorkflowFormulaStateTests.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/PowerFx/WorkflowFormulaStateTests.cs index 5193296db4d..d964e5f8420 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/PowerFx/WorkflowFormulaStateTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/PowerFx/WorkflowFormulaStateTests.cs @@ -1,8 +1,13 @@ // Copyright (c) Microsoft. All rights reserved. +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Agents.AI.Workflows; using Microsoft.Agents.AI.Workflows.Declarative.PowerFx; using Microsoft.Agents.ObjectModel; using Microsoft.PowerFx.Types; +using Moq; namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.PowerFx; @@ -81,4 +86,23 @@ public void SetOverwritesExistingValue() FormulaValue result = this.State.Get("key1"); Assert.Equal(newValue, result); } + + [Fact] + public async Task RestoreAsync_RestoresPersistedSensitivityAsync() + { + // Arrange + Mock context = new(MockBehavior.Strict); + context.Setup(c => c.ReadStateKeysAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync((string? scopeName, CancellationToken _) => scopeName == VariableScopeNames.Local ? new HashSet { "secret" } : []); + context.Setup(c => c.ReadStateAsync("secret", VariableScopeNames.Local, It.IsAny())) + .ReturnsAsync(new PortableValue("secret-value")); + context.Setup(c => c.ReadStateAsync("secret", WorkflowFormulaState.GetSensitivityScopeName(VariableScopeNames.Local), It.IsAny())) + .ReturnsAsync(SensitivityLevel.Sensitive); + + // Act + await this.State.RestoreAsync(context.Object, CancellationToken.None); + + // Assert + Assert.Equal(SensitivityLevel.Sensitive, this.State.GetSensitivity("secret")); + } } From 4d42029e169f9a7554457b71b28c62f840980c86 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Wed, 9 Sep 2026 13:46:27 -0400 Subject: [PATCH 06/46] .NET: address workflow sensitivity review comments Persist workflow sensitivity metadata for checkpoint restore, propagate sensitive default question values, and derive expression sensitivity from Power Fx syntax rather than raw text scanning. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ObjectModel/QuestionExecutor.cs | 6 +- .../PowerFx/WorkflowExpressionEngine.cs | 130 +++++++++++++----- .../PowerFx/WorkflowFormulaState.cs | 1 + .../ObjectModel/QuestionExecutorTest.cs | 28 +++- 4 files changed, 129 insertions(+), 36 deletions(-) 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 b21803706b7..dacdb9151f6 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/QuestionExecutor.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/QuestionExecutor.cs @@ -162,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. 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 38b34afd000..05404e91f37 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PowerFx/WorkflowExpressionEngine.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PowerFx/WorkflowExpressionEngine.cs @@ -4,13 +4,12 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; -using System.Text.RegularExpressions; 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.Core.Texl.Intellisense; +using Microsoft.PowerFx.Syntax; using Microsoft.PowerFx.Types; using Microsoft.Shared.Diagnostics; @@ -18,23 +17,6 @@ namespace Microsoft.Agents.AI.Workflows.Declarative.PowerFx; internal sealed class WorkflowExpressionEngine { - private static readonly TokenType[] s_nonReferenceTokenTypes = - [ - TokenType.BoolLit, - TokenType.Comment, - TokenType.DecLit, - TokenType.Delimiter, - TokenType.Function, - TokenType.NumLit, - TokenType.StrLit, - TokenType.UnaryOp, - TokenType.BinaryOp, - TokenType.VariadicOp, - TokenType.Punctuator, - TokenType.Self, - TokenType.Parent, - ]; - private readonly WorkflowFormulaState _state; public WorkflowExpressionEngine(WorkflowFormulaState state) @@ -366,27 +348,111 @@ private SensitivityLevel GetSensitivity(ExpressionBase expression) return SensitivityLevel.None; } - TokenTextSpan[] tokens = this._state.Engine.Check(expressionText).GetTextTokens(s_nonReferenceTokenTypes).ToArray(); + CheckResult checkResult = this._state.Engine.Check(expressionText); + checkResult.ThrowOnErrors(); SensitivityLevel sensitivity = SensitivityLevel.None; - for (int index = 0; index < tokens.Length; index++) + foreach ((string? ScopeName, string VariableName) reference in GetVariableReferences(checkResult.Parse.Root)) { - TokenTextSpan token = tokens[index]; - if (index + 1 < tokens.Length && IsDotSeparated(token, tokens[index + 1])) - { - TokenTextSpan nameToken = tokens[index + 1]; - sensitivity = MaxSensitivity(sensitivity, this._state.GetSensitivity(nameToken.TokenName, token.TokenName)); - index++; - continue; - } - - sensitivity = MaxSensitivity(sensitivity, this._state.GetSensitivity(token.TokenName)); + sensitivity = MaxSensitivity(sensitivity, this._state.GetSensitivity(reference.VariableName, reference.ScopeName)); } return sensitivity; } - private static bool IsDotSeparated(TokenTextSpan left, TokenTextSpan right) => left.EndIndex < right.StartIndex && right.StartIndex - left.EndIndex <= 1; + 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 FirstNameNode firstNameNode) + { + names.Add(firstNameNode.Ident.Name.Value); + } + + names.Reverse(); + if (names.Count == 0) + { + reference = default; + return false; + } + + 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 b75c592f952..8f661e4928a 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; diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/QuestionExecutorTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/QuestionExecutorTest.cs index dbe056f891e..b5cd92c4c2c 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/QuestionExecutorTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/QuestionExecutorTest.cs @@ -228,6 +228,27 @@ await this.CaptureResponseTestAsync( expectResponse: false); } + [Fact] + public async Task QuestionCaptureResponseExceedingRepeatCountPreservesDefaultValueSensitivityAsync() + { + // Arrange + this.State.Set("SOME_SECRET", FormulaValue.New("secret-value"), VariableScopeNames.Environment, SensitivityLevel.Sensitive); + this.State.Bind(); + Question model = this.CreateModel( + displayName: nameof(QuestionCaptureResponseExceedingRepeatCountPreservesDefaultValueSensitivityAsync), + variableName: "TestVariable", + repeatCount: 0, + defaultValueExpressionText: "Env.SOME_SECRET"); + + // Act & Assert + await this.CaptureResponseTestAsync( + model, + variableName: "TestVariable", + responseText: null, + expectResponse: false); + Assert.Equal(SensitivityLevel.Sensitive, this.State.GetSensitivity("TestVariable")); + } + [Fact] public async Task QuestionCaptureResponseWithAutoSendFalseAsync() { @@ -438,7 +459,8 @@ private Question CreateModel( SkipQuestionMode? skipMode = null, int? repeatCount = null, EntityReference? entity = null, - DataValue? autoSend = null) + DataValue? autoSend = null, + string? defaultValueExpressionText = null) { BoolExpression.Builder? alwaysPromptExpression = null; if (alwaysPrompt is not null) @@ -457,6 +479,10 @@ private Question CreateModel( { defaultValueExpression = ValueExpression.Literal(defaultValue).ToBuilder(); } + else if (defaultValueExpressionText is not null) + { + defaultValueExpression = ValueExpression.Expression(defaultValueExpressionText).ToBuilder(); + } EnumExpression.Builder? skipModeExpression = null; if (skipMode is not null) From 935532d36e892381d639cbf8392fe4ed450b6941 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Wed, 9 Sep 2026 13:57:30 -0400 Subject: [PATCH 07/46] .NET: fix ChatClient factory options validation Validate factory options before the base constructor initializer uses them so null handling remains explicit without nullable dereferences. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 668ac22f-5312-4df7-b470-55ca8535862d --- .../ChatClient/ChatClientPromptAgentFactory.cs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative/ChatClient/ChatClientPromptAgentFactory.cs b/dotnet/src/Microsoft.Agents.AI.Declarative/ChatClient/ChatClientPromptAgentFactory.cs index e061f1f9276..c57049acfc6 100644 --- a/dotnet/src/Microsoft.Agents.AI.Declarative/ChatClient/ChatClientPromptAgentFactory.cs +++ b/dotnet/src/Microsoft.Agents.AI.Declarative/ChatClient/ChatClientPromptAgentFactory.cs @@ -53,10 +53,19 @@ public ChatClientPromptAgentFactory( IChatClient chatClient, IList? functions, ChatClientPromptAgentFactoryOptions options) : - base(options?.Engine, options?.Configuration, options?.AllowedConfigurationVariables, options?.MaximumExpressionLength, options?.MaximumCallDepth) + this(chatClient, functions, ValidateOptions(options), isValidated: true) { + } + + private ChatClientPromptAgentFactory( + IChatClient chatClient, + IList? functions, + ChatClientPromptAgentFactoryOptions options, + bool isValidated) : + base(options.Engine, options.Configuration, options.AllowedConfigurationVariables, options.MaximumExpressionLength, options.MaximumCallDepth) + { + _ = isValidated; Throw.IfNull(chatClient); - Throw.IfNull(options); this._chatClient = chatClient; this._functions = functions; @@ -87,6 +96,9 @@ public ChatClientPromptAgentFactory( private readonly IChatClient _chatClient; private readonly IList? _functions; private readonly ILoggerFactory? _loggerFactory; + + private static ChatClientPromptAgentFactoryOptions ValidateOptions(ChatClientPromptAgentFactoryOptions? options) => + Throw.IfNull(options); #endregion } From c1d1611a59368f5cf7d02c45ff05d0f842be1188 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Wed, 9 Sep 2026 14:03:22 -0400 Subject: [PATCH 08/46] chore: formatting --- .../Kit/RootExecutor.cs | 1 - .../PowerFx/WorkflowFormulaState.cs | 1 - .../Kit/IWorkflowContextExtensionsTests.cs | 2 -- .../Kit/RootExecutorTests.cs | 2 -- .../PowerFx/WorkflowFormulaStateTests.cs | 1 - 5 files changed, 7 deletions(-) diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Kit/RootExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Kit/RootExecutor.cs index 4d018b72888..d5c7273a3f5 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Kit/RootExecutor.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Kit/RootExecutor.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Frozen; -using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; 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 8f661e4928a..b75c592f952 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PowerFx/WorkflowFormulaState.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PowerFx/WorkflowFormulaState.cs @@ -1,6 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. -using System; using System.Collections.Frozen; using System.Collections.Generic; using System.Diagnostics; 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 index 454510266d9..ec2fd8e13be 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Kit/IWorkflowContextExtensionsTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Kit/IWorkflowContextExtensionsTests.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. -using System.Threading; using System.Threading.Tasks; -using Microsoft.Agents.AI.Workflows; using Microsoft.Agents.AI.Workflows.Declarative.Interpreter; using Microsoft.Agents.AI.Workflows.Declarative.Kit; using Microsoft.Agents.AI.Workflows.Declarative.PowerFx; diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Kit/RootExecutorTests.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Kit/RootExecutorTests.cs index 49faffd29b7..376f3b93bd4 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Kit/RootExecutorTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Kit/RootExecutorTests.cs @@ -3,11 +3,9 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using Microsoft.Agents.AI.Workflows; using Microsoft.Agents.AI.Workflows.Declarative.Interpreter; using Microsoft.Agents.AI.Workflows.Declarative.Kit; using Microsoft.Agents.ObjectModel; -using Microsoft.Extensions.AI; using Microsoft.Extensions.Configuration; using Moq; diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/PowerFx/WorkflowFormulaStateTests.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/PowerFx/WorkflowFormulaStateTests.cs index d964e5f8420..0af699c501c 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/PowerFx/WorkflowFormulaStateTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/PowerFx/WorkflowFormulaStateTests.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using Microsoft.Agents.AI.Workflows; using Microsoft.Agents.AI.Workflows.Declarative.PowerFx; using Microsoft.Agents.ObjectModel; using Microsoft.PowerFx.Types; From e0e80921656af7f37216117f3857f0fa1e6ccfda Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 9 Sep 2026 18:26:30 +0000 Subject: [PATCH 09/46] fix review feedback on declarative sensitivity APIs Co-authored-by: baywet <7905502+baywet@users.noreply.github.com> --- .../ChatClientPromptAgentFactory.cs | 16 +++--- .../Extensions/IWorkflowContextExtensions.cs | 7 ++- .../Interpreter/DeclarativeWorkflowContext.cs | 11 +++++ .../Kit/IWorkflowContextExtensions.cs | 49 +++++++++++++++++++ .../PublicAPI/net10.0/PublicAPI.Unshipped.txt | 2 + .../PublicAPI/net472/PublicAPI.Unshipped.txt | 2 + .../PublicAPI/net8.0/PublicAPI.Unshipped.txt | 2 + .../PublicAPI/net9.0/PublicAPI.Unshipped.txt | 2 + .../netstandard2.0/PublicAPI.Unshipped.txt | 2 + .../ChatClient/ChatClientAgentFactoryTests.cs | 5 +- .../Kit/IWorkflowContextExtensionsTests.cs | 41 ++++++++++++++++ .../Workflows/SendActivity.cs | 4 +- 12 files changed, 126 insertions(+), 17 deletions(-) diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative/ChatClient/ChatClientPromptAgentFactory.cs b/dotnet/src/Microsoft.Agents.AI.Declarative/ChatClient/ChatClientPromptAgentFactory.cs index c57049acfc6..a43f75f7b50 100644 --- a/dotnet/src/Microsoft.Agents.AI.Declarative/ChatClient/ChatClientPromptAgentFactory.cs +++ b/dotnet/src/Microsoft.Agents.AI.Declarative/ChatClient/ChatClientPromptAgentFactory.cs @@ -39,7 +39,8 @@ public ChatClientPromptAgentFactory( Engine = engine, Configuration = configuration, LoggerFactory = loggerFactory, - }) + }, + isValidated: true) { } @@ -47,15 +48,14 @@ public ChatClientPromptAgentFactory( /// Creates a new instance of the class. /// /// The chat client used by created agents. - /// Optional functions exposed as tools to created agents. /// Options used to configure the created agents and declarative expression evaluation. - public ChatClientPromptAgentFactory( + /// Optional functions exposed as tools to created agents. + /// The configured instance. + public static ChatClientPromptAgentFactory Create( IChatClient chatClient, - IList? functions, - ChatClientPromptAgentFactoryOptions options) : - this(chatClient, functions, ValidateOptions(options), isValidated: true) - { - } + ChatClientPromptAgentFactoryOptions options, + IList? functions = null) => + new(chatClient, functions, ValidateOptions(options), isValidated: true); private ChatClientPromptAgentFactory( IChatClient chatClient, 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 57da281e2c2..942136c12af 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Extensions/IWorkflowContextExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Extensions/IWorkflowContextExtensions.cs @@ -44,13 +44,12 @@ public static ValueTask QueueStateUpdateAsync( TValue? value, SensitivityLevel sensitivity, CancellationToken cancellationToken = default) => - DeclarativeContext(context).UpdateStateAsync( + DeclarativeContext(context).QueueStateUpdateAsync( Throw.IfNull(variablePath.VariableName), value, Throw.IfNull(variablePath.NamespaceAlias), - allowSystem: false, - sensitivity: sensitivity, - cancellationToken: cancellationToken); + sensitivity, + cancellationToken); public static async ValueTask QueueEnvironmentUpdateAsync(this IWorkflowContext context, string key, TValue? value, CancellationToken cancellationToken = default) { 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 404d013debc..b3e61ea0907 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Interpreter/DeclarativeWorkflowContext.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Interpreter/DeclarativeWorkflowContext.cs @@ -77,6 +77,17 @@ public async ValueTask QueueStateUpdateAsync(string key, T? value, string? sc 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(); + } + private static bool IsManagedScope(string? scopeName) => scopeName is not null && VariableScopeNames.IsValidName(scopeName); /// 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 0656c4ded6e..0b7be39e40f 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Kit/IWorkflowContextExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Kit/IWorkflowContextExtensions.cs @@ -111,6 +111,55 @@ public static async ValueTask FormatTemplateAsync(this IWorkflowContext 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) + { + TValue? value = await context.ReadStateAsync(key, scopeName, cancellationToken).ConfigureAwait(false); + SensitivityLevel sensitivity = context is DeclarativeWorkflowContext declarativeContext + ? declarativeContext.State.GetSensitivity(key, scopeName) + : 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) + { + await declarativeContext.QueueStateUpdateAsync(key, value.Value, scopeName, value.Sensitivity, cancellationToken).ConfigureAwait(false); + return; + } + + await context.QueueStateUpdateAsync(key, value.Value, scopeName, cancellationToken).ConfigureAwait(false); + } + /// /// Convert the result of an expression to the specified target type. /// 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 ff5197a66ba..47f9ea9f7f2 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 @@ -5,3 +5,5 @@ Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.AllowedEnvi Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.AllowedEnvironmentVariables.init -> void Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.EnableSetFunction.get -> bool Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.EnableSetFunction.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!> 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 ff5197a66ba..47f9ea9f7f2 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 @@ -5,3 +5,5 @@ Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.AllowedEnvi Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.AllowedEnvironmentVariables.init -> void Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.EnableSetFunction.get -> bool Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.EnableSetFunction.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!> 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 ff5197a66ba..47f9ea9f7f2 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 @@ -5,3 +5,5 @@ Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.AllowedEnvi Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.AllowedEnvironmentVariables.init -> void Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.EnableSetFunction.get -> bool Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.EnableSetFunction.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!> 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 ff5197a66ba..47f9ea9f7f2 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 @@ -5,3 +5,5 @@ Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.AllowedEnvi Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.AllowedEnvironmentVariables.init -> void Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.EnableSetFunction.get -> bool Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.EnableSetFunction.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!> 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 ff5197a66ba..47f9ea9f7f2 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 @@ -5,3 +5,5 @@ Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.AllowedEnvi Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.AllowedEnvironmentVariables.init -> void Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.EnableSetFunction.get -> bool Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.EnableSetFunction.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!> 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 151add3eadd..8f8c5b16fab 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Declarative.UnitTests/ChatClient/ChatClientAgentFactoryTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Declarative.UnitTests/ChatClient/ChatClientAgentFactoryTests.cs @@ -1,8 +1,8 @@ // Copyright (c) Microsoft. All rights reserved. -using System.Threading.Tasks; using System.Collections.Generic; using System.Threading; +using System.Threading.Tasks; using Microsoft.Agents.ObjectModel; using Microsoft.Extensions.AI; using Microsoft.Extensions.Configuration; @@ -138,9 +138,8 @@ public async Task TryCreateAsync_WithOptions_LoadsAllowedConfigurationAsync() }) .Build(); GptComponentMetadata promptAgent = AgentBotElementYaml.FromYaml(PromptAgents.AgentWithVariableReferences); - ChatClientPromptAgentFactory factory = new( + ChatClientPromptAgentFactory factory = ChatClientPromptAgentFactory.Create( this._mockChatClient.Object, - functions: null, options: new ChatClientPromptAgentFactoryOptions() { Configuration = configuration, 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 index ec2fd8e13be..142c97fa591 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Kit/IWorkflowContextExtensionsTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Kit/IWorkflowContextExtensionsTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. 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; @@ -28,4 +29,44 @@ public async Task FormatTemplateAsync_WithSensitiveValue_ThrowsAsync() DeclarativeActionException exception = await Assert.ThrowsAsync(async () => await FormatAsync()); Assert.Contains("Cannot return sensitive workflow expression value", exception.Message); } + + [Fact] + public async Task QueueStateUpdateAsync_WithSensitivity_RebindsStateAsync() + { + // Arrange + WorkflowFormulaState state = new(RecalcEngineFactory.Create()); + state.Set("TestValue", FormulaValue.New("old-value")); + state.Bind(); + DeclarativeWorkflowContext context = new(new Mock().Object, state); + + // Act + await context.QueueStateUpdateAsync(PropertyPath.Create("Local.TestValue"), FormulaValue.New("new-value"), SensitivityLevel.Sensitive); + + // Assert + Assert.Equal("new-value", state.Engine.Eval("Local.TestValue").ToObject()); + Assert.Equal(SensitivityLevel.Sensitive, state.GetSensitivity("TestValue", VariableScopeNames.Local)); + } + + [Fact] + public async Task ReadStateWithSensitivityAsync_QueuesSensitiveAssignmentAsync() + { + // Arrange + WorkflowFormulaState state = new(RecalcEngineFactory.Create()); + state.Set(SystemScope.Names.LastMessageText, FormulaValue.New("secret-value"), VariableScopeNames.System, SensitivityLevel.Sensitive); + state.Bind(); + + Mock source = new(MockBehavior.Loose); + source + .Setup(c => c.ReadStateAsync(SystemScope.Names.LastMessageText, VariableScopeNames.System, default)) + .Returns(new ValueTask("secret-value")); + DeclarativeWorkflowContext context = new(source.Object, state); + + // Act + var evaluatedValue = await context.ReadStateWithSensitivityAsync(SystemScope.Names.LastMessageText, VariableScopeNames.System); + await context.QueueStateUpdateWithSensitivityAsync("TestValue", evaluatedValue, VariableScopeNames.Local); + + // Assert + Assert.Equal("secret-value", state.Engine.Eval("Local.TestValue").ToObject()); + Assert.Equal(SensitivityLevel.Sensitive, state.GetSensitivity("TestValue", VariableScopeNames.Local)); + } } diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Workflows/SendActivity.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Workflows/SendActivity.cs index 05cd29c5744..864871af418 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Workflows/SendActivity.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Workflows/SendActivity.cs @@ -56,8 +56,8 @@ internal sealed class SetInputExecutor(FormulaSession session) : ActionExecutor( // protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) { - object? evaluatedValue = await context.ReadStateAsync(key: "LastMessageText", scopeName: "System").ConfigureAwait(false); - await context.QueueStateUpdateAsync(key: "TestValue", value: evaluatedValue, scopeName: "Local").ConfigureAwait(false); + var evaluatedValue = await context.ReadStateWithSensitivityAsync(key: "LastMessageText", scopeName: "System").ConfigureAwait(false); + await context.QueueStateUpdateWithSensitivityAsync(key: "TestValue", value: evaluatedValue, scopeName: "Local").ConfigureAwait(false); return default; } From c77957e2c7f53413302e069e9f8c58964c0ec87b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 9 Sep 2026 19:01:11 +0000 Subject: [PATCH 10/46] fix declarative review feedback Co-authored-by: baywet <7905502+baywet@users.noreply.github.com> --- .../Interpreter/DeclarativeActionExecutor.cs | 16 ++--- .../Kit/IWorkflowContextExtensions.cs | 25 +++++--- .../AddConversationMessageExecutor.cs | 17 ++++- .../ObjectModel/EditTableExecutor.cs | 33 ++++++---- .../ObjectModel/EditTableV2Executor.cs | 27 +++++--- .../ObjectModel/ForeachExecutor.cs | 15 ++++- .../ObjectModel/ParseValueExecutor.cs | 2 +- .../AddConversationMessageExecutorTest.cs | 62 ++++++++++++++++++- .../PowerFx/RecalcEngineFactoryTests.cs | 4 +- .../Workflows/SendActivity.cs | 2 +- 10 files changed, 154 insertions(+), 49 deletions(-) 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 93c29f80722..c758407b1f0 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; @@ -88,7 +88,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) @@ -122,7 +122,7 @@ 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, SensitivityLevel sensitivity = SensitivityLevel.None) { @@ -133,7 +133,7 @@ protected async ValueTask AssignAsync(PropertyPath? targetPath, FormulaValue res 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); + this.State.SetSensitivity(variableName, targetPath.NamespaceAlias, sensitivity); #if DEBUG string? resultValue = sensitivity == SensitivityLevel.Sensitive ? "" : result.Format(); 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 0b7be39e40f..3eaa51860f8 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Kit/IWorkflowContextExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Kit/IWorkflowContextExtensions.cs @@ -117,7 +117,7 @@ public static async ValueTask FormatTemplateAsync(this IWorkflowContext /// 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. + /// 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( @@ -126,12 +126,16 @@ public static async ValueTask FormatTemplateAsync(this IWorkflowContext string? scopeName = null, CancellationToken cancellationToken = default) { - TValue? value = await context.ReadStateAsync(key, scopeName, cancellationToken).ConfigureAwait(false); - SensitivityLevel sensitivity = context is DeclarativeWorkflowContext declarativeContext - ? declarativeContext.State.GetSensitivity(key, scopeName) - : SensitivityLevel.None; + 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); + } - return new(value, sensitivity); + TValue? value = await context.ReadStateAsync(key, scopeName, cancellationToken).ConfigureAwait(false); + return new(value, SensitivityLevel.None); } /// @@ -141,7 +145,7 @@ public static async ValueTask FormatTemplateAsync(this IWorkflowContext /// 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. + /// 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( @@ -153,7 +157,8 @@ public static async ValueTask QueueStateUpdateWithSensitivityAsync( { if (context is DeclarativeWorkflowContext declarativeContext) { - await declarativeContext.QueueStateUpdateAsync(key, value.Value, scopeName, value.Sensitivity, cancellationToken).ConfigureAwait(false); + string effectiveScopeName = scopeName ?? WorkflowFormulaState.DefaultScopeName; + await declarativeContext.QueueStateUpdateAsync(key, value.Value, effectiveScopeName, value.Sensitivity, cancellationToken).ConfigureAwait(false); return; } @@ -180,7 +185,7 @@ public static async ValueTask QueueStateUpdateWithSensitivityAsync( /// 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 ConvertValueAsync(this IWorkflowContext context, VariableType targetType, string key, string? scopeName = null, CancellationToken cancellationToken = default) @@ -195,7 +200,7 @@ public static async ValueTask QueueStateUpdateWithSensitivityAsync( /// The type of the list element. /// The workflow execution context used to restore persisted state prior to formatting. /// 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 evaluated list expression public static async ValueTask?> ReadListAsync(this IWorkflowContext context, string key, string? scopeName = null, CancellationToken cancellationToken = default) 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/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/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/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/AddConversationMessageExecutorTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/AddConversationMessageExecutorTest.cs index 2f89de4dee5..abef7277cb0 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/AddConversationMessageExecutorTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/AddConversationMessageExecutorTest.cs @@ -70,6 +70,62 @@ await this.ExecuteTestAsync( metadata: metadataRecord); } + [Fact] + public async Task AddMessageWithSensitiveContentThrowsAsync() + { + // Arrange + this.State.Set("SOME_SECRET", FormulaValue.New("secret-value"), VariableScopeNames.Environment, SensitivityLevel.Sensitive); + MockAgentProvider mockAgentProvider = new(); + int messageCount = mockAgentProvider.TestMessages.Count; + AddConversationMessage model = + this.CreateModel( + this.FormatDisplayName(nameof(AddMessageWithSensitiveContentThrowsAsync)), + FormatVariablePath("TestMessage"), + "TestConversationId", + AgentMessageRoleWrapper.Get(AgentMessageRole.User), + "={Env.SOME_SECRET}", + metadata: null); + + AddConversationMessageExecutor action = new(model, mockAgentProvider.Object, this.State); + Task ExecuteAsync() => this.ExecuteAsync(action); + + // Act & Assert + DeclarativeActionException exception = await Assert.ThrowsAsync(ExecuteAsync); + Assert.Contains("Cannot send sensitive conversation message content", exception.Message); + Assert.Equal(messageCount, mockAgentProvider.TestMessages.Count); + } + + [Fact] + public async Task AddMessageWithSensitiveMetadataThrowsAsync() + { + // Arrange + Dictionary metadataValues = + new() + { + ["Key1"] = "secret-value", + }; + this.State.Set("SecretMetadata", metadataValues.ToRecordValue().ToFormula(), sensitivity: SensitivityLevel.Sensitive); + MockAgentProvider mockAgentProvider = new(); + int messageCount = mockAgentProvider.TestMessages.Count; + AddConversationMessage model = + this.CreateModel( + this.FormatDisplayName(nameof(AddMessageWithSensitiveMetadataThrowsAsync)), + FormatVariablePath("TestMessage"), + "TestConversationId", + AgentMessageRoleWrapper.Get(AgentMessageRole.User), + "Hello", + metadata: null, + ObjectExpression.Variable(PropertyPath.TopicVariable("SecretMetadata")).ToBuilder()); + + AddConversationMessageExecutor action = new(model, mockAgentProvider.Object, this.State); + Task ExecuteAsync() => this.ExecuteAsync(action); + + // Act & Assert + DeclarativeActionException exception = await Assert.ThrowsAsync(ExecuteAsync); + Assert.Contains("Cannot send sensitive conversation message metadata", exception.Message); + Assert.Equal(messageCount, mockAgentProvider.TestMessages.Count); + } + private async Task ExecuteTestAsync( string displayName, string variableName, @@ -112,10 +168,10 @@ private AddConversationMessage CreateModel( string conversationId, AgentMessageRoleWrapper role, string messageText, - RecordDataValue? metadata) + RecordDataValue? metadata, + ObjectExpression.Builder? metadataExpression = null) { - ObjectExpression.Builder? metadataExpression = null; - if (metadata is not null) + if (metadata is not null && metadataExpression is null) { metadataExpression = ObjectExpression.Literal(metadata).ToBuilder(); } diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/PowerFx/RecalcEngineFactoryTests.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/PowerFx/RecalcEngineFactoryTests.cs index 07923d57cca..cb081a7ba50 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/PowerFx/RecalcEngineFactoryTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/PowerFx/RecalcEngineFactoryTests.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using Microsoft.Agents.AI.Workflows.Declarative.PowerFx; using Microsoft.PowerFx; +using Microsoft.PowerFx.Types; namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.PowerFx; @@ -36,9 +37,10 @@ public void SetFunctionDisabledByDefault() { // Arrange RecalcEngine engine = RecalcEngineFactory.Create(); + engine.UpdateVariable("MyVariable", FormulaValue.New(0)); // Act - CheckResult result = engine.Check("Set(MyVariable, 1)"); + CheckResult result = engine.Check("Set(MyVariable, 1)", options: new ParserOptions() { AllowsSideEffects = true }); // Assert Assert.False(result.IsSuccess); diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Workflows/SendActivity.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Workflows/SendActivity.cs index 864871af418..b092f100023 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Workflows/SendActivity.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Workflows/SendActivity.cs @@ -56,7 +56,7 @@ internal sealed class SetInputExecutor(FormulaSession session) : ActionExecutor( // protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) { - var evaluatedValue = await context.ReadStateWithSensitivityAsync(key: "LastMessageText", scopeName: "System").ConfigureAwait(false); + var evaluatedValue = await context.ReadStateWithSensitivityAsync(key: "LastMessageText", scopeName: "System").ConfigureAwait(false); await context.QueueStateUpdateWithSensitivityAsync(key: "TestValue", value: evaluatedValue, scopeName: "Local").ConfigureAwait(false); return default; From 2f38485fe0ce4e283019e45e7102b2780da07e59 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Thu, 10 Sep 2026 09:27:28 -0400 Subject: [PATCH 11/46] fix: context forwarding --- .../Extensions/IWorkflowContextExtensions.cs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) 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 942136c12af..f7b2bfce695 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Extensions/IWorkflowContextExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Extensions/IWorkflowContextExtensions.cs @@ -43,13 +43,15 @@ public static ValueTask QueueStateUpdateAsync( PropertyPath variablePath, TValue? value, SensitivityLevel sensitivity, - CancellationToken cancellationToken = default) => - DeclarativeContext(context).QueueStateUpdateAsync( - Throw.IfNull(variablePath.VariableName), - value, - Throw.IfNull(variablePath.NamespaceAlias), - sensitivity, - cancellationToken); + 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) { From 9a551d0f2aa1829ae0f4052f006392253bf9740a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 10 Sep 2026 17:51:45 +0000 Subject: [PATCH 12/46] Fix declarative sensitivity review comments Co-authored-by: baywet <7905502+baywet@users.noreply.github.com> --- .../Interpreter/DeclarativeWorkflowContext.cs | 2 +- .../PowerFx/WorkflowExpressionEngine.cs | 10 +++------- .../Kit/RootExecutorTests.cs | 4 ++++ .../PowerFx/WorkflowExpressionEngineTests.cs | 17 +++++++++++++++++ 4 files changed, 25 insertions(+), 8 deletions(-) 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 b3e61ea0907..183b2e785e7 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Interpreter/DeclarativeWorkflowContext.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Interpreter/DeclarativeWorkflowContext.cs @@ -232,7 +232,7 @@ async ValueTask QueueNativeStateAsync(object rawValue) private ValueTask QueueSensitivityUpdateAsync(string key, string? scopeName, SensitivityLevel sensitivity, CancellationToken cancellationToken) { - if (scopeName is null || !ManagedScopes.Contains(scopeName)) + if (scopeName is null || (!ManagedScopes.Contains(scopeName) && scopeName != VariableScopeNames.Environment)) { return default; } 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 05404e91f37..b5c15104323 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PowerFx/WorkflowExpressionEngine.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PowerFx/WorkflowExpressionEngine.cs @@ -436,18 +436,14 @@ private static bool TryGetDottedReference(DottedNameNode dottedNameNode, out (st node = current.Left; } - if (node is FirstNameNode firstNameNode) - { - names.Add(firstNameNode.Ident.Name.Value); - } - - names.Reverse(); - if (names.Count == 0) + 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]); diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Kit/RootExecutorTests.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Kit/RootExecutorTests.cs index 376f3b93bd4..4fedeb6d0f5 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Kit/RootExecutorTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Kit/RootExecutorTests.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; 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.Extensions.Configuration; using Moq; @@ -34,6 +35,8 @@ public async Task InitializeEnvironmentAsync_OnlyQueuesAllowedVariablesAsync() Mock sourceContext = new(MockBehavior.Strict); sourceContext.Setup(c => c.QueueStateUpdateAsync("ALLOWED", It.IsAny(), VariableScopeNames.Environment, It.IsAny())) .Returns(default(ValueTask)); + sourceContext.Setup(c => c.QueueStateUpdateAsync("ALLOWED", SensitivityLevel.Sensitive, WorkflowFormulaState.GetSensitivityScopeName(VariableScopeNames.Environment), It.IsAny())) + .Returns(default(ValueTask)); DeclarativeWorkflowContext context = new(sourceContext.Object, executor.Session.State); @@ -42,6 +45,7 @@ public async Task InitializeEnvironmentAsync_OnlyQueuesAllowedVariablesAsync() // Assert sourceContext.Verify(c => c.QueueStateUpdateAsync("ALLOWED", It.IsAny(), VariableScopeNames.Environment, It.IsAny()), Times.Once); + sourceContext.Verify(c => c.QueueStateUpdateAsync("ALLOWED", SensitivityLevel.Sensitive, WorkflowFormulaState.GetSensitivityScopeName(VariableScopeNames.Environment), It.IsAny()), Times.Once); sourceContext.Verify(c => c.QueueStateUpdateAsync("HIDDEN", It.IsAny(), VariableScopeNames.Environment, It.IsAny()), Times.Never); } diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/PowerFx/WorkflowExpressionEngineTests.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/PowerFx/WorkflowExpressionEngineTests.cs index 6d64bdfc6b6..611953e55b1 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/PowerFx/WorkflowExpressionEngineTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/PowerFx/WorkflowExpressionEngineTests.cs @@ -151,6 +151,23 @@ public void StringExpressionGetValueForQuotedEnvironmentVariableIsSensitive() expectedSensitivity: SensitivityLevel.Sensitive); } + [Fact] + public void StringExpressionGetValueForComputedDottedAccessIsSensitive() + { + // Arrange + TableValue secretTable = FormulaValue.NewTable( + RecordType.Empty().Add("Value", FormulaType.String), + new RecordValue[] { FormulaValue.NewRecordFromFields(new NamedValue("Value", FormulaValue.New("secret-value"))) }); + this.State.Set("SecretTable", secretTable, VariableScopeNames.Local, SensitivityLevel.Sensitive); + this.State.Bind(); + + // Act & Assert + this.EvaluateExpression( + StringExpression.Expression("First(Local.SecretTable).Value"), + expectedValue: "secret-value", + expectedSensitivity: SensitivityLevel.Sensitive); + } + [Fact] public void StringExpressionGetValueForEnvironmentVariableTextLiteralIsNotSensitive() { From 1ea1ed2ff719113a85e1dba5c420568640ec9155 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 11 Sep 2026 12:41:30 +0000 Subject: [PATCH 13/46] Fix declarative review feedback Co-authored-by: baywet <7905502+baywet@users.noreply.github.com> --- .../ChatClientPromptAgentFactory.cs | 2 + .../Kit/IWorkflowContextExtensions.cs | 17 +++++++- .../ChatClient/ChatClientAgentFactoryTests.cs | 25 ++++++++++++ .../Kit/IWorkflowContextExtensionsTests.cs | 40 +++++++++++++++++++ 4 files changed, 83 insertions(+), 1 deletion(-) diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative/ChatClient/ChatClientPromptAgentFactory.cs b/dotnet/src/Microsoft.Agents.AI.Declarative/ChatClient/ChatClientPromptAgentFactory.cs index a43f75f7b50..2be060b1318 100644 --- a/dotnet/src/Microsoft.Agents.AI.Declarative/ChatClient/ChatClientPromptAgentFactory.cs +++ b/dotnet/src/Microsoft.Agents.AI.Declarative/ChatClient/ChatClientPromptAgentFactory.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using System.Collections.Generic; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.Agents.ObjectModel; @@ -38,6 +39,7 @@ public ChatClientPromptAgentFactory( { Engine = engine, Configuration = configuration, + AllowedConfigurationVariables = configuration?.AsEnumerable().Select(static pair => pair.Key), LoggerFactory = loggerFactory, }, isValidated: true) 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 3eaa51860f8..93beb091ab2 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Kit/IWorkflowContextExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Kit/IWorkflowContextExtensions.cs @@ -134,8 +134,12 @@ public static async ValueTask FormatTemplateAsync(this IWorkflowContext return new(declarativeValue, declarativeSensitivity); } + string plainScopeName = scopeName ?? WorkflowFormulaState.DefaultScopeName; TValue? value = await context.ReadStateAsync(key, scopeName, cancellationToken).ConfigureAwait(false); - return new(value, SensitivityLevel.None); + SensitivityLevel sensitivity = ShouldPersistSensitivity(plainScopeName) + ? await context.ReadStateAsync(key, WorkflowFormulaState.GetSensitivityScopeName(plainScopeName), cancellationToken).ConfigureAwait(false) + : SensitivityLevel.None; + return new(value, sensitivity); } /// @@ -163,6 +167,12 @@ public static async ValueTask QueueStateUpdateWithSensitivityAsync( } await context.QueueStateUpdateAsync(key, value.Value, scopeName, cancellationToken).ConfigureAwait(false); + + string plainScopeName = scopeName ?? WorkflowFormulaState.DefaultScopeName; + if (ShouldPersistSensitivity(plainScopeName)) + { + await context.QueueStateUpdateAsync(key, value.Sensitivity, WorkflowFormulaState.GetSensitivityScopeName(plainScopeName), cancellationToken).ConfigureAwait(false); + } } /// @@ -230,4 +240,9 @@ private static void ThrowIfSensitive(SensitivityLevel sensitivity) throw new DeclarativeActionException("Cannot return sensitive workflow expression value."); } } + + private static bool ShouldPersistSensitivity(string scopeName) => + DeclarativeWorkflowContext.ManagedScopes.Contains(scopeName) || + scopeName == VariableScopeNames.Environment || + scopeName == VariableScopeNames.System; } 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 8f8c5b16fab..56ddda7a89c 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Declarative.UnitTests/ChatClient/ChatClientAgentFactoryTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Declarative.UnitTests/ChatClient/ChatClientAgentFactoryTests.cs @@ -155,6 +155,31 @@ public async Task TryCreateAsync_WithOptions_LoadsAllowedConfigurationAsync() Assert.Equal(0.8F, chatClientAgent.ChatOptions?.TopP); } + [Fact] + public async Task TryCreateAsync_WithLegacyConfiguration_LoadsReferencedConfigurationAsync() + { + // 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 + 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_OnlyLoadsAllowedReferencedConfigurationAsync() { 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 index 142c97fa591..529779325c5 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Kit/IWorkflowContextExtensionsTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Kit/IWorkflowContextExtensionsTests.cs @@ -6,6 +6,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.PowerFx.Types; using Moq; @@ -69,4 +70,43 @@ public async Task ReadStateWithSensitivityAsync_QueuesSensitiveAssignmentAsync() Assert.Equal("secret-value", state.Engine.Eval("Local.TestValue").ToObject()); Assert.Equal(SensitivityLevel.Sensitive, state.GetSensitivity("TestValue", VariableScopeNames.Local)); } + + [Fact] + public async Task ReadStateWithSensitivityAsync_WithPlainContext_ReadsSensitivitySidecarAsync() + { + // Arrange + Mock context = new(MockBehavior.Loose); + context + .Setup(c => c.ReadStateAsync("TestValue", VariableScopeNames.Local, default)) + .Returns(new ValueTask("secret-value")); + context + .Setup(c => c.ReadStateAsync("TestValue", WorkflowFormulaState.GetSensitivityScopeName(VariableScopeNames.Local), default)) + .Returns(new ValueTask(SensitivityLevel.Sensitive)); + + // Act + var evaluatedValue = await context.Object.ReadStateWithSensitivityAsync("TestValue", VariableScopeNames.Local); + + // Assert + Assert.Equal("secret-value", evaluatedValue.Value); + Assert.Equal(SensitivityLevel.Sensitive, evaluatedValue.Sensitivity); + } + + [Fact] + public async Task QueueStateUpdateWithSensitivityAsync_WithPlainContext_QueuesSensitivitySidecarAsync() + { + // Arrange + Mock context = new(MockBehavior.Strict); + context + .Setup(c => c.QueueStateUpdateAsync("TestValue", "secret-value", VariableScopeNames.Local, default)) + .Returns(default(ValueTask)); + context + .Setup(c => c.QueueStateUpdateAsync("TestValue", SensitivityLevel.Sensitive, WorkflowFormulaState.GetSensitivityScopeName(VariableScopeNames.Local), default)) + .Returns(default(ValueTask)); + + // Act + await context.Object.QueueStateUpdateWithSensitivityAsync("TestValue", new EvaluationResult("secret-value", SensitivityLevel.Sensitive), VariableScopeNames.Local); + + // Assert + context.VerifyAll(); + } } From 8a23310d3f764d0ebd6a8dc2e492ae34d6496a50 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 11 Sep 2026 18:41:02 +0000 Subject: [PATCH 14/46] Merge main and resolve conflicts Co-authored-by: baywet <7905502+baywet@users.noreply.github.com> --- dotnet/global.json | 18 +++++++++--------- global.json | 10 ++++++++++ 2 files changed, 19 insertions(+), 9 deletions(-) create mode 100644 global.json diff --git a/dotnet/global.json b/dotnet/global.json index d46fcb1cf96..2684a553446 100644 --- a/dotnet/global.json +++ b/dotnet/global.json @@ -1,10 +1,10 @@ { - "sdk": { - "version": "10.0.401", - "rollForward": "minor", - "allowPrerelease": false - }, - "test": { - "runner": "Microsoft.Testing.Platform" - } -} \ No newline at end of file + "sdk": { + "version": "10.0.400", + "rollForward": "minor", + "allowPrerelease": false + }, + "test": { + "runner": "Microsoft.Testing.Platform" + } +} diff --git a/global.json b/global.json new file mode 100644 index 00000000000..d46fcb1cf96 --- /dev/null +++ b/global.json @@ -0,0 +1,10 @@ +{ + "sdk": { + "version": "10.0.401", + "rollForward": "minor", + "allowPrerelease": false + }, + "test": { + "runner": "Microsoft.Testing.Platform" + } +} \ No newline at end of file From 93cc03e1744124ef841727cbcfe447d38d33df2f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 11 Sep 2026 18:42:16 +0000 Subject: [PATCH 15/46] Revert "Merge main and resolve conflicts" This reverts commit 8a23310d3f764d0ebd6a8dc2e492ae34d6496a50. Co-authored-by: baywet <7905502+baywet@users.noreply.github.com> --- dotnet/global.json | 18 +++++++++--------- global.json | 10 ---------- 2 files changed, 9 insertions(+), 19 deletions(-) delete mode 100644 global.json diff --git a/dotnet/global.json b/dotnet/global.json index 2684a553446..d46fcb1cf96 100644 --- a/dotnet/global.json +++ b/dotnet/global.json @@ -1,10 +1,10 @@ { - "sdk": { - "version": "10.0.400", - "rollForward": "minor", - "allowPrerelease": false - }, - "test": { - "runner": "Microsoft.Testing.Platform" - } -} + "sdk": { + "version": "10.0.401", + "rollForward": "minor", + "allowPrerelease": false + }, + "test": { + "runner": "Microsoft.Testing.Platform" + } +} \ No newline at end of file diff --git a/global.json b/global.json deleted file mode 100644 index d46fcb1cf96..00000000000 --- a/global.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "sdk": { - "version": "10.0.401", - "rollForward": "minor", - "allowPrerelease": false - }, - "test": { - "runner": "Microsoft.Testing.Platform" - } -} \ No newline at end of file From 386b512a6dffa2c7cfad3d17435c882552426465 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 15 Sep 2026 19:21:50 +0000 Subject: [PATCH 16/46] Address declarative review feedback Co-authored-by: baywet <7905502+baywet@users.noreply.github.com> --- .../Kit/RootExecutor.cs | 2 +- .../CopyConversationMessagesExecutor.cs | 5 +++ .../ObjectModel/InvokeAzureAgentExecutor.cs | 5 +++ .../PowerFx/WorkflowExpressionEngine.cs | 6 ++-- .../PowerFx/WorkflowFormulaState.cs | 5 ++- .../CopyConversationMessagesExecutorTest.cs | 26 ++++++++++++++ .../InvokeAzureAgentExecutorTest.cs | 36 +++++++++++++++++++ .../PowerFx/WorkflowExpressionEngineTests.cs | 15 ++++++++ 8 files changed, 96 insertions(+), 4 deletions(-) diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Kit/RootExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Kit/RootExecutor.cs index d5c7273a3f5..7a9364a5c3d 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Kit/RootExecutor.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Kit/RootExecutor.cs @@ -48,7 +48,7 @@ protected RootExecutor(string id, DeclarativeWorkflowOptions options, Func 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/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/PowerFx/WorkflowExpressionEngine.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PowerFx/WorkflowExpressionEngine.cs index b5c15104323..158a6ed5c1a 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PowerFx/WorkflowExpressionEngine.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PowerFx/WorkflowExpressionEngine.cs @@ -18,10 +18,12 @@ namespace Microsoft.Agents.AI.Workflows.Declarative.PowerFx; internal sealed class WorkflowExpressionEngine { private readonly WorkflowFormulaState _state; + private readonly ParserOptions? _parserOptions; public WorkflowExpressionEngine(WorkflowFormulaState state) { this._state = state; + this._parserOptions = state.AllowsSideEffects ? new ParserOptions { AllowsSideEffects = true } : null; } public EvaluationResult GetValue(BoolExpression boolean) => this.Evaluate(boolean); @@ -325,7 +327,7 @@ private EvaluationResult EvaluateScope(ExpressionBase expression) expression.VariableReference?.ToString() : expression.ExpressionText; - FormulaValue result = this._state.Engine.Eval(expressionText); + FormulaValue result = this._state.Engine.Eval(expressionText, options: this._parserOptions); if (result is ErrorValue errorValue) { @@ -348,7 +350,7 @@ private SensitivityLevel GetSensitivity(ExpressionBase expression) return SensitivityLevel.None; } - CheckResult checkResult = this._state.Engine.Check(expressionText); + CheckResult checkResult = this._state.Engine.Check(expressionText, options: this._parserOptions); checkResult.ThrowOnErrors(); SensitivityLevel sensitivity = 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 a172b43d6a1..6851c9cd9c9 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PowerFx/WorkflowFormulaState.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PowerFx/WorkflowFormulaState.cs @@ -39,12 +39,15 @@ 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.AllowsSideEffects = allowsSideEffects; this.Evaluator = new WorkflowExpressionEngine(this); this.Bind(); } diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/CopyConversationMessagesExecutorTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/CopyConversationMessagesExecutorTest.cs index c0a2fdf6598..67327f87a2f 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/CopyConversationMessagesExecutorTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/CopyConversationMessagesExecutorTest.cs @@ -86,6 +86,32 @@ await this.ExecuteTestAsync( expectedMessageCount: 1); } + [Fact] + public async Task CopyMessagesWithSensitiveVariableThrowsAsync() + { + // Arrange + List testMessages = + [ + new ChatMessage(ChatRole.User, "Message from variable") + ]; + TableValue messagesTable = testMessages.ToTable(); + this.State.Set("SourceMessages", messagesTable, sensitivity: SensitivityLevel.Sensitive); + MockAgentProvider mockAgentProvider = new(); + int messageCount = mockAgentProvider.TestMessages.Count; + CopyConversationMessages model = this.CreateModel( + this.FormatDisplayName(nameof(CopyMessagesWithSensitiveVariableThrowsAsync)), + "TestConversationId", + ValueExpression.Variable(PropertyPath.TopicVariable("SourceMessages"))); + + CopyConversationMessagesExecutor action = new(model, mockAgentProvider.Object, this.State); + Task ExecuteAsync() => this.ExecuteAsync(action); + + // Act & Assert + DeclarativeActionException exception = await Assert.ThrowsAsync(ExecuteAsync); + Assert.Contains("Cannot send sensitive conversation messages", exception.Message); + Assert.Equal(messageCount, mockAgentProvider.TestMessages.Count); + } + [Fact] public async Task CopyMessagesToWorkflowConversationAsync() { diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/InvokeAzureAgentExecutorTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/InvokeAzureAgentExecutorTest.cs index b4f80f97c90..32c7e7ec8f3 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/InvokeAzureAgentExecutorTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/InvokeAzureAgentExecutorTest.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using Microsoft.Agents.AI.Workflows.Declarative.Extensions; using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel; using Microsoft.Agents.AI.Workflows.Declarative.PowerFx; using Microsoft.Agents.ObjectModel; @@ -102,6 +103,32 @@ public async Task RecordValuedArgumentIsBoundAsRecordAsync() Assert.Equal("beta", record["b"]); } + [Fact] + public async Task SensitiveInputMessagesThrowAsync() + { + // Arrange + this.State.InitializeSystem(); + List testMessages = + [ + new ChatMessage(ChatRole.User, "Message from variable") + ]; + this.State.Set("SourceMessages", testMessages.ToTable(), sensitivity: SensitivityLevel.Sensitive); + CapturingAgentProvider provider = new("acknowledged"); + InvokeAzureAgent model = + this.CreateModel( + displayName: nameof(SensitiveInputMessagesThrowAsync), + agentName: "BrainMessages", + messages: ValueExpression.Variable(PropertyPath.TopicVariable("SourceMessages"))); + + InvokeAzureAgentExecutor action = new(model, provider, this.State); + Task ExecuteAsync() => this.ExecuteAsync(action, isDiscrete: false); + + // Act & Assert + DeclarativeActionException exception = await Assert.ThrowsAsync(ExecuteAsync); + Assert.Contains("Cannot send sensitive agent input messages", exception.Message); + Assert.Null(provider.CapturedMessages); + } + [Fact] public async Task InlineRecordExpressionArgumentIsBoundAsRecordAsync() { @@ -291,6 +318,7 @@ private InvokeAzureAgent CreateModel( string displayName, string agentName, IReadOnlyList<(string Key, ValueExpression Value)>? arguments = null, + ValueExpression? messages = null, string? responseObjectVariable = null) { InvokeAzureAgent.Builder builder = @@ -315,6 +343,11 @@ private InvokeAzureAgent CreateModel( builder.Input = inputBuilder; } + if (messages is not null) + { + (builder.Input ??= new AzureAgentInput.Builder()).Messages = messages; + } + if (responseObjectVariable is not null) { builder.Output = @@ -360,6 +393,8 @@ private sealed class CapturingAgentProvider(string responseText) : ResponseAgent { public IDictionary? CapturedArguments { get; private set; } + public IEnumerable? CapturedMessages { get; private set; } + public override IAsyncEnumerable InvokeAgentAsync( string agentId, string? agentVersion, @@ -369,6 +404,7 @@ public override IAsyncEnumerable InvokeAgentAsync( CancellationToken cancellationToken = default) { this.CapturedArguments = inputArguments; + this.CapturedMessages = messages; return YieldAsync(responseText); } diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/PowerFx/WorkflowExpressionEngineTests.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/PowerFx/WorkflowExpressionEngineTests.cs index 611953e55b1..74e8530c5b7 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/PowerFx/WorkflowExpressionEngineTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/PowerFx/WorkflowExpressionEngineTests.cs @@ -181,6 +181,21 @@ public void StringExpressionGetValueForEnvironmentVariableTextLiteralIsNotSensit expectedValue: "Env.SOME_SECRET literal"); } + [Fact] + public void BoolExpressionGetValueForSetWhenEnabled() + { + // Arrange + WorkflowFormulaState state = new(RecalcEngineFactory.Create(enableSetFunction: true), allowsSideEffects: true); + state.Engine.UpdateVariable("MyVariable", FormulaValue.New("old-value")); + + // Act + EvaluationResult result = state.Evaluator.GetValue(BoolExpression.Expression("""Set(MyVariable, "new-value"); true""")); + + // Assert + Assert.True(result.Value); + Assert.Equal("new-value", ((StringValue)state.Engine.Eval("MyVariable")).Value); + } + [Fact] public void StringExpressionGetValueForFormula() => // Arrange, Act & Assert From 6befdf8d026011424b5e13f02ae2a8a3a68a0500 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Wed, 16 Sep 2026 13:29:03 -0400 Subject: [PATCH 17/46] Update dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/DeclarativeWorkflowBuilder.cs --- .../DeclarativeWorkflowBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/DeclarativeWorkflowBuilder.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/DeclarativeWorkflowBuilder.cs index e12370a53ca..2c4d4ca8d68 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/DeclarativeWorkflowBuilder.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/DeclarativeWorkflowBuilder.cs @@ -72,7 +72,7 @@ public static Workflow Build( AdaptiveDialog workflowElement = ReadWorkflow(yamlReader); string rootId = WorkflowActionVisitor.Steps.Root(workflowElement); - WorkflowFormulaState state = new(options.CreateRecalcEngine()); + WorkflowFormulaState state = new(options.CreateRecalcEngine(), options.EnableSetFunction); state.Initialize( workflowElement.WrapWithBot(), options.Configuration, From b57edf1e257c0769a6fdd91842d03b864e687878 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Wed, 16 Sep 2026 13:41:03 -0400 Subject: [PATCH 18/46] Update dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Kit/IWorkflowContextExtensions.cs --- .../Kit/IWorkflowContextExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 93beb091ab2..ec730fa3152 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Kit/IWorkflowContextExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Kit/IWorkflowContextExtensions.cs @@ -135,7 +135,7 @@ public static async ValueTask FormatTemplateAsync(this IWorkflowContext } string plainScopeName = scopeName ?? WorkflowFormulaState.DefaultScopeName; - TValue? value = await context.ReadStateAsync(key, scopeName, cancellationToken).ConfigureAwait(false); + 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; From 2c5abfa540aa5a0940f25d8be218c4cc52ac2ab3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 16 Sep 2026 17:48:38 +0000 Subject: [PATCH 19/46] Remove unusable Power Fx Set opt-in Co-authored-by: baywet <7905502+baywet@users.noreply.github.com> --- .../DeclarativeWorkflowBuilder.cs | 2 +- .../DeclarativeWorkflowOptions.cs | 5 ----- .../DeclarativeWorkflowOptionsExtensions.cs | 2 +- .../Kit/RootExecutor.cs | 2 +- .../PowerFx/RecalcEngineFactory.cs | 7 +------ .../PublicAPI/net10.0/PublicAPI.Unshipped.txt | 2 -- .../PublicAPI/net472/PublicAPI.Unshipped.txt | 2 -- .../PublicAPI/net8.0/PublicAPI.Unshipped.txt | 2 -- .../PublicAPI/net9.0/PublicAPI.Unshipped.txt | 2 -- .../netstandard2.0/PublicAPI.Unshipped.txt | 2 -- .../PowerFx/WorkflowExpressionEngineTests.cs | 15 --------------- 11 files changed, 4 insertions(+), 39 deletions(-) diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/DeclarativeWorkflowBuilder.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/DeclarativeWorkflowBuilder.cs index 2c4d4ca8d68..e12370a53ca 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/DeclarativeWorkflowBuilder.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/DeclarativeWorkflowBuilder.cs @@ -72,7 +72,7 @@ public static Workflow Build( AdaptiveDialog workflowElement = ReadWorkflow(yamlReader); string rootId = WorkflowActionVisitor.Steps.Root(workflowElement); - WorkflowFormulaState state = new(options.CreateRecalcEngine(), options.EnableSetFunction); + WorkflowFormulaState state = new(options.CreateRecalcEngine()); state.Initialize( workflowElement.WrapWithBot(), options.Configuration, diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/DeclarativeWorkflowOptions.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/DeclarativeWorkflowOptions.cs index a41196de29c..b5b05b910d3 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/DeclarativeWorkflowOptions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/DeclarativeWorkflowOptions.cs @@ -63,11 +63,6 @@ public sealed class DeclarativeWorkflowOptions(ResponseAgentProvider agentProvid /// public int? MaximumExpressionLength { get; init; } - /// - /// Gets a value indicating whether the Power Fx Set function is enabled. - /// - public bool EnableSetFunction { get; init; } - /// /// Gets the used to create loggers for workflow components. /// diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Extensions/DeclarativeWorkflowOptionsExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Extensions/DeclarativeWorkflowOptionsExtensions.cs index b4ce4587c2b..1e1c52ab887 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Extensions/DeclarativeWorkflowOptionsExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Extensions/DeclarativeWorkflowOptionsExtensions.cs @@ -10,5 +10,5 @@ internal static class DeclarativeWorkflowOptionsExtensions private const int DefaultMaximumExpressionLength = 10000; public static RecalcEngine CreateRecalcEngine(this DeclarativeWorkflowOptions? context) => - RecalcEngineFactory.Create(context?.MaximumExpressionLength ?? DefaultMaximumExpressionLength, context?.MaximumCallDepth, context?.EnableSetFunction ?? false); + RecalcEngineFactory.Create(context?.MaximumExpressionLength ?? DefaultMaximumExpressionLength, context?.MaximumCallDepth); } diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Kit/RootExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Kit/RootExecutor.cs index 7a9364a5c3d..d5c7273a3f5 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Kit/RootExecutor.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Kit/RootExecutor.cs @@ -48,7 +48,7 @@ protected RootExecutor(string id, DeclarativeWorkflowOptions options, Func void Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.AllowedEnvironmentVariables.get -> System.Collections.Generic.IEnumerable? Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.AllowedEnvironmentVariables.init -> void -Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.EnableSetFunction.get -> bool -Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.EnableSetFunction.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!> 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 47f9ea9f7f2..b35df5072f3 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 @@ -3,7 +3,5 @@ Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.AllowProces 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 -Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.EnableSetFunction.get -> bool -Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.EnableSetFunction.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!> 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 47f9ea9f7f2..b35df5072f3 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 @@ -3,7 +3,5 @@ Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.AllowProces 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 -Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.EnableSetFunction.get -> bool -Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.EnableSetFunction.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!> 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 47f9ea9f7f2..b35df5072f3 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 @@ -3,7 +3,5 @@ Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.AllowProces 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 -Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.EnableSetFunction.get -> bool -Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.EnableSetFunction.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!> 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 47f9ea9f7f2..b35df5072f3 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 @@ -3,7 +3,5 @@ Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.AllowProces 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 -Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.EnableSetFunction.get -> bool -Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.EnableSetFunction.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!> diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/PowerFx/WorkflowExpressionEngineTests.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/PowerFx/WorkflowExpressionEngineTests.cs index 74e8530c5b7..611953e55b1 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/PowerFx/WorkflowExpressionEngineTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/PowerFx/WorkflowExpressionEngineTests.cs @@ -181,21 +181,6 @@ public void StringExpressionGetValueForEnvironmentVariableTextLiteralIsNotSensit expectedValue: "Env.SOME_SECRET literal"); } - [Fact] - public void BoolExpressionGetValueForSetWhenEnabled() - { - // Arrange - WorkflowFormulaState state = new(RecalcEngineFactory.Create(enableSetFunction: true), allowsSideEffects: true); - state.Engine.UpdateVariable("MyVariable", FormulaValue.New("old-value")); - - // Act - EvaluationResult result = state.Evaluator.GetValue(BoolExpression.Expression("""Set(MyVariable, "new-value"); true""")); - - // Assert - Assert.True(result.Value); - Assert.Equal("new-value", ((StringValue)state.Engine.Eval("MyVariable")).Value); - } - [Fact] public void StringExpressionGetValueForFormula() => // Arrange, Act & Assert From adacc409f9ff9e20878337fc5dc1c6be15ca5fb0 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Wed, 16 Sep 2026 13:59:14 -0400 Subject: [PATCH 20/46] Update dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Kit/IWorkflowContextExtensions.cs --- .../Kit/IWorkflowContextExtensions.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 ec730fa3152..3c192342772 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Kit/IWorkflowContextExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Kit/IWorkflowContextExtensions.cs @@ -166,9 +166,8 @@ public static async ValueTask QueueStateUpdateWithSensitivityAsync( return; } - await context.QueueStateUpdateAsync(key, value.Value, scopeName, cancellationToken).ConfigureAwait(false); - 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); From 46a78b21f5690516e697cd1b3c61ec172224cd54 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Wed, 16 Sep 2026 14:49:22 -0400 Subject: [PATCH 21/46] fix: pass the env var allow list down the prompt --- .../PromptAgentFactory.cs | 3 +- .../Kit/IWorkflowContextExtensions.cs | 72 ++++++++++++++++- .../PublicAPI/net10.0/PublicAPI.Unshipped.txt | 5 ++ .../PublicAPI/net472/PublicAPI.Unshipped.txt | 5 ++ .../PublicAPI/net8.0/PublicAPI.Unshipped.txt | 5 ++ .../PublicAPI/net9.0/PublicAPI.Unshipped.txt | 5 ++ .../netstandard2.0/PublicAPI.Unshipped.txt | 5 ++ .../ChatClient/ChatClientAgentFactoryTests.cs | 79 +++++++++++++++++++ .../Kit/IWorkflowContextExtensionsTests.cs | 51 ++++++++++++ .../Workflows/Condition.cs | 12 +-- .../Workflows/ConditionElse.cs | 8 +- .../Workflows/EditTable.cs | 4 +- .../Workflows/EditTableV2.cs | 4 +- .../Workflows/LoopBreak.cs | 4 +- .../Workflows/LoopContinue.cs | 4 +- .../Workflows/LoopEach.cs | 4 +- .../Workflows/SetTextVariable.cs | 6 +- .../Workflows/SetVariable.cs | 4 +- 18 files changed, 250 insertions(+), 30 deletions(-) diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative/PromptAgentFactory.cs b/dotnet/src/Microsoft.Agents.AI.Declarative/PromptAgentFactory.cs index d16424c4411..351c37f44c4 100644 --- a/dotnet/src/Microsoft.Agents.AI.Declarative/PromptAgentFactory.cs +++ b/dotnet/src/Microsoft.Agents.AI.Declarative/PromptAgentFactory.cs @@ -29,7 +29,7 @@ public abstract class PromptAgentFactory /// 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. protected PromptAgentFactory(RecalcEngine? engine = null, IConfiguration? configuration = null) - : this(engine, configuration, allowedConfigurationVariables: null) + : this(engine, configuration, allowedConfigurationVariables: configuration?.AsEnumerable().Select(static pair => pair.Key)) { } @@ -105,6 +105,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/Kit/IWorkflowContextExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Kit/IWorkflowContextExtensions.cs index 3c192342772..a4182e7c518 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,18 +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) { EvaluationResult result = state.Evaluator.Format(TemplateLine.Parse(line)); - ThrowIfSensitive(result.Sensitivity); + sensitivity = MaxSensitivity(sensitivity, result.Sensitivity); builder.AppendLine(result.Value); } - return builder.ToString(); + return new(builder.ToString(), sensitivity); } /// @@ -84,13 +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)); - ThrowIfSensitive(result.Sensitivity); - return (TValue?)result.Value.ToObject(); + return new((TValue?)result.Value.ToObject(), result.Sensitivity); } /// @@ -240,6 +301,9 @@ private static void ThrowIfSensitive(SensitivityLevel sensitivity) } } + private static SensitivityLevel MaxSensitivity(SensitivityLevel left, SensitivityLevel right) => + left == SensitivityLevel.Sensitive || right == SensitivityLevel.Sensitive ? SensitivityLevel.Sensitive : SensitivityLevel.None; + private static bool ShouldPersistSensitivity(string scopeName) => DeclarativeWorkflowContext.ManagedScopes.Contains(scopeName) || scopeName == VariableScopeNames.Environment || 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 b35df5072f3..2e87ed9adec 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 @@ -5,3 +5,8 @@ Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.AllowedEnvi 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.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 b35df5072f3..2e87ed9adec 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 @@ -5,3 +5,8 @@ Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.AllowedEnvi 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.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 b35df5072f3..2e87ed9adec 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 @@ -5,3 +5,8 @@ Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.AllowedEnvi 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.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 b35df5072f3..2e87ed9adec 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 @@ -5,3 +5,8 @@ Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.AllowedEnvi 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.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 b35df5072f3..2e87ed9adec 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 @@ -5,3 +5,8 @@ Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.AllowedEnvi 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.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/tests/Microsoft.Agents.AI.Declarative.UnitTests/ChatClient/ChatClientAgentFactoryTests.cs b/dotnet/tests/Microsoft.Agents.AI.Declarative.UnitTests/ChatClient/ChatClientAgentFactoryTests.cs index 56ddda7a89c..d9ff1233c5d 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Declarative.UnitTests/ChatClient/ChatClientAgentFactoryTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Declarative.UnitTests/ChatClient/ChatClientAgentFactoryTests.cs @@ -180,6 +180,52 @@ public async Task TryCreateAsync_WithLegacyConfiguration_LoadsReferencedConfigur Assert.Equal(0.8F, chatClientAgent.ChatOptions?.TopP); } + [Fact] + public async Task ProtectedConstructor_WithLegacyConfiguration_LoadsReferencedConfigurationAsync() + { + // 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); + + // Assert + StringValue temperature = Assert.IsType(factory.Evaluate("Temperature")); + Assert.Equal("0.9", temperature.Value); + StringValue topP = Assert.IsType(factory.Evaluate("TopP")); + Assert.Equal("0.8", topP.Value); + } + + [Fact] + public async Task CreateAsync_WithLegacyConfiguration_InitializesVariablesBeforeTryCreateAsync() + { + // 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 + AIAgent agent = await factory.CreateAsync(promptAgent); + + // Assert + Assert.NotNull(agent); + Assert.Equal("0.9", factory.TemperatureValue); + } + [Fact] public async Task TryCreateAsync_OnlyLoadsAllowedReferencedConfigurationAsync() { @@ -219,4 +265,37 @@ private sealed class InspectingPromptAgentFactory(IConfiguration configuration, 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 Task TryCreateAsync(GptComponentMetadata promptAgent, CancellationToken cancellationToken = default) + { + // Arrange + StringValue temperature = Assert.IsType(this.Engine.Eval("Temperature")); + + // Act + this.TemperatureValue = temperature.Value; + + // Assert + return Task.FromResult(new ChatClientAgent(chatClient)); + } + } + + private sealed class LegacyInspectingPromptAgentFactory(IConfiguration configuration) + : PromptAgentFactory(engine: null, configuration: configuration) + { + public FormulaValue Evaluate(string expression) => this.Engine.Eval(expression); + + 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/Kit/IWorkflowContextExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Kit/IWorkflowContextExtensionsTests.cs index 529779325c5..4b90caaa03a 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Kit/IWorkflowContextExtensionsTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Kit/IWorkflowContextExtensionsTests.cs @@ -31,6 +31,57 @@ public async Task FormatTemplateAsync_WithSensitiveValue_ThrowsAsync() 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 EvaluateAsync() => context.EvaluateValueAsync("System.LastMessageText"); + + // Assert + DeclarativeActionException exception = await Assert.ThrowsAsync(async () => await EvaluateAsync()); + Assert.Contains("Cannot return sensitive workflow expression value", exception.Message); + } + + [Fact] + public async Task EvaluateValueWithSensitivityAsync_WithSensitiveValue_ReturnsSensitivityAsync() + { + // 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 + EvaluationResult result = await context.EvaluateValueWithSensitivityAsync("System.LastMessageText"); + + // Assert + Assert.Equal("secret-value", result.Value); + Assert.Equal(SensitivityLevel.Sensitive, result.Sensitivity); + } + [Fact] public async Task QueueStateUpdateAsync_WithSensitivity_RebindsStateAsync() { diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Workflows/Condition.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Workflows/Condition.cs index 3dfa7b4f68a..04eb0196bc5 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Workflows/Condition.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Workflows/Condition.cs @@ -56,8 +56,8 @@ internal sealed class SetvariableTestExecutor(FormulaSession session) : ActionEx // protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) { - object? evaluatedValue = await context.EvaluateValueAsync("Value(System.LastMessageText)").ConfigureAwait(false); - await context.QueueStateUpdateAsync(key: "TestValue", value: evaluatedValue, scopeName: "Local").ConfigureAwait(false); + var evaluatedValue = await context.EvaluateValueWithSensitivityAsync("Value(System.LastMessageText)").ConfigureAwait(false); + await context.QueueStateUpdateWithSensitivityAsync(key: "TestValue", value: evaluatedValue, scopeName: "Local").ConfigureAwait(false); return default; } @@ -71,14 +71,14 @@ internal sealed class ConditiongroupTestExecutor(FormulaSession session) : Actio // protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) { - bool condition0 = await context.EvaluateValueAsync("Mod(Local.TestValue, 2) = 1").ConfigureAwait(false); - if (condition0) + var condition0 = await context.EvaluateValueWithSensitivityAsync("Mod(Local.TestValue, 2) = 1").ConfigureAwait(false); + if (condition0.Value) { return "conditionItem_odd"; } - bool condition1 = await context.EvaluateValueAsync("Mod(Local.TestValue, 2) = 0").ConfigureAwait(false); - if (condition1) + var condition1 = await context.EvaluateValueWithSensitivityAsync("Mod(Local.TestValue, 2) = 0").ConfigureAwait(false); + if (condition1.Value) { return "conditionItem_even"; } diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Workflows/ConditionElse.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Workflows/ConditionElse.cs index 2f64bdd3e54..23ccaede2b8 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Workflows/ConditionElse.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Workflows/ConditionElse.cs @@ -56,8 +56,8 @@ internal sealed class SetvariableTestExecutor(FormulaSession session) : ActionEx // protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) { - object? evaluatedValue = await context.EvaluateValueAsync("Value(System.LastMessageText)").ConfigureAwait(false); - await context.QueueStateUpdateAsync(key: "TestValue", value: evaluatedValue, scopeName: "Local").ConfigureAwait(false); + var evaluatedValue = await context.EvaluateValueWithSensitivityAsync("Value(System.LastMessageText)").ConfigureAwait(false); + await context.QueueStateUpdateWithSensitivityAsync(key: "TestValue", value: evaluatedValue, scopeName: "Local").ConfigureAwait(false); return default; } @@ -71,8 +71,8 @@ internal sealed class ConditiongroupTestExecutor(FormulaSession session) : Actio // protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) { - bool condition0 = await context.EvaluateValueAsync("Mod(Local.TestValue, 2) = 1").ConfigureAwait(false); - if (condition0) + var condition0 = await context.EvaluateValueWithSensitivityAsync("Mod(Local.TestValue, 2) = 1").ConfigureAwait(false); + if (condition0.Value) { return "conditionItem_odd"; } diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Workflows/EditTable.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Workflows/EditTable.cs index 0c612dcae99..5b2fb4eecf2 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Workflows/EditTable.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Workflows/EditTable.cs @@ -56,8 +56,8 @@ internal sealed class SetVarExecutor(FormulaSession session) : ActionExecutor(id // protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) { - object? evaluatedValue = await context.EvaluateValueAsync("[{id: 3}]").ConfigureAwait(false); - await context.QueueStateUpdateAsync(key: "MyTable", value: evaluatedValue, scopeName: "Local").ConfigureAwait(false); + var evaluatedValue = await context.EvaluateValueWithSensitivityAsync("[{id: 3}]").ConfigureAwait(false); + await context.QueueStateUpdateWithSensitivityAsync(key: "MyTable", value: evaluatedValue, scopeName: "Local").ConfigureAwait(false); return default; } diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Workflows/EditTableV2.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Workflows/EditTableV2.cs index 0c612dcae99..5b2fb4eecf2 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Workflows/EditTableV2.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Workflows/EditTableV2.cs @@ -56,8 +56,8 @@ internal sealed class SetVarExecutor(FormulaSession session) : ActionExecutor(id // protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) { - object? evaluatedValue = await context.EvaluateValueAsync("[{id: 3}]").ConfigureAwait(false); - await context.QueueStateUpdateAsync(key: "MyTable", value: evaluatedValue, scopeName: "Local").ConfigureAwait(false); + var evaluatedValue = await context.EvaluateValueWithSensitivityAsync("[{id: 3}]").ConfigureAwait(false); + await context.QueueStateUpdateWithSensitivityAsync(key: "MyTable", value: evaluatedValue, scopeName: "Local").ConfigureAwait(false); return default; } diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Workflows/LoopBreak.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Workflows/LoopBreak.cs index 04459394b09..6833b323d44 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Workflows/LoopBreak.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Workflows/LoopBreak.cs @@ -119,8 +119,8 @@ internal sealed class SetVariableInnerExecutor(FormulaSession session) : ActionE // protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) { - object? evaluatedValue = await context.EvaluateValueAsync("Local.Count + 1").ConfigureAwait(false); - await context.QueueStateUpdateAsync(key: "Count", value: evaluatedValue, scopeName: "Local").ConfigureAwait(false); + var evaluatedValue = await context.EvaluateValueWithSensitivityAsync("Local.Count + 1").ConfigureAwait(false); + await context.QueueStateUpdateWithSensitivityAsync(key: "Count", value: evaluatedValue, scopeName: "Local").ConfigureAwait(false); return default; } diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Workflows/LoopContinue.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Workflows/LoopContinue.cs index d023184476a..616e89d0d3a 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Workflows/LoopContinue.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Workflows/LoopContinue.cs @@ -119,8 +119,8 @@ internal sealed class SetVariableInnerExecutor(FormulaSession session) : ActionE // protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) { - object? evaluatedValue = await context.EvaluateValueAsync("Local.Count + 1").ConfigureAwait(false); - await context.QueueStateUpdateAsync(key: "Count", value: evaluatedValue, scopeName: "Local").ConfigureAwait(false); + var evaluatedValue = await context.EvaluateValueWithSensitivityAsync("Local.Count + 1").ConfigureAwait(false); + await context.QueueStateUpdateWithSensitivityAsync(key: "Count", value: evaluatedValue, scopeName: "Local").ConfigureAwait(false); return default; } diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Workflows/LoopEach.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Workflows/LoopEach.cs index a467d42f338..dcb4507f931 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Workflows/LoopEach.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Workflows/LoopEach.cs @@ -119,8 +119,8 @@ internal sealed class SetVariableInnerExecutor(FormulaSession session) : ActionE // protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) { - object? evaluatedValue = await context.EvaluateValueAsync("Local.Count + 1").ConfigureAwait(false); - await context.QueueStateUpdateAsync(key: "Count", value: evaluatedValue, scopeName: "Local").ConfigureAwait(false); + var evaluatedValue = await context.EvaluateValueWithSensitivityAsync("Local.Count + 1").ConfigureAwait(false); + await context.QueueStateUpdateWithSensitivityAsync(key: "Count", value: evaluatedValue, scopeName: "Local").ConfigureAwait(false); return default; } diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Workflows/SetTextVariable.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Workflows/SetTextVariable.cs index 98b3bf23511..20157631ee7 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Workflows/SetTextVariable.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Workflows/SetTextVariable.cs @@ -55,12 +55,12 @@ internal sealed class SetTextExecutor(FormulaSession session) : ActionExecutor(i { protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) { - string textValue = - await context.FormatTemplateAsync( + var textValue = + await context.FormatTemplateWithSensitivityAsync( """ Test content """); - await context.QueueStateUpdateAsync(key: "TestVar", value: textValue, scopeName: "Local").ConfigureAwait(false); + await context.QueueStateUpdateWithSensitivityAsync(key: "TestVar", value: textValue, scopeName: "Local").ConfigureAwait(false); return default; } diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Workflows/SetVariable.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Workflows/SetVariable.cs index 84bf8ff5f56..b1939dd0d3a 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Workflows/SetVariable.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Workflows/SetVariable.cs @@ -56,8 +56,8 @@ internal sealed class SetVarExecutor(FormulaSession session) : ActionExecutor(id // protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) { - object? evaluatedValue = await context.EvaluateValueAsync("3").ConfigureAwait(false); - await context.QueueStateUpdateAsync(key: "TestVar", value: evaluatedValue, scopeName: "Local").ConfigureAwait(false); + var evaluatedValue = await context.EvaluateValueWithSensitivityAsync("3").ConfigureAwait(false); + await context.QueueStateUpdateWithSensitivityAsync(key: "TestVar", value: evaluatedValue, scopeName: "Local").ConfigureAwait(false); return default; } From c5425a5acc2294031d2bc2b066727d7239a32524 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Wed, 16 Sep 2026 15:12:48 -0400 Subject: [PATCH 22/46] fix: missing sensitivity for state --- .../Kit/IWorkflowContextExtensions.cs | 20 +++++++++++++++++++ .../PowerFx/WorkflowExpressionEngine.cs | 9 +++++++-- .../PowerFx/WorkflowFormulaState.cs | 12 +++++++++++ .../PublicAPI/net10.0/PublicAPI.Unshipped.txt | 1 + .../PublicAPI/net472/PublicAPI.Unshipped.txt | 1 + .../PublicAPI/net8.0/PublicAPI.Unshipped.txt | 1 + .../PublicAPI/net9.0/PublicAPI.Unshipped.txt | 1 + .../netstandard2.0/PublicAPI.Unshipped.txt | 1 + .../Kit/IWorkflowContextExtensionsTests.cs | 20 +++++++++++++++++++ .../PowerFx/WorkflowExpressionEngineTests.cs | 16 +++++++++++++++ .../Workflows/ParseValue.cs | 4 ++-- 11 files changed, 82 insertions(+), 4 deletions(-) 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 a4182e7c518..81569e2c575 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Kit/IWorkflowContextExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Kit/IWorkflowContextExtensions.cs @@ -264,6 +264,26 @@ public static async ValueTask QueueStateUpdateWithSensitivityAsync( return sourceValue.ConvertType(targetType); } + /// + /// Convert the variable value to the specified target type while preserving sensitivity metadata. + /// + /// 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. + /// A token that propagates notification when operation should be canceled. + /// The converted value and its sensitivity metadata. + public static async ValueTask> ConvertValueWithSensitivityAsync( + this IWorkflowContext context, + VariableType targetType, + string key, + string? scopeName = null, + CancellationToken cancellationToken = default) + { + EvaluationResult sourceValue = await context.ReadStateWithSensitivityAsync(key, scopeName, cancellationToken).ConfigureAwait(false); + return new(sourceValue.Value.ConvertType(targetType), sourceValue.Sensitivity); + } + /// /// Evaluate an expression using the workflow's declarative state. /// 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 158a6ed5c1a..eb0232d1b0f 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PowerFx/WorkflowExpressionEngine.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PowerFx/WorkflowExpressionEngine.cs @@ -341,7 +341,7 @@ private SensitivityLevel GetSensitivity(ExpressionBase expression) { if (expression.VariableReference is { VariableName: string variableName }) { - return this._state.GetSensitivity(variableName, expression.VariableReference.NamespaceAlias); + return GetReferenceSensitivity(expression.VariableReference.NamespaceAlias, variableName); } string? expressionText = expression.ExpressionText; @@ -356,10 +356,15 @@ private SensitivityLevel GetSensitivity(ExpressionBase expression) SensitivityLevel sensitivity = SensitivityLevel.None; foreach ((string? ScopeName, string VariableName) reference in GetVariableReferences(checkResult.Parse.Root)) { - sensitivity = MaxSensitivity(sensitivity, this._state.GetSensitivity(reference.VariableName, reference.ScopeName)); + 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) 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 6851c9cd9c9..0b7f7826abb 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PowerFx/WorkflowFormulaState.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PowerFx/WorkflowFormulaState.cs @@ -82,6 +82,18 @@ public SensitivityLevel GetSensitivity(string variableName, string? scopeName = 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; 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 2e87ed9adec..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 @@ -5,6 +5,7 @@ Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.AllowedEnvi 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!> 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 2e87ed9adec..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 @@ -5,6 +5,7 @@ Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.AllowedEnvi 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!> 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 2e87ed9adec..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 @@ -5,6 +5,7 @@ Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.AllowedEnvi 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!> 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 2e87ed9adec..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 @@ -5,6 +5,7 @@ Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.AllowedEnvi 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!> 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 2e87ed9adec..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 @@ -5,6 +5,7 @@ Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.AllowedEnvi 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!> 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 index 4b90caaa03a..f8338af17b9 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Kit/IWorkflowContextExtensionsTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Kit/IWorkflowContextExtensionsTests.cs @@ -160,4 +160,24 @@ public async Task QueueStateUpdateWithSensitivityAsync_WithPlainContext_QueuesSe // Assert context.VerifyAll(); } + + [Fact] + public async Task ConvertValueWithSensitivityAsync_WithPlainContext_PreservesSensitivitySidecarAsync() + { + // Arrange + Mock context = new(MockBehavior.Loose); + context + .Setup(c => c.ReadStateAsync("TestValue", VariableScopeNames.Local, default)) + .Returns(new ValueTask("42")); + context + .Setup(c => c.ReadStateAsync("TestValue", WorkflowFormulaState.GetSensitivityScopeName(VariableScopeNames.Local), default)) + .Returns(new ValueTask(SensitivityLevel.Sensitive)); + + // Act + EvaluationResult result = await context.Object.ConvertValueWithSensitivityAsync(typeof(decimal), "TestValue", VariableScopeNames.Local); + + // Assert + Assert.Equal(42M, result.Value); + Assert.Equal(SensitivityLevel.Sensitive, result.Sensitivity); + } } diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/PowerFx/WorkflowExpressionEngineTests.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/PowerFx/WorkflowExpressionEngineTests.cs index 611953e55b1..87054d436f7 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/PowerFx/WorkflowExpressionEngineTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/PowerFx/WorkflowExpressionEngineTests.cs @@ -151,6 +151,22 @@ public void StringExpressionGetValueForQuotedEnvironmentVariableIsSensitive() expectedSensitivity: SensitivityLevel.Sensitive); } + [Fact] + public void ValueExpressionGetValueForEnvironmentScopeIsSensitive() + { + // Arrange + this.State.Set("SOME_SECRET", FormulaValue.New("secret-value"), VariableScopeNames.Environment, SensitivityLevel.Sensitive); + this.State.Set("PUBLIC_VALUE", FormulaValue.New("public-value"), VariableScopeNames.Environment); + this.State.Bind(); + + // Act + EvaluationResult result = this.State.Evaluator.GetValue(ValueExpression.Expression("Env")); + + // Assert + Assert.Equal(SensitivityLevel.Sensitive, result.Sensitivity); + Assert.NotNull(result.Value.ToObject()); + } + [Fact] public void StringExpressionGetValueForComputedDottedAccessIsSensitive() { diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Workflows/ParseValue.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Workflows/ParseValue.cs index 55d4ba1efb3..d254ab9498c 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Workflows/ParseValue.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Workflows/ParseValue.cs @@ -73,8 +73,8 @@ internal sealed class ParseVarExecutor(FormulaSession session) : ActionExecutor( protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) { VariableType targetType = typeof(decimal); - object? parsedValue = await context.ConvertValueAsync(targetType, key: "MySource", scopeName: "Local", cancellationToken).ConfigureAwait(false); - await context.QueueStateUpdateAsync(key: "MyVar", value: parsedValue, scopeName: "Local").ConfigureAwait(false); + var parsedValue = await context.ConvertValueWithSensitivityAsync(targetType, key: "MySource", scopeName: "Local", cancellationToken).ConfigureAwait(false); + await context.QueueStateUpdateWithSensitivityAsync(key: "MyVar", value: parsedValue, scopeName: "Local").ConfigureAwait(false); return default; } From a27a86f8f22764fa028d57dd1c544870f26cee65 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Thu, 17 Sep 2026 09:03:18 -0400 Subject: [PATCH 23/46] tests: adds additional tests to cover edge scenarios --- .../ChatClient/ChatClientAgentFactoryTests.cs | 4 +- .../DeclarativeWorkflowTest.cs | 107 ++++++++++++++++++ 2 files changed, 109 insertions(+), 2 deletions(-) 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 d9ff1233c5d..2fcfbdd6341 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Declarative.UnitTests/ChatClient/ChatClientAgentFactoryTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Declarative.UnitTests/ChatClient/ChatClientAgentFactoryTests.cs @@ -234,7 +234,7 @@ public async Task TryCreateAsync_OnlyLoadsAllowedReferencedConfigurationAsync() .AddInMemoryCollection(new Dictionary { ["Temperature"] = "0.9", - ["SOME_SECRET"] = "secret-value", + ["TopP"] = "0.8", }) .Build(); GptComponentMetadata promptAgent = AgentBotElementYaml.FromYaml(PromptAgents.AgentWithVariableReferences); @@ -246,7 +246,7 @@ public async Task TryCreateAsync_OnlyLoadsAllowedReferencedConfigurationAsync() // Assert StringValue temperature = Assert.IsType(factory.Evaluate("Temperature")); Assert.Equal("0.9", temperature.Value); - Assert.False(factory.CanEvaluate("SOME_SECRET")); + Assert.False(factory.CanEvaluate("TopP")); } private sealed class InspectingPromptAgentFactory(IConfiguration configuration, IEnumerable allowedConfigurationVariables) 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..e523a88f4aa 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,99 @@ 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 GotoActionAsync() { @@ -372,6 +468,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"); From fc61f4db90f55e1626c6cd57012aaff239fd517a Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Thu, 17 Sep 2026 09:30:39 -0400 Subject: [PATCH 24/46] chore: Change ReadStateWithSensitivityAsync to use PortableValue Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../Kit/IWorkflowContextExtensions.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 81569e2c575..3217ea774af 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Kit/IWorkflowContextExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Kit/IWorkflowContextExtensions.cs @@ -280,8 +280,9 @@ public static async ValueTask QueueStateUpdateWithSensitivityAsync( string? scopeName = null, CancellationToken cancellationToken = default) { - EvaluationResult sourceValue = await context.ReadStateWithSensitivityAsync(key, scopeName, cancellationToken).ConfigureAwait(false); - return new(sourceValue.Value.ConvertType(targetType), sourceValue.Sensitivity); + EvaluationResult sourceValue = await context.ReadStateWithSensitivityAsync(key, scopeName, cancellationToken).ConfigureAwait(false); + object? convertedValue = sourceValue.Value is null ? null : sourceValue.Value.ToFormula().ToObject().ConvertType(targetType); + return new(convertedValue, sourceValue.Sensitivity); } /// From 4a795f5e7744a36f1b30c338817a99e3fda5f5c6 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Thu, 17 Sep 2026 10:20:55 -0400 Subject: [PATCH 25/46] tests: fixes build failure because of copilot suggestion --- .../Kit/IWorkflowContextExtensionsTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 index f8338af17b9..e367f24b26d 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Kit/IWorkflowContextExtensionsTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Kit/IWorkflowContextExtensionsTests.cs @@ -167,8 +167,8 @@ public async Task ConvertValueWithSensitivityAsync_WithPlainContext_PreservesSen // Arrange Mock context = new(MockBehavior.Loose); context - .Setup(c => c.ReadStateAsync("TestValue", VariableScopeNames.Local, default)) - .Returns(new ValueTask("42")); + .Setup(c => c.ReadStateAsync("TestValue", VariableScopeNames.Local, default)) + .Returns(new ValueTask(new PortableValue("42"))); context .Setup(c => c.ReadStateAsync("TestValue", WorkflowFormulaState.GetSensitivityScopeName(VariableScopeNames.Local), default)) .Returns(new ValueTask(SensitivityLevel.Sensitive)); From 6d06022f17bcd411370577916850534528745e9d Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Thu, 17 Sep 2026 12:13:44 -0400 Subject: [PATCH 26/46] fix: pass the sensitivity through the foreach loop --- .../Kit/IWorkflowContextExtensionsTests.cs | 33 +++++++++++ .../Workflows/LoopBreak.cs | 57 ++++++++++++++++--- .../Workflows/LoopContinue.cs | 57 ++++++++++++++++--- .../Workflows/LoopEach.cs | 57 ++++++++++++++++--- 4 files changed, 177 insertions(+), 27 deletions(-) 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 index e367f24b26d..d26a1b6a84b 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Kit/IWorkflowContextExtensionsTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Kit/IWorkflowContextExtensionsTests.cs @@ -1,5 +1,7 @@ // 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; @@ -161,6 +163,37 @@ public async Task QueueStateUpdateWithSensitivityAsync_WithPlainContext_QueuesSe context.VerifyAll(); } + [Fact] + public async Task GeneratedForeachPattern_WithSensitiveCollection_PreservesItemSensitivityAsync() + { + // Arrange + WorkflowFormulaState state = new(RecalcEngineFactory.Create()); + state.Set( + "SensitiveItems", + FormulaValue.NewTable( + RecordType.Empty(), + FormulaValue.NewRecordFromFields(new NamedValue("Value", FormulaValue.New("first"))), + FormulaValue.NewRecordFromFields(new NamedValue("Value", FormulaValue.New("second")))), + VariableScopeNames.Environment, + SensitivityLevel.Sensitive); + state.Bind(); + DeclarativeWorkflowContext context = new(new Mock().Object, state); + + // Act + EvaluationResult evaluatedValue = await context.EvaluateValueWithSensitivityAsync("Env.SensitiveItems"); + IEnumerable values = Assert.IsAssignableFrom(evaluatedValue.Value); + object? firstValue = values.Cast().First(); + await context.QueueStateUpdateWithSensitivityAsync( + key: "LoopValue", + value: new EvaluationResult(firstValue, evaluatedValue.Sensitivity), + scopeName: VariableScopeNames.Local); + + // Assert + Assert.Equal(SensitivityLevel.Sensitive, evaluatedValue.Sensitivity); + Assert.NotNull(state.Get("LoopValue").ToObject()); + Assert.Equal(SensitivityLevel.Sensitive, state.GetSensitivity("LoopValue")); + } + [Fact] public async Task ConvertValueWithSensitivityAsync_WithPlainContext_PreservesSensitivitySidecarAsync() { diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Workflows/LoopBreak.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Workflows/LoopBreak.cs index 6833b323d44..26525b6b710 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Workflows/LoopBreak.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Workflows/LoopBreak.cs @@ -17,6 +17,8 @@ using Microsoft.Agents.AI.Workflows; using Microsoft.Agents.AI.Workflows.Declarative; using Microsoft.Agents.AI.Workflows.Declarative.Kit; +using Microsoft.Agents.ObjectModel; +using Microsoft.Agents.ObjectModel.Abstractions; using Microsoft.Extensions.AI; namespace Test.WorkflowProviders; @@ -55,8 +57,14 @@ protected override async ValueTask ExecuteAsync(TInput message, IWorkflowContext /// internal sealed class ForeachLoopExecutor(FormulaSession session) : ActionExecutor(id: "foreach_loop", session) { + 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 object[] _values = []; + private PortableValue[] _values = []; + private SensitivityLevel _sensitivity; public bool HasValue { get; private set; } @@ -64,22 +72,23 @@ internal sealed class ForeachLoopExecutor(FormulaSession session) : ActionExecut protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) { this._index = 0; - object? evaluatedValue = await context.EvaluateValueAsync("""["a", "b", "c", "d", "e", "f"]""").ConfigureAwait(false); + EvaluationResult evaluatedValue = await context.EvaluateValueWithSensitivityAsync("""["a", "b", "c", "d", "e", "f"]""").ConfigureAwait(false); - if (evaluatedValue == null) + if (evaluatedValue.Value == null) { this._values = []; this.HasValue = false; } else - if (evaluatedValue is IEnumerable evaluatedList) + if (evaluatedValue.Value is IEnumerable evaluatedList) { - this._values = [.. evaluatedList]; + this._values = [.. evaluatedList.Cast().Select(ToPortableValue)]; } else { - this._values = [evaluatedValue]; + this._values = [ToPortableValue(evaluatedValue.Value)]; } + this._sensitivity = evaluatedValue.Sensitivity; await this.ResetAsync(context, cancellationToken).ConfigureAwait(false); @@ -90,10 +99,10 @@ public async ValueTask TakeNextAsync(IWorkflowContext context, object? _, Cancel { if (this.HasValue = this._index < this._values.Length) { - object value = this._values[this._index]; + object? value = this._values[this._index].NormalizePortableValue(); - await context.QueueStateUpdateAsync(key: "LoopValue", value: value, scopeName: "Local").ConfigureAwait(false); - await context.QueueStateUpdateAsync(key: "LoopIndex", value: this._index, scopeName: "Local").ConfigureAwait(false); + await context.QueueStateUpdateWithSensitivityAsync(key: "LoopValue", value: new EvaluationResult(value, this._sensitivity), scopeName: "Local").ConfigureAwait(false); + await context.QueueStateUpdateAsync(key: "LoopIndex", value: this._index, scopeName: "Local").ConfigureAwait(false); this._index++; } @@ -109,6 +118,36 @@ private async ValueTask ResetAsync(IWorkflowContext context, CancellationToken c await context.QueueStateUpdateAsync(key: "LoopValue", value: UnassignedValue.Instance, scopeName: "Local").ConfigureAwait(false); await context.QueueStateUpdateAsync(key: "LoopIndex", value: UnassignedValue.Instance, scopeName: "Local").ConfigureAwait(false); } + + protected override async ValueTask OnCheckpointingAsync(IWorkflowContext context, CancellationToken cancellationToken = default) + { + await context.QueueStateUpdateAsync(IndexStateKey, this._index, cancellationToken: cancellationToken).ConfigureAwait(false); + await context.QueueStateUpdateAsync(ValuesStateKey, this._values, 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); + } + + protected override async ValueTask OnCheckpointRestoredAsync(IWorkflowContext context, CancellationToken cancellationToken = default) + { + PortableValue[]? savedValues = + await context.ReadStateAsync(ValuesStateKey, cancellationToken: cancellationToken).ConfigureAwait(false); + if (savedValues is null) + { + return; + } + + this._values = savedValues; + 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); + + await base.OnCheckpointRestoredAsync(context, cancellationToken).ConfigureAwait(false); + } + + private static PortableValue ToPortableValue(object? value) => + new(value ?? UnassignedValue.Instance); } /// diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Workflows/LoopContinue.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Workflows/LoopContinue.cs index 616e89d0d3a..4e6f09438a7 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Workflows/LoopContinue.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Workflows/LoopContinue.cs @@ -17,6 +17,8 @@ using Microsoft.Agents.AI.Workflows; using Microsoft.Agents.AI.Workflows.Declarative; using Microsoft.Agents.AI.Workflows.Declarative.Kit; +using Microsoft.Agents.ObjectModel; +using Microsoft.Agents.ObjectModel.Abstractions; using Microsoft.Extensions.AI; namespace Test.WorkflowProviders; @@ -55,8 +57,14 @@ protected override async ValueTask ExecuteAsync(TInput message, IWorkflowContext /// internal sealed class ForeachLoopExecutor(FormulaSession session) : ActionExecutor(id: "foreach_loop", session) { + 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 object[] _values = []; + private PortableValue[] _values = []; + private SensitivityLevel _sensitivity; public bool HasValue { get; private set; } @@ -64,22 +72,23 @@ internal sealed class ForeachLoopExecutor(FormulaSession session) : ActionExecut protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) { this._index = 0; - object? evaluatedValue = await context.EvaluateValueAsync("""["a", "b", "c", "d", "e", "f"]""").ConfigureAwait(false); + EvaluationResult evaluatedValue = await context.EvaluateValueWithSensitivityAsync("""["a", "b", "c", "d", "e", "f"]""").ConfigureAwait(false); - if (evaluatedValue == null) + if (evaluatedValue.Value == null) { this._values = []; this.HasValue = false; } else - if (evaluatedValue is IEnumerable evaluatedList) + if (evaluatedValue.Value is IEnumerable evaluatedList) { - this._values = [.. evaluatedList]; + this._values = [.. evaluatedList.Cast().Select(ToPortableValue)]; } else { - this._values = [evaluatedValue]; + this._values = [ToPortableValue(evaluatedValue.Value)]; } + this._sensitivity = evaluatedValue.Sensitivity; await this.ResetAsync(context, cancellationToken).ConfigureAwait(false); @@ -90,10 +99,10 @@ public async ValueTask TakeNextAsync(IWorkflowContext context, object? _, Cancel { if (this.HasValue = this._index < this._values.Length) { - object value = this._values[this._index]; + object? value = this._values[this._index].NormalizePortableValue(); - await context.QueueStateUpdateAsync(key: "LoopValue", value: value, scopeName: "Local").ConfigureAwait(false); - await context.QueueStateUpdateAsync(key: "LoopIndex", value: this._index, scopeName: "Local").ConfigureAwait(false); + await context.QueueStateUpdateWithSensitivityAsync(key: "LoopValue", value: new EvaluationResult(value, this._sensitivity), scopeName: "Local").ConfigureAwait(false); + await context.QueueStateUpdateAsync(key: "LoopIndex", value: this._index, scopeName: "Local").ConfigureAwait(false); this._index++; } @@ -109,6 +118,36 @@ private async ValueTask ResetAsync(IWorkflowContext context, CancellationToken c await context.QueueStateUpdateAsync(key: "LoopValue", value: UnassignedValue.Instance, scopeName: "Local").ConfigureAwait(false); await context.QueueStateUpdateAsync(key: "LoopIndex", value: UnassignedValue.Instance, scopeName: "Local").ConfigureAwait(false); } + + protected override async ValueTask OnCheckpointingAsync(IWorkflowContext context, CancellationToken cancellationToken = default) + { + await context.QueueStateUpdateAsync(IndexStateKey, this._index, cancellationToken: cancellationToken).ConfigureAwait(false); + await context.QueueStateUpdateAsync(ValuesStateKey, this._values, 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); + } + + protected override async ValueTask OnCheckpointRestoredAsync(IWorkflowContext context, CancellationToken cancellationToken = default) + { + PortableValue[]? savedValues = + await context.ReadStateAsync(ValuesStateKey, cancellationToken: cancellationToken).ConfigureAwait(false); + if (savedValues is null) + { + return; + } + + this._values = savedValues; + 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); + + await base.OnCheckpointRestoredAsync(context, cancellationToken).ConfigureAwait(false); + } + + private static PortableValue ToPortableValue(object? value) => + new(value ?? UnassignedValue.Instance); } /// diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Workflows/LoopEach.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Workflows/LoopEach.cs index dcb4507f931..ffacf0fbd19 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Workflows/LoopEach.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Workflows/LoopEach.cs @@ -17,6 +17,8 @@ using Microsoft.Agents.AI.Workflows; using Microsoft.Agents.AI.Workflows.Declarative; using Microsoft.Agents.AI.Workflows.Declarative.Kit; +using Microsoft.Agents.ObjectModel; +using Microsoft.Agents.ObjectModel.Abstractions; using Microsoft.Extensions.AI; namespace Test.WorkflowProviders; @@ -55,8 +57,14 @@ protected override async ValueTask ExecuteAsync(TInput message, IWorkflowContext /// internal sealed class ForeachLoopExecutor(FormulaSession session) : ActionExecutor(id: "foreach_loop", session) { + 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 object[] _values = []; + private PortableValue[] _values = []; + private SensitivityLevel _sensitivity; public bool HasValue { get; private set; } @@ -64,22 +72,23 @@ internal sealed class ForeachLoopExecutor(FormulaSession session) : ActionExecut protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) { this._index = 0; - object? evaluatedValue = await context.EvaluateValueAsync("""["a", "b", "c", "d", "e", "f"]""").ConfigureAwait(false); + EvaluationResult evaluatedValue = await context.EvaluateValueWithSensitivityAsync("""["a", "b", "c", "d", "e", "f"]""").ConfigureAwait(false); - if (evaluatedValue == null) + if (evaluatedValue.Value == null) { this._values = []; this.HasValue = false; } else - if (evaluatedValue is IEnumerable evaluatedList) + if (evaluatedValue.Value is IEnumerable evaluatedList) { - this._values = [.. evaluatedList]; + this._values = [.. evaluatedList.Cast().Select(ToPortableValue)]; } else { - this._values = [evaluatedValue]; + this._values = [ToPortableValue(evaluatedValue.Value)]; } + this._sensitivity = evaluatedValue.Sensitivity; await this.ResetAsync(context, cancellationToken).ConfigureAwait(false); @@ -90,10 +99,10 @@ public async ValueTask TakeNextAsync(IWorkflowContext context, object? _, Cancel { if (this.HasValue = this._index < this._values.Length) { - object value = this._values[this._index]; + object? value = this._values[this._index].NormalizePortableValue(); - await context.QueueStateUpdateAsync(key: "LoopValue", value: value, scopeName: "Local").ConfigureAwait(false); - await context.QueueStateUpdateAsync(key: "LoopIndex", value: this._index, scopeName: "Local").ConfigureAwait(false); + await context.QueueStateUpdateWithSensitivityAsync(key: "LoopValue", value: new EvaluationResult(value, this._sensitivity), scopeName: "Local").ConfigureAwait(false); + await context.QueueStateUpdateAsync(key: "LoopIndex", value: this._index, scopeName: "Local").ConfigureAwait(false); this._index++; } @@ -109,6 +118,36 @@ private async ValueTask ResetAsync(IWorkflowContext context, CancellationToken c await context.QueueStateUpdateAsync(key: "LoopValue", value: UnassignedValue.Instance, scopeName: "Local").ConfigureAwait(false); await context.QueueStateUpdateAsync(key: "LoopIndex", value: UnassignedValue.Instance, scopeName: "Local").ConfigureAwait(false); } + + protected override async ValueTask OnCheckpointingAsync(IWorkflowContext context, CancellationToken cancellationToken = default) + { + await context.QueueStateUpdateAsync(IndexStateKey, this._index, cancellationToken: cancellationToken).ConfigureAwait(false); + await context.QueueStateUpdateAsync(ValuesStateKey, this._values, 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); + } + + protected override async ValueTask OnCheckpointRestoredAsync(IWorkflowContext context, CancellationToken cancellationToken = default) + { + PortableValue[]? savedValues = + await context.ReadStateAsync(ValuesStateKey, cancellationToken: cancellationToken).ConfigureAwait(false); + if (savedValues is null) + { + return; + } + + this._values = savedValues; + 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); + + await base.OnCheckpointRestoredAsync(context, cancellationToken).ConfigureAwait(false); + } + + private static PortableValue ToPortableValue(object? value) => + new(value ?? UnassignedValue.Instance); } /// From 132addeaddb4b412615db3d041d0538987b6f242 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Thu, 17 Sep 2026 13:07:39 -0400 Subject: [PATCH 27/46] tests: fixes build failure for netfx --- .../SampleSmokeTest.cs | 45 +++++-------------- 1 file changed, 12 insertions(+), 33 deletions(-) diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/SampleSmokeTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/SampleSmokeTest.cs index fa1ce7b974f..759c61a160b 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/SampleSmokeTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/SampleSmokeTest.cs @@ -117,17 +117,7 @@ internal async Task Test_RunSample_Step5Async(ExecutionEnvironment environment) { using StringWriter writer = new(); - VerifyingPlaybackResponder responder = new( - // Iteration 1 - ("Guess the number.", 50), - ("Your guess was too high. Try again.", 23), - - // Iteration 2 - ("Your guess was too high. Try again.", 23), - ("Your guess was too low. Try again.", 42) - ); - - string guessResult = await Step5EntryPoint.RunAsync(writer, userGuessCallback: responder.InvokeNext, environment.ToWorkflowExecutionEnvironment()); + string guessResult = await Step5EntryPoint.RunAsync(writer, userGuessCallback: RespondToGuessPrompt, environment.ToWorkflowExecutionEnvironment()); Assert.Equal("You guessed correctly! You Win!", guessResult); } @@ -139,17 +129,7 @@ internal async Task Test_RunSample_Step5aAsync(ExecutionEnvironment environment) { using StringWriter writer = new(); - VerifyingPlaybackResponder responder = new( - // Iteration 1 - ("Guess the number.", 50), - ("Your guess was too high. Try again.", 23), - - // Iteration 2 - ("Your guess was too high. Try again.", 23), - ("Your guess was too low. Try again.", 42) - ); - - string guessResult = await Step5EntryPoint.RunAsync(writer, userGuessCallback: responder.InvokeNext, environment.ToWorkflowExecutionEnvironment(), rehydrateToRestore: true); + string guessResult = await Step5EntryPoint.RunAsync(writer, userGuessCallback: RespondToGuessPrompt, environment.ToWorkflowExecutionEnvironment(), rehydrateToRestore: true); Assert.Equal("You guessed correctly! You Win!", guessResult); } @@ -161,21 +141,11 @@ internal async Task Test_RunSample_Step5bAsync(ExecutionEnvironment environment) { using StringWriter writer = new(); - VerifyingPlaybackResponder responder = new( - // Iteration 1 - ("Guess the number.", 50), - ("Your guess was too high. Try again.", 23), - - // Iteration 2 - ("Your guess was too high. Try again.", 23), - ("Your guess was too low. Try again.", 42) - ); - JsonSerializerOptions options = new(SampleJsonContext.Default.Options); options.MakeReadOnly(); CheckpointManager memoryJsonManager = CheckpointManager.CreateJson(new InMemoryJsonStore(), options); - string guessResult = await Step5EntryPoint.RunAsync(writer, userGuessCallback: responder.InvokeNext, environment.ToWorkflowExecutionEnvironment(), rehydrateToRestore: true, checkpointManager: memoryJsonManager); + string guessResult = await Step5EntryPoint.RunAsync(writer, userGuessCallback: RespondToGuessPrompt, environment.ToWorkflowExecutionEnvironment(), rehydrateToRestore: true, checkpointManager: memoryJsonManager); Assert.Equal("You guessed correctly! You Win!", guessResult); } @@ -555,6 +525,15 @@ internal async Task Test_RunSample_Step14a_SharedState_IsolatedAcrossSubworkflow Assert.IsType(actualError); } + + private static int RespondToGuessPrompt(string prompt) => + prompt switch + { + "Guess the number." => 50, + "Your guess was too high. Try again." => 23, + "Your guess was too low. Try again." => 42, + _ => throw new InvalidOperationException($"Unexpected prompt: {prompt}") + }; } internal sealed class VerifyingPlaybackResponder From dac7c3218b77cd02ac20ef1f6d05a52e9240d35b Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Thu, 17 Sep 2026 14:07:03 -0400 Subject: [PATCH 28/46] tests: fixes bound validation --- .../Sample/05_Simple_Workflow_Checkpointing.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/Sample/05_Simple_Workflow_Checkpointing.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/Sample/05_Simple_Workflow_Checkpointing.cs index f203f72c58b..9795d668de1 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/Sample/05_Simple_Workflow_Checkpointing.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/Sample/05_Simple_Workflow_Checkpointing.cs @@ -62,9 +62,9 @@ await environment.WithCheckpointing(checkpointManager) Assert.NotNull(result); // Depending on the timing of the response with respect to the underlying workflow - // we may end up with an extra superstep in between. - Assert.True(checkpoints.Count >= 6); - Assert.True(checkpoints.Count <= 7); + // we may end up with extra supersteps in between. + Assert.True(checkpoints.Count >= 6, $"Expected at least 6 checkpoints, got {checkpoints.Count}."); + Assert.True(checkpoints.Count <= 12, $"Expected at most 12 checkpoints, got {checkpoints.Count}."); cancellationSource.Dispose(); From c2cfbd6031d7fb34541c6f86fe162ec586b54cf9 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Thu, 17 Sep 2026 14:50:18 -0400 Subject: [PATCH 29/46] chore: moves new class to dedicated file --- .../ChatClientPromptAgentFactory.cs | 36 --------------- .../ChatClientPromptAgentFactoryOptions.cs | 44 +++++++++++++++++++ 2 files changed, 44 insertions(+), 36 deletions(-) create mode 100644 dotnet/src/Microsoft.Agents.AI.Declarative/ChatClient/ChatClientPromptAgentFactoryOptions.cs diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative/ChatClient/ChatClientPromptAgentFactory.cs b/dotnet/src/Microsoft.Agents.AI.Declarative/ChatClient/ChatClientPromptAgentFactory.cs index 2be060b1318..e7081e97754 100644 --- a/dotnet/src/Microsoft.Agents.AI.Declarative/ChatClient/ChatClientPromptAgentFactory.cs +++ b/dotnet/src/Microsoft.Agents.AI.Declarative/ChatClient/ChatClientPromptAgentFactory.cs @@ -103,39 +103,3 @@ private static ChatClientPromptAgentFactoryOptions ValidateOptions(ChatClientPro Throw.IfNull(options); #endregion } - -/// -/// Options for configuring . -/// -public sealed class ChatClientPromptAgentFactoryOptions -{ - /// - /// Gets or sets configuration keys that may be exposed to Power Fx when the agent definition references them through Env. - /// - public IEnumerable? AllowedConfigurationVariables { get; init; } - - /// - /// Gets or sets an optional Power Fx engine used to evaluate declarative expressions. - /// - public RecalcEngine? Engine { get; init; } - - /// - /// Gets or sets optional configuration used to resolve explicitly allowed environment variables referenced by the agent definition. - /// - public IConfiguration? Configuration { get; init; } - - /// - /// Gets or sets an optional logger factory used by created agents. - /// - public ILoggerFactory? LoggerFactory { get; init; } - - /// - /// Gets or sets an optional maximum length for Power Fx expressions evaluated by the factory-created engine. - /// - public int? MaximumExpressionLength { get; init; } - - /// - /// Gets or sets an optional maximum nested call depth for Power Fx expressions evaluated by the factory-created engine. - /// - public int? MaximumCallDepth { get; init; } -} diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative/ChatClient/ChatClientPromptAgentFactoryOptions.cs b/dotnet/src/Microsoft.Agents.AI.Declarative/ChatClient/ChatClientPromptAgentFactoryOptions.cs new file mode 100644 index 00000000000..0d552af5642 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Declarative/ChatClient/ChatClientPromptAgentFactoryOptions.cs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Microsoft.PowerFx; + +namespace Microsoft.Agents.AI; + +/// +/// Options for configuring . +/// +public sealed class ChatClientPromptAgentFactoryOptions +{ + /// + /// Gets or sets configuration keys that may be exposed to Power Fx when the agent definition references them through Env. + /// + public IEnumerable? AllowedConfigurationVariables { get; init; } + + /// + /// Gets or sets an optional Power Fx engine used to evaluate declarative expressions. + /// + public RecalcEngine? Engine { get; init; } + + /// + /// Gets or sets optional configuration used to resolve explicitly allowed environment variables referenced by the agent definition. + /// + public IConfiguration? Configuration { get; init; } + + /// + /// Gets or sets an optional logger factory used by created agents. + /// + public ILoggerFactory? LoggerFactory { get; init; } + + /// + /// Gets or sets an optional maximum length for Power Fx expressions evaluated by the factory-created engine. + /// + public int? MaximumExpressionLength { get; init; } + + /// + /// Gets or sets an optional maximum nested call depth for Power Fx expressions evaluated by the factory-created engine. + /// + public int? MaximumCallDepth { get; init; } +} From e0ef49cde65e31d554655e02f1faf41495a0828b Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Thu, 17 Sep 2026 15:32:51 -0400 Subject: [PATCH 30/46] tests: fixes netfx build issues --- .../InProc/InProcessRunner.cs | 2 +- .../CheckpointResumeTests.cs | 57 +++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) 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/tests/Microsoft.Agents.AI.Workflows.UnitTests/CheckpointResumeTests.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/CheckpointResumeTests.cs index e1d7af741c6..e5bba589cc0 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/CheckpointResumeTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/CheckpointResumeTests.cs @@ -299,6 +299,63 @@ internal async Task Checkpoint_Restore_ClearsQueuedExternalResponsesBeforeImport Assert.Equal(RunStatus.Idle, finalStatus); } + /// + /// Verifies restored runs continue superstep numbering from the checkpoint's saved step. + /// + [Theory] + [InlineData(ExecutionEnvironment.InProcess_Lockstep, false)] + [InlineData(ExecutionEnvironment.InProcess_Lockstep, true)] + internal async Task Checkpoint_Restore_ContinuesStepNumberFromCheckpointAsync( + ExecutionEnvironment environment, + bool rehydrateToRestore) + { + // Arrange + Workflow workflow = CreateSimpleRequestWorkflow(); + CheckpointManager checkpointManager = CheckpointManager.CreateInMemory(); + InProcessExecutionEnvironment env = environment.ToWorkflowExecutionEnvironment(); + + await using StreamingRun run = await env.WithCheckpointing(checkpointManager) + .RunStreamingAsync(workflow, "Hello"); + + (ExternalRequest pendingRequest, CheckpointInfo checkpoint) = await CapturePendingRequestAndCheckpointAsync(run); + + // Advance the original run so live restore must rewind the tracer. + await run.SendResponseAsync(pendingRequest.CreateResponse("World")); + List firstCompletionEvents = await ReadToHaltAsync(run); + Assert.Empty(firstCompletionEvents.OfType() ?? []); + + if (rehydrateToRestore) + { + await run.DisposeAsync(); + + await using StreamingRun resumedRun = await env.WithCheckpointing(checkpointManager) + .ResumeStreamingAsync(workflow, checkpoint); + + await AssertRestoredRunContinuesFromCheckpointAsync(resumedRun); + } + else + { + await run.RestoreCheckpointAsync(checkpoint); + + await AssertRestoredRunContinuesFromCheckpointAsync(run); + } + + static async ValueTask AssertRestoredRunContinuesFromCheckpointAsync(StreamingRun restoredRun) + { + List restoredEvents = await ReadToHaltAsync(restoredRun); + ExternalRequest replayedRequest = Assert.Single(restoredEvents.OfType() + .Select(evt => evt.Request)); + + await restoredRun.SendResponseAsync(replayedRequest.CreateResponse("Again")); + List restoredCompletionEvents = await ReadToHaltAsync(restoredRun); + + Assert.Empty(restoredCompletionEvents.OfType() ?? []); + SuperStepCompletedEvent? resumedCompletion = restoredCompletionEvents.OfType().FirstOrDefault(); + Assert.NotNull(resumedCompletion); + Assert.Equal(1, resumedCompletion.StepNumber); + } + } + /// /// Verifies that fan-in edge state buffered before a checkpoint is still present after resume. /// From bcb3ddb0fdd7fa2d3d92c764e365994986533a86 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Thu, 17 Sep 2026 15:34:25 -0400 Subject: [PATCH 31/46] fix: adds missing netfx conditional compilation directive --- .../CheckpointResumeTests.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/CheckpointResumeTests.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/CheckpointResumeTests.cs index e5bba589cc0..d725927777c 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/CheckpointResumeTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/CheckpointResumeTests.cs @@ -299,6 +299,7 @@ internal async Task Checkpoint_Restore_ClearsQueuedExternalResponsesBeforeImport Assert.Equal(RunStatus.Idle, finalStatus); } +#if NETFRAMEWORK /// /// Verifies restored runs continue superstep numbering from the checkpoint's saved step. /// @@ -355,6 +356,7 @@ static async ValueTask AssertRestoredRunContinuesFromCheckpointAsync(StreamingRu Assert.Equal(1, resumedCompletion.StepNumber); } } +#endif /// /// Verifies that fan-in edge state buffered before a checkpoint is still present after resume. From adae16a2f034496855c188b6f32ce7ee066d07bc Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Fri, 18 Sep 2026 10:08:24 -0400 Subject: [PATCH 32/46] chore: removes noisy additional options --- .../ChatClientPromptAgentFactory.cs | 46 ++----------------- .../ChatClientPromptAgentFactoryOptions.cs | 44 ------------------ .../ChatClient/ChatClientAgentFactoryTests.cs | 9 ++-- 3 files changed, 8 insertions(+), 91 deletions(-) delete mode 100644 dotnet/src/Microsoft.Agents.AI.Declarative/ChatClient/ChatClientPromptAgentFactoryOptions.cs diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative/ChatClient/ChatClientPromptAgentFactory.cs b/dotnet/src/Microsoft.Agents.AI.Declarative/ChatClient/ChatClientPromptAgentFactory.cs index e7081e97754..e49b6926003 100644 --- a/dotnet/src/Microsoft.Agents.AI.Declarative/ChatClient/ChatClientPromptAgentFactory.cs +++ b/dotnet/src/Microsoft.Agents.AI.Declarative/ChatClient/ChatClientPromptAgentFactory.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. using System.Collections.Generic; -using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.Agents.ObjectModel; @@ -26,52 +25,19 @@ public sealed class ChatClientPromptAgentFactory : PromptAgentFactory /// 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 = null, RecalcEngine? engine = null, IConfiguration? configuration = null, - ILoggerFactory? loggerFactory = null) - : this( - chatClient, - functions, - new ChatClientPromptAgentFactoryOptions() - { - Engine = engine, - Configuration = configuration, - AllowedConfigurationVariables = configuration?.AsEnumerable().Select(static pair => pair.Key), - LoggerFactory = loggerFactory, - }, - isValidated: true) + ILoggerFactory? loggerFactory = null, + IEnumerable? allowedConfigurationVariables = null) + : base(engine, configuration, allowedConfigurationVariables) { - } - - /// - /// Creates a new instance of the class. - /// - /// The chat client used by created agents. - /// Options used to configure the created agents and declarative expression evaluation. - /// Optional functions exposed as tools to created agents. - /// The configured instance. - public static ChatClientPromptAgentFactory Create( - IChatClient chatClient, - ChatClientPromptAgentFactoryOptions options, - IList? functions = null) => - new(chatClient, functions, ValidateOptions(options), isValidated: true); - - private ChatClientPromptAgentFactory( - IChatClient chatClient, - IList? functions, - ChatClientPromptAgentFactoryOptions options, - bool isValidated) : - base(options.Engine, options.Configuration, options.AllowedConfigurationVariables, options.MaximumExpressionLength, options.MaximumCallDepth) - { - _ = isValidated; - Throw.IfNull(chatClient); - this._chatClient = chatClient; this._functions = functions; - this._loggerFactory = options.LoggerFactory; + this._loggerFactory = loggerFactory; } /// @@ -99,7 +65,5 @@ private ChatClientPromptAgentFactory( private readonly IList? _functions; private readonly ILoggerFactory? _loggerFactory; - private static ChatClientPromptAgentFactoryOptions ValidateOptions(ChatClientPromptAgentFactoryOptions? options) => - Throw.IfNull(options); #endregion } diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative/ChatClient/ChatClientPromptAgentFactoryOptions.cs b/dotnet/src/Microsoft.Agents.AI.Declarative/ChatClient/ChatClientPromptAgentFactoryOptions.cs deleted file mode 100644 index 0d552af5642..00000000000 --- a/dotnet/src/Microsoft.Agents.AI.Declarative/ChatClient/ChatClientPromptAgentFactoryOptions.cs +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Collections.Generic; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; -using Microsoft.PowerFx; - -namespace Microsoft.Agents.AI; - -/// -/// Options for configuring . -/// -public sealed class ChatClientPromptAgentFactoryOptions -{ - /// - /// Gets or sets configuration keys that may be exposed to Power Fx when the agent definition references them through Env. - /// - public IEnumerable? AllowedConfigurationVariables { get; init; } - - /// - /// Gets or sets an optional Power Fx engine used to evaluate declarative expressions. - /// - public RecalcEngine? Engine { get; init; } - - /// - /// Gets or sets optional configuration used to resolve explicitly allowed environment variables referenced by the agent definition. - /// - public IConfiguration? Configuration { get; init; } - - /// - /// Gets or sets an optional logger factory used by created agents. - /// - public ILoggerFactory? LoggerFactory { get; init; } - - /// - /// Gets or sets an optional maximum length for Power Fx expressions evaluated by the factory-created engine. - /// - public int? MaximumExpressionLength { get; init; } - - /// - /// Gets or sets an optional maximum nested call depth for Power Fx expressions evaluated by the factory-created engine. - /// - public int? MaximumCallDepth { get; init; } -} diff --git a/dotnet/tests/Microsoft.Agents.AI.Declarative.UnitTests/ChatClient/ChatClientAgentFactoryTests.cs b/dotnet/tests/Microsoft.Agents.AI.Declarative.UnitTests/ChatClient/ChatClientAgentFactoryTests.cs index 2fcfbdd6341..471cb84eb56 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Declarative.UnitTests/ChatClient/ChatClientAgentFactoryTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Declarative.UnitTests/ChatClient/ChatClientAgentFactoryTests.cs @@ -138,13 +138,10 @@ public async Task TryCreateAsync_WithOptions_LoadsAllowedConfigurationAsync() }) .Build(); GptComponentMetadata promptAgent = AgentBotElementYaml.FromYaml(PromptAgents.AgentWithVariableReferences); - ChatClientPromptAgentFactory factory = ChatClientPromptAgentFactory.Create( + ChatClientPromptAgentFactory factory = new( this._mockChatClient.Object, - options: new ChatClientPromptAgentFactoryOptions() - { - Configuration = configuration, - AllowedConfigurationVariables = ["Temperature", "TopP", "OpenAIEndpoint", "OpenAIApiKey"], - }); + configuration: configuration, + allowedConfigurationVariables: ["Temperature", "TopP", "OpenAIEndpoint", "OpenAIApiKey"]); // Act AIAgent? agent = await factory.TryCreateAsync(promptAgent); From 5a19862fd0ae8e0d055454482d8d92ff2209d688 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Fri, 18 Sep 2026 10:16:54 -0400 Subject: [PATCH 33/46] chore: restore missing defensive programming Signed-off-by: Vincent Biret --- .../ChatClient/ChatClientPromptAgentFactory.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative/ChatClient/ChatClientPromptAgentFactory.cs b/dotnet/src/Microsoft.Agents.AI.Declarative/ChatClient/ChatClientPromptAgentFactory.cs index e49b6926003..4d558003f65 100644 --- a/dotnet/src/Microsoft.Agents.AI.Declarative/ChatClient/ChatClientPromptAgentFactory.cs +++ b/dotnet/src/Microsoft.Agents.AI.Declarative/ChatClient/ChatClientPromptAgentFactory.cs @@ -35,6 +35,7 @@ public ChatClientPromptAgentFactory( IEnumerable? allowedConfigurationVariables = null) : base(engine, configuration, allowedConfigurationVariables) { + Throw.IfNull(chatClient); this._chatClient = chatClient; this._functions = functions; this._loggerFactory = loggerFactory; From c51a4dfd2ea847ce1e3d438fce393fd262e2afcc Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Fri, 18 Sep 2026 10:19:55 -0400 Subject: [PATCH 34/46] chore: refactors duplicated constructor Signed-off-by: Vincent Biret --- .../PromptAgentFactory.cs | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative/PromptAgentFactory.cs b/dotnet/src/Microsoft.Agents.AI.Declarative/PromptAgentFactory.cs index 351c37f44c4..164cff237ed 100644 --- a/dotnet/src/Microsoft.Agents.AI.Declarative/PromptAgentFactory.cs +++ b/dotnet/src/Microsoft.Agents.AI.Declarative/PromptAgentFactory.cs @@ -23,16 +23,6 @@ public abstract class PromptAgentFactory 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 used to resolve explicitly allowed environment variables referenced by the agent definition. - protected PromptAgentFactory(RecalcEngine? engine = null, IConfiguration? configuration = null) - : this(engine, configuration, allowedConfigurationVariables: configuration?.AsEnumerable().Select(static pair => pair.Key)) - { - } - /// /// Initializes a new instance of the class. /// @@ -41,16 +31,15 @@ protected PromptAgentFactory(RecalcEngine? engine = null, IConfiguration? config /// 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, + protected PromptAgentFactory(RecalcEngine? engine = null, + IConfiguration? configuration = null, + IEnumerable? allowedConfigurationVariables = null, int? maximumExpressionLength = null, int? maximumCallDepth = null) { this.Engine = engine ?? new RecalcEngine(CreateConfig(maximumExpressionLength, maximumCallDepth)); this._configuration = configuration; - this._allowedConfigurationVariables = new(allowedConfigurationVariables ?? [], StringComparer.OrdinalIgnoreCase); + this._allowedConfigurationVariables = new(allowedConfigurationVariables ?? configuration?.AsEnumerable().Select(static pair => pair.Key), StringComparer.OrdinalIgnoreCase); } private static PowerFxConfig CreateConfig(int? maximumExpressionLength, int? maximumCallDepth) From 24253562ef2a5431c03425b1bb9c886cf78197e3 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Fri, 18 Sep 2026 10:37:26 -0400 Subject: [PATCH 35/46] chore: Initialize Sensitivities dictionary with case-insensitive comparer Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../PowerFx/WorkflowFormulaState.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 0b7f7826abb..3128f208386 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PowerFx/WorkflowFormulaState.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PowerFx/WorkflowFormulaState.cs @@ -238,6 +238,6 @@ public WorkflowScope(IDictionary values) } } - public Dictionary Sensitivities { get; } = []; + public Dictionary Sensitivities { get; } = new(StringComparer.OrdinalIgnoreCase); } } From 9db22b889aeb4fc140ac7b7b45158cfb9338112b Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Fri, 18 Sep 2026 10:37:51 -0400 Subject: [PATCH 36/46] fix: null reference exception Signed-off-by: Vincent Biret --- .../src/Microsoft.Agents.AI.Declarative/PromptAgentFactory.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative/PromptAgentFactory.cs b/dotnet/src/Microsoft.Agents.AI.Declarative/PromptAgentFactory.cs index 164cff237ed..58e3bef2089 100644 --- a/dotnet/src/Microsoft.Agents.AI.Declarative/PromptAgentFactory.cs +++ b/dotnet/src/Microsoft.Agents.AI.Declarative/PromptAgentFactory.cs @@ -39,7 +39,9 @@ protected PromptAgentFactory(RecalcEngine? engine = null, { this.Engine = engine ?? new RecalcEngine(CreateConfig(maximumExpressionLength, maximumCallDepth)); this._configuration = configuration; - this._allowedConfigurationVariables = new(allowedConfigurationVariables ?? configuration?.AsEnumerable().Select(static pair => pair.Key), StringComparer.OrdinalIgnoreCase); + this._allowedConfigurationVariables = new( + allowedConfigurationVariables ?? configuration?.AsEnumerable().Select(static pair => pair.Key) ?? [], + StringComparer.OrdinalIgnoreCase); } private static PowerFxConfig CreateConfig(int? maximumExpressionLength, int? maximumCallDepth) From 5cbd19e4ce2e3d210aebf4b890367dc50d340d6e Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Fri, 18 Sep 2026 10:49:27 -0400 Subject: [PATCH 37/46] chore: formatting Signed-off-by: Vincent Biret --- .../PowerFx/WorkflowFormulaState.cs | 1 + 1 file changed, 1 insertion(+) 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 3128f208386..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; From 1b6b6f8bd30c47103bb48721097aee5bd9a36da1 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Fri, 18 Sep 2026 10:49:45 -0400 Subject: [PATCH 38/46] tests: adds missing test to cover the fallback scenario Signed-off-by: Vincent Biret --- .../DeclarativeWorkflowTest.cs | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) 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 e523a88f4aa..1f702d52858 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/DeclarativeWorkflowTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/DeclarativeWorkflowTest.cs @@ -221,6 +221,103 @@ public async Task Build_OnlyInitializesAllowedReferencedEnvironmentVariablesAsyn } } + [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() { From ec87bddcefcad3930f23443ab698ca85ea3d555f Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Fri, 18 Sep 2026 12:05:52 -0400 Subject: [PATCH 39/46] fix: removes back compat allow list Signed-off-by: Vincent Biret --- .../src/Microsoft.Agents.AI.Declarative/PromptAgentFactory.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative/PromptAgentFactory.cs b/dotnet/src/Microsoft.Agents.AI.Declarative/PromptAgentFactory.cs index 58e3bef2089..4c45b11a29f 100644 --- a/dotnet/src/Microsoft.Agents.AI.Declarative/PromptAgentFactory.cs +++ b/dotnet/src/Microsoft.Agents.AI.Declarative/PromptAgentFactory.cs @@ -40,7 +40,7 @@ protected PromptAgentFactory(RecalcEngine? engine = null, this.Engine = engine ?? new RecalcEngine(CreateConfig(maximumExpressionLength, maximumCallDepth)); this._configuration = configuration; this._allowedConfigurationVariables = new( - allowedConfigurationVariables ?? configuration?.AsEnumerable().Select(static pair => pair.Key) ?? [], + allowedConfigurationVariables ?? [], StringComparer.OrdinalIgnoreCase); } @@ -76,7 +76,7 @@ protected void InitializeConfigurationVariables(GptComponentMetadata promptAgent return; } - foreach (string variableName in AgentBotElementYaml.GetReferencedEnvironmentVariableNames(promptAgent).Where(variableName => this._allowedConfigurationVariables.Contains(variableName))) + foreach (string variableName in AgentBotElementYaml.GetReferencedEnvironmentVariableNames(promptAgent).Where(this._allowedConfigurationVariables.Contains)) { this.Engine.UpdateVariable(variableName, this._configuration[variableName] ?? string.Empty); } From 602d8bba1d40ba09f38e934c8087f7fb221189ea Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Fri, 18 Sep 2026 12:43:19 -0400 Subject: [PATCH 40/46] tests: updates assertions to account for non back compat behaviour Signed-off-by: Vincent Biret --- .../ChatClient/ChatClientAgentFactoryTests.cs | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) 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 471cb84eb56..d6159980183 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Declarative.UnitTests/ChatClient/ChatClientAgentFactoryTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Declarative.UnitTests/ChatClient/ChatClientAgentFactoryTests.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -153,7 +154,7 @@ public async Task TryCreateAsync_WithOptions_LoadsAllowedConfigurationAsync() } [Fact] - public async Task TryCreateAsync_WithLegacyConfiguration_LoadsReferencedConfigurationAsync() + public async Task TryCreateAsync_WithLegacyConfiguration_ThrowsAsync() { // Arrange IConfiguration configuration = new ConfigurationBuilder() @@ -169,16 +170,14 @@ public async Task TryCreateAsync_WithLegacyConfiguration_LoadsReferencedConfigur ChatClientPromptAgentFactory factory = new(this._mockChatClient.Object, configuration: configuration); // Act - AIAgent? agent = await factory.TryCreateAsync(promptAgent); + var exception = await Assert.ThrowsAsync(async () => await factory.TryCreateAsync(promptAgent)); // Assert - ChatClientAgent chatClientAgent = Assert.IsType(agent); - Assert.Equal(0.9F, chatClientAgent.ChatOptions?.Temperature); - Assert.Equal(0.8F, chatClientAgent.ChatOptions?.TopP); + Assert.Contains("Name isn't valid. 'Temperature' isn't recognized.", exception.Message); } [Fact] - public async Task ProtectedConstructor_WithLegacyConfiguration_LoadsReferencedConfigurationAsync() + public async Task ProtectedConstructor_WithLegacyConfiguration_ThrowsAsync() { // Arrange IConfiguration configuration = new ConfigurationBuilder() @@ -194,16 +193,14 @@ public async Task ProtectedConstructor_WithLegacyConfiguration_LoadsReferencedCo // Act await factory.TryCreateAsync(promptAgent); + var exception = Assert.Throws(() => factory.Evaluate("Temperature")); // Assert - StringValue temperature = Assert.IsType(factory.Evaluate("Temperature")); - Assert.Equal("0.9", temperature.Value); - StringValue topP = Assert.IsType(factory.Evaluate("TopP")); - Assert.Equal("0.8", topP.Value); + Assert.Contains("Name isn't valid. 'Temperature' isn't recognized.", exception.Message); } [Fact] - public async Task CreateAsync_WithLegacyConfiguration_InitializesVariablesBeforeTryCreateAsync() + public async Task CreateAsync_WithLegacyConfiguration_ThrowsCreateAsync() { // Arrange IConfiguration configuration = new ConfigurationBuilder() @@ -216,11 +213,10 @@ public async Task CreateAsync_WithLegacyConfiguration_InitializesVariablesBefore CreateAsyncInspectingPromptAgentFactory factory = new(configuration, this._mockChatClient.Object); // Act - AIAgent agent = await factory.CreateAsync(promptAgent); + var exception = await Assert.ThrowsAsync(async () => await factory.CreateAsync(promptAgent)); // Assert - Assert.NotNull(agent); - Assert.Equal("0.9", factory.TemperatureValue); + Assert.Contains("Name isn't valid. 'Temperature' isn't recognized.", exception.Message); } [Fact] From 4152896ca397c2301ec2a46b386820035b785fc3 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Fri, 18 Sep 2026 12:54:10 -0400 Subject: [PATCH 41/46] tests: inspect the inner exception to avoid debounce differences between net and netfx Signed-off-by: Vincent Biret --- .../ChatClient/ChatClientAgentFactoryTests.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) 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 d6159980183..24190e130ac 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Declarative.UnitTests/ChatClient/ChatClientAgentFactoryTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Declarative.UnitTests/ChatClient/ChatClientAgentFactoryTests.cs @@ -173,7 +173,8 @@ public async Task TryCreateAsync_WithLegacyConfiguration_ThrowsAsync() var exception = await Assert.ThrowsAsync(async () => await factory.TryCreateAsync(promptAgent)); // Assert - Assert.Contains("Name isn't valid. 'Temperature' isn't recognized.", exception.Message); + var innerException = Assert.IsType(exception.InnerException); + Assert.Contains("Name isn't valid. 'Temperature' isn't recognized.", innerException.Message); } [Fact] @@ -196,7 +197,8 @@ public async Task ProtectedConstructor_WithLegacyConfiguration_ThrowsAsync() var exception = Assert.Throws(() => factory.Evaluate("Temperature")); // Assert - Assert.Contains("Name isn't valid. 'Temperature' isn't recognized.", exception.Message); + var innerException = Assert.IsType(exception.InnerException); + Assert.Contains("Name isn't valid. 'Temperature' isn't recognized.", innerException.Message); } [Fact] @@ -216,7 +218,8 @@ public async Task CreateAsync_WithLegacyConfiguration_ThrowsCreateAsync() var exception = await Assert.ThrowsAsync(async () => await factory.CreateAsync(promptAgent)); // Assert - Assert.Contains("Name isn't valid. 'Temperature' isn't recognized.", exception.Message); + var innerException = Assert.IsType(exception.InnerException); + Assert.Contains("Name isn't valid. 'Temperature' isn't recognized.", innerException.Message); } [Fact] From c607b3cbc0fa4cfe2ca2f16d2dcf0a088494f33a Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Fri, 18 Sep 2026 13:01:36 -0400 Subject: [PATCH 42/46] chore: makes comparison case sensitive --- .../Kit/RootExecutor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Kit/RootExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Kit/RootExecutor.cs index d5c7273a3f5..3a18a06c948 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Kit/RootExecutor.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Kit/RootExecutor.cs @@ -47,7 +47,7 @@ protected RootExecutor(string id, DeclarativeWorkflowOptions options, Func Date: Fri, 18 Sep 2026 13:01:49 -0400 Subject: [PATCH 43/46] chore: makes comparison case sensitive --- .../PowerFx/WorkflowDiagnostics.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 39dfc7efa54..c46016317f3 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PowerFx/WorkflowDiagnostics.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PowerFx/WorkflowDiagnostics.cs @@ -58,7 +58,7 @@ private static void InitializeEnvironment( IEnumerable? allowedEnvironmentVariables, bool allowProcessEnvironmentVariableFallback) { - HashSet allowedVariables = new(allowedEnvironmentVariables ?? [], StringComparer.OrdinalIgnoreCase); + HashSet allowedVariables = new(allowedEnvironmentVariables ?? [], StringComparer.Ordinal); foreach (string variableName in semanticModel.GetAllEnvironmentVariablesReferencedInTheBot()) { if (!allowedVariables.Contains(variableName)) From 6fdf451fcedf6256e5ee3919d275d194605181e9 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Fri, 18 Sep 2026 13:39:16 -0400 Subject: [PATCH 44/46] fix: avoid calling sync over async to potentially cause deadlocks Signed-off-by: Vincent Biret --- .../ChatClientPromptAgentFactory.cs | 6 ++-- .../Extensions/BoolExpressionExtensions.cs | 9 ++++-- .../Extensions/IntExpressionExtensions.cs | 9 ++++-- .../Extensions/NumberExpressionExtensions.cs | 9 ++++-- .../Extensions/PromptAgentExtensions.cs | 32 ++++++++++++++----- .../Extensions/StringExpressionExtensions.cs | 9 ++++-- .../AgentBotElementYamlTests.cs | 20 ++++++------ .../ChatClient/ChatClientAgentFactoryTests.cs | 27 +++++++--------- 8 files changed, 74 insertions(+), 47 deletions(-) diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative/ChatClient/ChatClientPromptAgentFactory.cs b/dotnet/src/Microsoft.Agents.AI.Declarative/ChatClient/ChatClientPromptAgentFactory.cs index 4d558003f65..1c3d6ab88d3 100644 --- a/dotnet/src/Microsoft.Agents.AI.Declarative/ChatClient/ChatClientPromptAgentFactory.cs +++ b/dotnet/src/Microsoft.Agents.AI.Declarative/ChatClient/ChatClientPromptAgentFactory.cs @@ -42,7 +42,7 @@ public ChatClientPromptAgentFactory( } /// - public override Task TryCreateAsync(GptComponentMetadata promptAgent, CancellationToken cancellationToken = default) + public override async Task TryCreateAsync(GptComponentMetadata promptAgent, CancellationToken cancellationToken = default) { Throw.IfNull(promptAgent); @@ -52,13 +52,13 @@ public ChatClientPromptAgentFactory( { 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 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..fab9c4371de 100644 --- a/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/StringExpressionExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/StringExpressionExtensions.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 @@ public static class StringExpressionExtensions /// /// 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 string? Eval(this StringExpression? expression, RecalcEngine? engine) + public static async Task EvalAsync(this StringExpression? expression, RecalcEngine? engine, CancellationToken cancellationToken = default) { if (expression is null) { @@ -35,11 +38,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/tests/Microsoft.Agents.AI.Declarative.UnitTests/AgentBotElementYamlTests.cs b/dotnet/tests/Microsoft.Agents.AI.Declarative.UnitTests/AgentBotElementYamlTests.cs index a0cfdc94acd..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() @@ -245,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)); } /// @@ -273,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) { @@ -289,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) { @@ -308,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 24190e130ac..ae6a4109438 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Declarative.UnitTests/ChatClient/ChatClientAgentFactoryTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Declarative.UnitTests/ChatClient/ChatClientAgentFactoryTests.cs @@ -170,11 +170,10 @@ public async Task TryCreateAsync_WithLegacyConfiguration_ThrowsAsync() ChatClientPromptAgentFactory factory = new(this._mockChatClient.Object, configuration: configuration); // Act - var exception = await Assert.ThrowsAsync(async () => await factory.TryCreateAsync(promptAgent)); + var exception = await Assert.ThrowsAsync(async () => await factory.TryCreateAsync(promptAgent)); // Assert - var innerException = Assert.IsType(exception.InnerException); - Assert.Contains("Name isn't valid. 'Temperature' isn't recognized.", innerException.Message); + Assert.Contains("Name isn't valid. 'Temperature' isn't recognized.", exception.Message); } [Fact] @@ -194,11 +193,10 @@ public async Task ProtectedConstructor_WithLegacyConfiguration_ThrowsAsync() // Act await factory.TryCreateAsync(promptAgent); - var exception = Assert.Throws(() => factory.Evaluate("Temperature")); + var exception = await Assert.ThrowsAsync(async () => await factory.EvaluateAsync("Temperature")); // Assert - var innerException = Assert.IsType(exception.InnerException); - Assert.Contains("Name isn't valid. 'Temperature' isn't recognized.", innerException.Message); + Assert.Contains("Name isn't valid. 'Temperature' isn't recognized.", exception.Message); } [Fact] @@ -215,11 +213,10 @@ public async Task CreateAsync_WithLegacyConfiguration_ThrowsCreateAsync() CreateAsyncInspectingPromptAgentFactory factory = new(configuration, this._mockChatClient.Object); // Act - var exception = await Assert.ThrowsAsync(async () => await factory.CreateAsync(promptAgent)); + var exception = await Assert.ThrowsAsync(async () => await factory.CreateAsync(promptAgent)); // Assert - var innerException = Assert.IsType(exception.InnerException); - Assert.Contains("Name isn't valid. 'Temperature' isn't recognized.", innerException.Message); + Assert.Contains("Name isn't valid. 'Temperature' isn't recognized.", exception.Message); } [Fact] @@ -240,7 +237,7 @@ public async Task TryCreateAsync_OnlyLoadsAllowedReferencedConfigurationAsync() await factory.TryCreateAsync(promptAgent); // Assert - StringValue temperature = Assert.IsType(factory.Evaluate("Temperature")); + StringValue temperature = Assert.IsType(await factory.EvaluateAsync("Temperature")); Assert.Equal("0.9", temperature.Value); Assert.False(factory.CanEvaluate("TopP")); } @@ -248,7 +245,7 @@ public async Task TryCreateAsync_OnlyLoadsAllowedReferencedConfigurationAsync() private sealed class InspectingPromptAgentFactory(IConfiguration configuration, IEnumerable allowedConfigurationVariables) : PromptAgentFactory(engine: null, configuration: configuration, allowedConfigurationVariables: allowedConfigurationVariables) { - public FormulaValue Evaluate(string expression) => this.Engine.Eval(expression); + public Task EvaluateAsync(string expression, CancellationToken cancellationToken = default) => this.Engine.EvalAsync(expression, cancellationToken); public bool CanEvaluate(string expression) => this.Engine.Check(expression).IsSuccess; @@ -267,23 +264,23 @@ private sealed class CreateAsyncInspectingPromptAgentFactory(IConfiguration conf { public string? TemperatureValue { get; private set; } - public override Task TryCreateAsync(GptComponentMetadata promptAgent, CancellationToken cancellationToken = default) + public override async Task TryCreateAsync(GptComponentMetadata promptAgent, CancellationToken cancellationToken = default) { // Arrange - StringValue temperature = Assert.IsType(this.Engine.Eval("Temperature")); + StringValue temperature = Assert.IsType(await this.Engine.EvalAsync("Temperature", cancellationToken)); // Act this.TemperatureValue = temperature.Value; // Assert - return Task.FromResult(new ChatClientAgent(chatClient)); + return new ChatClientAgent(chatClient); } } private sealed class LegacyInspectingPromptAgentFactory(IConfiguration configuration) : PromptAgentFactory(engine: null, configuration: configuration) { - public FormulaValue Evaluate(string expression) => this.Engine.Eval(expression); + public Task EvaluateAsync(string expression, CancellationToken cancellationToken = default) => this.Engine.EvalAsync(expression, cancellationToken); public override Task TryCreateAsync(GptComponentMetadata promptAgent, CancellationToken cancellationToken = default) { From 138de81ff8ed7a9e39a425aed8fdbf57a77bcf27 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Fri, 18 Sep 2026 15:01:17 -0400 Subject: [PATCH 45/46] fix: adds compatibility deprecated method Signed-off-by: Vincent Biret --- .../Extensions/StringExpressionExtensions.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/StringExpressionExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/StringExpressionExtensions.cs index fab9c4371de..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,7 @@ // Copyright (c) Microsoft. All rights reserved. +using System; +using System.ComponentModel; using System.Threading; using System.Threading.Tasks; using Microsoft.PowerFx; @@ -12,6 +14,18 @@ namespace Microsoft.Agents.ObjectModel; /// public static class StringExpressionExtensions { + /// + /// Evaluates the given using the provided . + /// + /// 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 . /// From bf1c2763f3ff337e7f271f694a90c23b505b6016 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Fri, 18 Sep 2026 15:05:46 -0400 Subject: [PATCH 46/46] fix: binary compatibility Signed-off-by: Vincent Biret --- .../ChatClientPromptAgentFactory.cs | 24 ++++++++++++++++--- .../PromptAgentFactory.cs | 17 ++++++++++--- 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative/ChatClient/ChatClientPromptAgentFactory.cs b/dotnet/src/Microsoft.Agents.AI.Declarative/ChatClient/ChatClientPromptAgentFactory.cs index 1c3d6ab88d3..69a598d2d34 100644 --- a/dotnet/src/Microsoft.Agents.AI.Declarative/ChatClient/ChatClientPromptAgentFactory.cs +++ b/dotnet/src/Microsoft.Agents.AI.Declarative/ChatClient/ChatClientPromptAgentFactory.cs @@ -25,14 +25,32 @@ public sealed class ChatClientPromptAgentFactory : PromptAgentFactory /// 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 = null, RecalcEngine? engine = null, IConfiguration? configuration = null, - ILoggerFactory? loggerFactory = null, - IEnumerable? allowedConfigurationVariables = 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); diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative/PromptAgentFactory.cs b/dotnet/src/Microsoft.Agents.AI.Declarative/PromptAgentFactory.cs index 4c45b11a29f..f9ea1f2deda 100644 --- a/dotnet/src/Microsoft.Agents.AI.Declarative/PromptAgentFactory.cs +++ b/dotnet/src/Microsoft.Agents.AI.Declarative/PromptAgentFactory.cs @@ -23,6 +23,17 @@ public abstract class PromptAgentFactory 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 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) + { + // BINARY COMPAT CONSTRUCTOR + } + /// /// Initializes a new instance of the class. /// @@ -31,9 +42,9 @@ public abstract class PromptAgentFactory /// 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 = null, - IConfiguration? configuration = null, - IEnumerable? allowedConfigurationVariables = null, + protected PromptAgentFactory(RecalcEngine? engine, + IConfiguration? configuration, + IEnumerable? allowedConfigurationVariables, int? maximumExpressionLength = null, int? maximumCallDepth = null) {