Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
#nullable enable
Microsoft.Agents.AI.Workflows.Declarative.Mcp.DefaultMcpToolHandler.InvokeToolInWorkflowSessionAsync(string! serverUrl, string? serverLabel, string! toolName, System.Collections.Generic.IDictionary<string!, object?>? arguments, System.Collections.Generic.IDictionary<string!, string!>? headers, string? connectionName, string! workflowSessionId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<Microsoft.Extensions.AI.McpServerToolResultContent!>!
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
#nullable enable
Microsoft.Agents.AI.Workflows.Declarative.Mcp.DefaultMcpToolHandler.InvokeToolInWorkflowSessionAsync(string! serverUrl, string? serverLabel, string! toolName, System.Collections.Generic.IDictionary<string!, object?>? arguments, System.Collections.Generic.IDictionary<string!, string!>? headers, string? connectionName, string! workflowSessionId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<Microsoft.Extensions.AI.McpServerToolResultContent!>!
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
#nullable enable
Microsoft.Agents.AI.Workflows.Declarative.Mcp.DefaultMcpToolHandler.InvokeToolInWorkflowSessionAsync(string! serverUrl, string? serverLabel, string! toolName, System.Collections.Generic.IDictionary<string!, object?>? arguments, System.Collections.Generic.IDictionary<string!, string!>? headers, string? connectionName, string! workflowSessionId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<Microsoft.Extensions.AI.McpServerToolResultContent!>!
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
#nullable enable
Microsoft.Agents.AI.Workflows.Declarative.Mcp.DefaultMcpToolHandler.InvokeToolInWorkflowSessionAsync(string! serverUrl, string? serverLabel, string! toolName, System.Collections.Generic.IDictionary<string!, object?>? arguments, System.Collections.Generic.IDictionary<string!, string!>? headers, string? connectionName, string! workflowSessionId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<Microsoft.Extensions.AI.McpServerToolResultContent!>!
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
#nullable enable
Microsoft.Agents.AI.Workflows.Declarative.Mcp.DefaultMcpToolHandler.InvokeToolInWorkflowSessionAsync(string! serverUrl, string? serverLabel, string! toolName, System.Collections.Generic.IDictionary<string!, object?>? arguments, System.Collections.Generic.IDictionary<string!, string!>? headers, string? connectionName, string! workflowSessionId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<Microsoft.Extensions.AI.McpServerToolResultContent!>!
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,34 @@ Task<McpServerToolResultContent> InvokeToolAsync(
string? connectionName,
CancellationToken cancellationToken = default);
}

/// <summary>
/// Defines the contract for MCP handlers that isolate stateful protocol sessions by workflow session.
/// </summary>
public interface IWorkflowScopedMcpToolHandler : IMcpToolHandler
{
/// <summary>
/// Invokes an MCP tool within the framework-owned workflow session scope.
/// </summary>
/// <param name="serverUrl">The URL of the MCP server.</param>
/// <param name="serverLabel">An optional label identifying the server connection.</param>
/// <param name="toolName">The name of the tool to invoke.</param>
/// <param name="arguments">Optional arguments to pass to the tool.</param>
/// <param name="headers">Optional headers to include in the request.</param>
/// <param name="connectionName">An optional connection name for managed connections.</param>
/// <param name="workflowSessionId">The framework-owned identifier for the current workflow session.</param>
/// <param name="cancellationToken">A token to observe cancellation.</param>
/// <returns>
/// A task representing the asynchronous operation. The result contains a <see cref="McpServerToolResultContent"/>
/// with the tool invocation output.
/// </returns>
Task<McpServerToolResultContent> InvokeToolInWorkflowSessionAsync(
string serverUrl,
string? serverLabel,
string toolName,
IDictionary<string, object?>? arguments,
IDictionary<string, string>? headers,
string? connectionName,
string workflowSessionId,
CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,9 @@ public override async ValueTask HandleAsync(ActionExecutorResult message, IWorkf

try
{
object? result = await this.ExecuteAsync(new DeclarativeWorkflowContext(context, this.State), cancellationToken).ConfigureAwait(false);
DeclarativeWorkflowContext declarativeContext =
await DeclarativeWorkflowContext.CreateAsync(context, this.State, cancellationToken).ConfigureAwait(false);
object? result = await this.ExecuteAsync(declarativeContext, cancellationToken).ConfigureAwait(false);
Debug.WriteLine($"RESULT #{this.Id} - {result ?? "(null)"}");

if (this.EmitResultEvent)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,25 +15,47 @@

namespace Microsoft.Agents.AI.Workflows.Declarative.Interpreter;

internal sealed class DeclarativeWorkflowContext : IWorkflowContext
internal sealed class DeclarativeWorkflowContext : IWorkflowContext, IWorkflowSessionContext
{
internal const string WorkflowSessionIdStateKey = "__declarative_mcp_workflow_session_id";

public static readonly FrozenSet<string> ManagedScopes =
[
VariableScopeNames.Local,
VariableScopeNames.Topic,
VariableScopeNames.Global,
];

public DeclarativeWorkflowContext(IWorkflowContext source, WorkflowFormulaState state)
private DeclarativeWorkflowContext(IWorkflowContext source, WorkflowFormulaState state, string sessionId)
{
this.Source = source;
this.State = state;
this.SessionId = sessionId;
}

public static async ValueTask<DeclarativeWorkflowContext> CreateAsync(
IWorkflowContext source,
WorkflowFormulaState state,
CancellationToken cancellationToken = default)
{
string generatedSessionId = Guid.NewGuid().ToString("N");
string sessionId = source is IWorkflowSessionContext sessionContext
? sessionContext.SessionId
: await source.ReadOrInitStateAsync(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For a legacy/custom IWorkflowContext that does not implement IWorkflowSessionContext, multiple declarative executors reaching this branch in one superstep each generate a different value. The IWorkflowContext contract permits initialization to be queued and warns that multiple executor initializations conflict, so this can split one workflow across MCP sessions or fail when state is published. Please make the workflow session ID an intrinsic shared runtime value, or require and propagate IWorkflowSessionContext, rather than deriving it through per-executor queued initialization.

WorkflowSessionIdStateKey,
() => generatedSessionId,
VariableScopeNames.System,
cancellationToken: cancellationToken).ConfigureAwait(false);

return new(source, state, sessionId);
}

private IWorkflowContext Source { get; }
public WorkflowFormulaState State { get; }
public IReadOnlyDictionary<string, string>? TraceContext => this.Source.TraceContext;

public string SessionId { get; }

/// <inheritdoc/>
public bool ConcurrentRunsEnabled => this.Source.ConcurrentRunsEnabled;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,8 @@ private async ValueTask AdvanceAsync(ChatMessage input, IWorkflowContext context
// No state to restore if we're starting from the beginning.
state.SetInitialized();

DeclarativeWorkflowContext declarativeContext = new(context, state);
DeclarativeWorkflowContext declarativeContext =
await DeclarativeWorkflowContext.CreateAsync(context, state, cancellationToken).ConfigureAwait(false);

// Conversation id resolution prefers state already persisted by a prior turn,
// so multi-turn invocations reuse the same backend conversation rather than
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,9 @@ public override async ValueTask HandleAsync(TMessage message, IWorkflowContext c
{
if (this._action is not null)
{
await this._action.Invoke(new DeclarativeWorkflowContext(context, this._state), message, cancellationToken).ConfigureAwait(false);
DeclarativeWorkflowContext declarativeContext =
await DeclarativeWorkflowContext.CreateAsync(context, this._state, cancellationToken).ConfigureAwait(false);
await this._action.Invoke(declarativeContext, message, cancellationToken).ConfigureAwait(false);
}

if (this._emitResult)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,9 @@ public ValueTask ResetAsync()
[SendsMessage(typeof(ActionExecutorResult))]
public override async ValueTask HandleAsync(TMessage message, IWorkflowContext context, CancellationToken cancellationToken)
{
object? result = await this.ExecuteAsync(new DeclarativeWorkflowContext(context, this._session.State), message, cancellationToken).ConfigureAwait(false);
DeclarativeWorkflowContext declarativeContext =
await DeclarativeWorkflowContext.CreateAsync(context, this._session.State, cancellationToken).ConfigureAwait(false);
object? result = await this.ExecuteAsync(declarativeContext, message, cancellationToken).ConfigureAwait(false);
Debug.WriteLine($"RESULT #{this.Id} - {result ?? "(null)"}");

await context.SendResultMessageAsync(this.Id, result, cancellationToken).ConfigureAwait(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ public ValueTask ResetAsync()
[SendsMessage(typeof(ActionExecutorResult))]
public override async ValueTask HandleAsync(TInput message, IWorkflowContext context, CancellationToken cancellationToken)
{
DeclarativeWorkflowContext declarativeContext = new(context, this._state);
DeclarativeWorkflowContext declarativeContext =
await DeclarativeWorkflowContext.CreateAsync(context, this._state, cancellationToken).ConfigureAwait(false);

ChatMessage input = (this._inputTransform ?? DefaultInputTransform).Invoke(message);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ internal sealed class InvokeMcpToolExecutor(
{
private const string ApprovalSnapshotStateKey = nameof(_approvalSnapshots);
private const string LegacyApprovalSnapshotStateKey = "_approvalSnapshot";

/// <summary>
/// Snapshots of evaluated parameters captured at approval-request time, keyed by
/// per-invocation request id. Each pending approval lives here until the matching
Expand Down Expand Up @@ -110,7 +109,8 @@ await this.RequestApprovalAsync(
}

// No approval required - invoke the tool directly
McpServerToolResultContent resultContent = await mcpToolHandler.InvokeToolAsync(
McpServerToolResultContent resultContent = await this.InvokeToolAsync(
context,
serverUrl,
serverLabel,
toolName,
Expand Down Expand Up @@ -193,7 +193,8 @@ public async ValueTask CaptureResponseAsync(

string? conversationId = this.GetConversationId();
bool autoSend = snapshot.AutoSend ?? this.GetAutoSendValue();
McpServerToolResultContent resultContent = await mcpToolHandler.InvokeToolAsync(
McpServerToolResultContent resultContent = await this.InvokeToolAsync(
context,
snapshot.ServerUrl,
snapshot.ServerLabel,
snapshot.ToolName,
Expand All @@ -206,6 +207,47 @@ public async ValueTask CaptureResponseAsync(
await context.SendResultMessageAsync(this.Id, result: null, cancellationToken).ConfigureAwait(false);
}

private async Task<McpServerToolResultContent> InvokeToolAsync(
IWorkflowContext context,
string serverUrl,
string? serverLabel,
string toolName,
IDictionary<string, object?>? arguments,
IDictionary<string, string>? headers,
string? connectionName,
CancellationToken cancellationToken)
{
if (mcpToolHandler is IWorkflowScopedMcpToolHandler scopedHandler)
{
string generatedWorkflowSessionId = Guid.NewGuid().ToString("N");
string workflowSessionId = context is IWorkflowSessionContext sessionContext
? sessionContext.SessionId
: await context.ReadOrInitStateAsync(
DeclarativeWorkflowContext.WorkflowSessionIdStateKey,
() => generatedWorkflowSessionId,
VariableScopeNames.System,
cancellationToken).ConfigureAwait(false);
return await scopedHandler.InvokeToolInWorkflowSessionAsync(
serverUrl,
serverLabel,
toolName,
arguments,
headers,
connectionName,
workflowSessionId,
cancellationToken).ConfigureAwait(false);
}

return await mcpToolHandler.InvokeToolAsync(
serverUrl,
serverLabel,
toolName,
arguments,
headers,
connectionName,
cancellationToken).ConfigureAwait(false);
}

/// <summary>
/// Completes the MCP tool invocation by raising the completion event.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.AllowedEnvi
Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.AllowProcessEnvironmentVariableFallback.get -> bool
Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.AllowProcessEnvironmentVariableFallback.init -> void
Microsoft.Agents.AI.Workflows.Declarative.Events.ExternalInputResponse.RequestId.get -> string?
Microsoft.Agents.AI.Workflows.Declarative.IWorkflowScopedMcpToolHandler
Microsoft.Agents.AI.Workflows.Declarative.IWorkflowScopedMcpToolHandler.InvokeToolInWorkflowSessionAsync(string! serverUrl, string? serverLabel, string! toolName, System.Collections.Generic.IDictionary<string!, object?>? arguments, System.Collections.Generic.IDictionary<string!, string!>? headers, string? connectionName, string! workflowSessionId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<Microsoft.Extensions.AI.McpServerToolResultContent!>!
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<Microsoft.Agents.ObjectModel.Abstractions.EvaluationResult<object?>!>
static Microsoft.Agents.AI.Workflows.Declarative.Kit.IWorkflowContextExtensions.EvaluateValueWithSensitivityAsync<TValue>(this Microsoft.Agents.AI.Workflows.IWorkflowContext! context, string! expression, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask<Microsoft.Agents.ObjectModel.Abstractions.EvaluationResult<TValue?>!>
static Microsoft.Agents.AI.Workflows.Declarative.Kit.IWorkflowContextExtensions.FormatTemplateWithSensitivityAsync(this Microsoft.Agents.AI.Workflows.IWorkflowContext! context, string! line) -> System.Threading.Tasks.ValueTask<Microsoft.Agents.ObjectModel.Abstractions.EvaluationResult<string!>!>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.AllowedEnvi
Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.AllowProcessEnvironmentVariableFallback.get -> bool
Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.AllowProcessEnvironmentVariableFallback.init -> void
Microsoft.Agents.AI.Workflows.Declarative.Events.ExternalInputResponse.RequestId.get -> string?
Microsoft.Agents.AI.Workflows.Declarative.IWorkflowScopedMcpToolHandler
Microsoft.Agents.AI.Workflows.Declarative.IWorkflowScopedMcpToolHandler.InvokeToolInWorkflowSessionAsync(string! serverUrl, string? serverLabel, string! toolName, System.Collections.Generic.IDictionary<string!, object?>? arguments, System.Collections.Generic.IDictionary<string!, string!>? headers, string? connectionName, string! workflowSessionId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<Microsoft.Extensions.AI.McpServerToolResultContent!>!
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<Microsoft.Agents.ObjectModel.Abstractions.EvaluationResult<object?>!>
static Microsoft.Agents.AI.Workflows.Declarative.Kit.IWorkflowContextExtensions.EvaluateValueWithSensitivityAsync<TValue>(this Microsoft.Agents.AI.Workflows.IWorkflowContext! context, string! expression, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask<Microsoft.Agents.ObjectModel.Abstractions.EvaluationResult<TValue?>!>
static Microsoft.Agents.AI.Workflows.Declarative.Kit.IWorkflowContextExtensions.FormatTemplateWithSensitivityAsync(this Microsoft.Agents.AI.Workflows.IWorkflowContext! context, string! line) -> System.Threading.Tasks.ValueTask<Microsoft.Agents.ObjectModel.Abstractions.EvaluationResult<string!>!>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.AllowedEnvi
Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.AllowProcessEnvironmentVariableFallback.get -> bool
Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.AllowProcessEnvironmentVariableFallback.init -> void
Microsoft.Agents.AI.Workflows.Declarative.Events.ExternalInputResponse.RequestId.get -> string?
Microsoft.Agents.AI.Workflows.Declarative.IWorkflowScopedMcpToolHandler
Microsoft.Agents.AI.Workflows.Declarative.IWorkflowScopedMcpToolHandler.InvokeToolInWorkflowSessionAsync(string! serverUrl, string? serverLabel, string! toolName, System.Collections.Generic.IDictionary<string!, object?>? arguments, System.Collections.Generic.IDictionary<string!, string!>? headers, string? connectionName, string! workflowSessionId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<Microsoft.Extensions.AI.McpServerToolResultContent!>!
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<Microsoft.Agents.ObjectModel.Abstractions.EvaluationResult<object?>!>
static Microsoft.Agents.AI.Workflows.Declarative.Kit.IWorkflowContextExtensions.EvaluateValueWithSensitivityAsync<TValue>(this Microsoft.Agents.AI.Workflows.IWorkflowContext! context, string! expression, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask<Microsoft.Agents.ObjectModel.Abstractions.EvaluationResult<TValue?>!>
static Microsoft.Agents.AI.Workflows.Declarative.Kit.IWorkflowContextExtensions.FormatTemplateWithSensitivityAsync(this Microsoft.Agents.AI.Workflows.IWorkflowContext! context, string! line) -> System.Threading.Tasks.ValueTask<Microsoft.Agents.ObjectModel.Abstractions.EvaluationResult<string!>!>
Expand Down
Loading
Loading